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
+64 -51
View File
@@ -1,11 +1,18 @@
cmake_minimum_required(VERSION 3.30) cmake_minimum_required(VERSION 3.30)
set(CMAKE_CXX_STANDARD 23) project(Toolbox LANGUAGES C CXX)
set(saucer_no_version_check ON)
set(BUILD_SHARED_LIBS OFF)
option(BUILD_SHARED_LIBS "Build as shared library" OFF)
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/c_tray)
add_subdirectory(lib/json) add_subdirectory(lib/json)
@@ -13,76 +20,79 @@ add_subdirectory(lib/cppcodec)
add_subdirectory(lib/saucer) add_subdirectory(lib/saucer)
add_subdirectory(lib/lua) add_subdirectory(lib/lua)
add_subdirectory(lib/sol2) add_subdirectory(lib/sol2)
add_subdirectory(lib/toolbox_win)
# add_subdirectory(lib/saucer4lua) # add_subdirectory(lib/saucer4lua)
include_directories(inc) file(GLOB_RECURSE APP_SOURCES CONFIGURE_DEPENDS
include_directories(lib/json/include/nlohmann) "src/*.cpp"
#include_directories(out_embed) )
file(GLOB_RECURSE APP_HEADERS CONFIGURE_DEPENDS
"inc/*.h"
"inc/*.hpp"
)
if (WIN32) if (WIN32)
set(RESOURCE_FILES toolbox.rc) list(APPEND APP_SOURCES toolbox.rc)
endif () endif ()
# Header-Datei einbinden add_executable(Toolbox
include_directories(lib/c_tray) main.cpp
${APP_SOURCES}
# Plattformabhängige Einstellungen ${APP_HEADERS}
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}
) )
target_sources(Toolbox target_sources(Toolbox
PUBLIC PUBLIC
FILE_SET cxx_modules TYPE CXX_MODULES 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) 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 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) if (WEBVIEW2_INCLUDE_DIR)
message(STATUS "Using WebView2 includes at: ${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 () else ()
message(WARNING "WebView2 headers not found in build tree. The build may fail when including WebView2.h.") message(WARNING "WebView2 headers not found in build tree. The build may fail when including WebView2.h.")
endif () endif ()
endif () endif ()
saucer_embed("res" TARGET ${PROJECT_NAME}) set(EMBED_DESTINATION "${CMAKE_BINARY_DIR}/embedded")
file(MAKE_DIRECTORY "${EMBED_DESTINATION}/saucer/embedded")
CPMFindPackage( file(GLOB_RECURSE EMBED_RESOURCE_FILES CONFIGURE_DEPENDS
NAME saucer-desktop "${CMAKE_CURRENT_SOURCE_DIR}/res/*"
VERSION 4.0.2
GIT_REPOSITORY "https://github.com/saucer/desktop"
) )
target_link_libraries( foreach (RESOURCE_FILE IN LISTS EMBED_RESOURCE_FILES)
${PROJECT_NAME} 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 PRIVATE
saucer::saucer saucer::saucer
sol2::sol2 sol2::sol2
@@ -90,11 +100,14 @@ target_link_libraries(
tray tray
nlohmann_json nlohmann_json
cppcodec cppcodec
#saucer4lua toolbox_win
saucer::embedded 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")) if (WIN32 AND (CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRel"))
set_target_properties(Toolbox PROPERTIES WIN32_EXECUTABLE TRUE) set_target_properties(Toolbox PROPERTIES WIN32_EXECUTABLE TRUE)
endif () endif ()
+81
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
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
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
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
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);
};
+8 -28
View File
@@ -2,14 +2,15 @@
#include <saucer/smartview.hpp> #include <saucer/smartview.hpp>
#include "modules/hwnd_module.h" #include "modules/hwnd_module.h"
#include <saucer/embedded/all.hpp>
#include <sol/sol.hpp>
#include <saucer/webview.hpp> #include <saucer/webview.hpp>
#include <unordered_map>
#include <mutex>
#include <chrono> #include <chrono>
#include "luaRunner.h"
#include "optionWindow.h" #include "optionWindow.h"
#include "resourceLoader.h"
#include "toolConfig.h" #include "toolConfig.h"
#include "toolboxJsBridge.h"
#include "toolRegistry.h"
#include "windowPlacement.h"
class ToolboxWindow { class ToolboxWindow {
public: public:
@@ -30,20 +31,6 @@ public:
std::atomic_bool isIndex = true; 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; std::vector<ToolboxItem> m_toolboxItems;
saucer::smartview m_webview; saucer::smartview m_webview;
@@ -60,8 +47,6 @@ private:
std::string jsonString; std::string jsonString;
int size_x,size_y;
#ifdef _WIN32 #ifdef _WIN32
std::filesystem::path userPath = std::getenv("USERPROFILE"); // Windows std::filesystem::path userPath = std::getenv("USERPROFILE"); // Windows
#else #else
@@ -75,7 +60,6 @@ private:
void configureWindow(); void configureWindow();
void moveWindowTo(int x, int y); void moveWindowTo(int x, int y);
void loadResources(); void loadResources();
void runLuaScript();
void ExposeToJs(); void ExposeToJs();
@@ -92,6 +76,8 @@ private:
void open_file_system(); void open_file_system();
void open_cmd();
void SetJsonItems(); void SetJsonItems();
void openEinst(); void openEinst();
@@ -107,14 +93,8 @@ private:
void setLuaTimeoutSeconds(int seconds); void setLuaTimeoutSeconds(int seconds);
std::string getActiveToolName(); 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; int m_activeToolId = -1;
LuaRunner m_luaRunner;
ToolConfigStore m_configStore; ToolConfigStore m_configStore;
std::filesystem::path getToolConfigPath(int id) const; std::filesystem::path getToolConfigPath(int id) const;
+21
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
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);
};
+18
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 ()
@@ -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);
}
+26
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
}
+13 -64
View File
@@ -1,82 +1,31 @@
#include <saucer/app.hpp>
#include <Windows.h> #include <Windows.h>
#include <saucer/app.hpp>
#include <memory>
#include "toolboxWindow.h" #include "toolboxWindow.h"
#include "resource.h" #include "trayController.h"
#include "tray.h"
import toolboxTray;
ToolboxWindow* g_toolboxWindow = nullptr;
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();
}
}
}
coco::stray start(saucer::application* app) { coco::stray start(saucer::application* app) {
// Toolbox-Fenster erstellen und initialisieren std::shared_ptr<saucer::application> appShared(app, [](saucer::application*) {});
std::shared_ptr<saucer::application> app_shared(app, [](saucer::application*){});
auto toolboxWindow = std::make_unique<ToolboxWindow>(app_shared);
toolboxWindow->initialize();
g_toolboxWindow = toolboxWindow.get(); auto window = std::make_unique<ToolboxWindow>(appShared);
window->initialize();
HICON hIcon = static_cast<HICON>(LoadImage( TrayController tray(*window);
GetModuleHandle(NULL), if (!tray.initialize()) {
MAKEINTRESOURCE(IDI_ICON1), co_return;
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(); co_await app->finish();
} }
#ifdef _DEBUG #ifdef _DEBUG
int main(int argc, char** argv) { int main(int, char**) {
return saucer::application::create({.id = "toolbox"})->run(start); return saucer::application::create({.id = "toolbox"})->run(start);
} }
#else #else
#include <Windows.h> int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) {
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
return saucer::application::create({.id = "toolbox"})->run(start); return saucer::application::create({.id = "toolbox"})->run(start);
} }
#endif #endif
+56 -152
View File
@@ -4,147 +4,10 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Toolbox</title> <title>Toolbox</title>
<style> <link rel="stylesheet" href="saucer://embedded/html/styles.css">
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>
</head> </head>
<body> <body class="settings-page">
<div id="shell">
<!-- Sidebar für Optionen --> <!-- Sidebar für Optionen -->
<div class="sidebar"> <div class="sidebar">
<h2>Einstellungen</h2> <h2>Einstellungen</h2>
@@ -173,19 +36,18 @@
</div> </div>
<div class="general" id="general"> <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> <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> <label for="lua-timeout">Lua Timeout (Sekunden)</label>
<input id="lua-timeout" type="number" min="0" step="1" value="30" <input id="lua-timeout" class="settings-input" 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 class="settings-help">
<div style="margin-top: 6px; font-size: 12px; color: #aaa;">
0 = kein Timeout 0 = kein Timeout
</div> </div>
<button id="save-settings" class="back-button" style="margin-top: 16px;">Speichern</button> <button id="save-settings" class="back-button settings-save">Speichern</button>
<div id="save-status" style="margin-top: 8px; font-size: 12px; color: #aaa;"></div> <div id="save-status" class="settings-status"></div>
</div> </div>
</div> </div>
@@ -195,6 +57,7 @@
<!--Schmidti.Digital &copy; 2024--> <!--Schmidti.Digital &copy; 2024-->
</div> </div>
</div> </div>
</div>
<script> <script>
function goBack() { function goBack() {
saucer.exposed.openIndex(); saucer.exposed.openIndex();
@@ -221,13 +84,50 @@
const output = document.getElementById('highlightedOutput'); const output = document.getElementById('highlightedOutput');
showContent('general'); 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 // Synchronisiere den Inhalt von textarea mit dem Codeblock
editor.addEventListener('input', () => { editor.addEventListener('input', () => {
output.innerHTML = ''; // Vorheriges Highlighting entfernen output.innerHTML = highlightLua(editor.value);
output.textContent = editor.value; // Aktuellen Text setzen
if (window.hljs && typeof hljs.highlightAll === 'function') {
hljs.highlightAll();
}
}); });
const toolName = document.getElementById('tool-name'); const toolName = document.getElementById('tool-name');
@@ -259,3 +159,7 @@
</body> </body>
</html> </html>
+175
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>
+36 -4
View File
@@ -10,13 +10,23 @@
<body> <body>
<div id="shell"> <div id="shell">
<div class="header"> <div class="header">
<button onclick="getNewContent()" class="button" id="reload-button"></button> <div class="header-cluster header-left">
<button onclick="openEinstFirst()" class="button" id="option-button" style="display: none;">⚙️</button> <button onclick="getNewContent()" class="button icon-button" id="reload-button">&#x21bb;</button>
</div>
<h1>Toolbox</h1> <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>
<div class="main" id="toolbox"> <div class="main" id="toolbox">
<!-- Tool Cards werden hier dynamisch geladen --> <!-- Tool Cards werden hier dynamisch geladen -->
<div class="add-tool-card" onclick="openAddTool()">
<span class="plus-icon">+</span>
</div>
</div> </div>
<div class="footer"> <div class="footer">
<!--Schmidti.Digital &copy; 2024--> <!--Schmidti.Digital &copy; 2024-->
@@ -64,10 +74,19 @@
saucer.exposed.open_file_system(); saucer.exposed.open_file_system();
} }
function openCmd() {
saucer.exposed.open_cmd();
}
function openAddTool() {
openFileSystem();
}
function reloadContent() { function reloadContent() {
const toolbox = document.getElementById('toolbox'); 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.innerHTML = '';
// Toolbox-Daten neu abrufen // Toolbox-Daten neu abrufen
@@ -97,6 +116,11 @@
toolbox.innerHTML += toolCardHTML; toolbox.innerHTML += toolCardHTML;
}); });
// Das add-tool-card Element wieder hinzufügen
if (addToolCard) {
toolbox.appendChild(addToolCard);
}
// Überprüfen und Wiederherstellen des Zustands der Buttons // Überprüfen und Wiederherstellen des Zustands der Buttons
document.querySelectorAll('.tool-card').forEach(card => { document.querySelectorAll('.tool-card').forEach(card => {
const toolId = card.getAttribute('data-tool-id'); const toolId = card.getAttribute('data-tool-id');
@@ -153,6 +177,9 @@
const toolbox = document.getElementById('toolbox'); 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 => { saucer.exposed.getToolboxItems().then(jsonString => {
const m_toolboxItems = JSON.parse(jsonString); const m_toolboxItems = JSON.parse(jsonString);
@@ -179,6 +206,11 @@
toolbox.innerHTML += toolCardHTML; 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 // Überprüfen und Wiederherstellen des Zustands beim Laden der Seite
document.querySelectorAll('.tool-card').forEach(card => { document.querySelectorAll('.tool-card').forEach(card => {
const toolId = card.getAttribute('data-tool-id'); const toolId = card.getAttribute('data-tool-id');
+340 -14
View File
@@ -29,6 +29,7 @@
height: 100vh; height: 100vh;
} }
#shell { #shell {
position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100vh; height: 100vh;
@@ -38,7 +39,11 @@
} }
.header { .header {
background: linear-gradient(180deg, rgba(20, 45, 70, 0.85), rgba(12, 28, 45, 0.7)); 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; text-align: center;
box-shadow: 0 10px 22px var(--shadow), inset 0 1px 0 var(--shine); box-shadow: 0 10px 22px var(--shadow), inset 0 1px 0 var(--shine);
border-bottom: 1px solid rgba(140, 210, 255, 0.2); border-bottom: 1px solid rgba(140, 210, 255, 0.2);
@@ -57,8 +62,6 @@
pointer-events: none; pointer-events: none;
} }
.button { .button {
position: absolute;
transform: translateY(-50%);
background: linear-gradient(180deg, rgba(120, 210, 255, 0.35), rgba(60, 150, 220, 0.55)); 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); border: 1px solid rgba(140, 210, 255, 0.45);
color: var(--ink); color: var(--ink);
@@ -71,27 +74,50 @@
} }
.button:hover { .button:hover {
background: linear-gradient(180deg, rgba(150, 225, 255, 0.45), rgba(80, 170, 235, 0.7)); 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); box-shadow: 0 10px 18px var(--shadow), inset 0 1px 0 var(--shine);
} }
.button:active { .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); box-shadow: 0 4px 10px var(--shadow), inset 0 2px 6px rgba(0,0,0,0.25);
} }
#file-system-button { .header-cluster {
top: 50%; display: flex;
right: 18px; align-items: center;
gap: 8px;
min-width: 120px;
} }
#reload-button { .header-left {
top: 50%; justify-content: flex-start;
left: 18px;
} }
#option-button { .header-right {
top: 50%; justify-content: flex-end;
right: 92px; }
.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 { .header h1 {
margin: 0; margin: 0;
flex: 1;
text-align: center;
font-size: 19px; font-size: 19px;
letter-spacing: 0.5px; letter-spacing: 0.5px;
color: var(--ink-soft); color: var(--ink-soft);
@@ -187,6 +213,36 @@
color: #3b2a00; color: #3b2a00;
cursor: progress; 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 { .footer {
background: linear-gradient(180deg, rgba(18, 40, 60, 0.65), rgba(8, 18, 28, 0.6)); background: linear-gradient(180deg, rgba(18, 40, 60, 0.65), rgba(8, 18, 28, 0.6));
padding: 10px 20px; padding: 10px 20px;
@@ -195,3 +251,273 @@
color: #9bbfd6; color: #9bbfd6;
border-top: 1px solid rgba(140, 210, 255, 0.2); 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
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);
}
}
+5 -4
View File
@@ -114,9 +114,10 @@ void optionWindow::configureWindow() {
int windowHeight = static_cast<int>(screenHeight * (700.0 / 1080.0)); int windowHeight = static_cast<int>(screenHeight * (700.0 / 1080.0));
m_webview.parent().set_size({windowWidth, windowHeight}); 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.set_background(bg);
m_webview.parent().set_background(bg); m_webview.parent().set_background(bg);
m_webview.parent().set_decorations(saucer::window::decoration::none);
int xPos = (screenWidth - windowWidth) / 2; int xPos = (screenWidth - windowWidth) / 2;
int yPos = (screenHeight - windowHeight) / 2; int yPos = (screenHeight - windowHeight) / 2;
@@ -135,14 +136,14 @@ void optionWindow::loadResources() {
return; return;
} }
auto it = resources.find("/html/einstellungen.html"); auto it = resources.find("/html/einstellungen_custom.html");
if (it == resources.end()) { 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; return;
} }
m_webview.embed(resources); m_webview.embed(resources);
// Explicit URL to avoid accidental fallback to main index // 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); m_webview.set_context_menu(false);
if (!resources.contains("/ico/toolbox.png")) { if (!resources.contains("/ico/toolbox.png")) {
+69
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
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
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);
});
}
+50 -376
View File
@@ -13,16 +13,10 @@
#endif #endif
#include <algorithm> #include <algorithm>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <cppcodec/base64_rfc4648.hpp>
#include <saucer/embedded/all.hpp>
#include <saucer/navigation.hpp> #include <saucer/navigation.hpp>
#include <saucer/app.hpp> #include <saucer/app.hpp>
#include <format> #include <format>
#include <chrono> #include <chrono>
extern "C" {
#include <lua.h>
#include <lauxlib.h>
}
#include <fstream> #include <fstream>
#include <sstream> #include <sstream>
#include <unordered_map> #include <unordered_map>
@@ -112,121 +106,25 @@ void ToolboxWindow::setupToolboxWindow() {
} }
void ToolboxWindow::startThreadMonitor() { void ToolboxWindow::startThreadMonitor() {
m_threadMonitor = std::thread([&]() { m_threadMonitor = std::thread([this]() {
monitor_focus(); monitor_focus();
}); });
m_threadMonitor.detach();
} }
bool ToolboxWindow::serveHtmlFile(const std::string& htmlPath, bool setDecorations) { bool ToolboxWindow::serveHtmlFile(const std::string& htmlPath, bool setDecorations) {
auto resources = saucer::embedded::all(); return ResourceLoader::serveHtml(m_webview, htmlPath, setDecorations);
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;
} }
void ToolboxWindow::configureWindow() { void ToolboxWindow::configureWindow() {
// DPI-Awareness berücksichtigen WindowPlacement::configureToolbox(m_webview, m_hwnd);
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);
} }
void ToolboxWindow::moveWindowTo(int x, int y) { void ToolboxWindow::moveWindowTo(int x, int y) {
if (const HWND hwnd = m_hwnd) { WindowPlacement::moveTo(m_hwnd, x, y);
SetWindowPos(hwnd, nullptr, x, y, 0, 0, SWP_NOZORDER | SWP_NOSIZE);
}
} }
void ToolboxWindow::loadResources() { void ToolboxWindow::loadResources() {
auto resources = saucer::embedded::all(); ResourceLoader::loadToolboxShell(m_webview);
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!')");
} }
void ToolboxWindow::show() { void ToolboxWindow::show() {
@@ -265,58 +163,21 @@ void ToolboxWindow::hide() {
} }
void ToolboxWindow::ExposeToJs() { void ToolboxWindow::ExposeToJs() {
// Funktion, die JSON-Code an das JavaScript exponiert ToolboxJsBridge::registerBindings(m_webview, {
m_webview.expose("getToolboxItems", [&]() -> std::string { .getToolboxItems = [this]() { return jsonString; },
return jsonString; .runLua = [this](int id) { run_lua(id); },
}); .cancelLua = [this](int id) { cancel_lua(id); },
.debugPrint = [this]() { debug_print(); },
m_webview.expose("run_lua", [&](int id) { .openFileSystem = [this]() { open_file_system(); },
run_lua(id); .openCommandPrompt = [this]() { open_cmd(); },
}); .openSettings = [this](int id) { openEinst(id); },
.openIndex = [this]() { openIndex(); },
m_webview.expose("cancel_lua", [&](int id) { .getNewContent = [this]() { getNewContent(); },
cancel_lua(id); .getLuaTimeoutSeconds = [this]() { return getLuaTimeoutSeconds(); },
}); .setLuaTimeoutSeconds = [this](int seconds) { setLuaTimeoutSeconds(seconds); },
.getActiveToolName = [this]() { return getActiveToolName(); },
m_webview.expose("debug_print", [&]() { .getToolConfigIni = [this]() { return getToolConfigIni(); },
debug_print(); .setToolConfigIni = [this](const std::string& content) { setToolConfigIni(content); },
});
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);
}); });
} }
@@ -344,213 +205,38 @@ void ToolboxWindow::SetJsonItems() {// Konvertierung des Vektors in JSON
} }
bool ToolboxWindow::ensureToolboxInUserPath() { bool ToolboxWindow::ensureToolboxInUserPath() {
// Bestimme das Benutzerverzeichnis abhängig vom Betriebssystem return ToolRegistry::ensureToolboxDirectory(userPath);
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;
}
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() { void ToolboxWindow::fillToolboxMenu() {
m_toolboxItems.clear(); m_toolboxItems = ToolRegistry::loadTools(userPath);
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);
}
} }
void ToolboxWindow::run_lua(int id) { void ToolboxWindow::run_lua(int id) {
if (id < 0 || id >= static_cast<int>(m_toolboxItems.size())) {
/*// Ö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};
}
// 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; 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 { const std::filesystem::path scriptPath = m_toolboxItems[id].lua_script_path_string;
std::string scriptPath = m_toolboxItems[id].lua_script_path_string; m_luaRunner.run(id, scriptPath, getLuaTimeoutSecondsForId(id), [this, id]() {
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.saucer::webview::execute(std::format("enableButtonByToolId({})", id));
m_webview.reload(); 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;
} }
void ToolboxWindow::cancel_lua(int id) { void ToolboxWindow::cancel_lua(int id) {
std::lock_guard<std::mutex> lock(m_luaTasksMutex); m_luaRunner.cancel(id);
auto it = m_luaTasks.find(id);
if (it != m_luaTasks.end() && it->second.cancel) {
it->second.cancel->store(true, std::memory_order_relaxed);
} }
}
void ToolboxWindow::debug_print() { void ToolboxWindow::debug_print() {
std::cout << "ToolboxWindow::debug_print()" << std::endl; std::cout << "ToolboxWindow::debug_print()" << std::endl;
} }
ToolboxWindow::~ToolboxWindow() { ToolboxWindow::~ToolboxWindow() {
m_runningMonitor = false; 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()"); 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: // Beispielhafte Nutzung:
void ToolboxWindow::openEinst() { void ToolboxWindow::openEinst() {
@@ -627,46 +330,17 @@ void ToolboxWindow::openEinst(int id) {
} }
void ToolboxWindow::centerWindow() { void ToolboxWindow::centerWindow() {
const int screenWidth = GetSystemMetrics(SM_CXSCREEN); WindowPlacement::center(m_hwnd);
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);
} }
void ToolboxWindow::resizeWindow(int width, int height) void ToolboxWindow::resizeWindow(int width, int height)
{ {
// Stellen Sie die Größe des Fensters ein WindowPlacement::resize(m_webview, width, height);
m_webview.parent().set_size({width, height});
} }
void ToolboxWindow::resizeWindow(double widthPercent, double heightPercent) void ToolboxWindow::resizeWindow(double widthPercent, double heightPercent)
{ {
const int screenWidth = GetSystemMetrics(SM_CXSCREEN); WindowPlacement::resizePercent(m_webview, widthPercent, heightPercent);
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});
} }
void ToolboxWindow::openIndex() { void ToolboxWindow::openIndex() {
+241
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
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)));
}