diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1885032..3fe93e4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,7 +32,7 @@ jobs: if: startsWith(matrix.os, 'ubuntu') run: | sudo apt-get update - sudo apt-get install -y --no-install-recommends build-essential libgl1-mesa-dev libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxext-dev + sudo apt-get install -y --no-install-recommends build-essential libcurl4-openssl-dev libgl1-mesa-dev libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxext-dev - name: Configure CMake run: cmake -B build -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DCOREDECK_BUILD_TESTS=ON diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 53d6ef3..10a831f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,7 +44,7 @@ jobs: if: startsWith(matrix.os, 'ubuntu') run: | sudo apt-get update - sudo apt-get install -y --no-install-recommends build-essential libgl1-mesa-dev libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxext-dev + sudo apt-get install -y --no-install-recommends build-essential libcurl4-openssl-dev libgl1-mesa-dev libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxext-dev - name: Configure CMake run: cmake -B build -DCMAKE_BUILD_TYPE=Release diff --git a/CMakeLists.txt b/CMakeLists.txt index 1dfa662..fff42ae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -89,6 +89,10 @@ target_include_directories(tinyfiledialogs PUBLIC ${TINYFD_DIR}) find_package(OpenGL REQUIRED) find_package(Threads REQUIRED) +if (NOT WIN32) + find_package(CURL REQUIRED) +endif () + add_library(coredeck_core STATIC src/core/app_settings.cpp src/core/avd.cpp @@ -123,9 +127,11 @@ target_compile_definitions(coredeck_core PUBLIC if (WIN32) target_compile_definitions(coredeck_core PUBLIC WIN32_LEAN_AND_MEAN NOMINMAX) - target_link_libraries(coredeck_core PUBLIC shell32 comdlg32 ole32) + target_link_libraries(coredeck_core PUBLIC shell32 comdlg32 ole32 winhttp) elseif (UNIX AND NOT APPLE) - target_link_libraries(coredeck_core PUBLIC ${CMAKE_DL_LIBS} Threads::Threads) + target_link_libraries(coredeck_core PUBLIC ${CMAKE_DL_LIBS} Threads::Threads CURL::libcurl) +else () + target_link_libraries(coredeck_core PUBLIC CURL::libcurl) endif () add_executable(${PROJECT_NAME} diff --git a/README.md b/README.md index 266bf94..6a56fd7 100644 --- a/README.md +++ b/README.md @@ -56,8 +56,10 @@ Grab the latest release for your platform from the [Releases](https://github.com **Linux dependencies (Ubuntu/Debian):** +`build-essential` does not include CMake, so it's listed separately: + ```bash -sudo apt-get install build-essential libgl1-mesa-dev libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxext-dev +sudo apt-get install build-essential cmake libcurl4-openssl-dev libgl1-mesa-dev libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxext-dev ``` **Build:** diff --git a/src/core/avd.cpp b/src/core/avd.cpp index e56cbdd..9a652da 100644 --- a/src/core/avd.cpp +++ b/src/core/avd.cpp @@ -49,6 +49,7 @@ namespace CoreDeck { const std::string path = Paths::JoinPaths({avdRoot, avdName + ".avd"}); avd.Name = avdName; + avd.DisplayName = avdName; avd.Path = path; std::string configPath = Paths::JoinPaths({avd.Path, "config.ini"}); @@ -61,7 +62,7 @@ namespace CoreDeck { avd.Device = it->second; } - if (auto it = config.find("avd.ini.displayname"); it != config.end()) { + if (auto it = config.find("avd.ini.displayname"); it != config.end() && !it->second.empty()) { avd.DisplayName = it->second; } diff --git a/src/core/paths.cpp b/src/core/paths.cpp index 1a78ae4..654a5ea 100644 --- a/src/core/paths.cpp +++ b/src/core/paths.cpp @@ -9,6 +9,12 @@ #ifdef _WIN32 #include #include +#elif defined(__APPLE__) +#include +#include +#elif defined(__linux__) +#include +#include #endif #include "log.h" @@ -151,6 +157,40 @@ namespace CoreDeck::Paths { } } + std::string GetExecutableDirectory() { +#ifdef _WIN32 + char buffer[MAX_PATH]; + const DWORD len = GetModuleFileNameA(nullptr, buffer, MAX_PATH); + if (len == 0) return {}; + return std::filesystem::path(std::string(buffer, len)).parent_path().string(); +#elif defined(__APPLE__) + char buffer[PATH_MAX]; + uint32_t size = sizeof(buffer); + if (_NSGetExecutablePath(buffer, &size) != 0) return {}; + return std::filesystem::canonical(buffer).parent_path().string(); +#elif defined(__linux__) + char buffer[PATH_MAX]; + const ssize_t len = readlink("/proc/self/exe", buffer, sizeof(buffer) - 1); + if (len <= 0) return {}; + buffer[len] = '\0'; + return std::filesystem::path(buffer).parent_path().string(); +#else + return {}; +#endif + } + + std::string GetResourcesDirectory() { + std::string exeDir = GetExecutableDirectory(); + if (exeDir.empty()) return {}; +#ifdef __APPLE__ + const std::filesystem::path p(exeDir); + if (p.filename() == "MacOS" && p.parent_path().filename() == "Contents") { + return (p.parent_path() / "Resources").string(); + } +#endif + return std::move(exeDir); + } + std::string JoinPaths(const std::vector &components) { if (components.empty()) return ""; diff --git a/src/core/paths.h b/src/core/paths.h index 522af25..bca8f20 100644 --- a/src/core/paths.h +++ b/src/core/paths.h @@ -34,6 +34,10 @@ namespace CoreDeck::Paths { std::string GetExecutableExtension(); + std::string GetExecutableDirectory(); + + std::string GetResourcesDirectory(); + std::string JoinPaths(const std::vector &components); std::string NormalizePath(const std::string &path); diff --git a/src/core/version_check.cpp b/src/core/version_check.cpp index 2684893..fb057c5 100644 --- a/src/core/version_check.cpp +++ b/src/core/version_check.cpp @@ -6,9 +6,16 @@ #include #include "version_check.h" -#include "process.h" #include "utilities.h" +#if defined(_WIN32) +#include +#include +#pragma comment(lib, "winhttp.lib") +#else +#include +#endif + namespace CoreDeck { struct GitHubLatestRelease { std::string tag_name; @@ -87,25 +94,115 @@ namespace CoreDeck { } } + namespace { +#if defined(_WIN32) + std::optional HttpGet(const wchar_t *host, const wchar_t *path, const std::wstring &userAgent) { + HINTERNET session = WinHttpOpen( + userAgent.c_str(), + WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, + WINHTTP_NO_PROXY_NAME, + WINHTTP_NO_PROXY_BYPASS, + 0 + ); + if (!session) return std::nullopt; + + WinHttpSetTimeouts(session, 15000, 15000, 15000, 15000); + + HINTERNET connect = WinHttpConnect(session, host, INTERNET_DEFAULT_HTTPS_PORT, 0); + if (!connect) { + WinHttpCloseHandle(session); + return std::nullopt; + } + + HINTERNET request = WinHttpOpenRequest( + connect, L"GET", path, nullptr, + WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, + WINHTTP_FLAG_SECURE + ); + if (!request) { + WinHttpCloseHandle(connect); + WinHttpCloseHandle(session); + return std::nullopt; + } + + const wchar_t *headers = L"Accept: application/vnd.github+json\r\n"; + BOOL sent = WinHttpSendRequest(request, headers, static_cast(-1L), WINHTTP_NO_REQUEST_DATA, 0, 0, 0) + && WinHttpReceiveResponse(request, nullptr); + + std::optional result; + if (sent) { + DWORD status = 0; + DWORD statusSize = sizeof(status); + WinHttpQueryHeaders(request, + WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, + WINHTTP_HEADER_NAME_BY_INDEX, &status, &statusSize, + WINHTTP_NO_HEADER_INDEX); + if (status >= 200 && status < 300) { + std::string body; + DWORD available = 0; + while (WinHttpQueryDataAvailable(request, &available) && available > 0) { + std::string chunk(available, '\0'); + DWORD read = 0; + if (!WinHttpReadData(request, chunk.data(), available, &read)) break; + chunk.resize(read); + body.append(chunk); + } + result = std::move(body); + } + } + + WinHttpCloseHandle(request); + WinHttpCloseHandle(connect); + WinHttpCloseHandle(session); + return result; + } +#else + size_t CurlWriteCallback(const char *ptr, const size_t size, size_t nmemb, void *userdata) { + auto *buf = static_cast(userdata); + buf->append(ptr, size * nmemb); + return size * nmemb; + } + + std::optional HttpGet(const std::string &url, const std::string &userAgent) { + CURL *curl = curl_easy_init(); + if (!curl) return std::nullopt; + + std::string body; + curl_slist *headers = curl_slist_append(nullptr, "Accept: application/vnd.github+json"); + + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(curl, CURLOPT_USERAGENT, userAgent.c_str()); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 15L); + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 15L); + curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &body); + + const CURLcode rc = curl_easy_perform(curl); + curl_slist_free_all(headers); + curl_easy_cleanup(curl); + + if (rc != CURLE_OK) return std::nullopt; + return body; + } +#endif + } + std::optional QueryRemoteNewerVersion() { #if defined(_WIN32) - const std::string cmd = StrConcat( - "curl -sfL --max-time 15 -H \"Accept: application/vnd.github+json\" -A \"CoreDeck/", - COREDECK_VERSION, - "\" \"", - COREDECK_GITHUB_API, - "\"" - ); + const std::string ua = StrConcat("CoreDeck/", COREDECK_VERSION); + std::wstring userAgent(ua.begin(), ua.end()); + auto fetched = HttpGet(L"api.github.com", L"/repos/devmuaz/CoreDeck/releases/latest", userAgent); #else - const std::string cmd = StrConcat( - "curl -sfL --max-time 15 -H 'Accept: application/vnd.github+json' -A 'CoreDeck/", - COREDECK_VERSION, - "' '", - COREDECK_GITHUB_API, - "'" - ); + const std::string userAgent = StrConcat("CoreDeck/", COREDECK_VERSION); + auto fetched = HttpGet(COREDECK_GITHUB_API, userAgent); #endif - std::string body = RunCommand(cmd); + if (!fetched) { + return std::nullopt; + } + std::string body = std::move(fetched.value()); TrimInPlace(body); if (body.empty()) { return std::nullopt; diff --git a/src/gui/context.h b/src/gui/context.h index df28ebd..51b7dac 100644 --- a/src/gui/context.h +++ b/src/gui/context.h @@ -92,6 +92,8 @@ namespace CoreDeck { int SelectedSystemImage = 0; int SelectedDevice = 0; int SelectedGpuMode = 0; + bool NameAutoFilled = true; + bool DisplayNameAutoFilled = true; struct { std::atomic Loading{false}; diff --git a/src/gui/icons.h b/src/gui/icons.h new file mode 100644 index 0000000..ac6798d --- /dev/null +++ b/src/gui/icons.h @@ -0,0 +1,31 @@ +// +// Created by AbdulMuaz Aqeel on 24/04/2026. +// + +#ifndef COREDECK_ICONS_H +#define COREDECK_ICONS_H + +namespace CoreDeck::Icons { + constexpr const char *Play = "\xef\x81\x8b"; // fa-play (f04b) + constexpr const char *Stop = "\xef\x81\x8d"; // fa-stop (f04d) + constexpr const char *Refresh = "\xef\x80\xa1"; // fa-arrows-rotate (f021) + constexpr const char *Trash = "\xef\x87\xb8"; // fa-trash-can (f1f8) + constexpr const char *Circle = "\xef\x84\x91"; // fa-circle (f111) + constexpr const char *Desktop = "\xef\x84\x88"; // fa-desktop (f108) + constexpr const char *Gear = "\xef\x80\x93"; // fa-gear (f013) + constexpr const char *Terminal = "\xef\x84\xa0"; // fa-terminal (f120) + constexpr const char *Info = "\xef\x81\x9a"; // fa-circle-info (f05a) + constexpr const char *Search = "\xef\x80\x82"; // fa-magnifying-glass (f002) + constexpr const char *Plus = "\xef\x81\xa7"; // fa-plus (f067) + constexpr const char *SortUp = "\xef\x83\x9e"; // fa-sort-up (f0de) + constexpr const char *SortDown = "\xef\x83\x9d"; // fa-sort-down (f0dd) + constexpr const char *Sort = "\xef\x83\x9c"; // fa-sort (f0dc) + constexpr const char *Times = "\xef\x80\x8d"; // fa-xmark (f00d) + constexpr const char *Mobile = "\xef\x8f\x8d"; // fa-mobile-screen-button (f3cd) + constexpr const char *Tablet = "\xef\x8f\xba"; // fa-tablet-screen-button (f3fa) + constexpr const char *Tv = "\xef\x89\xac"; // fa-tv (f26c) + constexpr const char *Watch = "\xef\x80\x97"; // fa-clock (f017) — used for Wear OS + constexpr const char *Car = "\xef\x86\xb9"; // fa-car (f1b9) — used for Automotive +} + +#endif //COREDECK_ICONS_H diff --git a/src/gui/theme.h b/src/gui/theme.h index 97977fd..fda8c7d 100644 --- a/src/gui/theme.h +++ b/src/gui/theme.h @@ -30,27 +30,6 @@ namespace CoreDeck { alpha }; } - - namespace Icons { - constexpr const char *Play = "\xef\x81\x8b"; - constexpr const char *Stop = "\xef\x81\x8d"; - constexpr const char *Refresh = "\xef\x80\xa1"; - constexpr const char *Trash = "\xef\x87\xb8"; - constexpr const char *Circle = "\xef\x84\x91"; - constexpr const char *Desktop = "\xef\x84\x88"; - constexpr const char *Gear = "\xef\x80\x93"; - constexpr const char *Terminal = "\xef\x84\xa0"; - constexpr const char *Info = "\xef\x81\x9a"; - constexpr const char *Search = "\xef\x80\x82"; - constexpr const char *Plus = "\xef\x81\xa7"; - constexpr const char *SortUp = "\xef\x83\x9e"; // fa-sort-up (f0de) - constexpr const char *SortDown = "\xef\x83\x9d"; // fa-sort-down (f0dd) - constexpr const char *Sort = "\xef\x83\x9c"; // fa-sort (f0dc) - constexpr const char *Times = "\xef\x80\x8d"; // fa-xmark (f00d) - } - - namespace Colors { - } } #endif //COREDECK_THEME_H diff --git a/src/gui/widgets.cpp b/src/gui/widgets.cpp index b457b75..d7dbb99 100644 --- a/src/gui/widgets.cpp +++ b/src/gui/widgets.cpp @@ -18,8 +18,9 @@ namespace CoreDeck { sc.push(ImGuiCol_Text, HexColor("#F2F2F2")); sc.push(ImGuiCol_Border, HexColor("#4D4D52")); + const bool clicked = ImGui::Button(label, size); if (!isEnabled) ImGui::EndDisabled(); - return ImGui::Button(label, size); + return clicked; } bool NegativeButton(const char *label, const bool isEnabled, const ImVec2 size) { @@ -32,8 +33,9 @@ namespace CoreDeck { sc.push(ImGuiCol_Text, HexColor("#E64D40")); sc.push(ImGuiCol_Border, HexColor("#E64D40")); + const bool clicked = ImGui::Button(label, size); if (!isEnabled) ImGui::EndDisabled(); - return ImGui::Button(label, size); + return clicked; } bool WarningButton(const char *label, const bool isEnabled, const ImVec2 size) { @@ -46,8 +48,9 @@ namespace CoreDeck { sc.push(ImGuiCol_Text, HexColor("#E6BF26")); sc.push(ImGuiCol_Border, HexColor("#E6BF26")); + const bool clicked = ImGui::Button(label, size); if (!isEnabled) ImGui::EndDisabled(); - return ImGui::Button(label, size); + return clicked; } bool PositiveButton(const char *label, const bool isEnabled, const ImVec2 size) { @@ -60,8 +63,9 @@ namespace CoreDeck { sc.push(ImGuiCol_Text, HexColor("#33CC47")); sc.push(ImGuiCol_Border, HexColor("#33CC47")); + const bool clicked = ImGui::Button(label, size); if (!isEnabled) ImGui::EndDisabled(); - return ImGui::Button(label, size); + return clicked; } void StatusBadge(const char *label, const bool isActive) { @@ -85,7 +89,14 @@ namespace CoreDeck { ImGui::Button(label); } - bool SelectableItem(const char *label, const bool isSelected, const char *rightText, const ImVec4 &rightColor) { + bool SelectableItem( + const char *label, + const bool isSelected, + const char *rightText, + const ImVec4 &rightColor, + const char *leftIcon, + const ImVec4 &leftIconColor + ) { StyleColor sc; StyleVar sv; @@ -100,7 +111,34 @@ namespace CoreDeck { sv.push(ImGuiStyleVar_FrameBorderSize, 0.0f); sv.push(ImGuiStyleVar_ButtonTextAlign, ImVec2(0.0f, 0.5f)); - const bool clicked = ImGui::Button(label, ImVec2(-1.0f, 0.0f)); + std::string buttonLabel; + if (leftIcon && leftIcon[0] != '\0') { + buttonLabel = leftIcon; + buttonLabel += " "; + buttonLabel += label; + } else { + buttonLabel = label; + } + + const bool clicked = ImGui::Button(buttonLabel.c_str(), ImVec2(-1.0f, 0.0f)); + + if (leftIcon && leftIcon[0] != '\0') { + const ImVec2 itemMin = ImGui::GetItemRectMin(); + const ImVec2 itemMax = ImGui::GetItemRectMax(); + const ImVec2 padding = ImGui::GetStyle().FramePadding; + const ImVec2 iconSize = ImGui::CalcTextSize(leftIcon); + + const auto iconPos = ImVec2( + itemMin.x + padding.x, + itemMin.y + (itemMax.y - itemMin.y - iconSize.y) * 0.5f + ); + + ImGui::GetWindowDrawList()->AddText( + iconPos, + ImGui::ColorConvertFloat4ToU32(leftIconColor), + leftIcon + ); + } if (rightText && rightText[0] != '\0') { const ImVec2 textSize = ImGui::CalcTextSize(rightText); diff --git a/src/gui/widgets.h b/src/gui/widgets.h index f5f6d22..5012904 100644 --- a/src/gui/widgets.h +++ b/src/gui/widgets.h @@ -76,7 +76,9 @@ namespace CoreDeck { const char *label, bool isSelected, const char *rightText = nullptr, - const ImVec4 &rightColor = ImVec4(1.0f, 1.0f, 1.0f, 1.0f) + const ImVec4 &rightColor = ImVec4(1.0f, 1.0f, 1.0f, 1.0f), + const char *leftIcon = nullptr, + const ImVec4 &leftIconColor = ImVec4(1.0f, 1.0f, 1.0f, 1.0f) ); bool PropertyText(const char *label, const char *value, bool isClickable = false, bool hasSpaceBetween = false); diff --git a/src/gui/windows/avd_list.cpp b/src/gui/windows/avd_list.cpp index fab36fc..81adcdc 100644 --- a/src/gui/windows/avd_list.cpp +++ b/src/gui/windows/avd_list.cpp @@ -2,7 +2,6 @@ // Created by AbdulMuaz Aqeel on 15/04/2026. // #include -#include #include "imgui.h" #include "avd_list.h" @@ -10,8 +9,27 @@ #include "../application.h" #include "../widgets.h" #include "../theme.h" +#include "../icons.h" namespace CoreDeck { + struct DeviceIconStyle { + const char *Icon; + const char *HexColor; + }; + + static DeviceIconStyle DeviceIconStyleFor(const std::string &device) { + std::string d; + d.reserve(device.size()); + for (const char c: device) d.push_back(static_cast(std::tolower(static_cast(c)))); + + if (d.find("wear") != std::string::npos) return {Icons::Watch, "#F5A623"}; + if (d.find("auto") != std::string::npos) return {Icons::Car, "#E64D40"}; + if (d.find("tv") != std::string::npos) return {Icons::Tv, "#7E57C2"}; + if (d.find("tablet") != std::string::npos || d.find("pixel_c") != std::string::npos) + return {Icons::Tablet, "#33CC47"}; + return {Icons::Mobile, "#4FC3F7"}; + } + static bool ContainsCaseInsensitive(const std::string &haystack, const char *needle) { if (needle[0] == '\0') return true; @@ -86,11 +104,34 @@ namespace CoreDeck { constexpr ImGuiWindowFlags panelFlags = ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse; ImGui::Begin("Available AVDs (Android Virtual Device)###AVDs", nullptr, panelFlags); + auto openCreateAvdDialog = [&context] { + context.AvdCreationWork.CreationData = {}; + context.AvdCreationWork.SelectedSystemImage = 0; + context.AvdCreationWork.SelectedDevice = 0; + context.AvdCreationWork.SelectedGpuMode = 0; + context.AvdCreationWork.NameAutoFilled = true; + context.AvdCreationWork.DisplayNameAutoFilled = true; + context.AvdCreationWork.Prefetch.Ready = false; + context.AvdCreationWork.Prefetch.Loading = true; + context.UI.ShowCreateAvdDialog = true; + + context.AvdCreationWork.Prefetch.Future = std::async(std::launch::async, [&context] { + auto images = ListSystemImages(context.Host.Sdk); + auto devices = ListDeviceProfiles(context.Host.Sdk); + context.AvdCreationWork.SystemImages = std::move(images); + context.AvdCreationWork.DeviceProfiles = std::move(devices); + context.AvdCreationWork.Prefetch.Loading = false; + context.AvdCreationWork.Prefetch.Ready = true; + }); + }; + if (PrimaryButton(Icons::Refresh)) RefreshAvds(context); if (ImGui::IsItemHovered()) ImGui::SetTooltip("Refresh the AVD list"); ImGui::SameLine(); + if (PrimaryButton(Icons::Plus)) openCreateAvdDialog(); + if (ImGui::IsItemHovered()) ImGui::SetTooltip("Create new AVD"); if (context.Catalog.SelectedAvd >= 0) { const auto &avd = context.Catalog.Avds[context.Catalog.SelectedAvd]; @@ -130,27 +171,6 @@ namespace CoreDeck { ImGui::SameLine(0, 15.0f); ImGui::Text("-"); } - - ImGui::SameLine(); - if (PrimaryButton(Icons::Plus)) { - context.AvdCreationWork.CreationData = {}; - context.AvdCreationWork.SelectedSystemImage = 0; - context.AvdCreationWork.SelectedDevice = 0; - context.AvdCreationWork.SelectedGpuMode = 0; - context.AvdCreationWork.Prefetch.Ready = false; - context.AvdCreationWork.Prefetch.Loading = true; - context.UI.ShowCreateAvdDialog = true; - - context.AvdCreationWork.Prefetch.Future = std::async(std::launch::async, [&context] { - auto images = ListSystemImages(context.Host.Sdk); - auto devices = ListDeviceProfiles(context.Host.Sdk); - context.AvdCreationWork.SystemImages = std::move(images); - context.AvdCreationWork.DeviceProfiles = std::move(devices); - context.AvdCreationWork.Prefetch.Loading = false; - context.AvdCreationWork.Prefetch.Ready = true; - }); - } - if (ImGui::IsItemHovered()) ImGui::SetTooltip("Create new AVD"); } ImGui::Separator(); @@ -230,7 +250,9 @@ namespace CoreDeck { ImGui::PushID(i); const char *avdStatusText = isRunning ? "Running..." : "Ready"; const ImVec4 avdStatusColor = isRunning ? HexColor("#33CC47") : HexColor("#66666B"); - if (SelectableItem(avd.DisplayName.c_str(), isSelected, avdStatusText, avdStatusColor)) { + const DeviceIconStyle iconStyle = DeviceIconStyleFor(avd.Device); + if (SelectableItem(avd.DisplayName.c_str(), isSelected, avdStatusText, avdStatusColor, + iconStyle.Icon, HexColor(iconStyle.HexColor))) { context.Catalog.SelectedAvd = i; } ImGui::PopID(); diff --git a/src/gui/windows/avd_logs.cpp b/src/gui/windows/avd_logs.cpp index cbcd873..1c6cc88 100644 --- a/src/gui/windows/avd_logs.cpp +++ b/src/gui/windows/avd_logs.cpp @@ -7,7 +7,7 @@ #include "avd_logs.h" #include "../context.h" -#include "../theme.h" +#include "../icons.h" #include "../widgets.h" namespace CoreDeck { diff --git a/src/gui/windows/avd_options.cpp b/src/gui/windows/avd_options.cpp index 8975273..4525902 100644 --- a/src/gui/windows/avd_options.cpp +++ b/src/gui/windows/avd_options.cpp @@ -8,6 +8,7 @@ #include "../application.h" #include "../widgets.h" #include "../theme.h" +#include "../icons.h" namespace CoreDeck { void BuildAvdOptionsWindow(Context &context) { diff --git a/src/gui/windows/create_avd.cpp b/src/gui/windows/create_avd.cpp index 1b33e4f..a1034ce 100644 --- a/src/gui/windows/create_avd.cpp +++ b/src/gui/windows/create_avd.cpp @@ -12,10 +12,12 @@ #include "../theme.h" namespace CoreDeck { + // ReSharper disable once CppParameterMayBeConstPtrOrRef static int DigitsOnlyFilter(ImGuiInputTextCallbackData *data) { - return (data->EventChar >= '0' && data->EventChar <= '9') ? 0 : 1; + return data->EventChar >= '0' && data->EventChar <= '9' ? 0 : 1; } + // ReSharper disable once CppParameterMayBeConstPtrOrRef static int AvdNameFilter(ImGuiInputTextCallbackData *data) { const ImWchar c = data->EventChar; const bool ok = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || @@ -50,11 +52,10 @@ namespace CoreDeck { ImGuiWindowFlags_NoDocking; if (ImGui::BeginPopupModal("Create New AVD###CreateAvdDialog", &context.UI.ShowCreateAvdDialog, flags)) { - auto &removal = context.AvdCreationWork.SystemImageRemoval; - if (removal.Future.valid()) { - if (removal.Future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { - const bool ok = removal.Future.get(); - if (ok) { + auto &[Busy, Future] = context.AvdCreationWork.SystemImageRemoval; + if (Future.valid()) { + if (Future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + if (Future.get()) { context.AvdCreationWork.SystemImages = ListSystemImages(context.Host.Sdk); if (context.AvdCreationWork.SystemImages.empty()) { context.AvdCreationWork.SelectedSystemImage = 0; @@ -69,45 +70,70 @@ namespace CoreDeck { const bool isLoading = context.AvdCreationWork.Prefetch.Loading.load(); const bool isCreating = context.Jobs.AvdCreation.Busy.load(); const bool formDisabled = isLoading || isCreating; - const bool systemImageRemovalBusy = removal.Busy.load() || removal.Future.valid(); + const bool systemImageRemovalBusy = Busy.load() || Future.valid(); if (formDisabled) ImGui::BeginDisabled(); - const float nameRowSpacing = ImGui::GetStyle().ItemSpacing.x; - const float nameColWidth = (ImGui::GetContentRegionAvail().x - nameRowSpacing) * 0.5f; - const float nameCol2X = ImGui::GetCursorPosX() + nameColWidth + nameRowSpacing; + auto &work = context.AvdCreationWork; + const bool hasDevice = !work.DeviceProfiles.empty() + && work.SelectedDevice >= 0 + && work.SelectedDevice < static_cast(work.DeviceProfiles.size()); + const bool hasImage = !work.SystemImages.empty() + && work.SelectedSystemImage >= 0 + && work.SelectedSystemImage < static_cast(work.SystemImages.size()); + if (hasDevice && hasImage) { + const auto &[Id, Name] = work.DeviceProfiles[work.SelectedDevice]; + const auto &img = work.SystemImages[work.SelectedSystemImage]; + if (work.NameAutoFilled) { + std::string base = Id + "_API_" + img.ApiLevel; + std::string sanitized; + sanitized.reserve(base.size()); + for (const char c: base) { + const bool keep = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') + || (c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-'; + sanitized.push_back(keep ? c : '_'); + } + work.CreationData.Name = std::move(sanitized); + } + if (work.DisplayNameAutoFilled) { + work.CreationData.DisplayName = Name + " API " + img.ApiLevel; + } + } ImGui::Text("AVD Name"); - ImGui::SameLine(); - ImGui::SetCursorPosX(nameCol2X); - ImGui::Text("Display Name"); - char nameBuffer[128]; strncpy(nameBuffer, context.AvdCreationWork.CreationData.Name.c_str(), sizeof(nameBuffer) - 1); nameBuffer[sizeof(nameBuffer) - 1] = '\0'; - ImGui::SetNextItemWidth(nameColWidth); + ImGui::SetNextItemWidth(-1.0f); if (ImGui::InputTextWithHint("##AvdName", "e.g. MyPixel7", nameBuffer, sizeof(nameBuffer), ImGuiInputTextFlags_CallbackCharFilter, AvdNameFilter)) { context.AvdCreationWork.CreationData.Name = nameBuffer; + context.AvdCreationWork.NameAutoFilled = (nameBuffer[0] == '\0'); } - ImGui::SameLine(); - ImGui::SetCursorPosX(nameCol2X); + const bool nameConflict = AvdNameExists( + context.Catalog.AvdNames, + context.AvdCreationWork.CreationData.Name + ); + if (nameConflict) { + ImGui::TextColored( + HexColor("#E64D40"), + " An AVD named \"%s\" already exists.", + context.AvdCreationWork.CreationData.Name.c_str() + ); + } + + ImGui::Spacing(); + + ImGui::Text("Display Name"); char displayBuffer[128]; strncpy(displayBuffer, context.AvdCreationWork.CreationData.DisplayName.c_str(), sizeof(displayBuffer) - 1); displayBuffer[sizeof(displayBuffer) - 1] = '\0'; - ImGui::SetNextItemWidth(nameColWidth); + ImGui::SetNextItemWidth(-1.0f); if (ImGui::InputTextWithHint("##DisplayName", "e.g. My Pixel 7", displayBuffer, sizeof(displayBuffer))) { context.AvdCreationWork.CreationData.DisplayName = displayBuffer; - } - - if (AvdNameExists(context.Catalog.AvdNames, context.AvdCreationWork.CreationData.Name)) { - ImGui::TextColored( - HexColor("#E64D40"), - "An AVD named \"%s\" already exists.", - context.AvdCreationWork.CreationData.Name.c_str() - ); + context.AvdCreationWork.DisplayNameAutoFilled = (displayBuffer[0] == '\0'); } ImGui::Spacing(); @@ -122,9 +148,9 @@ namespace CoreDeck { ); } else if (!context.AvdCreationWork.SystemImages.empty()) { ImGui::SetNextItemWidth(-1.0f); - if (ImGui::BeginCombo("##SystemImage", - context.AvdCreationWork.SystemImages[context.AvdCreationWork.SelectedSystemImage]. - DisplayName.c_str())) { + const auto &systemImages = context.AvdCreationWork.SystemImages; + const auto &selectedSystemImage = context.AvdCreationWork.SelectedSystemImage; + if (ImGui::BeginCombo("##SystemImage", systemImages[selectedSystemImage].DisplayName.c_str())) { for (int i = 0; i < static_cast(context.AvdCreationWork.SystemImages.size()); i++) { const bool isSelected = context.AvdCreationWork.SelectedSystemImage == i; if (ImGui::Selectable(context.AvdCreationWork.SystemImages[i].DisplayName.c_str(), @@ -149,7 +175,7 @@ namespace CoreDeck { context.UI.ShowInstallImageDialog = true; context.ImageInstallationWork.Prefetch.Future = std::async(std::launch::async, [&context] { - auto localImages = ListSystemImages(context.Host.Sdk); + const auto localImages = ListSystemImages(context.Host.Sdk); auto remoteImages = ListRemoteSystemImages(context.Host.Sdk, localImages); context.ImageInstallationWork.RemoteImages = std::move(remoteImages); context.ImageInstallationWork.Prefetch.Loading = false; @@ -168,7 +194,8 @@ namespace CoreDeck { context.AvdCreationWork.Prefetch.Ready && !context.AvdCreationWork.SystemImages.empty() && context.AvdCreationWork.SelectedSystemImage >= 0 && - context.AvdCreationWork.SelectedSystemImage < static_cast(context.AvdCreationWork.SystemImages.size()); + context.AvdCreationWork.SelectedSystemImage < static_cast(context.AvdCreationWork. + SystemImages.size()); if (systemImageRemovalBusy) { ImGui::BeginDisabled(); NegativeButton("Removing...", false, ImVec2(0, 0)); @@ -176,9 +203,10 @@ namespace CoreDeck { } else { if (NegativeButton("Remove Image...", canRemove)) { const std::string pkg = - context.AvdCreationWork.SystemImages[context.AvdCreationWork.SelectedSystemImage].PackagePath; - removal.Busy = true; - removal.Future = std::async(std::launch::async, [&context, pkg]() { + context.AvdCreationWork.SystemImages[context.AvdCreationWork.SelectedSystemImage]. + PackagePath; + Busy = true; + Future = std::async(std::launch::async, [&context, pkg]() { try { const bool ok = UninstallSystemImage(context.Host.Sdk, pkg); context.AvdCreationWork.SystemImageRemoval.Busy = false; @@ -280,6 +308,7 @@ namespace CoreDeck { const bool canCreate = !context.AvdCreationWork.CreationData.Name.empty() && !context.AvdCreationWork.SystemImages.empty() && !context.AvdCreationWork.DeviceProfiles.empty() + && !nameConflict && !formDisabled; if (isCreating) { diff --git a/src/gui/windows/main_menu_bar.cpp b/src/gui/windows/main_menu_bar.cpp index 7bd3d0f..c713379 100644 --- a/src/gui/windows/main_menu_bar.cpp +++ b/src/gui/windows/main_menu_bar.cpp @@ -7,7 +7,7 @@ #include "main_menu_bar.h" #include "../widgets.h" -#include "../theme.h" +#include "../icons.h" #include "../application.h" namespace CoreDeck { diff --git a/src/main.cpp b/src/main.cpp index 6df6900..986c4cf 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -26,15 +26,6 @@ #include "core/utilities.h" int main() { -#ifdef _WIN32 - // Ensure working directory is always the folder containing the executable. - // Without this, relative paths like "assets/..." fail when launched from - // a Desktop or Start Menu shortcut. - char exePath[MAX_PATH]; - GetModuleFileNameA(nullptr, exePath, MAX_PATH); - std::filesystem::current_path(std::filesystem::path(exePath).parent_path()); -#endif - if (!glfwInit()) { #ifdef _WIN32 MessageBoxA(nullptr, "Failed to initialize GLFW.", "CoreDeck", MB_OK | MB_ICONERROR); @@ -82,11 +73,19 @@ int main() { static std::string imguiIniPath = CoreDeck::Paths::GetAppConfigPath("imgui.ini"); io.IniFilename = imguiIniPath.c_str(); - if (std::filesystem::exists("assets/fonts/JetBrainsMono-Regular.ttf")) { - io.Fonts->AddFontFromFileTTF("assets/fonts/JetBrainsMono-Regular.ttf", 16.0f); + const std::string resourcesDir = CoreDeck::Paths::GetResourcesDirectory(); + const std::string textFontPath = CoreDeck::Paths::JoinPaths( + {resourcesDir, "assets", "fonts", "JetBrainsMono-Regular.ttf"} + ); + const std::string iconFontPath = CoreDeck::Paths::JoinPaths( + {resourcesDir, "assets", "fonts", "FontAwesome7Free-Solid-900.otf"} + ); + + if (std::filesystem::exists(textFontPath)) { + io.Fonts->AddFontFromFileTTF(textFontPath.c_str(), 16.0f); } - if (std::filesystem::exists("assets/fonts/FontAwesome7Free-Solid-900.otf")) { + if (std::filesystem::exists(iconFontPath)) { ImFontConfig iconConfig; iconConfig.MergeMode = true; iconConfig.PixelSnapH = true; @@ -94,7 +93,7 @@ int main() { static constexpr ImWchar iconRanges[] = {0xf000, 0xf8ff, 0}; io.Fonts->AddFontFromFileTTF( - "assets/fonts/FontAwesome7Free-Solid-900.otf", + iconFontPath.c_str(), 12.0f, &iconConfig, iconRanges @@ -138,7 +137,10 @@ int main() { }); while (!glfwWindowShouldClose(window)) { - glfwPollEvents(); + const bool focused = glfwGetWindowAttrib(window, GLFW_FOCUSED); + const bool hovered = glfwGetWindowAttrib(window, GLFW_HOVERED); + const double timeout = focused && hovered ? 1.0 / 60.0 : 0.25; + glfwWaitEventsTimeout(timeout); ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplGlfw_NewFrame();