1 /* vim: se cin sw=2 ts=2 et : */
2 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
3 *
4 * This Source Code Form is subject to the terms of the Mozilla Public
5 * License, v. 2.0. If a copy of the MPL was not distributed with this
6 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7
8 #include "TaskbarPreview.h"
9 #include <nsITaskbarPreviewController.h>
10 #include <windows.h>
11
12 #include <nsError.h>
13 #include <nsCOMPtr.h>
14 #include <nsIWidget.h>
15 #include <nsServiceManagerUtils.h>
16
17 #include "nsUXThemeData.h"
18 #include "nsWindow.h"
19 #include "nsAppShell.h"
20 #include "TaskbarPreviewButton.h"
21 #include "WinUtils.h"
22
23 #include "mozilla/dom/HTMLCanvasElement.h"
24 #include "mozilla/gfx/2D.h"
25 #include "mozilla/gfx/DataSurfaceHelpers.h"
26 #include "mozilla/StaticPrefs_layout.h"
27 #include "mozilla/Telemetry.h"
28
29 // Defined in dwmapi in a header that needs a higher numbered _WINNT #define
30 #ifndef DWM_SIT_DISPLAYFRAME
31 # define DWM_SIT_DISPLAYFRAME 0x1
32 #endif
33
34 namespace mozilla {
35 namespace widget {
36
37 ///////////////////////////////////////////////////////////////////////////////
38 // TaskbarPreview
39
TaskbarPreview(ITaskbarList4 * aTaskbar,nsITaskbarPreviewController * aController,HWND aHWND,nsIDocShell * aShell)40 TaskbarPreview::TaskbarPreview(ITaskbarList4* aTaskbar,
41 nsITaskbarPreviewController* aController,
42 HWND aHWND, nsIDocShell* aShell)
43 : mTaskbar(aTaskbar),
44 mController(aController),
45 mWnd(aHWND),
46 mVisible(false),
47 mDocShell(do_GetWeakReference(aShell)) {}
48
~TaskbarPreview()49 TaskbarPreview::~TaskbarPreview() {
50 // Avoid dangling pointer
51 if (sActivePreview == this) sActivePreview = nullptr;
52
53 // Our subclass should have invoked DetachFromNSWindow already.
54 NS_ASSERTION(
55 !mWnd,
56 "TaskbarPreview::DetachFromNSWindow was not called before destruction");
57
58 // Make sure to release before potentially uninitializing COM
59 mTaskbar = nullptr;
60
61 ::CoUninitialize();
62 }
63
Init()64 nsresult TaskbarPreview::Init() {
65 // TaskbarPreview may outlive the WinTaskbar that created it
66 if (FAILED(::CoInitialize(nullptr))) {
67 return NS_ERROR_NOT_INITIALIZED;
68 }
69
70 WindowHook* hook = GetWindowHook();
71 if (!hook) {
72 return NS_ERROR_NOT_AVAILABLE;
73 }
74 return hook->AddMonitor(WM_DESTROY, MainWindowHook, this);
75 }
76
77 NS_IMETHODIMP
SetController(nsITaskbarPreviewController * aController)78 TaskbarPreview::SetController(nsITaskbarPreviewController* aController) {
79 NS_ENSURE_ARG(aController);
80
81 mController = aController;
82 return NS_OK;
83 }
84
85 NS_IMETHODIMP
GetController(nsITaskbarPreviewController ** aController)86 TaskbarPreview::GetController(nsITaskbarPreviewController** aController) {
87 NS_ADDREF(*aController = mController);
88 return NS_OK;
89 }
90
91 NS_IMETHODIMP
GetTooltip(nsAString & aTooltip)92 TaskbarPreview::GetTooltip(nsAString& aTooltip) {
93 aTooltip = mTooltip;
94 return NS_OK;
95 }
96
97 NS_IMETHODIMP
SetTooltip(const nsAString & aTooltip)98 TaskbarPreview::SetTooltip(const nsAString& aTooltip) {
99 mTooltip = aTooltip;
100 return CanMakeTaskbarCalls() ? UpdateTooltip() : NS_OK;
101 }
102
103 NS_IMETHODIMP
SetVisible(bool visible)104 TaskbarPreview::SetVisible(bool visible) {
105 if (mVisible == visible) return NS_OK;
106 mVisible = visible;
107
108 // If the nsWindow has already been destroyed but the caller is still trying
109 // to use it then just pretend that everything succeeded. The caller doesn't
110 // actually have a way to detect this since it's the same case as when we
111 // CanMakeTaskbarCalls returns false.
112 if (!IsWindowAvailable()) return NS_OK;
113
114 return visible ? Enable() : Disable();
115 }
116
117 NS_IMETHODIMP
GetVisible(bool * visible)118 TaskbarPreview::GetVisible(bool* visible) {
119 *visible = mVisible;
120 return NS_OK;
121 }
122
123 NS_IMETHODIMP
SetActive(bool active)124 TaskbarPreview::SetActive(bool active) {
125 if (active)
126 sActivePreview = this;
127 else if (sActivePreview == this)
128 sActivePreview = nullptr;
129
130 return CanMakeTaskbarCalls() ? ShowActive(active) : NS_OK;
131 }
132
133 NS_IMETHODIMP
GetActive(bool * active)134 TaskbarPreview::GetActive(bool* active) {
135 *active = sActivePreview == this;
136 return NS_OK;
137 }
138
139 NS_IMETHODIMP
Invalidate()140 TaskbarPreview::Invalidate() {
141 if (!mVisible) return NS_OK;
142
143 // DWM Composition is required for previews
144 if (!gfxWindowsPlatform::GetPlatform()->DwmCompositionEnabled()) return NS_OK;
145
146 HWND previewWindow = PreviewWindow();
147 return FAILED(DwmInvalidateIconicBitmaps(previewWindow)) ? NS_ERROR_FAILURE
148 : NS_OK;
149 }
150
UpdateTaskbarProperties()151 nsresult TaskbarPreview::UpdateTaskbarProperties() {
152 nsresult rv = UpdateTooltip();
153
154 // If we are the active preview and our window is the active window, restore
155 // our active state - otherwise some other non-preview window is now active
156 // and should be displayed as so.
157 if (sActivePreview == this) {
158 if (mWnd == ::GetActiveWindow()) {
159 nsresult rvActive = ShowActive(true);
160 if (NS_FAILED(rvActive)) rv = rvActive;
161 } else {
162 sActivePreview = nullptr;
163 }
164 }
165 return rv;
166 }
167
Enable()168 nsresult TaskbarPreview::Enable() {
169 nsresult rv = NS_OK;
170 if (CanMakeTaskbarCalls()) {
171 rv = UpdateTaskbarProperties();
172 } else if (IsWindowAvailable()) {
173 WindowHook* hook = GetWindowHook();
174 MOZ_ASSERT(hook,
175 "IsWindowAvailable() should have eliminated the null case.");
176 hook->AddMonitor(nsAppShell::GetTaskbarButtonCreatedMessage(),
177 MainWindowHook, this);
178 }
179 return rv;
180 }
181
Disable()182 nsresult TaskbarPreview::Disable() {
183 if (!IsWindowAvailable()) {
184 // Window is already destroyed
185 return NS_OK;
186 }
187
188 WindowHook* hook = GetWindowHook();
189 MOZ_ASSERT(hook, "IsWindowAvailable() should have eliminated the null case.");
190 (void)hook->RemoveMonitor(nsAppShell::GetTaskbarButtonCreatedMessage(),
191 MainWindowHook, this);
192
193 return NS_OK;
194 }
195
IsWindowAvailable() const196 bool TaskbarPreview::IsWindowAvailable() const {
197 if (mWnd) {
198 nsWindow* win = WinUtils::GetNSWindowPtr(mWnd);
199 if (win && !win->Destroyed()) {
200 return true;
201 }
202 }
203 return false;
204 }
205
DetachFromNSWindow()206 void TaskbarPreview::DetachFromNSWindow() {
207 if (WindowHook* hook = GetWindowHook()) {
208 hook->RemoveMonitor(WM_DESTROY, MainWindowHook, this);
209 }
210 mWnd = nullptr;
211 }
212
213 LRESULT
WndProc(UINT nMsg,WPARAM wParam,LPARAM lParam)214 TaskbarPreview::WndProc(UINT nMsg, WPARAM wParam, LPARAM lParam) {
215 switch (nMsg) {
216 case WM_DWMSENDICONICTHUMBNAIL: {
217 uint32_t width = HIWORD(lParam);
218 uint32_t height = LOWORD(lParam);
219 float aspectRatio = width / float(height);
220
221 nsresult rv;
222 float preferredAspectRatio;
223 rv = mController->GetThumbnailAspectRatio(&preferredAspectRatio);
224 if (NS_FAILED(rv)) break;
225
226 uint32_t thumbnailWidth = width;
227 uint32_t thumbnailHeight = height;
228
229 if (aspectRatio > preferredAspectRatio) {
230 thumbnailWidth = uint32_t(thumbnailHeight * preferredAspectRatio);
231 } else {
232 thumbnailHeight = uint32_t(thumbnailWidth / preferredAspectRatio);
233 }
234
235 DrawBitmap(thumbnailWidth, thumbnailHeight, false);
236 } break;
237 case WM_DWMSENDICONICLIVEPREVIEWBITMAP: {
238 uint32_t width, height;
239 nsresult rv;
240 rv = mController->GetWidth(&width);
241 if (NS_FAILED(rv)) break;
242 rv = mController->GetHeight(&height);
243 if (NS_FAILED(rv)) break;
244
245 double scale = StaticPrefs::layout_css_devPixelsPerPx();
246 if (scale <= 0.0) {
247 scale = WinUtils::LogToPhysFactor(PreviewWindow());
248 }
249 DrawBitmap(NSToIntRound(scale * width), NSToIntRound(scale * height),
250 true);
251 } break;
252 }
253 return ::DefWindowProcW(PreviewWindow(), nMsg, wParam, lParam);
254 }
255
CanMakeTaskbarCalls()256 bool TaskbarPreview::CanMakeTaskbarCalls() {
257 // If the nsWindow has already been destroyed and we know it but our caller
258 // clearly doesn't so we can't make any calls.
259 if (!mWnd) return false;
260 // Certain functions like SetTabOrder seem to require a visible window. During
261 // window close, the window seems to be hidden before being destroyed.
262 if (!::IsWindowVisible(mWnd)) return false;
263 if (mVisible) {
264 nsWindow* window = WinUtils::GetNSWindowPtr(mWnd);
265 NS_ASSERTION(window, "Could not get nsWindow from HWND");
266 return window ? window->HasTaskbarIconBeenCreated() : false;
267 }
268 return false;
269 }
270
GetWindowHook()271 WindowHook* TaskbarPreview::GetWindowHook() {
272 nsWindow* window = WinUtils::GetNSWindowPtr(mWnd);
273 NS_ASSERTION(window, "Cannot use taskbar previews in an embedded context!");
274
275 return window ? &window->GetWindowHook() : nullptr;
276 }
277
EnableCustomDrawing(HWND aHWND,bool aEnable)278 void TaskbarPreview::EnableCustomDrawing(HWND aHWND, bool aEnable) {
279 BOOL enabled = aEnable;
280 DwmSetWindowAttribute(aHWND, DWMWA_FORCE_ICONIC_REPRESENTATION, &enabled,
281 sizeof(enabled));
282
283 DwmSetWindowAttribute(aHWND, DWMWA_HAS_ICONIC_BITMAP, &enabled,
284 sizeof(enabled));
285 }
286
UpdateTooltip()287 nsresult TaskbarPreview::UpdateTooltip() {
288 NS_ASSERTION(CanMakeTaskbarCalls() && mVisible,
289 "UpdateTooltip called on invisible tab preview");
290
291 if (FAILED(mTaskbar->SetThumbnailTooltip(PreviewWindow(), mTooltip.get())))
292 return NS_ERROR_FAILURE;
293 return NS_OK;
294 }
295
DrawBitmap(uint32_t width,uint32_t height,bool isPreview)296 void TaskbarPreview::DrawBitmap(uint32_t width, uint32_t height,
297 bool isPreview) {
298 nsresult rv;
299 nsCOMPtr<nsITaskbarPreviewCallback> callback =
300 do_CreateInstance("@mozilla.org/widget/taskbar-preview-callback;1", &rv);
301 if (NS_FAILED(rv)) {
302 return;
303 }
304
305 ((TaskbarPreviewCallback*)callback.get())->SetPreview(this);
306
307 if (isPreview) {
308 ((TaskbarPreviewCallback*)callback.get())->SetIsPreview();
309 mController->RequestPreview(callback);
310 } else {
311 mController->RequestThumbnail(callback, width, height);
312 }
313 }
314
315 ///////////////////////////////////////////////////////////////////////////////
316 // TaskbarPreviewCallback
317
NS_IMPL_ISUPPORTS(TaskbarPreviewCallback,nsITaskbarPreviewCallback)318 NS_IMPL_ISUPPORTS(TaskbarPreviewCallback, nsITaskbarPreviewCallback)
319
320 /* void done (in nsISupports aCanvas, in boolean aDrawBorder); */
321 NS_IMETHODIMP
322 TaskbarPreviewCallback::Done(nsISupports* aCanvas, bool aDrawBorder) {
323 // We create and destroy TaskbarTabPreviews from front end code in response
324 // to TabOpen and TabClose events. Each TaskbarTabPreview creates and owns a
325 // proxy HWND which it hands to Windows as a tab identifier. When a tab
326 // closes, TaskbarTabPreview Disable() method is called by front end, which
327 // destroys the proxy window and clears mProxyWindow which is the HWND
328 // returned from PreviewWindow(). So, since this is async, we should check to
329 // be sure the tab is still alive before doing all this gfx work and making
330 // dwm calls. To accomplish this we check the result of PreviewWindow().
331 if (!aCanvas || !mPreview || !mPreview->PreviewWindow() ||
332 !mPreview->IsWindowAvailable()) {
333 return NS_ERROR_FAILURE;
334 }
335
336 nsCOMPtr<nsIContent> content(do_QueryInterface(aCanvas));
337 auto canvas = dom::HTMLCanvasElement::FromNodeOrNull(content);
338 if (!canvas) {
339 return NS_ERROR_FAILURE;
340 }
341
342 RefPtr<gfx::SourceSurface> source = canvas->GetSurfaceSnapshot();
343 if (!source) {
344 return NS_ERROR_FAILURE;
345 }
346 RefPtr<gfxWindowsSurface> target = new gfxWindowsSurface(
347 source->GetSize(), gfx::SurfaceFormat::A8R8G8B8_UINT32);
348 if (!target) {
349 return NS_ERROR_FAILURE;
350 }
351
352 RefPtr<gfx::DataSourceSurface> srcSurface = source->GetDataSurface();
353 RefPtr<gfxImageSurface> imageSurface = target->GetAsImageSurface();
354 if (!srcSurface || !imageSurface) {
355 return NS_ERROR_FAILURE;
356 }
357
358 gfx::DataSourceSurface::MappedSurface sourceMap;
359 srcSurface->Map(gfx::DataSourceSurface::READ, &sourceMap);
360 mozilla::gfx::CopySurfaceDataToPackedArray(
361 sourceMap.mData, imageSurface->Data(), srcSurface->GetSize(),
362 sourceMap.mStride, BytesPerPixel(srcSurface->GetFormat()));
363 srcSurface->Unmap();
364
365 HDC hDC = target->GetDC();
366 HBITMAP hBitmap = (HBITMAP)GetCurrentObject(hDC, OBJ_BITMAP);
367
368 DWORD flags = aDrawBorder ? DWM_SIT_DISPLAYFRAME : 0;
369 POINT pptClient = {0, 0};
370 HRESULT hr;
371 if (!mIsThumbnail) {
372 hr = DwmSetIconicLivePreviewBitmap(mPreview->PreviewWindow(), hBitmap,
373 &pptClient, flags);
374 } else {
375 hr = DwmSetIconicThumbnail(mPreview->PreviewWindow(), hBitmap, flags);
376 }
377 MOZ_ASSERT(SUCCEEDED(hr));
378 return NS_OK;
379 }
380
381 /* static */
MainWindowHook(void * aContext,HWND hWnd,UINT nMsg,WPARAM wParam,LPARAM lParam,LRESULT * aResult)382 bool TaskbarPreview::MainWindowHook(void* aContext, HWND hWnd, UINT nMsg,
383 WPARAM wParam, LPARAM lParam,
384 LRESULT* aResult) {
385 NS_ASSERTION(nMsg == nsAppShell::GetTaskbarButtonCreatedMessage() ||
386 nMsg == WM_DESTROY,
387 "Window hook proc called with wrong message");
388 NS_ASSERTION(aContext, "Null context in MainWindowHook");
389 if (!aContext) return false;
390 TaskbarPreview* preview = reinterpret_cast<TaskbarPreview*>(aContext);
391 if (nMsg == WM_DESTROY) {
392 // nsWindow is being destroyed
393 // We can't really do anything at this point including removing hooks
394 return false;
395 } else {
396 nsWindow* window = WinUtils::GetNSWindowPtr(preview->mWnd);
397 if (window) {
398 window->SetHasTaskbarIconBeenCreated();
399
400 if (preview->mVisible) preview->UpdateTaskbarProperties();
401 }
402 }
403 return false;
404 }
405
406 TaskbarPreview* TaskbarPreview::sActivePreview = nullptr;
407
408 } // namespace widget
409 } // namespace mozilla
410