feat: Wine 支持 (#8960)

* feat: WineRuntimeInformation

* fix: system theme crash under wine

* refactor: toast notification

* feat: add destory callback

* feat: detect winegcc

* feat: use native MaaCore under Wine

* feat: libnotify integration

* chore: disable hardware accelration under Wine

* fix: distorted icon under Wine

* chore: use Environment.ProcessPath instead of cursed alternatives

* chore: don't filter *.exe when using native MaaCore

* chore: force Aero2 theme

* chore: allow build MaaWpfGui without Windows

You need to extract native .NET SDK on top of Windows .NET SDK to get a working WPF SDK.

* feat: fontconfig integration

* docs: run MaaWpfGui under Wine
This commit is contained in:
dantmnf
2024-05-12 23:27:46 +08:00
committed by GitHub
parent 254ed45e9b
commit 20a5574780
59 changed files with 2777 additions and 310 deletions

View File

@@ -13,6 +13,47 @@ icon: teenyicons:linux-alt-solid
[maa-cli](https://github.com/MaaAssistantArknights/maa-cli) 是一个使用 Rust 编写的简单 MAA 命令行工具。相关安装与使用教程请阅读[用户手册 - CLI使用指南](../CLI使用指南)。
### 使用 Wine
MAA WPF GUI 当前可以通过 Wine 运行。
#### 安装步骤
1. 前往 [.NET 发布页](https://dotnet.microsoft.com/en-us/download/dotnet/8.0)下载并安装 Windows 版 .NET **桌面**运行时。
3. 下载 Windows 版 MAA解压后运行 `wine MAA.exe`
::: info 注意
需要在连接设置中将 ADB 路径设置为 [Windows 版 `adb.exe`](https://dl.google.com/android/repository/platform-tools-latest-windows.zip)。
如果您需要通过 ADB 连接 USB 设备,请先在 Wine 外运行 `adb start-server`,即通过 Wine 连接原生 ADB server。
:::
#### 使用 Linux 原生 MaaCore实验性功能
下载 [MAA Wine Bridge](https://github.com/MaaAssistantArknights/MaaAssistantArknights/tree/dev/src/MaaWineBridge) 源码并构建,用生成的 `MaaCore.dll`ELF 文件)替换 Windows 版本,并将 Linux 原生动态库(`libMaaCore.so` 以及依赖)放在同一目录下。
此时通过 Wine 运行 `MAA.exe`,将会加载 Linux 原生动态库。
::: info 注意
使用 Linux 原生 MaaCore 时,需要在连接设置中将 ADB 路径设置为 Linux 原生 ADB。
:::
#### Linux 桌面整合(实验性功能)
桌面整合提供原生桌面通知支持,以及将 fontconfig 字体配置映射到 WPF 的功能。
将 MAA Wine Bridge 生成的 `MaaDesktopIntegration.so` 放到 `MAA.exe` 同目录下即可启用。
#### 已知问题
* Wine DirectWrite 强制启用 hinting并且不将 DPI 传递给 FreeType导致字体显示效果不佳。
* 不使用原生桌面通知时,弹出通知会抢占全系统鼠标焦点,导致无法操作其他窗口。可以通过 `winecfg` 启用虚拟桌面模式缓解,或禁用桌面通知。
* Wine-staging 用户需要关闭 `winecfg` 中的 `隐藏 Wine 版本` 选项,以便 MAA 正确检测 Wine 环境。
* Wine 的 Light 主题会导致 WPF 中部分文字颜色异常,建议在 `winecfg` 中切换到无主题Windows 经典主题)。
* Wine 使用旧式 XEmbed 托盘图标,在 GNOME 下可能无法正常工作。
* 使用 Linux 原生 MaaCore 时暂不支持自动更新(~~更新程序:我寻思我应该下载个 Windows 版~~
### 使用 Python
#### 1. 安装 MAA 动态库

View File

@@ -2,15 +2,17 @@
// The way how the function is called
#if !defined(ASST_CALL)
#if defined(_WIN32)
#if defined(__WINE__) && defined(__x86_64__)
#define ASST_CALL __attribute__((sysv_abi))
#elif defined(_WIN32)
#define ASST_CALL __stdcall
#else
#define ASST_CALL
#endif /* _WIN32 */
#endif /* __WINE__ / _WIN32 */
#endif /* ASST_CALL */
// The function exported symbols
#if defined _WIN32 || defined __CYGWIN__
#if (defined(_WIN32) || defined(__CYGWIN__)) && !defined(__WINE__)
#define ASST_DLL_IMPORT __declspec(dllimport)
#define ASST_DLL_EXPORT __declspec(dllexport)
#define ASST_DLL_LOCAL

View File

@@ -101,6 +101,7 @@ Assistant::~Assistant()
if (m_msg_thread.joinable()) {
m_msg_thread.join();
}
m_callback(static_cast<AsstMsgId>(AsstMsg::Destroyed), "{}", m_callback_arg);
}
bool asst::Assistant::set_instance_option(InstanceOptionKey key, const std::string& value)

View File

@@ -16,6 +16,7 @@ namespace asst
ConnectionInfo, // 连接相关错误
AllTasksCompleted, // 全部任务完成
AsyncCallInfo, // 外部异步调用信息
Destroyed, // 实例已销毁
/* TaskChain Info */
TaskChainError = 10000, // 任务链执行/识别错误
TaskChainStart, // 任务链开始
@@ -39,6 +40,7 @@ namespace asst
{ AsstMsg::ConnectionInfo, "ConnectionInfo" },
{ AsstMsg::AllTasksCompleted, "AllTasksCompleted" },
{ AsstMsg::AsyncCallInfo, "AsyncCallInfo" },
{ AsstMsg::Destroyed, "Destroyed" },
/* TaskChain Info */
{ AsstMsg::TaskChainError, "TaskChainError" },
{ AsstMsg::TaskChainStart, "TaskChainStart" },

View File

@@ -0,0 +1,5 @@
cmake_minimum_required(VERSION 3.14)
project(MaaWineBridge)
add_subdirectory(MaaCoreForwarder)
add_subdirectory(MaaDesktopIntegration)

View File

@@ -0,0 +1,43 @@
cmake_minimum_required(VERSION 3.14)
project(MaaCoreForwarder C)
add_library(MaaCore_INTERFACE INTERFACE)
set_target_properties(MaaCore_INTERFACE PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${CMAKE_CURRENT_SOURCE_DIR}/../../../include")
# fool cmake to add MaaCore.spec to linker command line
add_library(MaaCore_SPEC IMPORTED STATIC)
set_target_properties(MaaCore_SPEC PROPERTIES IMPORTED_LOCATION "${CMAKE_CURRENT_SOURCE_DIR}/MaaCore.spec")
add_library(MaaCoreForwarder SHARED
maacore.c
dl_maacore.c
entry.c
task_queue.c
task_queue_wine.c
MaaCore.spec
)
set_target_properties(MaaCoreForwarder PROPERTIES
NO_SONAME TRUE
BUILD_WITH_INSTALL_RPATH TRUE
BUILD_RPATH_USE_ORIGIN TRUE # FIXME: not working
INSTALL_RPATH "$ORIGIN"
PREFIX ""
OUTPUT_NAME "MaaCore"
SUFFIX ".dll"
)
target_link_options(MaaCoreForwarder PRIVATE -Wl,--wine-builtin)
target_link_libraries(MaaCoreForwarder MaaCore_INTERFACE MaaCore_SPEC)
# winegcc will force add .so suffix
add_custom_command(TARGET MaaCoreForwarder POST_BUILD
COMMAND ${CMAKE_COMMAND} -E rename "$<TARGET_FILE:MaaCoreForwarder>.so" "$<TARGET_FILE:MaaCoreForwarder>"
COMMENT "[Winelib hack] Rename $<TARGET_FILE_NAME:MaaCoreForwarder>.so to $<TARGET_FILE_NAME:MaaCoreForwarder>"
)
install(TARGETS MaaCoreForwarder LIBRARY DESTINATION ".")
# or a good old fake module? (FIXME: not working)
# add_library(MaaCoreForwarder_FAKE SHARED dummy.c)
# set_target_properties(MaaCoreForwarder_FAKE PROPERTIES PREFIX "" OUTPUT_NAME "MaaCore" SUFFIX ".dll" NO_SONAME TRUE)
# target_link_options(MaaCoreForwarder_FAKE PRIVATE -Wb,--fake-module)

View File

@@ -0,0 +1,33 @@
@ stdcall AsstSetUserDir(ptr) WineShimAsstSetUserDir
@ stdcall AsstLoadResource(ptr) WineShimAsstLoadResource
@ stdcall AsstSetStaticOption(long ptr) WineShimAsstSetStaticOption
@ stdcall AsstCreate() WineShimAsstCreate
@ stdcall AsstCreateEx(ptr ptr) WineShimAsstCreateEx
@ stdcall AsstDestroy(ptr) WineShimAsstDestroy
@ stdcall AsstSetInstanceOption(ptr long ptr) WineShimAsstSetInstanceOption
@ stdcall AsstConnect(ptr ptr ptr ptr) WineShimAsstConnect
@ stdcall AsstAppendTask(ptr ptr ptr) WineShimAsstAppendTask
@ stdcall AsstSetTaskParams(ptr long ptr) WineShimAsstSetTaskParams
@ stdcall AsstStart(ptr) WineShimAsstStart
@ stdcall AsstStop(ptr) WineShimAsstStop
@ stdcall AsstRunning(ptr) WineShimAsstRunning
@ stdcall AsstConnected(ptr) WineShimAsstConnected
@ stdcall AsstBackToHome(ptr) WineShimAsstBackToHome
@ stdcall AsstAsyncConnect(ptr ptr ptr ptr long) WineShimAsstAsyncConnect
@ stdcall AsstAsyncClick(ptr long long long) WineShimAsstAsyncClick
@ stdcall AsstAsyncScreencap(ptr long) WineShimAsstAsyncScreencap
@ stdcall AsstGetImage(ptr ptr long) WineShimAsstGetImage
@ stdcall AsstGetUUID(ptr ptr long) WineShimAsstGetUUID
@ stdcall AsstGetTasksList(ptr ptr long) WineShimAsstGetTasksList
@ stdcall AsstGetNullSize() WineShimAsstGetNullSize
@ stdcall AsstGetVersion() WineShimAsstGetVersion
@ stdcall AsstLog(ptr ptr) WineShimAsstLog
@ cdecl dl_has_maacore()

View File

@@ -0,0 +1,51 @@
#include <dlfcn.h>
#define DL_FUNC_IMPL
#include "dl_maacore.h"
void dl_init_maacore()
{
void* maacore = dlopen("libMaaCore.so", RTLD_NOW);
if (!maacore) {
return;
}
FILL_DL_FUNC(maacore, AsstSetUserDir)
FILL_DL_FUNC(maacore, AsstLoadResource)
FILL_DL_FUNC(maacore, AsstSetStaticOption)
FILL_DL_FUNC(maacore, AsstCreate)
FILL_DL_FUNC(maacore, AsstCreateEx)
FILL_DL_FUNC(maacore, AsstDestroy)
FILL_DL_FUNC(maacore, AsstSetInstanceOption)
FILL_DL_FUNC(maacore, AsstConnect)
FILL_DL_FUNC(maacore, AsstAppendTask)
FILL_DL_FUNC(maacore, AsstSetTaskParams)
FILL_DL_FUNC(maacore, AsstStart)
FILL_DL_FUNC(maacore, AsstStop)
FILL_DL_FUNC(maacore, AsstRunning)
FILL_DL_FUNC(maacore, AsstConnected)
FILL_DL_FUNC(maacore, AsstBackToHome)
FILL_DL_FUNC(maacore, AsstAsyncConnect)
FILL_DL_FUNC(maacore, AsstAsyncClick)
FILL_DL_FUNC(maacore, AsstAsyncScreencap)
FILL_DL_FUNC(maacore, AsstGetImage)
FILL_DL_FUNC(maacore, AsstGetUUID)
FILL_DL_FUNC(maacore, AsstGetTasksList)
FILL_DL_FUNC(maacore, AsstGetNullSize)
FILL_DL_FUNC(maacore, AsstGetVersion)
FILL_DL_FUNC(maacore, AsstLog)
}
#include <windef.h>
int __cdecl dl_has_maacore()
{
return dl_AsstCreateEx != NULL;
}

View File

@@ -0,0 +1,37 @@
#include <AsstCaller.h>
#include "dlshim.h"
DEFINE_DL_FUNC(AsstSetUserDir)
DEFINE_DL_FUNC(AsstLoadResource)
DEFINE_DL_FUNC(AsstSetStaticOption)
DEFINE_DL_FUNC(AsstCreate)
DEFINE_DL_FUNC(AsstCreateEx)
DEFINE_DL_FUNC(AsstDestroy)
DEFINE_DL_FUNC(AsstSetInstanceOption)
DEFINE_DL_FUNC(AsstConnect)
DEFINE_DL_FUNC(AsstAppendTask)
DEFINE_DL_FUNC(AsstSetTaskParams)
DEFINE_DL_FUNC(AsstStart)
DEFINE_DL_FUNC(AsstStop)
DEFINE_DL_FUNC(AsstRunning)
DEFINE_DL_FUNC(AsstConnected)
DEFINE_DL_FUNC(AsstBackToHome)
DEFINE_DL_FUNC(AsstAsyncConnect)
DEFINE_DL_FUNC(AsstAsyncClick)
DEFINE_DL_FUNC(AsstAsyncScreencap)
DEFINE_DL_FUNC(AsstGetImage)
DEFINE_DL_FUNC(AsstGetUUID)
DEFINE_DL_FUNC(AsstGetTasksList)
DEFINE_DL_FUNC(AsstGetNullSize)
DEFINE_DL_FUNC(AsstGetVersion)
DEFINE_DL_FUNC(AsstLog)
void dl_init_maacore();

View File

@@ -0,0 +1,14 @@
#pragma once
#ifdef DL_FUNC_IMPL
#define DL_FUNC_DEF
#else
#define DL_FUNC_DEF extern
#endif
#define DEFINE_DL_FUNC(func) DL_FUNC_DEF typeof(func)* dl_##func;
#define FILL_DL_FUNC(lib, func) \
do { \
dl_##func = (typeof(func)*)dlsym(lib, #func); \
} while (0);

View File

@@ -0,0 +1,7 @@
#include "dl_maacore.h"
static void lib_init() __attribute__((constructor));
void lib_init() {
dl_init_maacore();
}

View File

@@ -0,0 +1,257 @@
#include "dl_maacore.h"
#include <pthread.h>
#include <stdlib.h>
// clang-format off
// don't sort includes
#include <windef.h>
#include <winbase.h>
#include <winnls.h>
// clang-format on
#include "task_queue.h"
#include "task_queue_wine.h"
#define AsstMsg_Destroyed (5)
static char* translate_utf8_path(const char* utf8_win_path)
{
int len = MultiByteToWideChar(CP_UTF8, 0, utf8_win_path, -1, NULL, 0);
wchar_t* wpath = malloc(len * sizeof(wchar_t));
MultiByteToWideChar(CP_UTF8, 0, utf8_win_path, -1, wpath, len);
char* unix_path = wine_get_unix_file_name(wpath);
free(wpath);
return unix_path;
}
static int win32_free(void* ptr)
{
return HeapFree(GetProcessHeap(), 0, ptr);
}
typedef void(
__stdcall* AsstApiCallbackMsAbi)(AsstMsgId msg, const char* details_json, void* custom_arg);
typedef struct
{
AsstApiCallbackMsAbi x64abi_callback;
void* custom_arg;
TaskQueue* queue;
} CallbackState;
typedef struct
{
AsstApiCallbackMsAbi x64abi_callback;
AsstMsgId msg;
const char* details_json;
void* custom_arg;
TaskQueue* queue;
} CallbackInvocation;
static CallbackState* cb_state_new()
{
CallbackState* fwd_state = malloc(sizeof(CallbackState));
memset(fwd_state, 0, sizeof(CallbackState));
TaskQueue* queue = task_queue_new(task_queue_worker_wine_new);
if (!queue) {
free(fwd_state);
return NULL;
}
fwd_state->queue = queue;
return fwd_state;
}
static void cb_state_free(CallbackState* fwd_state)
{
task_queue_free(fwd_state->queue);
free(fwd_state);
}
static void invoke_helper(void* arg)
{
// called in win32 thread
CallbackInvocation* invocation = arg;
invocation->x64abi_callback(invocation->msg, invocation->details_json, invocation->custom_arg);
}
static void UnixCallbackShim(AsstMsgId msg, const char* details_json, void* custom_arg)
{
CallbackState* state = (CallbackState*)custom_arg;
CallbackInvocation invocation = {
.x64abi_callback = state->x64abi_callback,
.msg = msg,
.details_json = details_json,
.custom_arg = state->custom_arg,
};
Task* task = task_queue_push(state->queue, invoke_helper, &invocation);
task_wait(task);
task_release(task);
if (msg == AsstMsg_Destroyed) {
cb_state_free(state);
}
}
AsstBool __stdcall WineShimAsstSetUserDir(const char* path)
{
char* unix_path = translate_utf8_path(path);
AsstBool result = dl_AsstSetUserDir(unix_path);
win32_free(unix_path);
return result;
}
AsstBool __stdcall WineShimAsstLoadResource(const char* path)
{
char* unix_path = translate_utf8_path(path);
AsstBool result = dl_AsstLoadResource(unix_path);
win32_free(unix_path);
return result;
}
AsstBool __stdcall WineShimAsstSetStaticOption(AsstStaticOptionKey key, const char* value)
{
return dl_AsstSetStaticOption(key, value);
}
AsstHandle __stdcall WineShimAsstCreate()
{
return dl_AsstCreate();
}
AsstHandle __stdcall WineShimAsstCreateEx(AsstApiCallbackMsAbi callback, void* custom_arg)
{
CallbackState* fwd_state = cb_state_new();
fwd_state->x64abi_callback = callback;
fwd_state->custom_arg = custom_arg;
if (!fwd_state) {
return NULL;
}
// callback needs to be called in a wine thread
AsstHandle result = dl_AsstCreateEx(UnixCallbackShim, fwd_state);
return result;
}
void __stdcall WineShimAsstDestroy(AsstHandle handle)
{
dl_AsstDestroy(handle);
}
AsstBool __stdcall WineShimAsstSetInstanceOption(
AsstHandle handle,
AsstInstanceOptionKey key,
const char* value)
{
return dl_AsstSetInstanceOption(handle, key, value);
}
AsstBool __stdcall WineShimAsstConnect(
AsstHandle handle,
const char* adb_path,
const char* address,
const char* config)
{
char* unix_adb_path = translate_utf8_path(adb_path);
AsstBool result = dl_AsstConnect(handle, unix_adb_path, address, config);
HeapFree(GetProcessHeap(), 0, unix_adb_path);
return result;
}
AsstTaskId __stdcall WineShimAsstAppendTask(AsstHandle handle, const char* type, const char* params)
{
return dl_AsstAppendTask(handle, type, params);
}
AsstBool __stdcall WineShimAsstSetTaskParams(AsstHandle handle, AsstTaskId id, const char* params)
{
return dl_AsstSetTaskParams(handle, id, params);
}
AsstBool __stdcall WineShimAsstStart(AsstHandle handle)
{
return dl_AsstStart(handle);
}
AsstBool __stdcall WineShimAsstStop(AsstHandle handle)
{
return dl_AsstStop(handle);
}
AsstBool __stdcall WineShimAsstRunning(AsstHandle handle)
{
return dl_AsstRunning(handle);
}
AsstBool __stdcall WineShimAsstConnected(AsstHandle handle)
{
return dl_AsstConnected(handle);
}
AsstBool __stdcall WineShimAsstBackToHome(AsstHandle handle)
{
return dl_AsstBackToHome(handle);
}
AsstAsyncCallId __stdcall WineShimAsstAsyncConnect(
AsstHandle handle,
const char* adb_path,
const char* address,
const char* config,
AsstBool block)
{
char* unix_adb_path;
if (strchr(adb_path, '/') != NULL || strchr(adb_path, '\\') != NULL) {
unix_adb_path = translate_utf8_path(adb_path);
}
else {
unix_adb_path = (char*)adb_path;
}
AsstAsyncCallId result = dl_AsstAsyncConnect(handle, unix_adb_path, address, config, block);
if (unix_adb_path != adb_path) {
win32_free(unix_adb_path);
}
return result;
}
AsstAsyncCallId __stdcall WineShimAsstAsyncClick(
AsstHandle handle,
int32_t x,
int32_t y,
AsstBool block)
{
return dl_AsstAsyncClick(handle, x, y, block);
}
AsstAsyncCallId __stdcall WineShimAsstAsyncScreencap(AsstHandle handle, AsstBool block)
{
return dl_AsstAsyncScreencap(handle, block);
}
AsstSize __stdcall WineShimAsstGetImage(AsstHandle handle, void* buff, AsstSize buff_size)
{
return dl_AsstGetImage(handle, buff, buff_size);
}
AsstSize __stdcall WineShimAsstGetUUID(AsstHandle handle, char* buff, AsstSize buff_size)
{
return dl_AsstGetUUID(handle, buff, buff_size);
}
AsstSize __stdcall WineShimAsstGetTasksList(AsstHandle handle, AsstTaskId* buff, AsstSize buff_size)
{
return dl_AsstGetTasksList(handle, buff, buff_size);
}
AsstSize __stdcall WineShimAsstGetNullSize()
{
return dl_AsstGetNullSize();
}
const char* __stdcall WineShimAsstGetVersion()
{
return dl_AsstGetVersion();
}
void __stdcall WineShimAsstLog(const char* level, const char* message)
{
dl_AsstLog(level, message);
}

View File

@@ -0,0 +1,163 @@
#include "task_queue.h"
#include <pthread.h>
#include <stdatomic.h>
#include <stdlib.h>
#define MAX_TASKS 16
#define MAX_WORKERS 1
struct _Task
{
TaskFunc function;
void* arg;
pthread_mutex_t mutex;
pthread_cond_t cond_done;
int done;
atomic_int ref_count;
};
struct _TaskQueue
{
Task* tasks[MAX_TASKS];
int head;
int tail;
int stop;
pthread_mutex_t mutex;
pthread_cond_t cond_not_empty;
pthread_cond_t cond_not_full;
TaskQueueWorkerFactory worker_factory;
int worker_count;
TaskQueueWorker* workers[MAX_WORKERS];
};
static Task* task_new(TaskFunc function, void* arg)
{
Task* task = calloc(1, sizeof(Task));
task->function = function;
task->arg = arg;
pthread_mutex_init(&task->mutex, NULL);
pthread_cond_init(&task->cond_done, NULL);
task->done = 0;
atomic_init(&task->ref_count, 1);
return task;
}
void task_addref(Task* task)
{
atomic_fetch_add(&task->ref_count, 1);
}
void task_release(Task* task)
{
if (atomic_fetch_sub(&task->ref_count, 1) == 1) {
pthread_mutex_destroy(&task->mutex);
pthread_cond_destroy(&task->cond_done);
free(task);
}
}
void task_invoke(Task* task)
{
pthread_mutex_lock(&task->mutex);
task->function(task->arg);
task->done = 1;
pthread_cond_signal(&task->cond_done);
pthread_mutex_unlock(&task->mutex);
}
void task_wait(Task* task)
{
pthread_mutex_lock(&task->mutex);
while (!task->done) {
pthread_cond_wait(&task->cond_done, &task->mutex);
}
pthread_mutex_unlock(&task->mutex);
}
TaskQueue* task_queue_new(TaskQueueWorkerFactory worker_factory)
{
TaskQueue* queue = calloc(1, sizeof(TaskQueue));
queue->head = 0;
queue->tail = 0;
queue->stop = 0;
queue->worker_factory = worker_factory;
TaskQueueWorker* worker = worker_factory(queue);
if (!worker) {
free(queue);
return NULL;
}
queue->workers[queue->worker_count++] = worker;
pthread_mutex_init(&queue->mutex, NULL);
pthread_cond_init(&queue->cond_not_empty, NULL);
pthread_cond_init(&queue->cond_not_full, NULL);
return queue;
}
void task_queue_free(TaskQueue* queue)
{
pthread_mutex_lock(&queue->mutex);
queue->stop = 1;
pthread_cond_broadcast(&queue->cond_not_empty);
pthread_mutex_unlock(&queue->mutex);
for (int i = 0; i < queue->worker_count; i++) {
TaskQueueWorker* worker = queue->workers[i];
worker->join(worker);
worker->release(worker);
queue->workers[i] = NULL;
}
pthread_mutex_destroy(&queue->mutex);
pthread_cond_destroy(&queue->cond_not_empty);
pthread_cond_destroy(&queue->cond_not_full);
free(queue);
}
Task* task_queue_push(TaskQueue* queue, TaskFunc function, void* arg)
{
if (queue->stop) {
return NULL;
}
pthread_mutex_lock(&queue->mutex);
// TODO: add worker if queue is full
while ((queue->tail + 1) % MAX_TASKS == queue->head) {
pthread_cond_wait(&queue->cond_not_full, &queue->mutex);
}
Task* task = task_new(function, arg);
queue->tasks[queue->tail] = task;
queue->tail = (queue->tail + 1) % MAX_TASKS;
pthread_cond_signal(&queue->cond_not_empty);
pthread_mutex_unlock(&queue->mutex);
task_addref(task);
// 1 reference for the queue, 1 for the caller
return task;
}
Task* task_queue_pop(TaskQueue* queue)
{
if (queue->stop) {
return NULL;
}
pthread_mutex_lock(&queue->mutex);
while (queue->head == queue->tail && !queue->stop) {
pthread_cond_wait(&queue->cond_not_empty, &queue->mutex);
}
if (queue->stop) {
pthread_mutex_unlock(&queue->mutex);
return NULL;
}
Task* task = queue->tasks[queue->head];
queue->head = (queue->head + 1) % MAX_TASKS;
pthread_cond_signal(&queue->cond_not_full);
pthread_mutex_unlock(&queue->mutex);
return task;
}

View File

@@ -0,0 +1,28 @@
#pragma once
typedef struct _Task Task;
typedef struct _TaskQueue TaskQueue;
typedef struct _TaskQueueWorker TaskQueueWorker;
struct _TaskQueueWorker
{
TaskQueue* queue;
void (*addref)(TaskQueueWorker*);
void (*release)(TaskQueueWorker*);
void (*join)(TaskQueueWorker*);
};
typedef void (*TaskFunc)(void*);
typedef TaskQueueWorker* (*TaskQueueWorkerFactory)(TaskQueue*);
TaskQueue* task_queue_new(TaskQueueWorkerFactory worker_factory);
void task_queue_free(TaskQueue* queue);
Task* task_queue_push(TaskQueue* queue, TaskFunc function, void* arg);
Task* task_queue_pop(TaskQueue* queue);
void thread_pool_worker(TaskQueue* queue);
void task_wait(Task* task);
void task_invoke(Task* task);
void task_addref(Task* task);
void task_release(Task* task);

View File

@@ -0,0 +1,70 @@
#include "task_queue_wine.h"
#include <pthread.h>
#include <stdatomic.h>
#include <stdlib.h>
// clang-format off
#include <windef.h>
#include <winbase.h>
// clang-format on
typedef struct _TaskQueueWorkerWine
{
TaskQueueWorker base;
atomic_int ref_count;
pthread_t posix_thread;
} TaskQueueWorkerWine;
static void task_queue_worker_wine_addref(TaskQueueWorker* worker)
{
TaskQueueWorkerWine* worker_wine = (TaskQueueWorkerWine*)worker;
atomic_fetch_add(&worker_wine->ref_count, 1);
}
static void task_queue_worker_wine_release(TaskQueueWorker* worker)
{
TaskQueueWorkerWine* worker_wine = (TaskQueueWorkerWine*)worker;
if (atomic_fetch_sub(&worker_wine->ref_count, 1) == 1) {
free(worker_wine);
}
}
static void task_queue_worker_wine_join(TaskQueueWorker* worker)
{
TaskQueueWorkerWine* worker_wine = (TaskQueueWorkerWine*)worker;
pthread_join(worker_wine->posix_thread, NULL);
}
static DWORD WINAPI worker(void* arg)
{
TaskQueueWorkerWine* worker = arg;
worker->posix_thread = pthread_self();
TaskQueue* queue = worker->base.queue;
while (1) {
Task* task = task_queue_pop(queue);
if (task == NULL) {
break;
}
task_invoke(task);
task_release(task);
}
return 0;
}
TaskQueueWorker* task_queue_worker_wine_new(TaskQueue* queue)
{
TaskQueueWorkerWine* worker_wine = malloc(sizeof(TaskQueueWorkerWine));
worker_wine->base.queue = queue;
worker_wine->base.addref = task_queue_worker_wine_addref;
worker_wine->base.release = task_queue_worker_wine_release;
worker_wine->base.join = task_queue_worker_wine_join;
atomic_init(&worker_wine->ref_count, 1);
HANDLE thr = CreateThread(NULL, 0, worker, worker_wine, 0, NULL);
if (thr == NULL) {
free(worker_wine);
return NULL;
}
CloseHandle(thr);
return (TaskQueueWorker*)worker_wine;
}

View File

@@ -0,0 +1,3 @@
#include "task_queue.h"
TaskQueueWorker* task_queue_worker_wine_new(TaskQueue* queue);

View File

@@ -0,0 +1,31 @@
cmake_minimum_required(VERSION 3.14)
project(MaaDesktopIntegration C)
find_package(Fontconfig REQUIRED)
find_package(PkgConfig REQUIRED)
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
pkg_check_modules(GDK_PIXBUF REQUIRED IMPORTED_TARGET gdk-pixbuf-2.0)
pkg_check_modules(LIBNOTIFY REQUIRED IMPORTED_TARGET libnotify)
add_library(MaaDesktopIntegration_SPEC IMPORTED STATIC)
set_target_properties(MaaDesktopIntegration_SPEC PROPERTIES IMPORTED_LOCATION "${CMAKE_CURRENT_SOURCE_DIR}/MaaDesktopIntegration.spec")
add_library(MaaDesktopIntegration SHARED
notification.c
main_loop.c
gobject.c
gdk-pixbuf.c
fontconfig.c
MaaDesktopIntegration.spec
)
set_target_properties(MaaDesktopIntegration PROPERTIES
NO_SONAME TRUE
PREFIX ""
OUTPUT_NAME "MaaDesktopIntegration"
SUFFIX ".so"
)
target_link_libraries(MaaDesktopIntegration PkgConfig::GLIB PkgConfig::GDK_PIXBUF PkgConfig::LIBNOTIFY Fontconfig::Fontconfig MaaDesktopIntegration_SPEC)
install(TARGETS MaaDesktopIntegration LIBRARY DESTINATION ".")

View File

@@ -0,0 +1,42 @@
@ cdecl g_object_unref(ptr) maa_wine_bridge_g_object_unref
@ cdecl g_signal_connect_data(ptr str ptr ptr ptr long) maa_wine_bridge_g_signal_connect_data
@ cdecl notify_init(str) maa_wine_bridge_notify_init
@ cdecl notify_notification_new(str str str) maa_wine_bridge_notify_notification_new
@ cdecl notify_notification_add_action(ptr str str ptr ptr ptr) maa_wine_bridge_notify_notification_add_action
@ cdecl notify_notification_clear_actions(ptr) maa_wine_bridge_notify_notification_clear_actions
@ cdecl notify_notification_clear_hints(ptr) maa_wine_bridge_notify_notification_clear_hints
@ cdecl notify_notification_close(ptr ptr) maa_wine_bridge_notify_notification_close
@ cdecl notify_notification_get_activation_token(ptr) maa_wine_bridge_notify_notification_get_activation_token
@ cdecl notify_notification_get_closed_reason(ptr) maa_wine_bridge_notify_notification_get_closed_reason
@ cdecl notify_notification_set_app_name(ptr str) maa_wine_bridge_notify_notification_set_app_name
@ cdecl notify_notification_set_category(ptr str) maa_wine_bridge_notify_notification_set_category
@ cdecl notify_notification_set_hint(ptr str ptr) maa_wine_bridge_notify_notification_set_hint
@ cdecl notify_notification_set_image_from_pixbuf(ptr ptr) maa_wine_bridge_notify_notification_set_image_from_pixbuf
@ cdecl notify_notification_set_timeout(ptr long) maa_wine_bridge_notify_notification_set_timeout
@ cdecl notify_notification_set_urgency(ptr long) maa_wine_bridge_notify_notification_set_urgency
@ cdecl notify_notification_show(ptr ptr) maa_wine_bridge_notify_notification_show
@ cdecl notify_notification_update(ptr str str str) maa_wine_bridge_notify_notification_update
@ cdecl gdk_pixbuf_new_from_data(ptr long long long long long long ptr ptr) maa_wine_bridge_gdk_pixbuf_new_from_data
@ stdcall glib_default_main_loop_start() maa_wine_bridge_glib_default_main_loop_start
@ stdcall glib_default_main_loop_stop() maa_wine_bridge_glib_default_main_loop_stop
@ cdecl FcInitLoadConfigAndFonts() MaaFcInitLoadConfigAndFonts
@ cdecl FcNameParse(str) MaaFcNameParse
@ cdecl FcConfigSubstitute(ptr ptr long) MaaFcConfigSubstitute
@ cdecl FcDefaultSubstitute(ptr) MaaFcDefaultSubstitute
@ cdecl FcFontSort(ptr ptr long ptr ptr) MaaFcFontSort
@ cdecl FcCharSetCreate() MaaFcCharSetCreate
@ cdecl FcPatternGetString(ptr str long ptr) MaaFcPatternGetString
@ cdecl FcPatternGetCharSet(ptr str long ptr) MaaFcPatternGetCharSet
@ cdecl FcCharSetSubtract(ptr ptr) MaaFcCharSetSubtract
@ cdecl FcCharSetMerge(ptr ptr ptr) MaaFcCharSetMerge
@ cdecl FcCharSetFirstPage(ptr ptr ptr) MaaFcCharSetFirstPage
@ cdecl FcCharSetNextPage(ptr ptr ptr) MaaFcCharSetNextPage
@ cdecl FcCharSetDestroy(ptr) MaaFcCharSetDestroy
@ cdecl FcFontSetDestroy(ptr) MaaFcFontSetDestroy
@ cdecl FcPatternDestroy(ptr) MaaFcPatternDestroy
@ cdecl FcConfigDestroy(ptr) MaaFcConfigDestroy
@ cdecl MaaFcCharsetMapSize()

View File

@@ -0,0 +1,87 @@
#include <fontconfig/fontconfig.h>
#include <windef.h>
FcConfig* __cdecl MaaFcInitLoadConfigAndFonts()
{
return FcInitLoadConfigAndFonts();
}
FcPattern* __cdecl MaaFcNameParse(FcChar8* pattern)
{
return FcNameParse(pattern);
}
FcBool __cdecl MaaFcConfigSubstitute(FcConfig* config, FcPattern* p, FcMatchKind kind)
{
return FcConfigSubstitute(config, p, kind);
}
void __cdecl MaaFcDefaultSubstitute(FcPattern* p)
{
FcDefaultSubstitute(p);
}
FcResult __cdecl MaaFcPatternGetString(FcPattern* p, const char* object, int n, FcChar8** s)
{
return FcPatternGetString(p, object, n, s);
}
FcResult __cdecl MaaFcPatternGetCharSet(FcPattern* p, const char* object, int n, FcCharSet** c)
{
return FcPatternGetCharSet(p, object, n, c);
}
FcFontSet* __cdecl MaaFcFontSort(FcConfig* config, FcPattern* p, FcBool trim, FcCharSet** csp, FcResult* result)
{
return FcFontSort(config, p, trim, csp, result);
}
FcCharSet* __cdecl MaaFcCharSetCreate()
{
return FcCharSetCreate();
}
FcCharSet* __cdecl MaaFcCharSetSubtract(FcCharSet* a, FcCharSet* b)
{
return FcCharSetSubtract(a, b);
}
FcBool __cdecl MaaFcCharSetMerge(FcCharSet* a, FcCharSet* b, FcBool* c)
{
return FcCharSetMerge(a, b, c);
}
FcChar32 __cdecl MaaFcCharSetFirstPage(FcCharSet* a, FcChar32* pagemap, FcChar32* len)
{
return FcCharSetFirstPage(a, pagemap, len);
}
FcChar32 __cdecl MaaFcCharSetNextPage(FcCharSet* a, FcChar32* next, FcChar32* len)
{
return FcCharSetNextPage(a, next, len);
}
void __cdecl MaaFcCharSetDestroy(FcCharSet* a)
{
FcCharSetDestroy(a);
}
void __cdecl MaaFcPatternDestroy(FcPattern* p)
{
FcPatternDestroy(p);
}
void __cdecl MaaFcConfigDestroy(FcConfig* config)
{
FcConfigDestroy(config);
}
void __cdecl MaaFcFontSetDestroy(FcFontSet* fs)
{
FcFontSetDestroy(fs);
}
int __cdecl MaaFcCharsetMapSize() {
return FC_CHARSET_MAP_SIZE;
}

View File

@@ -0,0 +1,48 @@
#include <gdk-pixbuf/gdk-pixbuf.h>
#include "main_loop.h"
#include <windef.h>
typedef void(__stdcall* GdkPixbufDestroyNotifyMsAbi)(guchar* pixels, gpointer data);
struct PixBufDestroyCallbackState
{
GdkPixbufDestroyNotifyMsAbi free_func;
gpointer user_data;
};
static void pixbuf_destroy_callback(guchar* pixels, gpointer data)
{
struct PixBufDestroyCallbackState* state = (struct PixBufDestroyCallbackState*)data;
// not guaranteed to be called from the main loop
invoke_in_main_loop_stdcall(state->free_func, 2, pixels, state->user_data);
free(state);
}
GdkPixbuf* __cdecl maa_wine_bridge_gdk_pixbuf_new_from_data(
const guchar* data,
GdkColorspace colorspace,
gboolean has_alpha,
int bits_per_sample,
int width,
int height,
int rowstride,
GdkPixbufDestroyNotifyMsAbi destroy_fn,
gpointer destroy_fn_data)
{
struct PixBufDestroyCallbackState* state = malloc(sizeof(struct PixBufDestroyCallbackState));
state->free_func = destroy_fn;
state->user_data = destroy_fn_data;
return gdk_pixbuf_new_from_data(
data,
colorspace,
has_alpha,
bits_per_sample,
width,
height,
rowstride,
pixbuf_destroy_callback,
state);
}

View File

@@ -0,0 +1,58 @@
#include <glib.h>
#include <glib-object.h>
#include "main_loop.h"
#include <windef.h>
void __cdecl maa_wine_bridge_g_object_unref(void* object)
{
g_object_unref(G_OBJECT(object));
}
typedef void(__cdecl* GClosureNotifyMsAbi)(gpointer data, GClosure* closure);
typedef void(__cdecl* GCallbackMsAbi)(gpointer data, gpointer data2);
typedef struct _shim_g_signal_callback_state
{
GCallbackMsAbi c_handler;
gpointer data;
GClosureNotifyMsAbi destroy_data;
} shim_g_signal_callback_state;
static void shim_g_signal_callback(gpointer sender, gpointer arg)
{
shim_g_signal_callback_state* state = (shim_g_signal_callback_state*)arg;
// called from the main loop (wine-aware thread)
state->c_handler(sender, state->data);
}
static void shim_g_signal_callback_destroy(gpointer data, GClosure* closure)
{
shim_g_signal_callback_state* state = (shim_g_signal_callback_state*)data;
// called from the main loop (wine-aware thread)
state->destroy_data(state->data, closure);
free(state);
}
gulong __cdecl maa_wine_bridge_g_signal_connect_data(
void* instance,
const char* detailed_signal,
GCallbackMsAbi c_handler,
gpointer data,
GClosureNotifyMsAbi destroy_data,
GConnectFlags connect_flags)
{
shim_g_signal_callback_state* state = malloc(sizeof(shim_g_signal_callback_state));
state->c_handler = c_handler;
state->data = data;
state->destroy_data = destroy_data;
return g_signal_connect_data(
instance,
detailed_signal,
G_CALLBACK(shim_g_signal_callback),
state,
shim_g_signal_callback_destroy,
connect_flags);
}

View File

@@ -0,0 +1,108 @@
#include <glib.h>
#include <stdint.h>
// clang-format off
#include <windef.h>
#include <winbase.h>
// clang-format on
#include "main_loop.h"
typedef struct _pending_call
{
void* function;
int arg_count;
uintptr_t args[CALLBACK_FORWARDER_MAX_ARGS];
uintptr_t result;
} pending_call;
typedef uintptr_t(__stdcall* Callback0)();
typedef uintptr_t(__stdcall* Callback1)(uintptr_t);
typedef uintptr_t(__stdcall* Callback2)(uintptr_t, uintptr_t);
typedef uintptr_t(__stdcall* Callback3)(uintptr_t, uintptr_t, uintptr_t);
typedef uintptr_t(__stdcall* Callback4)(uintptr_t, uintptr_t, uintptr_t, uintptr_t);
static GMainLoop* main_loop;
static GMainContext* main_context;
static int WINAPI gmainloop_thread(void* arg)
{
GMainLoop* loop = arg;
g_main_loop_ref(loop);
g_main_loop_run(loop);
g_main_loop_unref(loop);
return 0;
}
int __cdecl maa_wine_bridge_glib_default_main_loop_start()
{
if (main_loop != NULL) {
return 0;
}
main_context = g_main_context_default();
GMainLoop* loop = g_main_loop_new(main_context, FALSE);
main_loop = loop;
HANDLE thread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)gmainloop_thread, loop, 0, NULL);
return 1;
}
int __cdecl maa_wine_bridge_glib_default_main_loop_stop()
{
if (!main_loop) {
return 0;
}
GMainLoop* loop = main_loop;
main_loop = NULL;
g_main_loop_quit(loop);
g_main_loop_unref(loop);
return 1;
}
static gboolean invoke_helper(gpointer lparam)
{
pending_call* pending = (pending_call*)lparam;
switch (pending->arg_count) {
case 0:
pending->result = ((Callback0)pending->function)();
break;
case 1:
pending->result = ((Callback1)pending->function)(pending->args[0]);
break;
case 2:
pending->result = ((Callback2)pending->function)(pending->args[0], pending->args[1]);
break;
case 3:
pending->result =
((Callback3)pending->function)(pending->args[0], pending->args[1], pending->args[2]);
break;
case 4:
pending->result = ((Callback4)pending->function)(
pending->args[0],
pending->args[1],
pending->args[2],
pending->args[3]);
break;
}
free(pending);
return FALSE;
}
void invoke_in_main_loop_stdcall(void* callback, int arg_count, ...)
{
if (arg_count > CALLBACK_FORWARDER_MAX_ARGS) {
abort();
}
pending_call* pending = malloc(sizeof(pending_call));
pending->function = callback;
pending->arg_count = arg_count;
va_list args;
va_start(args, arg_count);
for (int i = 0; i < arg_count; i++) {
pending->args[i] = va_arg(args, uintptr_t);
}
va_end(args);
g_main_context_invoke(main_context, invoke_helper, pending);
}
void invoke_in_main_loop(GSourceFunc callback, void* arg)
{
g_main_context_invoke(main_context, callback, arg);
}

View File

@@ -0,0 +1,10 @@
#pragma once
#include <stdint.h>
#include <windef.h>
#define CALLBACK_FORWARDER_MAX_ARGS 4
int __stdcall maa_wine_bridge_main_loop_start();
int __stdcall maa_wine_bridge_main_loop_stop();
void invoke_in_main_loop_stdcall(void* callback, int arg_count, ...);
void invoke_in_main_loop(GSourceFunc callback, void *arg);

View File

@@ -0,0 +1,159 @@
#include <libnotify/notify.h>
#include <windef.h>
#include "main_loop.h"
int __cdecl maa_wine_bridge_notify_init(const char* app_name)
{
return notify_init(app_name);
}
NotifyNotification* __cdecl maa_wine_bridge_notify_notification_new(
const char* summary,
const char* body,
const char* icon)
{
return notify_notification_new(summary, body, icon);
}
typedef void(__cdecl* NotifyActionCallbackMsAbi)(
NotifyNotification* notification,
char* action,
gpointer user_data);
typedef void(__cdecl* GFreeFuncMsAbi)(gpointer data);
struct NotifyActionCallbackState
{
NotifyActionCallbackMsAbi x64abi_callback;
gpointer user_data;
GFreeFuncMsAbi free_func;
};
static void maa_wine_bridge_notify_action_callback(
NotifyNotification* notification,
char* action,
gpointer user_data)
{
struct NotifyActionCallbackState* state = (struct NotifyActionCallbackState*)user_data;
// called from the main loop (wine-aware thread)
state->x64abi_callback(notification, action, state->user_data);
}
static void unix_free_func(gpointer data)
{
struct NotifyActionCallbackState* state = (struct NotifyActionCallbackState*)data;
state->free_func(state->user_data);
free(state);
}
void __cdecl maa_wine_bridge_notify_notification_add_action(
NotifyNotification* notification,
const char* action,
const char* label,
NotifyActionCallbackMsAbi callback,
gpointer user_data,
GFreeFuncMsAbi free_func)
{
struct NotifyActionCallbackState* state = malloc(sizeof(struct NotifyActionCallbackState));
state->x64abi_callback = callback;
state->user_data = user_data;
state->free_func = free_func;
notify_notification_add_action(
notification,
action,
label,
maa_wine_bridge_notify_action_callback,
state,
unix_free_func);
}
void __cdecl maa_wine_bridge_notify_notification_clear_actions(NotifyNotification* notification)
{
notify_notification_clear_actions(notification);
}
void __cdecl maa_wine_bridge_notify_notification_clear_hints(NotifyNotification* notification)
{
notify_notification_clear_hints(notification);
}
void __cdecl maa_wine_bridge_notify_notification_close(
NotifyNotification* notification,
GError** error)
{
notify_notification_close(notification, error);
}
const char* __cdecl maa_wine_bridge_notify_notification_get_activation_token(
NotifyNotification* notification)
{
return notify_notification_get_activation_token(notification);
}
gint __cdecl maa_wine_bridge_notify_notification_get_closed_reason(
const NotifyNotification* notification)
{
return notify_notification_get_closed_reason(notification);
}
void __cdecl maa_wine_bridge_notify_notification_set_app_name(
NotifyNotification* notification,
const char* app_name)
{
notify_notification_set_app_name(notification, app_name);
}
void __cdecl maa_wine_bridge_notify_notification_set_category(
NotifyNotification* notification,
const char* category)
{
notify_notification_set_category(notification, category);
}
void __cdecl maa_wine_bridge_notify_notification_set_hint(
NotifyNotification* notification,
const char* key,
GVariant* value)
{
notify_notification_set_hint(notification, key, value);
}
void __cdecl maa_wine_bridge_notify_notification_set_image_from_pixbuf(
NotifyNotification* notification,
GdkPixbuf* pixbuf)
{
notify_notification_set_image_from_pixbuf(notification, pixbuf);
}
void __cdecl maa_wine_bridge_notify_notification_set_timeout(
NotifyNotification* notification,
gint timeout)
{
notify_notification_set_timeout(notification, timeout);
}
void __cdecl maa_wine_bridge_notify_notification_set_urgency(
NotifyNotification* notification,
NotifyUrgency urgency)
{
notify_notification_set_urgency(notification, urgency);
}
void __cdecl maa_wine_bridge_notify_notification_show(
NotifyNotification* notification,
GError** error)
{
notify_notification_show(notification, error);
}
gboolean __cdecl maa_wine_bridge_notify_notification_update(
NotifyNotification* notification,
const char* summary,
const char* body,
const char* icon)
{
return notify_notification_update(notification, summary, body, icon);
}

View File

@@ -0,0 +1,11 @@
# MAA Wine Bridge
## Getting Started
```console
$ cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=winegcc.cmake
$ cmake --build build
$ cmake --install build --prefix build
```
Place `build/MaaCore.dll` next to `MAA.exe`. When is gets loaded by Wine, it will load `libMaaCore.so` from the same directory and forward calls to it.

View File

@@ -0,0 +1,7 @@
# CMake behaves differently when CMAKE_SYSTEM_NAME is set
# set(CMAKE_SYSTEM_NAME ${CMAKE_HOST_SYSTEM_NAME})
# set(CMAKE_SYSTEM_PROCESSOR ${CMAKE_HOST_SYSTEM_PROCESSOR})
set(CMAKE_C_COMPILER winegcc)
set(CMAKE_CXX_COMPILER wineg++)
set(CMAKE_C_STANDARD_INCLUDE_DIRECTORIES /usr/include/wine/windows)
set(CMAKE_CXX_STANDARD_INCLUDE_DIRECTORIES /usr/include/wine/windows)

View File

@@ -20,6 +20,7 @@
<main:Bootstrapper />
</s:ApplicationLoader.Bootstrapper>
</s:ApplicationLoader>
<ResourceDictionary Source="pack://application:,,,/PresentationFramework.Aero2;component/themes/Aero2.NormalColor.xaml" />
<ResourceDictionary Source="/MAA;component/Res/Theme.xaml" />
<ResourceDictionary Source="/MAA;component/Res/Style.xaml" />
</ResourceDictionary.MergedDictionaries>

View File

@@ -18,6 +18,8 @@ using System.Windows;
using System.Windows.Documents;
using MaaWpfGui.Helper;
using MaaWpfGui.Main;
using MaaWpfGui.WineCompat;
using MaaWpfGui.WineCompat.FontConfig;
using Serilog;
namespace MaaWpfGui
@@ -44,6 +46,12 @@ namespace MaaWpfGui
protected override void OnStartup(StartupEventArgs e)
{
if (WineRuntimeInformation.IsRunningUnderWine && MaaDesktopIntegration.Availabile)
{
// override buintin font map as early as possible
FontConfigIntegration.Install();
}
base.OnStartup(e);
string[] args = e.Args;

View File

@@ -0,0 +1,51 @@
// <copyright file="AppIcon.cs" company="MaaAssistantArknights">
// MaaWpfGui - A part of the MaaCoreArknights project
// Copyright (C) 2021 MistEO and Contributors
//
// 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 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY
// </copyright>
using System;
using System.IO;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace MaaWpfGui.Helper;
public class AppIcon
{
private static readonly Lazy<BitmapSource> lazyIcon = new(ExtractIcon);
private static BitmapSource ExtractIcon()
{
try
{
var sri = Application.GetResourceStream(new Uri("pack://application:,,,/newlogo.ico"));
if (sri == null)
{
return new BitmapImage();
}
using var s = sri.Stream;
var decoder = BitmapDecoder.Create(s, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.OnLoad);
var imageSource = decoder.Frames[0];
return imageSource;
}
catch (Exception)
{
throw;
return new BitmapImage();
}
}
public static BitmapSource GetIcon() => lazyIcon.Value;
}

View File

@@ -0,0 +1,23 @@
// <copyright file="INotificationPoster.cs" company="MaaAssistantArknights">
// MaaWpfGui - A part of the MaaCoreArknights project
// Copyright (C) 2021 MistEO and Contributors
//
// 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 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY
// </copyright>
using System;
namespace MaaWpfGui.Helper.Notification;
internal interface INotificationPoster
{
void ShowNotification(NotificationContent content);
event EventHandler<string> ActionActivated;
}

View File

@@ -0,0 +1,16 @@
// <copyright file="NotificationAction.cs" company="MaaAssistantArknights">
// MaaWpfGui - A part of the MaaCoreArknights project
// Copyright (C) 2021 MistEO and Contributors
//
// 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 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY
// </copyright>
namespace MaaWpfGui.Helper.Notification;
internal record class NotificationAction(string Label, string Tag);

View File

@@ -0,0 +1,31 @@
// <copyright file="NotificationContent.cs" company="MaaAssistantArknights">
// MaaWpfGui - A part of the MaaCoreArknights project
// Copyright (C) 2021 MistEO and Contributors
//
// 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 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY
// </copyright>
using System.Collections.Generic;
namespace MaaWpfGui.Helper.Notification;
internal class NotificationContent
{
public string Summary { get; set; }
public string Body { get; set; }
private List<NotificationAction> _actions = new List<NotificationAction>();
public IList<NotificationAction> Actions => _actions;
private List<NotificationHint> _hints = new List<NotificationHint>();
public IList<NotificationHint> Hints => _hints;
}

View File

@@ -0,0 +1,52 @@
// <copyright file="NotificationHint.cs" company="MaaAssistantArknights">
// MaaWpfGui - A part of the MaaCoreArknights project
// Copyright (C) 2021 MistEO and Contributors
//
// 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 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY
// </copyright>
using System;
namespace MaaWpfGui.Helper.Notification;
public class NotificationHint
{
public static NotificationHint Expandable { get; } = new NotificationHint();
public static NotificationHint RecruitHighRarity { get; } = new NotificationHint();
public static NotificationHint RecruitRobot { get; } = new NotificationHint();
public static NotificationHint NewVersion { get; } = new NotificationHint();
internal class NotificationHintRowCount : NotificationHint
{
public int Value { get; }
public NotificationHintRowCount(int rowCount)
{
Value = rowCount;
}
}
internal class NotificationHintExpirationTime : NotificationHint
{
public TimeSpan Value { get; }
public NotificationHintExpirationTime(TimeSpan value)
{
Value = value;
}
}
public static NotificationHint RowCount(int rowCount) => new NotificationHintRowCount(rowCount);
public static NotificationHint ExpirationTime(TimeSpan value) => new NotificationHintExpirationTime(value);
}

View File

@@ -0,0 +1,85 @@
// <copyright file="NotificationImplLibNotify.cs" company="MaaAssistantArknights">
// MaaWpfGui - A part of the MaaCoreArknights project
// Copyright (C) 2021 MistEO and Contributors
//
// 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 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY
// </copyright>
using System;
using System.Buffers.Binary;
using System.Numerics;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using MaaWpfGui.WineCompat;
namespace MaaWpfGui.Helper.Notification;
internal class NotificationImplLibNotify : INotificationPoster
{
public event EventHandler<string> ActionActivated;
public static bool IsSupported { get; } = CheckSupport();
private static GdkPixBuf _icon;
private static bool CheckSupport()
{
try
{
if (MaaDesktopIntegration.notify_init("MAA") == 0)
{
return false;
}
}
catch
{
return false;
}
Application.Current.Dispatcher.Invoke(() =>
{
var bmp3 = new FormatConvertedBitmap(AppIcon.GetIcon(), PixelFormats.Bgra32, null, 0);
var pixels = new byte[bmp3.PixelWidth * bmp3.PixelHeight * 4];
bmp3.CopyPixels(pixels, bmp3.PixelWidth * 4, 0);
// BGRA -> RGBA
var u32view = MemoryMarshal.Cast<byte, uint>(pixels.AsSpan());
for (int i = 0; i < u32view.Length; i++)
{
var bgra = u32view[i]; // BB GG RR AA -> 0xAARRGGBB
var argb = BinaryPrimitives.ReverseEndianness(bgra); // AA RR GG BB 0xBBGGRRAA
var rgba = BitOperations.RotateRight(argb, 8); // RR GG BB AA -> 0xAABBGGRR
u32view[i] = rgba;
}
_icon = GdkPixBuf.CreateFromPixels(bmp3.PixelWidth, bmp3.PixelHeight, bmp3.PixelWidth * 4, true, pixels);
});
return true;
}
public void ShowNotification(NotificationContent content)
{
var notification = new LibNotifyNotification(content.Summary, content.Body, "");
notification.SetImageFromPixbuf(_icon);
foreach (var action in content.Actions)
{
notification.AddAction(action.Tag, action.Label);
}
notification.ActionActivated += (sender, args) => ActionActivated?.Invoke(this, args);
notification.Show();
var gch = GCHandle.Alloc(notification);
notification.Closed += (sender, args) =>
{
gch.Free();
};
}
}

View File

@@ -0,0 +1,52 @@
// <copyright file="NotificationImplWinRT.cs" company="MaaAssistantArknights">
// MaaWpfGui - A part of the MaaCoreArknights project
// Copyright (C) 2021 MistEO and Contributors
//
// 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 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY
// </copyright>
using System;
using Microsoft.Toolkit.Uwp.Notifications;
namespace MaaWpfGui.Helper.Notification;
internal class NotificationImplWinRT : INotificationPoster, IDisposable
{
public event EventHandler<string> ActionActivated;
public NotificationImplWinRT()
{
ToastNotificationManagerCompat.OnActivated += OnWinRTActivated;
}
public void Dispose()
{
ToastNotificationManagerCompat.OnActivated -= OnWinRTActivated;
ToastNotificationManagerCompat.History.Clear();
}
private void OnWinRTActivated(ToastNotificationActivatedEventArgsCompat args)
{
ActionActivated?.Invoke(this, args.Argument);
}
public void ShowNotification(NotificationContent content)
{
var builder = new ToastContentBuilder().AddText(content.Body).AddText(content.Summary);
foreach (var action in content.Actions)
{
builder.AddButton(new ToastButton()
.SetContent(action.Label)
.AddArgument(action.Tag));
}
builder.Show();
}
}

View File

@@ -0,0 +1,150 @@
// <copyright file="NotificationImplWpf.cs" company="MaaAssistantArknights">
// MaaWpfGui - A part of the MaaCoreArknights project
// Copyright (C) 2021 MistEO and Contributors
//
// 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 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY
// </copyright>
using System;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Notification.Wpf;
using Notification.Wpf.Base;
using Notification.Wpf.Constants;
using Notification.Wpf.Controls;
namespace MaaWpfGui.Helper.Notification;
internal class NotificationImplWpf : INotificationPoster
{
private NotificationManager _notificationManager = new NotificationManager();
private BrushConverter _brushConverter = new BrushConverter();
public event EventHandler<string> ActionActivated;
public NotificationImplWpf()
{
// 同时显示最大数量
NotificationConstants.NotificationsOverlayWindowMaxCount = 5;
// 默认显示位置
NotificationConstants.MessagePosition = NotificationPosition.BottomRight;
// 最小显示宽度
NotificationConstants.MinWidth = 400d;
// 最大显示宽度
NotificationConstants.MaxWidth = 460d;
}
#region
/// <summary>
/// Gets basic text styles.
/// </summary>
/// <remarks>创建一个基本文本字体样式。</remarks>
public TextContentSettings BaseTextSettings => new TextContentSettings()
{
FontStyle = FontStyles.Normal,
FontFamily = new FontFamily("Microsoft Yahei"),
FontSize = 14,
FontWeight = FontWeights.Normal,
TextAlignment = TextAlignment.Left,
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalTextAlignment = VerticalAlignment.Stretch,
Opacity = 1d,
};
#endregion
public void ShowNotification(NotificationContent content)
{
var wpfcontent = new global::Notification.Wpf.NotificationContent()
{
Title = content.Summary,
Message = content.Body,
Type = NotificationType.None,
Icon = AppIcon.GetIcon(),
CloseOnClick = true,
RowsCount = 1,
TrimType = NotificationTextTrimType.AttachIfMoreRows,
Background = (SolidColorBrush)new BrushConverter().ConvertFrom("#FF1F3550"),
};
// 默认的标题文本样式
var titleTextSettings = BaseTextSettings;
titleTextSettings.FontSize = 18d;
wpfcontent.TitleTextSettings = titleTextSettings;
// 默认的内容文本样式
var messageTextSettings = BaseTextSettings;
wpfcontent.MessageTextSettings = messageTextSettings;
var expirationTime = TimeSpan.FromSeconds(3);
foreach (var hint in content.Hints)
{
if (hint == NotificationHint.Expandable)
{
wpfcontent.TrimType = NotificationTextTrimType.Attach;
}
else if (hint == NotificationHint.RecruitHighRarity)
{
wpfcontent.Background = (SolidColorBrush)_brushConverter.ConvertFrom("#FF401804");
wpfcontent.Foreground = (SolidColorBrush)_brushConverter.ConvertFrom("#FFFFC800");
}
else if (hint == NotificationHint.RecruitRobot)
{
// 给通知染上小车相似的颜色
wpfcontent.Background = (SolidColorBrush)_brushConverter.ConvertFrom("#FFFFFFF4");
wpfcontent.Foreground = (SolidColorBrush)_brushConverter.ConvertFrom("#FF111111");
}
else if (hint == NotificationHint.NewVersion)
{
wpfcontent.Background = (SolidColorBrush)_brushConverter.ConvertFrom("#FF007280");
}
else if (hint is NotificationHint.NotificationHintRowCount rowCount)
{
wpfcontent.RowsCount = (uint)rowCount.Value;
}
else if (hint is NotificationHint.NotificationHintExpirationTime expirationTimeHint)
{
expirationTime = expirationTimeHint.Value;
}
}
if (content.Actions.Count >= 1)
{
var action = content.Actions[0];
wpfcontent.LeftButtonContent = action.Label;
wpfcontent.LeftButtonAction = () => ActionActivated?.Invoke(this, action.Tag);
if (expirationTime < TimeSpan.FromSeconds(10))
{
expirationTime = TimeSpan.FromSeconds(10);
}
}
if (content.Actions.Count >= 2)
{
var action = content.Actions[1];
wpfcontent.RightButtonContent = action.Label;
wpfcontent.RightButtonAction = () => ActionActivated?.Invoke(this, action.Tag);
}
_notificationManager.Show(wpfcontent, expirationTime: expirationTime, ShowXbtn: false);
}
}

View File

@@ -16,6 +16,7 @@ using System.Windows.Media;
using HandyControl.Themes;
using HandyControl.Tools;
using MaaWpfGui.Constants;
using MaaWpfGui.WineCompat;
using Microsoft.Win32;
namespace MaaWpfGui.Helper
@@ -27,7 +28,12 @@ namespace MaaWpfGui.Helper
private static void SystemEvents_UserPreferenceChanged(object sender, UserPreferenceChangedEventArgs e)
{
ThemeManager.Current.ApplicationTheme = ThemeManager.GetSystemTheme(isSystemTheme: false);
ThemeManager.Current.AccentColor = ThemeManager.Current.GetAccentColorFromSystem();
if (!WineRuntimeInformation.IsRunningUnderWine)
{
ThemeManager.Current.AccentColor = ThemeManager.Current.GetAccentColorFromSystem();
}
Application.Current.Resources["TitleBrush"] = ThemeManager.Current.AccentColor;
}
@@ -49,9 +55,17 @@ namespace MaaWpfGui.Helper
public static void SwitchToSyncWithOsMode()
{
ThemeManager.Current.UsingWindowsAppTheme = true;
if (WineRuntimeInformation.IsRunningUnderWine)
{
ThemeManager.Current.ApplicationTheme = ThemeManager.GetSystemTheme(isSystemTheme: false);
}
else
{
ThemeManager.Current.UsingWindowsAppTheme = true;
SystemEvents.UserPreferenceChanged += SystemEvents_UserPreferenceChanged;
}
Application.Current.Resources["TitleBrush"] = ThemeManager.Current.AccentColor;
SystemEvents.UserPreferenceChanged += SystemEvents_UserPreferenceChanged;
}
#endregion Swith Theme

View File

@@ -13,30 +13,20 @@
#pragma warning disable SA1307
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.Media;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using HandyControl.Data;
using MaaWpfGui.Configuration;
using Microsoft.Toolkit.Uwp.Notifications;
using MaaWpfGui.Helper.Notification;
using MaaWpfGui.WineCompat;
using Microsoft.Win32;
using Notification.Wpf;
using Notification.Wpf.Base;
using Notification.Wpf.Constants;
using Notification.Wpf.Controls;
using Semver;
using Serilog;
using Stylet;
using Application = System.Windows.Forms.Application;
using FontFamily = System.Windows.Media.FontFamily;
namespace MaaWpfGui.Helper
{
@@ -45,58 +35,75 @@ namespace MaaWpfGui.Helper
/// </summary>
public class ToastNotification : IDisposable
{
// 这玩意有用吗?
// ReSharper disable once UnusedMember.Local
private ToastNotification()
private unsafe struct OSVERSIONINFOEXW
{
if (!CheckToastSystem())
{
NotificationConstants.MessagePosition = NotificationPosition.BottomRight;
}
public int dwOSVersionInfoSize;
public int dwMajorVersion;
public int dwMinorVersion;
public int dwBuildNumber;
public int dwPlatformId;
private fixed char _szCSDVersion[128];
public short wServicePackMajor;
public short wServicePackMinor;
public short wSuiteMask;
public byte wProductType;
public byte wReserved;
public Span<char> szCSDVersion => MemoryMarshal.CreateSpan(ref _szCSDVersion[0], 128);
}
private static bool _systemToastChecked;
private static bool _systemToastCheckInited;
[DllImport("ntdll.dll", ExactSpelling = true)]
private static extern int RtlGetVersion(ref OSVERSIONINFOEXW versionInfo);
// 这玩意有用吗?
// ReSharper disable once UnusedMember.Local
private static INotificationPoster _notificationPoster;
private static readonly string _openUrlPrefix;
static ToastNotification()
{
_notificationPoster = GetNotificationPoster();
_notificationPoster.ActionActivated += OnActionActivated;
_openUrlPrefix = $"OpenUrl:{_notificationPoster.GetHashCode()}:";
}
private static readonly ILogger _logger = Log.ForContext<ToastNotification>();
public static Action<string, string, NotifyIconInfoType> ShowBalloonTip { get; set; }
public static Action<string, Action> AddMenuItemOnFirst { get; set; }
private static unsafe bool IsWindows10OrGreater()
{
var osVersionInfo = default(OSVERSIONINFOEXW);
osVersionInfo.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEXW);
RtlGetVersion(ref osVersionInfo);
var version = new Version(osVersionInfo.dwMajorVersion, osVersionInfo.dwMinorVersion, osVersionInfo.dwBuildNumber);
return version > new Version(10, 0, 10240);
}
/// <summary>
/// Checks toast system.
/// </summary>
/// <returns>The toast system is initialized.</returns>
public bool CheckToastSystem()
private static INotificationPoster GetNotificationPoster()
{
if (_systemToastCheckInited)
if (WineRuntimeInformation.IsRunningUnderWine)
{
return _systemToastChecked;
if (NotificationImplLibNotify.IsSupported)
{
return new NotificationImplLibNotify();
}
}
_systemToastCheckInited = true;
// like "Microsoft Windows 10.0.10240 "
var osDesc = RuntimeInformation.OSDescription;
Regex versionRegex = new Regex(@"\d+\.\d+\.\d+");
var matched = versionRegex.Match(osDesc);
if (!matched.Success)
else if (IsWindows10OrGreater())
{
_systemToastChecked = false;
return _systemToastChecked;
return new NotificationImplWinRT();
}
var osVersion = matched.Groups[0].Value;
bool verParsed = SemVersion.TryParse(osVersion, SemVersionStyles.Strict, out var curVersionObj);
var minimumVersionObj = new SemVersion(10, 0, 10240);
_systemToastChecked = verParsed && curVersionObj.CompareSortOrderTo(minimumVersionObj) >= 0;
return _systemToastChecked;
return new NotificationImplWpf();
}
private NotificationManager _notificationManager = new NotificationManager();
/// <summary>
/// 按钮激活后的事件,参数为按钮标签
/// </summary>
public static event EventHandler<string> ActionActivated;
/// <summary>
/// 通知标题
@@ -108,27 +115,6 @@ namespace MaaWpfGui.Helper
/// </summary>
private StringBuilder _contentCollection = new StringBuilder();
/// <summary>
/// 应用图标资源
/// </summary>
private static ImageSource GetAppIcon()
{
try
{
var icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath);
var imageSource = Imaging.CreateBitmapSourceFromHIcon(
icon!.Handle,
Int32Rect.Empty,
BitmapSizeOptions.FromEmptyOptions());
return imageSource;
}
catch (Exception)
{
return new BitmapImage();
}
}
/// <summary>
/// Initializes a new instance of the <see cref="ToastNotification"/> class.
/// </summary>
@@ -142,18 +128,16 @@ namespace MaaWpfGui.Helper
{
// 初始化通知标题
_notificationTitle = title ?? _notificationTitle;
}
// 同时显示最大数量
NotificationConstants.NotificationsOverlayWindowMaxCount = 5;
// 默认显示位置
// NotificationConstants.MessagePosition = NotificationPosition.BottomRight;
// 最小显示宽度
NotificationConstants.MinWidth = 400d;
// 最大显示宽度
NotificationConstants.MaxWidth = 460d;
private static void OnActionActivated(object sender, string tag)
{
if (tag.StartsWith(_openUrlPrefix)) {
var url = tag.Substring(_openUrlPrefix.Length);
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
} else {
ActionActivated?.Invoke(sender, tag);
}
}
/// <summary>
@@ -276,126 +260,36 @@ namespace MaaWpfGui.Helper
#region
// 左边按钮
private string _buttonLeftText;
private Action _buttonLeftAction;
// 右边按钮
private string _buttonRightText;
private Action _buttonRightAction;
// 系统按钮
private string _buttonSystemText;
/// <summary>
/// Gets or sets the button system URL.
/// </summary>
public string ButtonSystemUrl { get; set; } = string.Empty;
private bool _buttonSystemEnabled;
private List<NotificationAction> _actions = new List<NotificationAction>();
#endregion
/// <summary>
/// 给通知添加一个在左边的按钮,比如确定按钮,多次设置只会按最后一次设置生效
/// 给通知添加一个按钮
/// </summary>
/// <param name="text">按钮标题</param>
/// <param name="action">按钮执行方法</param>
/// <param name="label">按钮标题</param>
/// <param name="tag">事件标签</param>
/// <returns>返回类本身,可继续调用其它方法</returns>
public ToastNotification AddButtonLeft(string text, Action action)
/// <remarks>按钮按下时,触发 <see cref="ActionActivated"/> 事件</remarks>
public ToastNotification AddButton(string label, string tag)
{
AddMenuItemOnFirst(text, action); // TODO: 整理过时代码
_buttonLeftText = text;
_buttonLeftAction = action;
_buttonSystemText = text;
_buttonSystemEnabled = true;
_actions.Add(new NotificationAction(label, tag));
return this;
}
/// <summary>
/// 给通知添加一个在右边的按钮,比如取消按钮,多次设置只会按最后一次设置生效
/// </summary>
/// <param name="text">按钮标题</param>
/// <param name="action">按钮执行方法</param>
/// <returns>返回类本身,可继续调用其它方法</returns>
// ReSharper disable once UnusedMember.Global
public ToastNotification AddButtonRight(string text, Action action)
public static string GetActionTagForOpenWeb(string url)
{
AddMenuItemOnFirst(text, action); // TODO: 整理过时代码
if (url.StartsWith("http://") || url.StartsWith("https://"))
{
return _openUrlPrefix + url;
}
_buttonRightText = text;
_buttonRightAction = action;
_buttonSystemText = text;
_buttonSystemEnabled = true;
return this;
throw new ArgumentException("URL must start with http:// or https://");
}
#endregion
#region
#region
/// <summary>
/// Gets basic text styles.
/// </summary>
/// <remarks>创建一个基本文本字体样式。</remarks>
public TextContentSettings BaseTextSettings => new TextContentSettings()
{
FontStyle = FontStyles.Normal,
FontFamily = new FontFamily("Microsoft Yahei"),
FontSize = 14,
FontWeight = FontWeights.Normal,
TextAlignment = TextAlignment.Left,
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalTextAlignment = VerticalAlignment.Stretch,
Opacity = 1d,
};
/// <summary>
/// 创建一个基本通知内容模板
/// </summary>
/// <returns>The notification content.</returns>
public NotificationContent BaseContent()
{
var content = new NotificationContent()
{
Title = _notificationTitle,
Message = _contentCollection.ToString(),
Type = NotificationType.None,
Icon = GetAppIcon(),
CloseOnClick = true,
RowsCount = 1,
TrimType = NotificationTextTrimType.AttachIfMoreRows,
Background = (SolidColorBrush)new BrushConverter().ConvertFrom("#FF1F3550"),
LeftButtonContent = _buttonLeftText ?? NotificationConstants.DefaultLeftButtonContent,
LeftButtonAction = _buttonLeftAction,
RightButtonContent = _buttonRightText ?? NotificationConstants.DefaultRightButtonContent,
RightButtonAction = _buttonRightAction,
};
// 默认的标题文本样式
var titleTextSettings = BaseTextSettings;
titleTextSettings.FontSize = 18d;
content.TitleTextSettings = titleTextSettings;
// 默认的内容文本样式
var messageTextSettings = BaseTextSettings;
content.MessageTextSettings = messageTextSettings;
return content;
}
#endregion
#region
/// <summary>
@@ -404,11 +298,10 @@ namespace MaaWpfGui.Helper
/// <param name="lifeTime">通知显示时间 (s)</param>
/// <param name="row">内容显示行数,如果内容太多建议使用 <see cref="ShowMore(double, uint, NotificationSounds, NotificationContent)"/></param>
/// <param name="sound">播放提示音</param>
/// <param name="notificationContent">通知内容</param>
/// <param name="hints">通知额外元数据,可能影响通知展示方式</param>
///
public void Show(double lifeTime = 10d, uint row = 1,
NotificationSounds sound = NotificationSounds.Notification,
NotificationContent notificationContent = null)
NotificationSounds sound = NotificationSounds.Notification, params NotificationHint[] hints)
{
// TODO: 整理过时代码
if (!ConfigFactory.CurrentConfig.GUI.UseNotify)
@@ -416,81 +309,35 @@ namespace MaaWpfGui.Helper
return;
}
try
var content = new NotificationContent
{
if (CheckToastSystem())
{
Execute.OnUIThreadAsync(() =>
{
if (_buttonSystemEnabled)
{
Uri burl = new Uri(ButtonSystemUrl);
new ToastContentBuilder()
.AddText(_notificationTitle)
.AddText(_contentCollection.ToString())
.AddButton(new ToastButton()
.SetContent(_buttonSystemText)
.SetProtocolActivation(burl))
.Show();
/*
var toastContent = new ToastContentBuilder()
.AddText(_notificationTitle)
.AddText(_contentCollection.ToString())
.AddButton(new ToastButton()
.SetContent(_buttonSystemText)
.SetProtocolActivation(burl))
.GetToastContent();
var toastXmlDoc = new XmlDocument();
toastXmlDoc.LoadXml(toastContent.GetContent());
var toastNotification = new Windows.UI.Notifications.ToastNotification(toastXmlDoc);
ToastNotificationManager.CreateToastNotifier().Show(toastNotification);
*/
}
else
{
new ToastContentBuilder()
.AddText(_notificationTitle)
.AddText(_contentCollection.ToString())
.Show();
/*
var toastContent = new ToastContentBuilder()
.AddText(_notificationTitle)
.AddText(_contentCollection.ToString())
.GetToastContent();
var toastXmlDoc = new XmlDocument();
toastXmlDoc.LoadXml(toastContent.GetContent());
var toastNotification = new Windows.UI.Notifications.ToastNotification(toastXmlDoc);
ToastNotificationManager.CreateToastNotifier().Show(toastNotification);
*/
}
});
Summary = _notificationTitle,
Body = _contentCollection.ToString(),
};
// 通知正常弹出了就直接 return否则用 catch 下面的兼容版通知
return;
}
}
catch (Exception e)
foreach (var action in _actions)
{
_logger.Error(e, "显示通知失败");
_systemToastChecked = false;
content.Actions.Add(action);
}
notificationContent ??= BaseContent();
notificationContent.RowsCount = row;
content.Hints.Add(NotificationHint.RowCount((int)row));
// 调整显示时间,如果存在按钮的情况下显示时间将强制设为最大时间
lifeTime = lifeTime < 3d ? 3d : lifeTime;
var timeSpan = _buttonLeftAction == null && _buttonRightAction == null
var timeSpan = _actions.Count != 0
? TimeSpan.FromSeconds(lifeTime)
: TimeSpan.MaxValue;
content.Hints.Add(NotificationHint.ExpirationTime(timeSpan));
foreach (var hint in hints)
{
content.Hints.Add(hint);
}
// 显示通知
_notificationManager.Show(
notificationContent,
expirationTime: timeSpan,
ShowXbtn: false);
_notificationPoster.ShowNotification(content);
// 播放通知提示音
PlayNotificationSound(sound);
@@ -505,18 +352,16 @@ namespace MaaWpfGui.Helper
/// <param name="lifeTime">通知显示时间 (s)</param>
/// <param name="row">内容显示行数,只用于预览一部分通知,多出内容会放在附加按钮的窗口中</param>
/// <param name="sound">播放提示音,不设置就没有声音</param>
/// <param name="notificationContent">通知内容</param>
public void ShowMore(double lifeTime = 12d, uint row = 2,
NotificationSounds sound = NotificationSounds.None,
NotificationContent notificationContent = null)
NotificationSounds sound = NotificationSounds.None, params NotificationHint[] hints)
{
notificationContent ??= BaseContent();
notificationContent.TrimType = NotificationTextTrimType.Attach;
var morehints = new NotificationHint[hints.Length + 1];
hints.CopyTo(morehints, 0);
morehints[hints.Length] = NotificationHint.Expandable;
Show(lifeTime: lifeTime,
row: row,
sound: sound,
notificationContent: notificationContent);
hints: morehints);
}
/// <summary>
@@ -525,15 +370,9 @@ namespace MaaWpfGui.Helper
/// <param name="row">内容显示行数,比如第 2 行用来放星星</param>
public void ShowRecruit(uint row = 1)
{
var content = BaseContent();
// 给通知染上资深标签相似的颜色
content.Background = (SolidColorBrush)new BrushConverter().ConvertFrom("#FF401804");
content.Foreground = (SolidColorBrush)new BrushConverter().ConvertFrom("#FFFFC800");
Show(row: row,
sound: NotificationSounds.Notification,
notificationContent: content);
hints: NotificationHint.RecruitHighRarity);
}
/// <summary>
@@ -542,15 +381,9 @@ namespace MaaWpfGui.Helper
/// <param name="row">内容显示行数,比如第 2 行用来放星星</param>
public void ShowRecruitRobot(uint row = 1)
{
var content = BaseContent();
// 给通知染上小车相似的颜色
content.Background = (SolidColorBrush)new BrushConverter().ConvertFrom("#FFFFFFF4");
content.Foreground = (SolidColorBrush)new BrushConverter().ConvertFrom("#FF111111");
Show(row: row,
sound: NotificationSounds.Notification,
notificationContent: content);
hints: NotificationHint.RecruitRobot);
}
/// <summary>
@@ -559,13 +392,9 @@ namespace MaaWpfGui.Helper
/// <param name="row">内容行数</param>
public void ShowUpdateVersion(uint row = 3)
{
var content = BaseContent();
content.Background = (SolidColorBrush)new BrushConverter().ConvertFrom("#FF007280");
ShowMore(row: row,
sound: NotificationSounds.Notification,
notificationContent: content);
hints: NotificationHint.NewVersion);
}
#endregion
@@ -693,12 +522,18 @@ namespace MaaWpfGui.Helper
public void Dispose()
{
_contentCollection.Clear();
}
_notificationManager = null;
_notificationTitle = null;
_contentCollection = null;
_buttonLeftText = _buttonRightText = null;
_buttonLeftAction = _buttonRightAction = null;
public static void Cleanup()
{
try
{
(_notificationPoster as IDisposable)?.Dispose();
}
catch (Exception e)
{
Log.ForContext<ToastNotification>().Error(e, "Cleanup error");
}
}
}
}

View File

@@ -11,6 +11,7 @@
<UseWpf>true</UseWpf>
<Configurations>Debug;Release;RelWithDebInfo</Configurations>
<Platforms>ARM64;x64</Platforms>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
</PropertyGroup>
<!-- Build and Publish -->

View File

@@ -19,6 +19,8 @@ using System.Runtime.InteropServices;
using System.Security.Principal;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Threading;
using GlobalHotKey;
using MaaWpfGui.Helper;
@@ -30,7 +32,7 @@ using MaaWpfGui.Services.Web;
using MaaWpfGui.States;
using MaaWpfGui.ViewModels.UI;
using MaaWpfGui.Views.UI;
using Microsoft.Toolkit.Uwp.Notifications;
using MaaWpfGui.WineCompat;
using Serilog;
using Serilog.Core;
using Stylet;
@@ -120,6 +122,14 @@ namespace MaaWpfGui.Main
_logger.Information("Run as Administrator");
}
if (WineRuntimeInformation.IsRunningUnderWine)
{
_logger.Information($"Running under Wine {WineRuntimeInformation.WineVersion} on {WineRuntimeInformation.HostSystemName}");
RenderOptions.ProcessRenderMode = RenderMode.SoftwareOnly;
_logger.Information($"MaaWineBridge status: {MaaWineBridge.Availability}");
_logger.Information($"MaaDesktopIntegration available: {MaaDesktopIntegration.Availabile}");
}
_logger.Information("===================================");
try
@@ -211,12 +221,7 @@ namespace MaaWpfGui.Main
Instances.MaaHotKeyManager.Release();
// 关闭程序时清理操作中心中的通知
var os = RuntimeInformation.OSDescription;
if (string.Compare(os, "Microsoft Windows 10.0.10240", StringComparison.Ordinal) >= 0)
{
// new ToastNotificationHistory().Clear();
ToastNotificationManagerCompat.History.Clear();
}
ToastNotification.Cleanup();
ConfigurationHelper.Release();
@@ -229,7 +234,7 @@ namespace MaaWpfGui.Main
{
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = System.Windows.Forms.Application.ExecutablePath,
FileName = Environment.ProcessPath,
};
Process.Start(startInfo);

