diff --git a/cmake.plugin.txt b/cmake.plugin.txt index 9288752..53977c6 100644 --- a/cmake.plugin.txt +++ b/cmake.plugin.txt @@ -99,6 +99,9 @@ function(add_new_target target_name is_synth) "plugin/components/searchable_popup.h" "plugin/components/modal_textinputbox.h" "plugin/components/divider.h" + "plugin/components/tokenizer.h" + "plugin/components/tokenizer_functions.h" + "plugin/components/tokenizer.cpp" "plugin/utility/audio_processor_suspender.h" "plugin/utility/functional_timer.h" "plugin/utility/async_updater.cpp" diff --git a/plugin/components/ide_view.cpp b/plugin/components/ide_view.cpp index 3308eb8..569c39c 100644 --- a/plugin/components/ide_view.cpp +++ b/plugin/components/ide_view.cpp @@ -18,7 +18,7 @@ #include "lookandfeel.h" #include "ide_view.h" #include "utility/functional_timer.h" -#include +#include "tokenizer.h" #include struct YsfxIDEView::Impl { @@ -27,7 +27,7 @@ struct YsfxIDEView::Impl { juce::Time m_changeTime; bool m_reloadDialogGuard = false; std::unique_ptr m_document; - std::unique_ptr m_tokenizer; + std::unique_ptr m_tokenizer; std::unique_ptr m_editor; std::unique_ptr m_btnSave; std::unique_ptr m_btnUpdate; @@ -49,6 +49,8 @@ struct YsfxIDEView::Impl { juce::Array m_vars; std::unique_ptr m_varsUpdateTimer; + juce::File m_file{}; + bool m_forceUpdate{false}; //========================================================================== @@ -71,8 +73,7 @@ YsfxIDEView::YsfxIDEView() m_impl->m_self = this; m_impl->m_document.reset(new juce::CodeDocument); - - //TODO code tokenizer + m_impl->m_tokenizer.reset(new JSFXTokenizer()); m_impl->createUI(); m_impl->connectUI(); @@ -86,6 +87,12 @@ YsfxIDEView::~YsfxIDEView() { } +void YsfxIDEView::setColourScheme(std::map> colormap) +{ + m_impl->m_tokenizer->setColours(colormap); + m_impl->m_editor->setColourScheme(m_impl->m_tokenizer->getDefaultColourScheme()); +} + void YsfxIDEView::setEffect(ysfx_t *fx, juce::Time timeStamp) { if (m_impl->m_fx.get() == fx) @@ -146,10 +153,11 @@ void YsfxIDEView::Impl::setupNewFx() } else { juce::File file{juce::CharPointer_UTF8{ysfx_get_file_path(fx)}}; + if (file != juce::File{}) m_file = file; { juce::MemoryBlock memBlock; - if (file.loadFileAsData(memBlock)) { + if (m_file.loadFileAsData(memBlock)) { juce::String newContent = memBlock.toString(); memBlock = {}; if (newContent != m_document->getAllContent()) { @@ -207,9 +215,8 @@ void YsfxIDEView::Impl::saveAs() if (m_fileChooserActive) return; juce::File initialPath; - juce::File prevFilePath{juce::CharPointer_UTF8{ysfx_get_file_path(m_fx.get())}}; - if (prevFilePath != juce::File{}) { - initialPath = prevFilePath.getParentDirectory(); + if (m_file != juce::File{}) { + initialPath = m_file.getParentDirectory(); } m_fileChooser.reset(new juce::FileChooser(TRANS("Choose filename to save JSFX to"), initialPath)); @@ -273,10 +280,9 @@ void YsfxIDEView::Impl::saveCurrentFile() if (!fx) return; - juce::File file = juce::File{juce::CharPointer_UTF8{ysfx_get_file_path(fx)}}; - if (file.existsAsFile()) { + if (m_file.existsAsFile()) { m_btnSave->setEnabled(false); - saveFile(file); + saveFile(m_file); } else { saveAs(); } @@ -288,11 +294,10 @@ void YsfxIDEView::Impl::checkFileForModifications() if (!fx) return; - juce::File file{juce::CharPointer_UTF8{ysfx_get_file_path(fx)}}; - if (file == juce::File{}) + if (m_file == juce::File{}) return; - juce::Time newMtime = file.getLastModificationTime(); + juce::Time newMtime = m_file.getLastModificationTime(); if (newMtime == juce::Time{}) return; @@ -302,11 +307,11 @@ void YsfxIDEView::Impl::checkFileForModifications() if (!m_reloadDialogGuard) { m_reloadDialogGuard = true; - auto callback = [this, file](int result) { + auto callback = [this](int result) { m_reloadDialogGuard = false; if (result != 0) { if (m_self->onReloadRequested) - m_self->onReloadRequested(file); + m_self->onReloadRequested(m_file); } }; diff --git a/plugin/components/ide_view.h b/plugin/components/ide_view.h index 754d4ee..6f475fb 100644 --- a/plugin/components/ide_view.h +++ b/plugin/components/ide_view.h @@ -25,6 +25,7 @@ class YsfxIDEView : public juce::Component { public: YsfxIDEView(); ~YsfxIDEView() override; + void setColourScheme(std::map> colormap); void setEffect(ysfx_t *fx, juce::Time timeStamp); void setStatusText(const juce::String &text); void focusOnCodeEditor(); diff --git a/plugin/components/modal_textinputbox.h b/plugin/components/modal_textinputbox.h index 975e3ef..529ef86 100644 --- a/plugin/components/modal_textinputbox.h +++ b/plugin/components/modal_textinputbox.h @@ -18,7 +18,7 @@ class ExclusionFilter : public juce::TextEditor::InputFilter }; -static juce::AlertWindow* show_async_text_input(juce::String title, juce::String message, std::function callback, std::optional> validator=std::nullopt) +static juce::AlertWindow* show_async_text_input(juce::String title, juce::String message, std::function callback, std::optional> validator=std::nullopt, juce::Component* parent=nullptr) { auto* window = new juce::AlertWindow(title, message, juce::AlertWindow::NoIcon); window->addTextEditor("textField", "", ""); @@ -61,11 +61,15 @@ static juce::AlertWindow* show_async_text_input(juce::String title, juce::String textEditor->setWantsKeyboardFocus(true); textEditor->grabKeyboardFocus(); + if (parent) { + window->setCentrePosition(parent->getScreenPosition() + juce::Point{parent->getScreenBounds().getWidth() / 2, parent->getScreenBounds().getHeight() / 2}); + } + return window; } -static juce::AlertWindow* show_overwrite_window(juce::String title, juce::String message, std::vector buttons, std::function callback) +static juce::AlertWindow* show_option_window(juce::String title, juce::String message, std::vector buttons, std::function callback, juce::Component* parent=nullptr) { auto* window = new juce::AlertWindow(title, message, juce::AlertWindow::NoIcon); window->setMessage(message); @@ -89,5 +93,9 @@ static juce::AlertWindow* show_overwrite_window(juce::String title, juce::String window->grabKeyboardFocus(); window->setEscapeKeyCancels(true); + if (parent) { + window->setCentrePosition(parent->getScreenPosition() + juce::Point{parent->getScreenBounds().getWidth() / 2, parent->getScreenBounds().getHeight() / 2}); + } + return window; } diff --git a/plugin/components/rpl_view.cpp b/plugin/components/rpl_view.cpp index 7b04065..989f3a3 100644 --- a/plugin/components/rpl_view.cpp +++ b/plugin/components/rpl_view.cpp @@ -213,9 +213,7 @@ class LoadedBank : public juce::Component, public juce::DragAndDropContainer { [this](const juce::FileChooser &chooser) { juce::File result = chooser.getResult(); if (result != juce::File()) { - if (this) { - this->setFile(result); - } + this->setFile(result); } } ); @@ -254,31 +252,27 @@ class LoadedBank : public juce::Component, public juce::DragAndDropContainer { if (names.empty()) return; - juce::AlertWindow::showAsync - ( - juce::MessageBoxOptions() - .withTitle("Are you certain?") - .withMessage( + m_confirmDialog.reset( + show_option_window( + TRANS("Are you certain?"), TRANS("Are you certain you want to delete ") + ((names.size() > 1) ? TRANS("several presets") : names[0]) - + "\n" + TRANS("This operation cannot be undone!") - ) - .withButton("Yes") - .withButton("No") - .withParentComponent(this) - .withIconType(juce::MessageBoxIconType::NoIcon), - [this, names](int result){ - if (result == 1) { - for (auto name : names) - { - m_bank.reset(ysfx_delete_preset_from_bank(m_bank.get(), name.c_str())); - } + + "?\n" + TRANS("This operation cannot be undone!"), + std::vector{"Yes", "No"}, + [this, names](int result){ + if (result == 1) { + for (auto name : names) + { + m_bank.reset(ysfx_delete_preset_from_bank(m_bank.get(), name.c_str())); + } - this->m_listBox->deselectAllRows(); - save_bank(m_file.getFullPathName().toStdString().c_str(), m_bank.get()); - if (m_bankUpdatedCallback) m_bankUpdatedCallback(); - } - } + this->m_listBox->deselectAllRows(); + save_bank(m_file.getFullPathName().toStdString().c_str(), m_bank.get()); + if (m_bankUpdatedCallback) m_bankUpdatedCallback(); + } + }, + this + ) ); } @@ -413,7 +407,7 @@ class LoadedBank : public juce::Component, public juce::DragAndDropContainer { if (ysfx_preset_exists(m_bank.get(), src_bank->presets[idx].name) && !force_accept) { // Ask for overwrite m_confirmDialog.reset( - show_overwrite_window( + show_option_window( TRANS("Are you certain?"), TRANS("Are you certain you want to overwrite the preset named ") + juce::String(src_bank->presets[idx].name) + "?", std::vector{"Yes", "No", "Yes to all", "Cancel"}, diff --git a/plugin/components/tokenizer.cpp b/plugin/components/tokenizer.cpp new file mode 100644 index 0000000..046f756 --- /dev/null +++ b/plugin/components/tokenizer.cpp @@ -0,0 +1,29 @@ +#include "tokenizer.h" +#include "tokenizer_functions.h" + + +JSFXTokenizer::JSFXTokenizer() { + +} + +void JSFXTokenizer::setColours(std::map> colormap) +{ + std::vector ideColors{ + "error", "comment", "builtin_variable", "builtin_function", "builtin_core_function", + "builtin_section", "operator", "identifier", "integer", "float", "string", "bracket", + "punctuation", "preprocessor_text", "string_hash" + }; + + for (auto const& key : ideColors) + m_colourScheme.set(key, juce::Colour(colormap[key][0], colormap[key][1], colormap[key][2])); +} + +juce::CodeEditorComponent::ColourScheme JSFXTokenizer::getDefaultColourScheme() +{ + return m_colourScheme; +} + +int JSFXTokenizer::readNextToken (juce::CodeDocument::Iterator& source) +{ + return JSFXTokenizerFunctions::readNextJSFXToken(source); +} diff --git a/plugin/components/tokenizer.h b/plugin/components/tokenizer.h new file mode 100644 index 0000000..4ec0b54 --- /dev/null +++ b/plugin/components/tokenizer.h @@ -0,0 +1,36 @@ +#include +#include +#include +#include + +class JSFXTokenizer : public juce::CPlusPlusCodeTokeniser +{ + public: + JSFXTokenizer(); + void setColours(std::map> colormap); + juce::CodeEditorComponent::ColourScheme getDefaultColourScheme() override; + + /** The token values returned by this tokeniser. */ + enum TokenType + { + tokenType_error = 0, + tokenType_comment, + tokenType_builtin_variable, + tokenType_builtin_function, + tokenType_builtin_core_function, + tokenType_builtin_section, + tokenType_operator, + tokenType_identifier, + tokenType_integer, + tokenType_float, + tokenType_string, + tokenType_bracket, + tokenType_punctuation, + tokenType_preprocessor, + tokenType_string_hash, + }; + + private: + int readNextToken(juce::CodeDocument::Iterator& source); + juce::CodeEditorComponent::ColourScheme m_colourScheme; +}; diff --git a/plugin/components/tokenizer_functions.h b/plugin/components/tokenizer_functions.h new file mode 100644 index 0000000..8bde6c0 --- /dev/null +++ b/plugin/components/tokenizer_functions.h @@ -0,0 +1,400 @@ +#include + + +namespace JSFXTokenizerFunctions { + static bool isIdentifierStart(const juce::juce_wchar c) noexcept + { + return juce::CharacterFunctions::isLetter(c) || c == '_' || c == '@'; + } + + static bool isSectionLike(juce::String::CharPointerType token, const int tokenLength) noexcept + { + static const char* const keywords2Char[] = { nullptr }; + static const char* const keywords3Char[] = { nullptr }; + static const char* const keywords4Char[] = { "@gfx", "desc", "tags", nullptr }; + static const char* const keywords5Char[] = { "@init", nullptr }; + static const char* const keywords6Char[] = { "@block", "import", "in_pin", nullptr }; + static const char* const keywords7Char[] = { "@sample", "@slider", "out_pin", nullptr }; + static const char* const keywords8Char[] = { nullptr }; + static const char* const keywords9Char[] = { nullptr }; + static const char* const keywords10Char[] = { "@serialize", nullptr }; + static const char* const keywordsOther[] = { nullptr }; + + const char* const* k; + + switch (tokenLength) + { + case 2: k = keywords2Char; break; + case 3: k = keywords3Char; break; + case 4: k = keywords4Char; break; + case 5: k = keywords5Char; break; + case 6: k = keywords6Char; break; + case 7: k = keywords7Char; break; + case 8: k = keywords8Char; break; + case 9: k = keywords9Char; break; + case 10: k = keywords10Char; break; + + default: + if (tokenLength < 2 || tokenLength > 16) + return false; + + k = keywordsOther; + break; + } + + for (int i = 0; k[i] != nullptr; ++i) + if (token.compare(juce::CharPointer_ASCII(k[i])) == 0) + return true; + + return false; + } + + static bool isCoreFuncLike(juce::String::CharPointerType token, const int tokenLength) noexcept + { + static const char* const keywords2Char[] = { nullptr }; + static const char* const keywords3Char[] = { nullptr }; + static const char* const keywords4Char[] = { "loop", "this", nullptr }; + static const char* const keywords5Char[] = { "local", nullptr }; + static const char* const keywords6Char[] = { "global", nullptr }; + static const char* const keywords7Char[] = { "_global", nullptr }; + static const char* const keywords8Char[] = { "function", "instance", nullptr }; + static const char* const keywordsOther[] = { nullptr }; + + const char* const* k; + + switch (tokenLength) + { + case 2: k = keywords2Char; break; + case 3: k = keywords3Char; break; + case 4: k = keywords4Char; break; + case 5: k = keywords5Char; break; + case 6: k = keywords6Char; break; + case 7: k = keywords7Char; break; + case 8: k = keywords8Char; break; + + default: + if (tokenLength < 2 || tokenLength > 16) + return false; + + k = keywordsOther; + break; + } + + for (int i = 0; k[i] != nullptr; ++i) + if (token.compare(juce::CharPointer_ASCII(k[i])) == 0) + return true; + + return false; + } + + static bool isBuiltinFunction(juce::String::CharPointerType token, const int tokenLength) noexcept + { + static const char* const keywords2Char[] = { nullptr }; + static const char* const keywords3Char[] = { "abs", "cos", "exp", "fft", "log", "max", "min", "pow", "sin", "spl", "sqr", "tan", nullptr }; + static const char* const keywords4Char[] = { "acos", "asin", "atan", "ceil", "ifft", "mdct", "rand", "sign", "sqrt", nullptr }; + static const char* const keywords5Char[] = { "atan2", "floor", "log10", "match", nullptr }; + static const char* const keywords6Char[] = { "matchi", "memcpy", "memset", "slider", "strcat", "strcmp", "strcpy", "strlen", nullptr }; + static const char* const keywords7Char[] = { "_memtop", "gfx_arc", "gfx_set", "invsqrt", "midisyx", "sprintf", "stricmp", "strncat", "strncmp", "strncpy", nullptr }; + static const char* const keywords8Char[] = { "fft_real", "file_mem", "file_var", "freembuf", "gfx_blit", "gfx_line", "gfx_rect", "midirecv", "midisend", "strnicmp", nullptr }; + static const char* const keywords9Char[] = { "file_open", "file_riff", "file_text", "ifft_real", "stack_pop", nullptr }; + static const char* const keywords10Char[] = { "atomic_add", "atomic_get", "atomic_set", "convolve_c", "file_avail", "file_close", "gfx_blurto", "gfx_circle", "gfx_lineto", "gfx_printf", "gfx_rectto", "stack_exch", "stack_peek", "stack_push", nullptr }; + static const char* const keywordsOther[] = { "atomic_exch", "atomic_setifequal", "fft_permute", "file_rewind", "file_string", "gfx_blitext", "gfx_deltablit", "gfx_drawchar", "gfx_drawnumber", "gfx_drawstr", "gfx_getchar", "gfx_getfont", "gfx_getimgdim", "gfx_getpixel", "gfx_gradrect", "gfx_loadimg", "gfx_measurestr", "gfx_muladdrect", "gfx_roundrect", "gfx_setcursor", "gfx_setfont", "gfx_setimgdim", "gfx_setpixel", "gfx_showmenu", "gfx_transformblit", "gfx_triangle", "ifft_permute", "mem_get_values", "mem_insert_shuffle", "mem_multiply_sum", "mem_set_values", "midirecv_buf", "midirecv_str", "midisend_buf", "midisend_str", "slider_automate", "slider_next_chg", "slider_show", "sliderchange", "str_getchar", "str_setchar", "strcpy_from", "strcpy_fromslider", "strcpy_substr", nullptr }; + const char* const* k; + + switch (tokenLength) + { + case 2: k = keywords2Char; break; + case 3: k = keywords3Char; break; + case 4: k = keywords4Char; break; + case 5: k = keywords5Char; break; + case 6: k = keywords6Char; break; + case 7: k = keywords7Char; break; + case 8: k = keywords8Char; break; + case 9: k = keywords9Char; break; + case 10: k = keywords10Char; break; + + default: + if (tokenLength < 2 || tokenLength > 16) + return false; + + k = keywordsOther; + break; + } + + for (int i = 0; k[i] != nullptr; ++i) + if (token.compare(juce::CharPointer_ASCII(k[i])) == 0) + return true; + + return false; + } + + static bool isBuiltinVar(juce::String::CharPointerType token, const int tokenLength) noexcept + { + static const char* const keywords2Char[] = { nullptr }; + static const char* const keywords3Char[] = { nullptr }; + static const char* const keywords4Char[] = { "reg0", "reg1", "reg2", "reg3", "reg4", "reg5", "reg6", "reg7", "reg8", "reg9", "spl0", "spl1", "spl2", "spl3", "spl4", "spl5", "spl6", "spl7", "spl8", "spl9", nullptr }; + static const char* const keywords5Char[] = { "gfx_a", "gfx_b", "gfx_g", "gfx_h", "gfx_r", "gfx_w", "gfx_x", "gfx_y", "reg10", "reg11", "reg12", "reg13", "reg14", "reg15", "reg16", "reg17", "reg18", "reg19", "reg20", "reg21", "reg22", "reg23", "reg24", "reg25", "reg26", "reg27", "reg28", "reg29", "reg30", "reg31", "reg32", "reg33", "reg34", "reg35", "reg36", "reg37", "reg38", "reg39", "reg40", "reg41", "reg42", "reg43", "reg44", "reg45", "reg46", "reg47", "reg48", "reg49", "reg50", "reg51", "reg52", "reg53", "reg54", "reg55", "reg56", "reg57", "reg58", "reg59", "reg60", "reg61", "reg62", "reg63", "reg64", "reg65", "reg66", "reg67", "reg68", "reg69", "reg70", "reg71", "reg72", "reg73", "reg74", "reg75", "reg76", "reg77", "reg78", "reg79", "reg80", "reg81", "reg82", "reg83", "reg84", "reg85", "reg86", "reg87", "reg88", "reg89", "reg90", "reg91", "reg92", "reg93", "reg94", "reg95", "reg96", "reg97", "reg98", "reg99", "spl10", "spl11", "spl12", "spl13", "spl14", "spl15", "spl16", "spl17", "spl18", "spl19", "spl20", "spl21", "spl22", "spl23", "spl24", "spl25", "spl26", "spl27", "spl28", "spl29", "spl30", "spl31", "spl32", "spl33", "spl34", "spl35", "spl36", "spl37", "spl38", "spl39", "spl40", "spl41", "spl42", "spl43", "spl44", "spl45", "spl46", "spl47", "spl48", "spl49", "spl50", "spl51", "spl52", "spl53", "spl54", "spl55", "spl56", "spl57", "spl58", "spl59", "spl60", "spl61", "spl62", "spl63", "tempo", nullptr }; + static const char* const keywords6Char[] = { "ts_num", nullptr }; + static const char* const keywords7Char[] = { "mouse_x", "mouse_y", "slider0", "slider1", "slider2", "slider3", "slider4", "slider5", "slider6", "slider7", "slider8", "slider9", "trigger", nullptr }; + static const char* const keywords8Char[] = { "gfx_dest", "gfx_mode", "midi_bus", "pdc_midi", "slider10", "slider11", "slider12", "slider13", "slider14", "slider15", "slider16", "slider17", "slider18", "slider19", "slider20", "slider21", "slider22", "slider23", "slider24", "slider25", "slider26", "slider27", "slider28", "slider29", "slider30", "slider31", "slider32", "slider33", "slider34", "slider35", "slider36", "slider37", "slider38", "slider39", "slider40", "slider41", "slider42", "slider43", "slider44", "slider45", "slider46", "slider47", "slider48", "slider49", "slider50", "slider51", "slider52", "slider53", "slider54", "slider55", "slider56", "slider57", "slider58", "slider59", "slider60", "slider61", "slider62", "slider63", "slider64", "slider65", "slider66", "slider67", "slider68", "slider69", "slider70", "slider71", "slider72", "slider73", "slider74", "slider75", "slider76", "slider77", "slider78", "slider79", "slider80", "slider81", "slider82", "slider83", "slider84", "slider85", "slider86", "slider87", "slider88", "slider89", "slider90", "slider91", "slider92", "slider93", "slider94", "slider95", "slider96", "slider97", "slider98", "slider99", "ts_denom", nullptr }; + static const char* const keywords9Char[] = { "gfx_clear", "gfx_texth", "gfx_textw", "mouse_cap", "pdc_delay", "slider100", "slider101", "slider102", "slider103", "slider104", "slider105", "slider106", "slider107", "slider108", "slider109", "slider110", "slider111", "slider112", "slider113", "slider114", "slider115", "slider116", "slider117", "slider118", "slider119", "slider120", "slider121", "slider122", "slider123", "slider124", "slider125", "slider126", "slider127", "slider128", "slider129", "slider130", "slider131", "slider132", "slider133", "slider134", "slider135", "slider136", "slider137", "slider138", "slider139", "slider140", "slider141", "slider142", "slider143", "slider144", "slider145", "slider146", "slider147", "slider148", "slider149", "slider150", "slider151", "slider152", "slider153", "slider154", "slider155", "slider156", "slider157", "slider158", "slider159", "slider160", "slider161", "slider162", "slider163", "slider164", "slider165", "slider166", "slider167", "slider168", "slider169", "slider170", "slider171", "slider172", "slider173", "slider174", "slider175", "slider176", "slider177", "slider178", "slider179", "slider180", "slider181", "slider182", "slider183", "slider184", "slider185", "slider186", "slider187", "slider188", "slider189", "slider190", "slider191", "slider192", "slider193", "slider194", "slider195", "slider196", "slider197", "slider198", "slider199", "slider200", "slider201", "slider202", "slider203", "slider204", "slider205", "slider206", "slider207", "slider208", "slider209", "slider210", "slider211", "slider212", "slider213", "slider214", "slider215", "slider216", "slider217", "slider218", "slider219", "slider220", "slider221", "slider222", "slider223", "slider224", "slider225", "slider226", "slider227", "slider228", "slider229", "slider230", "slider231", "slider232", "slider233", "slider234", "slider235", "slider236", "slider237", "slider238", "slider239", "slider240", "slider241", "slider242", "slider243", "slider244", "slider245", "slider246", "slider247", "slider248", "slider249", "slider250", "slider251", "slider252", "slider253", "slider254", "slider255", nullptr }; + static const char* const keywords10Char[] = { "ext_noinit", "pdc_bot_ch", "pdc_top_ch", "play_state", nullptr }; + static const char* const keywordsOther[] = { "beat_position", "ext_midi_bus", "ext_nodenorm", "ext_tail_size", "gfx_ext_flags", "gfx_ext_retina", "mouse_hwheel", "mouse_wheel", "play_position", "samplesblock", "sratenum_ch", nullptr }; + const char* const* k; + + switch (tokenLength) + { + case 2: k = keywords2Char; break; + case 3: k = keywords3Char; break; + case 4: k = keywords4Char; break; + case 5: k = keywords5Char; break; + case 6: k = keywords6Char; break; + case 7: k = keywords7Char; break; + case 8: k = keywords8Char; break; + case 9: k = keywords9Char; break; + case 10: k = keywords10Char; break; + + default: + if (tokenLength < 2 || tokenLength > 16) + return false; + + k = keywordsOther; + break; + } + + for (int i = 0; k[i] != nullptr; ++i) + if (token.compare(juce::CharPointer_ASCII(k[i])) == 0) + return true; + + return false; + } + + + template + static int parseIdentifier (Iterator& source) noexcept + { + int tokenLength = 0; + juce::String::CharPointerType::CharType possibleIdentifier[100] = {}; + juce::String::CharPointerType possible (possibleIdentifier); + + while (juce::CppTokeniserFunctions::isIdentifierBody(source.peekNextChar())) + { + auto c = source.nextChar(); + + if (tokenLength < 20) + possible.write(c); + + ++tokenLength; + } + + if (tokenLength > 1 && tokenLength <= 16) + { + possible.writeNull(); + + if (JSFXTokenizerFunctions::isSectionLike(juce::String::CharPointerType(possibleIdentifier), tokenLength)) + return JSFXTokenizer::tokenType_builtin_section; + + if (JSFXTokenizerFunctions::isBuiltinVar(juce::String::CharPointerType(possibleIdentifier), tokenLength)) + return JSFXTokenizer::tokenType_builtin_variable; + + if (JSFXTokenizerFunctions::isCoreFuncLike(juce::String::CharPointerType(possibleIdentifier), tokenLength)) + return JSFXTokenizer::tokenType_builtin_core_function; + + if (JSFXTokenizerFunctions::isBuiltinFunction(juce::String::CharPointerType(possibleIdentifier), tokenLength)) + return JSFXTokenizer::tokenType_builtin_function; + } + + return JSFXTokenizer::tokenType_identifier; + } + + template + static int parseNumber(Iterator& source) + { + const Iterator original (source); + + if (juce::CppTokeniserFunctions::parseFloatLiteral(source)) return JSFXTokenizer::tokenType_float; + source = original; + + if (juce::CppTokeniserFunctions::parseHexLiteral(source)) return JSFXTokenizer::tokenType_integer; + source = original; + + if (juce::CppTokeniserFunctions::parseOctalLiteral(source)) return JSFXTokenizer::tokenType_integer; + source = original; + + if (juce::CppTokeniserFunctions::parseDecimalLiteral(source)) return JSFXTokenizer::tokenType_integer; + source = original; + + return JSFXTokenizer::tokenType_error; + } + + template + static int readNextJSFXToken (Iterator& source) + { + source.skipWhitespace(); + auto firstChar = source.peekNextChar(); + + switch (firstChar) + { + case 0: + break; + + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + case '.': + { + auto result = JSFXTokenizerFunctions::parseNumber(source); + + if (result == JSFXTokenizer::tokenType_error) + { + source.skip(); + + if (firstChar == '.') + return JSFXTokenizer::tokenType_punctuation; + } + + return result; + } + + case ',': + case ';': + case ':': + source.skip(); + return JSFXTokenizer::tokenType_punctuation; + + case '(': case ')': + case '{': case '}': + case '[': case ']': + source.skip(); + return JSFXTokenizer::tokenType_bracket; + + // TODO: Parse the header separately. + // Strings cause a lot of issues right now, since in the header we can get random text which + // can have an open ended string. So we explicitly leave multi-line strings out for now. + case '"': + case '\'': + { + int rollback = 0; + auto c = source.nextChar(); + while (c != '\r' && c != '\n' && c != 0) { + rollback += 1; + c = source.nextChar(); + + // The line terminated within a line, so we can read it without trouble + if (c == firstChar) { + return JSFXTokenizer::tokenType_string; + } + } + for (auto i = 0; i < rollback; i++) source.previousChar(); + + return JSFXTokenizer::tokenType_punctuation; + } + case '+': + source.skip(); + juce::CppTokeniserFunctions::skipIfNextCharMatches(source, '='); + return JSFXTokenizer::tokenType_operator; + + case '-': + { + source.skip(); + auto result = JSFXTokenizerFunctions::parseNumber(source); + + if (result == JSFXTokenizer::tokenType_error) + { + juce::CppTokeniserFunctions::skipIfNextCharMatches(source, '-', '='); + return JSFXTokenizer::tokenType_operator; + } + + return result; + } + + case '*': case '%': + case '=': case '!': + source.skip(); + juce::CppTokeniserFunctions::skipIfNextCharMatches(source, '='); + return JSFXTokenizer::tokenType_operator; + + case '/': + { + auto previousChar = source.peekPreviousChar(); + source.skip(); + auto nextChar = source.peekNextChar(); + + if (nextChar == '/') + { + source.skipToEndOfLine(); + return JSFXTokenizer::tokenType_comment; + } + + if (nextChar == '*') + { + // TODO: Parse the header separately. + // This is a poor workaround for dealing with paths that have /* in them in the header. + if (!juce::CharacterFunctions::isLetter(previousChar)) { + source.skip(); + juce::CppTokeniserFunctions::skipComment(source); + return JSFXTokenizer::tokenType_comment; + } else { + return JSFXTokenizer::tokenType_operator; + } + } + + if (nextChar == '=') + source.skip(); + + return JSFXTokenizer::tokenType_operator; + } + + case '?': + { + source.skip(); + auto nextChar2 = source.peekNextChar(); + if (nextChar2 == '>') { + source.skip(); + return JSFXTokenizer::tokenType_preprocessor; + } + return JSFXTokenizer::tokenType_operator; + } + + case '~': + source.skip(); + return JSFXTokenizer::tokenType_operator; + + case '<': + { + source.skip(); + auto nextChar3 = source.peekNextChar(); + if (nextChar3 == '?') { + source.skip(); + return JSFXTokenizer::tokenType_preprocessor; + } + return JSFXTokenizer::tokenType_operator; + } + + case '>': case '|': case '&': case '^': + source.skip(); + juce::CppTokeniserFunctions::skipIfNextCharMatches(source, firstChar); + juce::CppTokeniserFunctions::skipIfNextCharMatches(source, '='); + return JSFXTokenizer::tokenType_operator; + + case '#': + { + source.skip(); + auto ch = source.peekNextChar(); + while (juce::CharacterFunctions::isLetterOrDigit(ch) || ch == '_') { + source.skip(); + ch = source.peekNextChar(); + } + return JSFXTokenizer::tokenType_string_hash; + } + + default: + if (JSFXTokenizerFunctions::isIdentifierStart(firstChar)) + return JSFXTokenizerFunctions::parseIdentifier(source); + + source.skip(); + break; + } + + return JSFXTokenizer::tokenType_error; + } +} diff --git a/plugin/editor.cpp b/plugin/editor.cpp index 9e09dfc..10d04f7 100644 --- a/plugin/editor.cpp +++ b/plugin/editor.cpp @@ -42,6 +42,7 @@ struct YsfxEditor::Impl { YsfxCurrentPresetInfo::Ptr m_currentPresetInfo; ysfx_bank_shared m_bank; std::unique_ptr m_editDialog; + std::unique_ptr m_modalAlert; std::unique_ptr m_infoTimer; std::unique_ptr m_relayoutTimer; std::unique_ptr m_fileChooser; @@ -70,6 +71,7 @@ struct YsfxEditor::Impl { void switchEditor(bool showGfx); void openCodeEditor(); void openPresetWindow(); + void quickAlertBox(bool confirmationRequired, std::function callbackOnSuccess, juce::Component* parent); static juce::File getAppDataDirectory(); static juce::File getDefaultEffectsDirectory(); juce::RecentlyOpenedFilesList loadRecentFiles(); @@ -183,8 +185,9 @@ void YsfxEditor::readTheme() if (!file.existsAsFile()) { try { writeThemeFile(file, getDefaultColors(), getDefaultParams()); - setColors(getLookAndFeel(), {}); - setParams(getLookAndFeel(), {}); + setColors(getLookAndFeel(), getDefaultColors()); + setParams(getLookAndFeel(), getDefaultParams()); + m_impl->m_ideView->setColourScheme(getDefaultColors()); } catch (nlohmann::json::exception e) { // Log: std::cout << "Failed to write theme: " << e.what() << std::endl; } @@ -197,6 +200,7 @@ void YsfxEditor::readTheme() // Fallback for version 1 files (upconvert the file) if (!jsonFile.contains("version")) { auto readTheme = jsonFile[0].get>>(); + readTheme = fillMissingColors(readTheme); writeThemeFile(file, readTheme, getDefaultParams()); // Reread it! @@ -207,8 +211,13 @@ void YsfxEditor::readTheme() auto readTheme = jsonFile.at("colors")[0].get>>(); auto readParams = jsonFile.at("params")[0].get>(); + readTheme = fillMissingColors(readTheme); + readParams = fillMissingParams(readParams); + setColors(getLookAndFeel(), readTheme); setParams(getLookAndFeel(), readParams); + m_impl->m_ideView->setColourScheme(readTheme); + writeThemeFile(file, readTheme, readParams); } catch (nlohmann::json::exception e) { // Log: std::cout << "Failed to read theme: " << e.what() << std::endl; } @@ -426,20 +435,19 @@ void YsfxEditor::Impl::updateInfo() relayoutUILater(); } -void _quickAlertBox(bool confirmationRequired, std::function callbackOnSuccess, juce::Component* parent) +void YsfxEditor::Impl::quickAlertBox(bool confirmationRequired, std::function callbackOnSuccess, juce::Component* parent) { if (confirmationRequired) { - juce::AlertWindow::showAsync( - juce::MessageBoxOptions() - .withTitle("Are you certain?") - .withMessage("Are you certain you want to (re)load the plugin?\n\nNote that you will lose your current preset.") - .withButton("Yes") - .withButton("No") - .withParentComponent(parent) - .withIconType(juce::MessageBoxIconType::NoIcon), - [callbackOnSuccess](int result){ - if (result == 1) callbackOnSuccess(); - } + m_modalAlert.reset( + show_option_window( + "Are you certain?", + "Are you certain you want to (re)load the plugin?\n\nNote that you will lose your current preset.", + std::vector{"Yes", "No"}, + [callbackOnSuccess](int result){ + if (result == 1) callbackOnSuccess(); + }, + parent + ) ); } else { callbackOnSuccess(); @@ -483,7 +491,7 @@ void YsfxEditor::Impl::chooseFileAndLoad() [this, normalLoad, mustAskConfirmation](const juce::FileChooser &chooser) { juce::File result = chooser.getResult(); if (result != juce::File()) { - _quickAlertBox( + quickAlertBox( mustAskConfirmation, [this, normalLoad, result]() { if (normalLoad) saveScaling(); @@ -591,7 +599,7 @@ void YsfxEditor::Impl::popupRecentFiles() m_recentFilesPopup->showMenuAsync(popupOptions, [this, recent](int index) { if (index != 0) { juce::File selectedFile = recent.getFile(index - 100); - _quickAlertBox( + quickAlertBox( ysfx_is_compiled(m_info->effect.get()), [this, selectedFile]() { saveScaling(); @@ -668,23 +676,24 @@ void YsfxEditor::Impl::popupPresetOptions() std::string preset = presetName.toStdString(); if (wantSave) { if (m_proc->presetExists(preset.c_str())) { - juce::AlertWindow::showAsync( - juce::MessageBoxOptions() - .withTitle("Overwrite?") - .withMessage("Preset with that name already exists.\nAre you sure you want to overwrite the preset?") - .withButton("Yes") - .withButton("No") - .withParentComponent(this->m_self) - .withIconType(juce::MessageBoxIconType::NoIcon), - [this, preset](int result){ - if (result == 1) m_proc->saveCurrentPreset(preset.c_str()); - } + this->m_modalAlert.reset( + show_option_window( + "Overwrite?", + "Preset with that name already exists.\nAre you sure you want to overwrite the preset?", + std::vector{"Yes", "No"}, + [this, preset](int result){ + if (result == 1) m_proc->saveCurrentPreset(preset.c_str()); + }, + this->m_self + ) ); } else { m_proc->saveCurrentPreset(preset.c_str()); } } - } + }, + std::nullopt, + this->m_self ) ); return; @@ -706,7 +715,8 @@ void YsfxEditor::Impl::popupPresetOptions() } else { return juce::String(""); } - } + }, + this->m_self ) ); break; @@ -718,17 +728,16 @@ void YsfxEditor::Impl::popupPresetOptions() break; case 5: // Delete - juce::AlertWindow::showAsync( - juce::MessageBoxOptions() - .withTitle("Delete?") - .withMessage("Are you sure you want to delete the preset named " + m_currentPresetInfo->m_lastChosenPreset + "?") - .withButton("Yes") - .withButton("No") - .withParentComponent(this->m_self) - .withIconType(juce::MessageBoxIconType::NoIcon), + this->m_modalAlert.reset( + show_option_window( + "Delete?", + "Are you sure you want to delete the preset named " + m_currentPresetInfo->m_lastChosenPreset + "?", + std::vector{"Yes", "No"}, [this](int result) { if (result == 1) m_proc->deleteCurrentPreset(); - } + }, + this->m_self + ) ); break; case 6: @@ -951,7 +960,7 @@ void YsfxEditor::Impl::connectUI() YsfxInfo::Ptr info = m_info; ysfx_t *fx = info->effect.get(); juce::File file{juce::CharPointer_UTF8{ysfx_get_file_path(fx)}}; - _quickAlertBox( + quickAlertBox( ysfx_is_compiled(fx), [this, file]() { resetScaling(file); diff --git a/plugin/lookandfeel.cpp b/plugin/lookandfeel.cpp index 84f11b1..879174c 100644 --- a/plugin/lookandfeel.cpp +++ b/plugin/lookandfeel.cpp @@ -29,7 +29,22 @@ std::map> getDefaultColors() {"off_fill", std::array{16, 16, 16}}, {"selection_fill", std::array{65, 65, 65}}, {"font_color", std::array{189, 189, 189}}, - {"font_color_light", std::array{210, 210, 210}} + {"font_color_light", std::array{210, 210, 210}}, + {"error", std::array{255, 204, 00}}, + {"comment", std::array{96, 128, 192}}, + {"builtin_variable", std::array{255, 128, 128}}, + {"builtin_function", std::array{255, 255, 48}}, + {"builtin_core_function", std::array{0, 192, 255}}, + {"builtin_section", std::array{0, 255, 255}}, + {"operator", std::array{0, 255, 255}}, + {"identifier", std::array{192, 192, 192}}, + {"integer", std::array{0, 255, 0}}, + {"float", std::array{0, 255, 0}}, + {"string", std::array{255, 192, 192}}, + {"bracket", std::array{192, 192, 255}}, + {"punctuation", std::array{0, 255, 255}}, + {"preprocessor_text", std::array{32, 192, 255}}, + {"string_hash", std::array{192, 255, 128}} }; } @@ -43,16 +58,11 @@ std::map getDefaultParams() void setParams(juce::LookAndFeel& lnf, std::map params) { - std::map currentParams = getDefaultParams(); - for (auto it = params.begin(); it != params.end(); ++it) { - currentParams[it->first] = it->second; - } - - auto get = [currentParams](std::string key) { - auto it = currentParams.find(key); - jassert(it != currentParams.end()); // This color doesn't have a default! + auto get = [params](std::string key) { + auto it = params.find(key); + jassert(it != params.end()); // This parameter doesn't have a default! - if (it != currentParams.end()) { + if (it != params.end()) { return it->second; } else { return 1.0f; @@ -64,22 +74,37 @@ void setParams(juce::LookAndFeel& lnf, std::map params) ysfx_lnf.m_pad = static_cast(get("left_pad")); } -void setColors(juce::LookAndFeel& lnf, std::map> colormap) +std::map fillMissingParams(std::map params) { + std::map currentParams = getDefaultParams(); + for (auto it = params.begin(); it != params.end(); ++it) { + currentParams[it->first] = it->second; + } + + return currentParams; +} + +// Grabs a complete theme from a possibly incomplete theme +std::map> fillMissingColors(std::map> colormap) { std::map> currentColorMap = getDefaultColors(); for (auto it = colormap.begin(); it != colormap.end(); ++it) { currentColorMap[it->first] = it->second; } - auto get = [currentColorMap](std::string key) { - auto it = currentColorMap.find(key); - jassert(it != currentColorMap.end()); // This color doesn't have a default! + return currentColorMap; +} - if (it != currentColorMap.end()) { +void setColors(juce::LookAndFeel& lnf, std::map> colormap) +{ + auto get = [colormap](std::string key) { + auto it = colormap.find(key); + jassert(it != colormap.end()); // This color doesn't have a default! + + if (it != colormap.end()) { return juce::Colour(int(it->second[0]), int(it->second[1]), int(it->second[2])); } else { return juce::Colour(255, 200, 200); - } + } }; juce::Colour backgroundColour = get("background"); diff --git a/plugin/lookandfeel.h b/plugin/lookandfeel.h index 02d507e..f22763c 100644 --- a/plugin/lookandfeel.h +++ b/plugin/lookandfeel.h @@ -23,6 +23,8 @@ void setColors(juce::LookAndFeel& lnf, std::map params); std::map> getDefaultColors(); std::map getDefaultParams(); +std::map fillMissingParams(std::map params); +std::map> fillMissingColors(std::map> colormap); class YsfxLookAndFeel : public juce::LookAndFeel_V4 { @@ -32,7 +34,7 @@ class YsfxLookAndFeel : public juce::LookAndFeel_V4 { YsfxLookAndFeel() { - setColors(*this, {}); + setColors(*this, getDefaultColors()); } void drawLinearSlider(juce::Graphics& g, int x, int y, int width, int height, float sliderPos, float minSliderPos, float maxSliderPos, const juce::Slider::SliderStyle style, juce::Slider& slider) override diff --git a/plugin/processor.cpp b/plugin/processor.cpp index dc637cf..c67a72f 100644 --- a/plugin/processor.cpp +++ b/plugin/processor.cpp @@ -1069,8 +1069,16 @@ void YsfxProcessor::Impl::Background::processLoadRequest(LoadRequest &req) m_impl->m_lastLoadPath = req.filePath; if (!ysfx_is_compiled(m_impl->m_fx.get())) { if (req.initialState) { - m_impl->m_failedLoadState.reset(ysfx_state_dup(req.initialState.get())); - m_impl->m_failedLoad.store(RetryState::mustRetry); // aww + if (!juce::File(req.filePath).existsAsFile()) + { + // If it is missing, we need to prompt the user to find the file, and NOT lose the state + m_impl->m_failedLoadState.reset(ysfx_state_dup(req.initialState.get())); + m_impl->m_failedLoad.store(RetryState::mustRetry); + } else { + // If it is just erroneous, then the state is gonna be useless and we drop it + m_impl->m_failedLoadState.reset(nullptr); + m_impl->m_failedLoad.store(RetryState::ok); + } } } else { // Successful compile this time. We can let it go.