Refakturiert die Anwendung und modernisiert das Build-System

Diese umfangreiche Überarbeitung verbessert die Architektur, den Build-Prozess und die Benutzeroberfläche der Toolbox.

Wesentliche Änderungen:
- **Build-System:** Umfassende Modernisierung des CMake-Builds für verbesserte Stabilität und Wartbarkeit. Die Konfiguration wurde auf zielbasierte Befehle umgestellt, `c_tray` stabilisiert und `saucer-desktop` entfernt.
- **Modulare Architektur:** Die Kernlogik von `ToolboxWindow` wurde in dedizierte Helferklassen ausgelagert, darunter `LuaRunner` für die Skriptausführung, `ToolRegistry` für die Tool-Erkennung, `ResourceLoader` für UI-Ressourcen, `ToolboxJsBridge` für JavaScript-Bindungen und `WindowPlacement` für die Fensterverwaltung. Dies reduziert die Komplexität und fördert die Wiederverwendbarkeit.
- **Benutzeroberfläche (UI):** Die HTML-Templates (`einstellungen_custom.html`, `index.html`) und Stylesheets (`styles.css`) wurden zentralisiert und überarbeitet, um eine kohärentere Benutzeroberfläche mit benutzerdefinierten Fensterdekorationen zu bieten. Neue Funktionen wie das Öffnen der Kommandozeile und eine "Tool hinzufügen"-Karte wurden integriert.
- **Tray-Verwaltung:** Die Tray-Icon-Logik wurde in `TrayController` gekapselt, um eine robustere und zentralisierte Steuerung zu ermöglichen, insbesondere im Umgang mit Explorer-Neustarts.
This commit is contained in:
2026-05-08 15:31:42 +02:00
parent 843f978309
commit 074fa73992
26 changed files with 1676 additions and 701 deletions

View File

@@ -1,11 +1,18 @@
cmake_minimum_required(VERSION 3.30)
set(CMAKE_CXX_STANDARD 23)
set(saucer_no_version_check ON)
set(BUILD_SHARED_LIBS OFF)
option(BUILD_SHARED_LIBS "Build as shared library" OFF)
project(Toolbox LANGUAGES C CXX)
project(Toolbox)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(BUILD_SHARED_LIBS OFF CACHE BOOL "Build shared libraries" FORCE)
set(saucer_no_version_check ON CACHE BOOL "Skip saucer version checks" FORCE)
# MSVC 19.44 provides the STL features saucer-fill may try to polyfill
# (jthread, stop_token, move_only_function, join_with). Keep the polyfill target
# present for saucer, but disable replacements to avoid duplicate declarations.
set(fill_replacements "" CACHE STRING "saucer-fill replacements" FORCE)
add_subdirectory(lib/c_tray)
add_subdirectory(lib/json)
@@ -13,88 +20,94 @@ add_subdirectory(lib/cppcodec)
add_subdirectory(lib/saucer)
add_subdirectory(lib/lua)
add_subdirectory(lib/sol2)
#add_subdirectory(lib/saucer4lua)
add_subdirectory(lib/toolbox_win)
# add_subdirectory(lib/saucer4lua)
include_directories(inc)
include_directories(lib/json/include/nlohmann)
#include_directories(out_embed)
file(GLOB_RECURSE APP_SOURCES CONFIGURE_DEPENDS
"src/*.cpp"
)
file(GLOB_RECURSE APP_HEADERS CONFIGURE_DEPENDS
"inc/*.h"
"inc/*.hpp"
)
if (WIN32)
set(RESOURCE_FILES toolbox.rc)
list(APPEND APP_SOURCES toolbox.rc)
endif ()
# Header-Datei einbinden
include_directories(lib/c_tray)
# Plattformabhängige Einstellungen
if (WIN32)
set(PLATFORM_SOURCES lib/c_tray/tray_windows.c)
set(LIBS shell32)
elseif (APPLE)
set(PLATFORM_SOURCES lib/c_tray/tray_darwin.m)
find_library(COCOA_LIBRARY Cocoa)
set(LIBS ${COCOA_LIBRARY})
elseif (UNIX)
set(PLATFORM_SOURCES lib/c_tray/tray_linux.c)
find_package(PkgConfig REQUIRED)
pkg_check_modules(APPINDICATOR REQUIRED appindicator3-0.1)
include_directories(${APPINDICATOR_INCLUDE_DIRS})
set(LIBS ${APPINDICATOR_LIBRARIES})
endif ()
file(GLOB_RECURSE SOURCES "src/*.cpp" "inc/*.h")
list(APPEND SOURCES "src/toolConfig.cpp") # Nur für .cpp und .h
file(GLOB_RECURSE MODULE_SOURCES "module/*.ixx") # Separates GLOB für Module
add_executable(Toolbox main.cpp
${SOURCES}
${RESOURCE_FILES}
${PLATFORM_SOURCES}
add_executable(Toolbox
main.cpp
${APP_SOURCES}
${APP_HEADERS}
)
target_sources(Toolbox
PUBLIC
PUBLIC
FILE_SET cxx_modules TYPE CXX_MODULES
FILES ${MODULE_SOURCES}
FILES
module/toolboxTray.ixx
)
target_include_directories(${PROJECT_NAME} PRIVATE "out_embed")
target_include_directories(Toolbox
PRIVATE
inc
lib/c_tray
)
# Ensure WebView2 headers from saucer's NuGet are available to this target (for saucer webview2 backend)
if (WIN32)
# Find the WebView2 include folder that saucer fetched into the build tree
target_compile_definitions(Toolbox PRIVATE NOMINMAX UNICODE _UNICODE)
file(GLOB WEBVIEW2_INCLUDE_DIR
"${CMAKE_BINARY_DIR}/lib/saucer/nuget/packages/Microsoft.Web.WebView2.*/build/native/include")
"${CMAKE_BINARY_DIR}/lib/saucer/nuget/packages/Microsoft.Web.WebView2.*/build/native/include"
)
if (WEBVIEW2_INCLUDE_DIR)
message(STATUS "Using WebView2 includes at: ${WEBVIEW2_INCLUDE_DIR}")
target_include_directories(${PROJECT_NAME} PRIVATE ${WEBVIEW2_INCLUDE_DIR})
target_include_directories(Toolbox PRIVATE ${WEBVIEW2_INCLUDE_DIR})
else ()
message(WARNING "WebView2 headers not found in build tree. The build may fail when including WebView2.h.")
endif ()
endif ()
saucer_embed("res" TARGET ${PROJECT_NAME})
set(EMBED_DESTINATION "${CMAKE_BINARY_DIR}/embedded")
file(MAKE_DIRECTORY "${EMBED_DESTINATION}/saucer/embedded")
CPMFindPackage(
NAME saucer-desktop
VERSION 4.0.2
GIT_REPOSITORY "https://github.com/saucer/desktop"
file(GLOB_RECURSE EMBED_RESOURCE_FILES CONFIGURE_DEPENDS
"${CMAKE_CURRENT_SOURCE_DIR}/res/*"
)
target_link_libraries(
${PROJECT_NAME}
PRIVATE
foreach (RESOURCE_FILE IN LISTS EMBED_RESOURCE_FILES)
if (IS_DIRECTORY "${RESOURCE_FILE}")
continue()
endif ()
file(RELATIVE_PATH RESOURCE_RELATIVE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/res" "${RESOURCE_FILE}")
get_filename_component(RESOURCE_RELATIVE_DIR "${RESOURCE_RELATIVE_PATH}" DIRECTORY)
file(MAKE_DIRECTORY "${EMBED_DESTINATION}/saucer/embedded/files/${RESOURCE_RELATIVE_DIR}")
endforeach ()
saucer_embed("res"
TARGET Toolbox
DESTINATION "${EMBED_DESTINATION}"
)
target_link_libraries(Toolbox
PRIVATE
saucer::saucer
sol2::sol2
lua::lib
tray
nlohmann_json
cppcodec
#saucer4lua
toolbox_win
saucer::embedded
saucer::desktop
)
if (WIN32)
target_link_libraries(Toolbox PRIVATE shell32 user32 gdi32)
endif ()
if (WIN32 AND (CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRel"))
set_target_properties(Toolbox PROPERTIES WIN32_EXECUTABLE TRUE)
endif ()

81
REPO_REVIEW_PLAN.md Normal file
View File

@@ -0,0 +1,81 @@
# Repository Review Plan
## Kurzbewertung
Das Projekt ist ein brauchbarer Prototyp fuer eine Windows-Tray-Toolbox mit Saucer/WebView-UI, lokalen `.Toolbox`-Tools, Lua-Ausfuehrung und Tool-Konfiguration. Die Produktidee ist klar, aber die technische Struktur ist noch stark gewachsen: viel Verantwortung sitzt in `ToolboxWindow`, Threading ist riskant, der Build ist fragil und die HTML-Bruecke braucht Haertung.
## Priorisierter Plan
### 1. Build stabilisieren
Status: teilweise erledigt. Der lokale Debug-Build, CLion-CMake-Reload und ein Runtime-Smoke-Test von `Toolbox.exe` sind auf diesem Arbeitsplatz stabil. Vollstaendig abgeschlossen ist der Punkt erst, wenn auch Release/MinSizeRel, Doku und optional CI geprueft sind.
Erledigt:
- `CMakeLists.txt` target-basiert aufgeraeumt.
- Globale `include_directories()` durch `target_include_directories()` ersetzt.
- Doppelte Source-Einbindung entfernt.
- Source-Erfassung auf Wildcards mit `CONFIGURE_DEPENDS` umgestellt.
- `tray_windows.c` nicht mehr direkt im App-Target mitkompiliert.
- `c_tray` Windows-Backend stabilisiert: fehlende C-Header ergaenzt, Win32-Wide-API explizit genutzt und den 64-bit `malloc`-Pointer-Crash beim Tray-Menue behoben.
- Harte `/MTd`-CRT-Option aus `c_tray` entfernt, damit die Library zum restlichen MSVC-Debug-Build passt.
- Ungenutztes `saucer-desktop` aus dem App-Link entfernt.
- MSVC/Saucer-Fill-Konflikt fuer MSVC 19.44 behoben, indem Polyfill-Ersetzungen deaktiviert wurden.
- `WIN32_LEAN_AND_MEAN` vom App-Target entfernt, weil es GDI+ Header unvollstaendig machte.
- Reproduzierbaren lokalen Debug-Smoke-Build und App-Start geprueft.
Noch offen:
- Release-Build pruefen.
- MinSizeRel-Build pruefen.
- README um Build-Voraussetzungen und Build-Befehl ergaenzen.
- Pruefen, ob generierte Build-/Embed-Artefakte sauber ignoriert werden.
- Optional CI-Workflow fuer Debug/Release-Smoke-Build anlegen.
Reproduzierbarer lokaler Smoke-Build:
```powershell
cmd /c "call ""C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat"" >nul && cmake -S . -B cmake-build-debug && cmake --build cmake-build-debug --target Toolbox --config Debug"
```
### 2. `ToolboxWindow` zerlegen
- Status: erledigt fuer die geplanten ersten Schnitte. `ToolRegistry`, `LuaRunner`, `ToolboxJsBridge`, `WindowPlacement` und `ResourceLoader` sind aus `ToolboxWindow` herausgezogen.
- `ToolRegistry`: erledigt. Scannt `.Toolbox`, liest `description.txt`, `icon.png`, `start.lua`.
- `LuaRunner`: erledigt. Startet/cancelt Lua-Tasks, verwaltet Timeout-Hook und haelt Task-State ausserhalb von `ToolboxWindow`.
- `ToolboxJsBridge`: erledigt. Registriert `m_webview.expose(...)` ueber explizite Callback-Bindings.
- `WindowPlacement`: erledigt. Buendelt DPI, Position, Groesse, Centering und Resize-Helfer.
- `ResourceLoader`: erledigt. Buendelt embedded resources, Start-URL, `serve`, Icon, Titel und Scheme.
### 3. Threading und UI-Zugriff bereinigen
- Detached Threads vermeiden oder zentral verwalten.
- Lua-Worker kontrolliert beenden.
- UI-Updates nicht direkt aus Worker-Threads ausfuehren.
- Sichtbarkeits- und Focus-State entweder UI-thread-lokal halten oder atomar absichern.
### 4. Frontend haerten
- `innerHTML`-Templates durch DOM-Erzeugung mit `textContent` ersetzen.
- Daten aus `.Toolbox` nicht ungeescaped rendern.
- Doppelte Render-Logik in `index.html` zusammenfuehren.
- UI-Zustand fuer laufende Tools klarer modellieren.
### 5. Lua-Policy klaeren
- Entscheiden, ob Tools trusted code sind.
- Falls nicht trusted: `os`, `io`, `package` standardmaessig deaktivieren oder Berechtigungen pro Tool einfuehren.
- Fehler und Status gezielt an die UI melden, statt pauschal zu reloaden.
### 6. Win32-Helfer vereinheitlichen
- F12-Subclassing aus Toolbox- und Optionsfenster in einen gemeinsamen Helper ziehen.
- `ShellExecuteW` statt `std::system` fuer Explorer/Terminal nutzen.
- Encoding-Probleme in Quelltext und UI bereinigen.
### 7. Tests und Smoke Checks einfuehren
- Unit-Tests fuer `ToolConfigStore`.
- Tests fuer Tool-Scanning ohne echte User-Home-Abhaengigkeit.
- Debug/Release-Smoke-Build lokal und spaeter in CI.
- Optional HTML-Lint oder DOM-Test fuer Renderfunktionen.

28
inc/luaRunner.h Normal file
View File

@@ -0,0 +1,28 @@
#pragma once
#include <atomic>
#include <filesystem>
#include <functional>
#include <memory>
#include <mutex>
#include <unordered_map>
class LuaRunner {
public:
using CompletionCallback = std::function<void()>;
void run(int id, const std::filesystem::path& scriptPath, int timeoutSeconds, CompletionCallback onComplete);
void cancel(int id);
private:
struct LuaTask {
std::shared_ptr<std::atomic_bool> cancel;
};
struct State {
std::mutex tasksMutex;
std::unordered_map<int, LuaTask> tasks;
};
std::shared_ptr<State> m_state = std::make_shared<State>();
};

11
inc/resourceLoader.h Normal file
View File

@@ -0,0 +1,11 @@
#pragma once
#include <string>
#include <saucer/smartview.hpp>
class ResourceLoader {
public:
static bool loadToolboxShell(saucer::smartview& webview);
static bool serveHtml(saucer::smartview& webview, const std::string& htmlPath, bool setDecorations);
};

29
inc/toolRegistry.h Normal file
View File

@@ -0,0 +1,29 @@
#pragma once
#include <filesystem>
#include <string>
#include <vector>
struct ToolboxItem {
std::string id;
std::string name;
std::string path;
std::string icon;
std::string lua_script_path_string;
std::string description;
std::string category;
std::string version;
std::string author;
std::string url;
std::string license;
};
class ToolRegistry {
public:
static std::filesystem::path toolboxDirectory(const std::filesystem::path& userPath);
static bool ensureToolboxDirectory(const std::filesystem::path& userPath);
static std::vector<ToolboxItem> loadTools(const std::filesystem::path& userPath);
private:
static void encodeIcon(const std::filesystem::path& imagePath, ToolboxItem& item);
};

28
inc/toolboxJsBridge.h Normal file
View File

@@ -0,0 +1,28 @@
#pragma once
#include <functional>
#include <string>
#include <saucer/smartview.hpp>
class ToolboxJsBridge {
public:
struct Bindings {
std::function<std::string()> getToolboxItems;
std::function<void(int)> runLua;
std::function<void(int)> cancelLua;
std::function<void()> debugPrint;
std::function<void()> openFileSystem;
std::function<void()> openCommandPrompt;
std::function<void(int)> openSettings;
std::function<void()> openIndex;
std::function<void()> getNewContent;
std::function<int()> getLuaTimeoutSeconds;
std::function<void(int)> setLuaTimeoutSeconds;
std::function<std::string()> getActiveToolName;
std::function<std::string()> getToolConfigIni;
std::function<void(const std::string&)> setToolConfigIni;
};
static void registerBindings(saucer::smartview& webview, Bindings bindings);
};

View File

@@ -2,14 +2,15 @@
#include <saucer/smartview.hpp>
#include "modules/hwnd_module.h"
#include <saucer/embedded/all.hpp>
#include <sol/sol.hpp>
#include <saucer/webview.hpp>
#include <unordered_map>
#include <mutex>
#include <chrono>
#include "luaRunner.h"
#include "optionWindow.h"
#include "resourceLoader.h"
#include "toolConfig.h"
#include "toolboxJsBridge.h"
#include "toolRegistry.h"
#include "windowPlacement.h"
class ToolboxWindow {
public:
@@ -30,20 +31,6 @@ public:
std::atomic_bool isIndex = true;
struct ToolboxItem{
std::string id;
std::string name;
std::string path;
std::string icon;
std::string lua_script_path_string;
std::string description;
std::string category;
std::string version;
std::string author;
std::string url;
std::string license;
};
std::vector<ToolboxItem> m_toolboxItems;
saucer::smartview m_webview;
@@ -60,8 +47,6 @@ private:
std::string jsonString;
int size_x,size_y;
#ifdef _WIN32
std::filesystem::path userPath = std::getenv("USERPROFILE"); // Windows
#else
@@ -75,7 +60,6 @@ private:
void configureWindow();
void moveWindowTo(int x, int y);
void loadResources();
void runLuaScript();
void ExposeToJs();
@@ -92,6 +76,8 @@ private:
void open_file_system();
void open_cmd();
void SetJsonItems();
void openEinst();
@@ -107,14 +93,8 @@ private:
void setLuaTimeoutSeconds(int seconds);
std::string getActiveToolName();
struct LuaTask {
std::shared_ptr<std::atomic_bool> cancel;
};
std::mutex m_luaTasksMutex;
std::unordered_map<int, LuaTask> m_luaTasks;
int m_activeToolId = -1;
LuaRunner m_luaRunner;
ToolConfigStore m_configStore;
std::filesystem::path getToolConfigPath(int id) const;

21
inc/trayController.h Normal file
View File

@@ -0,0 +1,21 @@
#pragma once
#include <memory>
class ToolboxWindow;
class TrayController {
public:
explicit TrayController(ToolboxWindow& window);
~TrayController();
TrayController(const TrayController&) = delete;
TrayController& operator=(const TrayController&) = delete;
bool initialize();
void shutdown();
private:
struct Impl;
std::unique_ptr<Impl> m_impl;
};

14
inc/windowPlacement.h Normal file
View File

@@ -0,0 +1,14 @@
#pragma once
#include <Windows.h>
#include <saucer/smartview.hpp>
class WindowPlacement {
public:
static void configureToolbox(saucer::smartview& webview, HWND hwnd);
static void moveTo(HWND hwnd, int x, int y);
static void center(HWND hwnd);
static void resize(saucer::smartview& webview, int width, int height);
static void resizePercent(saucer::smartview& webview, double widthPercent, double heightPercent);
};

View File

@@ -0,0 +1,18 @@
cmake_minimum_required(VERSION 3.30)
project(toolbox_win LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
add_library(toolbox_win STATIC
src/icon_utils.cpp
)
target_include_directories(toolbox_win
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
)
if (WIN32)
target_link_libraries(toolbox_win PUBLIC user32)
endif ()

View File

@@ -0,0 +1,12 @@
#pragma once
#ifdef _WIN32
#include <Windows.h>
#else
typedef void* HINSTANCE;
typedef void* HICON;
#endif
extern "C" {
HICON toolbox_load_small_icon(HINSTANCE instance, int resource_id);
}

View File

@@ -0,0 +1,26 @@
#include "toolbox_win/icon_utils.h"
#ifdef _WIN32
#include <Windows.h>
#endif
HICON toolbox_load_small_icon(HINSTANCE instance, int resource_id) {
#ifdef _WIN32
if (!instance) {
instance = GetModuleHandle(NULL);
}
return static_cast<HICON>(LoadImage(
instance,
MAKEINTRESOURCE(resource_id),
IMAGE_ICON,
GetSystemMetrics(SM_CXSMICON),
GetSystemMetrics(SM_CYSMICON),
LR_SHARED
));
#else
(void)instance;
(void)resource_id;
return nullptr;
#endif
}

View File

@@ -1,82 +1,31 @@
#include <saucer/app.hpp>
#include <Windows.h>
#include <saucer/app.hpp>
#include <memory>
#include "toolboxWindow.h"
#include "resource.h"
#include "tray.h"
#include "trayController.h"
import toolboxTray;
coco::stray start(saucer::application* app) {
std::shared_ptr<saucer::application> appShared(app, [](saucer::application*) {});
ToolboxWindow* g_toolboxWindow = nullptr;
auto window = std::make_unique<ToolboxWindow>(appShared);
window->initialize();
void on_quit(struct tray_menu *item) {
tray_exit();
}
void on_open(struct tray_menu *item = nullptr) {
if (g_toolboxWindow) {
if (g_toolboxWindow->m_bShow) {
g_toolboxWindow->hide();
} else {
g_toolboxWindow->show();
}
TrayController tray(*window);
if (!tray.initialize()) {
co_return;
}
}
coco::stray start(saucer::application *app){
// Toolbox-Fenster erstellen und initialisieren
std::shared_ptr<saucer::application> app_shared(app, [](saucer::application*){});
auto toolboxWindow = std::make_unique<ToolboxWindow>(app_shared);
toolboxWindow->initialize();
g_toolboxWindow = toolboxWindow.get();
HICON hIcon = static_cast<HICON>(LoadImage(
GetModuleHandle(NULL),
MAKEINTRESOURCE(IDI_ICON1),
IMAGE_ICON,
GetSystemMetrics(SM_CXSMICON),
GetSystemMetrics(SM_CYSMICON),
LR_SHARED
));
// Tray-Menue definieren
tray_menu tray_menu_items[] = {
{"Oeffnen", 0, 0, 0, on_open, nullptr},
{"-", 0, 0, 0, nullptr, nullptr}, // Separator
{"Beenden", 0, 0, 0, on_quit, nullptr},
{nullptr, 0, 0, 0, nullptr, nullptr} // Ende des Menues
};
// ToolboxTray erstellen
toolboxTray tray("toolbox.ico", "Toolbox");
tray.icon_handle = hIcon;
tray.icon_is_shared = (hIcon != nullptr) ? 1 : 0;
// Linksklick-Aktion registrieren
tray.setLeftClickAction([&]() {
on_open(nullptr); // Ruft die Funktion ohne Menueeintrag auf
});
// Tray-Menue registrieren
tray.setTrayMenu(tray_menu_items);
if (tray_init(&tray) != 0) {
fprintf(stderr, "Fehler beim Initialisieren des Tray-Icons\n");
co_return; // Fehlerfall: coroutine beenden
}
//toolboxWindow->show();
co_await app->finish();
}
#ifdef _DEBUG
int main(int argc, char** argv) {
int main(int, char**) {
return saucer::application::create({.id = "toolbox"})->run(start);
}
#else
#include <Windows.h>
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) {
return saucer::application::create({.id = "toolbox"})->run(start);
}
#endif

View File

@@ -4,147 +4,10 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Toolbox</title>
<style>
body {
font-family: Arial, sans-serif;
background: linear-gradient(to bottom, #1e1e2e, #28293d);
color: white;
margin: 0;
padding: 0;
display: flex;
height: 100vh;
}
/* Linke Sidebar */
.sidebar {
width: 200px;
background-color: #2b2c3b;
padding: 15px;
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.3);
overflow-y: auto;
}
.sidebar h2 {
color: #ff79c6;
font-size: 16px;
margin: 0 0 15px 0;
}
.sidebar ul {
list-style: none;
padding: 0;
margin: 0;
}
.sidebar ul li {
margin-bottom: 10px;
}
.sidebar ul li a {
text-decoration: none;
padding: 10px;
display: block;
border-radius: 4px;
background: #32334a;
color: #fff;
transition: background 0.3s;
}
.sidebar ul li a:hover {
background: #ff79c6;
color: #000;
}
/* Hauptbereich mit flexibler Anzeige */
.main-container {
flex: 1;
display: flex;
flex-direction: column;
}
.header {
background: #1b1b2b;
padding: 15px 20px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: space-between;
}
.header h1 {
margin: 0;
font-size: 18px;
color: #ff79c6;
}
.back-button {
background: #2b2c3b;
border: none;
color: #ff79c6;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
transition: background 0.3s;
}
.back-button:hover {
background: #32334a;
}
.editor {
flex: 1;
background: #1e1e2e;
padding: 15px;
display: none; /* Standardmäßig ausgeblendet */
flex-direction: column;
align-items: center;
justify-content: center;
}
.editor textarea {
width: 80%;
height: 80%;
background: #2b2c3b;
color: #fff;
border: 1px solid #ff79c6;
border-radius: 5px;
font-size: 14px;
padding: 10px;
font-family: monospace;
}
.editor pre {
width: 80%;
margin-top: 10px;
background: #2b2c3b;
color: #fff;
border: 1px solid #32334a;
border-radius: 5px;
padding: 10px;
overflow-x: auto;
}
.general {
flex: 1;
background: #1e1e2e;
padding: 15px;
display: flex; /* Standardmäßig sichtbar */
flex-direction: column;
align-items: center;
justify-content: center;
}
.footer {
background: #1b1b2b;
padding: 10px 20px;
text-align: center;
font-size: 12px;
color: #aaa;
}
</style>
<link rel="stylesheet" href="saucer://embedded/html/styles.css">
</head>
<body>
<body class="settings-page">
<div id="shell">
<!-- Sidebar für Optionen -->
<div class="sidebar">
<h2>Einstellungen</h2>
@@ -173,19 +36,18 @@
</div>
<div class="general" id="general">
<div style="width: 80%; max-width: 520px;">
<div class="settings-card">
<h3 style="margin: 0 0 10px 0;">Tool</h3>
<div id="tool-name" style="margin-bottom: 20px; color: #ff79c6;">-</div>
<div id="tool-name" class="settings-tool-name">-</div>
<label for="lua-timeout">Lua Timeout (Sekunden)</label>
<input id="lua-timeout" type="number" min="0" step="1" value="30"
style="width: 100%; margin-top: 6px; padding: 8px; border-radius: 4px; border: 1px solid #32334a; background: #2b2c3b; color: #fff;">
<div style="margin-top: 6px; font-size: 12px; color: #aaa;">
<input id="lua-timeout" class="settings-input" type="number" min="0" step="1" value="30">
<div class="settings-help">
0 = kein Timeout
</div>
<button id="save-settings" class="back-button" style="margin-top: 16px;">Speichern</button>
<div id="save-status" style="margin-top: 8px; font-size: 12px; color: #aaa;"></div>
<button id="save-settings" class="back-button settings-save">Speichern</button>
<div id="save-status" class="settings-status"></div>
</div>
</div>
@@ -195,6 +57,7 @@
<!--Schmidti.Digital &copy; 2024-->
</div>
</div>
</div>
<script>
function goBack() {
saucer.exposed.openIndex();
@@ -221,13 +84,50 @@
const output = document.getElementById('highlightedOutput');
showContent('general');
function escapeHtml(text) {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function highlightLua(code) {
let escaped = escapeHtml(code);
const tokens = [];
function stash(pattern, className, input) {
return input.replace(pattern, (match) => {
const key = `@@TOKEN${tokens.length}@@`;
tokens.push(`<span class="${className}">${match}</span>`);
return key;
});
}
escaped = stash(/--\[\[[\s\S]*?\]\]|--[^\n]*/g, 'code-comment', escaped);
escaped = stash(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\[\[[\s\S]*?\]\]/g, 'code-string', escaped);
escaped = escaped.replace(/\b(\d+(\.\d+)?)\b/g, '<span class="code-number">$1</span>');
const keywords = [
'and','break','do','else','elseif','end','false','for','function',
'if','in','local','nil','not','or','repeat','return','then','true',
'until','while'
];
const keywordRegex = new RegExp(`\\b(${keywords.join('|')})\\b`, 'g');
escaped = escaped.replace(keywordRegex, '<span class="code-keyword">$1</span>');
tokens.forEach((value, index) => {
escaped = escaped.replace(`@@TOKEN${index}@@`, value);
});
return escaped;
}
// Synchronisiere den Inhalt von textarea mit dem Codeblock
editor.addEventListener('input', () => {
output.innerHTML = ''; // Vorheriges Highlighting entfernen
output.textContent = editor.value; // Aktuellen Text setzen
if (window.hljs && typeof hljs.highlightAll === 'function') {
hljs.highlightAll();
}
output.innerHTML = highlightLua(editor.value);
});
const toolName = document.getElementById('tool-name');
@@ -259,3 +159,7 @@
</body>
</html>

View File

@@ -0,0 +1,175 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Toolbox</title>
<link rel="stylesheet" href="saucer://embedded/html/styles.css">
</head>
<body class="settings-page">
<div id="shell" class="settings-shell">
<div class="resize-handle resize-top" data-webview-resize="t"></div>
<div class="resize-handle resize-right" data-webview-resize="r"></div>
<div class="resize-handle resize-bottom" data-webview-resize="b"></div>
<div class="resize-handle resize-left" data-webview-resize="l"></div>
<div class="resize-handle resize-top-left" data-webview-resize="tl"></div>
<div class="resize-handle resize-top-right" data-webview-resize="tr"></div>
<div class="resize-handle resize-bottom-left" data-webview-resize="bl"></div>
<div class="resize-handle resize-bottom-right" data-webview-resize="br"></div>
<div class="titlebar" data-webview-drag>
<div class="titlebar-left" data-webview-drag>
<button class="titlebar-btn" onclick="goBack()" data-webview-ignore></button>
<span class="titlebar-title" data-webview-drag>Einstellungen</span>
</div>
<div class="titlebar-actions" data-webview-drag>
<button class="titlebar-btn" data-webview-minimize data-webview-ignore></button>
<button class="titlebar-btn close" data-webview-close data-webview-ignore>×</button>
</div>
</div>
<div class="settings-body">
<!-- Sidebar für Optionen -->
<div class="sidebar">
<h2>Einstellungen</h2>
<ul>
<li><a href="#" onclick="showContent('general')">Einstellungen</a></li>
<li><a href="#" onclick="showContent('editor')">Skript</a></li>
<li><a href="#" onclick="showContent('none')">Option 3</a></li>
<li><a href="#" onclick="showContent('none')">Option 4</a></li>
</ul>
</div>
<!-- Hauptcontainer -->
<div class="main-container">
<!-- Zentraler Editor (Standardmäßig versteckt) -->
<div class="editor" id="editor">
<label for="code-editor"></label>
<textarea id="code-editor" placeholder="Write Lua code here..."></textarea>
<pre><code id="highlightedOutput" class="language-lua"></code></pre>
</div>
<div class="general" id="general">
<div class="settings-card">
<h3 style="margin: 0 0 10px 0;">Tool</h3>
<div id="tool-name" class="settings-tool-name">-</div>
<label for="lua-timeout">Lua Timeout (Sekunden)</label>
<input id="lua-timeout" class="settings-input" type="number" min="0" step="1" value="30">
<div class="settings-help">
0 = kein Timeout
</div>
<button id="save-settings" class="back-button settings-save">Speichern</button>
<div id="save-status" class="settings-status"></div>
</div>
</div>
<!-- Footer -->
<div class="footer">
<!--Schmidti.Digital &copy; 2024-->
</div>
</div>
</div>
</div>
<script>
function goBack() {
saucer.exposed.openIndex();
}
function showContent(option) {
const general = document.getElementById('general');
const editor = document.getElementById('editor');
if (option === 'editor') {
editor.style.display = 'flex';
general.style.display = 'none';
} else if (option === 'general') {
general.style.display = 'flex';
editor.style.display = 'none';
} else {
editor.style.display = 'none';
general.style.display = 'none';
}
}
document.addEventListener('DOMContentLoaded', () => {
const editor = document.getElementById('code-editor');
const output = document.getElementById('highlightedOutput');
showContent('general');
function escapeHtml(text) {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function highlightLua(code) {
let escaped = escapeHtml(code);
const tokens = [];
function stash(pattern, className, input) {
return input.replace(pattern, (match) => {
const key = `@@TOKEN${tokens.length}@@`;
tokens.push(`<span class="${className}">${match}</span>`);
return key;
});
}
escaped = stash(/--\[\[[\s\S]*?\]\]|--[^\n]*/g, 'code-comment', escaped);
escaped = stash(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\[\[[\s\S]*?\]\]/g, 'code-string', escaped);
escaped = escaped.replace(/\b(\d+(\.\d+)?)\b/g, '<span class="code-number">$1</span>');
const keywords = [
'and','break','do','else','elseif','end','false','for','function',
'if','in','local','nil','not','or','repeat','return','then','true',
'until','while'
];
const keywordRegex = new RegExp(`\\b(${keywords.join('|')})\\b`, 'g');
escaped = escaped.replace(keywordRegex, '<span class="code-keyword">$1</span>');
tokens.forEach((value, index) => {
escaped = escaped.replace(`@@TOKEN${index}@@`, value);
});
return escaped;
}
// Synchronisiere den Inhalt von textarea mit dem Codeblock
editor.addEventListener('input', () => {
output.innerHTML = highlightLua(editor.value);
});
const toolName = document.getElementById('tool-name');
const timeoutInput = document.getElementById('lua-timeout');
const saveBtn = document.getElementById('save-settings');
const saveStatus = document.getElementById('save-status');
saucer.exposed.getActiveToolName().then(name => {
toolName.textContent = name || '-';
});
saucer.exposed.getLuaTimeoutSeconds().then(value => {
timeoutInput.value = value;
});
saveBtn.addEventListener('click', () => {
const seconds = parseInt(timeoutInput.value, 10);
saucer.exposed.setLuaTimeoutSeconds(Number.isFinite(seconds) ? seconds : 0);
saveStatus.textContent = 'Gespeichert';
setTimeout(() => {
saveStatus.textContent = '';
}, 1500);
});
});
</script>
</body>
</html>

View File

@@ -10,13 +10,23 @@
<body>
<div id="shell">
<div class="header">
<button onclick="getNewContent()" class="button" id="reload-button"></button>
<button onclick="openEinstFirst()" class="button" id="option-button" style="display: none;">⚙️</button>
<div class="header-cluster header-left">
<button onclick="getNewContent()" class="button icon-button" id="reload-button">&#x21bb;</button>
</div>
<h1>Toolbox</h1>
<button onclick="openFileSystem()" id="file-system-button" class="button">📁</button>
<div class="header-cluster header-right">
<div class="button-group">
<button onclick="openCmd()" class="button icon-button" id="cmd-button">&#x1f4bb;</button>
<button onclick="openFileSystem()" id="file-system-button" class="button icon-button">&#x1f4c1;</button>
</div>
<button onclick="openEinstFirst()" class="button icon-button" id="option-button" style="display: none;">&#9881;</button>
</div>
</div>
<div class="main" id="toolbox">
<!-- Tool Cards werden hier dynamisch geladen -->
<div class="add-tool-card" onclick="openAddTool()">
<span class="plus-icon">+</span>
</div>
</div>
<div class="footer">
<!--Schmidti.Digital &copy; 2024-->
@@ -64,10 +74,19 @@
saucer.exposed.open_file_system();
}
function openCmd() {
saucer.exposed.open_cmd();
}
function openAddTool() {
openFileSystem();
}
function reloadContent() {
const toolbox = document.getElementById('toolbox');
// Toolbox leeren
// Toolbox leeren, aber das add-tool-card Element behalten
const addToolCard = toolbox.querySelector('.add-tool-card');
toolbox.innerHTML = '';
// Toolbox-Daten neu abrufen
@@ -97,6 +116,11 @@
toolbox.innerHTML += toolCardHTML;
});
// Das add-tool-card Element wieder hinzufügen
if (addToolCard) {
toolbox.appendChild(addToolCard);
}
// Überprüfen und Wiederherstellen des Zustands der Buttons
document.querySelectorAll('.tool-card').forEach(card => {
const toolId = card.getAttribute('data-tool-id');
@@ -153,6 +177,9 @@
const toolbox = document.getElementById('toolbox');
// Das add-tool-card Element entfernen und später wieder hinzufügen
const addToolCard = toolbox.querySelector('.add-tool-card');
saucer.exposed.getToolboxItems().then(jsonString => {
const m_toolboxItems = JSON.parse(jsonString);
@@ -179,6 +206,11 @@
toolbox.innerHTML += toolCardHTML;
});
// Das add-tool-card Element wieder hinzufügen
if (addToolCard) {
toolbox.appendChild(addToolCard);
}
// Überprüfen und Wiederherstellen des Zustands beim Laden der Seite
document.querySelectorAll('.tool-card').forEach(card => {
const toolId = card.getAttribute('data-tool-id');

View File

@@ -29,6 +29,7 @@
height: 100vh;
}
#shell {
position: relative;
display: flex;
flex-direction: column;
height: 100vh;
@@ -38,7 +39,11 @@
}
.header {
background: linear-gradient(180deg, rgba(20, 45, 70, 0.85), rgba(12, 28, 45, 0.7));
padding: 16px 20px;
padding: 12px 18px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
text-align: center;
box-shadow: 0 10px 22px var(--shadow), inset 0 1px 0 var(--shine);
border-bottom: 1px solid rgba(140, 210, 255, 0.2);
@@ -57,8 +62,6 @@
pointer-events: none;
}
.button {
position: absolute;
transform: translateY(-50%);
background: linear-gradient(180deg, rgba(120, 210, 255, 0.35), rgba(60, 150, 220, 0.55));
border: 1px solid rgba(140, 210, 255, 0.45);
color: var(--ink);
@@ -71,27 +74,50 @@
}
.button:hover {
background: linear-gradient(180deg, rgba(150, 225, 255, 0.45), rgba(80, 170, 235, 0.7));
transform: translateY(-52%);
transform: translateY(-1px);
box-shadow: 0 10px 18px var(--shadow), inset 0 1px 0 var(--shine);
}
.button:active {
transform: translateY(-48%);
transform: translateY(1px);
box-shadow: 0 4px 10px var(--shadow), inset 0 2px 6px rgba(0,0,0,0.25);
}
#file-system-button {
top: 50%;
right: 18px;
.header-cluster {
display: flex;
align-items: center;
gap: 8px;
min-width: 120px;
}
#reload-button {
top: 50%;
left: 18px;
.header-left {
justify-content: flex-start;
}
#option-button {
top: 50%;
right: 92px;
.header-right {
justify-content: flex-end;
}
.button-group {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 6px;
border-radius: 999px;
background: rgba(12, 28, 45, 0.55);
border: 1px solid rgba(140, 210, 255, 0.25);
box-shadow: inset 0 1px 0 var(--shine), 0 8px 16px rgba(4, 10, 16, 0.35);
}
.icon-button {
width: 36px;
height: 36px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 50%;
font-size: 16px;
line-height: 1;
}
.header h1 {
margin: 0;
flex: 1;
text-align: center;
font-size: 19px;
letter-spacing: 0.5px;
color: var(--ink-soft);
@@ -187,6 +213,36 @@
color: #3b2a00;
cursor: progress;
}
.add-tool-card {
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: 2px dashed rgba(140, 210, 255, 0.4);
padding: 8px 12px;
border-radius: 14px;
margin-bottom: 12px;
height: calc((62px + 24px) / 2);
cursor: pointer;
transition: all 0.2s ease;
backdrop-filter: blur(6px);
}
.add-tool-card:hover {
border-color: rgba(140, 210, 255, 0.7);
background: rgba(18, 40, 60, 0.3);
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(4, 10, 16, 0.3);
}
.add-tool-card .plus-icon {
font-size: 24px;
color: var(--aqua-2);
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
font-weight: 300;
}
.add-tool-card:hover .plus-icon {
color: var(--aqua-1);
transform: scale(1.1);
}
.footer {
background: linear-gradient(180deg, rgba(18, 40, 60, 0.65), rgba(8, 18, 28, 0.6));
padding: 10px 20px;
@@ -195,3 +251,273 @@
color: #9bbfd6;
border-top: 1px solid rgba(140, 210, 255, 0.2);
}
.settings-page {
display: flex;
height: 100vh;
}
.settings-page #shell {
flex-direction: column;
width: 100%;
}
.settings-page .settings-shell {
height: 100vh;
}
.settings-page .settings-body {
flex: 1;
display: flex;
min-height: 0;
position: relative;
z-index: 1;
}
.settings-page .titlebar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 10px 12px;
background: linear-gradient(180deg, rgba(20, 45, 70, 0.85), rgba(12, 28, 45, 0.75));
border-bottom: 1px solid rgba(140, 210, 255, 0.2);
box-shadow: 0 10px 20px var(--shadow), inset 0 1px 0 var(--shine);
user-select: none;
z-index: 2;
}
.settings-page .titlebar-left {
display: flex;
align-items: center;
gap: 10px;
}
.settings-page .titlebar-title {
font-size: 14px;
letter-spacing: 0.4px;
color: var(--ink-soft);
}
.settings-page .titlebar-actions {
display: flex;
gap: 8px;
}
.settings-page .titlebar-btn {
border: 1px solid rgba(140, 210, 255, 0.35);
background: rgba(10, 26, 40, 0.7);
color: var(--ink);
width: 30px;
height: 26px;
border-radius: 8px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
box-shadow: inset 0 1px 0 var(--shine);
transition: background 0.2s ease, transform 0.2s ease;
}
.settings-page .titlebar-btn:hover {
background: rgba(30, 65, 95, 0.8);
transform: translateY(-1px);
}
.settings-page .titlebar-btn.close:hover {
background: rgba(160, 45, 55, 0.85);
}
.settings-page .resize-handle {
position: absolute;
z-index: 3;
}
.settings-page .resize-top,
.settings-page .resize-bottom {
left: 6px;
right: 6px;
height: 8px;
}
.settings-page .resize-left,
.settings-page .resize-right {
top: 6px;
bottom: 6px;
width: 8px;
}
.settings-page .resize-top {
top: 0;
cursor: ns-resize;
}
.settings-page .resize-bottom {
bottom: 0;
cursor: ns-resize;
}
.settings-page .resize-left {
left: 0;
cursor: ew-resize;
}
.settings-page .resize-right {
right: 0;
cursor: ew-resize;
}
.settings-page .resize-top-left,
.settings-page .resize-top-right,
.settings-page .resize-bottom-left,
.settings-page .resize-bottom-right {
width: 12px;
height: 12px;
}
.settings-page .resize-top-left {
top: 0;
left: 0;
cursor: nwse-resize;
}
.settings-page .resize-top-right {
top: 0;
right: 0;
cursor: nesw-resize;
}
.settings-page .resize-bottom-left {
bottom: 0;
left: 0;
cursor: nesw-resize;
}
.settings-page .resize-bottom-right {
bottom: 0;
right: 0;
cursor: nwse-resize;
}
.settings-page .sidebar {
width: 220px;
padding: 16px;
background: rgba(12, 28, 45, 0.7);
border-right: 1px solid rgba(140, 210, 255, 0.2);
box-shadow: 8px 0 18px var(--shadow);
overflow-y: auto;
}
.settings-page .sidebar h2 {
color: var(--ink-soft);
font-size: 16px;
margin: 0 0 12px 0;
letter-spacing: 0.4px;
}
.settings-page .sidebar ul {
list-style: none;
padding: 0;
margin: 0;
}
.settings-page .sidebar ul li {
margin-bottom: 10px;
}
.settings-page .sidebar ul li a {
text-decoration: none;
padding: 10px 12px;
display: block;
border-radius: 10px;
background: var(--glass);
color: var(--ink);
border: 1px solid var(--glass-border);
box-shadow: inset 0 1px 0 var(--shine);
transition: background 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
}
.settings-page .sidebar ul li a:hover {
background: var(--glass-strong);
transform: translateY(-1px);
box-shadow: 0 8px 16px var(--shadow), inset 0 1px 0 var(--shine);
}
.settings-page .main-container {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
min-height: 0;
}
.settings-page .header {
display: flex;
align-items: center;
justify-content: space-between;
text-align: left;
}
.settings-page .header h1 {
margin: 0;
font-size: 18px;
color: var(--ink-soft);
}
.settings-page .back-button {
border: 1px solid rgba(140, 210, 255, 0.45);
background: linear-gradient(180deg, rgba(120, 210, 255, 0.35), rgba(60, 150, 220, 0.55));
color: var(--ink);
padding: 8px 14px;
border-radius: 999px;
cursor: pointer;
box-shadow: 0 6px 12px var(--shadow), inset 0 1px 0 var(--shine);
transition: transform 0.15s ease, box-shadow 0.2s ease, background 0.2s ease;
}
.settings-page .back-button:hover {
background: linear-gradient(180deg, rgba(150, 225, 255, 0.45), rgba(80, 170, 235, 0.7));
transform: translateY(-1px);
box-shadow: 0 10px 18px var(--shadow), inset 0 1px 0 var(--shine);
}
.settings-page .editor,
.settings-page .general {
flex: 1;
padding: 16px;
display: none;
align-items: center;
justify-content: center;
background: transparent;
min-height: 0;
}
.settings-page .settings-card {
width: min(80%, 520px);
background: var(--glass);
border: 1px solid var(--glass-border);
border-radius: 14px;
padding: 16px;
box-shadow: 0 12px 22px var(--shadow), inset 0 1px 0 var(--shine);
}
.settings-page .settings-tool-name {
margin-bottom: 20px;
color: var(--accent);
}
.settings-page .settings-input,
.settings-page .editor textarea {
width: 100%;
margin-top: 6px;
padding: 10px;
border-radius: 10px;
border: 1px solid var(--glass-border);
background: rgba(12, 28, 45, 0.6);
color: var(--ink);
font-size: 14px;
box-shadow: inset 0 1px 0 var(--shine);
box-sizing: border-box;
}
.settings-page .editor textarea {
height: 70%;
font-family: "Consolas", "Courier New", monospace;
}
.settings-page .editor pre {
width: 100%;
margin-top: 10px;
background: rgba(12, 28, 45, 0.6);
color: var(--ink);
border: 1px solid var(--glass-border);
border-radius: 10px;
padding: 10px;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-word;
box-shadow: inset 0 1px 0 var(--shine);
}
.settings-page .code-keyword {
color: var(--accent);
font-weight: 600;
}
.settings-page .code-string {
color: #ffd27a;
}
.settings-page .code-number {
color: #8fe7ff;
}
.settings-page .code-comment {
color: #7aa0b5;
font-style: italic;
}
.settings-page .settings-help,
.settings-page .settings-status {
margin-top: 8px;
font-size: 12px;
color: #9bbfd6;
}
.settings-page .settings-save {
margin-top: 16px;
}

104
src/luaRunner.cpp Normal file
View File

@@ -0,0 +1,104 @@
#include "luaRunner.h"
#include <chrono>
#include <iostream>
#include <thread>
#include <sol/sol.hpp>
extern "C" {
#include <lauxlib.h>
#include <lua.h>
}
namespace {
constexpr int hookInstructionCount = 10000;
struct LuaHookContext {
std::shared_ptr<std::atomic_bool> cancel;
std::chrono::steady_clock::time_point deadline;
};
void luaCancelHook(lua_State* hookState, lua_Debug*) {
auto* hookCtx = *reinterpret_cast<LuaHookContext**>(lua_getextraspace(hookState));
if (!hookCtx) {
return;
}
if (hookCtx->cancel->load(std::memory_order_relaxed)) {
luaL_error(hookState, "Lua script cancelled");
}
if (hookCtx->deadline != (std::chrono::steady_clock::time_point::max)() &&
std::chrono::steady_clock::now() > hookCtx->deadline) {
luaL_error(hookState, "Lua script cancelled or timed out");
}
}
std::chrono::steady_clock::time_point deadlineFromTimeout(int timeoutSeconds) {
if (timeoutSeconds <= 0) {
return (std::chrono::steady_clock::time_point::max)();
}
return std::chrono::steady_clock::now() + std::chrono::seconds(timeoutSeconds);
}
} // namespace
void LuaRunner::run(int id, const std::filesystem::path& scriptPath, int timeoutSeconds, CompletionCallback onComplete) {
auto cancelFlag = std::make_shared<std::atomic_bool>(false);
auto state = m_state;
{
std::lock_guard<std::mutex> lock(state->tasksMutex);
state->tasks[id] = LuaTask{cancelFlag};
}
std::thread([state, id, scriptPath, timeoutSeconds, cancelFlag, onComplete = std::move(onComplete)]() mutable {
sol::state lua;
lua.open_libraries(sol::lib::base,
sol::lib::os,
sol::lib::string,
sol::lib::coroutine,
sol::lib::package,
sol::lib::table,
sol::lib::math,
sol::lib::utf8,
sol::lib::io);
LuaHookContext ctx{
cancelFlag,
deadlineFromTimeout(timeoutSeconds)
};
lua_State* L = lua.lua_state();
*reinterpret_cast<LuaHookContext**>(lua_getextraspace(L)) = &ctx;
lua_sethook(L, luaCancelHook, LUA_MASKCOUNT, hookInstructionCount);
try {
std::cout << "Running Lua script: " << scriptPath.string() << std::endl;
lua.script_file(scriptPath.string());
} catch (const sol::error& e) {
std::cerr << "Lua error: " << e.what() << std::endl;
}
{
std::lock_guard<std::mutex> lock(state->tasksMutex);
state->tasks.erase(id);
}
if (onComplete) {
onComplete();
}
}).detach();
std::cout << "Lua-Skript wurde gestartet." << std::endl;
}
void LuaRunner::cancel(int id) {
auto state = m_state;
std::lock_guard<std::mutex> lock(state->tasksMutex);
auto it = state->tasks.find(id);
if (it != state->tasks.end() && it->second.cancel) {
it->second.cancel->store(true, std::memory_order_relaxed);
}
}

View File

@@ -114,9 +114,10 @@ void optionWindow::configureWindow() {
int windowHeight = static_cast<int>(screenHeight * (700.0 / 1080.0));
m_webview.parent().set_size({windowWidth, windowHeight});
const saucer::color bg{0x1e, 0x1e, 0x2e, 0xFF};
const saucer::color bg{0x0b, 0x1c, 0x2c, 0x00};
m_webview.set_background(bg);
m_webview.parent().set_background(bg);
m_webview.parent().set_decorations(saucer::window::decoration::none);
int xPos = (screenWidth - windowWidth) / 2;
int yPos = (screenHeight - windowHeight) / 2;
@@ -135,14 +136,14 @@ void optionWindow::loadResources() {
return;
}
auto it = resources.find("/html/einstellungen.html");
auto it = resources.find("/html/einstellungen_custom.html");
if (it == resources.end()) {
std::cerr << "Fehler: 'einstellungen.html' wurde nicht gefunden!" << std::endl;
std::cerr << "Fehler: 'einstellungen_custom.html' wurde nicht gefunden!" << std::endl;
return;
}
m_webview.embed(resources);
// Explicit URL to avoid accidental fallback to main index
m_webview.set_url("saucer://embedded/html/einstellungen.html");
m_webview.set_url("saucer://embedded/html/einstellungen_custom.html");
m_webview.set_context_menu(false);
if (!resources.contains("/ico/toolbox.png")) {

69
src/resourceLoader.cpp Normal file
View File

@@ -0,0 +1,69 @@
#include "resourceLoader.h"
#include <iostream>
#include <saucer/embedded/all.hpp>
namespace {
std::string normalizeEmbeddedPath(const std::string& path) {
if (!path.empty() && path[0] != '/') {
return "/" + path;
}
return path;
}
bool applyToolboxIcon(saucer::smartview& webview, const decltype(saucer::embedded::all())& resources) {
const auto iconIt = resources.find("/ico/toolbox.png");
if (iconIt == resources.end()) {
std::cerr << "Fehler: 'toolbox.png' wurde nicht gefunden!" << std::endl;
return false;
}
if (auto windowIcon = saucer::icon::from(iconIt->second.content); windowIcon.has_value()) {
webview.parent().set_icon(windowIcon.value());
return true;
}
std::cerr << "Fehler: 'toolbox.png' konnte nicht als Fenster-Icon geladen werden!" << std::endl;
return false;
}
} // namespace
bool ResourceLoader::loadToolboxShell(saucer::smartview& webview) {
auto resources = saucer::embedded::all();
if (!resources.contains("/html/card.html")) {
std::cerr << "Fehler: 'card.html' wurde nicht gefunden!" << std::endl;
return false;
}
if (!resources.contains("/html/index.html")) {
std::cerr << "Fehler: 'index.html' wurde nicht gefunden!" << std::endl;
return false;
}
webview.embed(resources);
webview.set_url("saucer://embedded/html/index.html");
webview.set_context_menu(false);
applyToolboxIcon(webview, resources);
webview.parent().set_title("Toolbox");
saucer::webview::register_scheme("Toolbox");
return true;
}
bool ResourceLoader::serveHtml(saucer::smartview& webview, const std::string& htmlPath, bool setDecorations) {
auto resources = saucer::embedded::all();
const std::string key = normalizeEmbeddedPath(htmlPath);
if (!resources.contains(key)) {
std::cerr << "Fehler: '" << key << "' wurde nicht gefunden!" << std::endl;
return false;
}
webview.serve(key);
webview.parent().set_decorations(setDecorations ? saucer::window::decoration::full
: saucer::window::decoration::none);
return true;
}

88
src/toolRegistry.cpp Normal file
View File

@@ -0,0 +1,88 @@
#include "toolRegistry.h"
#include <cppcodec/base64_rfc4648.hpp>
#include <fstream>
#include <iostream>
std::filesystem::path ToolRegistry::toolboxDirectory(const std::filesystem::path& userPath) {
return userPath / ".Toolbox";
}
bool ToolRegistry::ensureToolboxDirectory(const std::filesystem::path& userPath) {
const std::filesystem::path toolboxPath = toolboxDirectory(userPath);
if (std::filesystem::exists(toolboxPath)) {
return true;
}
try {
if (std::filesystem::create_directory(toolboxPath)) {
std::cout << ".Toolbox Verzeichnis wurde erstellt." << std::endl;
return true;
}
std::cerr << "Fehler beim Erstellen des .Toolbox Verzeichnisses." << std::endl;
return false;
} catch (const std::filesystem::filesystem_error& e) {
std::cerr << "Filesystem-Fehler: " << e.what() << std::endl;
return false;
}
}
std::vector<ToolboxItem> ToolRegistry::loadTools(const std::filesystem::path& userPath) {
std::vector<ToolboxItem> items;
const std::filesystem::path toolboxDir = toolboxDirectory(userPath);
if (!std::filesystem::exists(toolboxDir) || !std::filesystem::is_directory(toolboxDir)) {
std::cerr << "Fehler: Das Verzeichnis .Toolbox existiert nicht." << std::endl;
return items;
}
int id = 0;
for (const auto& entry : std::filesystem::directory_iterator(toolboxDir)) {
if (!entry.is_directory()) {
continue;
}
ToolboxItem item;
item.id = std::to_string(id++);
item.name = entry.path().filename().string();
item.path = entry.path().string();
encodeIcon(entry.path() / "icon.png", item);
const std::filesystem::path descriptionFile = entry.path() / "description.txt";
if (std::filesystem::exists(descriptionFile)) {
std::ifstream descFile(descriptionFile);
item.description.assign(std::istreambuf_iterator<char>(descFile), std::istreambuf_iterator<char>());
}
const std::filesystem::path luaScript = entry.path() / "start.lua";
if (std::filesystem::exists(luaScript)) {
item.lua_script_path_string = luaScript.string();
}
items.push_back(std::move(item));
}
return items;
}
void ToolRegistry::encodeIcon(const std::filesystem::path& imagePath, ToolboxItem& item) {
std::ifstream file(imagePath, std::ios::binary | std::ios::ate);
if (!file) {
std::cerr << "Fehler beim Oeffnen des PNG-Bildes: " << imagePath << std::endl;
return;
}
const std::streamsize fileSize = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<unsigned char> fileData(fileSize);
if (!file.read(reinterpret_cast<char*>(fileData.data()), fileSize)) {
std::cerr << "Fehler beim Lesen des PNG-Bildes: " << imagePath << std::endl;
return;
}
item.icon = "data:image/png;base64," + cppcodec::base64_rfc4648::encode(fileData);
}

59
src/toolboxJsBridge.cpp Normal file
View File

@@ -0,0 +1,59 @@
#include "toolboxJsBridge.h"
void ToolboxJsBridge::registerBindings(saucer::smartview& webview, Bindings bindings) {
webview.expose("getToolboxItems", [bindings]() -> std::string {
return bindings.getToolboxItems();
});
webview.expose("run_lua", [bindings](int id) {
bindings.runLua(id);
});
webview.expose("cancel_lua", [bindings](int id) {
bindings.cancelLua(id);
});
webview.expose("debug_print", [bindings]() {
bindings.debugPrint();
});
webview.expose("open_file_system", [bindings]() {
bindings.openFileSystem();
});
webview.expose("open_cmd", [bindings]() {
bindings.openCommandPrompt();
});
webview.expose("openEinst", [bindings](int id) {
bindings.openSettings(id);
});
webview.expose("openIndex", [bindings]() {
bindings.openIndex();
});
webview.expose("getNewContent", [bindings]() {
bindings.getNewContent();
});
webview.expose("getLuaTimeoutSeconds", [bindings]() -> int {
return bindings.getLuaTimeoutSeconds();
});
webview.expose("setLuaTimeoutSeconds", [bindings](int seconds) {
bindings.setLuaTimeoutSeconds(seconds);
});
webview.expose("getActiveToolName", [bindings]() -> std::string {
return bindings.getActiveToolName();
});
webview.expose("getToolConfigIni", [bindings]() -> std::string {
return bindings.getToolConfigIni();
});
webview.expose("setToolConfigIni", [bindings](const std::string& content) {
bindings.setToolConfigIni(content);
});
}

View File

@@ -13,16 +13,10 @@
#endif
#include <algorithm>
#include <nlohmann/json.hpp>
#include <cppcodec/base64_rfc4648.hpp>
#include <saucer/embedded/all.hpp>
#include <saucer/navigation.hpp>
#include <saucer/app.hpp>
#include <format>
#include <chrono>
extern "C" {
#include <lua.h>
#include <lauxlib.h>
}
#include <fstream>
#include <sstream>
#include <unordered_map>
@@ -112,121 +106,25 @@ void ToolboxWindow::setupToolboxWindow() {
}
void ToolboxWindow::startThreadMonitor() {
m_threadMonitor = std::thread([&]() {
m_threadMonitor = std::thread([this]() {
monitor_focus();
});
m_threadMonitor.detach();
}
bool ToolboxWindow::serveHtmlFile(const std::string& htmlPath, bool setDecorations) {
auto resources = saucer::embedded::all();
const std::string key = (!htmlPath.empty() && htmlPath[0] != '/') ? "/" + htmlPath : htmlPath;
if (!resources.contains(key)) {
std::cerr << "Fehler: '" << key << "' wurde nicht gefunden!" << std::endl;
return false;
}
m_webview.serve(key);
m_webview.parent().set_decorations(setDecorations ? saucer::window::decoration::full : saucer::window::decoration::none);
return true;
return ResourceLoader::serveHtml(m_webview, htmlPath, setDecorations);
}
void ToolboxWindow::configureWindow() {
// DPI-Awareness berücksichtigen
SetProcessDPIAware(); // Macht die Anwendung DPI-bewusst
// Aktuelle DPI-Einstellungen ermitteln
HDC hdc = GetDC(NULL);
int dpiX = GetDeviceCaps(hdc, LOGPIXELSX);
int dpiY = GetDeviceCaps(hdc, LOGPIXELSY);
ReleaseDC(NULL, hdc);
// Skalierungsfaktor berechnen (96 DPI = 100% Skalierung)
const double scaleX = static_cast<double>(dpiX) / 96.0;
const double scaleY = static_cast<double>(dpiY) / 96.0;
// Physische Bildschirmauflösung
const int screenWidth = GetSystemMetrics(SM_CXSCREEN);
const int screenHeight = GetSystemMetrics(SM_CYSCREEN);
// Logische Auflösung (was der Benutzer sieht)
const int logicalScreenWidth = static_cast<int>(screenWidth / scaleX);
const int logicalScreenHeight = static_cast<int>(screenHeight / scaleY);
const int windowWidth = 420;
const int windowHeight = 720;
m_webview.parent().set_size({windowWidth, windowHeight});
const saucer::color bg{0x0b, 0x1c, 0x2c, 0x00};
m_webview.set_background(bg);
m_webview.parent().set_background(bg);
// Arbeitsbereich mit DPI-Skalierung berücksichtigen
RECT workArea;
SystemParametersInfo(SPI_GETWORKAREA, 0, &workArea, 0);
// Arbeitsbereich in logische Koordinaten umrechnen
const int logicalWorkWidth = static_cast<int>((workArea.right - workArea.left) / scaleX);
const int logicalWorkHeight = static_cast<int>((workArea.bottom - workArea.top) / scaleY);
const int rightMargin = 0;
const int bottomMargin = 0;
// Position in logischen Koordinaten berechnen
int xPos = logicalWorkWidth - rightMargin - windowWidth;
int yPos = logicalWorkHeight - bottomMargin - windowHeight;
// Boundary checks
if (xPos < 0) xPos = 10;
if (yPos < 0) yPos = 10;
// Zurück in physische Koordinaten für SetWindowPos
const int physicalX = static_cast<int>(xPos * scaleX);
const int physicalY = static_cast<int>(yPos * scaleY);
moveWindowTo(physicalX, physicalY);
WindowPlacement::configureToolbox(m_webview, m_hwnd);
}
void ToolboxWindow::moveWindowTo(int x, int y) {
if (const HWND hwnd = m_hwnd) {
SetWindowPos(hwnd, nullptr, x, y, 0, 0, SWP_NOZORDER | SWP_NOSIZE);
}
WindowPlacement::moveTo(m_hwnd, x, y);
}
void ToolboxWindow::loadResources() {
auto resources = saucer::embedded::all();
if (!resources.contains("/html/card.html")) {
std::cerr << "Fehler: 'card.html' wurde nicht gefunden!" << std::endl;
return;
}
if (const auto it = resources.find("/html/index.html"); it == resources.end()) {
std::cerr << "Fehler: 'index.html' wurde nicht gefunden!" << std::endl;
return;
}
m_webview.embed(resources);
m_webview.set_url("saucer://embedded/html/index.html");
//m_webview.set_url("https://nindo.de");
m_webview.set_context_menu(false);
if (!resources.contains("ico/toolbox.png")) {
std::cerr << "Fehler: 'toolbox.png' wurde nicht gefunden!" << std::endl;
return;
}
const saucer::icon window_icon = saucer::icon::from(resources.find("ico/toolbox.png")->second.content).value();
m_webview.parent().set_icon(window_icon);
m_webview.parent().set_title("Toolbox");
saucer::webview::register_scheme("Toolbox");
}
void ToolboxWindow::runLuaScript() {
sol::state lua;
lua.open_libraries(sol::lib::base);
lua.script("print('Hallo von Lua!')");
ResourceLoader::loadToolboxShell(m_webview);
}
void ToolboxWindow::show() {
@@ -265,58 +163,21 @@ void ToolboxWindow::hide() {
}
void ToolboxWindow::ExposeToJs() {
// Funktion, die JSON-Code an das JavaScript exponiert
m_webview.expose("getToolboxItems", [&]() -> std::string {
return jsonString;
});
m_webview.expose("run_lua", [&](int id) {
run_lua(id);
});
m_webview.expose("cancel_lua", [&](int id) {
cancel_lua(id);
});
m_webview.expose("debug_print", [&]() {
debug_print();
});
m_webview.expose("open_file_system", [&]() {
open_file_system();
});
m_webview.expose("openEinst", [&](int id) {
openEinst(id);
});
m_webview.expose("openIndex", [&]() {
openIndex();
});
m_webview.expose("getNewContent", [&]()
{
getNewContent();
});
m_webview.expose("getLuaTimeoutSeconds", [&]() -> int {
return getLuaTimeoutSeconds();
});
m_webview.expose("setLuaTimeoutSeconds", [&](int seconds) {
setLuaTimeoutSeconds(seconds);
});
m_webview.expose("getActiveToolName", [&]() -> std::string {
return getActiveToolName();
});
m_webview.expose("getToolConfigIni", [&]() -> std::string {
return getToolConfigIni();
});
m_webview.expose("setToolConfigIni", [&](const std::string &content) {
setToolConfigIni(content);
ToolboxJsBridge::registerBindings(m_webview, {
.getToolboxItems = [this]() { return jsonString; },
.runLua = [this](int id) { run_lua(id); },
.cancelLua = [this](int id) { cancel_lua(id); },
.debugPrint = [this]() { debug_print(); },
.openFileSystem = [this]() { open_file_system(); },
.openCommandPrompt = [this]() { open_cmd(); },
.openSettings = [this](int id) { openEinst(id); },
.openIndex = [this]() { openIndex(); },
.getNewContent = [this]() { getNewContent(); },
.getLuaTimeoutSeconds = [this]() { return getLuaTimeoutSeconds(); },
.setLuaTimeoutSeconds = [this](int seconds) { setLuaTimeoutSeconds(seconds); },
.getActiveToolName = [this]() { return getActiveToolName(); },
.getToolConfigIni = [this]() { return getToolConfigIni(); },
.setToolConfigIni = [this](const std::string& content) { setToolConfigIni(content); },
});
}
@@ -344,213 +205,38 @@ void ToolboxWindow::SetJsonItems() {// Konvertierung des Vektors in JSON
}
bool ToolboxWindow::ensureToolboxInUserPath() {
// Bestimme das Benutzerverzeichnis abhängig vom Betriebssystem
std::filesystem::path userPath;
#ifdef _WIN32
userPath = std::getenv("USERPROFILE"); // Windows
#else
userPath = std::getenv("HOME"); // Unix/Linux/MacOS
#endif
// Setze den Pfad zum .Toolbox-Verzeichnis
// Überprüfe, ob der Pfad existiert
if (const std::filesystem::path toolboxPath = userPath / ".Toolbox"; !std::filesystem::exists(toolboxPath)) {
try {
// Versuche, das Verzeichnis zu erstellen
if (std::filesystem::create_directory(toolboxPath)) {
std::cout << ".Toolbox Verzeichnis wurde erstellt." << std::endl;
return true;
} else {
std::cerr << "Fehler beim Erstellen des .Toolbox Verzeichnisses." << std::endl;
return false;
}
} catch (const std::filesystem::filesystem_error& e) {
std::cerr << "Filesystem-Fehler: " << e.what() << std::endl;
return false;
}
}
std::cout << "Das Verzeichnis .Toolbox existiert bereits." << std::endl;
return true;
return ToolRegistry::ensureToolboxDirectory(userPath);
}
void encodeImageToBase64(const std::filesystem::path& imagePath, ToolboxWindow::ToolboxItem& item) {
std::ifstream file(imagePath, std::ios::binary | std::ios::ate);
if (!file) {
std::cerr << "Fehler beim Öffnen des PNG-Bildes: " << imagePath << std::endl;
return;
}
const std::streamsize fileSize = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<unsigned char> fileData(fileSize);
if (!file.read(reinterpret_cast<char*>(fileData.data()), fileSize)) {
std::cerr << "Fehler beim Lesen des PNG-Bildes: " << imagePath << std::endl;
return;
}
// Kodieren der Binärdaten mit cppcodec zu Base64
const std::string base64Encoded = cppcodec::base64_rfc4648::encode(fileData);
item.icon = "data:image/png;base64," + base64Encoded;
}
void ToolboxWindow::fillToolboxMenu() {
m_toolboxItems.clear();
std::filesystem::path toolboxDir = userPath / ".Toolbox";
if (!std::filesystem::exists(toolboxDir) || !std::filesystem::is_directory(toolboxDir)) {
std::cerr << "Fehler: Das Verzeichnis .Toolbox existiert nicht." << std::endl;
return;
}
int id = 0;
for (const auto& entry : std::filesystem::directory_iterator(toolboxDir)) {
if (!entry.is_directory()) continue;
ToolboxItem item;
item.id = std::to_string(id++);
item.name = entry.path().filename().string();
item.path = entry.path().string();
// Encode Image
encodeImageToBase64(entry.path() / "icon.png", item);
// Lese Beschreibung von description.txt
std::filesystem::path descriptionFile = entry.path() / "description.txt";
if (std::filesystem::exists(descriptionFile)) {
std::ifstream descFile(descriptionFile);
item.description.assign((std::istreambuf_iterator<char>(descFile)),
std::istreambuf_iterator<char>());
}
// Lese Beschreibung von description.txt
std::filesystem::path luaScript = entry.path() / "start.lua";
if (std::filesystem::exists(luaScript)) {
item.lua_script_path_string = luaScript.string();
}
m_toolboxItems.push_back(item);
}
m_toolboxItems = ToolRegistry::loadTools(userPath);
}
void ToolboxWindow::run_lua(int id) {
/*// Öffnet ein CMD-Fenster, falls noch keins offen ist
static bool console_opened = false;
if (!console_opened) {
AllocConsole();
freopen("CONOUT$", "w", stdout); // stdout auf die Konsole umleiten
freopen("CONOUT$", "w", stderr); // stderr auf die Konsole umleiten
console_opened = true;
}*/
constexpr int kHookInstructionCount = 10000;
const int timeoutSeconds = getLuaTimeoutSecondsForId(id);
const auto deadline = (timeoutSeconds > 0)
? (std::chrono::steady_clock::now() + std::chrono::seconds(timeoutSeconds))
: (std::chrono::steady_clock::time_point::max)();
auto cancelFlag = std::make_shared<std::atomic_bool>(false);
{
std::lock_guard<std::mutex> lock(m_luaTasksMutex);
m_luaTasks[id] = LuaTask{cancelFlag};
if (id < 0 || id >= static_cast<int>(m_toolboxItems.size())) {
return;
}
// Lambda-Funktion, um das Lua-Skript auszuführen
auto lua_task = [this, id, cancelFlag, deadline]() {
sol::state lua;
lua.open_libraries(sol::lib::base,
sol::lib::os,
sol::lib::string,
sol::lib::coroutine,
sol::lib::package,
sol::lib::table,
sol::lib::math,
sol::lib::utf8,
sol::lib::io);
struct LuaHookContext {
std::shared_ptr<std::atomic_bool> cancel;
std::chrono::steady_clock::time_point deadline;
};
LuaHookContext ctx{
cancelFlag,
deadline
};
lua_State* L = lua.lua_state();
*reinterpret_cast<LuaHookContext**>(lua_getextraspace(L)) = &ctx;
lua_sethook(
L,
[](lua_State* hookState, lua_Debug*) {
auto* hookCtx = *reinterpret_cast<LuaHookContext**>(lua_getextraspace(hookState));
if (!hookCtx) {
return;
}
if (hookCtx->cancel->load(std::memory_order_relaxed)) {
luaL_error(hookState, "Lua script cancelled");
}
if (hookCtx->deadline != (std::chrono::steady_clock::time_point::max)() &&
std::chrono::steady_clock::now() > hookCtx->deadline) {
luaL_error(hookState, "Lua script cancelled or timed out");
}
},
LUA_MASKCOUNT,
kHookInstructionCount
);
try {
std::string scriptPath = m_toolboxItems[id].lua_script_path_string;
std::cout << "Running Lua script: " << scriptPath << std::endl;
// Lua-Skript ausführen
lua.script_file(scriptPath);
// Nach erfolgreichem Ausführen UI aktualisieren
m_webview.saucer::webview::execute(std::format("enableButtonByToolId({})", id));
m_webview.reload();
} catch (const sol::error& e) {
// Lua-Fehler in der Konsole ausgeben
std::cerr << "Lua error: " << e.what() << std::endl;
// Nach Fehler UI aktualisieren
m_webview.saucer::webview::execute(std::format("enableButtonByToolId({})", id));
m_webview.reload();
}
{
std::lock_guard<std::mutex> lock(m_luaTasksMutex);
m_luaTasks.erase(id);
}
};
// Erstellen und starten des Threads
std::thread lua_thread(lua_task);
// Thread lösen, um die Anwendung nicht zu blockieren
lua_thread.detach();
std::cout << "Lua-Skript wurde gestartet." << std::endl;
const std::filesystem::path scriptPath = m_toolboxItems[id].lua_script_path_string;
m_luaRunner.run(id, scriptPath, getLuaTimeoutSecondsForId(id), [this, id]() {
m_webview.saucer::webview::execute(std::format("enableButtonByToolId({})", id));
m_webview.reload();
});
}
void ToolboxWindow::cancel_lua(int id) {
std::lock_guard<std::mutex> lock(m_luaTasksMutex);
auto it = m_luaTasks.find(id);
if (it != m_luaTasks.end() && it->second.cancel) {
it->second.cancel->store(true, std::memory_order_relaxed);
}
m_luaRunner.cancel(id);
}
void ToolboxWindow::debug_print() {
std::cout << "ToolboxWindow::debug_print()" << std::endl;
}
ToolboxWindow::~ToolboxWindow() {
m_runningMonitor = false;
if (m_threadMonitor.joinable()) {
m_threadMonitor.join();
}
}
@@ -598,6 +284,23 @@ void ToolboxWindow::open_file_system() {
m_webview.saucer::webview::execute("reloadContent()");
}
void ToolboxWindow::open_cmd() {
std::string command;
#ifdef _WIN32
command = "cmd.exe";
ShellExecute(NULL, L"open", L"cmd.exe", NULL, NULL, SW_SHOWNORMAL);
#elif __APPLE__
command = "open -a Terminal";
std::system(command.c_str());
#elif __linux__
command = "x-terminal-emulator";
std::system(command.c_str());
#else
std::cerr << "Fehler: Plattform nicht unterstützt." << std::endl;
#endif
}
// Beispielhafte Nutzung:
void ToolboxWindow::openEinst() {
@@ -627,46 +330,17 @@ void ToolboxWindow::openEinst(int id) {
}
void ToolboxWindow::centerWindow() {
const int screenWidth = GetSystemMetrics(SM_CXSCREEN);
const int screenHeight = GetSystemMetrics(SM_CYSCREEN);
// Aktuelle Fenstergröße abrufen
int windowWidth = 0;
int windowHeight = 0;
if (const HWND hwnd = m_hwnd) {
RECT rect;
if (GetWindowRect(hwnd, &rect)) {
windowWidth = rect.right - rect.left;
windowHeight = rect.bottom - rect.top;
}
}
// Berechnung der zentralen Position
const int xPos = (screenWidth - windowWidth) / 2;
const int yPos = (screenHeight - windowHeight) / 2;
// Bewege das Fenster an die zentrale Position
moveWindowTo(xPos, yPos);
WindowPlacement::center(m_hwnd);
}
void ToolboxWindow::resizeWindow(int width, int height)
{
// Stellen Sie die Größe des Fensters ein
m_webview.parent().set_size({width, height});
WindowPlacement::resize(m_webview, width, height);
}
void ToolboxWindow::resizeWindow(double widthPercent, double heightPercent)
{
const int screenWidth = GetSystemMetrics(SM_CXSCREEN);
const int screenHeight = GetSystemMetrics(SM_CYSCREEN);
// Berechnung der Fenstergröße basierend auf Prozentwerten
const int width = static_cast<int>(screenWidth * (widthPercent / 100.0));
const int height = static_cast<int>(screenHeight * (heightPercent / 100.0));
// Stellen Sie die Größe des Fensters ein
m_webview.parent().set_size({width, height});
WindowPlacement::resizePercent(m_webview, widthPercent, heightPercent);
}
void ToolboxWindow::openIndex() {

241
src/trayController.cpp Normal file
View File

@@ -0,0 +1,241 @@
#include "trayController.h"
#include <Windows.h>
#include <algorithm>
#include <array>
#include <atomic>
#include <cstdio>
#include <thread>
#include "resource.h"
#include "toolboxWindow.h"
#include "toolbox_win/icon_utils.h"
#include "tray.h"
import toolboxTray;
namespace {
constexpr DWORD minExplorerRestartDelayMs = 500;
constexpr int maxTrayReinitAttempts = 5;
constexpr int maxRetryDelayMs = 5000;
struct TrayState {
bool reinitScheduled = false;
int reinitAttempts = 0;
DWORD lastExplorerRestartTime = 0;
};
void onQuit(tray_menu*) {
tray_exit();
}
void openCommandPrompt(tray_menu*) {
ShellExecuteW(nullptr, L"open", L"cmd.exe", nullptr, nullptr, SW_SHOWNORMAL);
}
bool isExplorerReady() {
const HWND taskbar = FindWindowW(L"Shell_TrayWnd", nullptr);
return taskbar && IsWindow(taskbar) && IsWindowVisible(taskbar) && GetDesktopWindow();
}
bool isTaskbarWindow(HWND hwnd) {
const HWND taskbar = FindWindowW(L"Shell_TrayWnd", nullptr);
return taskbar && hwnd == taskbar;
}
int retryDelayMs(int attempt) {
const int delay = 200 * (1 << std::max(0, attempt - 1));
return std::min(delay, maxRetryDelayMs);
}
} // namespace
struct TrayController::Impl {
explicit Impl(ToolboxWindow& toolboxWindow)
: window(toolboxWindow),
menuItems{{
{"CMD", 0, 0, 0, openCommandPrompt, nullptr},
{"-", 0, 0, 0, nullptr, nullptr},
{"Oeffnen", 0, 0, 0, &Impl::toggleToolboxWindow, nullptr},
{"-", 0, 0, 0, nullptr, nullptr},
{"Beenden", 0, 0, 0, onQuit, nullptr},
{nullptr, 0, 0, 0, nullptr, nullptr},
}} {}
bool initialize() {
activeController.store(this);
const HICON icon = toolbox_load_small_icon(GetModuleHandleW(nullptr), IDI_ICON1);
trayIcon = std::make_unique<toolboxTray>("toolbox.ico", "Toolbox");
trayIcon->icon_handle = icon;
trayIcon->icon_is_shared = icon ? 1 : 0;
trayIcon->setLeftClickAction([] { toggleToolboxWindow(nullptr); });
trayIcon->setTrayMenu(menuItems.data());
if (tray_init(trayIcon.get()) != 0) {
std::fprintf(stderr, "Fehler beim Initialisieren des Tray-Icons\n");
return false;
}
installExplorerHook();
return true;
}
void shutdown() {
uninstallExplorerHook();
Impl* expected = this;
activeController.compare_exchange_strong(expected, nullptr);
trayIcon.reset();
}
static Impl* current() {
return activeController.load();
}
static void toggleToolboxWindow(tray_menu*) {
Impl* controller = current();
if (!controller) {
return;
}
ToolboxWindow& window = controller->window;
if (window.m_bShow) {
window.hide();
return;
}
window.show();
}
bool reinitTray() {
if (!trayIcon || state.reinitScheduled) {
return false;
}
++state.reinitAttempts;
if (state.reinitAttempts > maxTrayReinitAttempts) {
std::fprintf(stderr, "Maximale Anzahl an Reinitialisierungsversuchen erreicht\n");
return false;
}
state.reinitScheduled = true;
int result = tray_init(trayIcon.get());
if (result != 0) {
std::fprintf(stderr, "Fehler beim Re-Initialisieren des Tray-Icons (Versuch %d/%d)\n",
state.reinitAttempts, maxTrayReinitAttempts);
Sleep(retryDelayMs(state.reinitAttempts));
result = tray_init(trayIcon.get());
}
if (result == 0) {
std::printf("Tray erfolgreich neu initialisiert\n");
state.reinitAttempts = 0;
}
state.reinitScheduled = false;
return result == 0;
}
void retryTrayReinitAsync() {
std::thread([] {
Sleep(500);
for (int attempt = 0; attempt < 3; ++attempt) {
Impl* controller = current();
if (controller && isExplorerReady() && controller->reinitTray()) {
return;
}
Sleep(500 * (attempt + 1));
}
std::fprintf(stderr, "Tray konnte nach Explorer-Neustart nicht wiederhergestellt werden\n");
}).detach();
}
void scheduleTrayReinit() {
const DWORD now = GetTickCount();
if (now - state.lastExplorerRestartTime < minExplorerRestartDelayMs) {
return;
}
state.lastExplorerRestartTime = now;
Sleep(300);
if (!reinitTray()) {
retryTrayReinitAsync();
}
}
static void CALLBACK handleExplorerEvent(HWINEVENTHOOK, DWORD event, HWND hwnd, LONG idObject, LONG, DWORD,
DWORD) {
Impl* controller = current();
if (!controller || idObject != OBJID_WINDOW || !isTaskbarWindow(hwnd)) {
return;
}
if (event == EVENT_OBJECT_CREATE) {
std::printf("Explorer-Neustart erkannt (Taskbar neu erstellt)\n");
std::printf(isExplorerReady()
? "Explorer ist bereit - Tray wird neu initialisiert\n"
: "Explorer noch nicht bereit - Warten...\n");
controller->scheduleTrayReinit();
return;
}
if (event == EVENT_OBJECT_DESTROY) {
std::printf("Explorer wird heruntergefahren\n");
}
}
void installExplorerHook() {
explorerHook = SetWinEventHook(EVENT_OBJECT_CREATE, EVENT_OBJECT_DESTROY, nullptr, handleExplorerEvent, 0, 0,
WINEVENT_OUTOFCONTEXT);
if (!explorerHook) {
std::fprintf(stderr, "Warnung: Explorer-Neustart-Ueberwachung konnte nicht initialisiert werden\n");
return;
}
std::printf("Explorer-Neustart-Ueberwachung erfolgreich initialisiert\n");
}
void uninstallExplorerHook() {
if (explorerHook) {
UnhookWinEvent(explorerHook);
explorerHook = nullptr;
}
}
ToolboxWindow& window;
std::unique_ptr<toolboxTray> trayIcon;
std::array<tray_menu, 6> menuItems;
TrayState state;
HWINEVENTHOOK explorerHook = nullptr;
static std::atomic<Impl*> activeController;
};
std::atomic<TrayController::Impl*> TrayController::Impl::activeController = nullptr;
TrayController::TrayController(ToolboxWindow& window)
: m_impl(std::make_unique<Impl>(window)) {}
TrayController::~TrayController() {
shutdown();
}
bool TrayController::initialize() {
return m_impl->initialize();
}
void TrayController::shutdown() {
if (m_impl) {
m_impl->shutdown();
}
}

92
src/windowPlacement.cpp Normal file
View File

@@ -0,0 +1,92 @@
#include "windowPlacement.h"
namespace {
constexpr int toolboxWidth = 420;
constexpr int toolboxHeight = 720;
constexpr int fallbackPosition = 10;
struct DpiScale {
double x = 1.0;
double y = 1.0;
};
DpiScale currentDpiScale() {
HDC hdc = GetDC(nullptr);
const int dpiX = GetDeviceCaps(hdc, LOGPIXELSX);
const int dpiY = GetDeviceCaps(hdc, LOGPIXELSY);
ReleaseDC(nullptr, hdc);
return {
static_cast<double>(dpiX) / 96.0,
static_cast<double>(dpiY) / 96.0
};
}
} // namespace
void WindowPlacement::configureToolbox(saucer::smartview& webview, HWND hwnd) {
SetProcessDPIAware();
const DpiScale scale = currentDpiScale();
webview.parent().set_size({toolboxWidth, toolboxHeight});
const saucer::color bg{0x0b, 0x1c, 0x2c, 0x00};
webview.set_background(bg);
webview.parent().set_background(bg);
RECT workArea;
SystemParametersInfo(SPI_GETWORKAREA, 0, &workArea, 0);
const int logicalWorkWidth = static_cast<int>((workArea.right - workArea.left) / scale.x);
const int logicalWorkHeight = static_cast<int>((workArea.bottom - workArea.top) / scale.y);
int xPos = logicalWorkWidth - toolboxWidth;
int yPos = logicalWorkHeight - toolboxHeight;
if (xPos < 0) {
xPos = fallbackPosition;
}
if (yPos < 0) {
yPos = fallbackPosition;
}
moveTo(hwnd, static_cast<int>(xPos * scale.x), static_cast<int>(yPos * scale.y));
}
void WindowPlacement::moveTo(HWND hwnd, int x, int y) {
if (hwnd) {
SetWindowPos(hwnd, nullptr, x, y, 0, 0, SWP_NOZORDER | SWP_NOSIZE);
}
}
void WindowPlacement::center(HWND hwnd) {
const int screenWidth = GetSystemMetrics(SM_CXSCREEN);
const int screenHeight = GetSystemMetrics(SM_CYSCREEN);
int windowWidth = 0;
int windowHeight = 0;
if (hwnd) {
RECT rect;
if (GetWindowRect(hwnd, &rect)) {
windowWidth = rect.right - rect.left;
windowHeight = rect.bottom - rect.top;
}
}
moveTo(hwnd, (screenWidth - windowWidth) / 2, (screenHeight - windowHeight) / 2);
}
void WindowPlacement::resize(saucer::smartview& webview, int width, int height) {
webview.parent().set_size({width, height});
}
void WindowPlacement::resizePercent(saucer::smartview& webview, double widthPercent, double heightPercent) {
const int screenWidth = GetSystemMetrics(SM_CXSCREEN);
const int screenHeight = GetSystemMetrics(SM_CYSCREEN);
resize(webview,
static_cast<int>(screenWidth * (widthPercent / 100.0)),
static_cast<int>(screenHeight * (heightPercent / 100.0)));
}