From 3b45bb81f6c9616528f1cf10d33c1195c1761f6b Mon Sep 17 00:00:00 2001 From: twanvl Date: Sun, 13 May 2007 01:22:11 +0000 Subject: [PATCH] Support for 2d bar graphs; separator_after for keywords; Slightly more advanced english_plural/singular; Windows uninstaller will remove app data git-svn-id: svn://svn.code.sf.net/p/magicseteditor/code/trunk@348 0fc631ac-6414-0410-93d0-97cfa31319b6 --- data/en.mse-locale/locale | 3 + data/magic.mse-game/game | 34 ++-- src/data/keyword.cpp | 89 +++++++---- src/data/keyword.hpp | 11 +- src/data/statistics.cpp | 24 ++- src/data/statistics.hpp | 7 +- src/gui/control/graph.cpp | 247 +++++++++++++++++++++++------- src/gui/control/graph.hpp | 49 ++++-- src/gui/set/stats_panel.cpp | 4 + src/script/functions/english.cpp | 48 ++++-- src/util/alignment.hpp | 4 +- tools/msw-installer/installer.iss | 5 + 12 files changed, 406 insertions(+), 119 deletions(-) diff --git a/data/en.mse-locale/locale b/data/en.mse-locale/locale index eed15b5f..5bba3b7d 100644 --- a/data/en.mse-locale/locale +++ b/data/en.mse-locale/locale @@ -518,6 +518,9 @@ error: # Update checking checking updates failed: Checking updates failed. no updates: There are no available updates. + + # Stats panel + dimension not found: There is no statistics dimension '%s' ############################################################## Types used in scripts / shape names type: diff --git a/data/magic.mse-game/game b/data/magic.mse-game/game index 7b266009..d4b66136 100644 --- a/data/magic.mse-game/game +++ b/data/magic.mse-game/game @@ -152,14 +152,20 @@ init script: }; # replaces — correctly - alternative_cost := replace_rule(match:"\\.$", replace:"") + replace_rule(match:"^[A-Z]", replace: { to_lower() }) add := "" # default is nothing for_mana_costs := format_cost := { - if input.separator=="—" then - "{alternative_cost(input.param)}" + if input.separator_before == "—" then + "{input.param}" else "{add}{input.param}" } + alternative_cost := replace_rule(match:"^[A-Z]", replace: { to_lower() }) + format_alt_cost := { + if input.separator_before == "—" then + alternative_cost(input.param) + else + input.param + } long_dash := replace_rule(match:"-", replace:"—") # Utilities for keywords has_cc := { card.casting_cost != "" } @@ -256,8 +262,11 @@ init script: replace: "Æ") + # step 2 : longdash for keywords replace_rule( - match: "--| - ", - replace: "—") + match: "--", + replace: "—") + + replace_rule( + match: " - ", + replace: " — ") #character filter for copyright line copyright_filter := @@ -955,7 +964,7 @@ card field: ############################################################## Statistics categories statistics dimension: - name: card color + name: card color2 script: primary_card_color(card.card_color) icon: stats/card_color.png colors: @@ -994,10 +1003,10 @@ statistics dimension: # numeric: true # icon: stats/toughness.png -#statistics category: -# name: color / rarity -# dimension: card_color -# dimension: rarity +statistics category: + name: color / rarity + dimension: card color2 + dimension: rarity #statistics category: # name: power / toughness @@ -1095,6 +1104,7 @@ pack type: has keywords: true +keyword match script: name_filter(value) #keyword preview: {keyword} ({reminder}) keyword mode: @@ -1122,6 +1132,7 @@ keyword parameter type: name: cost match: [ ][STXYZ0-9WUBRG/|]*|[-—][^(\n]* separator before is: [ —-] + separator after is: [.] optional: false # note: the separator is part of match refer script: @@ -1136,6 +1147,7 @@ keyword parameter type: name: add "pay " for mana costs description: When using mana only costs, words the reminder text as "pay " script: \{for_mana_costs(add:"pay ",{input})\} + reminder script: format_alt_cost() separator script: long_dash() keyword parameter type: name: number @@ -1161,7 +1173,7 @@ keyword parameter type: match: [^(,\n]+ keyword parameter type: name: name - match: [^(.,\n]+ + match: [^(.,\n-—]+ keyword parameter type: name: prefix description: Prefix for things like "walk" diff --git a/src/data/keyword.cpp b/src/data/keyword.cpp index 4b3ef5a3..7741d10a 100644 --- a/src/data/keyword.cpp +++ b/src/data/keyword.cpp @@ -38,6 +38,7 @@ IMPLEMENT_REFLECTION(KeywordParam) { REFLECT(optional); REFLECT(match); REFLECT(separator_before_is); + REFLECT(separator_after_is); REFLECT(eat_separator); REFLECT(script); REFLECT(reminder_script); @@ -119,12 +120,20 @@ String KeywordParam::make_separator_before() const { return ret; }*/ void KeywordParam::compile() { + // compile separator_before if (!separator_before_is.empty() && !separator_before_re.IsValid()) { separator_before_re.Compile(_("^") + separator_before_is, wxRE_ADVANCED); if (eat_separator) { separator_before_eat.Compile(separator_before_is + _("$"), wxRE_ADVANCED); } } + // compile separator_after + if (!separator_after_is.empty() && !separator_after_re.IsValid()) { + separator_after_re.Compile(separator_after_is + _("$"), wxRE_ADVANCED); + if (eat_separator) { + separator_after_eat.Compile(_("^") + separator_after_is, wxRE_ADVANCED); + } + } } size_t Keyword::findMode(const vector& modes) const { @@ -239,6 +248,12 @@ void Keyword::prepare(const vector& param_types, bool force) { // modify regex : match parameter regex += _("(") + make_non_capturing(p->match) + (p->optional ? _(")?") : _(")")); i = skip_tag(match, end); + // eat separator_after? + if (p->separator_after_eat.IsValid() && p->separator_after_eat.Matches(match.substr(i))) { + size_t start, len; + p->separator_before_eat.GetMatch(&start, &len); + i += start + len; + } } else { text += c; i++; @@ -347,22 +362,28 @@ void KeywordDatabase::add(const Keyword& kw) { for (size_t i = 0 ; i < kw.match.size() ;) { Char c = kw.match.GetChar(i); if (is_substr(kw.match, i, _("separator_before_eat; - if (sep.IsValid() && sep.Matches(text)) { + wxRegEx& sep_before = kw.parameters[param]->separator_before_eat; + wxRegEx& sep_after = kw.parameters[param]->separator_after_eat; + if (sep_before.IsValid() && sep_before.Matches(text)) { // remove the separator from the text to prevent duplicates size_t start, len; - sep.GetMatch(&start, &len); + sep_before.GetMatch(&start, &len); text = text.substr(0, start); } + if (sep_after.IsValid() && sep_after.Matches(kw.match.substr(i))) { + size_t start, len; + sep_after.GetMatch(&start, &len); + i += start + len; + } } ++param; // match anything cur = cur->insert(text); text.clear(); cur = cur->insertAnyStar(); - i = match_close_tag_end(kw.match, i); } else { text += c; i++; @@ -501,27 +522,40 @@ String KeywordDatabase::expand(const String& text, // parameter KeywordParam& kwp = *kw->parameters[j/2-1]; String param = untagged.substr(start_u, len_u); // untagged version - // strip separator - String separator; - if (kwp.separator_before_re.IsValid()) { - if (kwp.separator_before_re.Matches(param)) { - size_t s_start, s_len; // start should be 0 - kwp.separator_before_re.GetMatch(&s_start, &s_len); - separator = param.substr(0, s_start + s_len); - param = param.substr(s_start + s_len); - // strip from tagged version - size_t end_t = untagged_to_index(part, s_start + s_len, false); - part = get_tags(part, 0, end_t, true, true) + part.substr(end_t); - // transform? - if (kwp.separator_script) { - ctx.setVariable(_("input"), to_script(separator)); - separator = kwp.separator_script.invoke(ctx)->toString(); - } + // strip separator_before + String separator_before, separator_after; + if (kwp.separator_before_re.IsValid() && kwp.separator_before_re.Matches(param)) { + size_t s_start, s_len; // start should be 0 + kwp.separator_before_re.GetMatch(&s_start, &s_len); + separator_before = param.substr(0, s_start + s_len); + param = param.substr(s_start + s_len); + // strip from tagged version + size_t end_t = untagged_to_index(part, s_start + s_len, false); + part = get_tags(part, 0, end_t, true, true) + part.substr(end_t); + // transform? + if (kwp.separator_script) { + ctx.setVariable(_("input"), to_script(separator_before)); + separator_before = kwp.separator_script.invoke(ctx)->toString(); + } + } + // strip separator_after + if (kwp.separator_after_re.IsValid() && kwp.separator_after_re.Matches(param)) { + size_t s_start, s_len; // start + len should be param.size() + kwp.separator_after_re.GetMatch(&s_start, &s_len); + separator_after = param.substr(s_start); + param = param.substr(0, s_start); + // strip from tagged version + size_t start_t = untagged_to_index(part, s_start, false); + part = part.substr(0, start_t) + get_tags(part, start_t, part.size(), true, true); + // transform? + if (kwp.separator_script) { + ctx.setVariable(_("input"), to_script(separator_after)); + separator_after = kwp.separator_script.invoke(ctx)->toString(); } } // to script - KeywordParamValueP script_param(new KeywordParamValue(kwp.name, separator, param)); - KeywordParamValueP script_part (new KeywordParamValue(kwp.name, separator, part)); + KeywordParamValueP script_param(new KeywordParamValue(kwp.name, separator_before, separator_after, param)); + KeywordParamValueP script_part (new KeywordParamValue(kwp.name, separator_before, separator_after, part)); // process param if (param.empty()) { // placeholder @@ -538,7 +572,7 @@ String KeywordDatabase::expand(const String& text, script_param->value = kwp.reminder_script.invoke(ctx)->toString(); } } - part = separator + script_part->toString(); + part = separator_before + script_part->toString() + separator_after; ctx.setVariable(String(_("param")) << (int)(j/2), script_param); } total += part; @@ -601,9 +635,10 @@ KeywordParamValue::operator String() const { return _("") + value + _(""); } ScriptValueP KeywordParamValue::getMember(const String& name) const { - if (name == _("type")) return to_script(type_name); - if (name == _("separator")) return to_script(separator); - if (name == _("value")) return to_script(value); - if (name == _("param")) return to_script(value); + if (name == _("type")) return to_script(type_name); + if (name == _("separator before")) return to_script(separator_before); + if (name == _("separator after")) return to_script(separator_after); + if (name == _("value")) return to_script(value); + if (name == _("param")) return to_script(value); return ScriptValue::getMember(name); } diff --git a/src/data/keyword.hpp b/src/data/keyword.hpp index cf0d018e..c2628f57 100644 --- a/src/data/keyword.hpp +++ b/src/data/keyword.hpp @@ -42,6 +42,9 @@ class KeywordParam : public IntrusivePtrBase { String separator_before_is; ///< Regular expression of separator before the param wxRegEx separator_before_re; ///< Regular expression of separator before the param, compiled wxRegEx separator_before_eat;///< Regular expression of separator before the param, if eat_separator + String separator_after_is; ///< Regular expression of separator after the param + wxRegEx separator_after_re; ///< Regular expression of separator after the param, compiled + wxRegEx separator_after_eat; ///< Regular expression of separator after the param, if eat_separator bool eat_separator; ///< Remove the separator from the match string if it also appears there (prevent duplicates) OptionalScript script; ///< Transformation of the value for showing as the parameter OptionalScript reminder_script; ///< Transformation of the value for showing in the reminder text @@ -53,7 +56,7 @@ class KeywordParam : public IntrusivePtrBase { //% /** This tries to decode the separator_before_is regex */ //% String make_separator_before() const; - /// Compile regexes + /// Compile regexes for separators void compile(); DECLARE_REFLECTION(); @@ -150,11 +153,11 @@ class KeywordDatabase { /// A script value containing the value of a keyword parameter class KeywordParamValue : public ScriptValue { public: - KeywordParamValue(const String& type, const String& separator, const String& value) - : type_name(type), separator(separator), value(value) + KeywordParamValue(const String& type, const String& separator_before, const String& separator_after, const String& value) + : type_name(type), separator_before(separator_before), separator_after(separator_after), value(value) {} String type_name; - String separator; + String separator_before, separator_after; String value; virtual ScriptType type() const; diff --git a/src/data/statistics.cpp b/src/data/statistics.cpp index 854cd137..43905185 100644 --- a/src/data/statistics.cpp +++ b/src/data/statistics.cpp @@ -10,6 +10,9 @@ #include #include +DECLARE_TYPEOF_COLLECTION(String); +DECLARE_TYPEOF_COLLECTION(StatsDimensionP); + // ----------------------------------------------------------------------------- : Statistics dimension StatsDimension::StatsDimension() @@ -68,6 +71,7 @@ StatsCategory::StatsCategory(const StatsDimensionP& dim) IMPLEMENT_REFLECTION_ENUM(GraphType) { VALUE_N("bar", GRAPH_TYPE_BAR); + VALUE_N("stack", GRAPH_TYPE_STACK); VALUE_N("pie", GRAPH_TYPE_PIE); VALUE_N("scatter", GRAPH_TYPE_SCATTER); } @@ -78,6 +82,24 @@ IMPLEMENT_REFLECTION_NO_GET_MEMBER(StatsCategory) { REFLECT(description); REFLECT_N("icon", icon_filename); REFLECT(type); - REFLECT(dimensions); + REFLECT_N("dimensions", dimension_names); } } + +void StatsCategory::find_dimensions(const vector& available) { + if (!dimensions.empty()) return; + FOR_EACH_CONST(n, dimension_names) { + StatsDimensionP dim; + FOR_EACH_CONST(d, available) { + if (d->name == n) { + dim = d; + break; + } + } + if (!dim) { + handle_error(_ERROR_1_("dimension not found",dim),false); + } else { + dimensions.push_back(dim); + } + } +} \ No newline at end of file diff --git a/src/data/statistics.hpp b/src/data/statistics.hpp index 3b950995..5591f446 100644 --- a/src/data/statistics.hpp +++ b/src/data/statistics.hpp @@ -43,6 +43,7 @@ class StatsDimension : public IntrusivePtrBase { /// Types of graphs enum GraphType { GRAPH_TYPE_BAR +, GRAPH_TYPE_STACK , GRAPH_TYPE_PIE , GRAPH_TYPE_SCATTER }; @@ -59,9 +60,13 @@ class StatsCategory : public IntrusivePtrBase { String description; ///< Description, used in status bar String icon_filename; ///< Icon for lists Bitmap icon; ///< The loaded icon (optional of course) - vector dimensions; ///< The dimensions to use, higher dimensions may be null + vector dimension_names;///< Names of the dimensions to use + vector dimensions; ///< Actual dimensions GraphType type; ///< Type of graph to use + /// Initialize dimensions from dimension_names + void find_dimensions(const vector& available); + DECLARE_REFLECTION(); }; diff --git a/src/gui/control/graph.cpp b/src/gui/control/graph.cpp index 65fb9405..d8264022 100644 --- a/src/gui/control/graph.cpp +++ b/src/gui/control/graph.cpp @@ -16,6 +16,7 @@ DECLARE_TYPEOF_COLLECTION(GraphElementP); DECLARE_TYPEOF_COLLECTION(GraphGroup); DECLARE_TYPEOF_COLLECTION(GraphP); DECLARE_TYPEOF_COLLECTION(int); +DECLARE_TYPEOF_COLLECTION(vector); DECLARE_TYPEOF(map); template inline T sgn(T v) { return v < 0 ? -1 : 1; } @@ -41,7 +42,6 @@ GraphData::GraphData(const GraphDataPre& d) // total size size = (UInt)d.elements.size(); // find groups on each axis - size_t value_count = 1; size_t i = 0; FOR_EACH(a, axes) { map counts; // note: default constructor for UInt() does initialize to 0 @@ -105,29 +105,40 @@ GraphData::GraphData(const GraphDataPre& d) first = false; } } - value_count *= a->groups.size(); ++i; } // count elements in each position values.clear(); - values.resize(value_count, 0); FOR_EACH_CONST(e, d.elements) { // find index j in elements - size_t i = 0, j = 0; + vector group_nrs(axes.size(), -1); + int i = 0; FOR_EACH(a, axes) { String v = e->values[i]; - size_t k = 0, l = 0; + int j = 0; FOR_EACH(g, a->groups) { if (v == g.name) { - k = l; + group_nrs[i] = j; break; } - l += 1; + ++j; } - j = j * a->groups.size() + k; ++i; } - values[j] += 1; + values.push_back(group_nrs); + } +} + +void GraphData::crossAxis(size_t axis1, size_t axis2, vector& out) const { + size_t a1_size = axes[axis1]->groups.size(); + size_t a2_size = axes[axis2]->groups.size(); + out.clear(); + out.resize(a1_size * a2_size, 0); + FOR_EACH_CONST(v, values) { + int v1 = v[axis1], v2 = v[axis2]; + if (v1 >= 0 && v2 >= 0) { + out[a2_size * v1 + v2]++; + } } } @@ -148,71 +159,145 @@ bool Graph1D::findItem(const RealPoint& pos, const RealRect& rect, vector& } } +// ----------------------------------------------------------------------------- : Graph2D + +void Graph2D::setData(const GraphDataP& d) { + Graph::setData(d); + if (data->axes.size() <= max(axis1,axis2)) return; + d->crossAxis(axis1,axis2,values); +} + // ----------------------------------------------------------------------------- : Bar Graph +/// Rectangle for the bar of a bar graph +RealRect bar_graph_bar(const RealRect& rect, int group, int group_count, int start, int end, int max) { + double width_space = rect.width / group_count; // including spacing + double width = width_space / 5 * 4; + double space = width_space / 5; + double step_height = rect.height / max; // multiplier for bar height + double top = rect.bottom() + 1 - start * step_height; + double bottom = rect.bottom() - end * step_height; + RealRect result( + rect.x + width_space * group + space / 2, + top, + width, + (int)bottom - top + ); + if (result.height < 0) { + result.height = -result.height; + result.y -= result.height; + } + return result; +} +/// Which column of the bar graph with count bars is coordinate x in? +int find_bar_graph_column(double width, double x, int count) { + double width_space = width / count; // including spacing + double space = width_space / 5; + // Find column in which the point could be located + int col = int(x / width_space); + if (col < 0 || col >= count) return -1; // not a column + double in_col = x - col * width_space; + if (in_col < space / 2) return -1; // left + if (in_col > width_space - space / 2) return -1; // right + return col; +} + void BarGraph::draw(RotatedDC& dc, int current, DrawLayer layer) const { if (!data) return; // Rectangle for bars RealRect rect = dc.getInternalRect(); - // Bar sizes GraphAxis& axis = axis_data(); int count = int(axis.groups.size()); - double width_space = rect.width / count; // including spacing - double width = width_space / 5 * 4; - double space = width_space / 5; - double step_height = rect.height / axis.max; // multiplier for bar height + // Bar sizes if (layer == LAYER_SELECTION) { // Highlight current column Color bg = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); if (current >= 0) { - double x = rect.x + width_space * current; + const GraphGroup& group = axis.groups[current]; + RealRect bar = bar_graph_bar(rect, current, count, 0, group.size, axis.max); dc.SetPen(*wxTRANSPARENT_PEN); - dc.SetBrush(lerp(bg, axis.groups[current].color, 0.25)); - dc.DrawRectangle(RealRect(x + space / 2 - 5, rect.y + 1 - 15 * sgn(step_height), - width + 10, step_height * axis.groups[current].size + (1.999 + 20) * sgn(step_height))); - dc.SetBrush(lerp(bg, axis.groups[current].color, 0.5)); - dc.DrawRectangle(RealRect(x + space / 2 - 2, rect.y + 1 - 2 * sgn(step_height), - width + 4, step_height * axis.groups[current].size + (1.999 + 4) * sgn(step_height))); + dc.SetBrush(lerp(bg, group.color, 0.25)); + dc.DrawRectangle(bar.move(-5,-5,10,10)); + dc.SetBrush(lerp(bg, group.color, 0.5)); + dc.DrawRectangle(bar.move(-2,-2,4,4)); } } else if (layer == LAYER_VALUES) { // Draw bars - Color fg = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); - dc.SetPen(fg); - double x = rect.x; + dc.SetPen(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); + int i = 0; FOR_EACH_CONST(g, axis.groups) { // draw bar dc.SetBrush(g.color); - dc.DrawRectangle(RealRect(x + space / 2, rect.y - 1 * sgn(step_height), width, (int)(rect.y + step_height * g.size) - rect.y + 1 * sgn(step_height))); - // draw label, aligned bottom center - RealSize text_size = dc.GetTextExtent(g.name); - dc.SetClippingRegion(RealRect(x + 2, rect.y + 3, width_space - 4, text_size.height)); - dc.DrawText(g.name, align_in_rect(ALIGN_TOP_CENTER, text_size, RealRect(x, rect.y + 3, width_space, 0))); - dc.DestroyClippingRegion(); - x += width_space; + dc.DrawRectangle(bar_graph_bar(rect, i++, count, 0, g.size, axis.max)); } } } int BarGraph::findItem(const RealPoint& pos, const RealRect& rect) const { if (!data) return -1; + if (pos.y > max(rect.top(), rect.bottom())) return -1; // below + if (pos.y < min(rect.top(), rect.bottom())) return -1; // above + return find_bar_graph_column(rect.width, pos.x - rect.x, (int)axis_data().groups.size()); +} + +// ----------------------------------------------------------------------------- : Bar Graph 2D + +void BarGraph2D::draw(RotatedDC& dc, const vector& current, DrawLayer layer) const { + if (!data || data->axes.size() <= max(axis1,axis2)) return; + // Rectangle for bars + RealRect rect = dc.getInternalRect(); + GraphAxis& axis1 = axis1_data(); // the major axis + GraphAxis& axis2 = axis2_data(); // the stacked axis + int count = int(axis1.groups.size()); // Bar sizes - GraphAxis& axis = axis_data(); - int count = int(axis.groups.size()); - double width_space = rect.width / count; // including spacing - double space = width_space / 5; - // Find column in which this point could be located - int col = int((pos.x - rect.x) / width_space); - double in_col = (pos.x - rect.x) - col * width_space; - if (in_col < space / 2 || // left - in_col > width_space - space / 2 || // right - pos.y > max(rect.top(), rect.bottom()) || // below - pos.y < min(rect.top(), rect.bottom())) { // above - return -1; + if (layer == LAYER_SELECTION) { + // Highlight current column + // TODO + } else if (layer == LAYER_VALUES) { + // Draw bars + dc.SetPen(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); + for (int i = 0 ; i < count ; ++i) { + // draw stacked bars + int start = 0; + int j = 0; + FOR_EACH_CONST(g2, axis2.groups) { + int end = start + values[j++ + axis2.groups.size() * i]; + dc.SetBrush(g2.color); + dc.DrawRectangle(bar_graph_bar(rect, i, count, start, end, axis1.max)); + start = end; + } + } } - if (col < 0 || (size_t)col >= axis_data().groups.size()) { - return -1; - } else { - return col; +} +bool BarGraph2D::findItem(const RealPoint& pos, const RealRect& rect, vector& out) const { + if (!data || data->axes.size() <= max(axis1,axis2)) return false; + if (pos.y > max(rect.top(), rect.bottom())) return false; // below + if (pos.y < min(rect.top(), rect.bottom())) return false; // above + // column + GraphAxis& axis1 = axis1_data(); // the major axis + int count = (int)axis1.groups.size(); + int col = find_bar_graph_column(rect.width, pos.x - rect.x, count); + // row + int max_value = (int)axis1.max; + int value = (rect.bottom() - pos.y) / rect.height * max_value; + if (value < 0 || value > max_value) return false; + // find row + int row = -1; + size_t vs = col * axis2_data().groups.size(); + for (int i = 0 ; i < count ; ++i) { + value -= values[vs+i]; + if (value < 0) { + // in this layer of the stack + row = i; + break; + } } + if (row == -1) return false; + // done + out.clear(); + out.insert(out.begin(), data->axes.size(), -1); + out.at(this->axis1) = col; + out.at(this->axis2) = row; + return true; } // ----------------------------------------------------------------------------- : Pie Graph @@ -286,6 +371,59 @@ int PieGraph::findItem(const RealPoint& pos, const RealRect& rect) const { // ----------------------------------------------------------------------------- : Graph Legend +// ----------------------------------------------------------------------------- : Graph label axis + +void GraphLabelAxis::draw(RotatedDC& dc, int current, DrawLayer layer) const { + RealRect rect = dc.getInternalRect(); + GraphAxis& axis = axis_data(); + int count = int(axis.groups.size()); + // Draw + if (layer == LAYER_SELECTION) { + // highlight selection + } else if (layer != LAYER_AXES) { + if (direction == HORIZONTAL) { + double width = rect.width / count; // width of an item + // Draw labels + double x = rect.x; + FOR_EACH_CONST(g, axis.groups) { + // draw label, aligned bottom center + RealSize text_size = dc.GetTextExtent(g.name); + dc.SetClippingRegion(RealRect(x + 2, rect.bottom() + 3, width - 4, text_size.height)); + dc.DrawText(g.name, align_in_rect(ALIGN_TOP_CENTER, text_size, RealRect(x, rect.bottom() + 3, width, 0))); + dc.DestroyClippingRegion(); + x += width; + } + // Draw lines + Color bg = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); + Color fg = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); + if (draw_lines) { + dc.SetPen(lerp(bg, fg, 0.5)); + for (int i = 0 ; i < count ; ++i) { + dc.DrawLine(RealPoint(rect.x + i*width, rect.top()), RealPoint(rect.x + i*width, rect.bottom())); + } + } + // always draw axis line + dc.SetPen(fg); + dc.DrawLine(rect.topLeft(), rect.bottomLeft()); + } else { + // TODO + } + } +} +int GraphLabelAxis::findItem(const RealPoint& pos, const RealRect& rect) const { + GraphAxis& axis = axis_data(); + int col; + if (direction == HORIZONTAL) { + col = (pos.x - rect.x) / rect.width * axis.groups.size(); + if (pos.y < rect.bottom()) return -1; + } else { + col = (pos.y - rect.y) / rect.height * axis.groups.size(); + if (pos.x > rect.left()) return -1; + } + if (col < 0 || col >= (int)axis.groups.size()) return -1; + return col; +} + // ----------------------------------------------------------------------------- : Graph value axis void GraphValueAxis::draw(RotatedDC& dc, int current, DrawLayer layer) const { @@ -296,7 +434,7 @@ void GraphValueAxis::draw(RotatedDC& dc, int current, DrawLayer layer) const { GraphAxis& axis = axis_data(); double step_height = rect.height / axis.max; // height of a single value dc.SetFont(*wxNORMAL_FONT); - UInt label_step = UInt(max(1.0, (dc.GetCharHeight() + 1) / step_height)); // values per labeled line + UInt label_step = (UInt)floor(max(1.0, (dc.GetCharHeight() + 1) / step_height)); // values per labeled line // Colors Color bg = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); Color fg = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); @@ -304,7 +442,7 @@ void GraphValueAxis::draw(RotatedDC& dc, int current, DrawLayer layer) const { dc.SetPen(lerp(bg, fg, 0.5)); for (UInt i = 0 ; i <= axis.max ; ++i) { if (i % label_step == 0) { - int y = rect.top() + i * step_height; + int y = rect.bottom() - i * step_height; dc.DrawLine(RealPoint(rect.left() - 2, y), RealPoint(rect.right(), y)); // draw label, aligned middle right String label; label << i; @@ -312,10 +450,9 @@ void GraphValueAxis::draw(RotatedDC& dc, int current, DrawLayer layer) const { dc.DrawText(label, align_in_rect(ALIGN_MIDDLE_RIGHT, text_size, RealRect(rect.x - 4, y, 0, 0))); } } - // Draw axes + // Draw axis dc.SetPen(fg); - dc.DrawLine(rect.topLeft() - RealSize(2,0), rect.topRight()); - dc.DrawLine(rect.topLeft(), rect.bottomLeft()); + dc.DrawLine(rect.bottomLeft() - RealSize(2,0), rect.bottomRight()); } // ----------------------------------------------------------------------------- : Graph with margins @@ -369,8 +506,10 @@ GraphControl::GraphControl(Window* parent, int id) //* intrusive_ptr combined(new GraphContainer()); combined->add(new_intrusive1(0)); - combined->add(new_intrusive1(0)); - graph = new_intrusive6(combined, 23,8,7,20, true); + combined->add(new_intrusive2(0, HORIZONTAL)); + //combined->add(new_intrusive1(0)); + combined->add(new_intrusive2(0,1)); + graph = new_intrusive6(combined, 23,8,7,20, false); /*/ intrusive_ptr combined(new GraphContainer()); combined->add(new_intrusive1(0)); diff --git a/src/gui/control/graph.hpp b/src/gui/control/graph.hpp index 6ec170be..ca65a6dd 100644 --- a/src/gui/control/graph.hpp +++ b/src/gui/control/graph.hpp @@ -10,6 +10,7 @@ // ----------------------------------------------------------------------------- : Includes #include +#include #include DECLARE_POINTER_TYPE(GraphAxis); @@ -90,9 +91,12 @@ class GraphData : public IntrusivePtrBase { public: GraphData(const GraphDataPre&); - vector axes; ///< The axes in the data - vector values; ///< Multi dimensional (dim = axes.size()) array of values - UInt size; ///< Total number of elements + vector axes; ///< The axes in the data + vector > values; ///< All elements, with the group number for each axis, or -1 + UInt size; ///< Total number of elements + + /// Create a cross table for two axes + void crossAxis(size_t axis1, size_t axis2, vector& out) const; }; @@ -138,6 +142,18 @@ class Graph1D : public Graph { inline GraphAxis& axis_data() const { return *data->axes.at(axis); } }; +/// Base class for 2 dimensional graph components +class Graph2D : public Graph { + public: + inline Graph2D(size_t axis1, size_t axis2) : axis1(axis1), axis2(axis2) {} + virtual void setData(const GraphDataP& d); + protected: + size_t axis1, axis2; + vector values; // axis1.size * axis2.size array + inline GraphAxis& axis1_data() const { return *data->axes.at(axis1); } + inline GraphAxis& axis2_data() const { return *data->axes.at(axis2); } +}; + /// A bar graph class BarGraph : public Graph1D { public: @@ -146,9 +162,13 @@ class BarGraph : public Graph1D { virtual int findItem(const RealPoint& pos, const RealRect& rect) const; }; -// TODO -//class BarGraph2D { -//}; +// A bar graph with stacked bars +class BarGraph2D : public Graph2D { + public: + inline BarGraph2D(size_t axis_h, size_t axis_v) : Graph2D(axis_h, axis_v) {} + virtual void draw(RotatedDC& dc, const vector& current, DrawLayer layer) const; + virtual bool findItem(const RealPoint& pos, const RealRect& rect, vector& out) const; +}; /// A pie graph class PieGraph : public Graph1D { @@ -169,9 +189,20 @@ class GraphLegend : public Graph1D { //class GraphTable { //}; -//class GraphAxis : public Graph1D { -// virtual void draw(RotatedDC& dc) const; -//}; +/// Draws a horizontal/vertical axis for group labels +class GraphLabelAxis : public Graph1D { + public: + inline GraphLabelAxis(size_t axis, Direction direction, bool rotate = false, bool draw_lines = false) + : Graph1D(axis), direction(direction), rotate(rotate), draw_lines(draw_lines) + {} + virtual void draw(RotatedDC& dc, int current, DrawLayer layer) const; + virtual int findItem(const RealPoint& pos, const RealRect& rect) const; + private: + Direction direction; + int levels; + bool rotate; + bool draw_lines; +}; /// Draws an a vertical axis for counts class GraphValueAxis : public Graph1D { diff --git a/src/gui/set/stats_panel.cpp b/src/gui/set/stats_panel.cpp index 67a53a89..62dceebf 100644 --- a/src/gui/set/stats_panel.cpp +++ b/src/gui/set/stats_panel.cpp @@ -147,6 +147,8 @@ void StatsPanel::onCategorySelect() { if (categories->hasSelection()) { StatsCategory& cat = categories->getSelection(); GraphDataPre d; + cat.find_dimensions(set->game->statistics_dimensions); + // create axes FOR_EACH(dim, cat.dimensions) { d.axes.push_back(new_intrusive4( dim->name, @@ -156,6 +158,7 @@ void StatsPanel::onCategorySelect() { ) ); } + // find values FOR_EACH(card, set->cards) { Context& ctx = set->getContext(card); GraphElementP e(new GraphElement); @@ -173,6 +176,7 @@ void StatsPanel::onCategorySelect() { d.elements.push_back(e); } } + // TODO graph->setLayout(cat.type) graph->setData(d); filterCards(); } diff --git a/src/script/functions/english.cpp b/src/script/functions/english.cpp index f4ca2a74..e2daef3d 100644 --- a/src/script/functions/english.cpp +++ b/src/script/functions/english.cpp @@ -11,6 +11,16 @@ #include #include +// ----------------------------------------------------------------------------- : Util + +bool is_vowel(Char c) { + return c == _('a') || c == _('e') || c == _('i') || c == _('o') || c == _('u') + || c == _('A') || c == _('E') || c == _('I') || c == _('O') || c == _('U'); +} +inline bool is_constant(Char c) { + return !is_vowel(c); +} + // ----------------------------------------------------------------------------- : Numbers /// Write a number using words, for example 23 -> "twenty-three" @@ -110,20 +120,41 @@ SCRIPT_FUNCTION(english_number_multiple) { String english_singular(const String& str) { if (str.size() > 3 && is_substr(str, str.size()-3, _("ies"))) { return str.substr(0, str.size() - 3) + _("y"); + } else if (str.size() > 3 && is_substr(str, str.size()-3, _("oes"))) { + return str.substr(0, str.size() - 2); + } else if (str.size() > 4 && is_substr(str, str.size()-4, _("ches"))) { + return str.substr(0, str.size() - 2); + } else if (str.size() > 4 && is_substr(str, str.size()-4, _("shes"))) { + return str.substr(0, str.size() - 2); + } else if (str.size() > 4 && is_substr(str, str.size()-4, _("sses"))) { + return str.substr(0, str.size() - 2); + } else if (str.size() > 5 && is_substr(str, str.size()-3, _("ves")) && (is_substr(str, str.size()-5, _("el")) || is_substr(str, str.size()-5, _("ar")) )) { + return str.substr(0, str.size() - 3) + _("f"); } else if (str.size() > 1 && str.GetChar(str.size() - 1) == _('s')) { return str.substr(0, str.size() - 1); + } else if (str.size() >= 3 && is_substr(str, str.size()-3, _("men"))) { + return str.substr(0, str.size() - 2) + _("an"); } else { return str; } } String english_plural(const String& str) { - if (str.size() > 1 && str.GetChar(str.size() - 1) == _('y')) { - return str.substr(0, str.size() - 1) + _("ies"); - } else if (str.size() > 1 && str.GetChar(str.size() - 1) == _('s')) { - return str + _("es"); - } else { - return str + _("s"); + if (str.size() > 2) { + Char a = str.GetChar(str.size() - 2); + Char b = str.GetChar(str.size() - 1); + if (b == _('y') && is_constant(a)) { + return str.substr(0, str.size() - 1) + _("ies"); + } else if (b == _('o') && is_constant(a)) { + return str + _("es"); + } else if ((a == _('s') || a == _('c')) && b == _('h')) { + return str + _("es"); + } else if (b == _('s')) { + return str + _("es"); + } else { + return str + _("s"); + } } + return str + _("s"); } // script_english_singular/plural/singplur @@ -155,11 +186,6 @@ SCRIPT_FUNCTION(english_plural) { // ----------------------------------------------------------------------------- : Hints -bool is_vowel(Char c) { - return c == _('a') || c == _('e') || c == _('i') || c == _('o') || c == _('u') - || c == _('A') || c == _('E') || c == _('I') || c == _('O') || c == _('U'); -} - /// Process english hints in the input string /** A hint is formed by * 1. an insertion of a parameter, .... diff --git a/src/util/alignment.hpp b/src/util/alignment.hpp index 31e11153..f93975c6 100644 --- a/src/util/alignment.hpp +++ b/src/util/alignment.hpp @@ -57,7 +57,9 @@ RealPoint align_in_rect(Alignment align, const RealSize& to_align, const RealRec /// Direction to place something in enum Direction { LEFT_TO_RIGHT, RIGHT_TO_LEFT, - TOP_TO_BOTTOM, BOTTOM_TO_TOP + TOP_TO_BOTTOM, BOTTOM_TO_TOP, + HORIZONTAL = LEFT_TO_RIGHT, + VERTICAL = TOP_TO_BOTTOM }; /// Move a point in a direction diff --git a/tools/msw-installer/installer.iss b/tools/msw-installer/installer.iss index ed6c0769..5c88ce67 100644 --- a/tools/msw-installer/installer.iss +++ b/tools/msw-installer/installer.iss @@ -173,3 +173,8 @@ Root: HKCR; Subkey: "MagicSetEditor2Symbol\shell\open\command"; ValueType: strin [Run] Filename: "{app}\mse.exe"; Description: "Start Magic Set Editor"; Flags: postinstall nowait skipifsilent unchecked +; ------------------------------ : Uninstaller + +[UninstallDelete] +Type: filesandordirs; Name: "{userappdata}\Magic Set Editor" +