View File

@@ -10,7 +10,7 @@
<Style x:Key="MdXamlStyle" TargetType="FlowDocument">
<Setter Property="Background" Value="{DynamicResource MdXamlBackground}" />
<Setter Property="FontFamily" Value="Microsoft YaHei" />
<Setter Property="FontFamily" Value="Microsoft YaHei, Global User Interface" />
<Setter Property="FontSize" Value="16" />
<Setter Property="TextAlignment" Value="Left" />
<Setter Property="PagePadding" Value="10" />
@@ -57,7 +57,7 @@
<Setter Property="FontWeight" Value="Bold" />
</Trigger>
<Trigger Property="Tag" Value="CodeBlock">
<Setter Property="FontFamily" Value="Consolas, Microsoft YaHei" />
<Setter Property="FontFamily" Value="Consolas, Microsoft YaHei, Global Monospace" />
<Setter Property="FontSize" Value="14" />
<Setter Property="Background" Value="{DynamicResource MdXamlCodeBackground}" />
<Setter Property="Padding" Value="20, 10" />
@@ -76,7 +76,7 @@
<Style.Triggers>
<Trigger Property="Tag" Value="CodeSpan">
<Setter Property="Background" Value="{DynamicResource MdXamlCodeBackground}" />
<Setter Property="FontFamily" Value="Consolas, Microsoft YaHei" />
<Setter Property="FontFamily" Value="Consolas, Microsoft YaHei, Global Monospace" />
<Setter Property="FontSize" Value="14" />
</Trigger>
</Style.Triggers>
@@ -86,7 +86,7 @@
<Style.Triggers>
<Trigger Property="Tag" Value="CodeSpan">
<Setter Property="Background" Value="{DynamicResource MdXamlCodeBackground}" />
<Setter Property="FontFamily" Value="Consolas, Microsoft YaHei" />
<Setter Property="FontFamily" Value="Consolas, Microsoft YaHei, Global Monospace" />
<Setter Property="FontSize" Value="14" />
</Trigger>
</Style.Triggers>

View File

@@ -26,7 +26,7 @@ namespace MaaWpfGui.Utilities
{
private static readonly ILogger _logger = Log.ForContext("SourceContext", "AutoStart");
private static readonly string _fileValue = Process.GetCurrentProcess().MainModule?.FileName;
private static readonly string _fileValue = Environment.ProcessPath;
private static readonly string _uniqueIdentifier = GetHashCode(_fileValue);
private static readonly string _startupFolderPath = Environment.GetFolderPath(Environment.SpecialFolder.Startup);

View File

@@ -43,6 +43,7 @@ using MaaWpfGui.Services.RemoteControl;
using MaaWpfGui.States;
using MaaWpfGui.Utilities;
using MaaWpfGui.Utilities.ValueType;
using MaaWpfGui.WineCompat;
using Microsoft.Win32;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@@ -3608,10 +3609,11 @@ namespace MaaWpfGui.ViewModels.UI
// ReSharper disable once UnusedMember.Global
public void SelectFile()
{
var dialog = new OpenFileDialog
var dialog = new OpenFileDialog();
if (MaaWineBridge.Availability == WineBridgeAvailability.NotAvailable)
{
Filter = LocalizationHelper.GetString("AdbProgram") + "|*.exe",
};
dialog.Filter = LocalizationHelper.GetString("AdbProgram") + "|*.exe";
}
if (dialog.ShowDialog() == true)
{

View File

@@ -503,8 +503,7 @@ namespace MaaWpfGui.ViewModels.UI
}
}
toast.AddButtonLeft(text, action);
toast.ButtonSystemUrl = UpdateUrl;
toast.AddButton(text, ToastNotification.GetActionTagForOpenWeb(UpdateUrl));
toast.ShowUpdateVersion();
});
@@ -593,10 +592,9 @@ namespace MaaWpfGui.ViewModels.UI
OutputDownloadProgress(downloading: false, output: LocalizationHelper.GetString("NewVersionDownloadFailedTitle"));
_ = Execute.OnUIThreadAsync(() =>
{
using var toast = new ToastNotification(LocalizationHelper.GetString("NewVersionDownloadFailedTitle"));
toast.ButtonSystemUrl = UpdateUrl;
var toast = new ToastNotification(LocalizationHelper.GetString("NewVersionDownloadFailedTitle"));
toast.AppendContentText(LocalizationHelper.GetString("NewVersionDownloadFailedDesc"))
.AddButtonLeft(text, action)
.AddButton(text, ToastNotification.GetActionTagForOpenWeb(UpdateUrl))
.Show();
});
return CheckUpdateRetT.NoNeedToUpdate;

View File

@@ -8,6 +8,7 @@
xmlns:mdxam="clr-namespace:MdXaml;assembly=MdXaml"
xmlns:styles="clr-namespace:MaaWpfGui.Styles"
xmlns:ui="clr-namespace:MaaWpfGui.ViewModels.UI"
Icon="../../newlogo.ico"
Title="{DynamicResource Announcement}"
Width="600"
Height="500"

View File

@@ -11,6 +11,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:styles="clr-namespace:MaaWpfGui.Styles"
x:Name="ErrorViewWindow"
Icon="../../newlogo.ico"
Title="{DynamicResource Error}"
Width="600"
Height="480"

View File

@@ -33,12 +33,12 @@ namespace MaaWpfGui.Views.UI
InitializeComponent();
InitIcon();
_menuItemNum = notifyIcon.ContextMenu.Items.Count;
ToastNotification.ShowBalloonTip = notifyIcon.ShowBalloonTip;
ToastNotification.AddMenuItemOnFirst = AddMenuItemOnFirst;
}
private void InitIcon()
{
notifyIcon.Icon = AppIcon.GetIcon();
notifyIcon.Click += NotifyIcon_MouseClick;
notifyIcon.MouseDoubleClick += OnNotifyIconDoubleClick;

View File

@@ -154,7 +154,7 @@
Margin="10"
HorizontalAlignment="Stretch"
BorderThickness="0"
FontFamily="SimSun"
FontFamily="SimSun, Global Monospace"
FontSize="14"
IsReadOnly="True"
Text="{Binding OperBoxResult}"

View File

@@ -11,6 +11,7 @@
xmlns:ui="clr-namespace:MaaWpfGui.ViewModels.UI"
xmlns:viewModels="clr-namespace:MaaWpfGui.ViewModels"
xmlns:vm="clr-namespace:MaaWpfGui"
Icon="../../newlogo.ico"
Title="{DynamicResource VersionUpdated}"
Width="600"
Height="500"

View File

@@ -0,0 +1,261 @@
// <copyright file="FontConfigIntegration.cs" company="MaaAssistantArknights">
// MaaWpfGui - A part of the MAA project
// Copyright (C) 2021 MistEO and Contributors
// </copyright>
using System;
using System.Diagnostics;
using System.Numerics;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows;
using System.Windows.Markup;
using System.Windows.Media;
using MaaWpfGui.Helper;
using Serilog;
using static MaaWpfGui.WineCompat.FontConfig.Native;
namespace MaaWpfGui.WineCompat.FontConfig;
public unsafe class FontConfigIntegration
{
private readonly ref struct Defer(Action action)
{
public void Dispose() => action();
}
public static void Install(string wpf, string fontconfig)
{
var wpffont = new FontFamily(wpf);
var fceq = GetFontFamilyFromPattern(fontconfig);
if (fceq == null)
{
return;
}
var map = wpffont.FamilyMaps;
map.Clear();
int i = 0;
foreach (var item in fceq.FamilyMaps)
{
map.Insert(i++, item);
}
}
public static void Install()
{
Debug.WriteLine($"FontConfigIntegration Install");
// GC.KeepAlive(DefaultFont.UserInterface);
// DefaultFont.UserInterface = GetFontFamilyFromPattern("system-ui");
// DefaultFont.Monospace = GetFontFamilyFromPattern("monospace");
// DefaultFont.SansSerif = GetFontFamilyFromPattern("sans-serif");
// DefaultFont.Serif = GetFontFamilyFromPattern("serif");
Install("Global User Interface", "system-ui");
Install("Global Monospace", "monospace");
Install("Global Sans Serif", "sans-serif");
Install("Global Serif", "serif");
}
public static FontFamily GetFontFamilyFromPattern(string pattern)
{
var logger = Log.ForContext<FontConfigIntegration>();
logger.Information($"GetFontFamilyFromPattern: {pattern}");
var config = FcInitLoadConfigAndFonts();
using var _dtor_config = new Defer(() => FcConfigDestroy(config));
var fcPattern = FcNameParse(pattern);
using var _dtor_pattern = new Defer(() => FcPatternDestroy(fcPattern));
FcConfigSubstitute(config, fcPattern, FcMatchKind.Pattern);
FcDefaultSubstitute(fcPattern);
var fs = FcFontSort(config, fcPattern, FcBool.True, null, out var result);
if (fs == null)
{
logger.Error("FcFontSort failed");
return null;
}
var wpffont = new FontFamily();
wpffont.FamilyNames.Add(XmlLanguage.GetLanguage("en-us"), "FontConfig " + pattern);
var resolved = FcCharSetCreate();
using var _dtor_resolved = new Defer(() => FcCharSetDestroy(resolved));
for (var i = 0; i < fs->nfont; i++)
{
var item = new FontFamilyMap();
result = FcPatternGetString(fs->fonts[i], "family", 0, out var pfamily);
if (result == FcResult.Match)
{
item.Target = Marshal.PtrToStringUTF8(pfamily);
logger.Information(item.Target);
}
else
{
logger.Error("FcPatternGetString failed: {0}", result);
continue;
}
result = FcPatternGetCharSet(fs->fonts[i], "charset", 0, out var s);
if (result == FcResult.Match)
{
var fallback = FcCharSetSubtract(s, resolved);
using var _dtor_fallback = new Defer(() => FcCharSetDestroy(fallback));
FcCharSetMerge(resolved, s, out _);
var range = CharSetToUnicodeRange(fallback);
item.Unicode = range;
logger.Information($"{item.Target} {range}");
wpffont.FamilyMaps.Add(item);
}
else
{
logger.Error("FcPatternGetCharSet failed: {0}", result);
}
}
return wpffont;
}
private static string CharSetToUnicodeRange(FcCharSet* cs)
{
// return "0020-10FFFF";
Span<uint> bitmap = stackalloc uint[FC_CHARSET_MAP_SIZE];
var sb = new StringBuilder();
var offset = FcCharSetFirstPage(cs, ref bitmap[0], out var next);
var range_start = 0u;
var range_end = 0u;
bool in_range = false;
if ((bitmap[0] & 1) != 0)
{
BeginRange(offset);
}
void BeginRange(uint start)
{
range_start = start;
range_end = start;
in_range = true;
}
void AdvanceRange(uint length)
{
if (!in_range)
{
throw new InvalidOperationException("range not started");
}
range_end += length;
}
void EndRange()
{
if (!in_range)
{
throw new InvalidOperationException("range not started");
}
if (sb.Length > 0)
{
sb.Append(",");
}
if (range_end - range_start == 1)
{
sb.Append($"{range_start:X4}");
}
else
{
sb.Append($"{range_start:X4}-{range_end - 1:X4}");
}
in_range = false;
range_start = 0;
range_end = 0;
}
while (offset != FC_CHARSET_DONE)
{
for (var i = 0; i < bitmap.Length; i++)
{
var field = bitmap[i];
var bit_offset = 0u;
// happy path: no bits unset
if (field == 0xFFFFFFFF)
{
if (!in_range)
{
BeginRange(offset + ((uint)i * 32));
}
AdvanceRange(32);
continue;
}
if (field == 0)
{
if (in_range)
{
EndRange();
}
continue;
}
while (field != 0 && bit_offset < 32)
{
var trailing_0 = BitOperations.TrailingZeroCount(field);
if (trailing_0 > 0)
{
if (in_range)
{
EndRange();
}
field >>= trailing_0;
bit_offset += (uint)trailing_0;
if (field != 0)
{
BeginRange(offset + ((uint)i * 32) + bit_offset);
}
}
var trailing_1 = BitOperations.TrailingZeroCount(~field);
if (trailing_1 > 0)
{
if (!in_range)
{
BeginRange(offset + ((uint)i * 32) + bit_offset);
}
AdvanceRange((uint)trailing_1);
field >>= trailing_1;
bit_offset += (uint)trailing_1;
if (field == 0 && bit_offset < 32)
{
EndRange();
}
}
}
}
if (next == FC_CHARSET_DONE)
{
break;
}
if (in_range && next != offset + (bitmap.Length * 32))
{
EndRange();
}
offset = FcCharSetNextPage(cs, ref bitmap[0], ref next);
}
return sb.ToString();
}
}

View File

@@ -0,0 +1,67 @@
// <copyright file="Native.cs" company="MaaAssistantArknights">
// MaaWpfGui - A part of the MAA project
// Copyright (C) 2021 MistEO and Contributors
// </copyright>
using System.Runtime.InteropServices;
namespace MaaWpfGui.WineCompat.FontConfig;
internal unsafe class Native
{
private const string DllName = MaaDesktopIntegration.DllName;
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode, ExactSpelling = true)]
public static extern FcConfig* FcInitLoadConfigAndFonts();
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode, ExactSpelling = true)]
public static extern FcPattern* FcNameParse([MarshalAs(UnmanagedType.LPUTF8Str)] string pattern);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode, ExactSpelling = true)]
public static extern FcBool FcConfigSubstitute(FcConfig* config, FcPattern* pattern, FcMatchKind kind);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode, ExactSpelling = true)]
public static extern void FcDefaultSubstitute(FcPattern* pattern);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode, ExactSpelling = true)]
public static extern FcResult FcPatternGetString(FcPattern* pattern, [MarshalAs(UnmanagedType.LPUTF8Str)] string obj, int n, out nint s);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode, ExactSpelling = true)]
public static extern FcResult FcPatternGetCharSet(FcPattern* pattern, [MarshalAs(UnmanagedType.LPUTF8Str)] string obj, int n, out FcCharSet* s);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode, ExactSpelling = true)]
public static extern FcFontSet* FcFontSort(FcConfig* config, FcPattern* pattern, FcBool trim, nint* blanks, out FcResult result);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode, ExactSpelling = true)]
public static extern FcCharSet* FcCharSetCreate();
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode, ExactSpelling = true)]
public static extern FcCharSet* FcCharSetSubtract(FcCharSet* a, FcCharSet* b);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode, ExactSpelling = true)]
public static extern int FcCharSetMerge(FcCharSet* a, FcCharSet* b, out FcBool changed);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode, ExactSpelling = true)]
public static extern uint FcCharSetFirstPage(FcCharSet* a, ref uint map, out uint next);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode, ExactSpelling = true)]
public static extern uint FcCharSetNextPage(FcCharSet* a, ref uint map, ref uint next);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode, ExactSpelling = true)]
public static extern void FcCharSetDestroy(FcCharSet* a);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode, ExactSpelling = true)]
public static extern void FcPatternDestroy(FcPattern* pattern);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode, ExactSpelling = true)]
public static extern void FcFontSetDestroy(FcFontSet* fs);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode, ExactSpelling = true)]
public static extern void FcConfigDestroy(FcConfig* config);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode, ExactSpelling = true)]
public static extern int MaaFcCharsetMapSize();
public static int FC_CHARSET_MAP_SIZE { get; } = MaaFcCharsetMapSize();
public const uint FC_CHARSET_DONE = unchecked((uint)-1);
}

