Rewriting memon parsing

This commit is contained in:
Stepland 2022-04-02 04:10:09 +02:00
parent f32b642fe4
commit 64a952ce83
17 changed files with 346 additions and 120 deletions

View File

@ -3,7 +3,7 @@
#include <json.hpp>
namespace better {
nlohmann::ordered_json Chart::dump_for_memon_1_0_0(
nlohmann::ordered_json Chart::dump_to_memon_1_0_0(
const nlohmann::ordered_json& fallback_timing_object
) const {
nlohmann::ordered_json json_chart;
@ -14,9 +14,28 @@ namespace better {
if (not chart_timing.empty()) {
json_chart["timing"] = chart_timing;
}
json_chart["notes"] = notes.dump_for_memon_1_0_0();
json_chart["notes"] = notes.dump_to_memon_1_0_0();
};
Chart Chart::load_from_memon_legacy(const nlohmann::json& json, const Timing& timing) {
Chart chart {
.level = Decimal{json["level"].get<int>()},
.timing = timing,
.hakus = {},
.notes = {}
};
const auto resolution = json["resolution"].get<unsigned int>();
for (auto& json_note : json.at("notes")) {
try {
const auto note = load_legacy_note(json_note, resolution);
chart.notes.insert(note);
} catch (const std::exception&) {
continue;
}
}
return chart;
}
nlohmann::ordered_json remove_common_keys(
const nlohmann::ordered_json& object,
const nlohmann::ordered_json& fallback
@ -40,7 +59,7 @@ namespace better {
const nlohmann::ordered_json& fallback_timing_object
) {
auto complete_song_timing = nlohmann::ordered_json::object();
complete_song_timing.update(timing.dump_for_memon_1_0_0());
complete_song_timing.update(timing.dump_to_memon_1_0_0());
if (hakus) {
complete_song_timing["hakus"] = dump_hakus(*hakus);
}

View File

@ -18,9 +18,14 @@ namespace better {
std::optional<Hakus> hakus;
Notes notes;
nlohmann::ordered_json dump_for_memon_1_0_0(
nlohmann::ordered_json dump_to_memon_1_0_0(
const nlohmann::ordered_json& fallback_timing_object
) const;
static Chart load_from_memon_legacy(
const nlohmann::json& json,
const Timing& timing
);
};
/*

View File

@ -1,7 +1,7 @@
#include "better_metadata.hpp"
namespace better {
nlohmann::ordered_json Metadata::dump_for_memon_1_0_0() const {
nlohmann::ordered_json Metadata::dump_to_memon_1_0_0() const {
nlohmann::ordered_json json_metadata;
if (not title.empty()) {
json_metadata["title"] = title;
@ -29,4 +29,13 @@ namespace better {
}
return json_metadata;
};
Metadata Metadata::load_from_memon_legacy(const nlohmann::json& json) {
Metadata metadata;
json["song title"].get_to(metadata.title);
json["artist"].get_to(metadata.artist);
json["music path"].get_to(metadata.audio);
json["jacket path"].get_to(metadata.jacket);
return metadata;
}
}

View File

@ -21,6 +21,8 @@ namespace better {
std::string preview_file = "";
bool use_preview_file = false;
nlohmann::ordered_json dump_for_memon_1_0_0() const;
nlohmann::ordered_json dump_to_memon_1_0_0() const;
static Metadata load_from_memon_legacy(const nlohmann::json& json);
};
}

View File

@ -50,7 +50,7 @@ namespace better {
return position;
};
nlohmann::ordered_json TapNote::dump_for_memon_1_0_0() const {
nlohmann::ordered_json TapNote::dump_to_memon_1_0_0() const {
return {
{"n", position.index()},
{"t", beat_to_best_form(time)}
@ -135,7 +135,7 @@ namespace better {
}
};
nlohmann::ordered_json LongNote::dump_for_memon_1_0_0() const {
nlohmann::ordered_json LongNote::dump_to_memon_1_0_0() const {
return {
{"n", position.index()},
{"t", beat_to_best_form(time)},
@ -151,6 +151,31 @@ namespace better {
return 3 + tail_tip.get_y() - int(tail_tip.get_y() > position.get_y());
}
}
/*
* legacy long note tail index is given relative to the note position :
*
* 8
* 4
* 0
* 11 7 3 . 1 5 9
* 2
* 6
* 10
*/
Position legacy_memon_tail_index_to_position(const Position& pos, unsigned int tail_index) {
auto length = (tail_index / 4) + 1;
switch (tail_index % 4) {
case 0: // up
return {pos.get_x(), pos.get_y() - length};
case 1: // right
return {pos.get_x() + length, pos.get_y()};
case 2: // down
return {pos.get_x(), pos.get_y() + length};
case 3: // left
return {pos.get_x() - length, pos.get_y()};
}
}
auto _time_bounds = VariantVisitor {
[](const TapNote& t) -> std::pair<Fraction, Fraction> { return {t.get_time(), t.get_time()}; },
@ -173,8 +198,31 @@ namespace better {
return this->get_time_bounds().second;
}
nlohmann::ordered_json Note::dump_for_memon_1_0_0() const {
return std::visit([](const auto& n){return n.dump_for_memon_1_0_0();}, this->note);
nlohmann::ordered_json Note::dump_to_memon_1_0_0() const {
return std::visit([](const auto& n){return n.dump_to_memon_1_0_0();}, this->note);
}
Note Note::load_from_memon_legacy(const nlohmann::json& json, unsigned int resolution) {
const auto position = Position{json["n"].get<unsigned int>()};
const auto time = Fraction{
json["t"].get<unsigned int>(),
resolution,
};
const auto duration = Fraction{
json["l"].get<unsigned int>(),
resolution,
};
const auto tail_index = json["n"].get<unsigned int>();
if (duration > 0) {
return LongNote{
time,
position,
duration,
legacy_memon_tail_index_to_position(position, tail_index)
};
} else {
return TapNote{time, position};
}
}
}

View File

@ -48,7 +48,7 @@ namespace better {
bool operator==(const TapNote&) const = default;
nlohmann::ordered_json dump_for_memon_1_0_0() const;
nlohmann::ordered_json dump_to_memon_1_0_0() const;
private:
Fraction time;
Position position;
@ -68,7 +68,7 @@ namespace better {
bool operator==(const LongNote&) const = default;
nlohmann::ordered_json dump_for_memon_1_0_0() const;
nlohmann::ordered_json dump_to_memon_1_0_0() const;
int tail_as_6_notation() const;
private:
Fraction time;
@ -77,6 +77,8 @@ namespace better {
Position tail_tip;
};
Position legacy_memon_tail_index_to_position(const Position& pos, unsigned int tail_index);
class Note {
public:
template<typename ...Ts>
@ -91,7 +93,12 @@ namespace better {
bool operator==(const Note&) const = default;
nlohmann::ordered_json dump_for_memon_1_0_0() const;
nlohmann::ordered_json dump_to_memon_1_0_0() const;
static Note load_from_memon_legacy(
const nlohmann::json& json,
unsigned int resolution
);
private:
std::variant<TapNote, LongNote> note;
};

View File

@ -99,10 +99,10 @@ namespace better {
return found_collision;
};
nlohmann::ordered_json Notes::dump_for_memon_1_0_0() const {
nlohmann::ordered_json Notes::dump_to_memon_1_0_0() const {
auto json_notes = nlohmann::ordered_json::array();
for (const auto& [_, note] : *this) {
json_notes.push_back(note.dump_for_memon_1_0_0());
json_notes.push_back(note.dump_to_memon_1_0_0());
}
return json_notes;
}

View File

@ -32,6 +32,6 @@ namespace better {
*/
bool is_colliding(const better::Note& note, const better::Timing& timing);
nlohmann::ordered_json dump_for_memon_1_0_0() const;
nlohmann::ordered_json dump_to_memon_1_0_0() const;
};
}

View File

@ -1,10 +1,18 @@
#include "better_song.hpp"
#include <stdexcept>
#include <fmt/core.h>
#include <SFML/System/Time.hpp>
#include "better_hakus.hpp"
#include "better_timing.hpp"
#include "json.hpp"
#include "src/better_chart.hpp"
#include "src/better_hakus.hpp"
#include "src/better_metadata.hpp"
#include "src/better_note.hpp"
#include "src/special_numeric_types.hpp"
#include "std_optional_extras.hpp"
#include "variant_visitor.hpp"
@ -13,10 +21,10 @@ namespace better {
return stringify_or(level, "(no level defined)");
};
nlohmann::ordered_json Song::dump_for_memon_1_0_0() const {
nlohmann::ordered_json Song::dump_to_memon_1_0_0() const {
nlohmann::ordered_json memon;
memon["version"] = "1.0.0";
auto json_metadata = metadata.dump_for_memon_1_0_0();
auto json_metadata = metadata.dump_to_memon_1_0_0();
if (not json_metadata.empty()) {
memon["metadata"] = json_metadata;
}
@ -30,8 +38,79 @@ namespace better {
fallback_timing_object.update(song_timing);
auto json_charts = nlohmann::ordered_json::object();
for (const auto& [name, chart] : charts) {
json_charts[name] = chart.dump_for_memon_1_0_0(fallback_timing_object);
json_charts[name] = chart.dump_to_memon_1_0_0(fallback_timing_object);
}
return memon;
}
Song Song::load_from_memon(const nlohmann::json& memon) {
if (not memon.is_object()) {
throw std::invalid_argument(
"The json file you tried to load does not contain an object "
"at the top level. This is required for a json file to be a "
"valid memon file."
);
}
if (not memon.contains("version")) {
return Song::load_from_memon_legacy(memon);
}
if (not memon["version"].is_string()) {
throw std::invalid_argument(
"The json file you tried to load has a 'version' key at the "
"top level but its associated value is not a string. This is "
"required for a json file to be a valid memon file."
);
}
const auto version = memon["version"].get<std::string>();
if (version == "1.0.0") {
return Song::load_from_memon_1_0_0(memon);
} else if (version == "0.3.0") {
return Song::load_from_memon_0_3_0(memon);
} else if (version == "0.2.0") {
return Song::load_from_memon_0_2_0(memon);
} else if (version == "0.1.0") {
return Song::load_from_memon_0_1_0(memon);
} else {
throw std::invalid_argument(fmt::format(
"Unknown memon version : {}",
version
));
}
};
Song Song::load_from_memon_1_0_0(const nlohmann::json& memon) {
};
Song Song::load_from_memon_0_3_0(const nlohmann::json& memon) {
};
Song Song::load_from_memon_0_2_0(const nlohmann::json& memon) {
};
Song Song::load_from_memon_0_1_0(const nlohmann::json& memon) {
};
Song Song::load_from_memon_legacy(const nlohmann::json& memon) {
const auto json_metadata = memon["metadata"];
const auto metadata = Metadata::load_from_memon_legacy(memon["metadata"]);
const auto bpm = Decimal{json_metadata["BPM"].get<std::string>()};
const auto offset = convert_to_fraction(Decimal{json_metadata["offset"].get<std::string>()});
const auto timing = Timing::load_from_memon_legacy(bpm, offset);
Song song{
.charts = {},
.metadata = metadata,
.timing = timing,
.hakus = {}
};
for (const auto& chart_json : memon["data"]) {
const auto dif = chart_json["dif_name"].get<std::string>();
const auto chart = Chart::load_from_memon_legacy(chart_json, timing);
song.charts[dif] = chart;
}
return song;
};
}

View File

@ -47,6 +47,51 @@ namespace better {
Timing timing;
std::optional<Hakus> hakus;
nlohmann::ordered_json dump_for_memon_1_0_0() const;
nlohmann::ordered_json dump_to_memon_1_0_0() const;
/*
Read the json file as memon by trying to guess version.
Throws various exceptions on error
*/
static Song load_from_memon(const nlohmann::json& memon);
/*
Read the json file as memon v1.0.0.
https://memon-spec.readthedocs.io/en/latest/changelog.html#v1-0-0
*/
static Song load_from_memon_1_0_0(const nlohmann::json& memon);
/*
Read the json file as memon v0.3.0.
https://memon-spec.readthedocs.io/en/latest/changelog.html#v0-3-0
*/
static Song load_from_memon_0_3_0(const nlohmann::json& memon);
/*
Read the json file as memon v0.2.0.
https://memon-spec.readthedocs.io/en/latest/changelog.html#v0-2-0
*/
static Song load_from_memon_0_2_0(const nlohmann::json& memon);
/*
Read the json file as memon v0.1.0.
https://memon-spec.readthedocs.io/en/latest/changelog.html#v0-1-0
*/
static Song load_from_memon_0_1_0(const nlohmann::json& memon);
/*
Read the json file as a "legacy" (pre-versionning) memon file.
Notable quirks of this archaïc schema :
- "data" is an array of charts
- the difficulty name of a chart is stored as "dif_name" in the chart
object
- the album cover path field is named "jacket path"
*/
static Song load_from_memon_legacy(const nlohmann::json& memon);
};
Note load_legacy_note(const nlohmann::json& legacy_note, unsigned int resolution);
Position legacy_memon_tail_index_to_position(const Position& pos, unsigned int tail_index);
}

View File

@ -184,7 +184,7 @@ namespace better {
return bpm_change->get_beats() + beats_since_previous_event;
};
nlohmann::ordered_json Timing::dump_for_memon_1_0_0() const {
nlohmann::ordered_json Timing::dump_to_memon_1_0_0() const {
nlohmann::ordered_json j;
const auto offset = fractional_seconds_at(0);
j["offset"] = convert_to_decimal(offset, 5).format("f");
@ -198,4 +198,14 @@ namespace better {
j["bpms"] = bpms;
return j;
}
/*
For this function, offset is the OPPOSITE of the time (in seconds) at which
the first beat occurs in the music file
*/
Timing Timing::load_from_memon_legacy(Decimal bpm, Fraction offset) {
const BPMAtBeat bpm_at_beat{0, bpm};
const SecondsAtBeat seconds_at_beat{-1 * offset, 0};
return Timing{{bpm_at_beat}, seconds_at_beat};
}
}

View File

@ -56,7 +56,9 @@ namespace better {
Fraction beats_at(sf::Time time) const;
nlohmann::ordered_json dump_for_memon_1_0_0() const;
nlohmann::ordered_json dump_to_memon_1_0_0() const;
static Timing load_from_memon_legacy(Decimal bpm, Fraction offset);
private:
std::set<BPMEvent, decltype(order_by_beats)> events_by_beats{order_by_beats};

View File

@ -537,36 +537,62 @@ void EditorState::display_linear_view() {
ImGui::PopStyleVar(2);
};
UserWantsToSave EditorState::ask_to_save_if_needed() {
if (chart_state and (not chart_state->history.current_state_is_saved())) {
int response_code = tinyfd_messageBox(
"Warning",
"Do you want to save changes ?",
"yesnocancel",
"warning",
1
);
switch (response_code) {
// cancel
case 0:
return UserWantsToSave::Cancel;
// yes
case 1:
return UserWantsToSave::Yes;
// no
case 2:
return UserWantsToSave::No;
default:
throw std::runtime_error(fmt::format(
"Got unexcpected response code from tinyfd_messageBox : {}",
response_code
));
}
bool EditorState::needs_to_save() const {
if (chart_state) {
return not chart_state->history.current_state_is_saved();
} else {
return UserWantsToSave::DidNotDisplayDialog;
return false;
}
}
EditorState::UserWantsToSave EditorState::ask_if_user_wants_to_save() const {
int response_code = tinyfd_messageBox(
"Warning",
"Do you want to save changes ?",
"yesnocancel",
"warning",
1
);
switch (response_code) {
// cancel
case 0:
return EditorState::UserWantsToSave::Cancel;
// yes
case 1:
return EditorState::UserWantsToSave::Yes;
// no
case 2:
return EditorState::UserWantsToSave::No;
default:
throw std::runtime_error(fmt::format(
"Got unexcpected response code from tinyfd_messageBox : {}",
response_code
));
}
};
EditorState::SaveOutcome EditorState::save_if_needed() {
if (not needs_to_save()) {
return EditorState::SaveOutcome::NoSavingNeeded;
}
switch (ask_if_user_wants_to_save()) {
case EditorState::UserWantsToSave::Yes:
{
const auto path = ask_for_save_path_if_needed();
if (not path) {
return EditorState::SaveOutcome::UserCanceled;
} else {
save(*path);
return EditorState::SaveOutcome::UserSaved;
}
}
case EditorState::UserWantsToSave::No:
return EditorState::SaveOutcome::UserDeclindedSaving;
case EditorState::UserWantsToSave::Cancel:
return EditorState::SaveOutcome::UserCanceled;
}
}
std::optional<std::filesystem::path> EditorState::ask_for_save_path_if_needed() {
if (song_path) {
return song_path;
@ -575,22 +601,6 @@ std::optional<std::filesystem::path> EditorState::ask_for_save_path_if_needed()
}
}
/*
Saves if asked and returns false if user canceled
*/
bool EditorState::save_changes_or_cancel() {
switch (ask_to_save_if_needed()) {
case UserWantsToSave::Yes:
save();
case UserWantsToSave::No:
case UserWantsToSave::DidNotDisplayDialog:
return true;
case UserWantsToSave::Cancel:
default:
return false;
}
};
void EditorState::move_backwards_in_time() {
auto beats = current_exact_beats();
if (beats % get_snap_step() == 0) {
@ -621,7 +631,7 @@ void EditorState::undo(NotificationsQueue& nq) {
void EditorState::redo(NotificationsQueue& nq) {
if (chart_state) {
auto next = chart_state->history.gpop_next();
auto next = chart_state->history.pop_next();
if (next) {
nq.push(std::make_shared<RedoNotification>(**next));
(*next)->doAction(*this);
@ -731,7 +741,7 @@ void EditorState::open_chart(better::Chart& chart, const std::string& name) {
};
void EditorState::save(const std::filesystem::path& path) {
const auto memon = song.dump_for_memon_1_0_0();
const auto memon = song.dump_to_memon_1_0_0();
nowide::ofstream file{path};
if (not file) {
throw std::runtime_error(
@ -750,16 +760,16 @@ void EditorState::save(const std::filesystem::path& path) {
}
}
void ESHelper::open(std::optional<EditorState>& ed, std::filesystem::path assets, std::filesystem::path settings) {
void feis::open(std::optional<EditorState>& ed, std::filesystem::path assets, std::filesystem::path settings) {
const char* _filepath =
tinyfd_openFileDialog("Open File", nullptr, 0, nullptr, nullptr, false);
if (_filepath != nullptr) {
auto filepath = std::filesystem::path{_filepath};
ESHelper::openFromFile(ed, filepath, assets, settings);
feis::openFromFile(ed, filepath, assets, settings);
}
}
void ESHelper::openFromFile(
void feis::openFromFile(
std::optional<EditorState>& ed,
std::filesystem::path song_path,
std::filesystem::path assets,
@ -775,22 +785,10 @@ void ESHelper::openFromFile(
}
}
/*
* returns true if user saved or if saving wasn't necessary
* returns false if user canceled
*/
bool ESHelper::saveOrCancel(std::optional<EditorState>& ed) {
if (ed) {
return ed->save_changes_or_cancel();
} else {
return true;
}
}
/*
* Returns the newly created chart if there is one
*/
std::optional<better::Chart> ESHelper::NewChartDialog::display(EditorState& editorState) {
std::optional<better::Chart> feis::NewChartDialog::display(EditorState& editorState) {
std::optional<better::Chart> newChart;
if (ImGui::Begin(
"New Chart",
@ -876,7 +874,7 @@ std::optional<better::Chart> ESHelper::NewChartDialog::display(EditorState& edit
return newChart;
}
void ESHelper::ChartPropertiesDialog::display(EditorState& editorState, std::filesystem::path assets) {
void feis::ChartPropertiesDialog::display(EditorState& editorState, std::filesystem::path assets) {
assert(editorState.chart.has_value());
if (this->shouldRefreshValues) {

View File

@ -19,15 +19,6 @@
#include "widgets/linear_view.hpp"
#include "widgets/playfield.hpp"
class ActionWithMessage;
class OpenChart;
enum class UserWantsToSave {
Yes,
No,
Cancel,
DidNotDisplayDialog
};
/*
* The god class, holds everything there is to know about the currently open
@ -100,11 +91,24 @@ public:
bool showHistory;
bool showSoundSettings;
/*
Return ::DidNotDisplayDialog if the current chart state is marked as saved,
otherwise ask the user if they want to save and return their answer
*/
UserWantsToSave ask_to_save_if_needed();
enum class SaveOutcome {
UserSaved,
UserDeclindedSaving,
UserCanceled,
NoSavingNeeded,
};
SaveOutcome save_if_needed();
bool needs_to_save() const;
enum class UserWantsToSave {
Yes,
No,
Cancel,
};
UserWantsToSave ask_if_user_wants_to_save() const;
/*
If the given song already has a dedicated file on disk, returns its path.
@ -112,7 +116,6 @@ public:
return nothing if the user canceled
*/
std::optional<std::filesystem::path> ask_for_save_path_if_needed();
bool save_changes_or_cancel();
void toggle_note_at_current_time(const better::Position& pos);
@ -143,8 +146,6 @@ private:
void open_chart(better::Chart& chart, const std::string& name);
std::filesystem::path assets;
friend class ESHelper::NewChartDialog;
};
namespace feis {
@ -156,8 +157,6 @@ namespace feis {
std::filesystem::path settings
);
bool saveOrCancel(std::optional<EditorState>& ed);
class NewChartDialog {
public:
std::optional<better::Chart> display(EditorState& editorState);

View File

@ -95,7 +95,7 @@ public:
}
}
bool current_state_is_saved() {
bool current_state_is_saved() const {
return last_saved_action == previous_actions.front();
};

View File

@ -77,8 +77,8 @@ int main() {
BlankScreen bg{assets_folder};
std::optional<EditorState> editor_state;
NotificationsQueue notificationsQueue;
ESHelper::NewChartDialog newChartDialog;
ESHelper::ChartPropertiesDialog chartPropertiesDialog;
feis::NewChartDialog newChartDialog;
feis::ChartPropertiesDialog chartPropertiesDialog;
sf::Clock deltaClock;
while (window.isOpen()) {
@ -89,8 +89,10 @@ int main() {
switch (event.type) {
case sf::Event::Closed:
preferences.save();
if (ESHelper::saveOrCancel(editor_state)) {
window.close();
if (editor_state) {
if (editor_state->save_if_needed() != EditorState::SaveOutcome::UserCanceled) {
window.close();
}
}
break;
case sf::Event::Resized:
@ -112,8 +114,8 @@ int main() {
switch (event.mouseButton.button) {
case sf::Mouse::Button::Right:
if (editor_state and editor_state->chart_state) {
if (editor_state->chart_state->longNoteBeingCreated) {
auto pair = *editor_state->chart->longNoteBeingCreated;
if (editor_state->chart_state->long_note_being_created) {
auto pair = *editor_state->chart_state->long_note_being_created;
Note new_note = Note(pair.first, pair.second);
std::set<Note> new_note_set = {new_note};
editor_state->chart->ref.Notes.insert(new_note);
@ -367,8 +369,8 @@ int main() {
break;
case sf::Keyboard::O:
if (event.key.control) {
if (ESHelper::saveOrCancel(editor_state)) {
ESHelper::open(editor_state, assets_folder, settings_folder);
if (feis::saveOrCancel(editor_state)) {
feis::open(editor_state, assets_folder, settings_folder);
}
}
break;
@ -379,7 +381,7 @@ int main() {
break;
case sf::Keyboard::S:
if (event.key.control) {
ESHelper::save(*editor_state);
feis::save(*editor_state);
notificationsQueue.push(std::make_shared<TextNotification>(
"Saved file"));
}
@ -564,7 +566,7 @@ int main() {
if (ImGui::MenuItem("New")) {
bool user_canceled = false;
if (editor_state) {
switch (editor_state->ask_to_save_if_needed()) {
switch (editor_state->ask_if_user_wants_to_save()) {
case UserWantsToSave::Yes:
const auto path = editor_state->ask_for_save_path_if_needed();
if (path) {
@ -587,8 +589,8 @@ int main() {
}
ImGui::Separator();
if (ImGui::MenuItem("Open", "Ctrl+O")) {
if (ESHelper::saveOrCancel(editor_state)) {
ESHelper::open(editor_state, assets_folder, settings_folder);
if (feis::saveOrCancel(editor_state)) {
feis::open(editor_state, assets_folder, settings_folder);
}
}
if (ImGui::BeginMenu("Recent Files")) {
@ -596,8 +598,8 @@ int main() {
for (const auto& file : Toolbox::getRecentFiles(settings_folder)) {
ImGui::PushID(i);
if (ImGui::MenuItem(file.c_str())) {
if (ESHelper::saveOrCancel(editor_state)) {
ESHelper::openFromFile(editor_state, file, assets_folder, settings_folder);
if (feis::saveOrCancel(editor_state)) {
feis::openFromFile(editor_state, file, assets_folder, settings_folder);
}
}
ImGui::PopID();
@ -606,13 +608,13 @@ int main() {
ImGui::EndMenu();
}
if (ImGui::MenuItem("Close", "", false, editor_state.has_value())) {
if (ESHelper::saveOrCancel(editor_state)) {
if (feis::saveOrCancel(editor_state)) {
editor_state.reset();
}
}
ImGui::Separator();
if (ImGui::MenuItem("Save", "Ctrl+S", false, editor_state.has_value())) {
ESHelper::save(*editor_state);
feis::save(*editor_state);
}
if (ImGui::MenuItem("Save As", "", false, editor_state.has_value())) {
char const* options[1] = {"*.memon"};

View File

@ -37,7 +37,8 @@ const auto floor_beats = [](Fraction beats, unsigned int denominator = 240) {
return nearest / Fraction{denominator};
};
// Stolen from : https://github.com/progrock-libraries/kickstart/blob/d62c22efc92006dd76d455cf8f9d4f2a045e9126/source/library/kickstart/main_library/core/ns%E2%96%B8language/operations/intpow.hpp#L36
// Stolen from :
// https://github.com/progrock-libraries/kickstart/blob/master/source/library/kickstart/main_library/core/ns%E2%96%B8language/operations/intpow.hpp#L36
// Essentially this is Horner's rule adapted to calculating a power, so that the
// number of floating point multiplications is at worst O(log₂n).
template<class Number>