From dacbc236678740a67cef3a0f72ffa16c89f9b3d1 Mon Sep 17 00:00:00 2001 From: LI Daobing Date: Wed, 28 Jan 2026 21:23:12 -0800 Subject: [PATCH 1/3] 1 --- meson.build | 9 +- src/iptux/AppIndicatorMac.mm | 195 +++++++++++++++++++++++++++++++++++ src/iptux/meson.build | 5 +- 3 files changed, 206 insertions(+), 3 deletions(-) create mode 100644 src/iptux/AppIndicatorMac.mm diff --git a/meson.build b/meson.build index ecd63778..f26caacf 100644 --- a/meson.build +++ b/meson.build @@ -17,8 +17,13 @@ endif if (cc.has_argument('-Wno-c99-designator')) add_project_arguments('-Wno-c99-designator', language : 'cpp') endif -if host_machine.system() == 'darwin' and get_option('werror') - add_project_arguments('-U_LIBCPP_ENABLE_ASSERTIONS', language: 'cpp') +if host_machine.system() == 'darwin' + add_languages('objcpp') + add_project_arguments('-DGLIB_VERSION_MIN_REQUIRED=GLIB_VERSION_2_70', language : 'objcpp') + if get_option('werror') + add_project_arguments('-U_LIBCPP_ENABLE_ASSERTIONS', language: 'cpp') + add_project_arguments('-U_LIBCPP_ENABLE_ASSERTIONS', language: 'objcpp') + endif endif so_version = 2 diff --git a/src/iptux/AppIndicatorMac.mm b/src/iptux/AppIndicatorMac.mm new file mode 100644 index 00000000..56737f51 --- /dev/null +++ b/src/iptux/AppIndicatorMac.mm @@ -0,0 +1,195 @@ +#include "config.h" +#include "AppIndicator.h" + +#include +#include + +#import + +// Objective-C helper class for NSStatusItem callbacks. +// Must be at global scope (Objective-C declarations cannot appear inside a C++ +// namespace). +@interface IptuxStatusItemHelper : NSObject { + GActionGroup* actionGroup_; + iptux::IptuxAppIndicator* owner_; +} +- (instancetype)initWithActionGroup:(GActionGroup*)actionGroup + owner:(iptux::IptuxAppIndicator*)owner; +- (void)openMainWindow:(id)sender; +- (void)openPreferences:(id)sender; +- (void)quit:(id)sender; +- (void)statusItemClicked:(id)sender; +@end + +@implementation IptuxStatusItemHelper +- (instancetype)initWithActionGroup:(GActionGroup*)actionGroup + owner:(iptux::IptuxAppIndicator*)owner { + self = [super init]; + if (self) { + actionGroup_ = actionGroup; + owner_ = owner; + } + return self; +} + +- (void)openMainWindow:(id)sender { + (void)sender; + g_action_group_activate_action(actionGroup_, "open_main_window", NULL); +} + +- (void)openPreferences:(id)sender { + (void)sender; + g_action_group_activate_action(actionGroup_, "preferences", NULL); +} + +- (void)quit:(id)sender { + (void)sender; + g_action_group_activate_action(actionGroup_, "quit", NULL); +} + +- (void)statusItemClicked:(id)sender { + (void)sender; + owner_->sigActivateMainWindow.emit(); +} +@end + +static NSImage* pixbufToNSImage(GdkPixbuf* pixbuf) { + int width = gdk_pixbuf_get_width(pixbuf); + int height = gdk_pixbuf_get_height(pixbuf); + int stride = gdk_pixbuf_get_rowstride(pixbuf); + gboolean hasAlpha = gdk_pixbuf_get_has_alpha(pixbuf); + const guchar* pixels = gdk_pixbuf_get_pixels(pixbuf); + + int bitsPerSample = 8; + int channels = hasAlpha ? 4 : 3; + + NSBitmapImageRep* rep = [[NSBitmapImageRep alloc] + initWithBitmapDataPlanes:NULL + pixelsWide:width + pixelsHigh:height + bitsPerSample:bitsPerSample + samplesPerPixel:channels + hasAlpha:hasAlpha + isPlanar:NO + colorSpaceName:NSDeviceRGBColorSpace + bytesPerRow:stride + bitsPerPixel:bitsPerSample * channels]; + + memcpy([rep bitmapData], pixels, height * stride); + + NSImage* image = [[NSImage alloc] initWithSize:NSMakeSize(width, height)]; + [image addRepresentation:rep]; + [rep release]; + + return [image autorelease]; +} + +static NSImage* loadIcon(const char* iconName, int size) { + auto theme = gtk_icon_theme_get_default(); + GError* error = nullptr; + auto pixbuf = gtk_icon_theme_load_icon(theme, iconName, size, + GtkIconLookupFlags(0), &error); + if (!pixbuf) { + g_warning("Couldn't load icon %s: %s", iconName, error->message); + g_error_free(error); + return nil; + } + NSImage* image = pixbufToNSImage(pixbuf); + [image retain]; + g_object_unref(pixbuf); + + // Resize for menu bar (typically 18x18 points) + [image setSize:NSMakeSize(18, 18)]; + return image; +} + +namespace iptux { + +class IptuxAppIndicatorPrivate { + public: + IptuxAppIndicatorPrivate() {} + ~IptuxAppIndicatorPrivate() { + if (statusItem) { + [[NSStatusBar systemStatusBar] removeStatusItem:statusItem]; + [statusItem release]; + } + if (helper) { + [helper release]; + } + if (normalIcon) { + [normalIcon release]; + } + if (attentionIcon) { + [attentionIcon release]; + } + } + + NSStatusItem* statusItem = nil; + IptuxStatusItemHelper* helper = nil; + NSImage* normalIcon = nil; + NSImage* attentionIcon = nil; +}; + +IptuxAppIndicator::IptuxAppIndicator(GActionGroup* action_group) { + this->priv = std::make_shared(); + + priv->helper = [[IptuxStatusItemHelper alloc] initWithActionGroup:action_group + owner:this]; + + // Load icons + priv->normalIcon = loadIcon("iptux-icon", 64); + priv->attentionIcon = loadIcon("iptux-attention", 64); + + // Create status item + priv->statusItem = + [[[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength] retain]; + + if (priv->normalIcon) { + priv->statusItem.button.image = priv->normalIcon; + } + priv->statusItem.button.toolTip = @"Iptux"; + + // Build the menu + NSMenu* menu = [[NSMenu alloc] init]; + + NSMenuItem* openItem = + [[NSMenuItem alloc] initWithTitle:[NSString stringWithUTF8String:_("Open Iptux")] + action:@selector(openMainWindow:) + keyEquivalent:@""]; + openItem.target = priv->helper; + [menu addItem:openItem]; + [openItem release]; + + [menu addItem:[NSMenuItem separatorItem]]; + + NSMenuItem* prefsItem = + [[NSMenuItem alloc] initWithTitle:[NSString stringWithUTF8String:_("Preferences")] + action:@selector(openPreferences:) + keyEquivalent:@""]; + prefsItem.target = priv->helper; + [menu addItem:prefsItem]; + [prefsItem release]; + + NSMenuItem* quitItem = + [[NSMenuItem alloc] initWithTitle:[NSString stringWithUTF8String:_("Quit")] + action:@selector(quit:) + keyEquivalent:@""]; + quitItem.target = priv->helper; + [menu addItem:quitItem]; + [quitItem release]; + + priv->statusItem.menu = menu; + [menu release]; +} + +void IptuxAppIndicator::SetUnreadCount(int count) { + if (!priv->statusItem) return; + + if (count > 0 && priv->attentionIcon) { + priv->statusItem.button.image = priv->attentionIcon; + } else if (priv->normalIcon) { + priv->statusItem.button.image = priv->normalIcon; + } +} + +} // namespace iptux diff --git a/src/iptux/meson.build b/src/iptux/meson.build index a790d312..7dfd04d9 100644 --- a/src/iptux/meson.build +++ b/src/iptux/meson.build @@ -26,7 +26,10 @@ sources = files([ ]) dependencies = [gtk_dep, jsoncpp_dep, sigc_dep] -if appindicator_dep.found() +if host_machine.system() == 'darwin' + sources += ['AppIndicatorMac.mm'] + dependencies += [dependency('appleframeworks', modules: ['Cocoa'])] +elif appindicator_dep.found() sources += ['AppIndicator.cpp'] dependencies += [appindicator_dep] else From 7f2855e3e10b9bf36f071867bf1a63cc32faf7a9 Mon Sep 17 00:00:00 2001 From: LI Daobing Date: Wed, 28 Jan 2026 21:25:36 -0800 Subject: [PATCH 2/3] Update NEWS --- NEWS | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/NEWS b/NEWS index ff1d7eae..136906bf 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,10 @@ +Version 0.10.0 +~~~~~~~~~~~~~ +Released: TBD + +Features: + * [#711] Add macOS menu bar status item (NSStatusItem) with system tray icon. + Version 0.9.4 ~~~~~~~~~~~~~ Released: 2025-02-17 From 63ea050875bab1db82e7402c9d43d2e7525d2cb9 Mon Sep 17 00:00:00 2001 From: LI Daobing Date: Wed, 28 Jan 2026 21:30:32 -0800 Subject: [PATCH 3/3] 1 --- NEWS | 2 +- src/iptux/AppIndicatorMac.mm | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index 136906bf..d2fc6a43 100644 --- a/NEWS +++ b/NEWS @@ -1,6 +1,6 @@ Version 0.10.0 ~~~~~~~~~~~~~ -Released: TBD +Released: 2026-01-28 Features: * [#711] Add macOS menu bar status item (NSStatusItem) with system tray icon. diff --git a/src/iptux/AppIndicatorMac.mm b/src/iptux/AppIndicatorMac.mm index 56737f51..8704f6f5 100644 --- a/src/iptux/AppIndicatorMac.mm +++ b/src/iptux/AppIndicatorMac.mm @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + #include "config.h" #include "AppIndicator.h"