Add TOML support and playback error handling.

This commit is contained in:
Zachary Hall 2023-12-22 14:13:48 -08:00
parent 7edd8bbc87
commit 893cd8f9d4
No known key found for this signature in database
GPG key ID: 87248117B6C9569A
12 changed files with 18097 additions and 203 deletions

View file

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/neko-player.iml" filepath="$PROJECT_DIR$/.idea/neko-player.iml" />
</modules>
</component>
</project>

View file

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="CPP_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View file

@ -6,5 +6,8 @@
<mapping directory="$PROJECT_DIR$/imgui" vcs="Git" /> <mapping directory="$PROJECT_DIR$/imgui" vcs="Git" />
<mapping directory="$PROJECT_DIR$/subprojects/SDL-Mixer-X" vcs="Git" /> <mapping directory="$PROJECT_DIR$/subprojects/SDL-Mixer-X" vcs="Git" />
<mapping directory="$PROJECT_DIR$/subprojects/jsoncpp" vcs="Git" /> <mapping directory="$PROJECT_DIR$/subprojects/jsoncpp" vcs="Git" />
<mapping directory="$PROJECT_DIR$/subprojects/vgmstream" vcs="Git" />
<mapping directory="$PROJECT_DIR$/subprojects/vgmstream/dependencies/LibAtrac9" vcs="Git" />
<mapping directory="$PROJECT_DIR$/subprojects/vgmstream/dependencies/libg719_decode" vcs="Git" />
</component> </component>
</project> </project>

View file

@ -0,0 +1,16 @@
MIT License
Copyright (c) Mark Gillard <mark.gillard@outlook.com.au>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -35,6 +35,7 @@ add_basic 'licenses/libportal.txt' 'libportal_license'
add_basic 'licenses/ForkAwesome.txt' 'forkawesome_license' add_basic 'licenses/ForkAwesome.txt' 'forkawesome_license'
add_basic 'licenses/libintl.txt' 'libintl_license' add_basic 'licenses/libintl.txt' 'libintl_license'
add_basic 'licenses/cli11.txt' 'cli11_license' add_basic 'licenses/cli11.txt' 'cli11_license'
add_basic 'licenses/TomlPlusPlus.txt' 'tomlplusplus_license'
add_basic '../IconFontCppHeaders/licence.txt' 'icnfntcpphdrs_license' add_basic '../IconFontCppHeaders/licence.txt' 'icnfntcpphdrs_license'
echo '#pragma once' > 'assets.h' echo '#pragma once' > 'assets.h'
for i in "${ASSETS[@]}"; do for i in "${ASSETS[@]}"; do

View file

@ -335,7 +335,8 @@ void MainLoop::GuiFunction() {
#endif #endif
LicenseData(_TR_CTX("Library name", "Noto Sans"), "OFL-1.1-RFN"), LicenseData(_TR_CTX("Library name", "Noto Sans"), "OFL-1.1-RFN"),
LicenseData(_TR_CTX("Library name", "Fork Awesome"), "OFL-1.1-RFN"), LicenseData(_TR_CTX("Library name", "Fork Awesome"), "OFL-1.1-RFN"),
LicenseData(_TR_CTX("Library name", "IconFontCppHeaders"), "Zlib") LicenseData(_TR_CTX("Library name", "IconFontCppHeaders"), "Zlib"),
LicenseData(_TR_CTX("Library name", "TOML++"), "MIT")
}; };
// Do this in an inner scope so that 'i' isn't accidentally used outside it, // Do this in an inner scope so that 'i' isn't accidentally used outside it,
// and so that 'i' can refer to another variable such as in a for loop. // and so that 'i' can refer to another variable such as in a for loop.
@ -356,6 +357,7 @@ void MainLoop::GuiFunction() {
LOAD_LICENSE(projects[i], notosans); i++; LOAD_LICENSE(projects[i], notosans); i++;
LOAD_LICENSE(projects[i], forkawesome); i++; LOAD_LICENSE(projects[i], forkawesome); i++;
LOAD_LICENSE(projects[i], icnfntcpphdrs); i++; LOAD_LICENSE(projects[i], icnfntcpphdrs); i++;
LOAD_LICENSE(projects[i], tomlplusplus); i++;
} }
// Left // Left
static LicenseData selected = projects[0]; static LicenseData selected = projects[0];

View file

