From 9714e6ee6388f5715301f44495aca4ca84e87873 Mon Sep 17 00:00:00 2001 From: Ken Thomases Date: Sun, 17 Mar 2013 22:40:54 -0500 Subject: [PATCH] winemac: Implement rudimentary support for system tray icons as Mac status items. --- dlls/winemac.drv/Makefile.in | 2 + dlls/winemac.drv/cocoa_status_item.m | 175 +++++++++++++ dlls/winemac.drv/event.c | 5 + dlls/winemac.drv/image.c | 2 +- dlls/winemac.drv/macdrv.h | 3 + dlls/winemac.drv/macdrv_cocoa.h | 13 + dlls/winemac.drv/systray.c | 358 +++++++++++++++++++++++++++ dlls/winemac.drv/winemac.drv.spec | 3 + 8 files changed, 560 insertions(+), 1 deletion(-) create mode 100644 dlls/winemac.drv/cocoa_status_item.m create mode 100644 dlls/winemac.drv/systray.c diff --git a/dlls/winemac.drv/Makefile.in b/dlls/winemac.drv/Makefile.in index 9cb2c462fa..6bbac42227 100644 --- a/dlls/winemac.drv/Makefile.in +++ b/dlls/winemac.drv/Makefile.in @@ -16,6 +16,7 @@ C_SRCS = \ opengl.c \ scroll.c \ surface.c \ + systray.c \ window.c OBJC_SRCS = \ @@ -25,6 +26,7 @@ OBJC_SRCS = \ cocoa_event.m \ cocoa_main.m \ cocoa_opengl.m \ + cocoa_status_item.m \ cocoa_window.m @MAKE_DLL_RULES@ diff --git a/dlls/winemac.drv/cocoa_status_item.m b/dlls/winemac.drv/cocoa_status_item.m new file mode 100644 index 0000000000..1725abac57 --- /dev/null +++ b/dlls/winemac.drv/cocoa_status_item.m @@ -0,0 +1,175 @@ +/* + * MACDRV Cocoa status item class + * + * Copyright 2011, 2012, 2013 Ken Thomases for CodeWeavers Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#import +#include "macdrv_cocoa.h" +#import "cocoa_app.h" +#import "cocoa_event.h" + + +@interface WineStatusItem : NSObject +{ + NSStatusItem* item; + WineEventQueue* queue; +} + +@property (retain, nonatomic) NSStatusItem* item; +@property (assign, nonatomic) WineEventQueue* queue; + +@end + + +@implementation WineStatusItem + +@synthesize item, queue; + + - (id) initWithEventQueue:(WineEventQueue*)inQueue + { + self = [super init]; + if (self) + { + NSStatusBar* statusBar = [NSStatusBar systemStatusBar]; + item = [[statusBar statusItemWithLength:NSSquareStatusItemLength] retain]; + [item setTarget:self]; + [item setAction:@selector(clicked:)]; + [item setDoubleAction:@selector(doubleClicked:)]; + + queue = inQueue; + } + return self; + } + + - (void) dealloc + { + if (item) + { + NSStatusBar* statusBar = [NSStatusBar systemStatusBar]; + [statusBar removeStatusItem:item]; + [item release]; + } + [super dealloc]; + } + + - (void) removeFromStatusBar + { + if (item) + { + NSStatusBar* statusBar = [NSStatusBar systemStatusBar]; + [statusBar removeStatusItem:item]; + self.item = nil; + } + } + + - (void) postClickedEventWithCount:(int)count + { + macdrv_event event; + event.type = STATUS_ITEM_CLICKED; + event.window = NULL; + event.status_item_clicked.item = (macdrv_status_item)self; + event.status_item_clicked.count = count; + [queue postEvent:&event]; + } + + - (void) clicked:(id)sender + { + [self postClickedEventWithCount:1]; + } + + - (void) doubleClicked:(id)sender + { + [self postClickedEventWithCount:2]; + } + +@end + + +/*********************************************************************** + * macdrv_create_status_item + * + * Creates a new status item in the status bar. + */ +macdrv_status_item macdrv_create_status_item(macdrv_event_queue q) +{ + WineEventQueue* queue = (WineEventQueue*)q; + __block WineStatusItem* statusItem; + + OnMainThread(^{ + statusItem = [[WineStatusItem alloc] initWithEventQueue:queue]; + }); + + return (macdrv_status_item)statusItem; +} + +/*********************************************************************** + * macdrv_destroy_status_item + * + * Removes a status item previously returned by + * macdrv_create_status_item() from the status bar and destroys it. + */ +void macdrv_destroy_status_item(macdrv_status_item s) +{ + WineStatusItem* statusItem = (WineStatusItem*)s; + + OnMainThreadAsync(^{ + [statusItem removeFromStatusBar]; + [statusItem release]; + }); +} + +/*********************************************************************** + * macdrv_set_status_item_image + * + * Sets the image for a status item. If cgimage is NULL, clears the + * image of the status item (leaving it a blank spot on the menu bar). + */ +void macdrv_set_status_item_image(macdrv_status_item s, CGImageRef cgimage) +{ + WineStatusItem* statusItem = (WineStatusItem*)s; + + CGImageRetain(cgimage); + + OnMainThreadAsync(^{ + NSImage* image = nil; + if (cgimage) + { + image = [[NSImage alloc] initWithCGImage:cgimage size:NSZeroSize]; + CGImageRelease(cgimage); + } + [statusItem.item setImage:image]; + [image release]; + }); +} + +/*********************************************************************** + * macdrv_set_status_item_tooltip + * + * Sets the tooltip string for a status item. If cftip is NULL, clears + * the tooltip string for the status item. + */ +void macdrv_set_status_item_tooltip(macdrv_status_item s, CFStringRef cftip) +{ + WineStatusItem* statusItem = (WineStatusItem*)s; + NSString* tip = (NSString*)cftip; + + if (![tip length]) tip = nil; + OnMainThreadAsync(^{ + [statusItem.item setToolTip:tip]; + }); +} diff --git a/dlls/winemac.drv/event.c b/dlls/winemac.drv/event.c index a4161a4e04..4f4d2471cd 100644 --- a/dlls/winemac.drv/event.c +++ b/dlls/winemac.drv/event.c @@ -42,6 +42,7 @@ static const char *dbgstr_event(int type) "MOUSE_MOVED_ABSOLUTE", "MOUSE_SCROLL", "QUERY_EVENT", + "STATUS_ITEM_CLICKED", "WINDOW_CLOSE_REQUESTED", "WINDOW_DID_MINIMIZE", "WINDOW_DID_UNMINIMIZE", @@ -87,6 +88,7 @@ static macdrv_event_mask get_event_mask(DWORD mask) { event_mask |= event_mask_for_type(APP_DEACTIVATED); event_mask |= event_mask_for_type(DISPLAYS_CHANGED); + event_mask |= event_mask_for_type(STATUS_ITEM_CLICKED); event_mask |= event_mask_for_type(WINDOW_CLOSE_REQUESTED); event_mask |= event_mask_for_type(WINDOW_DID_MINIMIZE); event_mask |= event_mask_for_type(WINDOW_DID_UNMINIMIZE); @@ -186,6 +188,9 @@ void macdrv_handle_event(macdrv_event *event) case QUERY_EVENT: macdrv_query_event(hwnd, event); break; + case STATUS_ITEM_CLICKED: + macdrv_status_item_clicked(event); + break; case WINDOW_CLOSE_REQUESTED: macdrv_window_close_requested(hwnd); break; diff --git a/dlls/winemac.drv/image.c b/dlls/winemac.drv/image.c index 24c49e968c..422826569a 100644 --- a/dlls/winemac.drv/image.c +++ b/dlls/winemac.drv/image.c @@ -181,7 +181,7 @@ CGImageRef create_cgimage_from_icon_bitmaps(HDC hdc, HANDLE icon, HBITMAP hbmCol * * Create a CGImage from a Windows icon. */ -static CGImageRef create_cgimage_from_icon(HANDLE icon, int width, int height) +CGImageRef create_cgimage_from_icon(HANDLE icon, int width, int height) { CGImageRef ret = NULL; HDC hdc; diff --git a/dlls/winemac.drv/macdrv.h b/dlls/winemac.drv/macdrv.h index 585112945a..d4cbe1dcce 100644 --- a/dlls/winemac.drv/macdrv.h +++ b/dlls/winemac.drv/macdrv.h @@ -177,6 +177,9 @@ extern CGImageRef create_cgimage_from_icon_bitmaps(HDC hdc, HANDLE icon, HBITMAP unsigned char *color_bits, int color_size, HBITMAP hbmMask, unsigned char *mask_bits, int mask_size, int width, int height, int istep) DECLSPEC_HIDDEN; +extern CGImageRef create_cgimage_from_icon(HANDLE icon, int width, int height) DECLSPEC_HIDDEN; extern CFArrayRef create_app_icon_images(void) DECLSPEC_HIDDEN; +extern void macdrv_status_item_clicked(const macdrv_event *event) DECLSPEC_HIDDEN; + #endif /* __WINE_MACDRV_H */ diff --git a/dlls/winemac.drv/macdrv_cocoa.h b/dlls/winemac.drv/macdrv_cocoa.h index bb768722a0..c014fbe3cf 100644 --- a/dlls/winemac.drv/macdrv_cocoa.h +++ b/dlls/winemac.drv/macdrv_cocoa.h @@ -116,6 +116,7 @@ typedef struct macdrv_opaque_window* macdrv_window; typedef struct macdrv_opaque_event_queue* macdrv_event_queue; typedef struct macdrv_opaque_view* macdrv_view; typedef struct macdrv_opaque_opengl_context* macdrv_opengl_context; +typedef struct macdrv_opaque_status_item* macdrv_status_item; struct macdrv_event; struct macdrv_query; @@ -161,6 +162,7 @@ enum { MOUSE_MOVED_ABSOLUTE, MOUSE_SCROLL, QUERY_EVENT, + STATUS_ITEM_CLICKED, WINDOW_CLOSE_REQUESTED, WINDOW_DID_MINIMIZE, WINDOW_DID_UNMINIMIZE, @@ -211,6 +213,10 @@ typedef struct macdrv_event { struct { struct macdrv_query *query; } query_event; + struct { + macdrv_status_item item; + int count; + } status_item_clicked; struct { CGRect frame; } window_frame_changed; @@ -348,4 +354,11 @@ extern void macdrv_make_context_current(macdrv_opengl_context c, macdrv_view v) extern void macdrv_update_opengl_context(macdrv_opengl_context c) DECLSPEC_HIDDEN; extern void macdrv_flush_opengl_context(macdrv_opengl_context c) DECLSPEC_HIDDEN; + +/* systray / status item */ +extern macdrv_status_item macdrv_create_status_item(macdrv_event_queue q) DECLSPEC_HIDDEN; +extern void macdrv_destroy_status_item(macdrv_status_item s) DECLSPEC_HIDDEN; +extern void macdrv_set_status_item_image(macdrv_status_item s, CGImageRef cgimage) DECLSPEC_HIDDEN; +extern void macdrv_set_status_item_tooltip(macdrv_status_item s, CFStringRef cftip) DECLSPEC_HIDDEN; + #endif /* __WINE_MACDRV_COCOA_H */ diff --git a/dlls/winemac.drv/systray.c b/dlls/winemac.drv/systray.c new file mode 100644 index 0000000000..b45e3118b4 --- /dev/null +++ b/dlls/winemac.drv/systray.c @@ -0,0 +1,358 @@ +/* + * Mac driver system tray management + * + * Copyright (C) 2004 Mike Hearn, for CodeWeavers + * Copyright (C) 2005 Robert Shearman + * Copyright (C) 2008 Alexandre Julliard + * Copyright (C) 2012, 2013 Ken Thomases for CodeWeavers Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include "config.h" + +#include "macdrv.h" + +#include "windef.h" +#include "winuser.h" +#include "shellapi.h" + +#include "wine/list.h" +#include "wine/debug.h" + +WINE_DEFAULT_DEBUG_CHANNEL(systray); + + +#define CHECK_SYSTRAY_TIMER 1 +#define CHECK_SYSTRAY_INTERVAL_MS 2000 + + +/* an individual systray icon */ +struct tray_icon +{ + struct list entry; + HWND owner; /* the HWND passed in to the Shell_NotifyIcon call */ + UINT id; /* the unique id given by the app */ + UINT callback_message; + HICON image; /* the image to render */ + WCHAR tiptext[128]; /* tooltip text */ + DWORD state; /* state flags */ + macdrv_status_item status_item; +}; + +static struct list icon_list = LIST_INIT(icon_list); + + +static BOOL delete_icon(struct tray_icon *icon); + + +/*********************************************************************** + * check_icons + * + * Timer procedure for periodically checking that the systray icons are + * still valid (their owning windows still exist). + */ +static VOID CALLBACK check_icons(HWND hwnd, UINT msg, UINT_PTR timer, DWORD time) +{ + struct tray_icon *icon, *next; + + LIST_FOR_EACH_ENTRY_SAFE(icon, next, &icon_list, struct tray_icon, entry) + if (!IsWindow(icon->owner)) delete_icon(icon); +} + + +/*********************************************************************** + * setup_check_icons_timer + * + * Set up a window with a timer to check that tray icons are still valid + * (their owning windows still exist). + */ +static void setup_check_icons_timer(void) +{ + static BOOL done; + + if (!done) + { + static const WCHAR messageW[] = {'M','e','s','s','a','g','e',0}; + HWND timer_window; + + /* Whether we succeed or not, don't try again. */ + done = TRUE; + + timer_window = CreateWindowW(messageW, NULL, 0, 0, 0, 0, 0, HWND_MESSAGE, + NULL, NULL, NULL); + if (!timer_window) + { + WARN("Could not create systray checking message window\n"); + return; + } + + if (!SetTimer(timer_window, CHECK_SYSTRAY_TIMER, CHECK_SYSTRAY_INTERVAL_MS, check_icons)) + WARN("Could not create systray checking timer\n"); + } +} + + +/*********************************************************************** + * get_icon + * + * Retrieves an icon record by owner window and ID. + */ +static struct tray_icon *get_icon(HWND owner, UINT id) +{ + struct tray_icon *this; + + LIST_FOR_EACH_ENTRY(this, &icon_list, struct tray_icon, entry) + if ((this->id == id) && (this->owner == owner)) return this; + return NULL; +} + + +/*********************************************************************** + * modify_icon + * + * Modifies an existing tray icon and updates its status item as needed. + */ +static BOOL modify_icon(struct tray_icon *icon, NOTIFYICONDATAW *nid) +{ + BOOL update_image = FALSE, update_tooltip = FALSE; + + TRACE("hwnd %p id 0x%x flags %x\n", nid->hWnd, nid->uID, nid->uFlags); + + if (nid->uFlags & NIF_STATE) + { + DWORD changed = (icon->state ^ nid->dwState) & nid->dwStateMask; + icon->state = (icon->state & ~nid->dwStateMask) | (nid->dwState & nid->dwStateMask); + if (changed & NIS_HIDDEN) + { + if (icon->state & NIS_HIDDEN) + { + if (icon->status_item) + { + TRACE("destroying status item %p\n", icon->status_item); + macdrv_destroy_status_item(icon->status_item); + icon->status_item = NULL; + } + } + else + { + if (!icon->status_item) + { + struct macdrv_thread_data *thread_data = macdrv_init_thread_data(); + + icon->status_item = macdrv_create_status_item(thread_data->queue); + if (icon->status_item) + { + TRACE("created status item %p\n", icon->status_item); + + if (icon->image) + update_image = TRUE; + if (lstrlenW(icon->tiptext)) + update_tooltip = TRUE; + } + else + WARN("failed to create status item\n"); + } + } + } + } + + if (nid->uFlags & NIF_ICON) + { + if (icon->image) DestroyIcon(icon->image); + icon->image = CopyIcon(nid->hIcon); + if (icon->status_item) + update_image = TRUE; + } + + if (nid->uFlags & NIF_MESSAGE) + { + icon->callback_message = nid->uCallbackMessage; + } + if (nid->uFlags & NIF_TIP) + { + lstrcpynW(icon->tiptext, nid->szTip, sizeof(icon->tiptext)/sizeof(WCHAR)); + if (icon->status_item) + update_tooltip = TRUE; + } + + if (update_image) + { + CGImageRef cgimage = NULL; + if (icon->image) + cgimage = create_cgimage_from_icon(icon->image, 0, 0); + macdrv_set_status_item_image(icon->status_item, cgimage); + CGImageRelease(cgimage); + } + + if (update_tooltip) + { + CFStringRef s; + + TRACE("setting tooltip text for status item %p to %s\n", icon->status_item, + debugstr_w(icon->tiptext)); + s = CFStringCreateWithCharacters(NULL, (UniChar*)icon->tiptext, + lstrlenW(icon->tiptext)); + macdrv_set_status_item_tooltip(icon->status_item, s); + CFRelease(s); + } + + return TRUE; +} + + +/*********************************************************************** + * add_icon + * + * Creates a new tray icon structure and adds it to the list. + */ +static BOOL add_icon(NOTIFYICONDATAW *nid) +{ + NOTIFYICONDATAW new_nid; + struct tray_icon *icon; + + TRACE("hwnd %p id 0x%x\n", nid->hWnd, nid->uID); + + if ((icon = get_icon(nid->hWnd, nid->uID))) + { + WARN("duplicate tray icon add, buggy app?\n"); + return FALSE; + } + + setup_check_icons_timer(); + + if (!(icon = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(*icon)))) + { + ERR("out of memory\n"); + return FALSE; + } + + icon->id = nid->uID; + icon->owner = nid->hWnd; + icon->state = NIS_HIDDEN; + + list_add_tail(&icon_list, &icon->entry); + + if (!(nid->uFlags & NIF_STATE) || !(nid->dwStateMask & NIS_HIDDEN)) + { + new_nid = *nid; + new_nid.uFlags |= NIF_STATE; + new_nid.dwState &= ~NIS_HIDDEN; + new_nid.dwStateMask |= NIS_HIDDEN; + nid = &new_nid; + } + return modify_icon(icon, nid); +} + + +/*********************************************************************** + * delete_icon + * + * Destroy tray icon status item and delete structure. + */ +static BOOL delete_icon(struct tray_icon *icon) +{ + TRACE("hwnd %p id 0x%x\n", icon->owner, icon->id); + + if (icon->status_item) + { + TRACE("destroying status item %p\n", icon->status_item); + macdrv_destroy_status_item(icon->status_item); + } + list_remove(&icon->entry); + DestroyIcon(icon->image); + HeapFree(GetProcessHeap(), 0, icon); + return TRUE; +} + + +/*********************************************************************** + * wine_notify_icon (MACDRV.@) + * + * Driver-side implementation of Shell_NotifyIcon. + */ +int CDECL wine_notify_icon(DWORD msg, NOTIFYICONDATAW *data) +{ + BOOL ret = FALSE; + struct tray_icon *icon; + + switch (msg) + { + case NIM_ADD: + ret = add_icon(data); + break; + case NIM_DELETE: + if ((icon = get_icon(data->hWnd, data->uID))) ret = delete_icon(icon); + break; + case NIM_MODIFY: + if ((icon = get_icon(data->hWnd, data->uID))) ret = modify_icon(icon, data); + break; + default: + FIXME("unhandled tray message: %u\n", msg); + break; + } + return ret; +} + + +/*********************************************************************** + * macdrv_status_item_clicked + * + * Handle STATUS_ITEM_CLICKED events. + */ +void macdrv_status_item_clicked(const macdrv_event *event) +{ + struct tray_icon *icon; + + TRACE("item %p count %d\n", event->status_item_clicked.item, + event->status_item_clicked.count); + + LIST_FOR_EACH_ENTRY(icon, &icon_list, struct tray_icon, entry) + { + if (icon->status_item == event->status_item_clicked.item) + { + UINT down; + + if (event->status_item_clicked.count == 1) + { + down = WM_LBUTTONDOWN; + TRACE("posting WM_LBUTTONDOWN to hwnd %p id 0x%x\n", icon->owner, icon->id); + } + else + { + down = WM_LBUTTONDBLCLK; + TRACE("posting WM_LBUTTONDBLCLK to hwnd %p id 0x%x\n", icon->owner, icon->id); + } + + if (!PostMessageW(icon->owner, icon->callback_message, icon->id, down) && + GetLastError() == ERROR_INVALID_WINDOW_HANDLE) + { + WARN("window %p was destroyed, removing icon 0x%x\n", icon->owner, icon->id); + delete_icon(icon); + return; + } + + TRACE("posting WM_LBUTTONUP to hwnd %p id 0x%x\n", icon->owner, icon->id); + if (!PostMessageW(icon->owner, icon->callback_message, icon->id, WM_LBUTTONUP) && + GetLastError() == ERROR_INVALID_WINDOW_HANDLE) + { + WARN("window %p was destroyed, removing icon 0x%x\n", icon->owner, icon->id); + delete_icon(icon); + } + + break; + } + } +} diff --git a/dlls/winemac.drv/winemac.drv.spec b/dlls/winemac.drv/winemac.drv.spec index 8bf3691a58..9eb416a29f 100644 --- a/dlls/winemac.drv/winemac.drv.spec +++ b/dlls/winemac.drv/winemac.drv.spec @@ -46,3 +46,6 @@ @ cdecl WindowMessage(long long long long) macdrv_WindowMessage @ cdecl WindowPosChanged(long long long ptr ptr ptr ptr ptr) macdrv_WindowPosChanged @ cdecl WindowPosChanging(long long long ptr ptr ptr ptr) macdrv_WindowPosChanging + +# System tray +@ cdecl wine_notify_icon(long ptr)