diff --git a/doc/classes/DisplayServer.xml b/doc/classes/DisplayServer.xml index 3be64e2f62f7..09bb20af79df 100644 --- a/doc/classes/DisplayServer.xml +++ b/doc/classes/DisplayServer.xml @@ -56,6 +56,15 @@ [b]Note:[/b] This method is only implemented on Linux (X11/Wayland). + + + + + + + Creates a new application status indicator with the specified icon, tooltip, and activation callback. + + @@ -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]. + + + + + Removes the application status indicator. + + @@ -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. + + + + + + Sets the application status indicator activation callback. + + + + + + + + Sets the application status indicator icon. + + + + + + + + Sets the application status indicator tooltip. + + @@ -1748,6 +1788,9 @@ Display server supports reading screen pixels. See [method screen_get_pixel]. + + Display server supports application status indicators. + Makes the mouse cursor visible if it is hidden. @@ -1786,6 +1829,9 @@ The ID that refers to a nonexistent window. This is returned by some [DisplayServer] methods if no window matches the requested result. + + The ID that refers to a nonexistent application status indicator. + Default landscape orientation. diff --git a/doc/classes/StatusIndicator.xml b/doc/classes/StatusIndicator.xml new file mode 100644 index 000000000000..b92018e17207 --- /dev/null +++ b/doc/classes/StatusIndicator.xml @@ -0,0 +1,31 @@ + + + + Application status indicator (aka notification area icon). + [b]Note:[/b] Status indicator is implemented on macOS and Windows. + + + + + + + + Status indicator icon. + + + Status indicator tooltip. + + + If [code]true[/code], the status indicator is visible. + + + + + + + + Emitted when the status indicator is pressed. + + + + diff --git a/editor/icons/StatusIndicator.svg b/editor/icons/StatusIndicator.svg new file mode 100644 index 000000000000..09673b3354e3 --- /dev/null +++ b/editor/icons/StatusIndicator.svg @@ -0,0 +1 @@ + diff --git a/platform/macos/SCsub b/platform/macos/SCsub index 5a93c3a09ff3..4dfafc56f89c 100644 --- a/platform/macos/SCsub +++ b/platform/macos/SCsub @@ -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", diff --git a/platform/macos/display_server_macos.h b/platform/macos/display_server_macos.h index e298b54970ae..10c8abe663b1 100644 --- a/platform/macos/display_server_macos.h +++ b/platform/macos/display_server_macos.h @@ -201,6 +201,14 @@ private: HashMap windows; + struct IndicatorData { + id view; + id item; + }; + + IndicatorID indicator_id_counter = 0; + HashMap indicators; + IOPMAssertionID screen_keep_on_assertion = kIOPMNullAssertionID; struct MenuCall { @@ -486,6 +494,12 @@ public: virtual void set_native_icon(const String &p_filename) override; virtual void set_icon(const Ref &p_icon) override; + virtual IndicatorID create_status_indicator(const Ref &p_icon, const String &p_tooltip, const Callable &p_callback) override; + virtual void status_indicator_set_icon(IndicatorID p_id, const Ref &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 get_rendering_drivers_func(); diff --git a/platform/macos/display_server_macos.mm b/platform/macos/display_server_macos.mm index ad8afaf46b26..99d4554a42a2 100644 --- a/platform/macos/display_server_macos.mm +++ b/platform/macos/display_server_macos.mm @@ -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" @@ -838,6 +839,7 @@ bool DisplayServerMacOS::has_feature(Feature p_feature) const { case FEATURE_TEXT_TO_SPEECH: case FEATURE_EXTEND_TO_TITLE: case FEATURE_SCREEN_CAPTURE: + case FEATURE_STATUS_INDICATOR: return true; default: { } @@ -4296,6 +4298,124 @@ void DisplayServerMacOS::set_icon(const Ref &p_icon) { } } +DisplayServer::IndicatorID DisplayServerMacOS::create_status_indicator(const Ref &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 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 &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 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) { @@ -4700,6 +4820,11 @@ DisplayServerMacOS::~DisplayServerMacOS() { screen_keep_on_assertion = kIOPMNullAssertionID; } + // Destroy all status indicators. + for (HashMap::Iterator E = indicators.begin(); E;) { + [[NSStatusBar systemStatusBar] removeStatusItem:E->value.item]; + } + // Destroy all windows. for (HashMap::Iterator E = windows.begin(); E;) { HashMap::Iterator F = E; diff --git a/platform/macos/godot_status_item.h b/platform/macos/godot_status_item.h new file mode 100644 index 000000000000..1827baa9bdb6 --- /dev/null +++ b/platform/macos/godot_status_item.h @@ -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 +#import + +@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 diff --git a/platform/macos/godot_status_item.mm b/platform/macos/godot_status_item.mm new file mode 100644 index 000000000000..71ed0a0f719d --- /dev/null +++ b/platform/macos/godot_status_item.mm @@ -0,0 +1,101 @@ +/**************************************************************************/ +/* godot_status_item.mm */ +/**************************************************************************/ +/* 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. */ +/**************************************************************************/ + +#include "godot_status_item.h" + +#include "display_server_macos.h" + +@implementation GodotStatusItemView + +- (id)init { + self = [super init]; + image = nullptr; + return self; +} + +- (void)setImage:(NSImage *)newImage { + image = newImage; + [self setNeedsDisplayInRect:self.frame]; +} + +- (void)setCallback:(const Callable &)callback { + cb = callback; +} + +- (void)drawRect:(NSRect)rect { + if (image) { + [image drawInRect:rect]; + } +} + +- (void)processMouseEvent:(NSEvent *)event index:(MouseButton)index { + DisplayServerMacOS *ds = (DisplayServerMacOS *)DisplayServer::get_singleton(); + if (!ds) { + return; + } + + if (cb.is_valid()) { + Variant v_button = index; + Variant v_pos = ds->mouse_get_position(); + Variant *v_args[2] = { &v_button, &v_pos }; + Variant ret; + Callable::CallError ce; + cb.callp((const Variant **)&v_args, 2, ret, ce); + } +} + +- (void)mouseDown:(NSEvent *)event { + [super mouseDown:event]; + if (([event modifierFlags] & NSEventModifierFlagControl)) { + [self processMouseEvent:event index:MouseButton::RIGHT]; + } else { + [self processMouseEvent:event index:MouseButton::LEFT]; + } +} + +- (void)rightMouseDown:(NSEvent *)event { + [super rightMouseDown:event]; + + [self processMouseEvent:event index:MouseButton::RIGHT]; +} + +- (void)otherMouseDown:(NSEvent *)event { + [super otherMouseDown:event]; + + if ((int)[event buttonNumber] == 2) { + [self processMouseEvent:event index:MouseButton::MIDDLE]; + } else if ((int)[event buttonNumber] == 3) { + [self processMouseEvent:event index:MouseButton::MB_XBUTTON1]; + } else if ((int)[event buttonNumber] == 4) { + [self processMouseEvent:event index:MouseButton::MB_XBUTTON2]; + } +} + +@end diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp index 80863441ce0d..e0bad55b2035 100644 --- a/platform/windows/display_server_windows.cpp +++ b/platform/windows/display_server_windows.cpp @@ -62,6 +62,8 @@ #define DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1 19 #endif +#define WM_INDICATOR_CALLBACK_MESSAGE (WM_USER + 1) + #if defined(__GNUC__) // Workaround GCC warning from -Wcast-function-type. #define GetProcAddress (void *)GetProcAddress @@ -107,6 +109,7 @@ bool DisplayServerWindows::has_feature(Feature p_feature) const { case FEATURE_KEEP_SCREEN_ON: case FEATURE_TEXT_TO_SPEECH: case FEATURE_SCREEN_CAPTURE: + case FEATURE_STATUS_INDICATOR: return true; default: return false; @@ -2842,6 +2845,172 @@ void DisplayServerWindows::set_icon(const Ref &p_icon) { } } +DisplayServer::IndicatorID DisplayServerWindows::create_status_indicator(const Ref &p_icon, const String &p_tooltip, const Callable &p_callback) { + HICON hicon = nullptr; + if (p_icon.is_valid() && p_icon->get_width() > 0 && p_icon->get_height() > 0) { + Ref img = p_icon; + if (img != icon) { + img = img->duplicate(); + img->convert(Image::FORMAT_RGBA8); + } + + int w = img->get_width(); + int h = img->get_height(); + + // Create temporary bitmap buffer. + int icon_len = 40 + h * w * 4; + Vector v; + v.resize(icon_len); + BYTE *icon_bmp = v.ptrw(); + + encode_uint32(40, &icon_bmp[0]); + encode_uint32(w, &icon_bmp[4]); + encode_uint32(h * 2, &icon_bmp[8]); + encode_uint16(1, &icon_bmp[12]); + encode_uint16(32, &icon_bmp[14]); + encode_uint32(BI_RGB, &icon_bmp[16]); + encode_uint32(w * h * 4, &icon_bmp[20]); + encode_uint32(0, &icon_bmp[24]); + encode_uint32(0, &icon_bmp[28]); + encode_uint32(0, &icon_bmp[32]); + encode_uint32(0, &icon_bmp[36]); + + uint8_t *wr = &icon_bmp[40]; + const uint8_t *r = img->get_data().ptr(); + + for (int i = 0; i < h; i++) { + for (int j = 0; j < w; j++) { + const uint8_t *rpx = &r[((h - i - 1) * w + j) * 4]; + uint8_t *wpx = &wr[(i * w + j) * 4]; + wpx[0] = rpx[2]; + wpx[1] = rpx[1]; + wpx[2] = rpx[0]; + wpx[3] = rpx[3]; + } + } + + hicon = CreateIconFromResource(icon_bmp, icon_len, TRUE, 0x00030000); + } + + IndicatorData idat; + idat.callback = p_callback; + + NOTIFYICONDATAW ndat; + ZeroMemory(&ndat, sizeof(NOTIFYICONDATAW)); + ndat.cbSize = sizeof(NOTIFYICONDATAW); + ndat.hWnd = windows[MAIN_WINDOW_ID].hWnd; + ndat.uID = indicator_id_counter; + ndat.uFlags = NIF_ICON | NIF_TIP | NIF_MESSAGE; + ndat.uCallbackMessage = WM_INDICATOR_CALLBACK_MESSAGE; + ndat.hIcon = hicon; + memcpy(ndat.szTip, p_tooltip.utf16().ptr(), MIN(p_tooltip.utf16().length(), 127) * sizeof(WCHAR)); + ndat.uVersion = NOTIFYICON_VERSION; + + Shell_NotifyIconW(NIM_ADD, &ndat); + Shell_NotifyIconW(NIM_SETVERSION, &ndat); + + IndicatorID iid = indicator_id_counter++; + indicators[iid] = idat; + + return iid; +} + +void DisplayServerWindows::status_indicator_set_icon(IndicatorID p_id, const Ref &p_icon) { + ERR_FAIL_COND(!indicators.has(p_id)); + + HICON hicon = nullptr; + if (p_icon.is_valid() && p_icon->get_width() > 0 && p_icon->get_height() > 0) { + Ref img = p_icon; + if (img != icon) { + img = img->duplicate(); + img->convert(Image::FORMAT_RGBA8); + } + + int w = img->get_width(); + int h = img->get_height(); + + // Create temporary bitmap buffer. + int icon_len = 40 + h * w * 4; + Vector v; + v.resize(icon_len); + BYTE *icon_bmp = v.ptrw(); + + encode_uint32(40, &icon_bmp[0]); + encode_uint32(w, &icon_bmp[4]); + encode_uint32(h * 2, &icon_bmp[8]); + encode_uint16(1, &icon_bmp[12]); + encode_uint16(32, &icon_bmp[14]); + encode_uint32(BI_RGB, &icon_bmp[16]); + encode_uint32(w * h * 4, &icon_bmp[20]); + encode_uint32(0, &icon_bmp[24]); + encode_uint32(0, &icon_bmp[28]); + encode_uint32(0, &icon_bmp[32]); + encode_uint32(0, &icon_bmp[36]); + + uint8_t *wr = &icon_bmp[40]; + const uint8_t *r = img->get_data().ptr(); + + for (int i = 0; i < h; i++) { + for (int j = 0; j < w; j++) { + const uint8_t *rpx = &r[((h - i - 1) * w + j) * 4]; + uint8_t *wpx = &wr[(i * w + j) * 4]; + wpx[0] = rpx[2]; + wpx[1] = rpx[1]; + wpx[2] = rpx[0]; + wpx[3] = rpx[3]; + } + } + + hicon = CreateIconFromResource(icon_bmp, icon_len, TRUE, 0x00030000); + } + + NOTIFYICONDATAW ndat; + ZeroMemory(&ndat, sizeof(NOTIFYICONDATAW)); + ndat.cbSize = sizeof(NOTIFYICONDATAW); + ndat.hWnd = windows[MAIN_WINDOW_ID].hWnd; + ndat.uID = p_id; + ndat.uFlags = NIF_ICON; + ndat.hIcon = hicon; + ndat.uVersion = NOTIFYICON_VERSION; + + Shell_NotifyIconW(NIM_MODIFY, &ndat); +} + +void DisplayServerWindows::status_indicator_set_tooltip(IndicatorID p_id, const String &p_tooltip) { + ERR_FAIL_COND(!indicators.has(p_id)); + + NOTIFYICONDATAW ndat; + ZeroMemory(&ndat, sizeof(NOTIFYICONDATAW)); + ndat.cbSize = sizeof(NOTIFYICONDATAW); + ndat.hWnd = windows[MAIN_WINDOW_ID].hWnd; + ndat.uID = p_id; + ndat.uFlags = NIF_TIP; + memcpy(ndat.szTip, p_tooltip.utf16().ptr(), MIN(p_tooltip.utf16().length(), 127) * sizeof(WCHAR)); + ndat.uVersion = NOTIFYICON_VERSION; + + Shell_NotifyIconW(NIM_MODIFY, &ndat); +} + +void DisplayServerWindows::status_indicator_set_callback(IndicatorID p_id, const Callable &p_callback) { + ERR_FAIL_COND(!indicators.has(p_id)); + + indicators[p_id].callback = p_callback; +} + +void DisplayServerWindows::delete_status_indicator(IndicatorID p_id) { + ERR_FAIL_COND(!indicators.has(p_id)); + + NOTIFYICONDATAW ndat; + ZeroMemory(&ndat, sizeof(NOTIFYICONDATAW)); + ndat.cbSize = sizeof(NOTIFYICONDATAW); + ndat.hWnd = windows[MAIN_WINDOW_ID].hWnd; + ndat.uID = p_id; + ndat.uVersion = NOTIFYICON_VERSION; + + Shell_NotifyIconW(NIM_DELETE, &ndat); + indicators.erase(p_id); +} + void DisplayServerWindows::window_set_vsync_mode(DisplayServer::VSyncMode p_vsync_mode, WindowID p_window) { _THREAD_SAFE_METHOD_ #if defined(RD_ENABLED) @@ -3351,6 +3520,30 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA } } } break; + case WM_INDICATOR_CALLBACK_MESSAGE: { + if (lParam == WM_LBUTTONDOWN || lParam == WM_RBUTTONDOWN || lParam == WM_MBUTTONDOWN || lParam == WM_XBUTTONDOWN) { + IndicatorID iid = (IndicatorID)wParam; + MouseButton mb = MouseButton::LEFT; + if (lParam == WM_RBUTTONDOWN) { + mb = MouseButton::RIGHT; + } else if (lParam == WM_MBUTTONDOWN) { + mb = MouseButton::MIDDLE; + } else if (lParam == WM_XBUTTONDOWN) { + mb = MouseButton::MB_XBUTTON1; + } + if (indicators.has(iid)) { + if (indicators[iid].callback.is_valid()) { + Variant v_button = mb; + Variant v_pos = mouse_get_position(); + Variant *v_args[2] = { &v_button, &v_pos }; + Variant ret; + Callable::CallError ce; + indicators[iid].callback.callp((const Variant **)&v_args, 2, ret, ce); + } + } + return 0; + } + } break; case WM_CLOSE: // Did we receive a close message? { if (windows[window_id].focus_timer_id != 0U) { @@ -5166,6 +5359,18 @@ DisplayServerWindows::~DisplayServerWindows() { cursors_cache.clear(); + // Destroy all status indicators. + for (HashMap::Iterator E = indicators.begin(); E;) { + NOTIFYICONDATAW ndat; + ZeroMemory(&ndat, sizeof(NOTIFYICONDATAW)); + ndat.cbSize = sizeof(NOTIFYICONDATAW); + ndat.hWnd = windows[MAIN_WINDOW_ID].hWnd; + ndat.uID = E->key; + ndat.uVersion = NOTIFYICON_VERSION; + + Shell_NotifyIconW(NIM_DELETE, &ndat); + } + if (mouse_monitor) { UnhookWindowsHookEx(mouse_monitor); } diff --git a/platform/windows/display_server_windows.h b/platform/windows/display_server_windows.h index 91e7424de9d3..e66c533da505 100644 --- a/platform/windows/display_server_windows.h +++ b/platform/windows/display_server_windows.h @@ -447,6 +447,13 @@ class DisplayServerWindows : public DisplayServer { WNDPROC user_proc = nullptr; + struct IndicatorData { + Callable callback; + }; + + IndicatorID indicator_id_counter = 0; + HashMap indicators; + void _send_window_event(const WindowData &wd, WindowEvent p_event); void _get_window_style(bool p_main_window, bool p_fullscreen, bool p_multiwindow_fs, bool p_borderless, bool p_resizable, bool p_maximized, bool p_no_activate_focus, DWORD &r_style, DWORD &r_style_ex); @@ -655,6 +662,12 @@ public: virtual void set_native_icon(const String &p_filename) override; virtual void set_icon(const Ref &p_icon) override; + virtual IndicatorID create_status_indicator(const Ref &p_icon, const String &p_tooltip, const Callable &p_callback) override; + virtual void status_indicator_set_icon(IndicatorID p_id, const Ref &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; + virtual void set_context(Context p_context) 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); diff --git a/scene/main/status_indicator.cpp b/scene/main/status_indicator.cpp new file mode 100644 index 000000000000..ae58bc0b1831 --- /dev/null +++ b/scene/main/status_indicator.cpp @@ -0,0 +1,135 @@ +/**************************************************************************/ +/* status_indicator.cpp */ +/**************************************************************************/ +/* 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. */ +/**************************************************************************/ + +#include "status_indicator.h" + +void StatusIndicator::_notification(int p_what) { + ERR_MAIN_THREAD_GUARD; +#ifdef TOOLS_ENABLED + if (is_part_of_edited_scene()) { + return; + } +#endif + + switch (p_what) { + case NOTIFICATION_ENTER_TREE: { + if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_STATUS_INDICATOR)) { + if (visible && iid == DisplayServer::INVALID_INDICATOR_ID) { + iid = DisplayServer::get_singleton()->create_status_indicator(icon, tooltip, callable_mp(this, &StatusIndicator::_callback)); + } + } + } break; + case NOTIFICATION_EXIT_TREE: { + if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_STATUS_INDICATOR)) { + if (iid != DisplayServer::INVALID_INDICATOR_ID) { + DisplayServer::get_singleton()->delete_status_indicator(iid); + iid = DisplayServer::INVALID_INDICATOR_ID; + } + } + } break; + default: + break; + } +} + +void StatusIndicator::_bind_methods() { + ClassDB::bind_method(D_METHOD("set_tooltip", "tooltip"), &StatusIndicator::set_tooltip); + ClassDB::bind_method(D_METHOD("get_tooltip"), &StatusIndicator::get_tooltip); + ClassDB::bind_method(D_METHOD("set_icon", "texture"), &StatusIndicator::set_icon); + ClassDB::bind_method(D_METHOD("get_icon"), &StatusIndicator::get_icon); + ClassDB::bind_method(D_METHOD("set_visible", "visible"), &StatusIndicator::set_visible); + ClassDB::bind_method(D_METHOD("is_visible"), &StatusIndicator::is_visible); + + ADD_SIGNAL(MethodInfo("pressed", PropertyInfo(Variant::INT, "mouse_button"), PropertyInfo(Variant::VECTOR2I, "position"))); + + ADD_PROPERTY(PropertyInfo(Variant::STRING, "tooltip", PROPERTY_HINT_MULTILINE_TEXT), "set_tooltip", "get_tooltip"); + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "icon", PROPERTY_HINT_RESOURCE_TYPE, "Image"), "set_icon", "get_icon"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "visible"), "set_visible", "is_visible"); +} + +void StatusIndicator::_callback(MouseButton p_index, const Point2i &p_pos) { + emit_signal(SNAME("pressed"), p_index, p_pos); +} + +void StatusIndicator::set_icon(const Ref &p_icon) { + ERR_MAIN_THREAD_GUARD; + icon = p_icon; + if (iid != DisplayServer::INVALID_INDICATOR_ID) { + DisplayServer::get_singleton()->status_indicator_set_icon(iid, icon); + } +} + +Ref StatusIndicator::get_icon() const { + return icon; +} + +void StatusIndicator::set_tooltip(const String &p_tooltip) { + ERR_MAIN_THREAD_GUARD; + tooltip = p_tooltip; + if (iid != DisplayServer::INVALID_INDICATOR_ID) { + DisplayServer::get_singleton()->status_indicator_set_tooltip(iid, tooltip); + } +} + +String StatusIndicator::get_tooltip() const { + return tooltip; +} + +void StatusIndicator::set_visible(bool p_visible) { + ERR_MAIN_THREAD_GUARD; + if (visible == p_visible) { + return; + } + visible = p_visible; + + if (!is_inside_tree()) { + return; + } + +#ifdef TOOLS_ENABLED + if (is_part_of_edited_scene()) { + return; + } +#endif + + if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_STATUS_INDICATOR)) { + if (visible && iid == DisplayServer::INVALID_INDICATOR_ID) { + iid = DisplayServer::get_singleton()->create_status_indicator(icon, tooltip, callable_mp(this, &StatusIndicator::_callback)); + } + if (!visible && iid != DisplayServer::INVALID_INDICATOR_ID) { + DisplayServer::get_singleton()->delete_status_indicator(iid); + iid = DisplayServer::INVALID_INDICATOR_ID; + } + } +} + +bool StatusIndicator::is_visible() const { + return visible; +} diff --git a/scene/main/status_indicator.h b/scene/main/status_indicator.h new file mode 100644 index 000000000000..aa3aa68d782f --- /dev/null +++ b/scene/main/status_indicator.h @@ -0,0 +1,62 @@ +/**************************************************************************/ +/* status_indicator.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 STATUS_INDICATOR_H +#define STATUS_INDICATOR_H + +#include "scene/main/node.h" +#include "servers/display_server.h" + +class StatusIndicator : public Node { + GDCLASS(StatusIndicator, Node); + + Ref icon; + String tooltip; + bool visible = true; + DisplayServer::IndicatorID iid = DisplayServer::INVALID_INDICATOR_ID; + +protected: + void _notification(int p_what); + static void _bind_methods(); + + void _callback(MouseButton p_index, const Point2i &p_pos); + +public: + void set_icon(const Ref &p_icon); + Ref get_icon() const; + + void set_tooltip(const String &p_tooltip); + String get_tooltip() const; + + void set_visible(bool p_visible); + bool is_visible() const; +}; + +#endif // STATUS_INDICATOR_H diff --git a/scene/register_scene_types.cpp b/scene/register_scene_types.cpp index c7d044caf252..d7e5eb469884 100644 --- a/scene/register_scene_types.cpp +++ b/scene/register_scene_types.cpp @@ -136,6 +136,7 @@ #include "scene/main/multiplayer_api.h" #include "scene/main/resource_preloader.h" #include "scene/main/scene_tree.h" +#include "scene/main/status_indicator.h" #include "scene/main/timer.h" #include "scene/main/viewport.h" #include "scene/main/window.h" @@ -352,6 +353,8 @@ void register_scene_types() { GDREGISTER_CLASS(ResourcePreloader); GDREGISTER_CLASS(Window); + GDREGISTER_CLASS(StatusIndicator); + /* REGISTER GUI */ GDREGISTER_CLASS(ButtonGroup); diff --git a/servers/display_server.cpp b/servers/display_server.cpp index 847be469db71..0496fb9180b5 100644 --- a/servers/display_server.cpp +++ b/servers/display_server.cpp @@ -587,6 +587,27 @@ void DisplayServer::set_icon(const Ref &p_icon) { WARN_PRINT("Icon not supported by this display server."); } +DisplayServer::IndicatorID DisplayServer::create_status_indicator(const Ref &p_icon, const String &p_tooltip, const Callable &p_callback) { + WARN_PRINT("Status indicator not supported by this display server."); + return INVALID_INDICATOR_ID; +} + +void DisplayServer::status_indicator_set_icon(IndicatorID p_id, const Ref &p_icon) { + WARN_PRINT("Status indicator not supported by this display server."); +} + +void DisplayServer::status_indicator_set_tooltip(IndicatorID p_id, const String &p_tooltip) { + WARN_PRINT("Status indicator not supported by this display server."); +} + +void DisplayServer::status_indicator_set_callback(IndicatorID p_id, const Callable &p_callback) { + WARN_PRINT("Status indicator not supported by this display server."); +} + +void DisplayServer::delete_status_indicator(IndicatorID p_id) { + WARN_PRINT("Status indicator not supported by this display server."); +} + int64_t DisplayServer::window_get_native_handle(HandleType p_handle_type, WindowID p_window) const { WARN_PRINT("Native handle not supported by this display server."); return 0; @@ -825,6 +846,12 @@ void DisplayServer::_bind_methods() { ClassDB::bind_method(D_METHOD("set_native_icon", "filename"), &DisplayServer::set_native_icon); ClassDB::bind_method(D_METHOD("set_icon", "image"), &DisplayServer::set_icon); + ClassDB::bind_method(D_METHOD("create_status_indicator", "icon", "tooltip", "callback"), &DisplayServer::create_status_indicator); + ClassDB::bind_method(D_METHOD("status_indicator_set_icon", "id", "icon"), &DisplayServer::status_indicator_set_icon); + ClassDB::bind_method(D_METHOD("status_indicator_set_tooltip", "id", "tooltip"), &DisplayServer::status_indicator_set_tooltip); + ClassDB::bind_method(D_METHOD("status_indicator_set_callback", "id", "callback"), &DisplayServer::status_indicator_set_callback); + ClassDB::bind_method(D_METHOD("delete_status_indicator", "id"), &DisplayServer::delete_status_indicator); + ClassDB::bind_method(D_METHOD("tablet_get_driver_count"), &DisplayServer::tablet_get_driver_count); ClassDB::bind_method(D_METHOD("tablet_get_driver_name", "idx"), &DisplayServer::tablet_get_driver_name); ClassDB::bind_method(D_METHOD("tablet_get_current_driver"), &DisplayServer::tablet_get_current_driver); @@ -851,6 +878,7 @@ void DisplayServer::_bind_methods() { BIND_ENUM_CONSTANT(FEATURE_TEXT_TO_SPEECH); BIND_ENUM_CONSTANT(FEATURE_EXTEND_TO_TITLE); BIND_ENUM_CONSTANT(FEATURE_SCREEN_CAPTURE); + BIND_ENUM_CONSTANT(FEATURE_STATUS_INDICATOR); BIND_ENUM_CONSTANT(MOUSE_MODE_VISIBLE); BIND_ENUM_CONSTANT(MOUSE_MODE_HIDDEN); @@ -865,6 +893,7 @@ void DisplayServer::_bind_methods() { BIND_CONSTANT(MAIN_WINDOW_ID); BIND_CONSTANT(INVALID_WINDOW_ID); + BIND_CONSTANT(INVALID_INDICATOR_ID); BIND_ENUM_CONSTANT(SCREEN_LANDSCAPE); BIND_ENUM_CONSTANT(SCREEN_PORTRAIT); diff --git a/servers/display_server.h b/servers/display_server.h index 6ec0831500a0..6e5a4655a288 100644 --- a/servers/display_server.h +++ b/servers/display_server.h @@ -125,6 +125,7 @@ public: FEATURE_TEXT_TO_SPEECH, FEATURE_EXTEND_TO_TITLE, FEATURE_SCREEN_CAPTURE, + FEATURE_STATUS_INDICATOR, }; virtual bool has_feature(Feature p_feature) const = 0; @@ -332,10 +333,12 @@ public: virtual bool screen_is_kept_on() const; enum { MAIN_WINDOW_ID = 0, - INVALID_WINDOW_ID = -1 + INVALID_WINDOW_ID = -1, + INVALID_INDICATOR_ID = -1 }; typedef int WindowID; + typedef int IndicatorID; virtual Vector get_window_list() const = 0; @@ -540,6 +543,12 @@ public: virtual void set_native_icon(const String &p_filename); virtual void set_icon(const Ref &p_icon); + virtual IndicatorID create_status_indicator(const Ref &p_icon, const String &p_tooltip, const Callable &p_callback); + virtual void status_indicator_set_icon(IndicatorID p_id, const Ref &p_icon); + virtual void status_indicator_set_tooltip(IndicatorID p_id, const String &p_tooltip); + virtual void status_indicator_set_callback(IndicatorID p_id, const Callable &p_callback); + virtual void delete_status_indicator(IndicatorID p_id); + enum Context { CONTEXT_EDITOR, CONTEXT_PROJECTMAN,