Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
cmake --version
if [ "${{ matrix.platform }}" = "linux" ]; then
sudo apt-get update
sudo apt-get install -y ninja-build libgtk-3-dev libx11-dev libxi-dev
sudo apt-get install -y ninja-build libgtk-3-dev libx11-dev libxi-dev libayatana-appindicator3-dev
fi

- name: Configure CMake
Expand Down
9 changes: 5 additions & 4 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,17 @@ file(GLOB CAPI_SOURCES "capi/*.cpp" "capi/*.c")

# Platform-specific source files
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
file(GLOB PLATFORM_SOURCES "platform/linux/*_linux.cpp" "platform/linux/display_linux.cpp")
file(GLOB PLATFORM_SOURCES "platform/linux/*_linux.cpp")
# Find packages for Linux
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
pkg_check_modules(X11 REQUIRED IMPORTED_TARGET x11)
pkg_check_modules(XI REQUIRED IMPORTED_TARGET xi)
pkg_check_modules(AYATANA_APPINDICATOR REQUIRED IMPORTED_TARGET ayatana-appindicator3-0.1)
elseif(APPLE)
file(GLOB PLATFORM_SOURCES "platform/macos/*_macos.mm" "platform/macos/display_macos.mm")
file(GLOB PLATFORM_SOURCES "platform/macos/*_macos.mm")
elseif(WIN32)
file(GLOB PLATFORM_SOURCES "platform/windows/*_windows.cpp" "platform/windows/display_windows.cpp")
file(GLOB PLATFORM_SOURCES "platform/windows/*_windows.cpp")
else()
set(PLATFORM_SOURCES "")
endif()
Expand Down Expand Up @@ -64,7 +65,7 @@ if(APPLE)
target_link_libraries(nativeapi PUBLIC "-framework Cocoa")
target_compile_options(nativeapi PRIVATE "-x" "objective-c++")
elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux")
target_link_libraries(nativeapi PUBLIC PkgConfig::GTK PkgConfig::X11 PkgConfig::XI pthread)
target_link_libraries(nativeapi PUBLIC PkgConfig::GTK PkgConfig::X11 PkgConfig::XI PkgConfig::AYATANA_APPINDICATOR pthread)
elseif(WIN32)
target_link_libraries(nativeapi PUBLIC user32 shell32 dwmapi)
endif ()
119 changes: 58 additions & 61 deletions src/platform/linux/tray_icon_linux.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <glib.h>
#include <gtk/gtk.h>
#include <gdk-pixbuf/gdk-pixbuf.h>
#include <libayatana-appindicator/app-indicator.h>
#include "../../menu.h"
#include "../../tray_icon.h"
#include "../../tray_icon_event.h"
Expand All @@ -12,41 +13,42 @@ namespace nativeapi {
// Private implementation class
class TrayIcon::Impl {
public:
Impl(GtkStatusIcon* tray) : gtk_status_icon_(tray), title_(""), tooltip_(""), context_menu_(nullptr) {}
Impl(AppIndicator* indicator) : app_indicator_(indicator), title_(""), tooltip_(""), context_menu_(nullptr), visible_(false) {}

GtkStatusIcon* gtk_status_icon_;
AppIndicator* app_indicator_;
std::shared_ptr<Menu> context_menu_; // Store menu shared_ptr to keep it alive
std::string title_; // GTK StatusIcon doesn't have title, so we store it
std::string title_;
std::string tooltip_;
bool visible_;
};

TrayIcon::TrayIcon() : pimpl_(std::make_unique<Impl>(nullptr)) {
id = -1;
}

TrayIcon::TrayIcon(void* tray) : pimpl_(std::make_unique<Impl>((GtkStatusIcon*)tray)) {
TrayIcon::TrayIcon(void* tray) : pimpl_(std::make_unique<Impl>((AppIndicator*)tray)) {
id = -1; // Will be set by TrayManager when created
// Make the status icon visible
if (pimpl_->gtk_status_icon_) {
gtk_status_icon_set_visible(pimpl_->gtk_status_icon_, TRUE);
// Make the indicator visible by default
if (pimpl_->app_indicator_) {
pimpl_->visible_ = true;
}
}

TrayIcon::~TrayIcon() {
if (pimpl_->gtk_status_icon_) {
g_object_unref(pimpl_->gtk_status_icon_);
if (pimpl_->app_indicator_) {
g_object_unref(pimpl_->app_indicator_);
}

}

void TrayIcon::SetIcon(std::string icon) {
if (!pimpl_->gtk_status_icon_) {
if (!pimpl_->app_indicator_) {
return;
}

// Check if the icon is a base64 string
if (icon.find("data:image") != std::string::npos) {
// Extract the base64 part
// For base64 images, we need to save them to a temporary file
// since AppIndicator expects file paths or stock icon names
size_t pos = icon.find("base64,");
if (pos != std::string::npos) {
std::string base64Icon = icon.substr(pos + 7);
Expand All @@ -56,45 +58,40 @@ void TrayIcon::SetIcon(std::string icon) {
guchar* decoded_data = g_base64_decode(base64Icon.c_str(), &decoded_len);

if (decoded_data) {
// Create pixbuf from decoded data
GInputStream* stream = g_memory_input_stream_new_from_data(
decoded_data, decoded_len, g_free);
// Create a temporary file path
const char* temp_dir = g_get_tmp_dir();
std::string temp_path = std::string(temp_dir) + "/nativeapi_tray_icon_" + std::to_string(id) + ".png";

// Write to file
GError* error = nullptr;
GdkPixbuf* pixbuf = gdk_pixbuf_new_from_stream(stream, nullptr, &error);

if (pixbuf && !error) {
// Scale pixbuf to appropriate size (24x24 is common for tray icons)
GdkPixbuf* scaled_pixbuf = gdk_pixbuf_scale_simple(
pixbuf, 24, 24, GDK_INTERP_BILINEAR);

gtk_status_icon_set_from_pixbuf(pimpl_->gtk_status_icon_, scaled_pixbuf);

g_object_unref(scaled_pixbuf);
g_object_unref(pixbuf);
if (g_file_set_contents(temp_path.c_str(), (const gchar*)decoded_data, decoded_len, &error)) {
app_indicator_set_icon_full(pimpl_->app_indicator_, temp_path.c_str(), "Tray Icon");
} else if (error) {
std::cerr << "Error loading icon from base64: " << error->message << std::endl;
std::cerr << "Error saving icon to temp file: " << error->message << std::endl;
g_error_free(error);
}

g_object_unref(stream);
g_free(decoded_data);
}
}
} else {
// Use the icon as a file path or stock icon name
if (g_file_test(icon.c_str(), G_FILE_TEST_EXISTS)) {
// It's a file path
gtk_status_icon_set_from_file(pimpl_->gtk_status_icon_, icon.c_str());
app_indicator_set_icon_full(pimpl_->app_indicator_, icon.c_str(), "Tray Icon");
} else {
// Try as a stock icon name
gtk_status_icon_set_from_icon_name(pimpl_->gtk_status_icon_, icon.c_str());
app_indicator_set_icon_full(pimpl_->app_indicator_, icon.c_str(), "Tray Icon");
}
}
}

void TrayIcon::SetTitle(std::string title) {
pimpl_->title_ = title;
// GTK StatusIcon doesn't support title directly, so we just store it
// Some desktop environments might show this in tooltips or context
// AppIndicator uses the title as the accessible name and in some desktop environments
if (pimpl_->app_indicator_) {
app_indicator_set_title(pimpl_->app_indicator_, title.c_str());
}
}

std::string TrayIcon::GetTitle() {
Expand All @@ -103,9 +100,9 @@ std::string TrayIcon::GetTitle() {

void TrayIcon::SetTooltip(std::string tooltip) {
pimpl_->tooltip_ = tooltip;
if (pimpl_->gtk_status_icon_) {
gtk_status_icon_set_tooltip_text(pimpl_->gtk_status_icon_, tooltip.c_str());
}
// AppIndicator doesn't have direct tooltip support like GtkStatusIcon
// The tooltip functionality is typically handled through the title
// or through custom menu items. We'll store it for potential future use.
}

std::string TrayIcon::GetTooltip() {
Expand All @@ -116,8 +113,11 @@ void TrayIcon::SetContextMenu(std::shared_ptr<Menu> menu) {
// Store the menu shared_ptr to keep it alive
pimpl_->context_menu_ = menu;

// Note: Full GTK integration would need to connect popup-menu signal
// and show the GTK menu from the Menu object's GetNativeObject()
// AppIndicator requires a menu to be set
if (pimpl_->app_indicator_ && menu && menu->GetNativeObject()) {
GtkMenu* gtk_menu = static_cast<GtkMenu*>(menu->GetNativeObject());
app_indicator_set_menu(pimpl_->app_indicator_, gtk_menu);
}
}

std::shared_ptr<Menu> TrayIcon::GetContextMenu() {
Expand All @@ -127,41 +127,37 @@ std::shared_ptr<Menu> TrayIcon::GetContextMenu() {
Rectangle TrayIcon::GetBounds() {
Rectangle bounds = {0, 0, 0, 0};

if (pimpl_->gtk_status_icon_) {
GdkScreen* screen;
GdkRectangle area;
GtkOrientation orientation;

if (gtk_status_icon_get_geometry(pimpl_->gtk_status_icon_, &screen, &area, &orientation)) {
bounds.x = area.x;
bounds.y = area.y;
bounds.width = area.width;
bounds.height = area.height;
}
}
// AppIndicator doesn't provide geometry information like GtkStatusIcon did
// This is a limitation of the AppIndicator API as it's handled by the
// system tray implementation. We return empty bounds.
// In most modern desktop environments, this information isn't available
// to applications for security reasons.

return bounds;
}

bool TrayIcon::Show() {
if (pimpl_->gtk_status_icon_) {
gtk_status_icon_set_visible(pimpl_->gtk_status_icon_, TRUE);
if (pimpl_->app_indicator_) {
app_indicator_set_status(pimpl_->app_indicator_, APP_INDICATOR_STATUS_ACTIVE);
pimpl_->visible_ = true;
return true;
}
return false;
}

bool TrayIcon::Hide() {
if (pimpl_->gtk_status_icon_) {
gtk_status_icon_set_visible(pimpl_->gtk_status_icon_, FALSE);
if (pimpl_->app_indicator_) {
app_indicator_set_status(pimpl_->app_indicator_, APP_INDICATOR_STATUS_PASSIVE);
pimpl_->visible_ = false;
return true;
}
return false;
}

bool TrayIcon::IsVisible() {
if (pimpl_->gtk_status_icon_) {
return gtk_status_icon_get_visible(pimpl_->gtk_status_icon_) == TRUE;
if (pimpl_->app_indicator_) {
AppIndicatorStatus status = app_indicator_get_status(pimpl_->app_indicator_);
return status == APP_INDICATOR_STATUS_ACTIVE;
}
return false;
}
Expand All @@ -171,19 +167,20 @@ bool TrayIcon::ShowContextMenu(double x, double y) {
return false;
}

// Note: GTK implementation would need to show the menu at the specified coordinates
// This is a simplified implementation
return false;
// AppIndicator shows context menu automatically on right-click
// We don't need to manually show it at specific coordinates
// The menu is managed by the indicator framework
return true;
}

bool TrayIcon::ShowContextMenu() {
if (!pimpl_->context_menu_ || !pimpl_->context_menu_->GetNativeObject()) {
return false;
}

// Note: GTK implementation would need to show the menu at cursor position
// This is a simplified implementation
return false;
// AppIndicator shows context menu automatically on right-click
// We don't need to manually show it as it's managed by the indicator framework
return true;
}

// Internal method to handle click events
Expand Down
42 changes: 31 additions & 11 deletions src/platform/linux/tray_manager_linux.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
#include "../../tray_icon.h"
#include "../../tray_manager.h"

// Import GTK headers - these may not be available in all build environments
// Import headers
#ifdef __has_include
#if __has_include(<gtk/gtk.h>)
#include <gtk/gtk.h>
Expand All @@ -17,6 +17,18 @@
#define HAS_GTK 0
#endif

#ifdef __has_include
#if __has_include(<libayatana-appindicator/app-indicator.h>)
#include <libayatana-appindicator/app-indicator.h>
#define HAS_AYATANA_APPINDICATOR 1
#else
#define HAS_AYATANA_APPINDICATOR 0
#endif
#else
// Fallback for older compilers
#define HAS_AYATANA_APPINDICATOR 0
#endif

namespace nativeapi {

class TrayManager::Impl {
Expand All @@ -33,39 +45,47 @@ TrayManager::~TrayManager() {
for (auto& pair : trays_) {
auto tray = pair.second;
if (tray) {
// The TrayIcon destructor will handle cleanup of the GtkStatusIcon
// The TrayIcon destructor will handle cleanup of the AppIndicator
}
}
trays_.clear();
}

bool TrayManager::IsSupported() {
#if HAS_GTK
// Check if GTK is initialized and system tray is supported
#if HAS_GTK && HAS_AYATANA_APPINDICATOR
// Check if GTK is initialized and AppIndicator is available
return gtk_init_check(nullptr, nullptr);
#else
// If GTK is not available, assume no system tray support
// If GTK or AppIndicator is not available, assume no system tray support
return false;
#endif
}

std::shared_ptr<TrayIcon> TrayManager::Create() {
std::lock_guard<std::mutex> lock(mutex_);

#if HAS_GTK
// Create a new tray using GTK StatusIcon
GtkStatusIcon* status_icon = gtk_status_icon_new();
if (!status_icon) {
#if HAS_GTK && HAS_AYATANA_APPINDICATOR
// Create a unique ID for this tray icon
std::string indicator_id = "nativeapi-tray-" + std::to_string(next_tray_id_);

// Create a new tray using AppIndicator
AppIndicator* app_indicator = app_indicator_new(
indicator_id.c_str(),
"application-default-icon", // Default icon name
APP_INDICATOR_CATEGORY_APPLICATION_STATUS
);

if (!app_indicator) {
return nullptr;
}

auto tray = std::make_shared<TrayIcon>((void*)status_icon);
auto tray = std::make_shared<TrayIcon>((void*)app_indicator);
tray->id = next_tray_id_++;
trays_[tray->id] = tray;

return tray;
#else
// GTK not available, return nullptr
// AppIndicator not available, return nullptr
return nullptr;
#endif
}
Expand Down
7 changes: 4 additions & 3 deletions src/platform/linux/window_linux.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,17 @@ class Window::Impl {
};

Window::Window() : pimpl_(std::make_unique<Impl>(nullptr)) {
id = -1;
}

Window::Window(void* window) : pimpl_(std::make_unique<Impl>((GdkWindow*)window)) {
// Use pointer address as ID since GDK doesn't provide direct window IDs
id = pimpl_->gdk_window_ ? (WindowID)pimpl_->gdk_window_ : 0;
}

Window::~Window() {
}

WindowID Window::GetId() const{
// Use pointer address as ID since GDK doesn't provide direct window IDs
return pimpl_->gdk_window_ ? (WindowID)pimpl_->gdk_window_ : 0;
}

void Window::Focus() {
Expand Down
Loading