/*
 * ReactOS Explorer
 *
 * Copyright 2006 - 2007 Thomas Weidenmueller <w3seek@reactos.org>
 * Copyright 2018 Ged Murphy <gedmurphy@reactos.org>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include "precomp.h"

struct InternalIconData : NOTIFYICONDATA
{
    // Must keep a separate copy since the original is unioned with uTimeout.
    UINT uVersionCopy;
};

struct IconWatcherData
{
    HANDLE hProcess;
    DWORD ProcessId;
    NOTIFYICONDATA IconData;

    IconWatcherData(CONST NOTIFYICONDATA *iconData) :
        hProcess(NULL), ProcessId(0)
    {
        IconData.cbSize = sizeof(NOTIFYICONDATA);
        IconData.hWnd = iconData->hWnd;
        IconData.uID = iconData->uID;
        IconData.guidItem = iconData->guidItem;
    }

    ~IconWatcherData()
    {
        if (hProcess)
        {
            CloseHandle(hProcess);
        }
    }
};

class CIconWatcher
{
    CAtlList<IconWatcherData *> m_WatcherList;
    CRITICAL_SECTION m_ListLock;
    HANDLE m_hWatcherThread;
    HANDLE m_WakeUpEvent;
    HWND m_hwndSysTray;
    bool m_Loop;

public:
    CIconWatcher();

    virtual ~CIconWatcher();

    bool Initialize(_In_ HWND hWndParent);
    void Uninitialize();

    bool AddIconToWatcher(_In_ CONST NOTIFYICONDATA *iconData);
    bool RemoveIconFromWatcher(_In_ CONST NOTIFYICONDATA *iconData);

    IconWatcherData* GetListEntry(_In_opt_ CONST NOTIFYICONDATA *iconData, _In_opt_ HANDLE hProcess, _In_ bool Remove);

private:

    static UINT WINAPI WatcherThread(_In_opt_ LPVOID lpParam);
};

class CNotifyToolbar;

class CBalloonQueue
{
public:
    static const int TimerInterval = 2000;
    static const int BalloonsTimerId = 1;
    static const int MinTimeout = 10000;
    static const int MaxTimeout = 30000;
    static const int CooldownBetweenBalloons = 2000;

private:
    struct Info
    {
        InternalIconData * pSource;
        WCHAR szInfo[256];
        WCHAR szInfoTitle[64];
        WPARAM uIcon;
        UINT uTimeout;

        Info(InternalIconData * source)
        {
            pSource = source;
            StringCchCopy(szInfo, _countof(szInfo), source->szInfo);
            StringCchCopy(szInfoTitle, _countof(szInfoTitle), source->szInfoTitle);
            uIcon = source->dwInfoFlags & NIIF_ICON_MASK;
            if (source->dwInfoFlags == NIIF_USER)
                uIcon = reinterpret_cast<WPARAM>(source->hIcon);
            uTimeout = source->uTimeout;
        }
    };

    HWND m_hwndParent;

    CTooltips * m_tooltips;

    CAtlList<Info> m_queue;

    CNotifyToolbar * m_toolbar;

    InternalIconData * m_current;
    bool m_currentClosed;

    int m_timer;

public:
    CBalloonQueue();

    void Init(HWND hwndParent, CNotifyToolbar * toolbar, CTooltips * balloons);
    void Deinit();

    bool OnTimer(int timerId);
    void UpdateInfo(InternalIconData * notifyItem);
    void RemoveInfo(InternalIconData * notifyItem);
    void CloseCurrent();

private:

    int IndexOf(InternalIconData * pdata);
    void SetTimer(int length);
    void Show(Info& info);
    void Close(IN OUT InternalIconData * notifyItem, IN UINT uReason);
};

class CNotifyToolbar :
    public CWindowImplBaseT< CToolbar<InternalIconData>, CControlWinTraits >
{
    HIMAGELIST m_ImageList;
    int m_VisibleButtonCount;

    CBalloonQueue * m_BalloonQueue;

public:
    CNotifyToolbar();
    virtual ~CNotifyToolbar();

    int GetVisibleButtonCount();
    int FindItem(IN HWND hWnd, IN UINT uID, InternalIconData ** pdata);
    int FindExistingSharedIcon(HICON handle);
    BOOL AddButton(IN CONST NOTIFYICONDATA *iconData);
    BOOL SwitchVersion(IN CONST NOTIFYICONDATA *iconData);
    BOOL UpdateButton(IN CONST NOTIFYICONDATA *iconData);
    BOOL RemoveButton(IN CONST NOTIFYICONDATA *iconData);
    VOID ResizeImagelist();
    bool SendNotifyCallback(InternalIconData* notifyItem, UINT uMsg);

private:
    LRESULT OnCtxMenu(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
    VOID SendMouseEvent(IN WORD wIndex, IN UINT uMsg, IN WPARAM wParam);
    LRESULT OnMouseEvent(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
    LRESULT OnTooltipShow(INT uCode, LPNMHDR hdr, BOOL& bHandled);

public:
    BEGIN_MSG_MAP(CNotifyToolbar)
        MESSAGE_HANDLER(WM_CONTEXTMENU, OnCtxMenu)
        MESSAGE_RANGE_HANDLER(WM_MOUSEFIRST, WM_MOUSELAST, OnMouseEvent)
        NOTIFY_CODE_HANDLER(TTN_SHOW, OnTooltipShow)
    END_MSG_MAP()

    void Initialize(HWND hWndParent, CBalloonQueue * queue);
};

extern const WCHAR szSysPagerWndClass[];

class CSysPagerWnd :
    public CComCoClass<CSysPagerWnd>,
    public CComObjectRootEx<CComMultiThreadModelNoCS>,
    public CWindowImpl < CSysPagerWnd, CWindow, CControlWinTraits >,
    public IOleWindow,
    public CIconWatcher
{
    CNotifyToolbar Toolbar;
    CTooltips m_Balloons;
    CBalloonQueue m_BalloonQueue;

public:
    CSysPagerWnd();
    virtual ~CSysPagerWnd();

    LRESULT DrawBackground(HDC hdc);
    LRESULT OnEraseBackground(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
    LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
    LRESULT OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
    LRESULT OnGetInfoTip(INT uCode, LPNMHDR hdr, BOOL& bHandled);
    LRESULT OnCustomDraw(INT uCode, LPNMHDR hdr, BOOL& bHandled);
    LRESULT OnCommand(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
    LRESULT OnSize(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
    LRESULT OnCtxMenu(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
    LRESULT OnBalloonPop(UINT uCode, LPNMHDR hdr, BOOL& bHandled);
    LRESULT OnTimer(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
    LRESULT OnCopyData(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
    LRESULT OnSettingChanged(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
    LRESULT OnGetMinimumSize(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);

public:

    HRESULT WINAPI GetWindow(HWND* phwnd)
    {
        if (!phwnd)
            return E_INVALIDARG;
        *phwnd = m_hWnd;
        return S_OK;
    }

    HRESULT WINAPI ContextSensitiveHelp(BOOL fEnterMode)
    {
        return E_NOTIMPL;
    }

    DECLARE_NOT_AGGREGATABLE(CSysPagerWnd)

    DECLARE_PROTECT_FINAL_CONSTRUCT()
    BEGIN_COM_MAP(CSysPagerWnd)
        COM_INTERFACE_ENTRY_IID(IID_IOleWindow, IOleWindow)
    END_COM_MAP()

    BOOL NotifyIcon(DWORD dwMessage, _In_ CONST NOTIFYICONDATA *iconData);
    void GetSize(IN BOOL IsHorizontal, IN PSIZE size);

    DECLARE_WND_CLASS_EX(szSysPagerWndClass, CS_DBLCLKS, COLOR_3DFACE)

    BEGIN_MSG_MAP(CSysPagerWnd)
        MESSAGE_HANDLER(WM_CREATE, OnCreate)
        MESSAGE_HANDLER(WM_DESTROY, OnDestroy)
        MESSAGE_HANDLER(WM_ERASEBKGND, OnEraseBackground)
        MESSAGE_HANDLER(WM_COMMAND, OnCommand)
        MESSAGE_HANDLER(WM_SIZE, OnSize)
        MESSAGE_HANDLER(WM_CONTEXTMENU, OnCtxMenu)
        MESSAGE_HANDLER(WM_TIMER, OnTimer)
        MESSAGE_HANDLER(WM_COPYDATA, OnCopyData)
        MESSAGE_HANDLER(WM_SETTINGCHANGE, OnSettingChanged)
        MESSAGE_HANDLER(TNWM_GETMINIMUMSIZE, OnGetMinimumSize)
        NOTIFY_CODE_HANDLER(TTN_POP, OnBalloonPop)
        NOTIFY_CODE_HANDLER(TBN_GETINFOTIPW, OnGetInfoTip)
        NOTIFY_CODE_HANDLER(NM_CUSTOMDRAW, OnCustomDraw)
    END_MSG_MAP()

    HRESULT Initialize(IN HWND hWndParent);
};

CIconWatcher::CIconWatcher() :
    m_hWatcherThread(NULL),
    m_WakeUpEvent(NULL),
    m_hwndSysTray(NULL),
    m_Loop(false)
{
}

CIconWatcher::~CIconWatcher()
{
    Uninitialize();
    DeleteCriticalSection(&m_ListLock);

    if (m_WakeUpEvent)
        CloseHandle(m_WakeUpEvent);
    if (m_hWatcherThread)
        CloseHandle(m_hWatcherThread);
}

bool CIconWatcher::Initialize(_In_ HWND hWndParent)
{
    m_hwndSysTray = hWndParent;

    InitializeCriticalSection(&m_ListLock);
    m_WakeUpEvent = CreateEventW(NULL, FALSE, FALSE, NULL);
    if (m_WakeUpEvent == NULL)
        return false;

    m_hWatcherThread = (HANDLE)_beginthreadex(NULL,
                                                0,
                                                WatcherThread,
                                                (LPVOID)this,
                                                0,
                                                NULL);
    if (m_hWatcherThread == NULL)
        return false;

    return true;
}

void CIconWatcher::Uninitialize()
{
    m_Loop = false;
    if (m_WakeUpEvent)
        SetEvent(m_WakeUpEvent);

    EnterCriticalSection(&m_ListLock);

    POSITION Pos;
    for (size_t i = 0; i < m_WatcherList.GetCount(); i++)
    {
        Pos = m_WatcherList.FindIndex(i);
        if (Pos)
        {
            IconWatcherData *Icon;
            Icon = m_WatcherList.GetAt(Pos);
            delete Icon;
        }
    }
    m_WatcherList.RemoveAll();

    LeaveCriticalSection(&m_ListLock);
}

bool CIconWatcher::AddIconToWatcher(_In_ CONST NOTIFYICONDATA *iconData)
{
    DWORD ProcessId;
    (void)GetWindowThreadProcessId(iconData->hWnd, &ProcessId);

    HANDLE hProcess;
    hProcess = OpenProcess(SYNCHRONIZE, FALSE, ProcessId);
    if (hProcess == NULL)
    {
        return false;
    }

    IconWatcherData *Icon = new IconWatcherData(iconData);
    Icon->hProcess = hProcess;
    Icon->ProcessId = ProcessId;

    bool Added = false;
    EnterCriticalSection(&m_ListLock);

    // The likelyhood of someone having more than 64 icons in their tray is
    // pretty slim. We could spin up a new thread for each multiple of 64, but
    // it's not worth the effort, so we just won't bother watching those icons
    if (m_WatcherList.GetCount() < MAXIMUM_WAIT_OBJECTS)
    {
        m_WatcherList.AddTail(Icon);
        SetEvent(m_WakeUpEvent);
        Added = true;
    }

    LeaveCriticalSection(&m_ListLock);

    if (!Added)
    {
        delete Icon;
    }

    return Added;
}

bool CIconWatcher::RemoveIconFromWatcher(_In_ CONST NOTIFYICONDATA *iconData)
{
    EnterCriticalSection(&m_ListLock);
        
    IconWatcherData *Icon;
    Icon = GetListEntry(iconData, NULL, true);

    SetEvent(m_WakeUpEvent);
    LeaveCriticalSection(&m_ListLock);

    delete Icon;
    return true;
}

IconWatcherData* CIconWatcher::GetListEntry(_In_opt_ CONST NOTIFYICONDATA *iconData, _In_opt_ HANDLE hProcess, _In_ bool Remove)
{
    IconWatcherData *Entry = NULL;
    POSITION NextPosition = m_WatcherList.GetHeadPosition();
    POSITION Position;
    do
    {
        Position = NextPosition;

        Entry = m_WatcherList.GetNext(NextPosition);
        if (Entry)
        {
            if ((iconData && ((Entry->IconData.hWnd == iconData->hWnd) && (Entry->IconData.uID == iconData->uID))) ||
                    (hProcess && (Entry->hProcess == hProcess)))
            {
                if (Remove)
                    m_WatcherList.RemoveAt(Position);
                break;
            }
        }
        Entry = NULL;

    } while (NextPosition != NULL);

    return Entry;
}

UINT WINAPI CIconWatcher::WatcherThread(_In_opt_ LPVOID lpParam)
{
    CIconWatcher* This = reinterpret_cast<CIconWatcher *>(lpParam);
    HANDLE *WatchList = NULL;

    This->m_Loop = true;
    while (This->m_Loop)
    {
        EnterCriticalSection(&This->m_ListLock);

        DWORD Size;
        Size = This->m_WatcherList.GetCount() + 1;
        ASSERT(Size <= MAXIMUM_WAIT_OBJECTS);

        if (WatchList)
            delete[] WatchList;
        WatchList = new HANDLE[Size];
        WatchList[0] = This->m_WakeUpEvent;

        POSITION Pos;
        for (size_t i = 0; i < This->m_WatcherList.GetCount(); i++)
        {
            Pos = This->m_WatcherList.FindIndex(i);
            if (Pos)
            {
                IconWatcherData *Icon;
                Icon = This->m_WatcherList.GetAt(Pos);
                WatchList[i + 1] = Icon->hProcess;
            }
        }

        LeaveCriticalSection(&This->m_ListLock);

        DWORD Status;
        Status = WaitForMultipleObjects(Size,
                                        WatchList,
                                        FALSE,
                                        INFINITE);
        if (Status == WAIT_OBJECT_0)
        {
            // We've been kicked, we have updates to our list (or we're exiting the thread)
            if (This->m_Loop)
                TRACE("Updating watched icon list");
        }
        else if ((Status >= WAIT_OBJECT_0 + 1) && (Status < Size))
        {
            IconWatcherData *Icon;
            Icon = This->GetListEntry(NULL, WatchList[Status], false);

            TRACE("Pid %lu owns a notification icon and has stopped without deleting it. We'll cleanup on its behalf", Icon->ProcessId);

            TRAYNOTIFYDATAW tnid = {0};
            tnid.dwSignature = NI_NOTIFY_SIG;
            tnid.dwMessage   = NIM_DELETE;
            CopyMemory(&tnid.nid, &Icon->IconData, Icon->IconData.cbSize);

            COPYDATASTRUCT data;
            data.dwData = 1;
            data.cbData = sizeof(tnid);
            data.lpData = &tnid;

            BOOL Success = ::SendMessage(This->m_hwndSysTray, WM_COPYDATA,
                                         (WPARAM)&Icon->IconData, (LPARAM)&data);
            if (!Success)
            {
                // If we failed to handle the delete message, forcibly remove it
                This->RemoveIconFromWatcher(&Icon->IconData);
            }
        }
        else
        {
            if (Status == WAIT_FAILED)
            {
                Status = GetLastError();
            }
            ERR("Failed to wait on process handles : %lu\n", Status);
            This->Uninitialize();
        }
    }

    if (WatchList)
        delete[] WatchList;

    return 0;
}

/*
* NotifyToolbar
*/

CBalloonQueue::CBalloonQueue() :
    m_hwndParent(NULL),
    m_tooltips(NULL),
    m_toolbar(NULL),
    m_current(NULL),
    m_currentClosed(false),
    m_timer(-1)
{
}

void CBalloonQueue::Init(HWND hwndParent, CNotifyToolbar * toolbar, CTooltips * balloons)
{
    m_hwndParent = hwndParent;
    m_toolbar = toolbar;
    m_tooltips = balloons;
}

void CBalloonQueue::Deinit()
{
    if (m_timer >= 0)
    {
        ::KillTimer(m_hwndParent, m_timer);
    }
}

bool CBalloonQueue::OnTimer(int timerId)
{
    if (timerId != m_timer)
        return false;

    ::KillTimer(m_hwndParent, m_timer);
    m_timer = -1;

    if (m_current && !m_currentClosed)
    {
        Close(m_current, NIN_BALLOONTIMEOUT);
    }
    else
    {
        m_current = NULL;
        m_currentClosed = false;
        if (!m_queue.IsEmpty())
        {
            Info info = m_queue.RemoveHead();
            Show(info);
        }
    }

    return true;
}

void CBalloonQueue::UpdateInfo(InternalIconData * notifyItem)
{
    size_t len = 0;
    HRESULT hr = StringCchLength(notifyItem->szInfo, _countof(notifyItem->szInfo), &len);
    if (SUCCEEDED(hr) && len > 0)
    {
        Info info(notifyItem);

        // If m_current == notifyItem, we want to replace the previous balloon even if there is a queue.
        if (m_current != notifyItem && (m_current != NULL || !m_queue.IsEmpty()))
        {
            m_queue.AddTail(info);
        }
        else
        {
            Show(info);
        }
    }
    else
    {
        Close(notifyItem, NIN_BALLOONHIDE);
    }
}

void CBalloonQueue::RemoveInfo(InternalIconData * notifyItem)
{
    Close(notifyItem, NIN_BALLOONHIDE);

    POSITION position = m_queue.GetHeadPosition();
    while(position != NULL)
    {
        Info& info = m_queue.GetNext(position);
        if (info.pSource == notifyItem)
        {
            m_queue.RemoveAt(position);
        }
    }
}

void CBalloonQueue::CloseCurrent()
{
    if (m_current != NULL)
    {
        Close(m_current, NIN_BALLOONTIMEOUT);
    }
}

int CBalloonQueue::IndexOf(InternalIconData * pdata)
{
    int count = m_toolbar->GetButtonCount();
    for (int i = 0; i < count; i++)
    {
        if (m_toolbar->GetItemData(i) == pdata)
            return i;
    }
    return -1;
}

void CBalloonQueue::SetTimer(int length)
{
    m_timer = ::SetTimer(m_hwndParent, BalloonsTimerId, length, NULL);
}

void CBalloonQueue::Show(Info& info)
{
    TRACE("ShowBalloonTip called for flags=%x text=%ws; title=%ws\n", info.uIcon, info.szInfo, info.szInfoTitle);

    // TODO: NIF_REALTIME, NIIF_NOSOUND, other Vista+ flags

    const int index = IndexOf(info.pSource);
    RECT rc;
    m_toolbar->GetItemRect(index, &rc);
    m_toolbar->ClientToScreen(&rc);
    const WORD x = (rc.left + rc.right) / 2;
    const WORD y = (rc.top + rc.bottom) / 2;

    m_tooltips->SetTitle(info.szInfoTitle, info.uIcon);
    m_tooltips->TrackPosition(x, y);
    m_tooltips->UpdateTipText(m_hwndParent, reinterpret_cast<LPARAM>(m_toolbar->m_hWnd), info.szInfo);
    m_tooltips->TrackActivate(m_hwndParent, reinterpret_cast<LPARAM>(m_toolbar->m_hWnd));

    m_current = info.pSource;
    int timeout = info.uTimeout;
    if (timeout < MinTimeout) timeout = MinTimeout;
    if (timeout > MaxTimeout) timeout = MaxTimeout;

    SetTimer(timeout);

    m_toolbar->SendNotifyCallback(m_current, NIN_BALLOONSHOW);
}

void CBalloonQueue::Close(IN OUT InternalIconData * notifyItem, IN UINT uReason)
{
    TRACE("HideBalloonTip called\n");

    if (m_current == notifyItem && !m_currentClosed)
    {
        m_toolbar->SendNotifyCallback(m_current, uReason);

        // Prevent Re-entry
        m_currentClosed = true;
        m_tooltips->TrackDeactivate();
        SetTimer(CooldownBetweenBalloons);
    }
}

/*
 * NotifyToolbar
 */

CNotifyToolbar::CNotifyToolbar() :
    m_ImageList(NULL),
    m_VisibleButtonCount(0),
    m_BalloonQueue(NULL)
{
}

CNotifyToolbar::~CNotifyToolbar()
{
}

int CNotifyToolbar::GetVisibleButtonCount()
{
    return m_VisibleButtonCount;
}

int CNotifyToolbar::FindItem(IN HWND hWnd, IN UINT uID, InternalIconData ** pdata)
{
    int count = GetButtonCount();

    for (int i = 0; i < count; i++)
    {
        InternalIconData * data = GetItemData(i);

        if (data->hWnd == hWnd &&
            data->uID == uID)
        {
            if (pdata)
                *pdata = data;
            return i;
        }
    }

    return -1;
}

int CNotifyToolbar::FindExistingSharedIcon(HICON handle)
{
    int count = GetButtonCount();
    for (int i = 0; i < count; i++)
    {
        InternalIconData * data = GetItemData(i);
        if (data->hIcon == handle)
        {
            TBBUTTON btn;
            GetButton(i, &btn);
            return btn.iBitmap;
        }
    }

    return -1;
}

BOOL CNotifyToolbar::AddButton(_In_ CONST NOTIFYICONDATA *iconData)
{
    TBBUTTON tbBtn = { 0 };
    InternalIconData * notifyItem;
    WCHAR text[] = L"";

    TRACE("Adding icon %d from hWnd %08x flags%s%s state%s%s", 
        iconData->uID, iconData->hWnd,
        (iconData->uFlags & NIF_ICON) ? " ICON" : "",
        (iconData->uFlags & NIF_STATE) ? " STATE" : "",
        (iconData->dwState & NIS_HIDDEN) ? " HIDDEN" : "",
        (iconData->dwState & NIS_SHAREDICON) ? " SHARED" : "");

    int index = FindItem(iconData->hWnd, iconData->uID, &notifyItem);
    if (index >= 0)
    {
        TRACE("Icon %d from hWnd %08x ALREADY EXISTS!", iconData->uID, iconData->hWnd);
        return FALSE;
    }

    notifyItem = new InternalIconData();
    ZeroMemory(notifyItem, sizeof(*notifyItem));

    notifyItem->hWnd = iconData->hWnd;
    notifyItem->uID = iconData->uID;

    tbBtn.fsState = TBSTATE_ENABLED;
    tbBtn.fsStyle = BTNS_NOPREFIX;
    tbBtn.dwData = (DWORD_PTR)notifyItem;
    tbBtn.iString = (INT_PTR) text;
    tbBtn.idCommand = GetButtonCount();

    if (iconData->uFlags & NIF_STATE)
    {
        notifyItem->dwState = iconData->dwState & iconData->dwStateMask;
    }

    if (iconData->uFlags & NIF_MESSAGE)
    {
        notifyItem->uCallbackMessage = iconData->uCallbackMessage;
    }

    if (iconData->uFlags & NIF_ICON)
    {
        notifyItem->hIcon = iconData->hIcon;
        BOOL hasSharedIcon = notifyItem->dwState & NIS_SHAREDICON;
        if (hasSharedIcon)
        {
            INT iIcon = FindExistingSharedIcon(notifyItem->hIcon);
            if (iIcon < 0)
            {
                notifyItem->hIcon = NULL;
                TRACE("Shared icon requested, but HICON not found!!!");
            }
            tbBtn.iBitmap = iIcon;
        }
        else
        {
            tbBtn.iBitmap = ImageList_AddIcon(m_ImageList, notifyItem->hIcon);
        }
    }

    if (iconData->uFlags & NIF_TIP)
    {
        StringCchCopy(notifyItem->szTip, _countof(notifyItem->szTip), iconData->szTip);
    }

    if (iconData->uFlags & NIF_INFO)
    {
        // NOTE: In Vista+, the uTimeout value is disregarded, and the accessibility settings are used always.
        StringCchCopy(notifyItem->szInfo, _countof(notifyItem->szInfo), iconData->szInfo);
        StringCchCopy(notifyItem->szInfoTitle, _countof(notifyItem->szInfoTitle), iconData->szInfoTitle);
        notifyItem->dwInfoFlags = iconData->dwInfoFlags;
        notifyItem->uTimeout = iconData->uTimeout;
    }

    if (notifyItem->dwState & NIS_HIDDEN)
    {
        tbBtn.fsState |= TBSTATE_HIDDEN;
    }
    else
    {
        m_VisibleButtonCount++;
    }

    /* TODO: support VERSION_4 (NIF_GUID, NIF_REALTIME, NIF_SHOWTIP) */

    CToolbar::AddButton(&tbBtn);
    SetButtonSize(GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON));

    if (iconData->uFlags & NIF_INFO)
    {
        m_BalloonQueue->UpdateInfo(notifyItem);
    }

    return TRUE;
}

BOOL CNotifyToolbar::SwitchVersion(_In_ CONST NOTIFYICONDATA *iconData)
{
    InternalIconData * notifyItem;
    int index = FindItem(iconData->hWnd, iconData->uID, &notifyItem);
    if (index < 0)
    {
        WARN("Icon %d from hWnd %08x DOES NOT EXIST!", iconData->uID, iconData->hWnd);
        return FALSE;
    }

    if (iconData->uVersion != 0 && iconData->uVersion != NOTIFYICON_VERSION)
    {
        WARN("Tried to set the version of icon %d from hWnd %08x, to an unknown value %d. Vista+ program?", iconData->uID, iconData->hWnd, iconData->uVersion);
        return FALSE;
    }

    // We can not store the version in the uVersion field, because it's union'd with uTimeout,
    // which we also need to keep track of.
    notifyItem->uVersionCopy = iconData->uVersion;

    return TRUE;
}

BOOL CNotifyToolbar::UpdateButton(_In_ CONST NOTIFYICONDATA *iconData)
{
    InternalIconData * notifyItem;
    TBBUTTONINFO tbbi = { 0 };

    TRACE("Updating icon %d from hWnd %08x flags%s%s state%s%s",
        iconData->uID, iconData->hWnd,
        (iconData->uFlags & NIF_ICON) ? " ICON" : "",
        (iconData->uFlags & NIF_STATE) ? " STATE" : "",
        (iconData->dwState & NIS_HIDDEN) ? " HIDDEN" : "",
        (iconData->dwState & NIS_SHAREDICON) ? " SHARED" : "");

    int index = FindItem(iconData->hWnd, iconData->uID, &notifyItem);
    if (index < 0)
    {
        WARN("Icon %d from hWnd %08x DOES NOT EXIST!", iconData->uID, iconData->hWnd);
        return AddButton(iconData);
    }

    TBBUTTON btn;
    GetButton(index, &btn);
    int oldIconIndex = btn.iBitmap;

    tbbi.cbSize = sizeof(tbbi);
    tbbi.dwMask = TBIF_BYINDEX | TBIF_COMMAND;
    tbbi.idCommand = index;

    if (iconData->uFlags & NIF_STATE)
    {
        if (iconData->dwStateMask & NIS_HIDDEN &&
            (notifyItem->dwState & NIS_HIDDEN) != (iconData->dwState & NIS_HIDDEN))
        {
            tbbi.dwMask |= TBIF_STATE;
            if (iconData->dwState & NIS_HIDDEN)
            {
                tbbi.fsState |= TBSTATE_HIDDEN;
                m_VisibleButtonCount--;
            }
            else
            {
                tbbi.fsState &= ~TBSTATE_HIDDEN;
                m_VisibleButtonCount++;
            }
        }

        notifyItem->dwState &= ~iconData->dwStateMask;
        notifyItem->dwState |= (iconData->dwState & iconData->dwStateMask);
    }

    if (iconData->uFlags & NIF_MESSAGE)
    {
        notifyItem->uCallbackMessage = iconData->uCallbackMessage;
    }

    if (iconData->uFlags & NIF_ICON)
    {
        BOOL hasSharedIcon = notifyItem->dwState & NIS_SHAREDICON;
        if (hasSharedIcon)
        {
            INT iIcon = FindExistingSharedIcon(iconData->hIcon);
            if (iIcon >= 0)
            {
                notifyItem->hIcon = iconData->hIcon;
                tbbi.dwMask |= TBIF_IMAGE;
                tbbi.iImage = iIcon;
            }
            else
            {
                TRACE("Shared icon requested, but HICON not found!!! IGNORING!");
            }
        }
        else
        {
            notifyItem->hIcon = iconData->hIcon;
            tbbi.dwMask |= TBIF_IMAGE;
            tbbi.iImage = ImageList_ReplaceIcon(m_ImageList, oldIconIndex, notifyItem->hIcon);
        }
    }

    if (iconData->uFlags & NIF_TIP)
    {
        StringCchCopy(notifyItem->szTip, _countof(notifyItem->szTip), iconData->szTip);
    }

    if (iconData->uFlags & NIF_INFO)
    {
        // NOTE: In Vista+, the uTimeout value is disregarded, and the accessibility settings are used always.
        StringCchCopy(notifyItem->szInfo, _countof(notifyItem->szInfo), iconData->szInfo);
        StringCchCopy(notifyItem->szInfoTitle, _countof(notifyItem->szInfoTitle), iconData->szInfoTitle);
        notifyItem->dwInfoFlags = iconData->dwInfoFlags;
        notifyItem->uTimeout = iconData->uTimeout;
    }

    /* TODO: support VERSION_4 (NIF_GUID, NIF_REALTIME, NIF_SHOWTIP) */

    SetButtonInfo(index, &tbbi);

    if (iconData->uFlags & NIF_INFO)
    {
        m_BalloonQueue->UpdateInfo(notifyItem);
    }

    return TRUE;
}

BOOL CNotifyToolbar::RemoveButton(_In_ CONST NOTIFYICONDATA *iconData)
{
    InternalIconData * notifyItem;

    TRACE("Removing icon %d from hWnd %08x", iconData->uID, iconData->hWnd);

    int index = FindItem(iconData->hWnd, iconData->uID, &notifyItem);
    if (index < 0)
    {
        TRACE("Icon %d from hWnd %08x ALREADY MISSING!", iconData->uID, iconData->hWnd);

        return FALSE;
    }

    if (!(notifyItem->dwState & NIS_HIDDEN))
    {
        m_VisibleButtonCount--;
    }

    if (!(notifyItem->dwState & NIS_SHAREDICON))
    {
        TBBUTTON btn;
        GetButton(index, &btn);
        int oldIconIndex = btn.iBitmap;
        ImageList_Remove(m_ImageList, oldIconIndex);

        // Update other icons!
        int count = GetButtonCount();
        for (int i = 0; i < count; i++)
        {
            TBBUTTON btn;
            GetButton(i, &btn);

            if (btn.iBitmap > oldIconIndex)
            {
                TBBUTTONINFO tbbi2 = { 0 };
                tbbi2.cbSize = sizeof(tbbi2);
                tbbi2.dwMask = TBIF_BYINDEX | TBIF_IMAGE;
                tbbi2.iImage = btn.iBitmap-1;
                SetButtonInfo(i, &tbbi2);
            }
        }
    }

    m_BalloonQueue->RemoveInfo(notifyItem);

    DeleteButton(index);

    delete notifyItem;

    return TRUE;
}

VOID CNotifyToolbar::ResizeImagelist()
{
    int cx, cy;
    HIMAGELIST iml;

    if (!ImageList_GetIconSize(m_ImageList, &cx, &cy))
        return;

    if (cx == GetSystemMetrics(SM_CXSMICON) && cy == GetSystemMetrics(SM_CYSMICON))
        return;

    iml = ImageList_Create(GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON), ILC_COLOR32 | ILC_MASK, 0, 1000);
    if (!iml)
        return;

    ImageList_Destroy(m_ImageList);
    m_ImageList = iml;
    SetImageList(m_ImageList);

    int count = GetButtonCount();
    for (int i = 0; i < count; i++)
    {
        InternalIconData * data = GetItemData(i);
        BOOL hasSharedIcon = data->dwState & NIS_SHAREDICON;
        INT iIcon = hasSharedIcon ? FindExistingSharedIcon(data->hIcon) : -1;
        if (iIcon < 0)
            iIcon = ImageList_AddIcon(iml, data->hIcon);
        TBBUTTONINFO tbbi = { sizeof(tbbi), TBIF_BYINDEX | TBIF_IMAGE, 0, iIcon};
        SetButtonInfo(i, &tbbi);
    }

    SetButtonSize(GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON));
}

LRESULT CNotifyToolbar::OnCtxMenu(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
    bHandled = FALSE;

    /*
     * WM_CONTEXTMENU message can be generated either by the mouse,
     * in which case lParam encodes the mouse coordinates where the
     * user right-clicked the mouse, or can be generated by (Shift-)F10
     * keyboard press, in which case lParam equals -1.
     */
    INT iBtn = GetHotItem();
    if (iBtn < 0)
        return 0;

    InternalIconData* notifyItem = GetItemData(iBtn);

    if (!::IsWindow(notifyItem->hWnd))
        return 0;

    if (notifyItem->uVersionCopy >= NOTIFYICON_VERSION)
    {
        /* Transmit the WM_CONTEXTMENU message if the notification icon supports it */
        ::SendNotifyMessage(notifyItem->hWnd,
                            notifyItem->uCallbackMessage,
                            notifyItem->uID,
                            WM_CONTEXTMENU);
    }
    else if (lParam == -1)
    {
        /*
         * Otherwise, and only if the WM_CONTEXTMENU message was generated
         * from the keyboard, simulate right-click mouse messages. This is
         * not needed if the message came from the mouse because in this
         * case the right-click mouse messages were already sent together.
         */
        ::SendNotifyMessage(notifyItem->hWnd,
                            notifyItem->uCallbackMessage,
                            notifyItem->uID,
                            WM_RBUTTONDOWN);
        ::SendNotifyMessage(notifyItem->hWnd,
                            notifyItem->uCallbackMessage,
                            notifyItem->uID,
                            WM_RBUTTONUP);
    }

    return 0;
}

bool CNotifyToolbar::SendNotifyCallback(InternalIconData* notifyItem, UINT uMsg)
{
    if (!::IsWindow(notifyItem->hWnd))
    {
        // We detect and destroy icons with invalid handles only on mouse move over systray, same as MS does.
        // Alternatively we could search for them periodically (would waste more resources).
        TRACE("Destroying icon %d with invalid handle hWnd=%08x\n", notifyItem->uID, notifyItem->hWnd);

        RemoveButton(notifyItem);

        /* Ask the parent to resize */
        NMHDR nmh = {GetParent(), 0, NTNWM_REALIGN};
        GetParent().SendMessage(WM_NOTIFY, 0, (LPARAM) &nmh);

        return true;
    }

    DWORD pid;
    GetWindowThreadProcessId(notifyItem->hWnd, &pid);

    if (pid == GetCurrentProcessId() ||
        (uMsg >= WM_MOUSEFIRST && uMsg <= WM_MOUSELAST))
    {
        ::PostMessage(notifyItem->hWnd,
                      notifyItem->uCallbackMessage,
                      notifyItem->uID,
                      uMsg);
    }
    else
    {
        ::SendMessage(notifyItem->hWnd,
                      notifyItem->uCallbackMessage,
                      notifyItem->uID,
                      uMsg);
    }
    return false;
}

VOID CNotifyToolbar::SendMouseEvent(IN WORD wIndex, IN UINT uMsg, IN WPARAM wParam)
{
    static LPCWSTR eventNames [] = {
        L"WM_MOUSEMOVE",
        L"WM_LBUTTONDOWN",
        L"WM_LBUTTONUP",
        L"WM_LBUTTONDBLCLK",
        L"WM_RBUTTONDOWN",
        L"WM_RBUTTONUP",
        L"WM_RBUTTONDBLCLK",
        L"WM_MBUTTONDOWN",
        L"WM_MBUTTONUP",
        L"WM_MBUTTONDBLCLK",
        L"WM_MOUSEWHEEL",
        L"WM_XBUTTONDOWN",
        L"WM_XBUTTONUP",
        L"WM_XBUTTONDBLCLK"
    };

    InternalIconData * notifyItem = GetItemData(wIndex);

    if (uMsg >= WM_MOUSEFIRST && uMsg <= WM_MOUSELAST)
    {
        TRACE("Sending message %S from button %d to %p (msg=%x, w=%x, l=%x)...\n",
            eventNames[uMsg - WM_MOUSEFIRST], wIndex,
            notifyItem->hWnd, notifyItem->uCallbackMessage, notifyItem->uID, uMsg);
    }

    SendNotifyCallback(notifyItem, uMsg);
}

LRESULT CNotifyToolbar::OnMouseEvent(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
    POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
    INT iBtn = HitTest(&pt);

    if (iBtn >= 0)
    {
        SendMouseEvent(iBtn, uMsg, wParam);
    }

    bHandled = FALSE;
    return FALSE;
}

static VOID GetTooltipText(LPARAM data, LPTSTR szTip, DWORD cchTip)
{
    InternalIconData * notifyItem = reinterpret_cast<InternalIconData *>(data);
    if (notifyItem)
    {
        StringCchCopy(szTip, cchTip, notifyItem->szTip);
    }
    else
    {
        StringCchCopy(szTip, cchTip, L"");
    }
}

LRESULT CNotifyToolbar::OnTooltipShow(INT uCode, LPNMHDR hdr, BOOL& bHandled)
{
    RECT rcTip, rcItem;
    ::GetWindowRect(hdr->hwndFrom, &rcTip);

    SIZE szTip = { rcTip.right - rcTip.left, rcTip.bottom - rcTip.top };

    INT iBtn = GetHotItem();

    if (iBtn >= 0)
    {
        MONITORINFO monInfo = { 0 };
        HMONITOR hMon = MonitorFromWindow(m_hWnd, MONITOR_DEFAULTTONEAREST);

        monInfo.cbSize = sizeof(monInfo);

        if (hMon)
            GetMonitorInfo(hMon, &monInfo);
        else
            ::GetWindowRect(GetDesktopWindow(), &monInfo.rcMonitor);

        GetItemRect(iBtn, &rcItem);

        POINT ptItem = { rcItem.left, rcItem.top };
        SIZE szItem = { rcItem.right - rcItem.left, rcItem.bottom - rcItem.top };
        ClientToScreen(&ptItem);

        ptItem.x += szItem.cx / 2;
        ptItem.y -= szTip.cy;

        if (ptItem.x + szTip.cx > monInfo.rcMonitor.right)
            ptItem.x = monInfo.rcMonitor.right - szTip.cx;

        if (ptItem.y + szTip.cy > monInfo.rcMonitor.bottom)
            ptItem.y = monInfo.rcMonitor.bottom - szTip.cy;

        if (ptItem.x < monInfo.rcMonitor.left)
            ptItem.x = monInfo.rcMonitor.left;

        if (ptItem.y < monInfo.rcMonitor.top)
            ptItem.y = monInfo.rcMonitor.top;

        TRACE("ptItem { %d, %d }\n", ptItem.x, ptItem.y);

        ::SetWindowPos(hdr->hwndFrom, NULL, ptItem.x, ptItem.y, 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);

        return TRUE;
    }

    bHandled = FALSE;
    return 0;
}

void CNotifyToolbar::Initialize(HWND hWndParent, CBalloonQueue * queue)
{
    m_BalloonQueue = queue;

    DWORD styles =
        WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN |
        TBSTYLE_FLAT | TBSTYLE_TOOLTIPS | TBSTYLE_WRAPABLE | TBSTYLE_TRANSPARENT |
        CCS_TOP | CCS_NORESIZE | CCS_NOPARENTALIGN | CCS_NODIVIDER;

    SubclassWindow(CToolbar::Create(hWndParent, styles));

    // Force the toolbar tooltips window to always show tooltips even if not foreground
    HWND tooltipsWnd = (HWND)SendMessageW(TB_GETTOOLTIPS);
    if (tooltipsWnd)
    {
        ::SetWindowLong(tooltipsWnd, GWL_STYLE, ::GetWindowLong(tooltipsWnd, GWL_STYLE) | TTS_ALWAYSTIP);
    }

    SetWindowTheme(m_hWnd, L"TrayNotify", NULL);

    m_ImageList = ImageList_Create(GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON), ILC_COLOR32 | ILC_MASK, 0, 1000);        
    SetImageList(m_ImageList);

    TBMETRICS tbm = {sizeof(tbm)};
    tbm.dwMask = TBMF_BARPAD | TBMF_BUTTONSPACING | TBMF_PAD;
    tbm.cxPad = 1;
    tbm.cyPad = 1;
    tbm.cxBarPad = 1;
    tbm.cyBarPad = 1;
    tbm.cxButtonSpacing = 1;
    tbm.cyButtonSpacing = 1;
    SetMetrics(&tbm);

    SetButtonSize(GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON));
}

/*
 * SysPagerWnd
 */
const WCHAR szSysPagerWndClass[] = L"SysPager";

CSysPagerWnd::CSysPagerWnd() {}

CSysPagerWnd::~CSysPagerWnd() {}

LRESULT CSysPagerWnd::OnEraseBackground(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
    HDC hdc = (HDC) wParam;

    if (!IsAppThemed())
    {
        bHandled = FALSE;
        return 0;
    }

    RECT rect;
    GetClientRect(&rect);
    DrawThemeParentBackground(m_hWnd, hdc, &rect);

    return TRUE;
}

LRESULT CSysPagerWnd::OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
    Toolbar.Initialize(m_hWnd, &m_BalloonQueue);
    CIconWatcher::Initialize(m_hWnd);

    HWND hWndTop = GetAncestor(m_hWnd, GA_ROOT);

    m_Balloons.Create(hWndTop, TTS_NOPREFIX | TTS_BALLOON | TTS_CLOSE);
        
    TOOLINFOW ti = { 0 };
    ti.cbSize = TTTOOLINFOW_V1_SIZE;
    ti.uFlags = TTF_TRACK | TTF_IDISHWND;
    ti.uId = reinterpret_cast<UINT_PTR>(Toolbar.m_hWnd);
    ti.hwnd = m_hWnd;
    ti.lpszText = NULL;
    ti.lParam = NULL;

    BOOL ret = m_Balloons.AddTool(&ti);
    if (!ret)
    {
        WARN("AddTool failed, LastError=%d (probably meaningless unless non-zero)\n", GetLastError());
    }

    m_BalloonQueue.Init(m_hWnd, &Toolbar, &m_Balloons);

    // Explicitly request running applications to re-register their systray icons
    ::SendNotifyMessageW(HWND_BROADCAST,
                            RegisterWindowMessageW(L"TaskbarCreated"),
                            0, 0);

    return TRUE;
}

LRESULT CSysPagerWnd::OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
    m_BalloonQueue.Deinit();
    CIconWatcher::Uninitialize();
    return TRUE;
}

BOOL CSysPagerWnd::NotifyIcon(DWORD dwMessage, _In_ CONST NOTIFYICONDATA *iconData)
{
    BOOL ret = FALSE;

    int VisibleButtonCount = Toolbar.GetVisibleButtonCount();

    TRACE("NotifyIcon received. Code=%d\n", dwMessage);
    switch (dwMessage)
    {
    case NIM_ADD:
        ret = Toolbar.AddButton(iconData);
        if (ret == TRUE)
        {
            (void)AddIconToWatcher(iconData);
        }
        break;

    case NIM_MODIFY:
        ret = Toolbar.UpdateButton(iconData);
        break;

    case NIM_DELETE:
        ret = Toolbar.RemoveButton(iconData);
        if (ret == TRUE)
        {
            (void)RemoveIconFromWatcher(iconData);
        }
        break;

    case NIM_SETFOCUS:
        Toolbar.SetFocus();
        ret = TRUE;
        break;

    case NIM_SETVERSION:
        ret = Toolbar.SwitchVersion(iconData);
        break;

    default:
        TRACE("NotifyIcon received with unknown code %d.\n", dwMessage);
        return FALSE;
    }

    if (VisibleButtonCount != Toolbar.GetVisibleButtonCount())
    {
        /* Ask the parent to resize */
        NMHDR nmh = {GetParent(), 0, NTNWM_REALIGN};
        GetParent().SendMessage(WM_NOTIFY, 0, (LPARAM) &nmh);
    }

    return ret;
}

void CSysPagerWnd::GetSize(IN BOOL IsHorizontal, IN PSIZE size)
{
    /* Get the ideal height or width */
#if 0 
    /* Unfortunately this doens't work correctly in ros */
    Toolbar.GetIdealSize(!IsHorizontal, size);

    /* Make the reference dimension an exact multiple of the icon size */
    if (IsHorizontal)
        size->cy -= size->cy % GetSystemMetrics(SM_CYSMICON);
    else
        size->cx -= size->cx % GetSystemMetrics(SM_CXSMICON);

#else
    INT rows = 0;
    INT columns = 0;
    INT cyButton = GetSystemMetrics(SM_CYSMICON) + 2;
    INT cxButton = GetSystemMetrics(SM_CXSMICON) + 2;
    int VisibleButtonCount = Toolbar.GetVisibleButtonCount();

    if (IsHorizontal)
    {
        rows = max(size->cy / cyButton, 1);
        columns = (VisibleButtonCount + rows - 1) / rows;
    }
    else
    {
        columns = max(size->cx / cxButton, 1);
        rows = (VisibleButtonCount + columns - 1) / columns;
    }
    size->cx = columns * cxButton;
    size->cy = rows * cyButton;
#endif
}

LRESULT CSysPagerWnd::OnGetInfoTip(INT uCode, LPNMHDR hdr, BOOL& bHandled)
{
    NMTBGETINFOTIPW * nmtip = (NMTBGETINFOTIPW *) hdr;
    GetTooltipText(nmtip->lParam, nmtip->pszText, nmtip->cchTextMax);
    return TRUE;
}

LRESULT CSysPagerWnd::OnCustomDraw(INT uCode, LPNMHDR hdr, BOOL& bHandled)
{
    NMCUSTOMDRAW * cdraw = (NMCUSTOMDRAW *) hdr;
    switch (cdraw->dwDrawStage)
    {
    case CDDS_PREPAINT:
        return CDRF_NOTIFYITEMDRAW;

    case CDDS_ITEMPREPAINT:
        return TBCDRF_NOBACKGROUND | TBCDRF_NOEDGES | TBCDRF_NOOFFSET | TBCDRF_NOMARK | TBCDRF_NOETCHEDEFFECT;
    }
    return TRUE;
}

LRESULT CSysPagerWnd::OnCommand(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
    bHandled = FALSE;

    /* Handles the BN_CLICKED notifications sent by the CNotifyToolbar member */
    if (HIWORD(wParam) != BN_CLICKED)
        return 0;

    INT iBtn = LOWORD(wParam);
    if (iBtn < 0)
        return 0;

    InternalIconData* notifyItem = Toolbar.GetItemData(iBtn);

    if (!::IsWindow(notifyItem->hWnd))
        return 0;

    // TODO: Improve keyboard handling by looking whether one presses
    // on ENTER, etc..., which roughly translates into "double-clicking".

    if (notifyItem->uVersionCopy >= NOTIFYICON_VERSION)
    {
        /* Use new-style notifications if the notification icon supports them */
        ::SendNotifyMessage(notifyItem->hWnd,
                            notifyItem->uCallbackMessage,
                            notifyItem->uID,
                            NIN_SELECT); // TODO: Distinguish with NIN_KEYSELECT
    }
    else if (lParam == -1)
    {
        /*
         * Otherwise, and only if the icon was selected via the keyboard,
         * simulate right-click mouse messages. This is not needed if the
         * selection was done by mouse because in this case the mouse
         * messages were already sent.
         */
        ::SendNotifyMessage(notifyItem->hWnd,
                            notifyItem->uCallbackMessage,
                            notifyItem->uID,
                            WM_LBUTTONDOWN); // TODO: Distinguish with double-click WM_LBUTTONDBLCLK
        ::SendNotifyMessage(notifyItem->hWnd,
                            notifyItem->uCallbackMessage,
                            notifyItem->uID,
                            WM_LBUTTONUP);
    }

    return 0;
}

LRESULT CSysPagerWnd::OnSize(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
    LRESULT Ret = TRUE;
    SIZE szClient;
    szClient.cx = LOWORD(lParam);
    szClient.cy = HIWORD(lParam);

    Ret = DefWindowProc(uMsg, wParam, lParam);

    if (Toolbar)
    {
        Toolbar.SetWindowPos(NULL, 0, 0, szClient.cx, szClient.cy, SWP_NOZORDER);
        Toolbar.AutoSize();

        RECT rc;
        Toolbar.GetClientRect(&rc);

        SIZE szBar = { rc.right - rc.left, rc.bottom - rc.top };

        INT xOff = (szClient.cx - szBar.cx) / 2;
        INT yOff = (szClient.cy - szBar.cy) / 2;

        Toolbar.SetWindowPos(NULL, xOff, yOff, szBar.cx, szBar.cy, SWP_NOZORDER);
    }
    return Ret;
}

LRESULT CSysPagerWnd::OnCtxMenu(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
    bHandled = TRUE;
    return 0;
}

LRESULT CSysPagerWnd::OnBalloonPop(UINT uCode, LPNMHDR hdr , BOOL& bHandled)
{
    m_BalloonQueue.CloseCurrent();
    bHandled = TRUE;
    return 0;
}

LRESULT CSysPagerWnd::OnTimer(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
    if (m_BalloonQueue.OnTimer(wParam))
    {
        bHandled = TRUE;
    }

    return 0;
}

LRESULT CSysPagerWnd::OnCopyData(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
    PCOPYDATASTRUCT cpData = (PCOPYDATASTRUCT)lParam;
    if (cpData->dwData == 1)
    {
        /* A taskbar NotifyIcon notification */
        PTRAYNOTIFYDATAW pData = (PTRAYNOTIFYDATAW)cpData->lpData;
        if (pData->dwSignature == NI_NOTIFY_SIG)
            return NotifyIcon(pData->dwMessage, &pData->nid);
    }
    // TODO: Handle other types of taskbar notifications

    return FALSE;
}

LRESULT CSysPagerWnd::OnSettingChanged(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
    if (wParam == SPI_SETNONCLIENTMETRICS)
    {
        Toolbar.ResizeImagelist();
    }
    return 0;
}

LRESULT CSysPagerWnd::OnGetMinimumSize(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
    GetSize((BOOL)wParam, (PSIZE)lParam);
    return 0;
}

HRESULT CSysPagerWnd::Initialize(IN HWND hWndParent)
{
    /* Create the window. The tray window is going to move it to the correct
        position and resize it as needed. */
    DWORD dwStyle = WS_CHILD | WS_CLIPSIBLINGS | WS_VISIBLE;
    Create(hWndParent, 0, NULL, dwStyle);
    if (!m_hWnd)
        return E_FAIL;

    SetWindowTheme(m_hWnd, L"TrayNotify", NULL);

    return S_OK;
}

HRESULT CSysPagerWnd_CreateInstance(HWND hwndParent, REFIID riid, void **ppv)
{
    return ShellObjectCreatorInit<CSysPagerWnd>(hwndParent, riid, ppv);
}