mirror of
https://github.com/LizardByte/tray.git
synced 2026-10-23 03:27:02 +09:00
fix(linux): migrate to qt tray (#104)
This commit is contained in:
1 parent
6fa152fcec
commit
d05166f3bb
15 files changed
+1725
-498
No files matched your search
+41
-22
@@ -36,7 +36,7 @@ jobs:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
build:
|
||||
name: Build (${{ matrix.os }} - ${{ matrix.appindicator || 'default' }})
|
||||
name: Build (${{ matrix.os }}${{ matrix.qt_version && format(', Qt{0}', matrix.qt_version) || '' }})
|
||||
defaults:
|
||||
run:
|
||||
shell: ${{ matrix.shell }}
|
||||
@@ -49,16 +49,16 @@ jobs:
|
||||
include:
|
||||
- os: macos-latest
|
||||
shell: "bash"
|
||||
qt_version: ''
|
||||
- os: ubuntu-latest
|
||||
appindicator: "libayatana-appindicator3-dev"
|
||||
appindicator_type: "ayatana"
|
||||
shell: "bash"
|
||||
qt_version: '5'
|
||||
- os: ubuntu-latest
|
||||
appindicator: "libappindicator3-dev"
|
||||
appindicator_type: "legacy"
|
||||
shell: "bash"
|
||||
qt_version: '6'
|
||||
- os: windows-latest
|
||||
shell: "msys2 {0}"
|
||||
qt_version: ''
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
@@ -67,23 +67,41 @@ jobs:
|
||||
|
||||
- name: Setup Dependencies Linux
|
||||
if: runner.os == 'Linux'
|
||||
env:
|
||||
QT_VERSION: ${{ matrix.qt_version }}
|
||||
run: |
|
||||
dependencies=(
|
||||
"adwaita-icon-theme"
|
||||
"build-essential"
|
||||
"cmake"
|
||||
"imagemagick"
|
||||
"ninja-build"
|
||||
"xvfb"
|
||||
)
|
||||
|
||||
if [[ "${QT_VERSION}" == "5" ]]; then
|
||||
dependencies+=(
|
||||
"libqt5svg5-dev"
|
||||
"qt5-gtk-platformtheme"
|
||||
"qtbase5-dev"
|
||||
)
|
||||
elif [[ "${QT_VERSION}" == "6" ]]; then
|
||||
dependencies+=(
|
||||
"qt6-base-dev"
|
||||
"qt6-svg-dev"
|
||||
)
|
||||
else
|
||||
echo "Unsupported QT_VERSION: ${QT_VERSION}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
build-essential \
|
||||
cmake \
|
||||
${{ matrix.appindicator }} \
|
||||
imagemagick \
|
||||
libglib2.0-dev \
|
||||
libnotify-dev \
|
||||
ninja-build \
|
||||
xvfb
|
||||
sudo apt-get install -y "${dependencies[@]}"
|
||||
|
||||
- name: Setup virtual desktop
|
||||
if: runner.os == 'Linux'
|
||||
uses: LizardByte/actions/actions/virtual_desktop@0affa4f7bcb27562658960eee840eff8ff844578 # v2026.328.161128
|
||||
with:
|
||||
appindicator-version: ${{ matrix.appindicator_type }}
|
||||
environment: mate
|
||||
|
||||
- name: Setup Dependencies macOS
|
||||
@@ -169,7 +187,7 @@ jobs:
|
||||
echo "python-path=${python_path}"
|
||||
echo "python-path=${python_path}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Build
|
||||
- name: Configure
|
||||
run: |
|
||||
mkdir -p build
|
||||
|
||||
@@ -186,7 +204,9 @@ jobs:
|
||||
-B build \
|
||||
-G Ninja \
|
||||
-S .
|
||||
ninja -C build
|
||||
|
||||
- name: Build
|
||||
run: ninja -C build
|
||||
|
||||
- name: Init tray icon (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
@@ -228,6 +248,8 @@ jobs:
|
||||
# TODO: tests randomly hang on Linux, https://github.com/LizardByte/tray/issues/45
|
||||
timeout-minutes: 3
|
||||
working-directory: build/tests
|
||||
env:
|
||||
QT_QPA_PLATFORMTHEME: ${{ runner.os == 'Linux' && matrix.qt_version == '5' && 'gtk3' || '' }}
|
||||
run: ./test_tray --gtest_color=yes --gtest_output=xml:test_results.xml
|
||||
|
||||
- name: Upload screenshots
|
||||
@@ -236,7 +258,7 @@ jobs:
|
||||
(steps.test.outcome == 'success' || steps.test.outcome == 'failure')
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: tray-screenshots-${{ runner.os }}${{ matrix.appindicator && format('-{0}', matrix.appindicator) || '' }}
|
||||
name: tray-screenshots-${{ runner.os }}${{ matrix.qt_version && format('-qt{0}', matrix.qt_version) || '' }}
|
||||
path: build/tests/screenshots
|
||||
if-no-files-found: error
|
||||
|
||||
@@ -263,10 +285,7 @@ jobs:
|
||||
- name: Set codecov flags
|
||||
id: codecov_flags
|
||||
run: |
|
||||
flags="${{ runner.os }}"
|
||||
if [ -n "${{ matrix.appindicator }}" ]; then
|
||||
flags="${flags},${{ matrix.appindicator }}"
|
||||
fi
|
||||
flags="${{ runner.os }}${{ matrix.qt_version && format('-qt{0}', matrix.qt_version) || '' }}"
|
||||
echo "flags=${flags}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Upload coverage
|
||||
|
||||
+76
-42
@@ -5,99 +5,133 @@ cmake_minimum_required(VERSION 3.13 FATAL_ERROR) # target_link_directories
|
||||
project(tray VERSION 0.0.0
|
||||
DESCRIPTION "A cross-platform system tray library"
|
||||
HOMEPAGE_URL "https://app.lizardbyte.dev"
|
||||
LANGUAGES C)
|
||||
LANGUAGES C CXX)
|
||||
|
||||
set(PROJECT_LICENSE "MIT")
|
||||
|
||||
set(TRAY_IS_TOP_LEVEL OFF)
|
||||
if(CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR)
|
||||
set(TRAY_IS_TOP_LEVEL ON)
|
||||
endif()
|
||||
|
||||
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||
message(STATUS "Setting build type to 'Release' as none was specified.")
|
||||
set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the type of build." FORCE)
|
||||
endif()
|
||||
|
||||
# Add our custom CMake modules to the global path
|
||||
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
|
||||
# Add our custom CMake modules without overriding parent project settings.
|
||||
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
|
||||
|
||||
#
|
||||
# Project optional configuration
|
||||
#
|
||||
if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME)
|
||||
option(BUILD_DOCS "Build documentation" ON)
|
||||
option(BUILD_TESTS "Build tests" ON)
|
||||
endif()
|
||||
option(BUILD_DOCS "Build documentation" ${TRAY_IS_TOP_LEVEL})
|
||||
option(BUILD_TESTS "Build tests" ${TRAY_IS_TOP_LEVEL})
|
||||
option(BUILD_EXAMPLE "Build example app" ${TRAY_IS_TOP_LEVEL})
|
||||
|
||||
# Generate 'compile_commands.json' for clang_complete
|
||||
set(CMAKE_COLOR_MAKEFILE ON)
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
|
||||
find_package (PkgConfig REQUIRED)
|
||||
find_package(PkgConfig)
|
||||
|
||||
file(GLOB TRAY_SOURCES
|
||||
"${CMAKE_SOURCE_DIR}/src/*.h"
|
||||
"${CMAKE_SOURCE_DIR}/icons/*.ico"
|
||||
"${CMAKE_SOURCE_DIR}/icons/*.png")
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/*.h"
|
||||
)
|
||||
|
||||
set(TRAY_ICON_ICO "${CMAKE_CURRENT_SOURCE_DIR}/icons/icon.ico")
|
||||
set(TRAY_ICON_PNG "${CMAKE_CURRENT_SOURCE_DIR}/icons/icon.png")
|
||||
set(TRAY_ICON_SVG "${CMAKE_CURRENT_SOURCE_DIR}/icons/icon.svg")
|
||||
set(TRAY_ICON_FILES
|
||||
"${TRAY_ICON_ICO}"
|
||||
"${TRAY_ICON_PNG}"
|
||||
"${TRAY_ICON_SVG}"
|
||||
)
|
||||
|
||||
set(_TRAY_ICON_ICO "${TRAY_ICON_ICO}" CACHE INTERNAL "Default tray ICO icon path")
|
||||
set(_TRAY_ICON_PNG "${TRAY_ICON_PNG}" CACHE INTERNAL "Default tray PNG icon path")
|
||||
set(_TRAY_ICON_SVG "${TRAY_ICON_SVG}" CACHE INTERNAL "Default tray SVG icon path")
|
||||
|
||||
# Copy default tray icon files into the output directory of the specified target.
|
||||
function(tray_copy_default_icons target_name)
|
||||
if(NOT TARGET "${target_name}")
|
||||
message(FATAL_ERROR "tray_copy_default_icons expected an existing target: ${target_name}")
|
||||
endif()
|
||||
|
||||
foreach(icon_file IN LISTS TRAY_ICON_FILES)
|
||||
add_custom_command(TARGET "${target_name}" POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${icon_file}"
|
||||
"$<TARGET_FILE_DIR:${target_name}>"
|
||||
COMMENT "Copying ${icon_file} to $<TARGET_FILE_DIR:${target_name}>")
|
||||
endforeach()
|
||||
endfunction()
|
||||
|
||||
if(WIN32)
|
||||
list(APPEND TRAY_SOURCES "${CMAKE_SOURCE_DIR}/src/tray_windows.c")
|
||||
list(APPEND TRAY_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/tray_windows.c")
|
||||
else()
|
||||
if(UNIX)
|
||||
if(APPLE)
|
||||
find_library(COCOA Cocoa REQUIRED)
|
||||
list(APPEND TRAY_SOURCES "${CMAKE_SOURCE_DIR}/src/tray_darwin.m")
|
||||
list(APPEND TRAY_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/tray_darwin.m")
|
||||
else()
|
||||
find_package(APPINDICATOR REQUIRED)
|
||||
find_package(LibNotify REQUIRED)
|
||||
list(APPEND TRAY_SOURCES "${CMAKE_SOURCE_DIR}/src/tray_linux.c")
|
||||
find_package(Qt6 COMPONENTS Widgets DBus Svg)
|
||||
if(Qt6_FOUND)
|
||||
set(TRAY_QT_VERSION 6)
|
||||
else()
|
||||
find_package(Qt5 REQUIRED COMPONENTS Widgets DBus Svg)
|
||||
set(TRAY_QT_VERSION 5)
|
||||
endif()
|
||||
set(TRAY_QT_VERSION # cmake-lint: disable=C0103
|
||||
"${TRAY_QT_VERSION}"
|
||||
CACHE INTERNAL "Qt major version selected by tray"
|
||||
)
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
list(APPEND TRAY_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/tray_linux.cpp")
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_library(${PROJECT_NAME} STATIC ${TRAY_SOURCES})
|
||||
set_property(TARGET ${PROJECT_NAME} PROPERTY C_STANDARD 99)
|
||||
set_property(TARGET ${PROJECT_NAME} PROPERTY CXX_STANDARD 17)
|
||||
target_include_directories(${PROJECT_NAME}
|
||||
PUBLIC
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src>
|
||||
$<INSTALL_INTERFACE:include>)
|
||||
|
||||
if(WIN32)
|
||||
list(APPEND TRAY_DEFINITIONS TRAY_WINAPI=1 WIN32_LEAN_AND_MEAN NOMINMAX)
|
||||
if(MSVC)
|
||||
list(APPEND TRAY_COMPILE_OPTIONS "/MT$<$<CONFIG:Debug>:d>")
|
||||
endif()
|
||||
else()
|
||||
if(UNIX)
|
||||
if(APPLE)
|
||||
list(APPEND TRAY_DEFINITIONS TRAY_APPKIT=1)
|
||||
list(APPEND TRAY_EXTERNAL_LIBRARIES ${COCOA})
|
||||
else()
|
||||
list(APPEND TRAY_COMPILE_OPTIONS ${APPINDICATOR_CFLAGS})
|
||||
list(APPEND TRAY_EXTERNAL_DIRECTORIES ${APPINDICATOR_LIBRARY_DIRS})
|
||||
list(APPEND TRAY_DEFINITIONS TRAY_APPINDICATOR=1)
|
||||
if(APPINDICATOR_AYATANA)
|
||||
list(APPEND TRAY_DEFINITIONS TRAY_AYATANA_APPINDICATOR=1)
|
||||
if(TRAY_QT_VERSION EQUAL 6)
|
||||
list(APPEND TRAY_EXTERNAL_LIBRARIES Qt6::Widgets Qt6::DBus Qt6::Svg)
|
||||
else()
|
||||
list(APPEND TRAY_EXTERNAL_LIBRARIES Qt5::Widgets Qt5::DBus Qt5::Svg)
|
||||
endif()
|
||||
if(APPINDICATOR_LEGACY)
|
||||
list(APPEND TRAY_DEFINITIONS TRAY_LEGACY_APPINDICATOR=1)
|
||||
endif()
|
||||
list(APPEND TRAY_LIBNOTIFY=1)
|
||||
list(APPEND TRAY_EXTERNAL_LIBRARIES ${APPINDICATOR_LIBRARIES} ${LIBNOTIFY_LIBRARIES})
|
||||
|
||||
include_directories(SYSTEM ${APPINDICATOR_INCLUDE_DIRS} ${LIBNOTIFY_INCLUDE_DIRS})
|
||||
link_directories(${APPINDICATOR_LIBRARY_DIRS} ${LIBNOTIFY_LIBRARY_DIRS})
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_library(tray::tray ALIAS ${PROJECT_NAME})
|
||||
|
||||
add_executable(tray_example "${CMAKE_SOURCE_DIR}/src/example.c")
|
||||
target_link_libraries(tray_example tray::tray)
|
||||
if(BUILD_EXAMPLE)
|
||||
add_executable(tray_example "${CMAKE_CURRENT_SOURCE_DIR}/src/example.c")
|
||||
target_link_libraries(tray_example tray::tray)
|
||||
tray_copy_default_icons(tray_example)
|
||||
endif()
|
||||
|
||||
configure_file("${CMAKE_SOURCE_DIR}/icons/icon.ico" "${CMAKE_BINARY_DIR}/icon.ico" COPYONLY)
|
||||
configure_file("${CMAKE_SOURCE_DIR}/icons/icon.png" "${CMAKE_BINARY_DIR}/icon.png" COPYONLY)
|
||||
if(TRAY_IS_TOP_LEVEL)
|
||||
install(TARGETS tray DESTINATION lib)
|
||||
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/src/tray.h" DESTINATION include)
|
||||
install(FILES ${TRAY_ICON_FILES} DESTINATION share/tray/icons)
|
||||
endif()
|
||||
|
||||
INSTALL(TARGETS tray tray DESTINATION lib)
|
||||
|
||||
IF(NOT WIN32)
|
||||
INSTALL(FILES tray.h DESTINATION include)
|
||||
ENDIF()
|
||||
|
||||
target_compile_definitions(${PROJECT_NAME} PRIVATE ${TRAY_DEFINITIONS})
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE ${TRAY_COMPILE_OPTIONS})
|
||||
target_link_directories(${PROJECT_NAME} PRIVATE ${TRAY_EXTERNAL_DIRECTORIES})
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE ${TRAY_EXTERNAL_LIBRARIES})
|
||||
|
||||
@@ -15,11 +15,10 @@ The code is C++ friendly and will compile fine in C++98 and up. This is a fork o
|
||||
This fork adds the following features:
|
||||
|
||||
- system tray notifications
|
||||
- support for both linux appindicator versions
|
||||
- unit tests
|
||||
- code coverage
|
||||
- refactored code, e.g. moved source code into the `src` directory
|
||||
- doxygen documentation, and readthedocs configuration
|
||||
- refactored code, e.g., moved source code into the `src` directory
|
||||
- doxygen documentation and readthedocs configuration
|
||||
|
||||
## Screenshots
|
||||
|
||||
@@ -33,32 +32,47 @@ This fork adds the following features:
|
||||
|
||||
## Supported platforms
|
||||
|
||||
* Linux/Gtk (libayatana-appindicator3 or libappindicator3)
|
||||
* Linux/Qt (Qt5 or Qt6 Widgets)
|
||||
* Windows XP or newer (shellapi.h)
|
||||
* MacOS (Cocoa/AppKit)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
* CMake
|
||||
* [Ninja](https://ninja-build.org/), in order to have the same build commands on all platforms
|
||||
* [Ninja](https://ninja-build.org/), to have the same build commands on all platforms.
|
||||
|
||||
### Linux Dependencies
|
||||
|
||||
Install either Qt6 _or_ Qt5 development packages. The Linux backend requires
|
||||
Qt Widgets, DBus, and Svg modules.
|
||||
|
||||
<div class="tabbed">
|
||||
|
||||
- <b class="tab-title">Arch</b>
|
||||
```bash
|
||||
sudo pacman -S libayatana-appindicator
|
||||
# Qt6
|
||||
sudo pacman -S qt6-base qt6-svg
|
||||
|
||||
# Qt5
|
||||
sudo pacman -S qt5-base qt5-svg
|
||||
```
|
||||
|
||||
- <b class="tab-title">Debian/Ubuntu</b>
|
||||
```bash
|
||||
sudo apt install libappindicator3-dev
|
||||
# Qt6
|
||||
sudo apt install qt6-base-dev qt6-svg-dev
|
||||
|
||||
# Qt5
|
||||
sudo apt install qtbase5-dev libqt5svg5-dev
|
||||
```
|
||||
|
||||
- <b class="tab-title">Fedora</b>
|
||||
```bash
|
||||
sudo dnf install libappindicator-gtk3-devel
|
||||
# Qt6
|
||||
sudo dnf install qt6-qtbase-devel qt6-qtsvg-devel
|
||||
|
||||
# Qt5
|
||||
sudo dnf install qt5-qtbase-devel qt5-qtsvg-devel
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
# Remmina - The GTK+ Remote Desktop Client
|
||||
#
|
||||
# Copyright (C) 2011 Marc-Andre Moreau
|
||||
# Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo
|
||||
# Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
||||
# Boston, MA 02110-1301, USA.
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
|
||||
pkg_check_modules(APPINDICATOR ayatana-appindicator3-0.1)
|
||||
if(APPINDICATOR_FOUND)
|
||||
SET(APPINDICATOR_AYATANA 1)
|
||||
else()
|
||||
pkg_check_modules(APPINDICATOR appindicator3-0.1)
|
||||
if(APPINDICATOR_FOUND)
|
||||
SET(APPINDICATOR_LEGACY 1)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
mark_as_advanced(APPINDICATOR_INCLUDE_DIR APPINDICATOR_LIBRARY)
|
||||
@@ -1,55 +0,0 @@
|
||||
# - Try to find LibNotify
|
||||
# This module defines the following variables:
|
||||
#
|
||||
# LIBNOTIFY_FOUND - LibNotify was found
|
||||
# LIBNOTIFY_INCLUDE_DIRS - the LibNotify include directories
|
||||
# LIBNOTIFY_LIBRARIES - link these to use LibNotify
|
||||
#
|
||||
# Copyright (C) 2012 Raphael Kubo da Costa <rakuco@webkit.org>
|
||||
# Copyright (C) 2014 Collabora Ltd.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND ITS CONTRIBUTORS ``AS
|
||||
# IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
||||
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR ITS
|
||||
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
|
||||
# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
||||
# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
||||
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
find_package(PkgConfig)
|
||||
pkg_check_modules(LIBNOTIFY QUIET libnotify)
|
||||
|
||||
find_path(LIBNOTIFY_INCLUDE_DIRS
|
||||
NAMES notify.h
|
||||
HINTS ${LIBNOTIFY_INCLUDEDIR}
|
||||
${LIBNOTIFY_INCLUDE_DIRS}
|
||||
PATH_SUFFIXES libnotify
|
||||
)
|
||||
|
||||
find_library(LIBNOTIFY_LIBRARIES
|
||||
NAMES notify
|
||||
HINTS ${LIBNOTIFY_LIBDIR}
|
||||
${LIBNOTIFY_LIBRARY_DIRS}
|
||||
)
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
FIND_PACKAGE_HANDLE_STANDARD_ARGS(LibNotify REQUIRED_VARS LIBNOTIFY_INCLUDE_DIRS LIBNOTIFY_LIBRARIES
|
||||
VERSION_VAR LIBNOTIFY_VERSION)
|
||||
|
||||
mark_as_advanced(
|
||||
LIBNOTIFY_INCLUDE_DIRS
|
||||
LIBNOTIFY_LIBRARIES
|
||||
)
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||
<rect width="64" height="64" rx="12" fill="#2f6fed"/>
|
||||
<path d="M18 34h8l6 8 14-20h8L32 52z" fill="#ffffff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 204 B |
+4
-17
@@ -6,26 +6,15 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#if defined(_WIN32) || defined(_WIN64)
|
||||
#define TRAY_WINAPI 1 ///< Use WinAPI.
|
||||
#elif defined(__linux__) || defined(linux) || defined(__linux)
|
||||
#define TRAY_APPINDICATOR 1
|
||||
#elif defined(__APPLE__) || defined(__MACH__)
|
||||
#define TRAY_APPKIT 1
|
||||
#endif
|
||||
|
||||
// local includes
|
||||
#include "tray.h"
|
||||
|
||||
#if TRAY_APPINDICATOR
|
||||
#define TRAY_ICON1 "mail-message-new"
|
||||
#define TRAY_ICON2 "mail-message-new"
|
||||
#elif TRAY_APPKIT
|
||||
#define TRAY_ICON1 "icon.png"
|
||||
#define TRAY_ICON2 "icon.png"
|
||||
#elif TRAY_WINAPI
|
||||
#if defined(_WIN32)
|
||||
#define TRAY_ICON1 "icon.ico" ///< Path to first icon.
|
||||
#define TRAY_ICON2 "icon.ico" ///< Path to second icon.
|
||||
#else
|
||||
#define TRAY_ICON1 "icon.png"
|
||||
#define TRAY_ICON2 "icon.png"
|
||||
#endif
|
||||
|
||||
static struct tray tray;
|
||||
@@ -62,9 +51,7 @@ static void submenu_cb(struct tray_menu *item) {
|
||||
// Test tray init
|
||||
static struct tray tray = {
|
||||
.icon = TRAY_ICON1,
|
||||
#if TRAY_WINAPI
|
||||
.tooltip = "Tray",
|
||||
#endif
|
||||
.menu =
|
||||
(struct tray_menu[]) {
|
||||
{.text = "Hello", .cb = hello_cb},
|
||||
|
||||
+50
-2
@@ -5,7 +5,7 @@
|
||||
#ifndef TRAY_H
|
||||
#define TRAY_H
|
||||
|
||||
#if defined(TRAY_WINAPI)
|
||||
#if defined(_WIN32)
|
||||
#include <Windows.h>
|
||||
#endif
|
||||
|
||||
@@ -73,12 +73,60 @@ extern "C" {
|
||||
*/
|
||||
void tray_show_menu(void);
|
||||
|
||||
/**
|
||||
* @brief Simulate a notification click, invoking the notification callback (for testing purposes).
|
||||
*
|
||||
* On Linux (Qt): triggers the stored notification callback as if the user clicked the notification.
|
||||
* On other platforms: no-op.
|
||||
*/
|
||||
void tray_simulate_notification_click(void);
|
||||
|
||||
/**
|
||||
* @brief Simulate clicking a top-level menu item by index (for testing purposes).
|
||||
*
|
||||
* On Linux (Qt): triggers the QAction associated with the given top-level menu
|
||||
* index (separators and submenus are ignored).
|
||||
* On other platforms: no-op.
|
||||
*
|
||||
* @param index Zero-based index in the top-level tray menu.
|
||||
*/
|
||||
void tray_simulate_menu_item_click(int index);
|
||||
|
||||
/**
|
||||
* @brief Terminate UI loop.
|
||||
*/
|
||||
void tray_exit(void);
|
||||
|
||||
#if defined(TRAY_WINAPI)
|
||||
/**
|
||||
* @brief Set a callback for log messages produced by the tray library.
|
||||
*
|
||||
* On Linux the callback is installed as a Qt message handler so all Qt
|
||||
* diagnostic output is routed through it. On other platforms this function
|
||||
* is a no-op.
|
||||
*
|
||||
* @param cb Callback invoked with level (0=debug, 1=info, 2=warning, 3=error)
|
||||
* and the message string. Pass NULL to restore the default logging behaviour.
|
||||
*/
|
||||
void tray_set_log_callback(void (*cb)(int level, const char *msg));
|
||||
|
||||
/**
|
||||
* @brief Set application metadata used by the tray library.
|
||||
*
|
||||
* Must be called before tray_init(). On Linux (Qt), sets the Qt application
|
||||
* name, display name, and desktop file name used for D-Bus registration. On
|
||||
* other platforms this function is a no-op.
|
||||
*
|
||||
* @param app_name Application name used as a technical identifier (e.g., for
|
||||
* D-Bus registration). Converted to lowercase automatically. NULL uses the
|
||||
* default ("tray").
|
||||
* @param app_display_name Human-readable name shown in notifications and UI.
|
||||
* NULL derives from the tray tooltip or falls back to app_name.
|
||||
* @param desktop_name Desktop file name for D-Bus. NULL appends ".desktop"
|
||||
* to app_name.
|
||||
*/
|
||||
void tray_set_app_info(const char *app_name, const char *app_display_name, const char *desktop_name);
|
||||
|
||||
#if defined(_WIN32)
|
||||
/**
|
||||
* @brief Get the tray window handle.
|
||||
* @return The window handle.
|
||||
|
||||
@@ -130,6 +130,28 @@ void tray_show_menu(void) {
|
||||
[statusItem popUpStatusItemMenu:statusItem.menu];
|
||||
}
|
||||
|
||||
void tray_simulate_notification_click(void) {
|
||||
// macOS notification clicks are handled by the OS notification center.
|
||||
// Simulation is not supported here.
|
||||
}
|
||||
|
||||
void tray_simulate_menu_item_click(int index) {
|
||||
// Programmatic menu-item simulation is not supported here.
|
||||
(void) index;
|
||||
}
|
||||
|
||||
void tray_set_log_callback(void (*cb)(int level, const char *msg)) {
|
||||
// Qt is not used on macOS; log routing is not applicable.
|
||||
(void) cb;
|
||||
}
|
||||
|
||||
void tray_set_app_info(const char *app_name, const char *app_display_name, const char *desktop_name) {
|
||||
// Application metadata is not applicable on macOS.
|
||||
(void) app_name;
|
||||
(void) app_display_name;
|
||||
(void) desktop_name;
|
||||
}
|
||||
|
||||
void tray_exit(void) {
|
||||
// Remove the status item from the status bar on the main thread
|
||||
// NSStatusBar operations must be performed on the main thread
|
||||
|
||||
@@ -1,275 +0,0 @@
|
||||
/**
|
||||
* @file src/tray_linux.c
|
||||
* @brief System tray implementation for Linux.
|
||||
*/
|
||||
// standard includes
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
// local includes
|
||||
#include "tray.h"
|
||||
|
||||
// lib includes
|
||||
#ifdef TRAY_AYATANA_APPINDICATOR
|
||||
#include <libayatana-appindicator/app-indicator.h>
|
||||
#elif TRAY_LEGACY_APPINDICATOR
|
||||
#include <libappindicator/app-indicator.h>
|
||||
#endif
|
||||
#ifndef IS_APP_INDICATOR
|
||||
#define IS_APP_INDICATOR APP_IS_INDICATOR ///< Define IS_APP_INDICATOR for app-indicator compatibility.
|
||||
#endif
|
||||
#include <libnotify/notify.h>
|
||||
|
||||
// Use a per-process AppIndicator id to avoid DBus collisions when tests create multiple
|
||||
// tray instances in the same desktop/session.
|
||||
static unsigned long tray_appindicator_seq = 0;
|
||||
|
||||
static bool async_update_pending = false;
|
||||
static pthread_cond_t async_update_cv = PTHREAD_COND_INITIALIZER;
|
||||
static pthread_mutex_t async_update_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static AppIndicator *indicator = NULL;
|
||||
static int loop_result = 0;
|
||||
static NotifyNotification *currentNotification = NULL;
|
||||
static GtkMenu *current_menu = NULL;
|
||||
static GtkMenu *current_popup = NULL;
|
||||
static GtkWidget *menu_anchor_window = NULL;
|
||||
|
||||
static void _tray_menu_cb(GtkMenuItem *item, gpointer data) {
|
||||
(void) item;
|
||||
struct tray_menu *m = (struct tray_menu *) data;
|
||||
m->cb(m);
|
||||
}
|
||||
|
||||
static GtkMenuShell *_tray_menu(struct tray_menu *m) {
|
||||
GtkMenuShell *menu = (GtkMenuShell *) gtk_menu_new();
|
||||
for (; m != NULL && m->text != NULL; m++) {
|
||||
GtkWidget *item;
|
||||
if (strcmp(m->text, "-") == 0) {
|
||||
item = gtk_separator_menu_item_new();
|
||||
} else {
|
||||
if (m->submenu != NULL) {
|
||||
item = gtk_menu_item_new_with_label(m->text);
|
||||
gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), GTK_WIDGET(_tray_menu(m->submenu)));
|
||||
} else if (m->checkbox) {
|
||||
item = gtk_check_menu_item_new_with_label(m->text);
|
||||
gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item), !!m->checked);
|
||||
} else {
|
||||
item = gtk_menu_item_new_with_label(m->text);
|
||||
}
|
||||
gtk_widget_set_sensitive(item, !m->disabled);
|
||||
if (m->cb != NULL) {
|
||||
g_signal_connect(item, "activate", G_CALLBACK(_tray_menu_cb), m);
|
||||
}
|
||||
}
|
||||
gtk_widget_show(item);
|
||||
gtk_menu_shell_append(menu, item);
|
||||
}
|
||||
return menu;
|
||||
}
|
||||
|
||||
int tray_init(struct tray *tray) {
|
||||
if (gtk_init_check(0, NULL) == FALSE) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// If a previous tray instance wasn't fully torn down (common in unit tests),
|
||||
// drop our references before creating a new indicator.
|
||||
if (indicator != NULL) {
|
||||
g_object_unref(G_OBJECT(indicator));
|
||||
indicator = NULL;
|
||||
}
|
||||
loop_result = 0;
|
||||
notify_init("tray-icon");
|
||||
// The id is used as part of the exported DBus object path.
|
||||
// Make it unique per *tray instance* to prevent collisions inside a single test process.
|
||||
// Avoid underscores and other characters that may be normalized/stripped.
|
||||
char appindicator_id[64];
|
||||
tray_appindicator_seq++;
|
||||
snprintf(appindicator_id, sizeof(appindicator_id), "trayid%ld%lu", (long) getpid(), tray_appindicator_seq);
|
||||
|
||||
indicator = app_indicator_new(appindicator_id, tray->icon, APP_INDICATOR_CATEGORY_APPLICATION_STATUS);
|
||||
if (indicator == NULL || !IS_APP_INDICATOR(indicator)) {
|
||||
return -1;
|
||||
}
|
||||
app_indicator_set_status(indicator, APP_INDICATOR_STATUS_ACTIVE);
|
||||
tray_update(tray);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int tray_loop(int blocking) {
|
||||
gtk_main_iteration_do(blocking);
|
||||
return loop_result;
|
||||
}
|
||||
|
||||
static gboolean tray_update_internal(gpointer user_data) {
|
||||
struct tray *tray = user_data;
|
||||
|
||||
if (indicator != NULL && IS_APP_INDICATOR(indicator)) {
|
||||
app_indicator_set_icon_full(indicator, tray->icon, tray->icon);
|
||||
// GTK is all about reference counting, so previous menu should be destroyed
|
||||
// here
|
||||
GtkMenu *menu = GTK_MENU(_tray_menu(tray->menu));
|
||||
app_indicator_set_menu(indicator, menu);
|
||||
if (current_menu != NULL) {
|
||||
g_object_unref(current_menu);
|
||||
}
|
||||
current_menu = menu;
|
||||
g_object_ref(current_menu); // Keep a reference for showing
|
||||
}
|
||||
if (tray->notification_text != 0 && strlen(tray->notification_text) > 0 && notify_is_initted()) {
|
||||
if (currentNotification != NULL && NOTIFY_IS_NOTIFICATION(currentNotification)) {
|
||||
notify_notification_close(currentNotification, NULL);
|
||||
g_object_unref(G_OBJECT(currentNotification));
|
||||
}
|
||||
const char *notification_icon = tray->notification_icon != NULL ? tray->notification_icon : tray->icon;
|
||||
currentNotification = notify_notification_new(tray->notification_title, tray->notification_text, notification_icon);
|
||||
if (currentNotification != NULL && NOTIFY_IS_NOTIFICATION(currentNotification)) {
|
||||
if (tray->notification_cb != NULL) {
|
||||
notify_notification_add_action(currentNotification, "default", "Default", NOTIFY_ACTION_CALLBACK(tray->notification_cb), NULL, NULL);
|
||||
}
|
||||
notify_notification_show(currentNotification, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
// Unwait any pending tray_update() calls
|
||||
pthread_mutex_lock(&async_update_mutex);
|
||||
async_update_pending = false;
|
||||
pthread_cond_broadcast(&async_update_cv);
|
||||
pthread_mutex_unlock(&async_update_mutex);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
void tray_update(struct tray *tray) {
|
||||
// Perform the tray update on the tray loop thread, but block
|
||||
// in this thread to ensure none of the strings stored in the
|
||||
// tray icon struct go out of scope before the callback runs.
|
||||
|
||||
if (g_main_context_is_owner(g_main_context_default())) {
|
||||
// Invoke the callback directly if we're on the loop thread
|
||||
tray_update_internal(tray);
|
||||
} else {
|
||||
// If there's already an update pending, wait for it to complete
|
||||
// and claim the next pending update slot.
|
||||
pthread_mutex_lock(&async_update_mutex);
|
||||
while (async_update_pending) {
|
||||
pthread_cond_wait(&async_update_cv, &async_update_mutex);
|
||||
}
|
||||
async_update_pending = true;
|
||||
pthread_mutex_unlock(&async_update_mutex);
|
||||
|
||||
// Queue the update callback to the tray thread
|
||||
g_main_context_invoke(NULL, tray_update_internal, tray);
|
||||
|
||||
// Wait for the callback to run
|
||||
pthread_mutex_lock(&async_update_mutex);
|
||||
while (async_update_pending) {
|
||||
pthread_cond_wait(&async_update_cv, &async_update_mutex);
|
||||
}
|
||||
pthread_mutex_unlock(&async_update_mutex);
|
||||
}
|
||||
}
|
||||
|
||||
static void _tray_popup(GtkMenu *menu) {
|
||||
if (menu == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Dismiss any previously shown popup
|
||||
if (current_popup != NULL) {
|
||||
gtk_menu_popdown(current_popup);
|
||||
current_popup = NULL;
|
||||
}
|
||||
if (menu_anchor_window != NULL) {
|
||||
gtk_widget_destroy(menu_anchor_window);
|
||||
menu_anchor_window = NULL;
|
||||
}
|
||||
|
||||
GtkWidget *anchor_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
|
||||
if (anchor_window != NULL) {
|
||||
gtk_window_set_type_hint(GTK_WINDOW(anchor_window), GDK_WINDOW_TYPE_HINT_POPUP_MENU);
|
||||
gtk_window_set_decorated(GTK_WINDOW(anchor_window), FALSE);
|
||||
gtk_window_set_skip_taskbar_hint(GTK_WINDOW(anchor_window), TRUE);
|
||||
gtk_window_set_skip_pager_hint(GTK_WINDOW(anchor_window), TRUE);
|
||||
gtk_window_move(GTK_WINDOW(anchor_window), 100, 100);
|
||||
gtk_window_resize(GTK_WINDOW(anchor_window), 1, 1);
|
||||
gtk_widget_show(anchor_window);
|
||||
menu_anchor_window = anchor_window;
|
||||
|
||||
while (gtk_events_pending()) {
|
||||
gtk_main_iteration();
|
||||
}
|
||||
|
||||
if (gtk_check_version(3, 22, 0) == NULL) {
|
||||
GdkWindow *gdk_window = gtk_widget_get_window(anchor_window);
|
||||
if (gdk_window != NULL) {
|
||||
GdkRectangle rect = {0, 0, 1, 1};
|
||||
gtk_menu_popup_at_rect(menu, gdk_window, &rect, GDK_GRAVITY_NORTH_WEST, GDK_GRAVITY_NORTH_WEST, NULL);
|
||||
} else {
|
||||
gtk_menu_popup(menu, NULL, NULL, NULL, NULL, 0, gtk_get_current_event_time());
|
||||
}
|
||||
} else {
|
||||
gtk_menu_popup(menu, NULL, NULL, NULL, NULL, 0, gtk_get_current_event_time());
|
||||
}
|
||||
current_popup = menu;
|
||||
} else {
|
||||
gtk_menu_popup(menu, NULL, NULL, NULL, NULL, 0, gtk_get_current_event_time());
|
||||
current_popup = menu;
|
||||
}
|
||||
}
|
||||
|
||||
void tray_show_menu(void) {
|
||||
_tray_popup(current_menu);
|
||||
}
|
||||
|
||||
static gboolean tray_exit_internal(gpointer user_data) {
|
||||
(void) user_data;
|
||||
|
||||
if (currentNotification != NULL && NOTIFY_IS_NOTIFICATION(currentNotification)) {
|
||||
int v = notify_notification_close(currentNotification, NULL);
|
||||
if (v == TRUE) {
|
||||
g_object_unref(G_OBJECT(currentNotification));
|
||||
}
|
||||
currentNotification = NULL;
|
||||
}
|
||||
|
||||
if (current_popup != NULL) {
|
||||
gtk_menu_popdown(current_popup);
|
||||
current_popup = NULL;
|
||||
}
|
||||
|
||||
if (current_menu != NULL) {
|
||||
g_object_unref(current_menu);
|
||||
current_menu = NULL;
|
||||
}
|
||||
|
||||
if (menu_anchor_window != NULL) {
|
||||
gtk_widget_destroy(menu_anchor_window);
|
||||
menu_anchor_window = NULL;
|
||||
}
|
||||
|
||||
if (indicator != NULL) {
|
||||
// Make the indicator passive before unref to encourage a clean DBus unexport.
|
||||
app_indicator_set_status(indicator, APP_INDICATOR_STATUS_PASSIVE);
|
||||
g_object_unref(G_OBJECT(indicator));
|
||||
indicator = NULL;
|
||||
}
|
||||
notify_uninit();
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
void tray_exit(void) {
|
||||
// Wait for any pending callbacks to complete
|
||||
pthread_mutex_lock(&async_update_mutex);
|
||||
while (async_update_pending) {
|
||||
pthread_cond_wait(&async_update_cv, &async_update_mutex);
|
||||
}
|
||||
pthread_mutex_unlock(&async_update_mutex);
|
||||
|
||||
// Perform cleanup on the main thread
|
||||
loop_result = -1;
|
||||
g_main_context_invoke(NULL, tray_exit_internal, NULL);
|
||||
}
|
||||
+1007
File diff suppressed because it is too large.
Load diff
@@ -3,10 +3,17 @@
|
||||
* @brief System tray implementation for Windows.
|
||||
*/
|
||||
// standard includes
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN ///< Excludes rarely used APIs from Windows headers.
|
||||
#endif
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX ///< Prevents Windows.h from defining min and max macros.
|
||||
#endif
|
||||
#include <Windows.h>
|
||||
// clang-format off
|
||||
// build fails if shellapi.h is included before Windows.h
|
||||
#include <shellapi.h>
|
||||
#include <stdlib.h>
|
||||
// clang-format on
|
||||
|
||||
// local includes
|
||||
@@ -331,6 +338,28 @@ void tray_show_menu(void) {
|
||||
PostMessage(hwnd, WM_TRAY_CALLBACK_MESSAGE, 0, WM_RBUTTONUP);
|
||||
}
|
||||
|
||||
void tray_simulate_notification_click(void) {
|
||||
// Windows handles notification clicks via NIN_BALLOONUSERCLICK in the window proc.
|
||||
// Simulating this from outside the message pump is not supported here.
|
||||
}
|
||||
|
||||
void tray_simulate_menu_item_click(int index) {
|
||||
// Programmatic menu-item simulation is not supported here.
|
||||
(void) index;
|
||||
}
|
||||
|
||||
void tray_set_log_callback(void (*cb)(int level, const char *msg)) {
|
||||
// Qt is not used on Windows; log routing is not applicable.
|
||||
(void) cb;
|
||||
}
|
||||
|
||||
void tray_set_app_info(const char *app_name, const char *app_display_name, const char *desktop_name) {
|
||||
// Application metadata is not applicable on Windows.
|
||||
(void) app_name;
|
||||
(void) app_display_name;
|
||||
(void) desktop_name;
|
||||
}
|
||||
|
||||
void tray_exit(void) {
|
||||
Shell_NotifyIconW(NIM_DELETE, &nid);
|
||||
SendMessage(hwnd, WM_CLOSE, 0, 0);
|
||||
|
||||
+23
-20
@@ -3,21 +3,19 @@ cmake_minimum_required(VERSION 3.13)
|
||||
|
||||
project(test_tray)
|
||||
|
||||
include_directories("${CMAKE_SOURCE_DIR}")
|
||||
|
||||
# Add GoogleTest directory to the project
|
||||
set(GTEST_SOURCE_DIR "${CMAKE_SOURCE_DIR}/third-party/googletest")
|
||||
set(GTEST_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../third-party/googletest")
|
||||
|
||||
# For Windows: prevent overriding parent compiler/linker runtime settings.
|
||||
if(WIN32)
|
||||
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) # cmake-lint: disable=C0103
|
||||
endif()
|
||||
|
||||
set(INSTALL_GTEST OFF)
|
||||
set(INSTALL_GMOCK OFF)
|
||||
add_subdirectory("${GTEST_SOURCE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/googletest")
|
||||
include_directories("${GTEST_SOURCE_DIR}/googletest/include" "${GTEST_SOURCE_DIR}")
|
||||
|
||||
# if windows
|
||||
if (WIN32)
|
||||
# For Windows: Prevent overriding the parent project's compiler/linker settings
|
||||
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) # cmake-lint: disable=C0103
|
||||
endif ()
|
||||
|
||||
# extra libraries for tests
|
||||
if (APPLE)
|
||||
set(TEST_LIBS "-framework Cocoa")
|
||||
@@ -26,23 +24,28 @@ elseif (WIN32)
|
||||
endif()
|
||||
|
||||
file(GLOB_RECURSE TEST_SOURCES
|
||||
${CMAKE_SOURCE_DIR}/tests/conftest.cpp
|
||||
${CMAKE_SOURCE_DIR}/tests/utils.cpp
|
||||
${CMAKE_SOURCE_DIR}/tests/screenshot_utils.cpp
|
||||
${CMAKE_SOURCE_DIR}/tests/test_*.cpp)
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/conftest.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/utils.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/screenshot_utils.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/unit/test_*.cpp"
|
||||
)
|
||||
|
||||
add_executable(${PROJECT_NAME}
|
||||
${TEST_SOURCES}
|
||||
${TRAY_SOURCES})
|
||||
)
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES CXX_STANDARD 17)
|
||||
target_link_directories(${PROJECT_NAME} PRIVATE ${TRAY_EXTERNAL_DIRECTORIES})
|
||||
target_include_directories(${PROJECT_NAME}
|
||||
PRIVATE
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/..")
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
${TEST_LIBS}
|
||||
${TRAY_EXTERNAL_LIBRARIES}
|
||||
tray::tray
|
||||
gtest
|
||||
gtest_main) # if we use this we don't need our own main function
|
||||
target_compile_definitions(${PROJECT_NAME} PUBLIC ${TRAY_DEFINITIONS} ${TEST_DEFINITIONS})
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE $<$<COMPILE_LANGUAGE:CXX>:${TRAY_COMPILE_OPTIONS}>)
|
||||
gtest_main # if we use this we don't need our own main function
|
||||
)
|
||||
target_compile_definitions(${PROJECT_NAME} PUBLIC ${TEST_DEFINITIONS})
|
||||
target_link_options(${PROJECT_NAME} PRIVATE)
|
||||
|
||||
add_test(NAME ${PROJECT_NAME} COMMAND tray_test)
|
||||
tray_copy_default_icons(${PROJECT_NAME})
|
||||
|
||||
add_test(NAME ${PROJECT_NAME} COMMAND ${PROJECT_NAME})
|
||||
+172
-23
@@ -16,7 +16,7 @@
|
||||
// clang-format on
|
||||
#define TRAY_WINAPI 1
|
||||
#elif defined(__linux__) || defined(linux) || defined(__linux)
|
||||
#define TRAY_APPINDICATOR 1
|
||||
#define TRAY_QT 1
|
||||
#elif defined(__APPLE__) || defined(__MACH__)
|
||||
#include <Carbon/Carbon.h>
|
||||
#define TRAY_APPKIT 1
|
||||
@@ -26,9 +26,11 @@
|
||||
#include "src/tray.h"
|
||||
#include "tests/screenshot_utils.h"
|
||||
|
||||
#if TRAY_APPINDICATOR
|
||||
constexpr const char *TRAY_ICON1 = "mail-message-new";
|
||||
constexpr const char *TRAY_ICON2 = "mail-message-new";
|
||||
#if TRAY_QT
|
||||
constexpr const char *TRAY_ICON1 = "icon.png";
|
||||
constexpr const char *TRAY_ICON2 = "icon.png";
|
||||
constexpr const char *TRAY_ICON_SVG = "icon.svg";
|
||||
constexpr const char *TRAY_ICON_THEMED = "mail-message-new";
|
||||
#elif TRAY_APPKIT
|
||||
constexpr const char *TRAY_ICON1 = "icon.png";
|
||||
constexpr const char *TRAY_ICON2 = "icon.png";
|
||||
@@ -78,6 +80,18 @@ protected: // NOSONAR(cpp:S3656) - TEST_F generates subclasses that need access
|
||||
if (!trayRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure per-test notification state is cleared before teardown so
|
||||
// screenshot tests do not inherit prior notification popups.
|
||||
testTray.notification_title = nullptr;
|
||||
testTray.notification_text = nullptr;
|
||||
testTray.notification_icon = nullptr;
|
||||
testTray.notification_cb = nullptr;
|
||||
tray_update(&testTray);
|
||||
for (int i = 0; i < 20; ++i) {
|
||||
tray_loop(0);
|
||||
}
|
||||
|
||||
tray_exit();
|
||||
tray_loop(0);
|
||||
trayRunning = false;
|
||||
@@ -159,34 +173,43 @@ protected: // NOSONAR(cpp:S3656) - TEST_F generates subclasses that need access
|
||||
GTEST_SKIP() << "Screenshot output path not initialized";
|
||||
}
|
||||
|
||||
#if defined(TRAY_WINAPI) || defined(TRAY_APPKIT)
|
||||
// Ensure icon files exist in test binary directory
|
||||
std::filesystem::path projectRoot = testBinaryDir.parent_path();
|
||||
std::filesystem::path iconSource;
|
||||
auto ensureIconInTestDir = [&projectRoot, this](const char *iconName) {
|
||||
std::filesystem::path iconSource;
|
||||
|
||||
if (std::filesystem::exists(projectRoot / "icons" / TRAY_ICON1)) {
|
||||
iconSource = projectRoot / "icons" / TRAY_ICON1;
|
||||
} else if (std::filesystem::exists(projectRoot / TRAY_ICON1)) {
|
||||
iconSource = projectRoot / TRAY_ICON1;
|
||||
} else if (std::filesystem::exists(std::filesystem::path(TRAY_ICON1))) {
|
||||
iconSource = std::filesystem::path(TRAY_ICON1);
|
||||
}
|
||||
if (std::filesystem::exists(projectRoot / "icons" / iconName)) {
|
||||
iconSource = projectRoot / "icons" / iconName;
|
||||
} else if (std::filesystem::exists(projectRoot / iconName)) {
|
||||
iconSource = projectRoot / iconName;
|
||||
} else if (std::filesystem::exists(std::filesystem::path(iconName))) {
|
||||
iconSource = std::filesystem::path(iconName);
|
||||
}
|
||||
|
||||
if (!iconSource.empty()) {
|
||||
std::filesystem::path iconDest = testBinaryDir / TRAY_ICON1;
|
||||
if (!std::filesystem::exists(iconDest)) {
|
||||
std::error_code ec;
|
||||
std::filesystem::copy_file(iconSource, iconDest, ec);
|
||||
if (ec) {
|
||||
std::cout << "Warning: Failed to copy icon file: " << ec.message() << std::endl;
|
||||
if (!iconSource.empty()) {
|
||||
std::filesystem::path iconDest = testBinaryDir / iconName;
|
||||
if (!std::filesystem::exists(iconDest)) {
|
||||
std::error_code ec;
|
||||
std::filesystem::copy_file(iconSource, iconDest, ec);
|
||||
if (ec) {
|
||||
std::cout << "Warning: Failed to copy icon file: " << ec.message() << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ensureIconInTestDir(TRAY_ICON1);
|
||||
#if defined(TRAY_QT)
|
||||
ensureIconInTestDir(TRAY_ICON_SVG);
|
||||
#endif
|
||||
|
||||
trayRunning = false;
|
||||
testTray.icon = TRAY_ICON1;
|
||||
testTray.tooltip = "TestTray";
|
||||
testTray.notification_title = nullptr;
|
||||
testTray.notification_text = nullptr;
|
||||
testTray.notification_icon = nullptr;
|
||||
testTray.notification_cb = nullptr;
|
||||
testTray.menu = g_submenu;
|
||||
g_submenu[1].checked = 1;
|
||||
}
|
||||
@@ -199,7 +222,7 @@ protected: // NOSONAR(cpp:S3656) - TEST_F generates subclasses that need access
|
||||
// Process pending events to allow tray icon to appear.
|
||||
// Call this ONLY before screenshots to ensure the icon is visible.
|
||||
void WaitForTrayReady() {
|
||||
#if defined(TRAY_APPINDICATOR)
|
||||
#if defined(TRAY_QT)
|
||||
for (int i = 0; i < 100; i++) {
|
||||
tray_loop(0);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||
@@ -405,7 +428,7 @@ TEST_F(TrayTest, TestNotificationCallback) {
|
||||
|
||||
tray_update(&testTray);
|
||||
|
||||
// Note: callback would be invoked by user interaction in real scenario
|
||||
// Note: callback would be invoked by user interaction in a real scenario
|
||||
// In test environment, we verify it's set correctly
|
||||
EXPECT_NE(testTray.notification_cb, nullptr);
|
||||
|
||||
@@ -598,3 +621,129 @@ TEST_F(TrayTest, TestTrayShowMenu) {
|
||||
TEST_F(TrayTest, TestTrayExit) {
|
||||
tray_exit();
|
||||
}
|
||||
|
||||
#if defined(TRAY_QT)
|
||||
|
||||
TEST_F(TrayTest, TestTrayIconThemed) {
|
||||
testTray.icon = TRAY_ICON_THEMED;
|
||||
int result = tray_init(&testTray);
|
||||
trayRunning = (result == 0);
|
||||
ASSERT_EQ(result, 0);
|
||||
WaitForTrayReady();
|
||||
EXPECT_TRUE(captureScreenshot("tray_icon_themed"));
|
||||
testTray.icon = TRAY_ICON1;
|
||||
}
|
||||
|
||||
TEST_F(TrayTest, TestTrayIconSvgFile) {
|
||||
testTray.icon = TRAY_ICON_SVG;
|
||||
int result = tray_init(&testTray);
|
||||
trayRunning = (result == 0);
|
||||
ASSERT_EQ(result, 0);
|
||||
WaitForTrayReady();
|
||||
EXPECT_TRUE(captureScreenshot("tray_icon_svg"));
|
||||
testTray.icon = TRAY_ICON1;
|
||||
}
|
||||
|
||||
TEST_F(TrayTest, TestNotificationWithThemedIcon) {
|
||||
int initResult = tray_init(&testTray);
|
||||
trayRunning = (initResult == 0);
|
||||
ASSERT_EQ(initResult, 0);
|
||||
|
||||
testTray.notification_title = "Test Notification";
|
||||
testTray.notification_text = "This is a test notification message";
|
||||
testTray.notification_icon = TRAY_ICON_THEMED;
|
||||
tray_update(&testTray);
|
||||
|
||||
WaitForTrayReady();
|
||||
EXPECT_TRUE(captureScreenshot("tray_notification_themed_icon"));
|
||||
|
||||
testTray.notification_title = nullptr;
|
||||
testTray.notification_text = nullptr;
|
||||
testTray.notification_icon = nullptr;
|
||||
tray_update(&testTray);
|
||||
}
|
||||
|
||||
TEST_F(TrayTest, TestMenuAppearsOnLeftClick) {
|
||||
// Regression test for: clicking the tray icon did not bring up the menu.
|
||||
// The activated(Trigger) signal was not connected to the menu popup logic.
|
||||
// tray_show_menu() exercises the same code path that the activated handler calls.
|
||||
int initResult = tray_init(&testTray);
|
||||
trayRunning = (initResult == 0);
|
||||
ASSERT_EQ(initResult, 0);
|
||||
|
||||
captureMenuStateAndExit("tray_menu_left_click"); // NOSONAR(cpp:S6168) - helper uses std::thread for AppleClang 17 compatibility
|
||||
}
|
||||
|
||||
TEST_F(TrayTest, TestNotificationCallbackFiredOnClick) {
|
||||
// Regression test for: clicking a notification did not invoke the callback.
|
||||
// On the D-Bus path, QSystemTrayIcon::messageClicked is never emitted; the
|
||||
// callback must be routed through TrayNotificationHandler::onActionInvoked.
|
||||
static bool callbackInvoked = false;
|
||||
callbackInvoked = false;
|
||||
|
||||
int initResult = tray_init(&testTray);
|
||||
trayRunning = (initResult == 0);
|
||||
ASSERT_EQ(initResult, 0);
|
||||
|
||||
testTray.notification_title = "Clickable Notification";
|
||||
testTray.notification_text = "Click to test callback";
|
||||
testTray.notification_icon = TRAY_ICON1;
|
||||
testTray.notification_cb = []() {
|
||||
callbackInvoked = true;
|
||||
};
|
||||
tray_update(&testTray);
|
||||
|
||||
// Allow the notification to be sent before simulating the click.
|
||||
WaitForTrayReady();
|
||||
|
||||
tray_simulate_notification_click();
|
||||
tray_loop(0);
|
||||
|
||||
EXPECT_TRUE(callbackInvoked);
|
||||
|
||||
testTray.notification_title = nullptr;
|
||||
testTray.notification_text = nullptr;
|
||||
testTray.notification_icon = nullptr;
|
||||
testTray.notification_cb = nullptr;
|
||||
tray_update(&testTray);
|
||||
}
|
||||
|
||||
TEST_F(TrayTest, TestMenuCallbackAfterNotificationUpdate) {
|
||||
static int callbackCount = 0;
|
||||
callbackCount = 0;
|
||||
|
||||
auto first_item_callback = [](struct tray_menu *item) { // NOSONAR(cpp:S1172) - unused param required by tray_menu.cb function pointer type
|
||||
callbackCount++;
|
||||
(void) item;
|
||||
};
|
||||
|
||||
void (*original_cb)(struct tray_menu *) = testTray.menu[0].cb;
|
||||
testTray.menu[0].cb = first_item_callback;
|
||||
|
||||
int initResult = tray_init(&testTray);
|
||||
trayRunning = (initResult == 0);
|
||||
ASSERT_EQ(initResult, 0);
|
||||
|
||||
tray_simulate_menu_item_click(0);
|
||||
tray_loop(0);
|
||||
EXPECT_EQ(callbackCount, 1);
|
||||
|
||||
testTray.notification_title = "Menu Callback Regression";
|
||||
testTray.notification_text = "Notification update should not break menu callbacks";
|
||||
testTray.notification_icon = TRAY_ICON1;
|
||||
tray_update(&testTray);
|
||||
WaitForTrayReady();
|
||||
|
||||
tray_simulate_menu_item_click(0);
|
||||
tray_loop(0);
|
||||
EXPECT_EQ(callbackCount, 2);
|
||||
|
||||
testTray.notification_title = nullptr;
|
||||
testTray.notification_text = nullptr;
|
||||
testTray.notification_icon = nullptr;
|
||||
tray_update(&testTray);
|
||||
|
||||
testTray.menu[0].cb = original_cb;
|
||||
}
|
||||
|
||||
#endif // TRAY_QT
|
||||
@@ -0,0 +1,275 @@
|
||||
// test includes
|
||||
#include "tests/conftest.cpp"
|
||||
|
||||
// local includes
|
||||
#include "src/tray.h"
|
||||
|
||||
#if defined(__linux__) || defined(linux) || defined(__linux)
|
||||
|
||||
// standard includes
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
int g_menu_callback_count = 0;
|
||||
int g_notification_callback_count = 0;
|
||||
int g_log_callback_count = 0;
|
||||
|
||||
void menu_item_cb([[maybe_unused]] struct tray_menu *item) {
|
||||
g_menu_callback_count++;
|
||||
}
|
||||
|
||||
void notification_cb() {
|
||||
g_notification_callback_count++;
|
||||
}
|
||||
|
||||
void log_cb([[maybe_unused]] int level, [[maybe_unused]] const char *msg) {
|
||||
g_log_callback_count++;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
class TrayLinuxCoverageTest: public LinuxTest { // NOSONAR(cpp:S3656) - fixture members/methods are accessed by TEST_F-generated subclasses
|
||||
protected: // NOSONAR(cpp:S3656) - TEST_F requires protected fixture visibility
|
||||
void SetUp() override {
|
||||
LinuxTest::SetUp();
|
||||
|
||||
tray_set_log_callback(nullptr);
|
||||
tray_set_app_info(nullptr, nullptr, nullptr);
|
||||
|
||||
g_menu_callback_count = 0;
|
||||
g_notification_callback_count = 0;
|
||||
g_log_callback_count = 0;
|
||||
|
||||
submenuItems = {{{.text = "Nested", .cb = menu_item_cb}, {.text = nullptr}}};
|
||||
|
||||
menuItems = {{{.text = "Clickable", .cb = menu_item_cb}, {.text = "-"}, {.text = "Submenu", .submenu = submenuItems.data()}, {.text = "Disabled", .disabled = 1, .cb = menu_item_cb}, {.text = "Second Clickable", .cb = menu_item_cb}, {.text = nullptr}}};
|
||||
|
||||
trayData.icon = "icon.png";
|
||||
trayData.tooltip = "Linux Tray Coverage";
|
||||
trayData.notification_icon = nullptr;
|
||||
trayData.notification_text = nullptr;
|
||||
trayData.notification_title = nullptr;
|
||||
trayData.notification_cb = nullptr;
|
||||
trayData.menu = menuItems.data();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (trayRunning) {
|
||||
tray_exit();
|
||||
tray_loop(0);
|
||||
trayRunning = false;
|
||||
}
|
||||
|
||||
tray_set_log_callback(nullptr);
|
||||
LinuxTest::TearDown();
|
||||
}
|
||||
|
||||
void InitTray() {
|
||||
const int initResult = tray_init(&trayData);
|
||||
trayRunning = (initResult == 0);
|
||||
ASSERT_EQ(initResult, 0);
|
||||
}
|
||||
|
||||
void PumpEvents(int iterations = 20) {
|
||||
for (int i = 0; i < iterations; i++) {
|
||||
tray_loop(0);
|
||||
}
|
||||
}
|
||||
|
||||
bool trayRunning {false};
|
||||
std::array<struct tray_menu, 6> menuItems {};
|
||||
std::array<struct tray_menu, 2> submenuItems {};
|
||||
struct tray trayData {};
|
||||
};
|
||||
|
||||
TEST_F(TrayLinuxCoverageTest, SimulateMenuClickSkipsNonTriggerableActions) {
|
||||
InitTray();
|
||||
|
||||
tray_simulate_menu_item_click(-1);
|
||||
tray_simulate_menu_item_click(99);
|
||||
tray_simulate_menu_item_click(1);
|
||||
tray_simulate_menu_item_click(2);
|
||||
tray_simulate_menu_item_click(3);
|
||||
PumpEvents();
|
||||
|
||||
EXPECT_EQ(g_menu_callback_count, 0);
|
||||
|
||||
tray_simulate_menu_item_click(0);
|
||||
tray_simulate_menu_item_click(4);
|
||||
PumpEvents();
|
||||
|
||||
EXPECT_EQ(g_menu_callback_count, 2);
|
||||
}
|
||||
|
||||
TEST_F(TrayLinuxCoverageTest, ApiCallsAreNoOpsBeforeInit) {
|
||||
tray_update(&trayData);
|
||||
tray_show_menu();
|
||||
tray_simulate_menu_item_click(0);
|
||||
tray_simulate_notification_click();
|
||||
PumpEvents();
|
||||
|
||||
EXPECT_EQ(g_menu_callback_count, 0);
|
||||
EXPECT_EQ(g_notification_callback_count, 0);
|
||||
}
|
||||
|
||||
TEST_F(TrayLinuxCoverageTest, SimulateMenuClickWithNullMenuDoesNothing) {
|
||||
trayData.menu = nullptr;
|
||||
InitTray();
|
||||
|
||||
tray_simulate_menu_item_click(0);
|
||||
PumpEvents();
|
||||
|
||||
EXPECT_EQ(g_menu_callback_count, 0);
|
||||
}
|
||||
|
||||
TEST_F(TrayLinuxCoverageTest, SetAppInfoAppliesExplicitMetadata) {
|
||||
tray_set_app_info("tray-linux-tests", "Tray Linux Tests", "tray-linux-tests.desktop");
|
||||
InitTray();
|
||||
|
||||
// Trigger an update to exercise metadata-dependent notification/tray code paths.
|
||||
trayData.notification_title = "Metadata Test";
|
||||
trayData.notification_text = "Using explicit metadata";
|
||||
tray_update(&trayData);
|
||||
PumpEvents();
|
||||
}
|
||||
|
||||
TEST_F(TrayLinuxCoverageTest, SetAppInfoDefaultsUseFallbackValues) {
|
||||
tray_set_app_info(nullptr, nullptr, nullptr);
|
||||
trayData.tooltip = "Tooltip Display Name";
|
||||
InitTray();
|
||||
|
||||
trayData.notification_title = "Default Metadata Test";
|
||||
trayData.notification_text = "Using fallback metadata";
|
||||
tray_update(&trayData);
|
||||
PumpEvents();
|
||||
}
|
||||
|
||||
TEST_F(TrayLinuxCoverageTest, LogCallbackCanBeSetAndReset) {
|
||||
InitTray();
|
||||
tray_set_log_callback(log_cb);
|
||||
|
||||
// The callback is currently installed; this update path should remain stable.
|
||||
trayData.tooltip = "Log callback installed";
|
||||
tray_update(&trayData);
|
||||
PumpEvents();
|
||||
|
||||
EXPECT_EQ(g_log_callback_count, 0);
|
||||
|
||||
tray_set_log_callback(nullptr);
|
||||
|
||||
trayData.tooltip = "Log callback removed";
|
||||
tray_update(&trayData);
|
||||
PumpEvents();
|
||||
|
||||
EXPECT_EQ(g_log_callback_count, 0);
|
||||
}
|
||||
|
||||
TEST_F(TrayLinuxCoverageTest, TrayExitCausesLoopToReturnExitCode) {
|
||||
InitTray();
|
||||
|
||||
tray_exit();
|
||||
const int loopResult = tray_loop(0);
|
||||
trayRunning = false;
|
||||
|
||||
EXPECT_EQ(loopResult, -1);
|
||||
}
|
||||
|
||||
TEST_F(TrayLinuxCoverageTest, UpdateMenuStateWithSameLayoutKeepsCallbacksWorking) {
|
||||
InitTray();
|
||||
|
||||
menuItems[0].text = "Clickable Renamed";
|
||||
menuItems[0].disabled = 1;
|
||||
tray_update(&trayData);
|
||||
PumpEvents();
|
||||
|
||||
tray_simulate_menu_item_click(0);
|
||||
PumpEvents();
|
||||
EXPECT_EQ(g_menu_callback_count, 0);
|
||||
|
||||
menuItems[0].disabled = 0;
|
||||
tray_update(&trayData);
|
||||
PumpEvents();
|
||||
|
||||
tray_simulate_menu_item_click(0);
|
||||
PumpEvents();
|
||||
EXPECT_EQ(g_menu_callback_count, 1);
|
||||
}
|
||||
|
||||
TEST_F(TrayLinuxCoverageTest, ResolveTrayIconFromIconPathArray) {
|
||||
// Build a tray struct with iconPathCount/allIconPaths to exercise fallback icon resolution.
|
||||
const size_t iconCount = 2;
|
||||
const size_t bufSize = sizeof(struct tray) + iconCount * sizeof(const char *);
|
||||
std::vector<std::byte> buf(bufSize, std::byte {0});
|
||||
auto *iconPathTray = reinterpret_cast<struct tray *>(buf.data()); // NOSONAR(cpp:S3630) - reinterpret_cast is required to map a C flexible-array struct over raw storage
|
||||
|
||||
iconPathTray->icon = "missing-icon-name";
|
||||
iconPathTray->tooltip = "Icon path fallback";
|
||||
iconPathTray->notification_icon = nullptr;
|
||||
iconPathTray->notification_text = nullptr;
|
||||
iconPathTray->notification_title = nullptr;
|
||||
iconPathTray->notification_cb = nullptr;
|
||||
iconPathTray->menu = menuItems.data();
|
||||
|
||||
const int countVal = static_cast<int>(iconCount);
|
||||
std::memcpy(const_cast<int *>(&iconPathTray->iconPathCount), &countVal, sizeof(countVal)); // NOSONAR(cpp:S859) - const member initialization is required for this C interop allocation pattern
|
||||
const char *badIcon = "missing-icon-name";
|
||||
const char *goodIcon = "icon.png";
|
||||
std::memcpy(const_cast<char **>(&iconPathTray->allIconPaths[0]), &badIcon, sizeof(badIcon)); // NOSONAR(cpp:S859) - required to initialize const flexible-array entries
|
||||
std::memcpy(const_cast<char **>(&iconPathTray->allIconPaths[1]), &goodIcon, sizeof(goodIcon)); // NOSONAR(cpp:S859) - required to initialize const flexible-array entries
|
||||
|
||||
const int initResult = tray_init(iconPathTray);
|
||||
trayRunning = (initResult == 0);
|
||||
ASSERT_EQ(initResult, 0);
|
||||
|
||||
tray_update(iconPathTray);
|
||||
PumpEvents();
|
||||
}
|
||||
|
||||
TEST_F(TrayLinuxCoverageTest, NotificationWithoutCallbackDoesNotInvokeOnSimulation) {
|
||||
InitTray();
|
||||
|
||||
trayData.notification_title = "No callback notification";
|
||||
trayData.notification_text = "Should not invoke callback";
|
||||
trayData.notification_icon = "icon.png";
|
||||
trayData.notification_cb = nullptr;
|
||||
|
||||
tray_update(&trayData);
|
||||
PumpEvents();
|
||||
|
||||
tray_simulate_notification_click();
|
||||
PumpEvents();
|
||||
|
||||
EXPECT_EQ(g_notification_callback_count, 0);
|
||||
}
|
||||
|
||||
TEST_F(TrayLinuxCoverageTest, ClearingNotificationDisablesSimulatedClickCallback) {
|
||||
InitTray();
|
||||
|
||||
trayData.notification_title = "Linux Notification";
|
||||
trayData.notification_text = "Notification body";
|
||||
trayData.notification_icon = "mail-message-new";
|
||||
trayData.notification_cb = notification_cb;
|
||||
|
||||
tray_update(&trayData);
|
||||
PumpEvents();
|
||||
|
||||
tray_simulate_notification_click();
|
||||
PumpEvents();
|
||||
EXPECT_EQ(g_notification_callback_count, 1);
|
||||
|
||||
trayData.notification_title = nullptr;
|
||||
trayData.notification_text = nullptr;
|
||||
trayData.notification_icon = nullptr;
|
||||
trayData.notification_cb = nullptr;
|
||||
|
||||
tray_update(&trayData);
|
||||
PumpEvents();
|
||||
|
||||
tray_simulate_notification_click();
|
||||
PumpEvents();
|
||||
EXPECT_EQ(g_notification_callback_count, 1);
|
||||
}
|
||||
|
||||
#endif
|
||||
Reference in new issue
Block a user