From 9899794b81a7d9744fb0c0db45e4ae35b587b313 Mon Sep 17 00:00:00 2001 From: Zachary Hall Date: Sat, 21 Dec 2024 14:23:00 -0800 Subject: [PATCH] Add cat support --- assets/update_assets.py | 3 +- backends/ui/haiku/main_window.cpp | 51 +++++++++- backends/ui/haiku/main_window.h | 2 + backends/ui/haiku/prefs.cpp | 73 +++++++++++++- backends/ui/haiku/prefs.h | 9 ++ backends/ui/imgui/RendererBackend.cpp | 6 +- backends/ui/imgui/main.cpp | 133 +++++++++++++++++++++++++- backends/ui/imgui/main.h | 9 ++ backends/ui/qt/main_window.cpp | 14 ++- backends/ui/qt/main_window.h | 3 + backends/ui/qt/preferences.cpp | 69 +++++++++++++ backends/ui/qt/preferences.h | 9 +- base85.cpp | 11 ++- base85.h | 6 +- cats.hpp | 88 +++++++++++++++++ file_backend.cpp | 13 ++- file_backend.hpp | 2 + main.cpp | 20 ++++ 18 files changed, 499 insertions(+), 22 deletions(-) create mode 100644 cats.hpp diff --git a/assets/update_assets.py b/assets/update_assets.py index 2dc223a..623f6e6 100755 --- a/assets/update_assets.py +++ b/assets/update_assets.py @@ -44,7 +44,7 @@ def add_font(input: str, output: str|None = None): def add_graphic(input: str): GRAPHIC=path.basename(input).removesuffix(".png").lower().replace('-', '_') print("Adding graphic '%s' from file '%s'" % (GRAPHIC, input)) - add_base85(input, GRAPHIC) + add_basic(input, GRAPHIC) def add_license(input: str, output: str): LICENSE = output + "_license" print("Adding license '%s' (C identifier: '%s') from file '%s'" % (output, LICENSE, input)) @@ -72,6 +72,7 @@ for i in glob("Noto_Sans_JP/*.ttf"): add_font(i) add_font("ForkAwesome/fonts/forkawesome-webfont.ttf", "forkawesome") add_graphic("icon.png") +add_graphic("catoc.png") add_license("Noto_Sans/OFL.txt", "notosans") add_license("Noto_Sans_JP/OFL.txt", "notosansjp") add_license("../LICENSE.MIT", "looper_mit") diff --git a/backends/ui/haiku/main_window.cpp b/backends/ui/haiku/main_window.cpp index 5abe30e..60c7e8f 100644 --- a/backends/ui/haiku/main_window.cpp +++ b/backends/ui/haiku/main_window.cpp @@ -18,7 +18,9 @@ #include #include #include +#include #include +#include #include "icons.h" #include "utils.h" #include "aboutwindow.h" @@ -61,6 +63,22 @@ void HaikuLooperWindow::UpdateViewFlags(BLayout *layout) { } UpdateViewFlags(owner); } +void HaikuLooperWindow::AddCat(std::string name, BBitmap *bitmap) { + if (bitmap == NULL) return; + if (cats.contains(name)) { + delete cats[name]; + } + cats[name] = bitmap; +} +void HaikuLooperWindow::LoadCat(const char *path) { + return BTranslationUtils::LoadBitmap(path); +} +void HaikuLooperWindow::LoadCat(const void *ptr, size_t len, const char *name) { + BMemoryIO *io = new BMemoryIO(ptr, len); + BBitmap *output = BTranslationUtils::LoadBitmap(io); + delete io; + return output; +} HaikuLooperWindow::HaikuLooperWindow(Playback *playback) : BWindow(BRect(100, 100, 500, 100), "Looper", B_TITLED_WINDOW, 0) { pause_bitmap = load_icon(ICON_PAUSE); resume_bitmap = load_icon(ICON_PLAY); @@ -94,7 +112,7 @@ HaikuLooperWindow::HaikuLooperWindow(Playback *playback) : BWindow(BRect(100, 10 menu_bar->AddItem(file_menu); menu_bar->AddItem(help_menu); layout->AddView(menu_bar); - BView *spacer = new BView("spacer", B_SUPPORTS_LAYOUT|B_FRAME_EVENTS); + spacer = new BView("spacer", B_SUPPORTS_LAYOUT|B_FRAME_EVENTS); spacer->SetExplicitPreferredSize(BSize(0, 0)); spacer->SetExplicitMinSize(BSize(0, 0)); layout->AddView(spacer); @@ -161,6 +179,30 @@ HaikuLooperWindow::HaikuLooperWindow(Playback *playback) : BWindow(BRect(100, 10 MessageReceived(msg); delete msg; } +void HaikuLooperWindow::UpdateCat(BBitmap *cat) { + if (cat != NULL) { + auto bounds = cat->Bounds(); + auto w = bounds.Width(), h = bounds.Height(); + auto sb = spacer->Bounds(); + auto sw = sb.Width(), sh = sb.Height(); + BRect src(0, 0, w, h); + if (w > sw || h > sh) { + float aspect = w / h; + if (w > sw) { + w = sw; + h = sw / aspect; + } + if (h > sh) { + h = sh; + w = sh * aspect; + } + } + BRect dst(sw - w, sh - h, w, h); + spacer->SetOverlayBitmap(cat, src, dst, NULL, B_FOLLOW_RIGHT|B_FOLLOW_BOTTOM); + } else { + spacer->ClearOverlayBitmap(); + } +} HaikuLooperWindow::~HaikuLooperWindow() { delete ref_handler; delete pause_bitmap; @@ -245,6 +287,13 @@ void HaikuLooperWindow::MessageReceived(BMessage *msg) { case CMD_PREFS: { prefs_subwindow->Show(); } break; + case CMD_SET_CAT: { + BBitmap *cat = NULL; + if (msg->FindBitmap("cat", &cat) != B_OK) { + cat = NULL; + } + UpdateCat(cat); + } case CMD_UPDATE_LABEL_SETTING: { volume_slider->MessageReceived(msg); speed_slider->MessageReceived(msg); diff --git a/backends/ui/haiku/main_window.h b/backends/ui/haiku/main_window.h index 1ddc56e..014fe34 100644 --- a/backends/ui/haiku/main_window.h +++ b/backends/ui/haiku/main_window.h @@ -59,6 +59,8 @@ class HaikuLooperWindow : public BWindow { BButton *restart_btn; BButton *stop_btn; BButton *pause_resume_btn; + BView *spacer; + void UpdateCat(BBitmap *cat); bool volume_clicked = false; bool speed_clicked = false; bool pitch_clicked = false; diff --git a/backends/ui/haiku/prefs.cpp b/backends/ui/haiku/prefs.cpp index 8677fc2..ce6e84c 100644 --- a/backends/ui/haiku/prefs.cpp +++ b/backends/ui/haiku/prefs.cpp @@ -13,10 +13,25 @@ using namespace Looper::Options; #define CMD_SET_SETTING 0x1002 #define CMD_REVERT 0x1003 #define CMD_APPLY 0x1004 +#define CMD_SET_SETTING_CHECKBOX = 0x1005 bool show_icons, show_labels; HaikuPrefsWindow::HaikuPrefsWindow(BLooper *next_handler) : BWindow(BRect(100, 100, 0, 0), "Preferences", B_TITLED_WINDOW, B_AUTO_UPDATE_SIZE_LIMITS) { this->next_handler = next_handler; + + for (auto &cat : get_cat_data()) { + switch (cat.get_type()) { + case CatDataType::Memory: { + MemoryCat mem_cat = cat.get_memory_cat(); + + AddCat(cat.get_name(), LoadCat(mem_cat.get_ptr(), mem_cat.get_len(), cat.get_path().c_str())); + } break; + case CatDataType::File: { + fs::path cat_path = cat.get_path(); + AddCat(cat.get_name(), LoadCat(cat_path.c_str())); + } break; + } + } auto *root_layout = new BGroupLayout(B_VERTICAL); SetLayout(root_layout); root_layout->SetSpacing(0.0); @@ -56,7 +71,35 @@ HaikuPrefsWindow::HaikuPrefsWindow(BLooper *next_handler) : BWindow(BRect(100, 1 label_settings_layout->AddView(labels_only); label_settings_layout->AddView(icons_only); label_settings_layout->AddView(both_labels_icons); - box->AddChild(label_settings_group); + BMessage *cat_enable_msg = new BMessage(CMD_SET_SETTING_CHECKBOX); + cat_enable_msg->AddString("pref_path", "ui.cat_enable"); + cat_enable = new BCheckBox("Enable Cat", cat_enable_msg); + BGroupView *cat_group = new BGroupView(B_VERTICAL); + BGroupLayout *cat_layout = cat_group->GroupLayout(); + for (auto &kv : cats) { + BMessage *msg = new BMessage(CMD_SET_SETTING); + msg->AddString("pref_path", "ui.cat"); + msg->AddString("pref_value", kv.first.c_str()); + BRadioButton *cat_radio = new BRadioButton(fmt::format("prefs:cat:{}", kv.first).c_str(), msg); + cat_btns[kv.first] = cat_radio; + BBitmap *cat_bmp = kv.second; + BRect src = cat_bmp->bounds(); + float w = src.Width(), h = src.Height(); + float rw = cat_radio->Bounds().Width(), rh = cat_radio->Bounds().Height(); + if (w > rw || h > rh) { + float aspect = w / h; + if (h > rh) { + h = rh; + w = h * aspect; + } + if (w > rw) { + w = rw; + h = h / aspect; + } + } + BRect dst(rw - w, (rh - h) / 2, rw, rh); + cat_radio->SetOverlayBitmap(cat_bmp, src, dst, B_FOLLOW_RIGHT|B_FOLLOW_TOP_BOTTOM); + } root_layout->AddView(box, 3.0); BGroupView *btn_view = new BGroupView(B_HORIZONTAL); BGroupLayout *btn_box = btn_view->GroupLayout(); @@ -102,7 +145,12 @@ void HaikuPrefsWindow::MessageReceived(BMessage *msg) { else restart_warning->Hide(); set_option("ui.haiku.label_setting", new_label_setting); update_label_setting(); + set_option("ui.cat", cat_id); + set_option("ui.enable_cat", enable_cat); next_handler->PostMessage(CMD_UPDATE_LABEL_SETTING); + BMessage *cat_msg = new BMessage(CMD_SET_CAT); + if (enable_cat) cat_msg->AddBitmap("cat", cats[cat_id]) + next_handler->PostMessage(cat_msg); } break; case CMD_REVERT: { set_options_changed(false); @@ -117,8 +165,16 @@ void HaikuPrefsWindow::MessageReceived(BMessage *msg) { break; } } + cat_btns[cat_id].SetValue(B_CONTROL_OFF); + cat_id = get_option("ui.cat", cats.empty() ? "" : cats.begin()->first); + if (cat_btns.contains(cat_id)) cat_btns[cat_id].SetValue(B_CONTROL_ON); + enable_cat = get_option("ui.enable_cat", false); + cat_enable->SetValue(enable_cat ? B_CONTROL_ON : B_CONTROL_OFF); update_label_setting(); next_handler->PostMessage(CMD_UPDATE_LABEL_SETTING); + BMessage *cat_msg = new BMessage(CMD_SET_CAT); + if (enable_cat) cat_msg->AddBitmap("cat", cats[cat_id]) + next_handler->PostMessage(cat_msg); } break; case CMD_FRONTEND: { auto option = msg->GetInt32("be:value", cur_option); @@ -136,6 +192,21 @@ void HaikuPrefsWindow::MessageReceived(BMessage *msg) { if (std::string(setting) == "ui.haiku.label_setting") { new_label_setting = setting_value; set_options_changed(true); + } else if (std::string(setting) == "ui.cat") { + cat_id = setting_value; + set_options_changed(true); + } + } + } + } break; + case CMD_SET_SETTING_CHECKBOX: { + const char *setting; + if (msg->FindString("pref_path", &setting) == B_OK) { + int32 setting_value_int32; + if (msg->FindInt32("be:value", &setting_value_int32) == B_OK) { + if (std::string(setting) == "ui.enable_cat") { + enable_cat = setting_value_int32 == B_CONTROL_ON; + set_options_changed(true); } } } diff --git a/backends/ui/haiku/prefs.h b/backends/ui/haiku/prefs.h index eaa8fd4..5db6641 100644 --- a/backends/ui/haiku/prefs.h +++ b/backends/ui/haiku/prefs.h @@ -13,8 +13,17 @@ #include #include #include +#define CMD_SET_CAT 0x1010 class HaikuPrefsWindow : public BWindow { std::vector backend_ids; + std::string cat_id; + bool enable_cat; + std::map cats; + std::map cat_btns; + BCheckBox *cat_enable; + void AddCat(std:string name, BBitmap* img); + BBitmap *LoadCat(const char *path); + BBitmap *LoadCat(const void *ptr, size_t len, const char *name); int32 cur_option = 0; BLooper *next_handler; BStringView *restart_warning; diff --git a/backends/ui/imgui/RendererBackend.cpp b/backends/ui/imgui/RendererBackend.cpp index 96a8ee4..789f52d 100644 --- a/backends/ui/imgui/RendererBackend.cpp +++ b/backends/ui/imgui/RendererBackend.cpp @@ -329,8 +329,7 @@ void RendererBackend::BackendInit() { } #endif SDL_EventState(SDL_DROPFILE, SDL_ENABLE); - const vector icon_data = DecodeBase85(icon_compressed_data_base85); - SDL_Surface* icon = IMG_Load_RW(SDL_RWFromConstMem(icon_data.data(), icon_data.size()), 1); + SDL_Surface* icon = IMG_Load_RW(SDL_RWFromConstMem(icon_data, icon_size), 1); SDL_SetWindowIcon(window, icon); // Setup Dear ImGui context @@ -403,7 +402,8 @@ int RendererBackend::Run() { #else try { BackendInit(); - } catch (std::exception) { + } catch (std::exception &e) { + ERROR.writefln("Error occurred during initialization: %s", e.what()); return -1; } started = true; diff --git a/backends/ui/imgui/main.cpp b/backends/ui/imgui/main.cpp index 0a20c52..81a9b47 100644 --- a/backends/ui/imgui/main.cpp +++ b/backends/ui/imgui/main.cpp @@ -9,6 +9,7 @@ #include "thirdparty/CLI11.hpp" #include "imgui/misc/cpp/imgui_stdlib.h" #include +#include using namespace Looper::Options; void MainLoop::Init() { #ifdef PORTALS @@ -28,6 +29,25 @@ void MainLoop::Init() { theme_editor = false; stopped = true; about_window = false; + for (auto &cat : get_cat_data()) { + switch (cat.get_type()) { + case CatDataType::Memory: { + auto mem_cat = cat.get_memory_cat(); + AddCat(cat.get_name(), LoadCatFromMemory(mem_cat.get_ptr(), mem_cat.get_len(), cat.get_path().c_str())); + } break; + case CatDataType::File: { + fs::path cat_path = cat.get_path(); + try { + AddCat(cat.get_name(), LoadCat(cat_path.string())); + } catch (std::exception &e) { + WARNING.writefln("Failed to load cat %s at path %s: %s", cat_path.c_str(), cat.get_name().c_str(), e.what()); + } + } break; + default: { + WARNING.writefln("Invalid cat type with numerical value: %d", cat.get_type()); + } break; + } + } string lang; { Json::Value config; @@ -103,6 +123,11 @@ void MainLoop::Init() { accent_color.w = (float)get_option("ui.imgui.accent_color.a", accent_color.w); debug_mode = get_option("ui.imgui.debug_mode", false); } + { + enable_cat = get_option("ui.enable_cat", false); + cat_setting = get_option("ui.cat", cats.empty() ? "" : cats.begin()->first); + if (cats.contains(cat_setting)) cat = cats[cat_setting]; + } Theme::updateAvailableThemes(); if (Theme::availableThemes.empty()) { path lightPath = Theme::themeDir / "light.toml"; @@ -134,6 +159,44 @@ void MainLoop::Init() { void MainLoop::Drop(std::string file) { LoadFile(file); } +SDL_Texture *MainLoop::LoadCat(File *file) { + size_t pos = file->get_pos(); + file->seek(0, SeekType::SET); + std::string fname = file->name; + + SDL_RWops *rwops = get_sdl_file(file); + const char *ext = std::filesystem::path(fname).extension().c_str(); + DEBUG.writefln("Extension: %s\n", ext); + if (ext[0] == '.') ext = ext + 1; + SDL_Texture *tex = IMG_LoadTextureTyped_RW(rend, rwops, 0, ext); + delete rwops; + if (!tex) throw CustomException(fmt::format("Failed to load cat {}: {}", fname, IMG_GetError())); + file->seek(pos, SeekType::SET); + return tex; +} +SDL_Texture *MainLoop::LoadCat(std::string path) { + + std::string fname = path; + + SDL_RWops *rwops = SDL_RWFromFile(path.c_str(), "rb"); + SDL_Texture *tex = IMG_LoadTexture_RW(rend, rwops, 1); + if (!tex) throw CustomException(fmt::format("Failed to load cat {}: {}", fname, IMG_GetError())); + return tex; +} +SDL_Texture *MainLoop::LoadCatFromMemory(const void *ptr, size_t len, const char *name) { + std::string fname = name; + + SDL_RWops *rwops = SDL_RWFromConstMem(ptr, len); + SDL_Texture *tex = IMG_LoadTexture_RW(rend, rwops, 1); + if (!tex) throw CustomException(fmt::format("Failed to load cat {}: {}", fname, IMG_GetError())); + return tex; +} +void MainLoop::AddCat(std::string name, SDL_Texture *tex) { + if (cats.contains(name)) { + SDL_DestroyTexture(cats[name]); + } + cats[name] = tex; +} void MainLoop::FileLoaded() { auto file_maybe = playback->get_current_title(); if (file_maybe.has_value()) { @@ -216,6 +279,17 @@ void MainLoop::GuiFunction() { ImGui::SetNextWindowDockID(dockid); ImGui::Begin(_TRI_CTX(ICON_FK_PLAY, "Main window title", "Player"), nullptr, 0); { + ImGui::SetCursorPosY(0); + float y_pos = ImGui::GetWindowHeight() - ImGui::GetFrameHeightWithSpacing() - ImGui::GetFrameHeight() - ImGui::GetStyle().WindowPadding.y; + if (enable_cat && cat) { + int cw, ch; + SDL_QueryTexture(cat, NULL, NULL, &cw, &ch); + float aspect = ((float)cw) / ((float)ch); + float x_size = y_pos * aspect; + float x_pos = ImGui::GetWindowWidth() - ImGui::GetStyle().WindowPadding.x - x_size; + ImGui::SetCursorPosX(x_pos); + ImGui::Image((ImTextureID)cat, ImVec2(x_size, y_pos)); + } float centerSpace = ImGui::GetWindowHeight() - ImGui::GetFrameHeightWithSpacing() - ImGui::GetFrameHeight() - ImGui::GetStyle().WindowPadding.y; if (streams.size() > 0) { static string filter = ""; @@ -255,7 +329,7 @@ void MainLoop::GuiFunction() { } ImGui::EndChildFrame(); } - ImGui::SetCursorPosY(ImGui::GetWindowHeight() - ImGui::GetFrameHeightWithSpacing() - ImGui::GetFrameHeight() - ImGui::GetStyle().WindowPadding.y); + ImGui::SetCursorPosY(y_pos); if (ImGui::Button(playback->IsPaused() ? ICON_FK_PLAY "##Pause" : ICON_FK_PAUSE "##Pause")) { playback->Pause(); } @@ -517,6 +591,54 @@ void MainLoop::GuiFunction() { set_option("ui.imgui.lang", lang); } } + + static SDL_Texture *disp_cat = nullptr; + if (ImGui::Checkbox(_TR_CTX("Preference | cat enable checkbox", "Enable cat"), &enable_cat)) { + disp_cat = nullptr; + set_option("ui.enable_cat", enable_cat); + } + if (enable_cat) { + + ImVec2 TableSize = ImVec2(0, 0); + if (ImGui::BeginTable("##Cats", 2, ImGuiTableFlags_SizingFixedFit|ImGuiTableFlags_NoSavedSettings|ImGuiTableFlags_ScrollY, TableSize)) { + ImGui::TableSetupColumn("Cat Name", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("Preview", 0); + for (auto &kv : cats) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + if (ImGui::Selectable(kv.first.c_str(), kv.first == cat_setting, 0)) { + cat_setting = kv.first; + cat = cats[cat_setting]; + set_option("ui.cat", cat_setting); + } + ImGui::TableSetColumnIndex(1); + int cw, ch; + SDL_QueryTexture(kv.second, NULL, NULL, &cw, &ch); + float aspect = ((float)cw) / ((float)ch); + bool portrait = ch > cw; + ImVec2 size = ImVec2(16.0f * aspect, 16.0f); + if (ImGui::ImageButton(fmt::format("disp_cat_{}", kv.first).c_str(), (ImTextureID)kv.second, size)) { + if (disp_cat == kv.second) { + disp_cat = nullptr; + } else { + disp_cat = kv.second; + } + } + } + ImGui::EndTable(); + } + bool show_cat_preview = disp_cat != nullptr; + if (show_cat_preview && ImGui::Begin("Cat Preview", &show_cat_preview, ImGuiWindowFlags_NoSavedSettings|ImGuiWindowFlags_AlwaysAutoResize)) { + int cw, ch; + SDL_QueryTexture(disp_cat, NULL, NULL, &cw, &ch); + ImGui::Image((ImTextureID)disp_cat, ImVec2(cw, ch)); + ImGui::End(); + } + // Handle window close. + if (!show_cat_preview) { + disp_cat = nullptr; + } + } if (override_lang) { ImGui::SameLine(); ImGui::SetNextItemWidth(ImGui::GetWindowWidth() - ImGui::GetCursorPosX() - (ImGui::GetFontSize()) - ((ImGui::GetStyle().ItemSpacing.x + (ImGui::GetStyle().FramePadding.x * 2.0f))) - (ImGui::GetStyle().WindowPadding.x)); @@ -689,13 +811,18 @@ void MainLoop::LoadFile(std::string file) { playback->Start(file); } void MainLoop::Deinit() { - + for (auto kv : cats) { + SDL_DestroyTexture(kv.second); + } + cats.clear(); { path themePath(theme->file_path); themePath = themePath.stem(); if (!themePath.empty()) { set_option("ui.imgui.theme", themePath.string()); } + set_option("ui.enable_cat", enable_cat); + set_option("ui.cat", cat_setting); set_option("ui.imgui.accent_color.h", accent_color.x); set_option("ui.imgui.accent_color.s", accent_color.y); set_option("ui.imgui.accent_color.v", accent_color.z); @@ -731,7 +858,7 @@ void ImGuiUIBackend::QuitHandler() { // Main code int ImGuiUIBackend::run(std::vector realArgs, int argc, char** argv) { - SDL_setenv("SDL_VIDEO_X11_WMCLASS", "looper"); + SDL_setenv("SDL_VIDEO_X11_WMCLASS", "looper", 1); int possible_error = UIBackend::run(realArgs, argc, argv); if (possible_error != 0) { return possible_error; diff --git a/backends/ui/imgui/main.h b/backends/ui/imgui/main.h index 8a0d2bd..3f06cdd 100644 --- a/backends/ui/imgui/main.h +++ b/backends/ui/imgui/main.h @@ -36,6 +36,7 @@ using namespace std::filesystem; using std::string; #define IMGUI_FRONTEND + class MainLoop : public RendererBackend { bool show_demo_window = false; FileBrowser fileDialog = FileBrowser(false); @@ -49,6 +50,8 @@ class MainLoop : public RendererBackend { bool property_editor = false; bool restart_needed = false; bool stopped = true; + bool enable_cat = false; + std::string cat_setting = "__default__"; std::vector backends; UIBackend *cur_backend; friend class ImGuiUIBackend; @@ -59,6 +62,12 @@ class MainLoop : public RendererBackend { std::map string_properties; std::vector properties; std::vector streams; + std::map cats; + SDL_Texture *cat = nullptr; + SDL_Texture *LoadCatFromMemory(const void *ptr, size_t len, const char *name); + SDL_Texture *LoadCat(File *file); + SDL_Texture *LoadCat(std::string path); + void AddCat(std::string name, SDL_Texture *tex); public: Playback *playback; vector args; diff --git a/backends/ui/qt/main_window.cpp b/backends/ui/qt/main_window.cpp index eb82ad1..5c8e2e7 100644 --- a/backends/ui/qt/main_window.cpp +++ b/backends/ui/qt/main_window.cpp @@ -80,9 +80,11 @@ LooperWindow::LooperWindow(Playback *playback) : QMainWindow() { help_menu->addAction(about_item); bar->addMenu(file_menu); bar->addMenu(help_menu); - root_layout->addWidget(bar); QSpacerItem *spacer = new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::MinimumExpanding); root_layout->addSpacerItem(spacer); + cat_disp = new QLabel(); + cat_disp->setAlignment(Qt::Alignment::enum_type::AlignRight); + root_layout->addWidget(cat_disp); QBoxLayout *top_row = new QBoxLayout(QBoxLayout::LeftToRight, this); pause_resume_btn = new QPushButton("Pause", this); QObject::connect(pause_resume_btn, &QPushButton::pressed, [=,this]() { @@ -146,6 +148,8 @@ LooperWindow::LooperWindow(Playback *playback) : QMainWindow() { timer->setInterval(1); timer->start(); QObject::connect(prefs_window, &PrefsWindow::settings_changed, this, &LooperWindow::update_label_setting); + QObject::connect(prefs_window, &PrefsWindow::cat_set, this, &LooperWindow::update_cat); + QObject::connect(prefs_window, &PrefsWindow::cat_unset, this, &LooperWindow::clear_cat); //setLayout(layout); } void LooperWindow::update_label_setting(bool labels_visible, bool icons_visible) { @@ -173,3 +177,11 @@ void LooperWindow::update_label_setting(bool labels_visible, bool icons_visible) } } } + +void LooperWindow::clear_cat() { + cat_disp->hide(); +} +void LooperWindow::update_cat(QPixmap &img) { + cat_disp->setPixmap(img); + cat_disp->show(); +} diff --git a/backends/ui/qt/main_window.h b/backends/ui/qt/main_window.h index 2eb3e1b..68c5fce 100644 --- a/backends/ui/qt/main_window.h +++ b/backends/ui/qt/main_window.h @@ -41,7 +41,10 @@ class LooperWindow : public QMainWindow { QAction *about_item; QFileDialog *file_dialog; QBoxLayout *root_layout; + QLabel *cat_disp; void update_label_setting(bool labels_visible, bool icons_visible); + void update_cat(QPixmap &img); + void clear_cat(); public: AboutWindow *about_window; PrefsWindow *prefs_window; diff --git a/backends/ui/qt/preferences.cpp b/backends/ui/qt/preferences.cpp index 1d79225..daf8171 100644 --- a/backends/ui/qt/preferences.cpp +++ b/backends/ui/qt/preferences.cpp @@ -1,10 +1,35 @@ #include "preferences.h" #include #include +#include +#include +#include #include #include +#include +#include using namespace Looper::Options; PrefsWindow::PrefsWindow() { + for (auto &cat : get_cat_data()) { + switch (cat.get_type()) { + case CatDataType::Memory: { + QImage image; + QBuffer buffer; + QDataStream in(&buffer); + auto mem_cat = cat.get_memory_cat(); + buffer.setData(const_cast((const char*)mem_cat.get_ptr()), mem_cat.get_len()); + in >> image; + cats[cat.get_name()] = QPixmap::fromImage(image); + } break; + case CatDataType::File: { + try { + cats[cat.get_name()] = QPixmap(cat.get_path().c_str()); + } catch (std::exception e) { + WARNING.writefln("Failed to load cat %s at path %s: %s", cat.get_name().c_str(), cat.get_path().c_str(), e.what()); + } + } break; + } + } auto *root_layout = new QBoxLayout(QBoxLayout::TopToBottom, this); this->setLayout(root_layout); restart_warning = new QLabel("A restart is needed to apply some changes.", this); @@ -49,6 +74,31 @@ PrefsWindow::PrefsWindow() { label_settings_group->addWidget(icons_only); label_settings_group->addWidget(both_labels_icons); root_layout->addWidget(frame); + cat_enable = new QCheckBox("Enable Cat", this); + QObject::connect(cat_enable, &QCheckBox::pressed, [=,this]() { + this->enable_cat = cat_enable->isChecked(); + this->set_options_changed(true); + }); + root_layout->addWidget(cat_enable); + QFrame *catFrame = new QFrame(this); + catFrame->setWindowTitle("Cat Selection"); + auto *cat_btns_layout = new QBoxLayout(QBoxLayout::TopToBottom, this); + catFrame->setLayout(cat_btns_layout); + for (auto &kv : cats) { + auto id = kv.first; + auto pixmap = kv.second; + QRadioButton *cat_radio = new QRadioButton(id.c_str(), catFrame); + QLabel *cat_img = new QLabel(cat_radio); + cat_img->setPixmap(pixmap); + cat_img->setAlignment(Qt::Alignment::enum_type::AlignRight); + QObject::connect(cat_radio, &QRadioButton::pressed, [=,this]() { + this->cat_setting = id; + this->set_options_changed(true); + }); + cat_btns_layout->addWidget(cat_radio); + cat_btns[id] = cat_radio; + } + root_layout->addWidget(catFrame); QWidget *btn_view = new QWidget(this); QBoxLayout *btn_box = new QBoxLayout(QBoxLayout::LeftToRight, this); revert_btn = new QPushButton("Revert", btn_view); @@ -83,6 +133,21 @@ void PrefsWindow::revert() { if (new_frontend != "qt") restart_warning->show(); else restart_warning->hide(); frontend_btn->setText(new_frontend.c_str()); + enable_cat = get_option("ui.enable_cat"); + cat_enable->setCheckState(enable_cat ? Qt::CheckState::Checked : Qt::CheckState::Unchecked); + cat_setting = get_option("ui.cat", cats.empty() ? "" : cats.begin()->first); + std::map radio_btn_values = {{labels_only, false}, {icons_only, false}, {both_labels_icons, false}}; + if (new_label_setting == "labels") { + radio_btn_values[labels_only] = true; + } else if (new_label_setting == "icons") { + radio_btn_values[icons_only] = true; + } else if (new_label_setting == "both") { + radio_btn_values[both_labels_icons] = true; + } + for (auto &kv : radio_btn_values) { + kv.first->setDown(kv.second); + } + if (cat_btns.contains(cat_setting)) cat_btns[cat_setting]->setDown(true); update_label_setting(); } void PrefsWindow::apply() { @@ -93,4 +158,8 @@ void PrefsWindow::apply() { else restart_warning->hide(); frontend_btn->setText(new_frontend.c_str()); update_label_setting(); + set_option("ui.enable_cat", enable_cat); + set_option("ui.cat", cat_setting); + if (enable_cat && cats.contains(cat_setting)) emit(cat_set(cats[cat_setting])); + else emit(cat_unset()); } diff --git a/backends/ui/qt/preferences.h b/backends/ui/qt/preferences.h index 5dad13a..cfb6491 100644 --- a/backends/ui/qt/preferences.h +++ b/backends/ui/qt/preferences.h @@ -18,12 +18,17 @@ class PrefsWindow : public QWidget { QPushButton *frontend_btn; QMenu *frontend_menu; std::vector frontend_options; + std::map cats; + std::map cat_btns; QCheckBox *menu_icons; QRadioButton *labels_only; QRadioButton *icons_only; QRadioButton *both_labels_icons; QPushButton *revert_btn; QPushButton *apply_btn; + QCheckBox *cat_enable; + bool enable_cat; + std::string cat_setting; void update_label_setting(); void set_options_changed(bool changed); void revert(); @@ -31,5 +36,7 @@ class PrefsWindow : public QWidget { public: PrefsWindow(); Q_SIGNALS: + void cat_set(QPixmap &img); + void cat_unset(); void settings_changed(bool use_labels, bool use_icons); -}; \ No newline at end of file +}; diff --git a/base85.cpp b/base85.cpp index e6cfe39..29f822e 100644 --- a/base85.cpp +++ b/base85.cpp @@ -4,10 +4,11 @@ using std::vector; static unsigned int DecodeBase85Byte(char c) { return c >= '\\' ? c-36 : c-35; } -vector DecodeBase85(const char *src) { - vector dst_vec; - dst_vec.resize(((strlen(src) + 4) / 5) * 4); - unsigned char *dst = dst_vec.data(); +vector *DecodeBase85(const char *src) { + vector *dst_vec = new vector(); + dst_vec->resize(((strlen(src) + 4) / 5) * 4); + unsigned char *dst = dst_vec->data(); + memset(dst, 0, dst_vec->size()); size_t dst_size = 0; while (*src) { @@ -17,6 +18,6 @@ vector DecodeBase85(const char *src) { dst += 4; dst_size += 4; } - dst_vec.resize(dst_size); + dst_vec->resize(dst_size); return dst_vec; } \ No newline at end of file diff --git a/base85.h b/base85.h index f88ec62..bdfd7df 100644 --- a/base85.h +++ b/base85.h @@ -3,8 +3,4 @@ #include #include // Modified from Dear ImGui -std::vector DecodeBase85(const char *src); -// May not be needed now, but could be useful in the future. -static inline std::vector DecodeBase85(const std::string src) { - return DecodeBase85(src.c_str()); -} \ No newline at end of file +std::vector *DecodeBase85(const char *src); \ No newline at end of file diff --git a/cats.hpp b/cats.hpp new file mode 100644 index 0000000..498582c --- /dev/null +++ b/cats.hpp @@ -0,0 +1,88 @@ +#pragma once +#include +#include +#include +#include +namespace fs = std::filesystem; +class MemoryCat { + const void *ptr; + size_t len; + bool owned; + public: + inline size_t get_len() { + return len; + } + inline const void *get_ptr() { + return (const void*)ptr; + } + inline ~MemoryCat() { + if (owned) free((void*)ptr); + } + /// \brief Constructor that doesn't take ownership of the pointer + inline MemoryCat(const void *ptr, size_t len) { + this->owned = false; + this->ptr = ptr; + this->len = len; + } + /// \brief Constructor that takes ownership of the pointer + inline MemoryCat(void *ptr, size_t len) { + this->owned = true; + this->ptr = ptr; + this->len = len; + } +}; +enum class CatDataType { + Memory, + File +}; +class CatData { + std::optional mem; + fs::path path; + std::string name; + CatDataType type; + public: + inline std::string get_name() { + return name; + } + inline CatDataType get_type() { + return type; + } + inline fs::path get_path() { + return path; + } + inline MemoryCat get_memory_cat() { + if (type == CatDataType::Memory) { + return mem.value(); + } else { + throw std::exception(); + } + } + inline MemoryCat as_memory_cat() { + if (mem.has_value()) return mem.value(); + if (type == CatDataType::File) { + FILE *file = fopen(path.c_str(), "rb"); + fseek(file, 0, SEEK_END); + size_t len = ftell(file); + fseek(file, 0, SEEK_SET); + void *ptr = malloc(len); + len = fread(ptr, 1, len, file); + ptr = realloc(ptr, len); + MemoryCat output(ptr, len); + this->mem = output; + return output; + } + throw std::exception(); + } + inline CatData(const void *ptr, size_t len, std::string name, fs::path virtual_path) { + this->type = CatDataType::Memory; + this->name = name; + this->path = virtual_path; + this->mem = MemoryCat(ptr, len); + } + inline CatData(fs::path path) { + this->name = path.stem(); + this->type = CatDataType::File; + this->path = path; + } +}; +extern std::vector &get_cat_data(); \ No newline at end of file diff --git a/file_backend.cpp b/file_backend.cpp index 66aa6c9..e5acbd8 100644 --- a/file_backend.cpp +++ b/file_backend.cpp @@ -1,6 +1,7 @@ #include "file_backend.hpp" #include #include +#include File::File() { } @@ -56,9 +57,18 @@ bool CFile::is_open() { return file != NULL; } MemFile::MemFile() { + memory_owned = false; ptr = NULL; len = 0; } +void MemFile::open_memory(void *ptr, size_t len, const char *name) { + assert(name != NULL); + this->name = strdup(name); + this->ptr = ptr; + this->len = len; + this->pos = 0; + memory_owned = false; +} void MemFile::open(const char *path) { CFile file; file.open(path); @@ -67,9 +77,10 @@ void MemFile::open(const char *path) { this->len = file.read(this->ptr, 1, file.get_len()); file.close(); this->pos = 0; + memory_owned = true; } void MemFile::close() { - free(this->ptr); + if (memory_owned) free(this->ptr); free((void*)this->name); this->ptr = NULL; this->name = NULL; diff --git a/file_backend.hpp b/file_backend.hpp index d9431e8..62a950f 100644 --- a/file_backend.hpp +++ b/file_backend.hpp @@ -93,8 +93,10 @@ class MemFile : public File { void *ptr; size_t len; int64_t pos; + bool memory_owned = true; public: MemFile(); + void open_memory(void *ptr, size_t len, const char *name = ""); void open(const char *path) override; void close() override; size_t read(void *ptr, size_t size, size_t len) override; diff --git a/main.cpp b/main.cpp index 371352e..1ee8985 100644 --- a/main.cpp +++ b/main.cpp @@ -16,6 +16,7 @@ #include #endif #include "web_functions.hpp" +#include "cats.hpp" using namespace Looper; using namespace Looper::Options; using namespace Looper::Log; @@ -24,6 +25,10 @@ extern "C" { void quit(); } #endif +std::vector &get_cat_data() { + static std::vector data; + return data; +} std::unordered_set license_data; std::unordered_set &get_license_data() { return license_data; @@ -175,6 +180,21 @@ extern "C" int looper_run_as_executable(std::vector args) { } DEBUG.writeln("Loading options file..."); load_options(); + { + auto &cat_data = get_cat_data(); + cat_data.push_back(CatData(catoc_data, catoc_size, "Cat OC (Built-In)", "catoc.png")); + #ifndef __EMSCRIPTEN__ + fs::path baseDir = get_prefs_path() / fs::path("looper") / fs::path("themes"); + if (!fs::exists(baseDir)) { + fs::create_directories(baseDir); + } + for (auto const&dir_entry : std::filesystem::directory_iterator(baseDir)) { + if (dir_entry.is_regular_file()) { + cat_data.push_back(CatData(dir_entry.path())); + } + } + #endif + } std::string backend_id = get_option("ui.frontend", "imgui"); UIBackend *backend = UIBackend::get_backend(ui_backend_option).value_or(UIBackend::get_backend(backend_id).value_or(UIBackend::get_first_backend())); int output = 0;