Commit 95c154d6 authored by twanvl's avatar twanvl

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.
parent f6a4b4c5
......@@ -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) {
......
......@@ -52,9 +52,11 @@ class ChoiceField::Choice : public IntrusivePtrBase<ChoiceField::Choice> {
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<ChoiceP> 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<ChoiceP> choices; ///< Choices and sub groups in this group
bool line_below; ///< Show a line after this item?
Scriptable<bool> 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
......
......@@ -25,6 +25,7 @@ IMPLEMENT_REFLECTION(MultipleChoiceField) {
REFLECT_BASE(ChoiceField);
REFLECT(minimum_selection);
REFLECT(maximum_selection);
REFLECT(empty_choice);
}
// ----------------------------------------------------------------------------- : MultipleChoiceStyle
......
......@@ -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<String>& 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
......@@ -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())) {
......
......@@ -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; }
......
......@@ -60,6 +60,7 @@ NewSetWindow::NewSetWindow(Window* parent)
void NewSetWindow::onGameSelect(wxCommandEvent&) {
wxBusyCursor wait;
GameP game = game_list->getSelection<Game>();
handle_pending_errors();
settings.default_game = game->name();
GameSettings& gs = settings.gameSettingsFor(*game);
stylesheet_list->showData<StyleSheet>(game->name() + _("-*"));
......@@ -78,6 +79,7 @@ void NewSetWindow::onStyleSheetSelect(wxCommandEvent&) {
// store this as default selection
GameP game = game_list ->getSelection<Game>();
StyleSheetP stylesheet = stylesheet_list->getSelection<StyleSheet>();
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 ()
......@@ -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();
......
......@@ -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);
}
......@@ -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
......@@ -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<ChoiceValueEditor&>(cve).change( Defaultable<String>() );
......@@ -206,8 +223,6 @@ void DropDownChoiceList::select(size_t item) {
}
size_t DropDownChoiceList::selection() const {
// we need thumbnail images soon
const_cast<DropDownChoiceList*>(this)->generateThumbnailImages();
// selected item
const Defaultable<String>& value = dynamic_cast<ChoiceValueEditor&>(cve).value().value();
int id = field().choices->choiceId(value);
......
......@@ -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;
......
......@@ -33,27 +33,31 @@ DropDownMultipleChoiceList::DropDownMultipleChoiceList
}
void DropDownMultipleChoiceList::select(size_t item) {
MultipleChoiceValueEditor& mcve = dynamic_cast<MultipleChoiceValueEditor&>(cve);
if (isFieldDefault(item)) {
// should not happen
// make default
mcve.getSet().actions.add(value_action(mcve.valueP(), Defaultable<String>()));
} else {
ChoiceField::ChoiceP choice = getChoice(item);
dynamic_cast<MultipleChoiceValueEditor&>(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<MultipleChoiceValueEditor&>(cve).active[choice->first_id];
} else {
active = dynamic_cast<MultipleChoiceValueEditor&>(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);
}
......
......@@ -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<String,ScriptableImage>::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
}
}
......
......@@ -13,6 +13,7 @@
#include <data/game.hpp>
#include <data/field/text.hpp>
#include <data/field/choice.hpp>
#include <data/field/multiple_choice.hpp>
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<ChoiceValue>(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<MultipleChoiceValue>(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);
}
......@@ -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) {
......
......@@ -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<ScriptParseError> 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
}
}
......
......@@ -147,6 +147,7 @@ void Reader::handle(Scriptable<T>& s) {
} else if (s.script.unparsed.find_first_of('{') != String::npos) {
s.script.parse(*this, true);
} else {
unhandle();
handle(s.value);
}
}
......
......@@ -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<char> 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
......
......@@ -11,7 +11,6 @@
#include <util/prec.hpp>
#include <util/version.hpp>
#include <wx/txtstrm.h>
template <typename T> class Defaultable;
template <typename T> 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 <typename T>
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<T>& pointer) {
template <typename V>
void Reader::handle(map<String, V>& 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();
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment