Added desktop notifications implementation for Windows 7 (and earlier)
This commit is contained in:
parent
e6a30388da
commit
fe05b66a6c
10 changed files with 1674 additions and 4 deletions
47
brightray/browser/win/notification_presenter_win7.cc
Normal file
47
brightray/browser/win/notification_presenter_win7.cc
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
#include "notification_presenter_win7.h"
|
||||||
|
#include "win32_notification.h"
|
||||||
|
|
||||||
|
namespace brightray {
|
||||||
|
|
||||||
|
brightray::Notification* NotificationPresenterWin7::CreateNotificationObject(NotificationDelegate* delegate)
|
||||||
|
{
|
||||||
|
return new Win32Notification(delegate, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
Win32Notification* NotificationPresenterWin7::GetNotificationObjectByRef(const DesktopNotificationController::Notification& ref)
|
||||||
|
{
|
||||||
|
for(auto n : this->notifications())
|
||||||
|
{
|
||||||
|
auto w32n = static_cast<Win32Notification*>(n);
|
||||||
|
if(w32n->GetRef() == ref)
|
||||||
|
return w32n;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
Win32Notification* NotificationPresenterWin7::GetNotificationObjectByTag(const std::string& tag)
|
||||||
|
{
|
||||||
|
for(auto n : this->notifications())
|
||||||
|
{
|
||||||
|
auto w32n = static_cast<Win32Notification*>(n);
|
||||||
|
if(w32n->GetTag() == tag)
|
||||||
|
return w32n;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NotificationPresenterWin7::OnNotificationClicked(Notification& notification)
|
||||||
|
{
|
||||||
|
auto n = GetNotificationObjectByRef(notification);
|
||||||
|
if(n) n->NotificationClicked();
|
||||||
|
}
|
||||||
|
|
||||||
|
void NotificationPresenterWin7::OnNotificationDismissed(Notification& notification)
|
||||||
|
{
|
||||||
|
auto n = GetNotificationObjectByRef(notification);
|
||||||
|
if(n) n->NotificationDismissed();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
28
brightray/browser/win/notification_presenter_win7.h
Normal file
28
brightray/browser/win/notification_presenter_win7.h
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
#pragma once
|
||||||
|
#include "browser/notification_presenter.h"
|
||||||
|
#include "browser/win/win32_desktop_notifications/desktop_notification_controller.h"
|
||||||
|
|
||||||
|
namespace brightray {
|
||||||
|
|
||||||
|
class Win32Notification;
|
||||||
|
|
||||||
|
class NotificationPresenterWin7 :
|
||||||
|
public NotificationPresenter,
|
||||||
|
public DesktopNotificationController
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
NotificationPresenterWin7() = default;
|
||||||
|
|
||||||
|
Win32Notification* GetNotificationObjectByRef(const DesktopNotificationController::Notification& ref);
|
||||||
|
Win32Notification* GetNotificationObjectByTag(const std::string& tag);
|
||||||
|
|
||||||
|
private:
|
||||||
|
DISALLOW_COPY_AND_ASSIGN(NotificationPresenterWin7);
|
||||||
|
|
||||||
|
brightray::Notification* CreateNotificationObject(NotificationDelegate* delegate) override;
|
||||||
|
|
||||||
|
void OnNotificationClicked(Notification& notification) override;
|
||||||
|
void OnNotificationDismissed(Notification& notification) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
57
brightray/browser/win/win32_desktop_notifications/common.h
Normal file
57
brightray/browser/win/win32_desktop_notifications/common.h
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
#pragma once
|
||||||
|
#include <Windows.h>
|
||||||
|
|
||||||
|
namespace brightray {
|
||||||
|
|
||||||
|
struct NotificationData
|
||||||
|
{
|
||||||
|
DesktopNotificationController* controller = nullptr;
|
||||||
|
|
||||||
|
std::wstring caption;
|
||||||
|
std::wstring bodyText;
|
||||||
|
HBITMAP image = NULL;
|
||||||
|
|
||||||
|
|
||||||
|
NotificationData() = default;
|
||||||
|
|
||||||
|
~NotificationData()
|
||||||
|
{
|
||||||
|
if(image) DeleteObject(image);
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationData(const NotificationData& other) = delete;
|
||||||
|
NotificationData& operator=(const NotificationData& other) = delete;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
inline T ScaleForDpi(T value, unsigned dpi)
|
||||||
|
{
|
||||||
|
return value * dpi / 96;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ScreenMetrics
|
||||||
|
{
|
||||||
|
UINT dpiX, dpiY;
|
||||||
|
|
||||||
|
ScreenMetrics()
|
||||||
|
{
|
||||||
|
typedef HRESULT WINAPI GetDpiForMonitor_t(HMONITOR, int, UINT*, UINT*);
|
||||||
|
auto GetDpiForMonitor = (GetDpiForMonitor_t*)GetProcAddress(GetModuleHandle(TEXT("shcore")), "GetDpiForMonitor");
|
||||||
|
if(GetDpiForMonitor)
|
||||||
|
{
|
||||||
|
auto monitor = MonitorFromPoint({}, MONITOR_DEFAULTTOPRIMARY);
|
||||||
|
if(GetDpiForMonitor(monitor, 0, &dpiX, &dpiY) == S_OK)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HDC hdc = GetDC(NULL);
|
||||||
|
dpiX = GetDeviceCaps(hdc, LOGPIXELSX);
|
||||||
|
dpiY = GetDeviceCaps(hdc, LOGPIXELSY);
|
||||||
|
ReleaseDC(NULL, hdc);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T> T X(T value) const { return ScaleForDpi(value, dpiX); }
|
||||||
|
template<typename T> T Y(T value) const { return ScaleForDpi(value, dpiY); }
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,426 @@
|
||||||
|
#define NOMINMAX
|
||||||
|
#define WIN32_LEAN_AND_MEAN
|
||||||
|
#include "desktop_notification_controller.h"
|
||||||
|
#include "common.h"
|
||||||
|
#include "toast.h"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <vector>
|
||||||
|
#include <windowsx.h>
|
||||||
|
|
||||||
|
using namespace std;
|
||||||
|
|
||||||
|
namespace brightray {
|
||||||
|
|
||||||
|
HBITMAP CopyBitmap(HBITMAP bitmap)
|
||||||
|
{
|
||||||
|
HBITMAP ret = NULL;
|
||||||
|
|
||||||
|
BITMAP bm;
|
||||||
|
if(bitmap && GetObject(bitmap, sizeof(bm), &bm))
|
||||||
|
{
|
||||||
|
HDC hdcScreen = GetDC(NULL);
|
||||||
|
ret = CreateCompatibleBitmap(hdcScreen, bm.bmWidth, bm.bmHeight);
|
||||||
|
ReleaseDC(NULL, hdcScreen);
|
||||||
|
|
||||||
|
if(ret)
|
||||||
|
{
|
||||||
|
HDC hdcSrc = CreateCompatibleDC(NULL);
|
||||||
|
HDC hdcDst = CreateCompatibleDC(NULL);
|
||||||
|
SelectBitmap(hdcSrc, bitmap);
|
||||||
|
SelectBitmap(hdcDst, ret);
|
||||||
|
BitBlt(hdcDst, 0, 0, bm.bmWidth, bm.bmHeight, hdcSrc, 0, 0, SRCCOPY);
|
||||||
|
DeleteDC(hdcDst);
|
||||||
|
DeleteDC(hdcSrc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
HINSTANCE DesktopNotificationController::RegisterWndClasses()
|
||||||
|
{
|
||||||
|
// We keep a static `module` variable which serves a dual purpose:
|
||||||
|
// 1. Stores the HINSTANCE where the window classes are registered, which can be passed to `CreateWindow`
|
||||||
|
// 2. Indicates whether we already attempted the registration so that we don't do it twice (we don't retry
|
||||||
|
// even if registration fails, as there is no point.
|
||||||
|
static HMODULE module = NULL;
|
||||||
|
|
||||||
|
if(!module)
|
||||||
|
{
|
||||||
|
if(GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast<LPCWSTR>(&RegisterWndClasses), &module))
|
||||||
|
{
|
||||||
|
Toast::Register(module);
|
||||||
|
|
||||||
|
WNDCLASSEX wc = { sizeof(wc) };
|
||||||
|
wc.lpfnWndProc = &WndProc;
|
||||||
|
wc.lpszClassName = className;
|
||||||
|
wc.cbWndExtra = sizeof(DesktopNotificationController*);
|
||||||
|
wc.hInstance = module;
|
||||||
|
|
||||||
|
RegisterClassEx(&wc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return module;
|
||||||
|
}
|
||||||
|
|
||||||
|
DesktopNotificationController::DesktopNotificationController(unsigned maximumToasts)
|
||||||
|
{
|
||||||
|
instances.reserve(maximumToasts);
|
||||||
|
}
|
||||||
|
|
||||||
|
DesktopNotificationController::~DesktopNotificationController()
|
||||||
|
{
|
||||||
|
for(auto&& inst : instances) DestroyToast(inst);
|
||||||
|
if(hwndController) DestroyWindow(hwndController);
|
||||||
|
ClearAssets();
|
||||||
|
}
|
||||||
|
|
||||||
|
LRESULT CALLBACK DesktopNotificationController::WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
||||||
|
{
|
||||||
|
switch(message)
|
||||||
|
{
|
||||||
|
case WM_CREATE:
|
||||||
|
{
|
||||||
|
auto& cs = reinterpret_cast<const CREATESTRUCT*&>(lParam);
|
||||||
|
SetWindowLongPtr(hWnd, 0, (LONG_PTR)cs->lpCreateParams);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WM_TIMER:
|
||||||
|
if(wParam == TimerID_Animate)
|
||||||
|
{
|
||||||
|
Get(hWnd)->AnimateAll();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
case WM_DISPLAYCHANGE:
|
||||||
|
{
|
||||||
|
auto inst = Get(hWnd);
|
||||||
|
inst->ClearAssets();
|
||||||
|
inst->AnimateAll();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WM_SETTINGCHANGE:
|
||||||
|
if(wParam == SPI_SETWORKAREA)
|
||||||
|
{
|
||||||
|
Get(hWnd)->AnimateAll();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DefWindowProc(hWnd, message, wParam, lParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopNotificationController::StartAnimation()
|
||||||
|
{
|
||||||
|
_ASSERT(hwndController);
|
||||||
|
|
||||||
|
if(!isAnimating && hwndController)
|
||||||
|
{
|
||||||
|
// NOTE: 15ms is shorter than what we'd need for 60 fps, but since the timer
|
||||||
|
// is not accurate we must request a higher frame rate to get at least 60
|
||||||
|
|
||||||
|
SetTimer(hwndController, TimerID_Animate, 15, nullptr);
|
||||||
|
isAnimating = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HFONT DesktopNotificationController::GetCaptionFont()
|
||||||
|
{
|
||||||
|
InitializeFonts();
|
||||||
|
return captionFont;
|
||||||
|
}
|
||||||
|
|
||||||
|
HFONT DesktopNotificationController::GetBodyFont()
|
||||||
|
{
|
||||||
|
InitializeFonts();
|
||||||
|
return bodyFont;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopNotificationController::InitializeFonts()
|
||||||
|
{
|
||||||
|
if(!bodyFont)
|
||||||
|
{
|
||||||
|
NONCLIENTMETRICS metrics = { sizeof(metrics) };
|
||||||
|
if(SystemParametersInfo(SPI_GETNONCLIENTMETRICS, 0, &metrics, 0))
|
||||||
|
{
|
||||||
|
auto baseHeight = metrics.lfMessageFont.lfHeight;
|
||||||
|
|
||||||
|
HDC hdc = GetDC(NULL);
|
||||||
|
auto dpiY = GetDeviceCaps(hdc, LOGPIXELSY);
|
||||||
|
ReleaseDC(NULL, hdc);
|
||||||
|
|
||||||
|
metrics.lfMessageFont.lfHeight = (LONG)ScaleForDpi(baseHeight * 1.1f, dpiY);
|
||||||
|
bodyFont = CreateFontIndirect(&metrics.lfMessageFont);
|
||||||
|
|
||||||
|
if(captionFont) DeleteFont(captionFont);
|
||||||
|
metrics.lfMessageFont.lfHeight = (LONG)ScaleForDpi(baseHeight * 1.4f, dpiY);
|
||||||
|
captionFont = CreateFontIndirect(&metrics.lfMessageFont);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopNotificationController::ClearAssets()
|
||||||
|
{
|
||||||
|
if(captionFont) { DeleteFont(captionFont); captionFont = NULL; }
|
||||||
|
if(bodyFont) { DeleteFont(bodyFont); bodyFont = NULL; }
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopNotificationController::AnimateAll()
|
||||||
|
{
|
||||||
|
// NOTE: This function refreshes position and size of all toasts according
|
||||||
|
// to all current conditions. Animation time is only one of the variables
|
||||||
|
// influencing them. Screen resolution is another.
|
||||||
|
|
||||||
|
bool keepAnimating = false;
|
||||||
|
|
||||||
|
if(!instances.empty())
|
||||||
|
{
|
||||||
|
RECT workArea;
|
||||||
|
if(SystemParametersInfo(SPI_GETWORKAREA, 0, &workArea, 0))
|
||||||
|
{
|
||||||
|
ScreenMetrics metrics;
|
||||||
|
POINT origin = { workArea.right,
|
||||||
|
workArea.bottom - metrics.Y(toastMargin<int>) };
|
||||||
|
|
||||||
|
auto hdwp = BeginDeferWindowPos((int)instances.size());
|
||||||
|
for(auto&& inst : instances)
|
||||||
|
{
|
||||||
|
if(!inst.hwnd) continue;
|
||||||
|
|
||||||
|
auto notification = Toast::Get(inst.hwnd);
|
||||||
|
hdwp = notification->Animate(hdwp, origin);
|
||||||
|
if(!hdwp) break;
|
||||||
|
keepAnimating |= notification->IsAnimationActive();
|
||||||
|
}
|
||||||
|
if(hdwp) EndDeferWindowPos(hdwp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!keepAnimating)
|
||||||
|
{
|
||||||
|
_ASSERT(hwndController);
|
||||||
|
if(hwndController) KillTimer(hwndController, TimerID_Animate);
|
||||||
|
isAnimating = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Purge dismissed notifications and collapse the stack between
|
||||||
|
// items which are highlighted
|
||||||
|
if(!instances.empty())
|
||||||
|
{
|
||||||
|
auto isAlive = [](ToastInstance& inst) {
|
||||||
|
return inst.hwnd && IsWindowVisible(inst.hwnd);
|
||||||
|
};
|
||||||
|
|
||||||
|
auto isHighlighted = [](ToastInstance& inst) {
|
||||||
|
return inst.hwnd && Toast::Get(inst.hwnd)->IsHighlighted();
|
||||||
|
};
|
||||||
|
|
||||||
|
for(auto it = instances.begin();; ++it)
|
||||||
|
{
|
||||||
|
// find next highlighted item
|
||||||
|
auto it2 = find_if(it, instances.end(), isHighlighted);
|
||||||
|
|
||||||
|
// collapse the stack in front of the highlighted item
|
||||||
|
it = stable_partition(it, it2, isAlive);
|
||||||
|
|
||||||
|
// purge the dead items
|
||||||
|
for_each(it, it2, [this](auto&& inst) { DestroyToast(inst); });
|
||||||
|
|
||||||
|
if(it2 == instances.end())
|
||||||
|
{
|
||||||
|
instances.erase(it, it2);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
it = move(it2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set new toast positions
|
||||||
|
if(!instances.empty())
|
||||||
|
{
|
||||||
|
ScreenMetrics metrics;
|
||||||
|
auto margin = metrics.Y(toastMargin<int>);
|
||||||
|
|
||||||
|
int targetPos = 0;
|
||||||
|
for(auto&& inst : instances)
|
||||||
|
{
|
||||||
|
if(inst.hwnd)
|
||||||
|
{
|
||||||
|
auto toast = Toast::Get(inst.hwnd);
|
||||||
|
|
||||||
|
if(toast->IsHighlighted())
|
||||||
|
targetPos = toast->GetVerticalPosition();
|
||||||
|
else
|
||||||
|
toast->SetVerticalPosition(targetPos);
|
||||||
|
|
||||||
|
targetPos += toast->GetHeight() + margin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new toasts from the queue
|
||||||
|
CheckQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
DesktopNotificationController::Notification DesktopNotificationController::AddNotification(std::wstring caption, std::wstring bodyText, HBITMAP image)
|
||||||
|
{
|
||||||
|
NotificationLink data(this);
|
||||||
|
|
||||||
|
data->caption = move(caption);
|
||||||
|
data->bodyText = move(bodyText);
|
||||||
|
data->image = CopyBitmap(image);
|
||||||
|
|
||||||
|
// Enqueue new notification
|
||||||
|
Notification ret = *queue.insert(queue.end(), move(data));
|
||||||
|
CheckQueue();
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopNotificationController::CloseNotification(Notification& notification)
|
||||||
|
{
|
||||||
|
// Remove it from the queue
|
||||||
|
auto it = find(queue.begin(), queue.end(), notification.data);
|
||||||
|
if(it != queue.end())
|
||||||
|
{
|
||||||
|
queue.erase(it);
|
||||||
|
this->OnNotificationClosed(notification);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dismiss active toast
|
||||||
|
auto hwnd = GetToast(notification.data.get());
|
||||||
|
if(hwnd)
|
||||||
|
{
|
||||||
|
auto toast = Toast::Get(hwnd);
|
||||||
|
toast->Dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopNotificationController::CheckQueue()
|
||||||
|
{
|
||||||
|
while(instances.size() < instances.capacity() && !queue.empty())
|
||||||
|
{
|
||||||
|
CreateToast(move(queue.front()));
|
||||||
|
queue.pop_front();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopNotificationController::CreateToast(NotificationLink&& data)
|
||||||
|
{
|
||||||
|
auto hInstance = RegisterWndClasses();
|
||||||
|
auto hwnd = Toast::Create(hInstance, data);
|
||||||
|
if(hwnd)
|
||||||
|
{
|
||||||
|
int toastPos = 0;
|
||||||
|
if(!instances.empty())
|
||||||
|
{
|
||||||
|
auto& item = instances.back();
|
||||||
|
_ASSERT(item.hwnd);
|
||||||
|
|
||||||
|
ScreenMetrics scr;
|
||||||
|
auto toast = Toast::Get(item.hwnd);
|
||||||
|
toastPos = toast->GetVerticalPosition() + toast->GetHeight() + scr.Y(toastMargin<int>);
|
||||||
|
}
|
||||||
|
|
||||||
|
instances.push_back({ hwnd, move(data) });
|
||||||
|
|
||||||
|
if(!hwndController)
|
||||||
|
{
|
||||||
|
// NOTE: We cannot use a message-only window because we need to receive system notifications
|
||||||
|
hwndController = CreateWindow(className, nullptr, 0, 0, 0, 0, 0, NULL, NULL, hInstance, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto toast = Toast::Get(hwnd);
|
||||||
|
toast->PopUp(toastPos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HWND DesktopNotificationController::GetToast(const NotificationData* data) const
|
||||||
|
{
|
||||||
|
auto it = find_if(instances.cbegin(), instances.cend(), [data](auto&& inst) {
|
||||||
|
auto toast = Toast::Get(inst.hwnd);
|
||||||
|
return data == toast->GetNotification().get();
|
||||||
|
});
|
||||||
|
|
||||||
|
return (it != instances.cend()) ? it->hwnd : NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopNotificationController::DestroyToast(ToastInstance& inst)
|
||||||
|
{
|
||||||
|
if(inst.hwnd)
|
||||||
|
{
|
||||||
|
auto data = Toast::Get(inst.hwnd)->GetNotification();
|
||||||
|
|
||||||
|
DestroyWindow(inst.hwnd);
|
||||||
|
inst.hwnd = NULL;
|
||||||
|
|
||||||
|
Notification notification(data);
|
||||||
|
OnNotificationClosed(notification);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
DesktopNotificationController::Notification::Notification(const shared_ptr<NotificationData>& data) :
|
||||||
|
data(data)
|
||||||
|
{
|
||||||
|
_ASSERT(data != nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DesktopNotificationController::Notification::operator==(const Notification& other) const
|
||||||
|
{
|
||||||
|
return data == other.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopNotificationController::Notification::Close()
|
||||||
|
{
|
||||||
|
// No business calling this when not pointing to a valid instance
|
||||||
|
_ASSERT(data);
|
||||||
|
|
||||||
|
if(data->controller)
|
||||||
|
data->controller->CloseNotification(*this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopNotificationController::Notification::Set(std::wstring caption, std::wstring bodyText, HBITMAP image)
|
||||||
|
{
|
||||||
|
// No business calling this when not pointing to a valid instance
|
||||||
|
_ASSERT(data);
|
||||||
|
|
||||||
|
// Do nothing when the notification has been closed
|
||||||
|
if(!data->controller)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if(data->image) DeleteBitmap(data->image);
|
||||||
|
|
||||||
|
data->caption = move(caption);
|
||||||
|
data->bodyText = move(bodyText);
|
||||||
|
data->image = CopyBitmap(image);
|
||||||
|
|
||||||
|
auto hwnd = data->controller->GetToast(data.get());
|
||||||
|
if(hwnd)
|
||||||
|
{
|
||||||
|
auto toast = Toast::Get(hwnd);
|
||||||
|
toast->ResetContents();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change of contents can affect size and position of all toasts
|
||||||
|
data->controller->StartAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
DesktopNotificationController::NotificationLink::NotificationLink(DesktopNotificationController* controller) :
|
||||||
|
shared_ptr(make_shared<NotificationData>())
|
||||||
|
{
|
||||||
|
get()->controller = controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
DesktopNotificationController::NotificationLink::~NotificationLink()
|
||||||
|
{
|
||||||
|
auto p = get();
|
||||||
|
if(p) p->controller = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
#pragma once
|
||||||
|
#include <deque>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <Windows.h>
|
||||||
|
|
||||||
|
namespace brightray {
|
||||||
|
|
||||||
|
struct NotificationData;
|
||||||
|
|
||||||
|
class DesktopNotificationController
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
DesktopNotificationController(unsigned maximumToasts = 3);
|
||||||
|
~DesktopNotificationController();
|
||||||
|
|
||||||
|
class Notification;
|
||||||
|
Notification AddNotification(std::wstring caption, std::wstring bodyText, HBITMAP image);
|
||||||
|
void CloseNotification(Notification& notification);
|
||||||
|
|
||||||
|
// Event handlers -- override to receive the events
|
||||||
|
private:
|
||||||
|
virtual void OnNotificationClosed(Notification& notification) {}
|
||||||
|
virtual void OnNotificationClicked(Notification& notification) {}
|
||||||
|
virtual void OnNotificationDismissed(Notification& notification) {}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static HINSTANCE RegisterWndClasses();
|
||||||
|
void StartAnimation();
|
||||||
|
HFONT GetCaptionFont();
|
||||||
|
HFONT GetBodyFont();
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum TimerID {
|
||||||
|
TimerID_Animate = 1
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
static constexpr T toastMargin = 20;
|
||||||
|
|
||||||
|
// Wrapper around `NotificationData` which makes sure that
|
||||||
|
// the `controller` member is cleared when the controller object
|
||||||
|
// stops tracking the notification
|
||||||
|
struct NotificationLink : std::shared_ptr<NotificationData> {
|
||||||
|
NotificationLink(DesktopNotificationController* controller);
|
||||||
|
~NotificationLink();
|
||||||
|
|
||||||
|
NotificationLink(NotificationLink&&) = default;
|
||||||
|
NotificationLink(const NotificationLink&) = delete;
|
||||||
|
NotificationLink& operator=(NotificationLink&&) = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ToastInstance {
|
||||||
|
HWND hwnd;
|
||||||
|
NotificationLink data;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Toast;
|
||||||
|
|
||||||
|
static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
|
||||||
|
static DesktopNotificationController* Get(HWND hWnd)
|
||||||
|
{
|
||||||
|
return reinterpret_cast<DesktopNotificationController*>(GetWindowLongPtr(hWnd, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
DesktopNotificationController(const DesktopNotificationController&) = delete;
|
||||||
|
|
||||||
|
void InitializeFonts();
|
||||||
|
void ClearAssets();
|
||||||
|
void AnimateAll();
|
||||||
|
void CheckQueue();
|
||||||
|
void CreateToast(NotificationLink&& data);
|
||||||
|
HWND GetToast(const NotificationData* data) const;
|
||||||
|
void DestroyToast(ToastInstance& inst);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr const TCHAR className[] = TEXT("DesktopNotificationController");
|
||||||
|
HWND hwndController = NULL;
|
||||||
|
HFONT captionFont = NULL, bodyFont = NULL;
|
||||||
|
std::vector<ToastInstance> instances;
|
||||||
|
std::deque<NotificationLink> queue;
|
||||||
|
bool isAnimating = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DesktopNotificationController::Notification
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Notification() = default;
|
||||||
|
Notification(const std::shared_ptr<NotificationData>& data);
|
||||||
|
|
||||||
|
bool operator==(const Notification& other) const;
|
||||||
|
|
||||||
|
void Close();
|
||||||
|
void Set(std::wstring caption, std::wstring bodyText, HBITMAP image);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::shared_ptr<NotificationData> data;
|
||||||
|
|
||||||
|
friend class DesktopNotificationController;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
811
brightray/browser/win/win32_desktop_notifications/toast.cc
Normal file
811
brightray/browser/win/win32_desktop_notifications/toast.cc
Normal file
|
@ -0,0 +1,811 @@
|
||||||
|
#define NOMINMAX
|
||||||
|
#include "toast.h"
|
||||||
|
#include "common.h"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <uxtheme.h>
|
||||||
|
#include <windowsx.h>
|
||||||
|
|
||||||
|
#pragma comment(lib, "msimg32.lib")
|
||||||
|
#pragma comment(lib, "uxtheme.lib")
|
||||||
|
|
||||||
|
using namespace std;
|
||||||
|
|
||||||
|
namespace brightray {
|
||||||
|
|
||||||
|
static COLORREF GetAccentColor()
|
||||||
|
{
|
||||||
|
bool success = false;
|
||||||
|
if(IsAppThemed())
|
||||||
|
{
|
||||||
|
HKEY hkey;
|
||||||
|
if(RegOpenKeyEx(HKEY_CURRENT_USER, TEXT("SOFTWARE\\Microsoft\\Windows\\DWM"), 0, KEY_QUERY_VALUE, &hkey) == ERROR_SUCCESS)
|
||||||
|
{
|
||||||
|
COLORREF color;
|
||||||
|
DWORD type, size;
|
||||||
|
if(RegQueryValueEx(hkey, TEXT("AccentColor"), nullptr, &type, (BYTE*)&color, &(size = sizeof(color))) == ERROR_SUCCESS && type == REG_DWORD)
|
||||||
|
{
|
||||||
|
// convert from RGBA
|
||||||
|
color = RGB(GetRValue(color), GetGValue(color), GetBValue(color));
|
||||||
|
success = true;
|
||||||
|
}
|
||||||
|
else if(RegQueryValueEx(hkey, TEXT("ColorizationColor"), nullptr, &type, (BYTE*)&color, &(size = sizeof(color))) == ERROR_SUCCESS && type == REG_DWORD)
|
||||||
|
{
|
||||||
|
// convert from BGRA
|
||||||
|
color = RGB(GetBValue(color), GetGValue(color), GetRValue(color));
|
||||||
|
success = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
RegCloseKey(hkey);
|
||||||
|
|
||||||
|
if(success) return color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetSysColor(COLOR_ACTIVECAPTION);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stretches a bitmap to the specified size, preserves alpha channel
|
||||||
|
static HBITMAP StretchBitmap(HBITMAP bitmap, unsigned width, unsigned height)
|
||||||
|
{
|
||||||
|
// We use StretchBlt for the scaling, but that discards the alpha channel.
|
||||||
|
// Therefore we first create a separate grayscale bitmap from the alpha channel,
|
||||||
|
// scale that separately and copy it back to the scaled color bitmap.
|
||||||
|
|
||||||
|
BITMAP bm;
|
||||||
|
if(!GetObject(bitmap, sizeof(bm), &bm))
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
if(width == 0 || height == 0)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
HBITMAP resultBitmap = NULL;
|
||||||
|
|
||||||
|
HDC hdcScreen = GetDC(NULL);
|
||||||
|
|
||||||
|
HBITMAP alphaSrcBitmap;
|
||||||
|
{
|
||||||
|
BITMAPINFOHEADER bmi = { sizeof(BITMAPINFOHEADER) };
|
||||||
|
bmi.biWidth = bm.bmWidth;
|
||||||
|
bmi.biHeight = bm.bmHeight;
|
||||||
|
bmi.biPlanes = bm.bmPlanes;
|
||||||
|
bmi.biBitCount = bm.bmBitsPixel;
|
||||||
|
bmi.biCompression = BI_RGB;
|
||||||
|
|
||||||
|
void* alphaSrcBits;
|
||||||
|
alphaSrcBitmap = CreateDIBSection(NULL, (BITMAPINFO*)&bmi, DIB_RGB_COLORS, &alphaSrcBits, NULL, 0);
|
||||||
|
|
||||||
|
if(alphaSrcBitmap)
|
||||||
|
{
|
||||||
|
if(GetDIBits(hdcScreen, bitmap, 0, 0, 0, (BITMAPINFO*)&bmi, DIB_RGB_COLORS) &&
|
||||||
|
bmi.biSizeImage > 0 &&
|
||||||
|
(bmi.biSizeImage % 4) == 0)
|
||||||
|
{
|
||||||
|
auto buf = (DWORD*)_aligned_malloc(bmi.biSizeImage, sizeof(DWORD));
|
||||||
|
if(buf)
|
||||||
|
{
|
||||||
|
GetDIBits(hdcScreen, bitmap, 0, bm.bmHeight, buf, (BITMAPINFO*)&bmi, DIB_RGB_COLORS);
|
||||||
|
|
||||||
|
BYTE* dest = (BYTE*)alphaSrcBits;
|
||||||
|
for(const DWORD *src = buf, *end = (DWORD*)((BYTE*)buf + bmi.biSizeImage);
|
||||||
|
src != end;
|
||||||
|
++src, ++dest)
|
||||||
|
{
|
||||||
|
BYTE a = *src >> 24;
|
||||||
|
*dest++ = a;
|
||||||
|
*dest++ = a;
|
||||||
|
*dest++ = a;
|
||||||
|
}
|
||||||
|
|
||||||
|
_aligned_free(buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(alphaSrcBitmap)
|
||||||
|
{
|
||||||
|
BITMAPINFOHEADER bmi = { sizeof(BITMAPINFOHEADER) };
|
||||||
|
bmi.biWidth = width;
|
||||||
|
bmi.biHeight = height;
|
||||||
|
bmi.biPlanes = 1;
|
||||||
|
bmi.biBitCount = 32;
|
||||||
|
bmi.biCompression = BI_RGB;
|
||||||
|
|
||||||
|
void* colorBits;
|
||||||
|
auto colorBitmap = CreateDIBSection(NULL, (BITMAPINFO*)&bmi, DIB_RGB_COLORS, &colorBits, NULL, 0);
|
||||||
|
|
||||||
|
void* alphaBits;
|
||||||
|
auto alphaBitmap = CreateDIBSection(NULL, (BITMAPINFO*)&bmi, DIB_RGB_COLORS, &alphaBits, NULL, 0);
|
||||||
|
|
||||||
|
HDC hdc = CreateCompatibleDC(NULL);
|
||||||
|
HDC hdcSrc = CreateCompatibleDC(NULL);
|
||||||
|
|
||||||
|
if(colorBitmap && alphaBitmap && hdc && hdcSrc)
|
||||||
|
{
|
||||||
|
SetStretchBltMode(hdc, HALFTONE);
|
||||||
|
|
||||||
|
// resize color channels
|
||||||
|
SelectObject(hdc, colorBitmap);
|
||||||
|
SelectObject(hdcSrc, bitmap);
|
||||||
|
StretchBlt(hdc, 0, 0, width, height, hdcSrc, 0, 0, bm.bmWidth, bm.bmHeight, SRCCOPY);
|
||||||
|
|
||||||
|
// resize alpha channel
|
||||||
|
SelectObject(hdc, alphaBitmap);
|
||||||
|
SelectObject(hdcSrc, alphaSrcBitmap);
|
||||||
|
StretchBlt(hdc, 0, 0, width, height, hdcSrc, 0, 0, bm.bmWidth, bm.bmHeight, SRCCOPY);
|
||||||
|
|
||||||
|
// flush before touching the bits
|
||||||
|
GdiFlush();
|
||||||
|
|
||||||
|
// apply the alpha channel
|
||||||
|
auto dest = (BYTE*)colorBits;
|
||||||
|
auto src = (const BYTE*)alphaBits;
|
||||||
|
auto end = src + (width * height * 4);
|
||||||
|
while(src != end)
|
||||||
|
{
|
||||||
|
dest[3] = src[0];
|
||||||
|
dest += 4;
|
||||||
|
src += 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the resulting bitmap
|
||||||
|
resultBitmap = CreateDIBitmap(hdcScreen, &bmi, CBM_INIT, colorBits, (BITMAPINFO*)&bmi, DIB_RGB_COLORS);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(hdcSrc) DeleteDC(hdcSrc);
|
||||||
|
if(hdc) DeleteDC(hdc);
|
||||||
|
|
||||||
|
if(alphaBitmap) DeleteObject(alphaBitmap);
|
||||||
|
if(colorBitmap) DeleteObject(colorBitmap);
|
||||||
|
|
||||||
|
DeleteObject(alphaSrcBitmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
ReleaseDC(NULL, hdcScreen);
|
||||||
|
|
||||||
|
return resultBitmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
DesktopNotificationController::Toast::Toast(HWND hWnd, shared_ptr<NotificationData>* data) :
|
||||||
|
hWnd(hWnd), data(*data)
|
||||||
|
{
|
||||||
|
HDC hdcScreen = GetDC(NULL);
|
||||||
|
hdc = CreateCompatibleDC(hdcScreen);
|
||||||
|
ReleaseDC(NULL, hdcScreen);
|
||||||
|
}
|
||||||
|
|
||||||
|
DesktopNotificationController::Toast::~Toast()
|
||||||
|
{
|
||||||
|
DeleteDC(hdc);
|
||||||
|
if(bitmap) DeleteBitmap(bitmap);
|
||||||
|
if(scaledImage) DeleteBitmap(scaledImage);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopNotificationController::Toast::Register(HINSTANCE hInstance)
|
||||||
|
{
|
||||||
|
WNDCLASSEX wc = { sizeof(wc) };
|
||||||
|
wc.lpfnWndProc = &Toast::WndProc;
|
||||||
|
wc.lpszClassName = className;
|
||||||
|
wc.cbWndExtra = sizeof(Toast*);
|
||||||
|
wc.hInstance = hInstance;
|
||||||
|
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
|
||||||
|
|
||||||
|
RegisterClassEx(&wc);
|
||||||
|
}
|
||||||
|
|
||||||
|
LRESULT DesktopNotificationController::Toast::WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
||||||
|
{
|
||||||
|
switch(message)
|
||||||
|
{
|
||||||
|
case WM_CREATE:
|
||||||
|
{
|
||||||
|
auto& cs = reinterpret_cast<const CREATESTRUCT*&>(lParam);
|
||||||
|
auto inst = new Toast(hWnd, static_cast<shared_ptr<NotificationData>*>(cs->lpCreateParams));
|
||||||
|
SetWindowLongPtr(hWnd, 0, (LONG_PTR)inst);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WM_NCDESTROY:
|
||||||
|
delete Get(hWnd);
|
||||||
|
SetWindowLongPtr(hWnd, 0, 0);
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
case WM_MOUSEACTIVATE:
|
||||||
|
return MA_NOACTIVATE;
|
||||||
|
|
||||||
|
case WM_TIMER:
|
||||||
|
if(wParam == TimerID_AutoDismiss)
|
||||||
|
{
|
||||||
|
Get(hWnd)->AutoDismiss();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
case WM_LBUTTONDOWN:
|
||||||
|
{
|
||||||
|
auto inst = Get(hWnd);
|
||||||
|
|
||||||
|
inst->Dismiss();
|
||||||
|
|
||||||
|
Notification notification(inst->data);
|
||||||
|
if(inst->isCloseHot)
|
||||||
|
inst->data->controller->OnNotificationDismissed(notification);
|
||||||
|
else
|
||||||
|
inst->data->controller->OnNotificationClicked(notification);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
case WM_MOUSEMOVE:
|
||||||
|
{
|
||||||
|
auto inst = Get(hWnd);
|
||||||
|
if(!inst->isHighlighted)
|
||||||
|
{
|
||||||
|
inst->isHighlighted = true;
|
||||||
|
|
||||||
|
TRACKMOUSEEVENT tme = { sizeof(tme), TME_LEAVE, hWnd };
|
||||||
|
TrackMouseEvent(&tme);
|
||||||
|
}
|
||||||
|
|
||||||
|
POINT cursor = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
|
||||||
|
inst->isCloseHot = (PtInRect(&inst->closeButtonRect, cursor) != FALSE);
|
||||||
|
|
||||||
|
if(!inst->isNonInteractive)
|
||||||
|
inst->CancelDismiss();
|
||||||
|
|
||||||
|
inst->UpdateContents();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
case WM_MOUSELEAVE:
|
||||||
|
{
|
||||||
|
auto inst = Get(hWnd);
|
||||||
|
inst->isHighlighted = false;
|
||||||
|
inst->isCloseHot = false;
|
||||||
|
inst->UpdateContents();
|
||||||
|
|
||||||
|
if(!inst->easeOutActive && inst->easeInPos == 1.0f)
|
||||||
|
inst->ScheduleDismissal();
|
||||||
|
|
||||||
|
// Make sure stack collapse happens if needed
|
||||||
|
inst->data->controller->StartAnimation();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
case WM_WINDOWPOSCHANGED:
|
||||||
|
{
|
||||||
|
auto& wp = reinterpret_cast<WINDOWPOS*&>(lParam);
|
||||||
|
if(wp->flags & SWP_HIDEWINDOW)
|
||||||
|
{
|
||||||
|
if(!IsWindowVisible(hWnd))
|
||||||
|
Get(hWnd)->isHighlighted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DefWindowProc(hWnd, message, wParam, lParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
HWND DesktopNotificationController::Toast::Create(HINSTANCE hInstance, shared_ptr<NotificationData>& data)
|
||||||
|
{
|
||||||
|
return CreateWindowEx(WS_EX_LAYERED | WS_EX_NOACTIVATE | WS_EX_TOPMOST, className, nullptr, WS_POPUP, 0, 0, 0, 0, NULL, NULL, hInstance, &data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopNotificationController::Toast::Draw()
|
||||||
|
{
|
||||||
|
const COLORREF accent = GetAccentColor();
|
||||||
|
|
||||||
|
COLORREF backColor;
|
||||||
|
{
|
||||||
|
// base background color is 2/3 of accent
|
||||||
|
// highlighted adds a bit of intensity to every channel
|
||||||
|
|
||||||
|
int h = isHighlighted ? (0xff / 20) : 0;
|
||||||
|
|
||||||
|
backColor = RGB(min(0xff, (GetRValue(accent) * 2 / 3) + h),
|
||||||
|
min(0xff, (GetGValue(accent) * 2 / 3) + h),
|
||||||
|
min(0xff, (GetBValue(accent) * 2 / 3) + h));
|
||||||
|
}
|
||||||
|
|
||||||
|
const float backLuma =
|
||||||
|
(GetRValue(backColor) * 0.299f / 255) +
|
||||||
|
(GetGValue(backColor) * 0.587f / 255) +
|
||||||
|
(GetBValue(backColor) * 0.114f / 255);
|
||||||
|
|
||||||
|
const struct { float r, g, b; } backF = {
|
||||||
|
GetRValue(backColor) / 255.0f,
|
||||||
|
GetGValue(backColor) / 255.0f,
|
||||||
|
GetBValue(backColor) / 255.0f,
|
||||||
|
};
|
||||||
|
|
||||||
|
COLORREF foreColor, dimmedColor;
|
||||||
|
{
|
||||||
|
// based on the lightness of background, we draw foreground in light or dark
|
||||||
|
// shades of gray blended onto the background with slight transparency
|
||||||
|
// to avoid sharp contrast
|
||||||
|
|
||||||
|
constexpr float alpha = 0.9f;
|
||||||
|
constexpr float intensityLight[] = { (1.0f * alpha), (0.8f * alpha) };
|
||||||
|
constexpr float intensityDark[] = { (0.1f * alpha), (0.3f * alpha) };
|
||||||
|
|
||||||
|
// select foreground intensity values (light or dark)
|
||||||
|
auto& i = (backLuma < 0.6f) ? intensityLight : intensityDark;
|
||||||
|
|
||||||
|
float r, g, b;
|
||||||
|
|
||||||
|
r = i[0] + backF.r * (1 - alpha);
|
||||||
|
g = i[0] + backF.g * (1 - alpha);
|
||||||
|
b = i[0] + backF.b * (1 - alpha);
|
||||||
|
foreColor = RGB(r * 0xff, g * 0xff, b * 0xff);
|
||||||
|
|
||||||
|
r = i[1] + backF.r * (1 - alpha);
|
||||||
|
g = i[1] + backF.g * (1 - alpha);
|
||||||
|
b = i[1] + backF.b * (1 - alpha);
|
||||||
|
dimmedColor = RGB(r * 0xff, g * 0xff, b * 0xff);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw background
|
||||||
|
{
|
||||||
|
auto brush = CreateSolidBrush(backColor);
|
||||||
|
|
||||||
|
RECT rc = { 0, 0, toastSize.cx, toastSize.cy };
|
||||||
|
FillRect(hdc, &rc, brush);
|
||||||
|
|
||||||
|
DeleteBrush(brush);
|
||||||
|
}
|
||||||
|
|
||||||
|
SetBkMode(hdc, TRANSPARENT);
|
||||||
|
|
||||||
|
const auto close = L'\x2715';
|
||||||
|
auto captionFont = data->controller->GetCaptionFont();
|
||||||
|
auto bodyFont = data->controller->GetBodyFont();
|
||||||
|
|
||||||
|
TEXTMETRIC tmCap;
|
||||||
|
SelectFont(hdc, captionFont);
|
||||||
|
GetTextMetrics(hdc, &tmCap);
|
||||||
|
|
||||||
|
auto textOffsetX = margin.cx;
|
||||||
|
|
||||||
|
BITMAP imageInfo = {};
|
||||||
|
if(scaledImage)
|
||||||
|
{
|
||||||
|
GetObject(scaledImage, sizeof(imageInfo), &imageInfo);
|
||||||
|
|
||||||
|
textOffsetX += margin.cx + imageInfo.bmWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate close button rect
|
||||||
|
POINT closePos;
|
||||||
|
{
|
||||||
|
SIZE extent = {};
|
||||||
|
GetTextExtentPoint32W(hdc, &close, 1, &extent);
|
||||||
|
|
||||||
|
closeButtonRect.right = toastSize.cx;
|
||||||
|
closeButtonRect.top = 0;
|
||||||
|
|
||||||
|
closePos.x = closeButtonRect.right - margin.cy - extent.cx;
|
||||||
|
closePos.y = closeButtonRect.top + margin.cy;
|
||||||
|
|
||||||
|
closeButtonRect.left = closePos.x - margin.cy;
|
||||||
|
closeButtonRect.bottom = closePos.y + extent.cy + margin.cy;
|
||||||
|
}
|
||||||
|
|
||||||
|
// image
|
||||||
|
if(scaledImage)
|
||||||
|
{
|
||||||
|
HDC hdcImage = CreateCompatibleDC(NULL);
|
||||||
|
SelectBitmap(hdcImage, scaledImage);
|
||||||
|
BLENDFUNCTION blend = { AC_SRC_OVER, 0, 255, AC_SRC_ALPHA };
|
||||||
|
AlphaBlend(hdc, margin.cx, margin.cy, imageInfo.bmWidth, imageInfo.bmHeight, hdcImage, 0, 0, imageInfo.bmWidth, imageInfo.bmHeight, blend);
|
||||||
|
DeleteDC(hdcImage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// caption
|
||||||
|
{
|
||||||
|
RECT rc = {
|
||||||
|
textOffsetX,
|
||||||
|
margin.cy,
|
||||||
|
closeButtonRect.left,
|
||||||
|
toastSize.cy
|
||||||
|
};
|
||||||
|
|
||||||
|
SelectFont(hdc, captionFont);
|
||||||
|
SetTextColor(hdc, foreColor);
|
||||||
|
DrawText(hdc, data->caption.data(), (UINT)data->caption.length(), &rc, DT_SINGLELINE | DT_END_ELLIPSIS | DT_NOPREFIX);
|
||||||
|
}
|
||||||
|
|
||||||
|
// body text
|
||||||
|
if(!data->bodyText.empty())
|
||||||
|
{
|
||||||
|
RECT rc = {
|
||||||
|
textOffsetX,
|
||||||
|
2 * margin.cy + tmCap.tmAscent,
|
||||||
|
toastSize.cx - margin.cx,
|
||||||
|
toastSize.cy - margin.cy
|
||||||
|
};
|
||||||
|
|
||||||
|
SelectFont(hdc, bodyFont);
|
||||||
|
SetTextColor(hdc, dimmedColor);
|
||||||
|
DrawText(hdc, data->bodyText.data(), (UINT)data->bodyText.length(), &rc, DT_LEFT | DT_WORDBREAK | DT_NOPREFIX | DT_END_ELLIPSIS | DT_EDITCONTROL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// close button
|
||||||
|
{
|
||||||
|
SelectFont(hdc, captionFont);
|
||||||
|
SetTextColor(hdc, isCloseHot ? foreColor : dimmedColor);
|
||||||
|
ExtTextOut(hdc, closePos.x, closePos.y, 0, nullptr, &close, 1, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
isContentUpdated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopNotificationController::Toast::Invalidate()
|
||||||
|
{
|
||||||
|
isContentUpdated = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DesktopNotificationController::Toast::IsRedrawNeeded() const
|
||||||
|
{
|
||||||
|
return !isContentUpdated;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopNotificationController::Toast::UpdateBufferSize()
|
||||||
|
{
|
||||||
|
if(hdc)
|
||||||
|
{
|
||||||
|
SIZE newSize;
|
||||||
|
{
|
||||||
|
TEXTMETRIC tmCap = {};
|
||||||
|
HFONT font = data->controller->GetCaptionFont();
|
||||||
|
if(font)
|
||||||
|
{
|
||||||
|
SelectFont(hdc, font);
|
||||||
|
if(!GetTextMetrics(hdc, &tmCap)) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEXTMETRIC tmBody = {};
|
||||||
|
font = data->controller->GetBodyFont();
|
||||||
|
if(font)
|
||||||
|
{
|
||||||
|
SelectFont(hdc, font);
|
||||||
|
if(!GetTextMetrics(hdc, &tmBody)) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->margin = { tmCap.tmAveCharWidth * 2, tmCap.tmAscent / 2 };
|
||||||
|
|
||||||
|
newSize.cx = margin.cx + (32 * tmCap.tmAveCharWidth) + margin.cx;
|
||||||
|
newSize.cy = margin.cy + (tmCap.tmHeight) + margin.cy;
|
||||||
|
|
||||||
|
if(!data->bodyText.empty())
|
||||||
|
newSize.cy += margin.cy + (3 * tmBody.tmHeight);
|
||||||
|
|
||||||
|
if(data->image)
|
||||||
|
{
|
||||||
|
BITMAP bm;
|
||||||
|
if(GetObject(data->image, sizeof(bm), &bm))
|
||||||
|
{
|
||||||
|
// cap the image size
|
||||||
|
const int maxDimSize = 80;
|
||||||
|
|
||||||
|
auto width = bm.bmWidth;
|
||||||
|
auto height = bm.bmHeight;
|
||||||
|
if(width < height)
|
||||||
|
{
|
||||||
|
if(height > maxDimSize)
|
||||||
|
{
|
||||||
|
width = width * maxDimSize / height;
|
||||||
|
height = maxDimSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if(width > maxDimSize)
|
||||||
|
{
|
||||||
|
height = height * maxDimSize / width;
|
||||||
|
width = maxDimSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ScreenMetrics scr;
|
||||||
|
SIZE imageDrawSize = { scr.X(width), scr.Y(height) };
|
||||||
|
|
||||||
|
newSize.cx += imageDrawSize.cx + margin.cx;
|
||||||
|
|
||||||
|
auto heightWithImage = margin.cy + (imageDrawSize.cy) + margin.cy;
|
||||||
|
if(newSize.cy < heightWithImage) newSize.cy = heightWithImage;
|
||||||
|
|
||||||
|
UpdateScaledImage(imageDrawSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(newSize.cx != this->toastSize.cx || newSize.cy != this->toastSize.cy)
|
||||||
|
{
|
||||||
|
HDC hdcScreen = GetDC(NULL);
|
||||||
|
auto newBitmap = CreateCompatibleBitmap(hdcScreen, newSize.cx, newSize.cy);
|
||||||
|
ReleaseDC(NULL, hdcScreen);
|
||||||
|
|
||||||
|
if(newBitmap)
|
||||||
|
{
|
||||||
|
if(SelectBitmap(hdc, newBitmap))
|
||||||
|
{
|
||||||
|
RECT dirty1 = {}, dirty2 = {};
|
||||||
|
if(toastSize.cx < newSize.cx) dirty1 = { toastSize.cx, 0, newSize.cx, toastSize.cy };
|
||||||
|
if(toastSize.cy < newSize.cy) dirty2 = { 0, toastSize.cy, newSize.cx, newSize.cy };
|
||||||
|
|
||||||
|
if(this->bitmap) DeleteBitmap(this->bitmap);
|
||||||
|
this->bitmap = newBitmap;
|
||||||
|
this->toastSize = newSize;
|
||||||
|
|
||||||
|
Invalidate();
|
||||||
|
|
||||||
|
// Resize also the DWM buffer to prevent flicker during window resizing.
|
||||||
|
// Make sure any existing data is not overwritten by marking the dirty region.
|
||||||
|
{
|
||||||
|
POINT origin = { 0, 0 };
|
||||||
|
|
||||||
|
UPDATELAYEREDWINDOWINFO ulw;
|
||||||
|
ulw.cbSize = sizeof(ulw);
|
||||||
|
ulw.hdcDst = NULL;
|
||||||
|
ulw.pptDst = nullptr;
|
||||||
|
ulw.psize = &toastSize;
|
||||||
|
ulw.hdcSrc = hdc;
|
||||||
|
ulw.pptSrc = &origin;
|
||||||
|
ulw.crKey = 0;
|
||||||
|
ulw.pblend = nullptr;
|
||||||
|
ulw.dwFlags = 0;
|
||||||
|
ulw.prcDirty = &dirty1;
|
||||||
|
auto b1 = UpdateLayeredWindowIndirect(hWnd, &ulw);
|
||||||
|
ulw.prcDirty = &dirty2;
|
||||||
|
auto b2 = UpdateLayeredWindowIndirect(hWnd, &ulw);
|
||||||
|
_ASSERT(b1 && b2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DeleteBitmap(newBitmap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopNotificationController::Toast::UpdateScaledImage(const SIZE& size)
|
||||||
|
{
|
||||||
|
BITMAP bm;
|
||||||
|
if(!GetObject(scaledImage, sizeof(bm), &bm) ||
|
||||||
|
bm.bmWidth != size.cx ||
|
||||||
|
bm.bmHeight != size.cy)
|
||||||
|
{
|
||||||
|
if(scaledImage) DeleteBitmap(scaledImage);
|
||||||
|
scaledImage = StretchBitmap(data->image, size.cx, size.cy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopNotificationController::Toast::UpdateContents()
|
||||||
|
{
|
||||||
|
Draw();
|
||||||
|
|
||||||
|
if(IsWindowVisible(hWnd))
|
||||||
|
{
|
||||||
|
RECT rc;
|
||||||
|
GetWindowRect(hWnd, &rc);
|
||||||
|
POINT origin = { 0, 0 };
|
||||||
|
SIZE size = { rc.right - rc.left, rc.bottom - rc.top };
|
||||||
|
UpdateLayeredWindow(hWnd, NULL, nullptr, &size, hdc, &origin, 0, nullptr, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopNotificationController::Toast::Dismiss()
|
||||||
|
{
|
||||||
|
if(!isNonInteractive)
|
||||||
|
{
|
||||||
|
// Set a flag to prevent further interaction. We don't disable the HWND because
|
||||||
|
// we still want to receive mouse move messages in order to keep the toast under
|
||||||
|
// the cursor and not collapse it while dismissing.
|
||||||
|
isNonInteractive = true;
|
||||||
|
|
||||||
|
AutoDismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopNotificationController::Toast::AutoDismiss()
|
||||||
|
{
|
||||||
|
KillTimer(hWnd, TimerID_AutoDismiss);
|
||||||
|
StartEaseOut();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopNotificationController::Toast::CancelDismiss()
|
||||||
|
{
|
||||||
|
KillTimer(hWnd, TimerID_AutoDismiss);
|
||||||
|
easeOutActive = false;
|
||||||
|
easeOutPos = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopNotificationController::Toast::ScheduleDismissal()
|
||||||
|
{
|
||||||
|
SetTimer(hWnd, TimerID_AutoDismiss, 4000, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopNotificationController::Toast::ResetContents()
|
||||||
|
{
|
||||||
|
if(scaledImage)
|
||||||
|
{
|
||||||
|
DeleteBitmap(scaledImage);
|
||||||
|
scaledImage = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopNotificationController::Toast::PopUp(int y)
|
||||||
|
{
|
||||||
|
verticalPosTarget = verticalPos = y;
|
||||||
|
StartEaseIn();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopNotificationController::Toast::SetVerticalPosition(int y)
|
||||||
|
{
|
||||||
|
// Don't restart animation if current target is the same
|
||||||
|
if(y == verticalPosTarget)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Make sure the new animation's origin is at the current position
|
||||||
|
verticalPos += (int)((verticalPosTarget - verticalPos) * stackCollapsePos);
|
||||||
|
|
||||||
|
// Set new target position and start the animation
|
||||||
|
verticalPosTarget = y;
|
||||||
|
stackCollapseStart = GetTickCount();
|
||||||
|
data->controller->StartAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
HDWP DesktopNotificationController::Toast::Animate(HDWP hdwp, const POINT& origin)
|
||||||
|
{
|
||||||
|
UpdateBufferSize();
|
||||||
|
|
||||||
|
if(IsRedrawNeeded())
|
||||||
|
Draw();
|
||||||
|
|
||||||
|
POINT srcOrigin = { 0, 0 };
|
||||||
|
|
||||||
|
UPDATELAYEREDWINDOWINFO ulw;
|
||||||
|
ulw.cbSize = sizeof(ulw);
|
||||||
|
ulw.hdcDst = NULL;
|
||||||
|
ulw.pptDst = nullptr;
|
||||||
|
ulw.psize = nullptr;
|
||||||
|
ulw.hdcSrc = hdc;
|
||||||
|
ulw.pptSrc = &srcOrigin;
|
||||||
|
ulw.crKey = 0;
|
||||||
|
ulw.pblend = nullptr;
|
||||||
|
ulw.dwFlags = 0;
|
||||||
|
ulw.prcDirty = nullptr;
|
||||||
|
|
||||||
|
POINT pt = { 0, 0 };
|
||||||
|
SIZE size = { 0, 0 };
|
||||||
|
BLENDFUNCTION blend;
|
||||||
|
UINT dwpFlags = SWP_NOACTIVATE | SWP_SHOWWINDOW | SWP_NOREDRAW | SWP_NOCOPYBITS;
|
||||||
|
|
||||||
|
auto easeInPos = AnimateEaseIn();
|
||||||
|
auto easeOutPos = AnimateEaseOut();
|
||||||
|
auto stackCollapsePos = AnimateStackCollapse();
|
||||||
|
|
||||||
|
auto yOffset = (verticalPosTarget - verticalPos) * stackCollapsePos;
|
||||||
|
|
||||||
|
size.cx = (int)(toastSize.cx * easeInPos);
|
||||||
|
size.cy = toastSize.cy;
|
||||||
|
|
||||||
|
pt.x = origin.x - size.cx;
|
||||||
|
pt.y = (int)(origin.y - verticalPos - yOffset - size.cy);
|
||||||
|
|
||||||
|
ulw.pptDst = &pt;
|
||||||
|
ulw.psize = &size;
|
||||||
|
|
||||||
|
if(easeInActive && easeInPos == 1.0f)
|
||||||
|
{
|
||||||
|
easeInActive = false;
|
||||||
|
ScheduleDismissal();
|
||||||
|
}
|
||||||
|
|
||||||
|
this->easeInPos = easeInPos;
|
||||||
|
this->stackCollapsePos = stackCollapsePos;
|
||||||
|
|
||||||
|
if(easeOutPos != this->easeOutPos)
|
||||||
|
{
|
||||||
|
blend.BlendOp = AC_SRC_OVER;
|
||||||
|
blend.BlendFlags = 0;
|
||||||
|
blend.SourceConstantAlpha = (BYTE)(255 * (1.0f - easeOutPos));
|
||||||
|
blend.AlphaFormat = 0;
|
||||||
|
|
||||||
|
ulw.pblend = &blend;
|
||||||
|
ulw.dwFlags = ULW_ALPHA;
|
||||||
|
|
||||||
|
this->easeOutPos = easeOutPos;
|
||||||
|
|
||||||
|
if(easeOutPos == 1.0f)
|
||||||
|
{
|
||||||
|
easeOutActive = false;
|
||||||
|
|
||||||
|
dwpFlags &= ~SWP_SHOWWINDOW;
|
||||||
|
dwpFlags |= SWP_HIDEWINDOW;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(stackCollapsePos == 1.0f)
|
||||||
|
{
|
||||||
|
verticalPos = verticalPosTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
// `UpdateLayeredWindowIndirect` updates position, size, and transparency.
|
||||||
|
// `DeferWindowPos` updates z-order, and also position and size in case ULWI fails,
|
||||||
|
// which can happen when one of the dimensions is zero (e.g. at the beginning of ease-in).
|
||||||
|
|
||||||
|
auto ulwResult = UpdateLayeredWindowIndirect(hWnd, &ulw);
|
||||||
|
hdwp = DeferWindowPos(hdwp, hWnd, HWND_TOPMOST, pt.x, pt.y, size.cx, size.cy, dwpFlags);
|
||||||
|
return hdwp;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopNotificationController::Toast::StartEaseIn()
|
||||||
|
{
|
||||||
|
_ASSERT(!easeInActive);
|
||||||
|
easeInStart = GetTickCount();
|
||||||
|
easeInActive = true;
|
||||||
|
data->controller->StartAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopNotificationController::Toast::StartEaseOut()
|
||||||
|
{
|
||||||
|
_ASSERT(!easeOutActive);
|
||||||
|
easeOutStart = GetTickCount();
|
||||||
|
easeOutActive = true;
|
||||||
|
data->controller->StartAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DesktopNotificationController::Toast::IsStackCollapseActive() const
|
||||||
|
{
|
||||||
|
return (verticalPos != verticalPosTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
float DesktopNotificationController::Toast::AnimateEaseIn()
|
||||||
|
{
|
||||||
|
if(!easeInActive)
|
||||||
|
return easeInPos;
|
||||||
|
|
||||||
|
constexpr float duration = 500.0f;
|
||||||
|
float time = std::min(duration, (float)(GetTickCount() - easeInStart)) / duration;
|
||||||
|
|
||||||
|
// decelerating exponential ease
|
||||||
|
const float a = -8.0f;
|
||||||
|
auto pos = (std::exp(a * time) - 1.0f) / (std::exp(a) - 1.0f);
|
||||||
|
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
float DesktopNotificationController::Toast::AnimateEaseOut()
|
||||||
|
{
|
||||||
|
if(!easeOutActive)
|
||||||
|
return easeOutPos;
|
||||||
|
|
||||||
|
constexpr float duration = 120.0f;
|
||||||
|
float time = std::min(duration, (float)(GetTickCount() - easeOutStart)) / duration;
|
||||||
|
|
||||||
|
// accelerating circle ease
|
||||||
|
auto pos = 1.0f - std::sqrt(1 - time * time);
|
||||||
|
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
float DesktopNotificationController::Toast::AnimateStackCollapse()
|
||||||
|
{
|
||||||
|
if(!IsStackCollapseActive())
|
||||||
|
return stackCollapsePos;
|
||||||
|
|
||||||
|
constexpr float duration = 500.0f;
|
||||||
|
float time = std::min(duration, (float)(GetTickCount() - stackCollapseStart)) / duration;
|
||||||
|
|
||||||
|
// decelerating exponential ease
|
||||||
|
const float a = -8.0f;
|
||||||
|
auto pos = (std::exp(a * time) - 1.0f) / (std::exp(a) - 1.0f);
|
||||||
|
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
99
brightray/browser/win/win32_desktop_notifications/toast.h
Normal file
99
brightray/browser/win/win32_desktop_notifications/toast.h
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
#pragma once
|
||||||
|
#include "desktop_notification_controller.h"
|
||||||
|
|
||||||
|
namespace brightray {
|
||||||
|
|
||||||
|
class DesktopNotificationController::Toast
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static void Register(HINSTANCE hInstance);
|
||||||
|
static HWND Create(HINSTANCE hInstance, std::shared_ptr<NotificationData>& data);
|
||||||
|
static Toast* Get(HWND hWnd)
|
||||||
|
{
|
||||||
|
return reinterpret_cast<Toast*>(GetWindowLongPtr(hWnd, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
|
||||||
|
|
||||||
|
const std::shared_ptr<NotificationData>& GetNotification() const
|
||||||
|
{
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResetContents();
|
||||||
|
|
||||||
|
void Dismiss();
|
||||||
|
|
||||||
|
void PopUp(int y);
|
||||||
|
void SetVerticalPosition(int y);
|
||||||
|
int GetVerticalPosition() const
|
||||||
|
{
|
||||||
|
return verticalPosTarget;
|
||||||
|
}
|
||||||
|
int GetHeight() const
|
||||||
|
{
|
||||||
|
return toastSize.cy;
|
||||||
|
}
|
||||||
|
HDWP Animate(HDWP hdwp, const POINT& origin);
|
||||||
|
bool IsAnimationActive() const
|
||||||
|
{
|
||||||
|
return easeInActive || easeOutActive || IsStackCollapseActive();
|
||||||
|
}
|
||||||
|
bool IsHighlighted() const
|
||||||
|
{
|
||||||
|
_ASSERT(!(isHighlighted && !IsWindowVisible(hWnd)));
|
||||||
|
return isHighlighted;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum TimerID {
|
||||||
|
TimerID_AutoDismiss = 1
|
||||||
|
};
|
||||||
|
|
||||||
|
Toast(HWND hWnd, std::shared_ptr<NotificationData>* data);
|
||||||
|
~Toast();
|
||||||
|
|
||||||
|
void UpdateBufferSize();
|
||||||
|
void UpdateScaledImage(const SIZE& size);
|
||||||
|
void Draw();
|
||||||
|
void Invalidate();
|
||||||
|
bool IsRedrawNeeded() const;
|
||||||
|
void UpdateContents();
|
||||||
|
|
||||||
|
void AutoDismiss();
|
||||||
|
void CancelDismiss();
|
||||||
|
void ScheduleDismissal();
|
||||||
|
|
||||||
|
void StartEaseIn();
|
||||||
|
void StartEaseOut();
|
||||||
|
bool IsStackCollapseActive() const;
|
||||||
|
|
||||||
|
float AnimateEaseIn();
|
||||||
|
float AnimateEaseOut();
|
||||||
|
float AnimateStackCollapse();
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr const TCHAR className[] = TEXT("DesktopNotificationToast");
|
||||||
|
|
||||||
|
const HWND hWnd;
|
||||||
|
HDC hdc;
|
||||||
|
HBITMAP bitmap = NULL;
|
||||||
|
|
||||||
|
const std::shared_ptr<NotificationData> data; // never null
|
||||||
|
|
||||||
|
SIZE toastSize = {};
|
||||||
|
SIZE margin = {};
|
||||||
|
RECT closeButtonRect = {};
|
||||||
|
HBITMAP scaledImage = NULL;
|
||||||
|
|
||||||
|
int verticalPos = 0;
|
||||||
|
int verticalPosTarget = 0;
|
||||||
|
bool isNonInteractive = false;
|
||||||
|
bool easeInActive = false;
|
||||||
|
bool easeOutActive = false;
|
||||||
|
bool isContentUpdated = false, isHighlighted = false, isCloseHot = false;
|
||||||
|
DWORD easeInStart, easeOutStart, stackCollapseStart;
|
||||||
|
float easeInPos = 0, easeOutPos = 0, stackCollapsePos = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
60
brightray/browser/win/win32_notification.cc
Normal file
60
brightray/browser/win/win32_notification.cc
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
#define WIN32_LEAN_AND_MEAN
|
||||||
|
#include "win32_notification.h"
|
||||||
|
#include "third_party/skia/include/core/SkBitmap.h"
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
namespace brightray {
|
||||||
|
|
||||||
|
void Win32Notification::Show(const base::string16& title, const base::string16& msg, const std::string& tag, const GURL& icon_url, const SkBitmap& icon, const bool silent)
|
||||||
|
{
|
||||||
|
auto presenter = static_cast<NotificationPresenterWin7*>(this->presenter());
|
||||||
|
if(!presenter) return;
|
||||||
|
|
||||||
|
HBITMAP image = NULL;
|
||||||
|
|
||||||
|
if(!icon.drawsNothing())
|
||||||
|
{
|
||||||
|
if(icon.colorType() == kBGRA_8888_SkColorType)
|
||||||
|
{
|
||||||
|
icon.lockPixels();
|
||||||
|
|
||||||
|
BITMAPINFOHEADER bmi = { sizeof(BITMAPINFOHEADER) };
|
||||||
|
bmi.biWidth = icon.width();
|
||||||
|
bmi.biHeight = -icon.height();
|
||||||
|
bmi.biPlanes = 1;
|
||||||
|
bmi.biBitCount = 32;
|
||||||
|
bmi.biCompression = BI_RGB;
|
||||||
|
|
||||||
|
HDC hdcScreen = GetDC(NULL);
|
||||||
|
image = CreateDIBitmap(hdcScreen, &bmi, CBM_INIT, icon.getPixels(), (BITMAPINFO*)&bmi, DIB_RGB_COLORS);
|
||||||
|
ReleaseDC(NULL, hdcScreen);
|
||||||
|
|
||||||
|
icon.unlockPixels();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Win32Notification* existing = nullptr;
|
||||||
|
if(!tag.empty()) existing = presenter->GetNotificationObjectByTag(tag);
|
||||||
|
|
||||||
|
if(existing)
|
||||||
|
{
|
||||||
|
existing->tag.clear();
|
||||||
|
this->notificationRef = std::move(existing->notificationRef);
|
||||||
|
this->notificationRef.Set(title, msg, image);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this->notificationRef = presenter->AddNotification(title, msg, image);
|
||||||
|
}
|
||||||
|
|
||||||
|
this->tag = tag;
|
||||||
|
|
||||||
|
if(image) DeleteObject(image);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Win32Notification::Dismiss()
|
||||||
|
{
|
||||||
|
notificationRef.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
30
brightray/browser/win/win32_notification.h
Normal file
30
brightray/browser/win/win32_notification.h
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
#pragma once
|
||||||
|
#include "browser/notification.h"
|
||||||
|
#include "browser/win/notification_presenter_win7.h"
|
||||||
|
|
||||||
|
namespace brightray {
|
||||||
|
|
||||||
|
class Win32Notification : public brightray::Notification
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Win32Notification(NotificationDelegate* delegate, NotificationPresenterWin7* presenter) : Notification(delegate, presenter) {}
|
||||||
|
void Show(const base::string16& title, const base::string16& msg, const std::string& tag, const GURL& icon_url, const SkBitmap& icon, const bool silent) override;
|
||||||
|
void Dismiss() override;
|
||||||
|
|
||||||
|
const DesktopNotificationController::Notification& GetRef() const
|
||||||
|
{
|
||||||
|
return notificationRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& GetTag() const
|
||||||
|
{
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
DISALLOW_COPY_AND_ASSIGN(Win32Notification);
|
||||||
|
DesktopNotificationController::Notification notificationRef;
|
||||||
|
std::string tag;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
|
@ -84,12 +84,21 @@
|
||||||
'browser/linux/libnotify_notification.cc',
|
'browser/linux/libnotify_notification.cc',
|
||||||
'browser/linux/notification_presenter_linux.h',
|
'browser/linux/notification_presenter_linux.h',
|
||||||
'browser/linux/notification_presenter_linux.cc',
|
'browser/linux/notification_presenter_linux.cc',
|
||||||
'browser/win/notification_presenter_win.h',
|
|
||||||
'browser/win/notification_presenter_win.cc',
|
'browser/win/notification_presenter_win.cc',
|
||||||
'browser/win/windows_toast_notification.h',
|
'browser/win/notification_presenter_win.h',
|
||||||
'browser/win/windows_toast_notification.cc',
|
'browser/win/notification_presenter_win7.cc',
|
||||||
'browser/win/scoped_hstring.h',
|
'browser/win/notification_presenter_win7.h',
|
||||||
'browser/win/scoped_hstring.cc',
|
'browser/win/scoped_hstring.cc',
|
||||||
|
'browser/win/scoped_hstring.h',
|
||||||
|
'browser/win/win32_desktop_notifications/common.h',
|
||||||
|
'browser/win/win32_desktop_notifications/desktop_notification_controller.cc',
|
||||||
|
'browser/win/win32_desktop_notifications/desktop_notification_controller.h',
|
||||||
|
'browser/win/win32_desktop_notifications/toast.cc',
|
||||||
|
'browser/win/win32_desktop_notifications/toast.h',
|
||||||
|
'browser/win/win32_notification.cc',
|
||||||
|
'browser/win/win32_notification.h',
|
||||||
|
'browser/win/windows_toast_notification.cc',
|
||||||
|
'browser/win/windows_toast_notification.h',
|
||||||
'browser/special_storage_policy.cc',
|
'browser/special_storage_policy.cc',
|
||||||
'browser/special_storage_policy.h',
|
'browser/special_storage_policy.h',
|
||||||
'browser/url_request_context_getter.cc',
|
'browser/url_request_context_getter.cc',
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue