From 8b11433cbd2f2abf347e25af6ab4e12a24b38619 Mon Sep 17 00:00:00 2001 From: twanvl Date: Tue, 12 Jun 2007 19:35:24 +0000 Subject: [PATCH] Reader now warns about invalid UTF-8 files; Fixed possible hang when reading multiline strings with incorrect indentation; Warnings from reading are shown also in NewSetWindow; Script parse errors get reported with the correct line number; Added support for showing multiple choice fields as a single image; Added 'line_below' to ChoiceField::Choice, for putting a line below menu items. git-svn-id: svn://svn.code.sf.net/p/magicseteditor/code/trunk@420 0fc631ac-6414-0410-93d0-97cfa31319b6 --- src/data/field/choice.cpp | 9 +- src/data/field/choice.hpp | 12 ++- src/data/field/multiple_choice.cpp | 1 + src/data/field/multiple_choice.hpp | 7 +- src/gui/drop_down_list.cpp | 30 ++++-- src/gui/drop_down_list.hpp | 6 ++ src/gui/new_window.cpp | 8 ++ src/gui/new_window.hpp | 3 +- src/gui/util.cpp | 4 +- src/gui/util.hpp | 2 +- src/gui/value/choice.cpp | 23 ++++- src/gui/value/choice.hpp | 5 +- src/gui/value/multiple_choice.cpp | 12 ++- src/render/value/multiple_choice.cpp | 45 ++++++++- src/script/functions/editor.cpp | 39 +++++++- src/script/parser.cpp | 2 +- src/script/scriptable.cpp | 16 +++- src/script/scriptable.hpp | 1 + src/util/io/reader.cpp | 132 +++++++++++++++++++++------ src/util/io/reader.hpp | 47 +++++----- 20 files changed, 318 insertions(+), 86 deletions(-) diff --git a/src/data/field/choice.cpp b/src/data/field/choice.cpp index 5d052271..a750da86 100644 --- a/src/data/field/choice.cpp +++ b/src/data/field/choice.cpp @@ -49,9 +49,11 @@ IMPLEMENT_REFLECTION(ChoiceField) { ChoiceField::Choice::Choice() : first_id(0) + , line_below(false), enabled(true) {} ChoiceField::Choice::Choice(const String& name) : name(name), first_id(0) + , line_below(false), enabled(true) {} @@ -138,11 +140,13 @@ String ChoiceField::Choice::choiceNameNice(int id) const { IMPLEMENT_REFLECTION_NO_GET_MEMBER(ChoiceField::Choice) { - if (isGroup() || (tag.reading() && tag.isComplex())) { + if (isGroup() || line_below || enabled.isScripted() || (tag.reading() && tag.isComplex())) { // complex values are groups REFLECT(name); REFLECT_N("group_choice", default_name); REFLECT(choices); + REFLECT(line_below); + REFLECT(enabled); } else { REFLECT_NAMELESS(name); } @@ -237,6 +241,9 @@ IMPLEMENT_REFLECTION_ENUM(ChoiceRenderStyle) { VALUE_N("checklist", RENDER_TEXT_CHECKLIST); VALUE_N("image checklist", RENDER_IMAGE_CHECKLIST); VALUE_N("both checklist", RENDER_BOTH_CHECKLIST); + VALUE_N("text list", RENDER_IMAGE_LIST); + VALUE_N("image list", RENDER_IMAGE_LIST); + VALUE_N("both list", RENDER_IMAGE_LIST); } IMPLEMENT_REFLECTION(ChoiceStyle) { diff --git a/src/data/field/choice.hpp b/src/data/field/choice.hpp index 384a56e3..c2ff29fe 100644 --- a/src/data/field/choice.hpp +++ b/src/data/field/choice.hpp @@ -52,9 +52,11 @@ class ChoiceField::Choice : public IntrusivePtrBase { Choice(); Choice(const String& name); - String name; ///< Name/value of the item - String default_name; ///< A default item, if this is a group and default_name.empty() there is no default - vector choices; ///< Choices and sub groups in this group + String name; ///< Name/value of the item + String default_name; ///< A default item, if this is a group and default_name.empty() there is no default + vector choices; ///< Choices and sub groups in this group + bool line_below; ///< Show a line after this item? + Scriptable enabled; ///< Is this item enabled? /// First item-id in this group (can be the default item) /** Item-ids are consecutive integers, a group uses all ids [first_id..lastId()). * The top level group has first_id 0. @@ -109,11 +111,15 @@ enum ChoiceRenderStyle , RENDER_IMAGE = 0x10 // render an image , RENDER_HIDDEN = 0x20 // don't render anything, only have a menu , RENDER_CHECKLIST = 0x100 // render as a checklist, intended for multiple choice +, RENDER_LIST = 0x200 // render as a list of images/text, intended for multiple choice , RENDER_BOTH = RENDER_TEXT | RENDER_IMAGE , RENDER_HIDDEN_IMAGE = RENDER_HIDDEN | RENDER_IMAGE , RENDER_TEXT_CHECKLIST = RENDER_CHECKLIST | RENDER_TEXT , RENDER_IMAGE_CHECKLIST = RENDER_CHECKLIST | RENDER_IMAGE , RENDER_BOTH_CHECKLIST = RENDER_CHECKLIST | RENDER_BOTH +, RENDER_TEXT_LIST = RENDER_LIST | RENDER_TEXT +, RENDER_IMAGE_LIST = RENDER_LIST | RENDER_IMAGE +, RENDER_BOTH_LIST = RENDER_LIST | RENDER_BOTH }; enum ThumbnailStatus diff --git a/src/data/field/multiple_choice.cpp b/src/data/field/multiple_choice.cpp index 8a668b41..73a0d280 100644 --- a/src/data/field/multiple_choice.cpp +++ b/src/data/field/multiple_choice.cpp @@ -25,6 +25,7 @@ IMPLEMENT_REFLECTION(MultipleChoiceField) { REFLECT_BASE(ChoiceField); REFLECT(minimum_selection); REFLECT(maximum_selection); + REFLECT(empty_choice); } // ----------------------------------------------------------------------------- : MultipleChoiceStyle diff --git a/src/data/field/multiple_choice.hpp b/src/data/field/multiple_choice.hpp index fdae7edf..08451f27 100644 --- a/src/data/field/multiple_choice.hpp +++ b/src/data/field/multiple_choice.hpp @@ -25,6 +25,7 @@ class MultipleChoiceField : public ChoiceField { DECLARE_FIELD_TYPE(MultipleChoiceField); UInt minimum_selection, maximum_selection; ///< How many choices can be selected simultaniously? + String empty_choice; ///< Name to use when nothing is selected private: DECLARE_REFLECTION(); @@ -56,7 +57,7 @@ class MultipleChoiceValue : public ChoiceValue { inline MultipleChoiceValue(const MultipleChoiceFieldP& field) : ChoiceValue(field, true) {} DECLARE_HAS_FIELD(MultipleChoice); - // no extra data + String last_choice; ///< Which of the choices was selected/deselected last? /// Splits the value, stores the selected choices in the out parameter void get(vector& out) const; @@ -65,6 +66,10 @@ class MultipleChoiceValue : public ChoiceValue { DECLARE_REFLECTION(); }; +// ----------------------------------------------------------------------------- : Utilities + +/// Is the given choice selected in the value? +bool chosen(const String& multiple_choice_value, const String& chioce); // ----------------------------------------------------------------------------- : EOF #endif diff --git a/src/gui/drop_down_list.cpp b/src/gui/drop_down_list.cpp index 468552f5..048f2e98 100644 --- a/src/gui/drop_down_list.cpp +++ b/src/gui/drop_down_list.cpp @@ -84,6 +84,7 @@ DropDownList::~DropDownList() { void DropDownList::show(bool in_place, wxPoint pos) { if (IsShown()) return; + onShow(); // find selection selected_item = selection(); // width @@ -248,6 +249,9 @@ void DropDownList::drawItem(DC& dc, int y, size_t item) { dc.SetBrush (wxSystemSettings::GetColour(wxSYS_COLOUR_HIGHLIGHT)); dc.SetTextForeground(wxSystemSettings::GetColour(wxSYS_COLOUR_HIGHLIGHTTEXT)); dc.DrawRectangle(marginW, y, (int)item_size.width, (int)item_size.height); + } else if (!itemEnabled(item)) { + // mix between foreground and background + dc.SetTextForeground(wxSystemSettings::GetColour(wxSYS_COLOUR_GRAYTEXT)); } else if (highlightItem(item)) { // mix a color between selection and window dc.SetBrush (lerp(wxSystemSettings::GetColour(wxSYS_COLOUR_HIGHLIGHT), @@ -266,7 +270,7 @@ void DropDownList::drawItem(DC& dc, int y, size_t item) { } // draw line below if (lineBelow(item)) { - dc.SetPen(wxSystemSettings::GetColour(wxSYS_COLOUR_3DSHADOW)); + dc.SetPen(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); dc.DrawLine(marginW, y + (int)item_size.height, marginW + (int)item_size.width, y + (int)item_size.height); } } @@ -295,8 +299,10 @@ void DropDownList::onMotion(wxMouseEvent& ev) { for (size_t i = 0 ; i < count ; ++i) { int endY = startY + (int)item_size.height; if (ev.GetY() >= startY && ev.GetY() < endY) { - selected_item = i; - showSubMenu(i, startY); + if (itemEnabled(i)) { + selected_item = i; + showSubMenu(i, startY); + } Refresh(false); return; } @@ -321,18 +327,27 @@ bool DropDownList::onCharInParent(wxKeyEvent& ev) { // sub menu always takes keys return open_sub_menu->onCharInParent(ev); } else { + size_t old_sel = selected_item; switch (k) { case WXK_UP: - if (selected_item > 0) { + while (selected_item > 0) { selected_item -= 1; - Refresh(false); + if (itemEnabled(selected_item)) { + Refresh(false); + return true; + } } + selected_item = old_sel; break; case WXK_DOWN: - if (selected_item + 1 < itemCount()) { + while (selected_item + 1 < itemCount()) { selected_item += 1; - Refresh(false); + if (itemEnabled(selected_item)) { + Refresh(false); + return true; + } } + selected_item = old_sel; break; case WXK_RETURN: if (!showSubMenu()) { @@ -351,6 +366,7 @@ bool DropDownList::onCharInParent(wxKeyEvent& ev) { size_t count = itemCount(); for (size_t i = 0 ; i < count ; ++i) { size_t index = (si + i) % count; + if (!itemEnabled(index)) continue; String c = itemText(index); #ifdef UNICODE if (!c.empty() && toUpper(c.GetChar(0)) == toUpper(ev.GetUnicodeKey())) { diff --git a/src/gui/drop_down_list.hpp b/src/gui/drop_down_list.hpp index f961d93a..60a2ec7f 100644 --- a/src/gui/drop_down_list.hpp +++ b/src/gui/drop_down_list.hpp @@ -40,6 +40,10 @@ class DropDownList : public wxPopupWindow { bool onMouseInParent(wxMouseEvent&, bool open_in_place); protected: + + /// Prepare for showing the list + virtual void onShow() {} + // --------------------------------------------------- : Selection static const size_t NO_SELECTION = (size_t)-1; @@ -59,6 +63,8 @@ class DropDownList : public wxPopupWindow { virtual bool lineBelow(size_t item) const { return false; } /// Should the item be highlighted? virtual bool highlightItem(size_t item) const { return false; } + /// Is the item enabled? + virtual bool itemEnabled(size_t item) const { return true; } // An extra submenu that pops up from an item, or null if there is no popup menu virtual DropDownList* submenu(size_t item) const { return nullptr; } diff --git a/src/gui/new_window.cpp b/src/gui/new_window.cpp index fb5b5fe7..625873ca 100644 --- a/src/gui/new_window.cpp +++ b/src/gui/new_window.cpp @@ -60,6 +60,7 @@ NewSetWindow::NewSetWindow(Window* parent) void NewSetWindow::onGameSelect(wxCommandEvent&) { wxBusyCursor wait; GameP game = game_list->getSelection(); + handle_pending_errors(); settings.default_game = game->name(); GameSettings& gs = settings.gameSettingsFor(*game); stylesheet_list->showData(game->name() + _("-*")); @@ -78,6 +79,7 @@ void NewSetWindow::onStyleSheetSelect(wxCommandEvent&) { // store this as default selection GameP game = game_list ->getSelection(); StyleSheetP stylesheet = stylesheet_list->getSelection(); + handle_pending_errors(); GameSettings& gs = settings.gameSettingsFor(*game); gs.default_stylesheet = stylesheet->name(); UpdateWindowUI(wxUPDATE_UI_RECURSE); @@ -113,10 +115,16 @@ void NewSetWindow::onUpdateUI(wxUpdateUIEvent& ev) { } } +void NewSetWindow::onIdle(wxIdleEvent& ev) { + // Stuff that must be done in the main thread + handle_pending_errors(); +} + BEGIN_EVENT_TABLE(NewSetWindow, wxDialog) EVT_GALLERY_SELECT (ID_GAME_LIST, NewSetWindow::onGameSelect) EVT_GALLERY_SELECT (ID_STYLESHEET_LIST, NewSetWindow::onStyleSheetSelect) EVT_GALLERY_ACTIVATE(ID_STYLESHEET_LIST, NewSetWindow::onStyleSheetActivate) EVT_BUTTON (wxID_OK, NewSetWindow::OnOK) EVT_UPDATE_UI (wxID_ANY, NewSetWindow::onUpdateUI) + EVT_IDLE ( NewSetWindow::onIdle) END_EVENT_TABLE () diff --git a/src/gui/new_window.hpp b/src/gui/new_window.hpp index 2d4c57f8..37508ed4 100644 --- a/src/gui/new_window.hpp +++ b/src/gui/new_window.hpp @@ -42,10 +42,11 @@ class NewSetWindow : public wxDialog { void onStyleSheetSelect (wxCommandEvent&); void onStyleSheetActivate(wxCommandEvent&); - + virtual void OnOK(wxCommandEvent&); void onUpdateUI(wxUpdateUIEvent&); + void onIdle(wxIdleEvent&); // we are done, close the window void done(); diff --git a/src/gui/util.cpp b/src/gui/util.cpp index 5ed7877f..60c71263 100644 --- a/src/gui/util.cpp +++ b/src/gui/util.cpp @@ -202,7 +202,7 @@ void draw_drop_down_arrow(Window* win, DC& dc, const wxRect& rect, bool active) , active ? wxCONTROL_PRESSED : 0); } -void draw_checkbox(Window* win, DC& dc, const wxRect& rect, bool checked) { +void draw_checkbox(Window* win, DC& dc, const wxRect& rect, bool checked, bool enabled) { #if wxUSE_UXTHEME && defined(__WXMSW__) // TODO: Windows version? #endif @@ -210,7 +210,7 @@ void draw_checkbox(Window* win, DC& dc, const wxRect& rect, bool checked) { if (checked) { dc.DrawCheckMark(wxRect(rect.x-1,rect.y-1,rect.width+2,rect.height+2)); } - dc.SetPen(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); + dc.SetPen(wxSystemSettings::GetColour(enabled ? wxSYS_COLOUR_WINDOWTEXT: wxSYS_COLOUR_GRAYTEXT)); dc.SetBrush(*wxTRANSPARENT_BRUSH); dc.DrawRectangle(rect.x, rect.y, rect.width, rect.height); } diff --git a/src/gui/util.hpp b/src/gui/util.hpp index 56ec49c4..392d4313 100644 --- a/src/gui/util.hpp +++ b/src/gui/util.hpp @@ -61,7 +61,7 @@ void draw_menu_arrow(Window* win, DC& dc, const wxRect& rect, bool active); void draw_drop_down_arrow(Window* win, DC& dc, const wxRect& rect, bool active); /// Draws a check box -void draw_checkbox(Window* win, DC& dc, const wxRect& rect, bool checked); +void draw_checkbox(Window* win, DC& dc, const wxRect& rect, bool checked, bool enabled = true); // ----------------------------------------------------------------------------- : EOF #endif diff --git a/src/gui/value/choice.cpp b/src/gui/value/choice.cpp index 0bbd47c6..b4da29ea 100644 --- a/src/gui/value/choice.cpp +++ b/src/gui/value/choice.cpp @@ -106,6 +106,14 @@ DropDownChoiceListBase::DropDownChoiceListBase item_size.height = max(16., item_size.height); } +void DropDownChoiceListBase::onShow() { + // update 'enabled' + Context& ctx = cve.viewer.getContext(); + FOR_EACH(c, group->choices) { + c->enabled.update(ctx); + } +} + size_t DropDownChoiceListBase::itemCount() const { return group->choices.size() + hasDefault(); } @@ -129,7 +137,10 @@ String DropDownChoiceListBase::itemText(size_t item) const { } } bool DropDownChoiceListBase::lineBelow(size_t item) const { - return isDefault(item); + return isDefault(item) || getChoice(item)->line_below; +} +bool DropDownChoiceListBase::itemEnabled(size_t item) const { + return isDefault(item) || getChoice(item)->enabled; } DropDownList* DropDownChoiceListBase::submenu(size_t item) const { if (isDefault(item)) return nullptr; @@ -157,7 +168,7 @@ void DropDownChoiceListBase::drawIcon(DC& dc, int x, int y, size_t item, bool se } // draw image if (image_id < il->GetImageCount()) { - il->Draw(image_id, dc, x, y); + il->Draw(image_id, dc, x, y, itemEnabled(item) ? wxIMAGELIST_DRAW_NORMAL : wxIMAGELIST_DRAW_TRANSPARENT); } } @@ -196,6 +207,12 @@ DropDownChoiceList::DropDownChoiceList(Window* parent, bool is_submenu, ValueVie : DropDownChoiceListBase(parent, is_submenu, cve, group) {} +void DropDownChoiceList::onShow() { + DropDownChoiceListBase::onShow(); + // we need thumbnail images soon + generateThumbnailImages(); +} + void DropDownChoiceList::select(size_t item) { if (isFieldDefault(item)) { dynamic_cast(cve).change( Defaultable() ); @@ -206,8 +223,6 @@ void DropDownChoiceList::select(size_t item) { } size_t DropDownChoiceList::selection() const { - // we need thumbnail images soon - const_cast(this)->generateThumbnailImages(); // selected item const Defaultable& value = dynamic_cast(cve).value().value(); int id = field().choices->choiceId(value); diff --git a/src/gui/value/choice.hpp b/src/gui/value/choice.hpp index 65c2ad4d..29a00ca5 100644 --- a/src/gui/value/choice.hpp +++ b/src/gui/value/choice.hpp @@ -48,9 +48,11 @@ class DropDownChoiceListBase : public DropDownList { public: DropDownChoiceListBase(Window* parent, bool is_submenu, ValueViewer& cve, ChoiceField::ChoiceP group); - protected: + protected: + virtual void onShow(); virtual size_t itemCount() const; virtual bool lineBelow(size_t item) const; + virtual bool itemEnabled(size_t item) const; virtual String itemText(size_t item) const; virtual void drawIcon(DC& dc, int x, int y, size_t item, bool selected) const; virtual DropDownList* submenu(size_t item) const; @@ -92,6 +94,7 @@ class DropDownChoiceList : public DropDownChoiceListBase { DropDownChoiceList(Window* parent, bool is_submenu, ValueViewer& cve, ChoiceField::ChoiceP group); protected: + virtual void onShow(); virtual void select(size_t item); virtual size_t selection() const; virtual DropDownList* createSubMenu(ChoiceField::ChoiceP group) const; diff --git a/src/gui/value/multiple_choice.cpp b/src/gui/value/multiple_choice.cpp index 0d2001b9..580c9116 100644 --- a/src/gui/value/multiple_choice.cpp +++ b/src/gui/value/multiple_choice.cpp @@ -33,27 +33,31 @@ DropDownMultipleChoiceList::DropDownMultipleChoiceList } void DropDownMultipleChoiceList::select(size_t item) { + MultipleChoiceValueEditor& mcve = dynamic_cast(cve); if (isFieldDefault(item)) { - // should not happen + // make default + mcve.getSet().actions.add(value_action(mcve.valueP(), Defaultable())); } else { ChoiceField::ChoiceP choice = getChoice(item); - dynamic_cast(cve).toggle(choice->first_id); + mcve.toggle(choice->first_id); } } void DropDownMultipleChoiceList::drawIcon(DC& dc, int x, int y, size_t item, bool selected) const { - // is this item active? + // is this item active/checked? bool active = false; if (!isFieldDefault(item)) { ChoiceField::ChoiceP choice = getChoice(item); active = dynamic_cast(cve).active[choice->first_id]; + } else { + active = dynamic_cast(cve).value().value.isDefault(); } // draw checkbox dc.SetPen(*wxTRANSPARENT_PEN); dc.SetBrush(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); dc.DrawRectangle(x,y,16,16); wxRect rect = RealRect(x+2,y+2,12,12); - draw_checkbox(nullptr, dc, rect, active); + draw_checkbox(nullptr, dc, rect, active, itemEnabled(item)); // draw icon DropDownChoiceListBase::drawIcon(dc, x + 16, y, item, selected); } diff --git a/src/render/value/multiple_choice.cpp b/src/render/value/multiple_choice.cpp index 32a2dd16..4bfd86e2 100644 --- a/src/render/value/multiple_choice.cpp +++ b/src/render/value/multiple_choice.cpp @@ -32,11 +32,54 @@ void MultipleChoiceValueViewer::draw(RotatedDC& dc) { if (active) select_it++; drawChoice(dc, pos, choice, active); } - } else { + } else if (style().render_style & RENDER_LIST) { // render only selected choices FOR_EACH(choice, selected) { drawChoice(dc, pos, choice); } + } else { + // COPY FROM ChoiceValueViewer + if (value().value().empty()) return; + double margin = 0; + if (style().render_style & RENDER_IMAGE) { + // draw image + map::iterator it = style().choice_images.find(cannocial_name_form(value().value())); + if (it != style().choice_images.end() && it->second.isReady()) { + ScriptableImage& img = it->second; + GeneratedImage::Options img_options(0,0, viewer.stylesheet.get(), &getSet()); + if (nativeLook()) { + img_options.width = img_options.height = 16; + img_options.preserve_aspect = ASPECT_BORDER; + } else if(style().render_style & RENDER_TEXT) { + // also drawing text, use original size + } else { + img_options.width = (int) dc.trS(style().width); + img_options.height = (int) dc.trS(style().height); + img_options.preserve_aspect = style().alignment == ALIGN_STRETCH ? ASPECT_STRETCH : ASPECT_FIT; + } + Image image = img.generate(img_options, true); + ImageCombine combine = img.combine(); + // apply mask? + style().loadMask(*viewer.stylesheet); + if (style().mask.Ok()) { + set_alpha(image, style().mask); + } + // draw + dc.DrawImage(image, + align_in_rect(style().alignment, RealSize(image.GetWidth(), image.GetHeight()), style().getRect()), + combine == COMBINE_NORMAL ? style().combine : combine, + style().angle + ); + margin = dc.trInvS(image.GetWidth()) + 1; + } + } + if (style().render_style & RENDER_TEXT) { + // draw text + dc.DrawText(tr(*viewer.stylesheet, value().value(), capitalize(value().value())), + align_in_rect(ALIGN_MIDDLE_LEFT, RealSize(0, dc.GetCharHeight()), style().getRect()) + RealSize(margin, 0) + ); + } + // COPY ENDS HERE } } diff --git a/src/script/functions/editor.cpp b/src/script/functions/editor.cpp index c9072d0b..76d4a4c4 100644 --- a/src/script/functions/editor.cpp +++ b/src/script/functions/editor.cpp @@ -13,6 +13,7 @@ #include #include #include +#include DECLARE_TYPEOF_COLLECTION(FieldP); DECLARE_TYPEOF_COLLECTION(TextValue*); @@ -140,7 +141,7 @@ SCRIPT_FUNCTION(primary_choice) { SCRIPT_PARAM(ValueP,input); ChoiceValueP value = dynamic_pointer_cast(input); if (!value) { - throw ScriptError(_("Argument to 'primary_choice' should be a choice field")); + throw ScriptError(_("Argument to 'primary_choice' should be a choice value")); } // determine choice int id = value->field().choices->choiceId(value->value); @@ -154,10 +155,46 @@ SCRIPT_FUNCTION(primary_choice) { SCRIPT_RETURN(_("")); } +// ----------------------------------------------------------------------------- : Multiple choice values + +// is the given choice active? +SCRIPT_FUNCTION(chosen) { + SCRIPT_PARAM(String,choice); + SCRIPT_PARAM(String,input); + for (size_t pos = 0 ; pos < input.size() ; ) { + if (input.GetChar(pos) == _(' ')) { + ++pos; // ingore whitespace + } else { + // does this choice match the one asked about? + size_t end = input.find_first_of(_(','), pos); + if (end == String::npos) end = input.size(); + if (end - pos == choice.size() && is_substr(input, pos, choice)) { + SCRIPT_RETURN(true); + } + pos = end + 1; + } + } + SCRIPT_RETURN(false); +} + +// add the given choice if it is not already active +SCRIPT_FUNCTION(require_choice) { + SCRIPT_PARAM(ValueP,input); + MultipleChoiceValueP value = dynamic_pointer_cast(input); + if (!value) { + throw ScriptError(_("Argument 'input' to 'require_choice' should be a multiple choice value")); + } + SCRIPT_PARAM(String,choice); + // TODO + SCRIPT_RETURN(input); +} + // ----------------------------------------------------------------------------- : Init void init_script_editor_functions(Context& ctx) { ctx.setVariable(_("forward editor"), script_combined_editor); // combatability ctx.setVariable(_("combined editor"), script_combined_editor); ctx.setVariable(_("primary choice"), script_primary_choice); + ctx.setVariable(_("chosen"), script_chosen); + ctx.setVariable(_("require choice"), script_require_choice); } diff --git a/src/script/parser.cpp b/src/script/parser.cpp index 95fa6b2d..6898878f 100644 --- a/src/script/parser.cpp +++ b/src/script/parser.cpp @@ -567,7 +567,7 @@ void parseOper(TokenIterator& input, Script& script, Precedence minPrec, Instruc t = input.peek(); } } - input.read(); // skip the ) + expectToken(input, _(")")); // generate instruction script.addInstruction(I_CALL, (unsigned int)arguments.size()); FOR_EACH(arg,arguments) { diff --git a/src/script/scriptable.cpp b/src/script/scriptable.cpp index 166959f9..754e5725 100644 --- a/src/script/scriptable.cpp +++ b/src/script/scriptable.cpp @@ -14,6 +14,8 @@ Alignment from_string(const String&); +DECLARE_TYPEOF_COLLECTION(ScriptParseError); + // ----------------------------------------------------------------------------- : Store void store(const ScriptValueP& val, String& var) { var = val->toString(); } @@ -43,10 +45,16 @@ ScriptValueP OptionalScript::invoke(Context& ctx, bool open_scope) const { } void OptionalScript::parse(Reader& reader, bool string_mode) { - try { - script = ::parse(unparsed, string_mode); - } catch (const ParseError& e) { - reader.warning(e.what()); + vector errors; + script = ::parse(unparsed, string_mode, errors); + // show parse errors as warnings + FOR_EACH(e, errors) { + // find line number + int line = 0; + for (size_t i = 0 ; i < unparsed.size() && i < e.start ; ++i) { + if (unparsed.GetChar(i) == _('\n')) line++; + } + reader.warning(e.ParseError::what(), line); // use ParseError::what because we don't want e.start in the error message } } diff --git a/src/script/scriptable.hpp b/src/script/scriptable.hpp index 5457fefd..627de0c6 100644 --- a/src/script/scriptable.hpp +++ b/src/script/scriptable.hpp @@ -147,6 +147,7 @@ void Reader::handle(Scriptable& s) { } else if (s.script.unparsed.find_first_of('{') != String::npos) { s.script.parse(*this, true); } else { + unhandle(); handle(s.value); } } diff --git a/src/util/io/reader.cpp b/src/util/io/reader.cpp index af6661d3..d4954a59 100644 --- a/src/util/io/reader.cpp +++ b/src/util/io/reader.cpp @@ -14,19 +14,19 @@ // ----------------------------------------------------------------------------- : Reader Reader::Reader(const InputStreamP& input, const String& filename, bool ignore_invalid) - : indent(0), expected_indent(0), just_opened(false) - , filename(filename), line_number(0) + : indent(0), expected_indent(0), state(OUTSIDE) + , filename(filename), line_number(0), previous_line_number(0) , ignore_invalid(ignore_invalid) - , input(input), stream(*input) + , input(input) { moveNext(); handleAppVersion(); } Reader::Reader(const String& filename) - : indent(0), expected_indent(0), just_opened(false) - , filename(filename), line_number(0) - , input(packages.openFileFromPackage(filename)), stream(*input) + : indent(0), expected_indent(0), state(OUTSIDE) + , filename(filename), line_number(0), previous_line_number(0) + , input(packages.openFileFromPackage(filename)) { moveNext(); handleAppVersion(); @@ -48,8 +48,10 @@ void Reader::handleAppVersion() { } } -void Reader::warning(const String& msg) { - warnings += String(_("\nOn line ")) << line_number << _(": \t") << msg; +void Reader::warning(const String& msg, int line_number_delta, bool warn_on_previous_line) { + warnings += String(_("\nOn line ")) + << ((warn_on_previous_line ? previous_line_number : line_number) + line_number_delta) + << _(": \t") << msg; } void Reader::showWarnings() { @@ -59,11 +61,19 @@ void Reader::showWarnings() { } } +bool Reader::enterAnyBlock() { + if (state == ENTERED) moveNext(); // on the key of the parent block, first move inside it + if (indent != expected_indent) return false; // not enough indentation + state = ENTERED; + expected_indent += 1; // the indent inside the block must be at least this much + return true; +} + bool Reader::enterBlock(const Char* name) { - if (just_opened) moveNext(); // on the key of the parent block, first move inside it + if (state == ENTERED) moveNext(); // on the key of the parent block, first move inside it if (indent != expected_indent) return false; // not enough indentation if (cannocial_name_compare(key, name)) { - just_opened = true; + state = ENTERED; expected_indent += 1; // the indent inside the block must be at least this much return true; } else { @@ -74,20 +84,21 @@ bool Reader::enterBlock(const Char* name) { void Reader::exitBlock() { assert(expected_indent > 0); expected_indent -= 1; - multi_line_str.clear(); - if (just_opened) moveNext(); // leave this key + assert(state != UNHANDLED); + previous_value.clear(); + if (state == ENTERED) moveNext(); // leave this key // Dump the remainder of the block // TODO: issue warnings? while (indent > expected_indent) { moveNext(); } - handled = true; + state = HANDLED; } void Reader::moveNext() { - just_opened = false; + previous_line_number = line_number; + state = HANDLED; key.clear(); - multi_line_str.clear(); indent = -1; // if no line is read it never has the expected indentation // repeat until we have a good line while (key.empty() && !input->Eof()) { @@ -100,10 +111,55 @@ void Reader::moveNext() { } } +/// Read an UTF-8 encoded line from an input stream +/** As opposed to wx functions, this one actually reports errors + */ +String read_utf8_line(wxInputStream& input, bool eat_bom = true, bool until_eof = false); +String read_utf8_line(wxInputStream& input, bool eat_bom, bool until_eof) { + vector buffer; + while (!input.Eof()) { + Byte c = input.GetC(); if (input.LastRead() <= 0) break; + if (!until_eof) { + if (c == '\n') break; + if (c == '\r') { + if (input.Eof()) break; + c = input.GetC(); if (input.LastRead() <= 0) break; + if (c != '\n') { + input.Ungetch(c); // \r but not \r\n + } + break; + } + } + buffer.push_back(c); + } + // convert to string + buffer.push_back('\0'); + size_t size = wxConvUTF8.MB2WC(nullptr, &buffer[0], 0); + if (size == -1) { + throw ParseError(_("Invalid UTF-8 sequence")); + } else if (size == 0) { + return _(""); + } + String result; + #ifdef UNICODE + // NOTE: wx doc is wrong, parameter to GetWritableChar is numer of characters, not bytes + Char* result_buf = result.GetWriteBuf(size + 1); + wxConvUTF8.MB2WC(result_buf, &buffer[0], size + 1); + result.UngetWriteBuf(size); + #else + // TODO! + #endif + return eat_bom ? decodeUTF8BOM(result) : result; +} + void Reader::readLine(bool in_string) { - // fix UTF8 in ascii builds; skip BOM - line = decodeUTF8BOM(stream.ReadLine()); line_number += 1; + // We have to do our own line reading, because wxTextInputStream is insane + try { + line = read_utf8_line(*input, line_number == 1); + } catch (const ParseError& e) { + throw ParseError(e.what() + String(_(" on line ")) << line_number); + } // read indentation indent = 0; while ((UInt)indent < line.size() && line.GetChar(indent) == _('\t')) { @@ -118,7 +174,7 @@ void Reader::readLine(bool in_string) { } key = line.substr(indent, pos - indent); if (!ignore_invalid && !in_string && starts_with(key, _(" "))) { - warning(_("key: '") + key + _("' starts with a space; only use TABs for indentation!")); + warning(_("key: '") + key + _("' starts with a space; only use TABs for indentation!"), 0, false); // try to fix up: 8 spaces is a tab while (starts_with(key, _(" "))) { key = key.substr(8); @@ -146,7 +202,7 @@ void Reader::unknownKey() { } else if (it->second.end_version <= file_app_version) { // alias not used for this version, use in warning if (indent == expected_indent) { - warning(_("Unexpected key: '") + key + _("' use '") + it->second.new_key + _("'")); + warning(_("Unexpected key: '") + key + _("' use '") + it->second.new_key + _("'"), 0, false); do { moveNext(); } while (indent > expected_indent); @@ -159,7 +215,7 @@ void Reader::unknownKey() { } } if (indent >= expected_indent) { - warning(_("Unexpected key: '") + key + _("'")); + warning(_("Unexpected key: '") + key + _("'"), 0, false); do { moveNext(); } while (indent > expected_indent); @@ -169,23 +225,31 @@ void Reader::unknownKey() { // ----------------------------------------------------------------------------- : Handling basic types +void Reader::unhandle() { + assert(state == HANDLED); + state = UNHANDLED; +} + const String& Reader::getValue() { - handled = true; - if (!multi_line_str.empty()) { - return multi_line_str; + assert(state != HANDLED); // don't try to handle things twice + if (state == UNHANDLED) { + state = HANDLED; + return previous_value; } else if (value.empty()) { // a multiline string + previous_value.clear(); bool first = true; // read all lines that are indented enough readLine(); + previous_line_number = line_number; while (indent >= expected_indent && !input->Eof()) { - if (!first) multi_line_str += _('\n'); + if (!first) previous_value += _('\n'); first = false; - multi_line_str += line.substr(expected_indent); // strip expected indent + previous_value += line.substr(expected_indent); // strip expected indent readLine(true); } - // moveNext(), but without emptying multi_line_str - just_opened = false; + // moveNext(), but without the initial readLine() + state = HANDLED; while (key.empty() && !input->Eof()) { readLine(); } @@ -194,9 +258,16 @@ const String& Reader::getValue() { line_number += 1; indent = -1; } - return multi_line_str; + if (indent >= expected_indent) { + warning(_("Blank line or comment in text block, that is insufficiently indented.\n") + _("\t\tEither indent the comment/blank line, or add a 'key:' after it.\n") + _("\t\tThis could cause more more error messages.\n"), -1, false); + } + return previous_value; } else { - return value; + previous_value = value; + moveNext(); + return previous_value; } } @@ -217,7 +288,8 @@ template <> void Reader::handle(double& d) { getValue().ToDouble(&d); } template <> void Reader::handle(bool& b) { - b = (getValue()==_("true") || getValue()==_("1") || getValue()==_("yes")); + const String& v = getValue(); + b = (v==_("true") || v==_("1") || v==_("yes")); } // ----------------------------------------------------------------------------- : Handling less basic util types diff --git a/src/util/io/reader.hpp b/src/util/io/reader.hpp index 30e867fd..4f262692 100644 --- a/src/util/io/reader.hpp +++ b/src/util/io/reader.hpp @@ -11,7 +11,6 @@ #include #include -#include template class Defaultable; template class Scriptable; @@ -57,7 +56,7 @@ class Reader { void handleAppVersion(); /// Add a warning message, but continue reading - void warning(const String& msg); + void warning(const String& msg, int line_number_delta = 0, bool warn_on_previous_line = true); /// Show all warning messages, but continue reading void showWarnings(); @@ -66,11 +65,9 @@ class Reader { template void handle_greedy(T& object) { do { -// UInt l = line_number; - handled = false; handle(object); -// if (l == line_number && !handled) unknownKey(object); - if (!handled) unknownKey(object); + if (state != HANDLED) unknownKey(object); + state = OUTSIDE; } while (indent >= expected_indent); } @@ -106,6 +103,9 @@ class Reader { void handle(GameP&); void handle(StyleSheetP&); + /// Indicate that the last value from getValue() was not handled, allowing it to be handled again + void unhandle(); + // --------------------------------------------------- : Data /// App version this file was made with Version file_app_version; @@ -114,16 +114,19 @@ class Reader { String line; /// The key and value of the last line we read String key, value; - /// A string spanning multiple lines - String multi_line_str; - /// Has the current line been handled? - bool handled; + /// Value of the *previous* line, only valid in state==HANDLED + String previous_value; /// Indentation of the last line we read int indent; /// Indentation of the block we are in int expected_indent; - /// Did we just open a block (i.e. not read any more lines of it)? - bool just_opened; + /// State of the reader + enum State { + OUTSIDE, ///< We have not entered the block of the current key + ENTERED, ///< We just entered the block of the current key + HANDLED, ///< We have handled a value, and moved to the next line, previous_value is the value we just handled + UNHANDLED, ///< Something has been 'unhandled()' + } state; /// Aliasses for compatability struct Alias { String new_key; @@ -136,19 +139,21 @@ class Reader { /// Filename for error messages String filename; - /// Line number for error messages - UInt line_number; + /// Line number of the current line for error messages + int line_number; + /// Line number of the previous_line + int previous_line_number; /// Input stream we are reading from InputStreamP input; - /// Text stream wrapping the input stream - wxTextInputStream stream; /// Accumulated warning messages String warnings; // --------------------------------------------------- : Reading the stream - /// Is there a block with the given key under the current cursor? + /// Is there a block with the given key under the current cursor? if so, enter it bool enterBlock(const Char* name); + /// Enter any block, no matter what the key + bool enterAnyBlock(); /// Leave the block we are in void exitBlock(); @@ -208,13 +213,7 @@ void Reader::handle(intrusive_ptr& pointer) { template void Reader::handle(map& m) { - while (true) { - // same as enterBlock - if (just_opened) moveNext(); // on the key of the parent block, first move inside it - if (indent != expected_indent) return; // not enough indentation - just_opened = true; - expected_indent += 1; - // now read the value + while (enterAnyBlock()) { handle_greedy(m[key]); exitBlock(); }