diff options
Diffstat (limited to '')
-rw-r--r-- | widget/windows/SystemStatusBar.cpp | 339 |
1 files changed, 339 insertions, 0 deletions
diff --git a/widget/windows/SystemStatusBar.cpp b/widget/windows/SystemStatusBar.cpp new file mode 100644 index 0000000000..d179b18f4c --- /dev/null +++ b/widget/windows/SystemStatusBar.cpp @@ -0,0 +1,339 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sts=2 sw=2 et cin: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <strsafe.h> +#include "SystemStatusBar.h" + +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Element.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/LinkedList.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/widget/IconLoader.h" +#include "mozilla/dom/XULButtonElement.h" +#include "nsComputedDOMStyle.h" +#include "nsIContentPolicy.h" +#include "nsISupports.h" +#include "nsMenuPopupFrame.h" +#include "nsXULPopupManager.h" +#include "nsIDocShell.h" +#include "nsDocShell.h" +#include "nsWindowGfx.h" + +#include "shellapi.h" + +namespace mozilla::widget { + +using mozilla::LinkedListElement; +using mozilla::dom::Element; + +class StatusBarEntry final : public LinkedListElement<RefPtr<StatusBarEntry>>, + public IconLoader::Listener, + public nsISupports { + public: + explicit StatusBarEntry(Element* aMenu); + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(StatusBarEntry) + nsresult Init(); + void Destroy(); + + MOZ_CAN_RUN_SCRIPT LRESULT OnMessage(HWND hWnd, UINT msg, WPARAM wp, + LPARAM lp); + const Element* GetMenu() { return mMenu; }; + + nsresult OnComplete(imgIContainer* aImage) override; + + private: + ~StatusBarEntry(); + RefPtr<mozilla::widget::IconLoader> mIconLoader; + // Effectively const but is cycle collected + MOZ_KNOWN_LIVE RefPtr<Element> mMenu; + NOTIFYICONDATAW mIconData; + boolean mInitted; +}; + +NS_IMPL_CYCLE_COLLECTION_CLASS(StatusBarEntry) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(StatusBarEntry) + tmp->Destroy(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(StatusBarEntry) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mIconLoader) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMenu) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(StatusBarEntry) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(StatusBarEntry) +NS_IMPL_CYCLE_COLLECTING_RELEASE(StatusBarEntry) + +StatusBarEntry::StatusBarEntry(Element* aMenu) : mMenu(aMenu), mInitted(false) { + mIconData = {/* cbSize */ sizeof(NOTIFYICONDATA), + /* hWnd */ 0, + /* uID */ 2, + /* uFlags */ NIF_ICON | NIF_MESSAGE | NIF_TIP | NIF_SHOWTIP, + /* uCallbackMessage */ WM_USER, + /* hIcon */ 0, + /* szTip */ L"", // This is updated in Init() + /* dwState */ 0, + /* dwStateMask */ 0, + /* szInfo */ L"", + /* uVersion */ {NOTIFYICON_VERSION_4}, + /* szInfoTitle */ L"", + /* dwInfoFlags */ 0}; + MOZ_ASSERT(mMenu); +} + +StatusBarEntry::~StatusBarEntry() { + if (!mInitted) { + return; + } + Destroy(); + ::Shell_NotifyIconW(NIM_DELETE, &mIconData); + VERIFY(::DestroyWindow(mIconData.hWnd)); +} + +void StatusBarEntry::Destroy() { + if (mIconLoader) { + mIconLoader->Destroy(); + mIconLoader = nullptr; + } +} + +nsresult StatusBarEntry::Init() { + MOZ_ASSERT(NS_IsMainThread()); + + // First, look at the content node's "image" attribute. + nsAutoString imageURIString; + bool hasImageAttr = mMenu->GetAttr(nsGkAtoms::image, imageURIString); + + nsresult rv; + nsCOMPtr<nsIURI> iconURI; + if (!hasImageAttr) { + // If the content node has no "image" attribute, get the + // "list-style-image" property from CSS. + RefPtr<mozilla::dom::Document> document = mMenu->GetComposedDoc(); + if (!document) { + return NS_ERROR_FAILURE; + } + + RefPtr<const ComputedStyle> sc = + nsComputedDOMStyle::GetComputedStyle(mMenu); + if (!sc) { + return NS_ERROR_FAILURE; + } + + iconURI = sc->StyleList()->GetListStyleImageURI(); + } else { + uint64_t dummy = 0; + nsContentPolicyType policyType; + nsCOMPtr<nsIPrincipal> triggeringPrincipal = mMenu->NodePrincipal(); + nsContentUtils::GetContentPolicyTypeForUIImageLoading( + mMenu, getter_AddRefs(triggeringPrincipal), policyType, &dummy); + if (policyType != nsIContentPolicy::TYPE_INTERNAL_IMAGE) { + return NS_ERROR_ILLEGAL_VALUE; + } + + // If this menu item shouldn't have an icon, the string will be empty, + // and NS_NewURI will fail. + rv = NS_NewURI(getter_AddRefs(iconURI), imageURIString); + if (NS_FAILED(rv)) return rv; + } + + mIconLoader = new IconLoader(this); + + if (iconURI) { + rv = mIconLoader->LoadIcon(iconURI, mMenu); + } + + HWND iconWindow; + NS_ENSURE_TRUE(iconWindow = ::CreateWindowExW( + /* extended style */ 0, + /* className */ L"IconWindowClass", + /* title */ 0, + /* style */ WS_CAPTION, + /* x, y, cx, cy */ 0, 0, 0, 0, + /* parent */ 0, + /* menu */ 0, + /* instance */ 0, + /* create struct */ 0), + NS_ERROR_FAILURE); + ::SetWindowLongPtr(iconWindow, GWLP_USERDATA, (LONG_PTR)this); + + mIconData.hWnd = iconWindow; + mIconData.hIcon = ::LoadIcon(::GetModuleHandle(NULL), IDI_APPLICATION); + + nsAutoString labelAttr; + mMenu->GetAttr(kNameSpaceID_None, nsGkAtoms::label, labelAttr); + const nsString& label = PromiseFlatString(labelAttr); + + size_t destLength = sizeof mIconData.szTip / (sizeof mIconData.szTip[0]); + wchar_t* tooltip = &(mIconData.szTip[0]); + ::StringCchCopyNW(tooltip, destLength, label.get(), label.Length()); + + ::Shell_NotifyIconW(NIM_ADD, &mIconData); + ::Shell_NotifyIconW(NIM_SETVERSION, &mIconData); + + mInitted = true; + return NS_OK; +} + +nsresult StatusBarEntry::OnComplete(imgIContainer* aImage) { + NS_ENSURE_ARG_POINTER(aImage); + + RefPtr<StatusBarEntry> kungFuDeathGrip = this; + + nsresult rv = nsWindowGfx::CreateIcon( + aImage, false, LayoutDeviceIntPoint(), + nsWindowGfx::GetIconMetrics(nsWindowGfx::kRegularIcon), &mIconData.hIcon); + NS_ENSURE_SUCCESS(rv, rv); + + ::Shell_NotifyIconW(NIM_MODIFY, &mIconData); + + if (mIconData.hIcon) { + ::DestroyIcon(mIconData.hIcon); + mIconData.hIcon = nullptr; + } + + // To simplify things, we won't react to CSS changes to update the icon + // with this implementation. We can get rid of the IconLoader at this point. + mIconLoader->Destroy(); + mIconLoader = nullptr; + return NS_OK; +} + +LRESULT StatusBarEntry::OnMessage(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp) { + if (msg == WM_USER && + (LOWORD(lp) == NIN_SELECT || LOWORD(lp) == NIN_KEYSELECT || + LOWORD(lp) == WM_CONTEXTMENU)) { + auto* menu = dom::XULButtonElement::FromNode(mMenu); + if (!menu) { + return TRUE; + } + + nsMenuPopupFrame* popupFrame = menu->GetMenuPopup(FlushType::None); + if (NS_WARN_IF(!popupFrame)) { + return TRUE; + } + + nsIWidget* widget = popupFrame->GetNearestWidget(); + MOZ_DIAGNOSTIC_ASSERT(widget); + if (!widget) { + return TRUE; + } + + HWND win = static_cast<HWND>(widget->GetNativeData(NS_NATIVE_WINDOW)); + MOZ_DIAGNOSTIC_ASSERT(win); + if (!win) { + return TRUE; + } + + if (LOWORD(lp) == NIN_KEYSELECT && ::GetForegroundWindow() == win) { + // When enter is pressed on the icon, the shell sends two NIN_KEYSELECT + // notifications. This might cause us to open two windows. To work around + // this, if we're already the foreground window (which happens below), + // ignore this notification. + return TRUE; + } + + if (LOWORD(lp) != WM_CONTEXTMENU && + mMenu->HasAttr(kNameSpaceID_None, nsGkAtoms::contextmenu)) { + ::SetForegroundWindow(win); + nsEventStatus status = nsEventStatus_eIgnore; + WidgetMouseEvent event(true, eXULSystemStatusBarClick, nullptr, + WidgetMouseEvent::eReal); + RefPtr<nsPresContext> presContext = popupFrame->PresContext(); + EventDispatcher::Dispatch(mMenu, presContext, &event, nullptr, &status); + return DefWindowProc(hWnd, msg, wp, lp); + } + + nsPresContext* pc = popupFrame->PresContext(); + const CSSIntPoint point = gfx::RoundedToInt( + LayoutDeviceIntPoint(GET_X_LPARAM(wp), GET_Y_LPARAM(wp)) / + pc->CSSToDevPixelScale()); + + // The menu that is being opened is a Gecko <xul:menu>, and the popup code + // that manages it expects that the window that the <xul:menu> belongs to + // will be in the foreground when it opens. If we don't do this, then if the + // icon is clicked when the window is _not_ in the foreground, then the + // opened menu will not be keyboard focusable, nor will it close on its own + // if the user clicks away from the menu (at least, not until the user + // focuses any window in the parent process). + ::SetForegroundWindow(win); + nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); + pm->ShowPopupAtScreen(popupFrame->GetContent()->AsElement(), point.x, + point.y, false, nullptr); + } + + return DefWindowProc(hWnd, msg, wp, lp); +} + +NS_IMPL_ISUPPORTS(SystemStatusBar, nsISystemStatusBar) + +MOZ_CAN_RUN_SCRIPT static LRESULT CALLBACK WindowProc(HWND hWnd, UINT msg, + WPARAM wp, LPARAM lp) { + if (RefPtr<StatusBarEntry> entry = + (StatusBarEntry*)GetWindowLongPtr(hWnd, GWLP_USERDATA)) { + return entry->OnMessage(hWnd, msg, wp, lp); + } + return TRUE; +} + +static StaticRefPtr<SystemStatusBar> sSingleton; + +SystemStatusBar& SystemStatusBar::GetSingleton() { + if (!sSingleton) { + sSingleton = new SystemStatusBar(); + ClearOnShutdown(&sSingleton); + } + return *sSingleton; +} + +already_AddRefed<SystemStatusBar> SystemStatusBar::GetAddRefedSingleton() { + RefPtr<SystemStatusBar> sm = &GetSingleton(); + return sm.forget(); +} + +nsresult SystemStatusBar::Init() { + WNDCLASS classStruct = {/* style */ 0, + /* lpfnWndProc */ &WindowProc, + /* cbClsExtra */ 0, + /* cbWndExtra */ 0, + /* hInstance */ 0, + /* hIcon */ 0, + /* hCursor */ 0, + /* hbrBackground */ 0, + /* lpszMenuName */ 0, + /* lpszClassName */ L"IconWindowClass"}; + NS_ENSURE_TRUE(::RegisterClass(&classStruct), NS_ERROR_FAILURE); + return NS_OK; +} + +NS_IMETHODIMP +SystemStatusBar::AddItem(Element* aElement) { + RefPtr<StatusBarEntry> entry = new StatusBarEntry(aElement); + nsresult rv = entry->Init(); + NS_ENSURE_SUCCESS(rv, rv); + + mStatusBarEntries.insertBack(entry); + return NS_OK; +} + +NS_IMETHODIMP +SystemStatusBar::RemoveItem(Element* aElement) { + for (StatusBarEntry* entry : mStatusBarEntries) { + if (entry->GetMenu() == aElement) { + entry->removeFrom(mStatusBarEntries); + return NS_OK; + } + } + return NS_ERROR_NOT_AVAILABLE; +} + +} // namespace mozilla::widget |