Skip to content

Commit

Permalink
Implement support for application status indicators (tray icons).
Browse files Browse the repository at this point in the history
  • Loading branch information
bruvzg committed Feb 13, 2024
1 parent dfe226b commit 8da3603
Show file tree
Hide file tree
Showing 15 changed files with 827 additions and 1 deletion.
46 changes: 46 additions & 0 deletions doc/classes/DisplayServer.xml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,15 @@
[b]Note:[/b] This method is only implemented on Linux (X11/Wayland).
</description>
</method>
<method name="create_status_indicator">
<return type="int" />
<param index="0" name="icon" type="Image" />
<param index="1" name="tooltip" type="String" />
<param index="2" name="callback" type="Callable" />
<description>
Creates a new application status indicator with the specified icon, tooltip, and activation callback.
</description>
</method>
<method name="cursor_get_shape" qualifiers="const">
<return type="int" enum="DisplayServer.CursorShape" />
<description>
Expand All @@ -78,6 +87,13 @@
Sets the default mouse cursor shape. The cursor's appearance will vary depending on the user's operating system and mouse cursor theme. See also [method cursor_get_shape] and [method cursor_set_custom_image].
</description>
</method>
<method name="delete_status_indicator">
<return type="void" />
<param index="0" name="id" type="int" />
<description>
Removes the application status indicator.
</description>
</method>
<method name="dialog_input_text">
<return type="int" enum="Error" />
<param index="0" name="title" type="String" />
Expand Down Expand Up @@ -1120,6 +1136,30 @@
Sets the window icon (usually displayed in the top-left corner) in the operating system's [i]native[/i] format. The file at [param filename] must be in [code].ico[/code] format on Windows or [code].icns[/code] on macOS. By using specially crafted [code].ico[/code] or [code].icns[/code] icons, [method set_native_icon] allows specifying different icons depending on the size the icon is displayed at. This size is determined by the operating system and user preferences (including the display scale factor). To use icons in other formats, use [method set_icon] instead.
</description>
</method>
<method name="status_indicator_set_callback">
<return type="void" />
<param index="0" name="id" type="int" />
<param index="1" name="callback" type="Callable" />
<description>
Sets the application status indicator activation callback.
</description>
</method>
<method name="status_indicator_set_icon">
<return type="void" />
<param index="0" name="id" type="int" />
<param index="1" name="icon" type="Image" />
<description>
Sets the application status indicator icon.
</description>
</method>
<method name="status_indicator_set_tooltip">
<return type="void" />
<param index="0" name="id" type="int" />
<param index="1" name="tooltip" type="String" />
<description>
Sets the application status indicator tooltip.
</description>
</method>
<method name="tablet_get_current_driver" qualifiers="const">
<return type="String" />
<description>
Expand Down Expand Up @@ -1748,6 +1788,9 @@
<constant name="FEATURE_SCREEN_CAPTURE" value="21" enum="Feature">
Display server supports reading screen pixels. See [method screen_get_pixel].
</constant>
<constant name="FEATURE_STATUS_INDICATOR" value="22" enum="Feature">
Display server supports application status indicators.
</constant>
<constant name="MOUSE_MODE_VISIBLE" value="0" enum="MouseMode">
Makes the mouse cursor visible if it is hidden.
</constant>
Expand Down Expand Up @@ -1786,6 +1829,9 @@
<constant name="INVALID_WINDOW_ID" value="-1">
The ID that refers to a nonexistent window. This is returned by some [DisplayServer] methods if no window matches the requested result.
</constant>
<constant name="INVALID_INDICATOR_ID" value="-1">
The ID that refers to a nonexistent application status indicator.
</constant>
<constant name="SCREEN_LANDSCAPE" value="0" enum="ScreenOrientation">
Default landscape orientation.
</constant>
Expand Down
31 changes: 31 additions & 0 deletions doc/classes/StatusIndicator.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" ?>
<class name="StatusIndicator" inherits="Node" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd">
<brief_description>
Application status indicator (aka notification area icon).
[b]Note:[/b] Status indicator is implemented on macOS and Windows.
</brief_description>
<description>
</description>
<tutorials>
</tutorials>
<members>
<member name="icon" type="Image" setter="set_icon" getter="get_icon">
Status indicator icon.
</member>
<member name="tooltip" type="String" setter="set_tooltip" getter="get_tooltip" default="&quot;&quot;">
Status indicator tooltip.
</member>
<member name="visible" type="bool" setter="set_visible" getter="is_visible" default="true">
If [code]true[/code], the status indicator is visible.
</member>
</members>
<signals>
<signal name="pressed">
<param index="0" name="mouse_button" type="int" />
<param index="1" name="position" type="Vector2i" />
<description>
Emitted when the status indicator is pressed.
</description>
</signal>
</signals>
</class>
1 change: 1 addition & 0 deletions editor/icons/StatusIndicator.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions platform/macos/SCsub
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ files = [
"display_server_macos.mm",
"godot_button_view.mm",
"godot_content_view.mm",
"godot_status_item.mm",
"godot_window_delegate.mm",
"godot_window.mm",
"key_mapping_macos.mm",
Expand Down
14 changes: 14 additions & 0 deletions platform/macos/display_server_macos.h
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,14 @@ class DisplayServerMacOS : public DisplayServer {

HashMap<WindowID, WindowData> windows;

struct IndicatorData {
id view;
id item;
};

IndicatorID indicator_id_counter = 0;
HashMap<IndicatorID, IndicatorData> indicators;

IOPMAssertionID screen_keep_on_assertion = kIOPMNullAssertionID;

struct MenuCall {
Expand Down Expand Up @@ -486,6 +494,12 @@ class DisplayServerMacOS : public DisplayServer {
virtual void set_native_icon(const String &p_filename) override;
virtual void set_icon(const Ref<Image> &p_icon) override;

virtual IndicatorID create_status_indicator(const Ref<Image> &p_icon, const String &p_tooltip, const Callable &p_callback) override;
virtual void status_indicator_set_icon(IndicatorID p_id, const Ref<Image> &p_icon) override;
virtual void status_indicator_set_tooltip(IndicatorID p_id, const String &p_tooltip) override;
virtual void status_indicator_set_callback(IndicatorID p_id, const Callable &p_callback) override;
virtual void delete_status_indicator(IndicatorID p_id) override;

static DisplayServer *create_func(const String &p_rendering_driver, WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i *p_position, const Vector2i &p_resolution, int p_screen, Error &r_error);
static Vector<String> get_rendering_drivers_func();

Expand Down
125 changes: 125 additions & 0 deletions platform/macos/display_server_macos.mm
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
#include "godot_menu_delegate.h"
#include "godot_menu_item.h"
#include "godot_open_save_delegate.h"
#include "godot_status_item.h"
#include "godot_window.h"
#include "godot_window_delegate.h"
#include "key_mapping_macos.h"
Expand Down Expand Up @@ -838,6 +839,7 @@
case FEATURE_TEXT_TO_SPEECH:
case FEATURE_EXTEND_TO_TITLE:
case FEATURE_SCREEN_CAPTURE:
case FEATURE_STATUS_INDICATOR:
return true;
default: {
}
Expand Down Expand Up @@ -4296,6 +4298,124 @@
}
}

DisplayServer::IndicatorID DisplayServerMacOS::create_status_indicator(const Ref<Image> &p_icon, const String &p_tooltip, const Callable &p_callback) {
NSImage *nsimg = nullptr;
if (p_icon.is_valid() && p_icon->get_width() > 0 && p_icon->get_height() > 0) {
Ref<Image> img = p_icon->duplicate();
img->convert(Image::FORMAT_RGBA8);

NSBitmapImageRep *imgrep = [[NSBitmapImageRep alloc]
initWithBitmapDataPlanes:nullptr
pixelsWide:img->get_width()
pixelsHigh:img->get_height()
bitsPerSample:8
samplesPerPixel:4
hasAlpha:YES
isPlanar:NO
colorSpaceName:NSDeviceRGBColorSpace
bytesPerRow:img->get_width() * 4
bitsPerPixel:32];
if (imgrep) {
uint8_t *pixels = [imgrep bitmapData];

int len = img->get_width() * img->get_height();
const uint8_t *r = img->get_data().ptr();

/* Premultiply the alpha channel */
for (int i = 0; i < len; i++) {
uint8_t alpha = r[i * 4 + 3];
pixels[i * 4 + 0] = (uint8_t)(((uint16_t)r[i * 4 + 0] * alpha) / 255);
pixels[i * 4 + 1] = (uint8_t)(((uint16_t)r[i * 4 + 1] * alpha) / 255);
pixels[i * 4 + 2] = (uint8_t)(((uint16_t)r[i * 4 + 2] * alpha) / 255);
pixels[i * 4 + 3] = alpha;
}

nsimg = [[NSImage alloc] initWithSize:NSMakeSize(img->get_width(), img->get_height())];
if (nsimg) {
[nsimg addRepresentation:imgrep];
}
}
}

IndicatorData idat;

idat.item = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength];
idat.view = [[GodotStatusItemView alloc] init];

[idat.view setToolTip:[NSString stringWithUTF8String:p_tooltip.utf8().get_data()]];
[idat.view setImage:nsimg];
[idat.view setCallback:p_callback];
[idat.item setView:idat.view];

IndicatorID iid = indicator_id_counter++;
indicators[iid] = idat;

return iid;
}

void DisplayServerMacOS::status_indicator_set_icon(IndicatorID p_id, const Ref<Image> &p_icon) {
ERR_FAIL_COND(!indicators.has(p_id));

NSImage *nsimg = nullptr;
if (p_icon.is_valid() && p_icon->get_width() > 0 && p_icon->get_height() > 0) {
Ref<Image> img = p_icon->duplicate();
img->convert(Image::FORMAT_RGBA8);

NSBitmapImageRep *imgrep = [[NSBitmapImageRep alloc]
initWithBitmapDataPlanes:nullptr
pixelsWide:img->get_width()
pixelsHigh:img->get_height()
bitsPerSample:8
samplesPerPixel:4
hasAlpha:YES
isPlanar:NO
colorSpaceName:NSDeviceRGBColorSpace
bytesPerRow:img->get_width() * 4
bitsPerPixel:32];
if (imgrep) {
uint8_t *pixels = [imgrep bitmapData];

int len = img->get_width() * img->get_height();
const uint8_t *r = img->get_data().ptr();

/* Premultiply the alpha channel */
for (int i = 0; i < len; i++) {
uint8_t alpha = r[i * 4 + 3];
pixels[i * 4 + 0] = (uint8_t)(((uint16_t)r[i * 4 + 0] * alpha) / 255);
pixels[i * 4 + 1] = (uint8_t)(((uint16_t)r[i * 4 + 1] * alpha) / 255);
pixels[i * 4 + 2] = (uint8_t)(((uint16_t)r[i * 4 + 2] * alpha) / 255);
pixels[i * 4 + 3] = alpha;
}

nsimg = [[NSImage alloc] initWithSize:NSMakeSize(img->get_width(), img->get_height())];
if (nsimg) {
[nsimg addRepresentation:imgrep];
}
}
}

[indicators[p_id].view setImage:nsimg];
}

void DisplayServerMacOS::status_indicator_set_tooltip(IndicatorID p_id, const String &p_tooltip) {
ERR_FAIL_COND(!indicators.has(p_id));

[indicators[p_id].view setToolTip:[NSString stringWithUTF8String:p_tooltip.utf8().get_data()]];
}

void DisplayServerMacOS::status_indicator_set_callback(IndicatorID p_id, const Callable &p_callback) {
ERR_FAIL_COND(!indicators.has(p_id));

[indicators[p_id].view setCallback:p_callback];
}

void DisplayServerMacOS::delete_status_indicator(IndicatorID p_id) {
ERR_FAIL_COND(!indicators.has(p_id));

[[NSStatusBar systemStatusBar] removeStatusItem:indicators[p_id].item];
indicators.erase(p_id);
}

DisplayServer *DisplayServerMacOS::create_func(const String &p_rendering_driver, WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i *p_position, const Vector2i &p_resolution, int p_screen, Error &r_error) {
DisplayServer *ds = memnew(DisplayServerMacOS(p_rendering_driver, p_mode, p_vsync_mode, p_flags, p_position, p_resolution, p_screen, r_error));
if (r_error != OK) {
Expand Down Expand Up @@ -4700,6 +4820,11 @@
screen_keep_on_assertion = kIOPMNullAssertionID;
}

// Destroy all status indicators.
for (HashMap<IndicatorID, IndicatorData>::Iterator E = indicators.begin(); E;) {
[[NSStatusBar systemStatusBar] removeStatusItem:E->value.item];
}

// Destroy all windows.
for (HashMap<WindowID, WindowData>::Iterator E = windows.begin(); E;) {
HashMap<WindowID, WindowData>::Iterator F = E;
Expand Down
51 changes: 51 additions & 0 deletions platform/macos/godot_status_item.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**************************************************************************/
/* godot_status_item.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/

#ifndef GODOT_STATUS_ITEM_H
#define GODOT_STATUS_ITEM_H

#include "core/input/input_enums.h"
#include "core/variant/callable.h"

#import <AppKit/AppKit.h>
#import <Foundation/Foundation.h>

@interface GodotStatusItemView : NSView {
NSImage *image;
Callable cb;
}

- (void)processMouseEvent:(NSEvent *)event index:(MouseButton)index;
- (void)setImage:(NSImage *)image;
- (void)setCallback:(const Callable &)callback;

@end

#endif // GODOT_STATUS_ITEM_H
Loading

0 comments on commit 8da3603

Please sign in to comment.