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