1/*
2 * Copyright © 2015 Patrick Griffis
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2.1 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General
15 * Public License along with this library; if not, see <http://www.gnu.org/licenses/>.
16 *
17 * Authors: Patrick Griffis
18 */
19
20#include "config.h"
21
22#import <Cocoa/Cocoa.h>
23#include "gnotificationbackend.h"
24#include "gapplication.h"
25#include "gaction.h"
26#include "gactiongroup.h"
27#include "giomodule-priv.h"
28#include "gnotification-private.h"
29#include "gthemedicon.h"
30#include "gfileicon.h"
31#include "gfile.h"
32
33#define G_TYPE_COCOA_NOTIFICATION_BACKEND  (g_cocoa_notification_backend_get_type ())
34#define G_COCOA_NOTIFICATION_BACKEND(o)    (G_TYPE_CHECK_INSTANCE_CAST ((o), G_TYPE_COCOA_NOTIFICATION_BACKEND, GCocoaNotificationBackend))
35
36typedef struct _GCocoaNotificationBackend GCocoaNotificationBackend;
37typedef GNotificationBackendClass            GCocoaNotificationBackendClass;
38struct _GCocoaNotificationBackend
39{
40  GNotificationBackend parent;
41};
42
43GType g_cocoa_notification_backend_get_type (void);
44
45G_DEFINE_TYPE_WITH_CODE (GCocoaNotificationBackend, g_cocoa_notification_backend, G_TYPE_NOTIFICATION_BACKEND,
46  _g_io_modules_ensure_extension_points_registered ();
47  g_io_extension_point_implement (G_NOTIFICATION_BACKEND_EXTENSION_POINT_NAME, g_define_type_id, "cocoa", 200));
48
49static NSString *
50nsstring_from_cstr (const char *cstr)
51{
52  if (!cstr)
53    return nil;
54
55  return [[NSString alloc] initWithUTF8String:cstr];
56}
57
58static NSImage*
59nsimage_from_gicon (GIcon *icon)
60{
61  if (G_IS_FILE_ICON (icon))
62    {
63      NSImage *image = nil;
64      GFile *file;
65      char *path;
66
67      file = g_file_icon_get_file (G_FILE_ICON (icon));
68      path = g_file_get_path (file);
69      if (path)
70        {
71          NSString *str_path = nsstring_from_cstr (path);
72          image = [[NSImage alloc] initByReferencingFile:str_path];
73
74          [str_path release];
75          g_free (path);
76        }
77      return image;
78    }
79  else
80    {
81      g_warning ("This icon type is not handled by this NotificationBackend");
82      return nil;
83    }
84}
85
86static void
87activate_detailed_action (const char * action)
88{
89  char *name;
90  GVariant *target;
91
92  if (!g_str_has_prefix (action, "app."))
93    {
94      g_warning ("Notification action does not have \"app.\" prefix");
95      return;
96    }
97
98  if (g_action_parse_detailed_name (action, &name, &target, NULL))
99    {
100      g_action_group_activate_action (G_ACTION_GROUP (g_application_get_default()), name + 4, target);
101      g_free (name);
102      if (target)
103        g_variant_unref (target);
104    }
105}
106
107@interface GNotificationCenterDelegate : NSObject<NSUserNotificationCenterDelegate> @end
108@implementation GNotificationCenterDelegate
109
110-(void) userNotificationCenter:(NSUserNotificationCenter*) center
111       didActivateNotification:(NSUserNotification*)       notification
112{
113  if ([notification activationType] == NSUserNotificationActivationTypeContentsClicked)
114    {
115      const char *action = [[notification userInfo][@"default"] UTF8String];
116      if (action)
117        activate_detailed_action (action);
118      /* OSX Always activates the front window */
119    }
120  else if ([notification activationType] == NSUserNotificationActivationTypeActionButtonClicked)
121    {
122      const char *action = [[notification userInfo][@"button0"] UTF8String];
123      if (action)
124        activate_detailed_action (action);
125    }
126
127  [center removeDeliveredNotification:notification];
128}
129
130@end
131
132static GNotificationCenterDelegate *cocoa_notification_delegate;
133
134static gboolean
135g_cocoa_notification_backend_is_supported (void)
136{
137  NSBundle *bundle = [NSBundle mainBundle];
138
139  /* This is always actually supported, but without a bundle it does nothing */
140  if (![bundle bundleIdentifier])
141    return FALSE;
142
143  return TRUE;
144}
145
146static void
147add_actions_to_notification (NSUserNotification   *userNotification,
148                             GNotification        *notification)
149{
150  guint n_buttons = g_notification_get_n_buttons (notification);
151  char *action = NULL, *label = NULL;
152  GVariant *target = NULL;
153  NSMutableDictionary *user_info = nil;
154
155  if (g_notification_get_default_action (notification, &action, &target))
156    {
157      char *detailed_name = g_action_print_detailed_name (action, target);
158      NSString *action_name = nsstring_from_cstr (detailed_name);
159      user_info = [[NSMutableDictionary alloc] init];
160
161      user_info[@"default"] = action_name;
162
163      [action_name release];
164      g_free (detailed_name);
165      g_clear_pointer (&action, g_free);
166      g_clear_pointer (&target, g_variant_unref);
167    }
168
169  if (n_buttons)
170    {
171      g_notification_get_button (notification, 0, &label, &action, &target);
172      if (label)
173        {
174          NSString *str_label = nsstring_from_cstr (label);
175          char *detailed_name = g_action_print_detailed_name (action, target);
176          NSString *action_name = nsstring_from_cstr (detailed_name);
177
178          if (!user_info)
179            user_info = [[NSMutableDictionary alloc] init];
180
181          user_info[@"button0"] = action_name;
182          userNotification.actionButtonTitle = str_label;
183
184          [str_label release];
185          [action_name release];
186          g_free (label);
187          g_free (action);
188          g_free (detailed_name);
189          g_clear_pointer (&target, g_variant_unref);
190        }
191
192      if (n_buttons > 1)
193        g_warning ("Only a single button is currently supported by this NotificationBackend");
194    }
195
196    userNotification.userInfo = user_info;
197    [user_info release];
198}
199
200static void
201g_cocoa_notification_backend_send_notification (GNotificationBackend *backend,
202                                                const gchar          *cstr_id,
203                                                GNotification        *notification)
204{
205  NSString *str_title = nil, *str_text = nil, *str_id = nil;
206  NSImage *content = nil;
207  const char *cstr;
208  GIcon *icon;
209  NSUserNotification *userNotification;
210  NSUserNotificationCenter *center;
211
212  if ((cstr = g_notification_get_title (notification)))
213    str_title = nsstring_from_cstr (cstr);
214  if ((cstr = g_notification_get_body (notification)))
215    str_text = nsstring_from_cstr (cstr);
216  if (cstr_id != NULL)
217    str_id = nsstring_from_cstr (cstr_id);
218  if ((icon = g_notification_get_icon (notification)))
219    content = nsimage_from_gicon (icon);
220  /* NOTE: There is no priority */
221
222  userNotification = [NSUserNotification new];
223  userNotification.title = str_title;
224  userNotification.informativeText = str_text;
225  userNotification.identifier = str_id;
226  userNotification.contentImage = content;
227  /* NOTE: Buttons only show up if your bundle has NSUserNotificationAlertStyle set to "alerts" */
228  add_actions_to_notification (userNotification, notification);
229
230  if (!cocoa_notification_delegate)
231    cocoa_notification_delegate = [[GNotificationCenterDelegate alloc] init];
232
233  center = [NSUserNotificationCenter defaultUserNotificationCenter];
234  center.delegate = cocoa_notification_delegate;
235  [center deliverNotification:userNotification];
236
237  [str_title release];
238  [str_text release];
239  [str_id release];
240  [content release];
241  [userNotification release];
242}
243
244static void
245g_cocoa_notification_backend_withdraw_notification (GNotificationBackend *backend,
246                                                    const gchar          *cstr_id)
247{
248  NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter];
249  NSArray *notifications = [center deliveredNotifications];
250  NSString *str_id = nsstring_from_cstr (cstr_id);
251
252  for (NSUserNotification *notification in notifications)
253    {
254      if ([notification.identifier compare:str_id] == NSOrderedSame)
255        {
256          [center removeDeliveredNotification:notification];
257          break;
258        }
259    }
260
261  [str_id release];
262}
263
264static void
265g_cocoa_notification_backend_init (GCocoaNotificationBackend *backend)
266{
267}
268
269static void
270g_cocoa_notification_backend_class_init (GCocoaNotificationBackendClass *klass)
271{
272  GNotificationBackendClass *backend_class = G_NOTIFICATION_BACKEND_CLASS (klass);
273
274  backend_class->is_supported = g_cocoa_notification_backend_is_supported;
275  backend_class->send_notification = g_cocoa_notification_backend_send_notification;
276  backend_class->withdraw_notification = g_cocoa_notification_backend_withdraw_notification;
277}
278