View File

@@ -0,0 +1,44 @@
// <copyright file="Types.cs" company="MaaAssistantArknights">
// MaaWpfGui - A part of the MAA project
// Copyright (C) 2021 MistEO and Contributors
// </copyright>
using System;
using System.Runtime.InteropServices;
namespace MaaWpfGui.WineCompat.FontConfig;
internal enum FcResult
{
Match,
NoMatch,
TypeMismatch,
NoId,
OutOfMemory
}
internal enum FcMatchKind
{
Pattern,
Font,
Scan,
FcMatchKindEnd,
FcMatchKindBegin = Pattern
}
internal unsafe struct FcFontSet
{
public int nfont;
public int sfont;
public FcPattern** fonts;
}
internal struct FcConfig { }
internal struct FcPattern { }
internal struct FcCharSet { }
internal enum FcBool
{
False,
True,
DontCare
}

View File

@@ -0,0 +1,34 @@
// <copyright file="GObject.cs" company="MaaAssistantArknights">
// MaaWpfGui - A part of the MaaCoreArknights project
// Copyright (C) 2021 MistEO and Contributors
//
// 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 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY
// </copyright>
using System;
using static MaaWpfGui.WineCompat.MaaDesktopIntegration;
namespace MaaWpfGui.WineCompat
{
internal class GObject
{
public nint Handle { get; }
protected GObject(nint handle)
{
Handle = handle;
}
~GObject()
{
g_object_unref(Handle);
}
}
}

View File

@@ -0,0 +1,60 @@
// <copyright file="GdkPixBuf.cs" company="MaaAssistantArknights">
// MaaWpfGui - A part of the MaaCoreArknights project
// Copyright (C) 2021 MistEO and Contributors
//
// 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 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY
// </copyright>
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using static MaaWpfGui.WineCompat.MaaDesktopIntegration;
namespace MaaWpfGui.WineCompat;
internal class GdkPixBuf : GObject
{
public const int GDK_COLORSPACE_RGB = 0;
private class PinnedPixels : IDisposable
{
public MemoryHandle Handle { get; }
public PinnedPixels(Memory<byte> pixels)
{
Handle = pixels.Pin();
}
public void Dispose()
{
Handle.Dispose();
}
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
public static void DestroyCallback(IntPtr pixels, IntPtr data)
{
var handle = GCHandle.FromIntPtr(data);
handle.Free();
}
}
private GdkPixBuf(nint handle)
: base(handle)
{
}
public static unsafe GdkPixBuf CreateFromPixels(int width, int height, int rowStride, bool has_alpha, Memory<byte> pixels)
{
var pin = new PinnedPixels(pixels);
var gch = GCHandle.Alloc(pin);
var obj = gdk_pixbuf_new_from_data((IntPtr)pin.Handle.Pointer, GDK_COLORSPACE_RGB, has_alpha ? 1 : 0, 8, width, height, rowStride, &PinnedPixels.DestroyCallback, GCHandle.ToIntPtr(gch));
return new GdkPixBuf(obj);
}
}

View File

@@ -0,0 +1,137 @@
// <copyright file="LibNotifyNotification.cs" company="MaaAssistantArknights">
// MaaWpfGui - A part of the MaaCoreArknights project
// Copyright (C) 2021 MistEO and Contributors
//
// 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 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY
// </copyright>
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using static MaaWpfGui.WineCompat.MaaDesktopIntegration;
namespace MaaWpfGui.WineCompat;
internal class LibNotifyNotification : GObject
{
protected GCHandle WeakGCHandle { get; }
public LibNotifyNotification(string title, string message, string iconPath)
: base(CreateObject(title, message, iconPath))
{
WeakGCHandle = GCHandle.Alloc(this, GCHandleType.Weak);
unsafe
{
g_signal_connect_data(Handle, "closed", &CloseSignalCallback, GCHandle.ToIntPtr(WeakGCHandle), &CloseSignalFreeCallback, 0);
}
}
~LibNotifyNotification()
{
WeakGCHandle.Free();
}
private static IntPtr CreateObject(string title, string message, string iconPath)
{
return notify_notification_new(title, message, iconPath);
}
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
private static void ActionCallback(nint notification, nint action, nint userData)
{
var instance = GCHandle.FromIntPtr(userData).Target as LibNotifyNotification;
instance?.ActionActivated?.Invoke(instance, Marshal.PtrToStringUTF8(action)!);
}
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
private static void ActionFreeCallback(nint userData)
{
}
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
private static void CloseSignalCallback(nint notification, nint userData)
{
var instance = GCHandle.FromIntPtr(userData).Target as LibNotifyNotification;
instance?.Closed?.Invoke(instance, EventArgs.Empty);
}
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
private static void CloseSignalFreeCallback(nint userData, nint closure)
{
}
public event EventHandler<string> ActionActivated;
public event EventHandler Closed;
public unsafe void AddAction(string tag, string label)
{
notify_notification_add_action(Handle, tag, label, &ActionCallback, GCHandle.ToIntPtr(WeakGCHandle), &ActionFreeCallback);
}
public void ClearActions()
{
notify_notification_clear_actions(Handle);
}
public void ClearHints()
{
notify_notification_clear_hints(Handle);
}
public void Close()
{
notify_notification_close(Handle, out var error);
}
public void SetAppIcon(string appIcon)
{
notify_notification_set_app_icon(Handle, appIcon);
}
public void SetAppName(string appName)
{
notify_notification_set_app_name(Handle, appName);
}
public void SetCategory(string category)
{
notify_notification_set_category(Handle, category);
}
public void SetHint(string key, nint value)
{
notify_notification_set_hint(Handle, key, value);
}
public void SetImageFromPixbuf(GdkPixBuf pixbuf)
{
notify_notification_set_image_from_pixbuf(Handle, pixbuf.Handle);
}
public void SetTimeout(int timeout)
{
notify_notification_set_timeout(Handle, timeout);
}
public void SetUrgency(int urgency)
{
notify_notification_set_urgency(Handle, urgency);
}
public void Show()
{
notify_notification_show(Handle, out var error);
}
public bool Update(string title, string message, string iconPath)
{
return notify_notification_update(Handle, title, message, iconPath) != 0;
}
}

View File

@@ -0,0 +1,107 @@
// <copyright file="MaaWineBridge.cs" company="MaaAssistantArknights">
// MaaWpfGui - A part of the MaaCoreArknights project
// Copyright (C) 2021 MistEO and Contributors
//
// 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 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY
// </copyright>
using System;
using System.Runtime.InteropServices;
namespace MaaWpfGui.WineCompat;
internal unsafe class MaaDesktopIntegration
{
public static bool Availabile { get; }
static MaaDesktopIntegration()
{
if (WineRuntimeInformation.IsRunningUnderWine)
{
try
{
glib_default_main_loop_start();
AppDomain.CurrentDomain.ProcessExit += (sender, args) => glib_default_main_loop_stop();
Availabile = true;
}
catch
{
// Ignore
}
}
}
public const string DllName = "MaaDesktopIntegration.so";
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
public static extern int glib_default_main_loop_start();
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
public static extern void glib_default_main_loop_stop();
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
public static extern int notify_init([MarshalAs(UnmanagedType.LPUTF8Str)] string app_name);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
public static extern void g_object_unref(IntPtr ptr);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr notify_notification_new([MarshalAs(UnmanagedType.LPUTF8Str)] string summary, [MarshalAs(UnmanagedType.LPUTF8Str)] string body, [MarshalAs(UnmanagedType.LPUTF8Str)] string icon);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
public static extern void notify_notification_add_action(IntPtr notification, [MarshalAs(UnmanagedType.LPUTF8Str)] string action, [MarshalAs(UnmanagedType.LPUTF8Str)] string label, delegate* unmanaged[Stdcall]<nint, nint, nint, void> callback, IntPtr user_data, delegate* unmanaged[Stdcall]<nint, void> free_func);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
public static extern void notify_notification_clear_actions(IntPtr notification);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
public static extern void notify_notification_clear_hints(IntPtr notification);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
public static extern void notify_notification_close(IntPtr notification, out IntPtr error);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr notify_notification_get_activation_token(IntPtr notification);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
public static extern int notify_notification_get_closed_reason(IntPtr notification);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
public static extern void notify_notification_set_app_icon(IntPtr notification, [MarshalAs(UnmanagedType.LPUTF8Str)] string app_icon);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
public static extern void notify_notification_set_app_name(IntPtr notification, [MarshalAs(UnmanagedType.LPUTF8Str)] string app_name);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
public static extern void notify_notification_set_category(IntPtr notification, [MarshalAs(UnmanagedType.LPUTF8Str)] string category);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
public static extern void notify_notification_set_hint(IntPtr notification, [MarshalAs(UnmanagedType.LPUTF8Str)] string key, IntPtr value);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
public static extern void notify_notification_set_image_from_pixbuf(IntPtr notification, IntPtr pixbuf);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
public static extern void notify_notification_set_timeout(IntPtr notification, int timeout);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
public static extern void notify_notification_set_urgency(IntPtr notification, int urgency);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
public static extern void notify_notification_show(IntPtr notification, out IntPtr error);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
public static extern int notify_notification_update(IntPtr notification, [MarshalAs(UnmanagedType.LPUTF8Str)] string summary, [MarshalAs(UnmanagedType.LPUTF8Str)] string body, [MarshalAs(UnmanagedType.LPUTF8Str)] string icon);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr gdk_pixbuf_new_from_data(IntPtr data, int colorspace, int has_alpha, int bits_per_sample, int width, int height, int rowstride, delegate* unmanaged[Stdcall]<nint, nint, void> destroy_fn, IntPtr destroy_fn_data);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr g_signal_connect_data(nint instance, [MarshalAs(UnmanagedType.LPUTF8Str)] string detailed_signal, delegate* unmanaged[Stdcall]<nint, nint, void> c_handler, nint data, delegate* unmanaged[Stdcall]<nint, nint, void> destroy_data, uint connect_flags);
}

View File

@@ -0,0 +1,42 @@
// <copyright file="MaaWineBridge.cs" company="MaaAssistantArknights">
// MaaWpfGui - A part of the MAA project
// Copyright (C) 2021 MistEO and Contributors
// </copyright>
using System.Runtime.InteropServices;
namespace MaaWpfGui.WineCompat;
internal static class MaaWineBridge
{
// MaaWineBridge
[DllImport("MaaCore.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern int dl_has_maacore();
public static WineBridgeAvailability Availability { get; }
static MaaWineBridge()
{
if (!WineRuntimeInformation.IsRunningUnderWine)
{
Availability = WineBridgeAvailability.NotAvailable;
return;
}
try
{
if (dl_has_maacore() != 0)
{
Availability = WineBridgeAvailability.Operational;
}
else
{
Availability = WineBridgeAvailability.Faulted;
}
}
catch
{
Availability = WineBridgeAvailability.NotAvailable;
}
}
}

View File

@@ -0,0 +1,11 @@
// <copyright file="WineBridgeAvailability.cs" company="MaaAssistantArknights">
// MaaWpfGui - A part of the MAA project
// Copyright (C) 2021 MistEO and Contributors
// </copyright>
public enum WineBridgeAvailability
{
Operational,
Faulted,
NotAvailable,
}

View File

@@ -0,0 +1,59 @@
// <copyright file="WineRuntimeInformation.cs" company="MaaAssistantArknights">
// MaaWpfGui - A part of the MaaCoreArknights project
// Copyright (C) 2021 MistEO and Contributors
//
// 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 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY
// </copyright>
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace MaaWpfGui.WineCompat
{
internal class WineRuntimeInformation
{
[DllImport("ntdll.dll", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr wine_get_version();
[DllImport("ntdll.dll", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
private static extern void wine_get_host_version(out IntPtr sysname, out IntPtr release);
public static bool IsRunningUnderWine { get; }
public static string? WineVersion { get; }
public static string? HostSystemName { get; }
static WineRuntimeInformation()
{
try
{
var ptr = wine_get_version();
if (ptr != IntPtr.Zero)
{
IsRunningUnderWine = true;
}
WineVersion = Marshal.PtrToStringUTF8(ptr);
wine_get_host_version(out var sysname, out var release);
HostSystemName = Marshal.PtrToStringUTF8(sysname);
}
catch
{
IsRunningUnderWine = false;
}
}
}
}