@ -62,6 +62,7 @@ if get_option('portals') and target_machine.system() == 'linux'
endif endif
srcs = [ srcs = [
'IconSet.cpp',
'main.cpp', 'main.cpp',
'RendererBackend.cpp', 'RendererBackend.cpp',
'playback.cpp', 'playback.cpp',

View file

@ -16,26 +16,27 @@ void Playback::SDLCallbackInner(Uint8 *stream, int len) {
SDL_memset((void*)stream, 0, len); SDL_memset((void*)stream, 0, len);
if (!playback_ready.load()) { if (!playback_ready.load()) {
return; return;
}
if (st == nullptr) {
return;
} }
size_t i = 0; size_t i = 0;
size_t max = 0; size_t max = 0;
size_t unit = sizeof(SAMPLETYPE) * spec.channels; size_t unit = sizeof(SAMPLETYPE) * spec.channels;
size_t bytes_per_iter = std::min(((bufsize / unit)) * unit, (size_t)fakespec.size); size_t bytes_per_iter = std::min(((bufsize / unit)) * unit, (size_t)fakespec.size);
while (st->numSamples() <= (uint)len) { while (st->numSamples() < (size_t)len) {
if (general_mixer == nullptr) { if (general_mixer == nullptr) {
return; return;
} }
general_mixer(NULL, buf + i, bytes_per_iter); general_mixer(nullptr, buf + i, (int)bytes_per_iter);
i += bytes_per_iter; i += bytes_per_iter;
max = i + bytes_per_iter; max = i + bytes_per_iter;
if (i + bytes_per_iter >= bufsize) { if (max >= bufsize) {
st->putSamples((SAMPLETYPE*)buf, i / unit); st->putSamples((SAMPLETYPE*)buf, i/unit);
max = 0;
i = 0; i = 0;
SDL_memset((void*)buf, 0, bufsize); max = i + bytes_per_iter;
} }
} }
st->putSamples((SAMPLETYPE*)buf, max / unit);
st->receiveSamples((SAMPLETYPE*)stream, len / unit); st->receiveSamples((SAMPLETYPE*)stream, len / unit);
} }
void Playback::SDLCallback(void *userdata, Uint8 *stream, int len) { void Playback::SDLCallback(void *userdata, Uint8 *stream, int len) {
@ -43,9 +44,12 @@ void Playback::SDLCallback(void *userdata, Uint8 *stream, int len) {
} }
Mix_Music *Playback::Load(const char *file) { Mix_Music *Playback::Load(const char *file) {
Mix_Music *output = Mix_LoadMUS(file); Mix_Music *output = Mix_LoadMUS(file);
if (!output) { if (output == nullptr) {
printf("Error loading music '%s': %s\n", file, Mix_GetError()); printf("Error loading music '%s': %s\n", file, Mix_GetError());
throw std::exception(); error_mutex.lock();
errors.emplace("Error loading music!");
error_mutex.unlock();
return nullptr;
} }
Mix_PlayMusicStream(output, -1); Mix_PlayMusicStream(output, -1);
length = Mix_MusicDuration(output); length = Mix_MusicDuration(output);
@ -57,18 +61,22 @@ void Playback::Unload(Mix_Music *music) {
Mix_FreeMusic(music); Mix_FreeMusic(music);
} }
void Playback::UpdateST() { void Playback::UpdateST() {
if (speed > 0.0f) { if (speed > 0.0f) {
pitch = std::max(std::min(speed, MaxSpeed), MinSpeed);
st->setRate(speed); st->setRate(speed);
} }
if (tempo > 0.0f) { if (tempo > 0.0f) {
pitch = std::max(std::min(tempo, MaxTempo), MinTempo);
st->setTempo(tempo); st->setTempo(tempo);
} }
if (pitch > 0.0f) { if (pitch > 0.0f) {
pitch = std::max(std::min(pitch, MaxPitch), MinPitch);
st->setPitch(pitch); st->setPitch(pitch);
} }
} }
double Playback::GetMaxSeconds() { double Playback::GetMaxSeconds() {
return std::max(MaxSpeed * MaxTempo, st->getInputOutputSampleRatio()); return std::max((double)(MaxSpeed * MaxTempo), st->getInputOutputSampleRatio());
} }
void Playback::ThreadFunc() { void Playback::ThreadFunc() {
#ifdef __linux__ #ifdef __linux__
@ -77,13 +85,13 @@ void Playback::ThreadFunc() {
bool reload = false; bool reload = false;
while (running) { while (running) {
playback_ready.store(false); playback_ready.store(false);
if (reload) {
printf("Resuming playback...\n");
}
if (!SDL_WasInit(SDL_INIT_AUDIO)) { if (!SDL_WasInit(SDL_INIT_AUDIO)) {
if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) { if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) {
printf("Error initializing SDL: '%s'\n", SDL_GetError()); printf("Error initializing SDL: '%s'\n", SDL_GetError());
throw std::exception(); error_mutex.lock();
errors.emplace("Failed to initialize SDL!");
error_mutex.unlock();
return;
} }
} }
SDL_AudioSpec obtained; SDL_AudioSpec obtained;
@ -101,9 +109,13 @@ void Playback::ThreadFunc() {
desired.userdata = this; desired.userdata = this;
st = new SoundTouch(); st = new SoundTouch();
Mix_Init(MIX_INIT_FLAC|MIX_INIT_MID|MIX_INIT_MOD|MIX_INIT_MP3|MIX_INIT_OGG|MIX_INIT_OPUS|MIX_INIT_WAVPACK); Mix_Init(MIX_INIT_FLAC|MIX_INIT_MID|MIX_INIT_MOD|MIX_INIT_MP3|MIX_INIT_OGG|MIX_INIT_OPUS|MIX_INIT_WAVPACK);
if ((device = SDL_OpenAudioDevice(NULL, 0, &desired, &obtained, SDL_AUDIO_ALLOW_CHANNELS_CHANGE|SDL_AUDIO_ALLOW_FREQUENCY_CHANGE|SDL_AUDIO_ALLOW_SAMPLES_CHANGE)) == 0) { if ((device = SDL_OpenAudioDevice(nullptr, 0, &desired, &obtained, SDL_AUDIO_ALLOW_CHANNELS_CHANGE|SDL_AUDIO_ALLOW_FREQUENCY_CHANGE|SDL_AUDIO_ALLOW_SAMPLES_CHANGE)) == 0) {
printf("Error opening audio device: '%s'\n", SDL_GetError()); printf("Error opening audio device: '%s'\n", SDL_GetError());
throw std::exception(); error_mutex.lock();
errors.emplace("Failed to open audio device!");
error_mutex.unlock();
running = false;
break;
} }
spec = obtained; spec = obtained;
st->setSampleRate(spec.freq); st->setSampleRate(spec.freq);
@ -116,23 +128,33 @@ void Playback::ThreadFunc() {
fakespec.samples *= maxSeconds; fakespec.samples *= maxSeconds;
size_t new_bufsize = CalculateBufSize(&spec, GetMaxSeconds(), MaxSeconds); size_t new_bufsize = CalculateBufSize(&spec, GetMaxSeconds(), MaxSeconds);
buf = (Uint8*)malloc(new_bufsize); buf = (Uint8*)malloc(new_bufsize);
if (buf == NULL) { if (buf == nullptr) {
throw std::exception(); error_mutex.lock();
errors.emplace("Failed to allocate memory for playback!");
error_mutex.unlock();
running = false;
break;
} }
bufsize = new_bufsize; bufsize = new_bufsize;
general_mixer = Mix_GetGeneralMixer(); general_mixer = Mix_GetGeneralMixer();
Mix_InitMixer(&fakespec, SDL_TRUE); Mix_InitMixer(&fakespec, SDL_FALSE);
SDL_PauseAudioDevice(device, 0); SDL_PauseAudioDevice(device, 0);
Mix_Music *music = Load(filePath.c_str()); Mix_Music *music = Load(filePath.c_str());
if (reload) {
Seek(position);
}
reload = false; reload = false;
if (music) {
playback_ready.store(true); playback_ready.store(true);
while (running && !reload) { } else {
playback_ready.store(false);
}
while (running) {
if (file_changed.exchange(false)) { if (file_changed.exchange(false)) {
Unload(music); Unload(music);
music = Load(filePath.c_str()); music = Load(filePath.c_str());
if (music) {
playback_ready.store(true);
} else {
playback_ready.store(false);
}
} }
if (flag_mutex.try_lock()) { if (flag_mutex.try_lock()) {
if (seeking.exchange(false)) { if (seeking.exchange(false)) {
@ -144,7 +166,7 @@ void Playback::ThreadFunc() {
Mix_ResumeMusicStream(music); Mix_ResumeMusicStream(music);
} }
if (update.exchange(false)) { if (update.exchange(false)) {
Mix_VolumeMusicStream(music, (volume / 100.0 * MIX_MAX_VOLUME)); Mix_VolumeMusicStream(music, (int)(volume / 100.0 * MIX_MAX_VOLUME));
SDL_LockAudioDevice(device); SDL_LockAudioDevice(device);
UpdateST(); UpdateST();
size_t correct_buf_size = CalculateBufSize(&spec, GetMaxSeconds(), MaxSeconds); size_t correct_buf_size = CalculateBufSize(&spec, GetMaxSeconds(), MaxSeconds);
@ -161,7 +183,11 @@ void Playback::ThreadFunc() {
general_mixer = nullptr; general_mixer = nullptr;
bufsize = 0; bufsize = 0;
buf = (Uint8*)realloc((void*)buf, correct_buf_size); buf = (Uint8*)realloc((void*)buf, correct_buf_size);
if (buf == NULL) { if (buf == nullptr) {
error_mutex.lock();
errors.emplace("Failed to allocate memory for playback!");
error_mutex.unlock();
running = false;
break; break;
} }
bufsize = correct_buf_size; bufsize = correct_buf_size;
@ -251,3 +277,20 @@ void Playback::Update() {
bool Playback::IsStopped() { bool Playback::IsStopped() {
return !running; return !running;
} }
optional<std::string> Playback::GetError() {
if (ErrorExists()) {
error_mutex.lock();
std::string error = errors.back();
errors.pop();
error_mutex.unlock();
return error;
} else {
return {};
}
}
bool Playback::ErrorExists() {
error_mutex.lock();
bool output = !errors.empty();
error_mutex.unlock();
return output;
}

View file

@ -10,9 +10,13 @@
#include <SoundTouch.h> #include <SoundTouch.h>
#include <span> #include <span>
#include <optional> #include <optional>
#include <vector>
#include <queue>
using namespace soundtouch; using namespace soundtouch;
using std::span; using std::span;
using std::optional; using std::optional;
using std::vector;
using std::queue;
class Playback { class Playback {
private: private:
std::string filePath; std::string filePath;
@ -23,6 +27,7 @@ private:
std::atomic_bool restart; std::atomic_bool restart;
std::atomic_bool playback_ready; std::atomic_bool playback_ready;
std::mutex flag_mutex; std::mutex flag_mutex;
std::mutex error_mutex;
std::thread thread; std::thread thread;
double position; double position;
double length; double length;
@ -42,6 +47,7 @@ private:
void ThreadFunc(); void ThreadFunc();
void UpdateST(); void UpdateST();
double GetMaxSeconds(); double GetMaxSeconds();
queue<std::string> errors;
public: public:
Playback(); Playback();
~Playback(); ~Playback();
@ -58,11 +64,13 @@ public:
float speed; float speed;
float tempo; float tempo;
float pitch; float pitch;
double MaxSeconds = 100.0; float MaxSeconds = 100.0;
double MaxSpeed = 4.0; float MaxSpeed = 4.0;
double MaxPitch = 4.0; float MaxPitch = 4.0;
double MaxTempo = 4.0; float MaxTempo = 4.0;
double MinSpeed = 0.25; float MinSpeed = 0.25;
double MinPitch = 0.25; float MinPitch = 0.25;
double MinTempo = 0.25; float MinTempo = 0.25;
optional<std::string> GetError();
bool ErrorExists();
}; };

374
theme.cpp
View file

@ -1,6 +1,7 @@
#include "theme.h" #include "theme.h"
#include "imgui.h" #include "imgui.h"
#include "json/value.h" #include "json/value.h"
#include "thirdparty/toml.hpp"
#include "translation.h" #include "translation.h"
#include <cmath> #include <cmath>
#include <exception> #include <exception>
@ -303,7 +304,7 @@ bool Theme::ShowEditor(bool* open, Theme* &theme, ImGuiID dockid, int window_wid
selectedThemeName = ""; selectedThemeName = "";
filter = ""; filter = "";
saveAsOpen = false; saveAsOpen = false;
theme->Save(Theme::themeDir / selectedThemePath.replace_extension(".json")); theme->Save(Theme::themeDir / selectedThemePath.replace_extension(".toml"));
theme->file_path = selectedThemePath.generic_string(); theme->file_path = selectedThemePath.generic_string();
} }
} }
@ -324,25 +325,18 @@ AccentColorizer::AccentColorizer() {
Value = false; Value = false;
Alpha = false; Alpha = false;
} }
AccentColorizer::AccentColorizer(Json::Value json) : AccentColorizer() { AccentColorizer::AccentColorizer(toml::table table) : AccentColorizer() {
if (json.isBool()) { Hue = **table["hue"].as_boolean();
Hue = json.asBool(); Saturation = **table["saturation"].as_boolean();
Saturation = Hue; Value = **table["value"].as_boolean();
Value = Hue; Alpha = **table["alpha"].as_boolean();
Alpha = Hue;
} else {
Hue = json["hue"].asBool();
Saturation = json["saturation"].asBool();
Value = json["value"].asBool();
Alpha = json["alpha"].asBool();
}
} }
Json::Value AccentColorizer::Serialize() { toml::table AccentColorizer::Serialize() {
Json::Value output; toml::table output;
output["hue"] = Hue; output.insert("hue", Hue);
output["saturation"] = Saturation; output.insert("saturation", Saturation);
output["value"] = Value; output.insert("value", Value);
output["alpha"] = Alpha; output.insert("alpha", Alpha);
return output; return output;
} }
void AccentColorizer::Colorize(ImVec4 accent, ImVec4 &color) { void AccentColorizer::Colorize(ImVec4 accent, ImVec4 &color) {
@ -375,79 +369,78 @@ void Theme::Apply(ImVec4 accent) {
void Theme::Save(string path) { void Theme::Save(string path) {
printf("Saving theme to %s...\n", path.c_str()); printf("Saving theme to %s...\n", path.c_str());
{ {
Json::Value config; toml::table config;
std::ofstream stream; std::ofstream stream;
stream.open(path); stream.open(path);
{ {
Json::Value metadata; toml::table metadata;
metadata["SchemaVersion"] = 2; metadata.insert("SchemaVersion", 3);
{ {
Json::Value stringsList; toml::table stringsList;
for (auto kv : strings) { for (auto kv : strings) {
Json::Value stringsEntryJson; toml::table stringsEntryJson;
string language = kv.first; string language = kv.first;
ThemeStrings stringsEntry = kv.second; ThemeStrings stringsEntry = kv.second;
stringsEntryJson["Name"] = stringsEntry.name; stringsEntryJson.insert("name", stringsEntry.name);
stringsEntryJson["Description"] = stringsEntry.description; stringsEntryJson.insert("desc", stringsEntry.description);
stringsList[language] = stringsEntryJson; stringsList.insert(language, stringsEntryJson);
} }
metadata["Strings"] = stringsList; metadata.insert("Strings", stringsList);
} }
config["meta"] = metadata; config.insert("meta", metadata);
} }
{ {
Json::Value rounding; toml::table rounding;
rounding["Frame"] = style.FrameRounding; rounding.insert("Frame", style.FrameRounding);
rounding["Window"] = style.WindowRounding; rounding.insert("Window", style.WindowRounding);
rounding["Child"] = style.ChildRounding; rounding.insert("Child", style.ChildRounding);
rounding["Popup"] = style.PopupRounding; rounding.insert("Popup", style.PopupRounding);
rounding["Scrollbar"] = style.ScrollbarRounding; rounding.insert("Scrollbar", style.ScrollbarRounding);
rounding["Grab"] = style.GrabRounding; rounding.insert("Grab", style.GrabRounding);
rounding["Tab"] = style.TabRounding; rounding.insert("Tab", style.TabRounding);
config["rounding"] = rounding; config.insert("rounding", rounding);
} }
{ {
Json::Value sizing; toml::table sizing;
sizing["FrameX"] = style.FramePadding.x; sizing.insert("FrameX", style.FramePadding.x);
sizing["FrameY"] = style.FramePadding.y; sizing.insert("FrameY", style.FramePadding.y);
sizing["WindowX"] = style.WindowPadding.x; sizing.insert("WindowX", style.WindowPadding.x);
sizing["WindowY"] = style.WindowPadding.y; sizing.insert("WindowY", style.WindowPadding.y);
sizing["CellX"] = style.CellPadding.x; sizing.insert("CellX", style.CellPadding.x);
sizing["CellY"] = style.CellPadding.y; sizing.insert("CellY", style.CellPadding.y);
sizing["SeparatorTextX"] = style.SeparatorTextPadding.x; sizing.insert("SeparatorTextX", style.SeparatorTextPadding.x);
sizing["SeparatorTextY"] = style.SeparatorTextPadding.y; sizing.insert("SeparatorTextY", style.SeparatorTextPadding.y);
sizing["ItemSpacingX"] = style.ItemSpacing.x; sizing.insert("ItemSpacingX", style.ItemSpacing.x);
sizing["ItemSpacingY"] = style.ItemSpacing.y; sizing.insert("ItemSpacingY", style.ItemSpacing.y);
sizing["Scrollbar"] = style.ScrollbarSize; sizing.insert("Scrollbar", style.ScrollbarSize);
sizing["Grab"] = style.GrabMinSize; sizing.insert("Grab", style.GrabMinSize);
config["sizing"] = sizing; config.insert("sizing", sizing);
} }
{ {
Json::Value borders; toml::table borders;
borders["Frame"] = style.FrameBorderSize; borders.insert("Frame", style.FrameBorderSize);
borders["Window"] = style.WindowBorderSize; borders.insert("Window", style.WindowBorderSize);
borders["Child"] = style.ChildBorderSize; borders.insert("Child", style.ChildBorderSize);
borders["Popup"] = style.PopupBorderSize; borders.insert("Popup", style.PopupBorderSize);
borders["Tab"] = style.TabBorderSize; borders.insert("Tab", style.TabBorderSize);
borders["Tab"] = style.TabBorderSize; borders.insert("SeparatorText", style.SeparatorTextBorderSize);
borders["SeparatorText"] = style.SeparatorTextBorderSize; config.insert("borders", borders);
config["borders"] = borders;
} }
{ {
Json::Value colors; toml::table colors;
for (int i = 0; i < ImGuiCol_COUNT; i++) for (int i = 0; i < ImGuiCol_COUNT; i++)
{ {
const char* name = ImGui::GetStyleColorName(i); const char* name = ImGui::GetStyleColorName(i);
ImVec4 color = style.Colors[i]; ImVec4 color = style.Colors[i];
Json::Value colorValue; toml::table colorValue;
colorValue["r"] = color.x; colorValue.insert("r", color.x);
colorValue["g"] = color.y; colorValue.insert("g", color.y);
colorValue["b"] = color.z; colorValue.insert("b", color.z);
colorValue["a"] = color.w; colorValue.insert("a", color.w);
colorValue["ConvertToAccent"] = AccentColorizers[i].Serialize(); colorValue.insert("accent", AccentColorizers[i].Serialize());
colors[name] = colorValue; colors.insert(name, colorValue);
} }
config["colors"] = colors; config.insert("colors", colors);
} }
stream << config; stream << config;
stream.close(); stream.close();
@ -465,15 +458,16 @@ void Theme::updateAvailableThemes() {
for (auto const& dir_entry : directory_iterator(themeDir)) { for (auto const& dir_entry : directory_iterator(themeDir)) {
if (dir_entry.is_regular_file()) { if (dir_entry.is_regular_file()) {
if (dir_entry.path().extension().string() == ".json") { if (dir_entry.path().extension().string() == ".json") {
path path = dir_entry.path(); string curpath = Migrate(dir_entry.path().string());
availableThemes.insert(path); std::filesystem::remove(dir_entry.path());
Json::Value config; availableThemes.insert(curpath);
std::ifstream stream; toml::table config = toml::parse_file(curpath);
stream.open(path); themeStrings[curpath] = ThemeStrings(config);
if (stream.is_open()) { } else if (dir_entry.path().extension().string() == ".toml") {
stream >> config; string curpath = dir_entry.path().string();
themeStrings[path] = ThemeStrings(config); availableThemes.insert(curpath);
} toml::table config = toml::parse_file(curpath);
themeStrings[curpath] = ThemeStrings(config);
} }
} }
} }
@ -524,109 +518,201 @@ ThemeStrings Theme::GetStrings() {
return strings["fallback"]; return strings["fallback"];
} }
} }
ThemeStrings::ThemeStrings(Json::Value config) : ThemeStrings() { ThemeStrings::ThemeStrings(toml::table config) : ThemeStrings() {
char *language_c = CURRENT_LANGUAGE; char *language_c = CURRENT_LANGUAGE;
string language = language_c; string language = language_c;
if (config.isMember("meta")) { if (config.contains("meta")) {
Json::Value metadata = config["meta"]; toml::table metadata = *config["meta"].as_table();
//metadata["SchemaVersion"] = 1; //metadata["SchemaVersion"] = 1;
if (metadata.isMember("Strings")) { if (metadata.contains("Strings")) {
Json::Value stringsList = metadata["Strings"]; toml::table stringsList = *metadata["Strings"].as_table();
if (stringsList.isMember(language)) { if (stringsList.contains(language)) {
Json::Value stringsEntryJson = stringsList[language]; toml::table stringsEntryJson = *stringsList[language].as_table();
if (stringsEntryJson.isMember("Name")) { if (stringsEntryJson.contains("name")) {
name = stringsEntryJson["Name"].asString(); name = **stringsEntryJson["name"].as_string();
} }
if (stringsEntryJson.isMember("Description")) { if (stringsEntryJson.contains("desc")) {
description = stringsEntryJson["Description"].asString(); description = **stringsEntryJson["desc"].as_string();
} }
} else if (metadata.isMember("fallback")) { } else if (metadata.contains("fallback")) {
Json::Value stringsEntryJson = stringsList["fallback"]; toml::table stringsEntryJson = *stringsList["fallback"].as_table();
if (stringsEntryJson.isMember("Name")) { if (stringsEntryJson.contains("name")) {
name = stringsEntryJson["Name"].asString(); name = **stringsEntryJson["name"].as_string();
} }
if (stringsEntryJson.isMember("Description")) { if (stringsEntryJson.contains("desc")) {
description = stringsEntryJson["Description"].asString(); description = **stringsEntryJson["desc"].as_string();
} }
} }
} }
} }
} }
Theme::Theme(string path) : Theme() {
std::string Theme::Migrate(std::string path) {
if (path.ends_with(".json")) {
std::ifstream stream(path);
Json::Value config; Json::Value config;
std::ifstream stream;
stream.open(path);
if (stream.is_open()) {
stream >> config; stream >> config;
toml::table newConfig;
if (config.isMember("meta")) { if (config.isMember("meta")) {
Json::Value metadata = config["meta"]; Json::Value metadata = config["meta"];
//metadata["SchemaVersion"] = 1; toml::table newMeta;
if (metadata.isMember("Strings")) { if (metadata.isMember("Strings")) {
Json::Value stringsList = metadata["Strings"]; Json::Value stringsList = metadata["Strings"];
toml::table newStringsList;
for (string language : stringsList.getMemberNames()) { for (string language : stringsList.getMemberNames()) {
Json::Value stringsEntryJson = stringsList[language]; Json::Value stringsEntryJson = stringsList[language];
ThemeStrings stringsEntry; toml::table newStringsEntry;
if (stringsEntryJson.isMember("Name")) { if (stringsEntryJson.isMember("Name")) {
stringsEntry.name = stringsEntryJson["Name"].asString(); string value = stringsEntryJson["Name"].asString();
newStringsEntry.insert("name", value);
} }
if (stringsEntryJson.isMember("Description")) { if (stringsEntryJson.isMember("Description")) {
stringsEntry.description = stringsEntryJson["Description"].asString(); string value = stringsEntryJson["Description"].asString();
newStringsEntry.insert("desc", value);
}
newStringsList.insert(language, newStringsEntry);
}
newMeta.insert("Strings", newStringsList);
}
newConfig.insert("meta", newMeta);
}
if (config.isMember("rounding")) {
Json::Value rounding = config["rounding"];
toml::table newRounding;
for (string key : rounding.getMemberNames()) {
newRounding.insert(key, rounding[key].asFloat());
}
newConfig.insert("rounding", newRounding);
}
if (config.isMember("sizing")) {
Json::Value rounding = config["sizing"];
toml::table newRounding;
for (string key : rounding.getMemberNames()) {
newRounding.insert(key, rounding[key].asFloat());
}
newConfig.insert("sizing", newRounding);
}
if (config.isMember("borders")) {
Json::Value rounding = config["borders"];
toml::table newRounding;
for (string key : rounding.getMemberNames()) {
newRounding.insert(key, rounding[key].asFloat());
}
newConfig.insert("borders", newRounding);
}
if (config.isMember("colors")) {
Json::Value colors = config["colors"];
toml::table newColors;
for (int i = 0; i < ImGuiCol_COUNT; i++)
{
toml::table newColor;
const char* name = ImGui::GetStyleColorName(i);
if (colors.isMember(name)) {
Json::Value colorValue = colors[name];
ImVec4 color = ImVec4(colorValue["r"].asFloat(), colorValue["g"].asFloat(), colorValue["b"].asFloat(), colorValue["a"].asFloat());
newColor.insert("r", colorValue["r"].asFloat());
newColor.insert("g", colorValue["g"].asFloat());
newColor.insert("b", colorValue["b"].asFloat());
newColor.insert("a", colorValue["a"].asFloat());
toml::table newAccentOptions;
if (colorValue["ConvertToAccent"].isBool()) {
newAccentOptions.insert("hue", colorValue["ConvertToAccent"].asBool());
newAccentOptions.insert("saturation", false);
newAccentOptions.insert("value", false);
newAccentOptions.insert("alpha", false);
} else {
Json::Value accentOptions = colorValue["ConvertToAccent"];
newAccentOptions.insert("hue", accentOptions["hue"].asBool());
newAccentOptions.insert("saturation", accentOptions["saturation"].asBool());
newAccentOptions.insert("value", accentOptions["value"].asBool());
newAccentOptions.insert("alpha", accentOptions["alpha"].asBool());
}
newColor.insert("accent", newAccentOptions);
newColors.insert(name, newColor);
}
}
}
stream.close();
std::string newPath = path.replace(path.size() - 4, 4, "toml");
std::ofstream outStream(newPath);
outStream << newConfig;
outStream.close();
return newPath;
}
return path;
}
Theme::Theme(string path) : Theme() {
path = Migrate(path);
toml::table config;
config = toml::parse_file(path);
if (config.contains("meta")) {
toml::table metadata = *config["meta"].as_table();
//metadata["SchemaVersion"] = 1;
if (metadata.contains("Strings")) {
toml::table stringsList = *metadata["Strings"].as_table();
for (auto kv : stringsList) {
string language = string(kv.first.str());
toml::table stringEntryToml = *kv.second.as_table();
ThemeStrings stringsEntry;
if (stringEntryToml.contains("name")) {
stringsEntry.name = stringEntryToml["name"].as_string()->value_or("Unknown");
}
if (stringEntryToml.contains("desc")) {
stringsEntry.description = stringEntryToml["desc"].as_string()->value_or("No description.");
} }
strings[language] = stringsEntry; strings[language] = stringsEntry;
} }
} }
} }
if (config.isMember("rounding")) { if (config.contains("rounding")) {
Json::Value rounding = config["rounding"]; toml::table rounding = *config["rounding"].as_table();
style.FrameRounding = rounding["Frame"].asFloat(); style.FrameRounding = (float)(**(rounding["Frame"].as_floating_point()));
style.WindowRounding = rounding["Window"].asFloat(); style.WindowRounding = (float)(**(rounding["Window"].as_floating_point()));
style.ChildRounding = rounding["Child"].asFloat(); style.ChildRounding = (float)(**(rounding["Child"].as_floating_point()));
style.PopupRounding = rounding["Popup"].asFloat(); style.PopupRounding = (float)(**(rounding["Popup"].as_floating_point()));
style.ScrollbarRounding = rounding["Scrollbar"].asFloat(); style.ScrollbarRounding = (float)(**(rounding["Scrollbar"].as_floating_point()));
style.GrabRounding = rounding["Grab"].asFloat(); style.GrabRounding = (float)(**(rounding["Grab"].as_floating_point()));
style.TabRounding = rounding["Tab"].asFloat(); style.TabRounding = (float)(**(rounding["Tab"].as_floating_point()));
} }
if (config.isMember("sizing")) { if (config.contains("sizing")) {
Json::Value sizing = config["sizing"]; toml::table sizing = *config["sizing"].as_table();
style.FramePadding.x = sizing["FrameX"].asFloat(); style.FramePadding.x = (float)(**sizing["FrameX"].as_floating_point());
style.FramePadding.y = sizing["FrameY"].asFloat(); style.FramePadding.y = (float)(**sizing["FrameY"].as_floating_point());
style.WindowPadding.x = sizing["WindowX"].asFloat(); style.WindowPadding.x = (float)(**sizing["WindowX"].as_floating_point());
style.WindowPadding.y = sizing["WindowY"].asFloat(); style.WindowPadding.y = (float)(**sizing["WindowY"].as_floating_point());
style.CellPadding.x = sizing["CellX"].asFloat(); style.CellPadding.x = (float)(**sizing["CellX"].as_floating_point());
style.CellPadding.y = sizing["CellY"].asFloat(); style.CellPadding.y = (float)(**sizing["CellY"].as_floating_point());
style.SeparatorTextPadding.x = sizing["SeparatorTextX"].asFloat(); style.SeparatorTextPadding.x = (float)(**sizing["SeparatorTextX"].as_floating_point());
style.SeparatorTextPadding.y = sizing["SeparatorTextY"].asFloat(); style.SeparatorTextPadding.y = (float)(**sizing["SeparatorTextY"].as_floating_point());
style.ItemSpacing.x = sizing["ItemSpacingX"].asFloat(); style.ItemSpacing.x = (float)(**sizing["ItemSpacingX"].as_floating_point());
style.ItemSpacing.y = sizing["ItemSpacingY"].asFloat(); style.ItemSpacing.y = (float)(**sizing["ItemSpacingY"].as_floating_point());
style.ScrollbarSize = sizing["Scrollbar"].asFloat(); style.ScrollbarSize = (float)(**sizing["Scrollbar"].as_floating_point());
style.GrabMinSize = sizing["Grab"].asFloat(); style.GrabMinSize = (float)(**sizing["Grab"].as_floating_point());
} }
if (config.isMember("borders")) { if (config.contains("borders")) {
Json::Value borders = config["borders"]; toml::table borders = *config["borders"].as_table();
style.FrameBorderSize = borders["Frame"].asFloat(); style.FrameBorderSize = (float)(**borders["Frame"].as_floating_point());
style.WindowBorderSize = borders["Window"].asFloat(); style.WindowBorderSize = (float)(**borders["Window"].as_floating_point());
style.ChildBorderSize = borders["Child"].asFloat(); style.ChildBorderSize = (float)(**borders["Child"].as_floating_point());
style.PopupBorderSize = borders["Popup"].asFloat(); style.PopupBorderSize = (float)(**borders["Popup"].as_floating_point());
style.TabBorderSize = borders["Tab"].asFloat(); style.TabBorderSize = (float)(**borders["Tab"].as_floating_point());
if (borders.isMember("SeparatorText")) { if (borders.contains("SeparatorText")) {
style.SeparatorTextBorderSize = borders["SeparatorText"].asFloat(); style.SeparatorTextBorderSize = (float)(**borders["SeparatorText"].as_floating_point());
} }
} }
if (config.isMember("colors")) { if (config.contains("colors")) {
Json::Value colors = config["colors"]; toml::table colors = *config["colors"].as_table();
for (int i = 0; i < ImGuiCol_COUNT; i++) for (int i = 0; i < ImGuiCol_COUNT; i++)
{ {
const char* name = ImGui::GetStyleColorName(i); const char* name = ImGui::GetStyleColorName(i);
if (colors.isMember(name)) { if (colors.contains(name)) {
Json::Value colorValue = colors[name]; toml::table colorValue = *colors[name].as_table();
ImVec4 color = ImVec4(colorValue["r"].asFloat(), colorValue["g"].asFloat(), colorValue["b"].asFloat(), colorValue["a"].asFloat()); ImVec4 color = ImVec4((float)**colorValue["r"].as_floating_point(), (float)**colorValue["g"].as_floating_point(), (float)**colorValue["b"].as_floating_point(), (float)**colorValue["a"].as_floating_point());
AccentColorizers[i] = AccentColorizer(colorValue["ConvertToAccent"]); AccentColorizers[i] = AccentColorizer(*colorValue["accent"].as_table());
style.Colors[i] = color; style.Colors[i] = color;
} }
} }
} }
stream.close();
}
file_path = path; file_path = path;
} }
Theme::Theme(path path) : Theme(path.string()) { Theme::Theme(path path) : Theme(path.string()) {

14
theme.h
View file

@ -7,6 +7,7 @@
#include "file_browser.h" #include "file_browser.h"
#include <json/json.h> #include <json/json.h>
#include <filesystem> #include <filesystem>
#include "thirdparty/toml.hpp"
using std::string; using std::string;
using namespace std::filesystem; using namespace std::filesystem;
@ -14,7 +15,7 @@ struct ThemeStrings {
string name; string name;
string description; string description;
ThemeStrings(); ThemeStrings();
ThemeStrings(Json::Value config); ThemeStrings(toml::table config);
}; };
struct AccentColorizer { struct AccentColorizer {
@ -29,18 +30,19 @@ struct AccentColorizer {
/// @brief Colorizes a color stored as an ImVec4 according to preferences. /// @brief Colorizes a color stored as an ImVec4 according to preferences.
void Colorize(ImVec4 accent, ImVec4 &color); void Colorize(ImVec4 accent, ImVec4 &color);
/// @brief Serialize the settings to json. /// @brief Serialize the settings to json.
/// @returns The serialized JSON, as a Json::Value. /// @returns The serialized TOML, as a toml::table.
Json::Value Serialize(); toml::table Serialize();
/// @brief Create a default accent colorizer /// @brief Create a default accent colorizer
AccentColorizer(); AccentColorizer();
/// @brief Deserialize the settings from JSON and construct. /// @brief Deserialize the settings from TOML and construct.
/// @param json The JSON to deserialize from. /// @param table The TOML to deserialize from.
AccentColorizer(Json::Value json); AccentColorizer(toml::table table);
}; };
class Theme { class Theme {
ImGuiStyle style; ImGuiStyle style;
static std::string Migrate(string path);
public: public:
static std::set<path> availableThemes; static std::set<path> availableThemes;
static std::map<path, ThemeStrings> themeStrings; static std::map<path, ThemeStrings> themeStrings;

17748
thirdparty/toml.hpp vendored Normal file

File diff suppressed because it is too large Load diff