[gimp] Bug 76616 – Size entry widgets could use some simple math



commit bcee243fa349c082e9405cf591291576bbf0581b
Author: Fredrik Alströmer <roe excu se>
Date:   Sun May 17 11:33:35 2009 +0200

    Bug 76616 â?? Size entry widgets could use some simple math
    
    Add a simple parser to the GimpSizeEntry widget so that one can write
    things such as "40in" and "50%" in a size entry widget and get that
    converted to the current unit.
    
    The parser also handles basic expresions such as "20cm + 20px" and
    "2 * 3.14in".
---
 libgimpwidgets/Makefile.am     |   29 ++-
 libgimpwidgets/gimpeevl.c      |  579 ++++++++++++++++++++++++++++++++++++++++
 libgimpwidgets/gimpeevl.h      |   67 +++++
 libgimpwidgets/gimpsizeentry.c |  212 ++++++++++++++-
 libgimpwidgets/test-eevl.c     |  191 +++++++++++++
 5 files changed, 1063 insertions(+), 15 deletions(-)

diff --git a/libgimpwidgets/Makefile.am b/libgimpwidgets/Makefile.am
index a1b03eb..aff4728 100644
--- a/libgimpwidgets/Makefile.am
+++ b/libgimpwidgets/Makefile.am
@@ -102,6 +102,8 @@ libgimpwidgets_2_0_la_sources = \
 	gimpcontroller.h		\
 	gimpdialog.c			\
 	gimpdialog.h			\
+	gimpeevl.c			\
+	gimpeevl.h			\
 	gimpenumcombobox.c		\
 	gimpenumcombobox.h		\
 	gimpenumlabel.c			\
@@ -321,7 +323,10 @@ gimp-wilber-pixbufs.h: $(WILBER_IMAGES) Makefile.am
 # test programs, not installed
 #
 
-EXTRA_PROGRAMS = test-preview-area
+EXTRA_PROGRAMS = \
+	test-preview-area	\
+	test-eevl
+
 
 test_preview_area_SOURCES = test-preview-area.c 
 
@@ -334,6 +339,28 @@ test_preview_area_LDADD = \
 	$(test_preview_area_DEPENDENCIES)
 
 
+test_eevl_SOURCES = \
+	test-eevl.c
+
+test_eevl_DEPENDENCIES = \
+	$(top_builddir)/libgimpwidgets/libgimpwidgets-$(GIMP_API_VERSION).la
+
+test_eevl_LDADD = \
+	$(GLIB_LIBS) 		\
+	$(test_eevl_DEPENDENCIES)
+
+
+#
+# test programs, not to be built by default and never installed
+#
+
+TESTS = test-eevl$(EXEEXT)
+
+
+
+
+CLEANFILES += $(EXTRA_PROGRAMS)
+
 
 install-data-local: install-ms-lib install-libtool-import-lib
 
diff --git a/libgimpwidgets/gimpeevl.c b/libgimpwidgets/gimpeevl.c
new file mode 100644
index 0000000..b024242
--- /dev/null
+++ b/libgimpwidgets/gimpeevl.c
@@ -0,0 +1,579 @@
+/* LIBGIMP - The GIMP Library
+ * Copyright (C) 1995-1997 Peter Mattis and Spencer Kimball
+ *
+ * gimpeevl.c
+ * Copyright (C) 2008 Fredrik Alstromer <roe excu se>
+ * Copyright (C) 2008 Martin Nordholts <martinn svn gnome org>
+ *
+ * This library is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ */
+
+/* Introducing eevl eva, the evaluator. A straightforward recursive
+ * descent parser, no fuss, no new dependencies. The lexer is hand
+ * coded, tedious, not extremely fast but works. It evaluates the
+ * expression as it goes along, and does not create a parse tree or
+ * anything, and will not optimize anything. It uses doubles for
+ * precision, with the given use case, that's enough to combat any
+ * rounding errors (as opposed to optimizing the evalutation).
+ *
+ * It relies on external unit resolving through a callback and does
+ * elementary dimensionality constraint check (e.g. "2 mm + 3 px * 4
+ * in" is an error, as L + L^2 is a missmatch). It uses setjmp/longjmp
+ * for try/catch like pattern on error, it uses g_strtod() for numeric
+ * conversions and it's non-destructive in terms of the paramters, and
+ * it's reentrant.
+ *
+ * EBNF:
+ *
+ *   expression    ::= term { ('+' | '-') term }*  |
+ *                     <empty string> ;
+ *
+ *   term          ::= signed factor { ( '*' | '/' ) signed factor }* ;
+ *
+ *   signed factor ::= ( '+' | '-' )? factor ;
+ *
+ *   unit factor   ::= factor unit? ;
+ *
+ *   factor        ::= number | '(' expression ')' ;
+ *
+ *   number        ::= ? what g_strtod() consumes ? ;
+ *
+ *   unit          ::= ? what not g_strtod() consumes and not whitespace ? ;
+ *
+ * The code should match the EBNF rather closely (except for the
+ * non-terminal unit factor, which is inlined into factor) for
+ * maintainability reasons.
+ *
+ * It will allow 1++1 and 1+-1 (resulting in 2 and 0, respectively),
+ * but I figured one might want that, and I don't think it's going to
+ * throw anyone off.
+ */
+
+#include "config.h"
+
+#include <setjmp.h>
+#include <string.h>
+
+#include <glib-object.h>
+
+#include "gimpeevl.h"
+#include "gimpwidgets-error.h"
+
+#include "libgimp/libgimp-intl.h"
+
+
+typedef enum
+{
+  GIMP_EEVL_TOKEN_NUM        = 30000,
+  GIMP_EEVL_TOKEN_IDENTIFIER = 30001,
+
+  GIMP_EEVL_TOKEN_ANY        = 40000,
+
+  GIMP_EEVL_TOKEN_END        = 50000
+} GimpEevlTokenType;
+
+
+typedef struct
+{
+  GimpEevlTokenType type;
+
+  union
+  {
+    gdouble fl;
+
+    struct
+    {
+      const gchar *c;
+      gint         size;
+    };
+
+  } value;
+
+} GimpEevlToken;
+
+typedef struct
+{
+  const gchar             *string;
+  GimpEevlUnitResolverProc unit_resolver_proc;
+  gpointer                 data;
+
+  GimpEevlToken            current_token;
+  const gchar             *start_of_current_token;
+  
+
+  jmp_buf                  catcher;
+  const gchar             *error_message;
+
+} GimpEevl;
+
+
+static void             gimp_eevl_init                     (GimpEevl                 *eva,
+                                                            const gchar              *string,
+                                                            GimpEevlUnitResolverProc  unit_resolver_proc,
+                                                            gpointer                  data);
+static GimpEevlQuantity gimp_eevl_complete                 (GimpEevl                 *eva);
+static GimpEevlQuantity gimp_eevl_expression               (GimpEevl                 *eva);
+static GimpEevlQuantity gimp_eevl_term                     (GimpEevl                 *eva);
+static GimpEevlQuantity gimp_eevl_signed_factor            (GimpEevl                 *eva);
+static GimpEevlQuantity gimp_eevl_factor                   (GimpEevl                 *eva);
+static gboolean         gimp_eevl_accept                   (GimpEevl                 *eva,
+                                                            GimpEevlTokenType         token_type,
+                                                            GimpEevlToken            *consumed_token);
+static void             gimp_eevl_lex                      (GimpEevl                 *eva);
+static void             gimp_eevl_lex_accept_count          (GimpEevl                 *eva,
+                                                            gint                      count,
+                                                            GimpEevlTokenType         token_type);
+static void             gimp_eevl_lex_accept_to             (GimpEevl                 *eva,
+                                                            gchar                    *to,
+                                                            GimpEevlTokenType         token_type);
+static void             gimp_eevl_move_past_whitespace     (GimpEevl                 *eva);
+static gboolean         gimp_eevl_unit_identifier_start    (gunichar                  c);
+static gboolean         gimp_eevl_unit_identifier_continue (gunichar                  c);
+static gint             gimp_eevl_unit_identifier_size     (const gchar              *s,
+                                                            gint                      start);
+static void             gimp_eevl_expect                   (GimpEevl                 *eva,
+                                                            GimpEevlTokenType         token_type,
+                                                            GimpEevlToken            *value);
+static void             gimp_eevl_error                    (GimpEevl                 *eva,
+                                                            gchar                    *msg);
+
+
+/**
+ * gimp_eevl_evaluate:
+ * @string:             The NULL-terminated string to be evaluated.
+ * @unit_resolver_proc: Unit resolver callback.
+ * @result:             Result of evaluation.
+ * @data:               Data passed to unit resolver.
+ * @error_pos:          Will point to the poisiton within the string,
+ *                      before which the parse / evaluation error
+ *                      occured. Will be set to null of no error occured.
+ * @error_message:      Will point to a static string with a semi-descriptive
+ *                      error message if parsing / evaluation failed.
+ *
+ * Evaluates the given arithmetic expression, along with an optional dimension
+ * analysis, and basic unit conversions.
+ *
+ * All units conversions factors are relative to some implicit
+ * base-unit (which in GIMP is inches). This is also the unit of the
+ * returned value.
+ *
+ * Returns: A #GimpEevlQuantity with a value given in the base unit along with
+ * the order of the dimension (i.e. if the base unit is inches, a dimension
+ * order of two menas in^2).
+ **/
+gboolean
+gimp_eevl_evaluate (const gchar               *string,
+                    GimpEevlUnitResolverProc   unit_resolver_proc,
+                    GimpEevlQuantity          *result,
+                    gpointer                   data,
+                    const gchar              **error_pos,
+                    GError                   **error)
+{
+  GimpEevl eva;
+
+  g_return_val_if_fail (g_utf8_validate (string, -1, NULL), FALSE);
+  g_return_val_if_fail (unit_resolver_proc != NULL, FALSE);
+  g_return_val_if_fail (result != NULL, FALSE);
+  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+
+  gimp_eevl_init (&eva,
+                  string,
+                  unit_resolver_proc,
+                  data);
+
+  if (!setjmp (eva.catcher))  /* try... */
+    {
+      *result = gimp_eevl_complete (&eva);
+
+      return TRUE;
+    }
+  else   /* catch.. */
+    {
+      if (error_pos)
+        *error_pos = eva.start_of_current_token;
+
+      g_set_error_literal (error,
+                           GIMP_WIDGETS_ERROR,
+                           GIMP_WIDGETS_PARSE_ERROR,
+                           eva.error_message);
+
+      return FALSE;
+    }
+}
+
+static void
+gimp_eevl_init (GimpEevl                  *eva,
+                const gchar               *string,
+                GimpEevlUnitResolverProc   unit_resolver_proc,
+                gpointer                   data)
+{
+  eva->string              = string;
+  eva->unit_resolver_proc  = unit_resolver_proc;
+  eva->data                = data;
+
+  eva->current_token.type  = GIMP_EEVL_TOKEN_END;
+
+  eva->error_message       = NULL;
+
+  /* Preload symbol... */
+  gimp_eevl_lex (eva);
+}
+
+static GimpEevlQuantity
+gimp_eevl_complete (GimpEevl *eva)
+{
+  GimpEevlQuantity result = {0, 0};
+  GimpEevlQuantity default_unit_factor;
+
+  /* Empty expression evaluates to 0 */
+  if (gimp_eevl_accept (eva, GIMP_EEVL_TOKEN_END, NULL))
+    return result;
+
+  result = gimp_eevl_expression (eva);
+
+  /* There should be nothing left to parse by now */
+  gimp_eevl_expect (eva, GIMP_EEVL_TOKEN_END, 0);
+
+  eva->unit_resolver_proc (NULL,
+                           &default_unit_factor,
+                           eva->data);
+
+  /* Entire expression is dimensionless, apply default unit if
+   * applicable
+   */
+  if (result.dimension == 0 && default_unit_factor.dimension != 0)
+    {
+      result.value     /= default_unit_factor.value;
+      result.dimension  = default_unit_factor.dimension;
+    }
+  return result;
+}
+
+static GimpEevlQuantity
+gimp_eevl_expression (GimpEevl *eva)
+{
+  gboolean         subtract;
+  GimpEevlQuantity evaluated_terms;
+
+  evaluated_terms = gimp_eevl_term (eva);
+
+  /* continue evaluating terms, chained with + or -. */
+  for (subtract = FALSE;
+       gimp_eevl_accept (eva, '+', NULL) ||
+       (subtract = gimp_eevl_accept (eva, '-', NULL));
+       subtract = FALSE)
+    {
+      GimpEevlQuantity new_term = gimp_eevl_term (eva);
+
+      /* If dimensions missmatch, attempt default unit assignent */
+      if (new_term.dimension != evaluated_terms.dimension)
+        {
+          GimpEevlQuantity default_unit_factor;
+
+          eva->unit_resolver_proc (NULL,
+                                   &default_unit_factor,
+                                   eva->data);
+
+          if (new_term.dimension == 0 &&
+              evaluated_terms.dimension == default_unit_factor.dimension)
+            {
+              new_term.value     /= default_unit_factor.value;
+              new_term.dimension  = default_unit_factor.dimension;
+            }
+          else if (evaluated_terms.dimension == 0 &&
+                   new_term.dimension == default_unit_factor.dimension)
+            {
+              evaluated_terms.value     /= default_unit_factor.value;
+              evaluated_terms.dimension  = default_unit_factor.dimension;
+            }
+          else
+            {
+              gimp_eevl_error (eva, "Dimension missmatch during addition");
+            }
+        }
+
+      evaluated_terms.value += (subtract ? -new_term.value : new_term.value);
+    }
+
+  return evaluated_terms;
+}
+
+static GimpEevlQuantity
+gimp_eevl_term (GimpEevl *eva)
+{
+  gboolean         division;
+  GimpEevlQuantity evaluated_signed_factors;
+
+  evaluated_signed_factors = gimp_eevl_signed_factor (eva);
+
+  for (division = FALSE;
+       gimp_eevl_accept (eva, '*', NULL) ||
+       (division = gimp_eevl_accept (eva, '/', NULL));
+       division = FALSE)
+    {
+      GimpEevlQuantity new_signed_factor = gimp_eevl_signed_factor (eva);
+
+      if (division)
+        {
+          evaluated_signed_factors.value     /= new_signed_factor.value;
+          evaluated_signed_factors.dimension -= new_signed_factor.dimension;
+
+        }
+      else
+        {
+          evaluated_signed_factors.value     *= new_signed_factor.value;
+          evaluated_signed_factors.dimension += new_signed_factor.dimension;
+        }
+    }
+
+  return evaluated_signed_factors;
+}
+
+static GimpEevlQuantity
+gimp_eevl_signed_factor (GimpEevl *eva)
+{
+  GimpEevlQuantity result;
+  gboolean         negate = FALSE;
+
+  if (! gimp_eevl_accept (eva, '+', NULL))
+    negate = gimp_eevl_accept (eva, '-', NULL);
+
+  result = gimp_eevl_factor (eva);
+
+  if (negate) result.value = -result.value;
+
+  return result;
+}
+
+static GimpEevlQuantity
+gimp_eevl_factor (GimpEevl *eva)
+{
+  GimpEevlQuantity evaluated_factor = { 0, 0 };
+  GimpEevlToken    consumed_token;
+
+  if (gimp_eevl_accept (eva,
+                        GIMP_EEVL_TOKEN_NUM,
+                        &consumed_token))
+    {
+      evaluated_factor.value = consumed_token.value.fl;
+    }
+  else if (gimp_eevl_accept (eva, '(', NULL))
+    {
+      evaluated_factor = gimp_eevl_expression (eva);
+      gimp_eevl_expect (eva, ')', 0);
+    }
+  else
+    {
+      gimp_eevl_error (eva, "Expected number or '('");
+    }
+
+  if (eva->current_token.type == GIMP_EEVL_TOKEN_IDENTIFIER)
+    {
+      gchar            *identifier;
+      GimpEevlQuantity  result;
+
+      gimp_eevl_accept (eva,
+                        GIMP_EEVL_TOKEN_ANY,
+                        &consumed_token);
+
+      identifier = g_newa (gchar, consumed_token.value.size + 1);
+
+      strncpy (identifier, consumed_token.value.c, consumed_token.value.size);
+      identifier[consumed_token.value.size] = '\0';
+
+      if (eva->unit_resolver_proc (identifier,
+                                   &result,
+                                   eva->data))
+        {
+          evaluated_factor.value      /= result.value;
+          evaluated_factor.dimension  += result.dimension;
+        }
+      else
+        {
+          gimp_eevl_error (eva, "Unit was not resolved");
+        }
+    }
+
+  return evaluated_factor;
+}
+
+static gboolean
+gimp_eevl_accept (GimpEevl          *eva,
+                  GimpEevlTokenType  token_type,
+                  GimpEevlToken     *consumed_token)
+{
+  gboolean existed = FALSE;
+
+  if (token_type == eva->current_token.type ||
+      token_type == GIMP_EEVL_TOKEN_ANY)
+    {
+      existed = TRUE;
+
+      if (consumed_token)
+        *consumed_token = eva->current_token;
+
+      /* Parse next token */
+      gimp_eevl_lex (eva);
+    }
+
+  return existed;
+}
+
+static void
+gimp_eevl_lex (GimpEevl *eva)
+{
+  const gchar *s;
+
+  gimp_eevl_move_past_whitespace (eva);
+  s = eva->string;
+  eva->start_of_current_token = s;
+
+  if (! s || s[0] == '\0')
+    {
+      /* We're all done */
+      eva->current_token.type = GIMP_EEVL_TOKEN_END;
+    }
+  else if (s[0] == '+' || s[0] == '-')
+    {
+      /* Snatch these before the g_strtod() does, othewise they might
+       * be used in a numeric conversion.
+       */
+      gimp_eevl_lex_accept_count (eva, 1, s[0]);
+    }
+  else
+    
+    {
+      /* Attempt to parse a numeric value */
+      gchar  *endptr = NULL;
+      gdouble value      = g_strtod (s, &endptr);
+
+      if (endptr && endptr != s)
+        {
+          /* A numeric could be parsed, use it */
+          eva->current_token.value.fl = value;
+
+          gimp_eevl_lex_accept_to (eva, endptr, GIMP_EEVL_TOKEN_NUM);
+        }
+      else if (gimp_eevl_unit_identifier_start (s[0]))
+        {
+          /* Unit identifier */
+          eva->current_token.value.c    = s;
+          eva->current_token.value.size = gimp_eevl_unit_identifier_size (s, 0);
+
+          gimp_eevl_lex_accept_count (eva,
+                                      eva->current_token.value.size,
+                                      GIMP_EEVL_TOKEN_IDENTIFIER);
+        }
+      else
+        {
+          /* Everything else is a single character token */
+          gimp_eevl_lex_accept_count (eva, 1, s[0]);
+        }
+    }
+}
+
+static void
+gimp_eevl_lex_accept_count (GimpEevl          *eva,
+                            gint               count,
+                            GimpEevlTokenType  token_type)
+{
+  eva->current_token.type  = token_type;
+  eva->string             += count;
+}
+
+static void
+gimp_eevl_lex_accept_to (GimpEevl          *eva,
+                         gchar             *to,
+                         GimpEevlTokenType  token_type)
+{
+  eva->current_token.type = token_type;
+  eva->string             = to;
+}
+
+static void
+gimp_eevl_move_past_whitespace (GimpEevl *eva)
+{
+  if (! eva->string)
+    return;
+
+  while (g_ascii_isspace (*eva->string))
+    eva->string++;
+}
+
+static gboolean
+gimp_eevl_unit_identifier_start (gunichar c)
+{
+  return (g_unichar_isalpha (c) ||
+          c == (gunichar) '%'   ||
+          c == (gunichar) '\'');
+}
+
+static gboolean
+gimp_eevl_unit_identifier_continue (gunichar c)
+{
+  return (gimp_eevl_unit_identifier_start (c) ||
+          g_unichar_isdigit (c));
+}
+
+/**
+ * gimp_eevl_unit_identifier_size:
+ * @s:
+ * @start:
+ *
+ * Returns: Size of identifier in bytes (not including NULL
+ * terminator).
+ **/
+static gint
+gimp_eevl_unit_identifier_size (const gchar *string,
+                                gint         start_offset)
+{
+  const gchar *start  = g_utf8_offset_to_pointer (string, start_offset);
+  const gchar *s      = start;
+  gunichar     c      = g_utf8_get_char (s);
+  gint         length = 0;
+
+  if (gimp_eevl_unit_identifier_start (c))
+    {
+      s = g_utf8_next_char (s);
+      c = g_utf8_get_char (s);
+      length++;
+
+      while (gimp_eevl_unit_identifier_continue (c))
+        {
+          s = g_utf8_next_char (s);
+          c = g_utf8_get_char (s);
+          length++;
+        }
+    }
+  
+  return g_utf8_offset_to_pointer (start, length) - start;
+}
+
+static void
+gimp_eevl_expect (GimpEevl          *eva,
+                  GimpEevlTokenType  token_type,
+                  GimpEevlToken     *value)
+{
+  if (! gimp_eevl_accept (eva, token_type, value))
+    gimp_eevl_error (eva, "Unexpected token");
+}
+
+static void
+gimp_eevl_error (GimpEevl *eva,
+                 gchar    *msg)
+{
+  eva->error_message = msg;
+  longjmp (eva->catcher, 1);
+}
diff --git a/libgimpwidgets/gimpeevl.h b/libgimpwidgets/gimpeevl.h
new file mode 100644
index 0000000..a7a2c5b
--- /dev/null
+++ b/libgimpwidgets/gimpeevl.h
@@ -0,0 +1,67 @@
+/* LIBGIMP - The GIMP Library
+ * Copyright (C) 1995-1997 Peter Mattis and Spencer Kimball
+ *
+ * gimpeevl.h
+ * Copyright (C) 2008-2009 Fredrik Alstromer <roe excu se>
+ * Copyright (C) 2008-2009 Martin Nordholts <martinn svn gnome org>
+ *
+ * This library is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __GIMP_EEVL_H__
+#define __GIMP_EEVL_H__
+
+G_BEGIN_DECLS
+
+
+/**
+ * GimpEevlQuantity:
+ * @value:     In reference units.
+ * @dimension: in has a dimension of 1, in^2 has a dimension of 2 etc
+ */
+typedef struct
+{
+  gdouble value;
+  gint dimension;
+} GimpEevlQuantity;
+
+
+/**
+ * GimpEevlUnitResolverProc:
+ * @identifier: Identifier of unit to resolve or %NULL if default unit
+ *              should be resolved.
+ * @result:     Units per reference unit. For example, in GIMP the
+ *              reference unit is inches so resolving "mm" should
+ *              return 25.4 since there are 25.4 millimeters per inch.
+ * @data:       Data given to gimp_eevl_evaluate().
+ *
+ * Returns: If the unit was successfully resolved or not.
+ *
+ */
+typedef gboolean (* GimpEevlUnitResolverProc) (const gchar      *identifier,
+                                               GimpEevlQuantity *result,
+                                               gpointer          data);
+
+gboolean gimp_eevl_evaluate (const gchar               *string,
+                             GimpEevlUnitResolverProc   unit_resolver_proc,
+                             GimpEevlQuantity          *result,
+                             gpointer                   data,
+                             const gchar              **error_pos,
+                             GError                   **error);
+
+
+G_END_DECLS
+
+#endif /* __GIMP_EEVL_H__ */
diff --git a/libgimpwidgets/gimpsizeentry.c b/libgimpwidgets/gimpsizeentry.c
index 5310968..361c1b9 100644
--- a/libgimpwidgets/gimpsizeentry.c
+++ b/libgimpwidgets/gimpsizeentry.c
@@ -22,12 +22,15 @@
 
 #include "config.h"
 
+#include <string.h>
+
 #include <gtk/gtk.h>
 
 #include "libgimpbase/gimpbase.h"
 
 #include "gimpwidgets.h"
 
+#include "gimpeevl.h"
 #include "gimpsizeentry.h"
 
 
@@ -70,20 +73,27 @@ struct _GimpSizeEntryField
 };
 
 
-static void   gimp_size_entry_finalize        (GObject            *object);
-
-static void   gimp_size_entry_update_value    (GimpSizeEntryField *gsef,
-                                               gdouble             value);
-static void   gimp_size_entry_value_callback  (GtkWidget          *widget,
-                                               gpointer            data);
-static void   gimp_size_entry_update_refval   (GimpSizeEntryField *gsef,
-                                               gdouble             refval);
-static void   gimp_size_entry_refval_callback (GtkWidget          *widget,
-                                               gpointer            data);
-static void   gimp_size_entry_update_unit     (GimpSizeEntry      *gse,
-                                               GimpUnit            unit);
-static void   gimp_size_entry_unit_callback   (GtkWidget          *widget,
-                                               GimpSizeEntry      *sizeentry);
+static void      gimp_size_entry_finalize            (GObject            *object);
+static void      gimp_size_entry_update_value        (GimpSizeEntryField *gsef,
+                                                      gdouble             value);
+static void      gimp_size_entry_value_callback      (GtkWidget          *widget,
+                                                      gpointer            data);
+static void      gimp_size_entry_update_refval       (GimpSizeEntryField *gsef,
+                                                      gdouble             refval);
+static void      gimp_size_entry_refval_callback     (GtkWidget          *widget,
+                                                      gpointer            data);
+static void      gimp_size_entry_update_unit         (GimpSizeEntry      *gse,
+                                                      GimpUnit            unit);
+static void      gimp_size_entry_unit_callback       (GtkWidget          *widget,
+                                                      GimpSizeEntry      *sizeentry);
+static void      gimp_size_entry_attach_eevl         (GtkSpinButton      *spin_button,
+                                                      GimpSizeEntryField *gsef);
+static gint      gimp_size_entry_eevl_input_callback (GtkSpinButton      *spinner,
+                                                      gdouble            *return_val,
+                                                      gpointer           *data);
+static gboolean  gimp_size_entry_eevl_unit_resolver  (const gchar        *ident,
+                                                      GimpEevlQuantity   *result,
+                                                      gpointer            data);
 
 
 G_DEFINE_TYPE (GimpSizeEntry, gimp_size_entry, GTK_TYPE_TABLE)
@@ -287,6 +297,9 @@ gimp_size_entry_new (gint                       number_of_fields,
                                                      1.0, 10.0, 0.0,
                                                      1.0, digits);
 
+      gimp_size_entry_attach_eevl (GTK_SPIN_BUTTON (gsef->value_spinbutton),
+                                   gsef);
+
       if (spinbutton_width > 0)
         {
           if (spinbutton_width < 17)
@@ -403,6 +416,9 @@ gimp_size_entry_add_field  (GimpSizeEntry *gse,
                     G_CALLBACK (gimp_size_entry_value_callback),
                     gsef);
 
+  gimp_size_entry_attach_eevl (GTK_SPIN_BUTTON (gsef->value_spinbutton),
+                               gsef);
+
   if (gse->show_refval)
     {
       gsef->refval_adjustment =
@@ -1144,6 +1160,174 @@ gimp_size_entry_unit_callback (GtkWidget     *widget,
 }
 
 /**
+ * gimp_size_entry_attach_eevl:
+ * @spin_button:
+ * @gsef:
+ *
+ * Hooks in the GimpEevl unit expression parser into the
+ * #GtkSpinButton of the #GimpSizeEntryField.
+ **/
+static void
+gimp_size_entry_attach_eevl (GtkSpinButton      *spin_button,
+                             GimpSizeEntryField *gsef)
+{
+  gtk_spin_button_set_numeric (spin_button,
+                               FALSE);
+  gtk_spin_button_set_update_policy (spin_button,
+                                     GTK_UPDATE_IF_VALID);
+  g_signal_connect (spin_button, "input",
+                    G_CALLBACK (gimp_size_entry_eevl_input_callback),
+                    gsef);
+}
+
+static gint
+gimp_size_entry_eevl_input_callback (GtkSpinButton *spinner,
+                                     gdouble       *return_val,
+                                     gpointer      *data)
+{
+  GimpSizeEntryField *gsef      = (GimpSizeEntryField *) data;
+  gboolean            success   = FALSE;
+  const gchar        *error_pos = 0;
+  GError             *error     = NULL;
+  GimpEevlQuantity    result;
+
+  g_return_val_if_fail (GTK_IS_SPIN_BUTTON (spinner), FALSE);
+  g_return_val_if_fail (GIMP_IS_SIZE_ENTRY (gsef->gse), FALSE);
+
+  success = gimp_eevl_evaluate (gtk_entry_get_text (GTK_ENTRY (spinner)),
+                                gimp_size_entry_eevl_unit_resolver,
+                                &result,
+                                data,
+                                &error_pos,
+                                &error);
+  if (! success)
+    {
+      if (error && error_pos)
+        {
+          g_printerr ("ERROR: %s at '%s'\n",
+                      error->message,
+                      *error_pos ? error_pos : "<End of input>");
+        }
+      else
+        {
+          g_printerr ("ERROR: Expression evaluation failed without error.\n");
+        }
+
+      gtk_widget_error_bell (GTK_WIDGET (spinner));
+      return GTK_INPUT_ERROR;
+    }
+  else if (result.dimension != 1 && gsef->gse->unit != GIMP_UNIT_PERCENT)
+    {
+      g_printerr ("ERROR: result has wrong dimension (expected 1, got %d)\n", result.dimension);
+
+      gtk_widget_error_bell (GTK_WIDGET (spinner));
+      return GTK_INPUT_ERROR;
+    }
+  else if (result.dimension != 0 && gsef->gse->unit == GIMP_UNIT_PERCENT)
+    {
+      g_printerr ("ERROR: result has wrong dimension (expected 0, got %d)\n", result.dimension);
+
+      gtk_widget_error_bell (GTK_WIDGET (spinner));
+      return GTK_INPUT_ERROR;
+    }
+  else
+    {
+      /* transform back to UI-unit */
+      GimpEevlQuantity ui_unit;
+
+      switch (gsef->gse->unit)
+        {
+        case GIMP_UNIT_PIXEL:
+          ui_unit.value     = gsef->resolution;
+          ui_unit.dimension = 1;
+          break;
+        case GIMP_UNIT_PERCENT:
+          ui_unit.value     = 1.0;
+          ui_unit.dimension = 0;
+          break;
+        default:
+          ui_unit.value     = gimp_unit_get_factor(gsef->gse->unit);
+          ui_unit.dimension = 1;
+          break;
+        }
+
+      *return_val = result.value * ui_unit.value;
+
+      return TRUE;
+    }
+}
+
+static gboolean
+gimp_size_entry_eevl_unit_resolver (const gchar      *identifier,
+                                    GimpEevlQuantity *result,
+                                    gpointer          data)
+{
+  GimpSizeEntryField *gsef                 = (GimpSizeEntryField *) data;
+  gboolean            resolve_default_unit = (identifier == NULL);
+  GimpUnit            unit;
+  
+  g_return_val_if_fail (gsef, FALSE);
+  g_return_val_if_fail (result != NULL, FALSE);
+  g_return_val_if_fail (GIMP_IS_SIZE_ENTRY (gsef->gse), FALSE);
+
+
+  for (unit = 0;
+       unit <= gimp_unit_get_number_of_units ();
+       unit++)
+    {
+      /* Hack to handle percent within the loop */
+      if (unit == gimp_unit_get_number_of_units ())
+        unit = GIMP_UNIT_PERCENT;
+
+      if ((resolve_default_unit && unit == gsef->gse->unit) ||
+          (identifier &&
+           (strcmp (gimp_unit_get_symbol (unit),       identifier) == 0 ||
+            strcmp (gimp_unit_get_abbreviation (unit), identifier) == 0)))
+        {
+          switch (unit)
+            {
+            case GIMP_UNIT_PERCENT:
+              if (gsef->gse->unit == GIMP_UNIT_PERCENT)
+                {
+                  result->value = 1;
+                  result->dimension = 0;
+                }
+              else
+                {
+                  /* gsef->upper contains the '100%'-value */
+                  result->value = 100*gsef->resolution/gsef->upper;
+                  result->dimension = 1;
+                }
+              /* return here, don't perform percentage conversion */
+              return TRUE;
+            case GIMP_UNIT_PIXEL:
+              result->value     = gsef->resolution;
+              break;
+            default:
+              result->value     = gimp_unit_get_factor (unit);
+              break;
+            }
+
+          if (gsef->gse->unit == GIMP_UNIT_PERCENT)
+            {
+              /* map non-percentages onto percent */
+              result->value = gsef->upper/(100*gsef->resolution);
+              result->dimension = 0;
+            }
+          else
+            {
+              result->dimension = 1;
+            }
+
+          /* We are done */
+          return TRUE;
+        }
+    }
+
+  return FALSE;
+}
+
+/**
  * gimp_size_entry_show_unit_menu:
  * @gse: a #GimpSizeEntry
  * @show: Boolean
diff --git a/libgimpwidgets/test-eevl.c b/libgimpwidgets/test-eevl.c
new file mode 100644
index 0000000..eb1bbda
--- /dev/null
+++ b/libgimpwidgets/test-eevl.c
@@ -0,0 +1,191 @@
+/* LIBGIMP - The GIMP Library
+ * Copyright (C) 1995-1997 Peter Mattis and Spencer Kimball
+ *
+ * test-eevl.c
+ * Copyright (C) 2008 Fredrik Alstromer <roe excu se>
+ * Copyright (C) 2008 Martin Nordholts <martinn svn gnome org>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ * Boston, MA 02111-1307, USA.
+ */
+
+/* A small regression test case for the evaluator */
+
+#include "config.h"
+
+#include <string.h>
+
+#include <glib-object.h>
+
+#include "gimpeevl.h"
+
+
+typedef struct
+{
+  const gchar      *string;
+  GimpEevlQuantity  result;
+  gboolean          should_succeed;
+} TestCase;
+
+static TestCase cases[] =
+{
+  /* "Default" test case */
+  { "2in + 3in",                 { 2 + 3, 1},        TRUE },
+
+  /* Whitespace variations */
+  { "2in+3in",                   { 2 + 3, 1},        TRUE },
+  { "   2in + 3in",              { 2 + 3, 1},        TRUE },
+  { "2in + 3in   ",              { 2 + 3, 1},        TRUE },
+  { "2 in + 3 in",               { 2 + 3, 1},        TRUE },
+  { "   2   in   +   3   in   ", { 2 + 3, 1},        TRUE },
+
+  /* Make sure the default unit is applied as it should */
+  { "2 + 3in",                   { 2 + 3, 1 },       TRUE },
+  { "3",                         { 3, 1 },           TRUE },
+
+  /* Somewhat complicated input */
+  { "(2 + 3)in",                 { 2 + 3, 1},        TRUE },
+//  { "2 / 3 in",                  { 2 / 3., 1},        TRUE },
+  { "(2 + 2/3)in",               { 2 + 2 / 3., 1},    TRUE },
+  { "1/2 + 1/2",                 { 1, 1},            TRUE },
+
+  /* Mixing of units */
+  { "2mm + 3in",                 { 2 / 25.4 + 3, 1}, TRUE },
+
+  /* 'odd' behavior */
+  { "2 ++ 1",                    { 3, 1},            TRUE },
+  { "2 +- 1",                    { 1, 1},            TRUE },
+  { "2 -- 1",                    { 3, 1},            TRUE },
+
+  /* End of test cases */
+  { NULL, { 0, 0 }, TRUE }
+};
+
+
+static gboolean
+test_units (const gchar      *ident,
+            GimpEevlQuantity *result,
+            gpointer          data)
+{
+  gboolean resolved     = FALSE;
+  gboolean default_unit = (ident == NULL);
+
+  if (default_unit ||
+      (ident && strcmp ("in", ident) == 0))
+    {
+      result->dimension = 1;
+      result->value     = 1.;
+
+      resolved          = TRUE;
+    }
+  else if (ident && strcmp ("mm", ident) == 0)
+    {
+      result->dimension = 1;
+      result->value     = 25.4;
+
+      resolved          = TRUE;
+    }
+
+  return resolved;
+}
+
+
+int
+main(void)
+{
+  gint i;
+  gint failed    = 0;
+  gint succeeded = 0;
+
+  g_print ("Testing Eevl Eva, the Evaluator\n\n");
+
+  for (i = 0; cases[i].string; i++)
+    {
+      const gchar     *test           = cases[i].string;
+      GimpEevlQuantity should         = cases[i].result;
+      gboolean         success        = FALSE;
+      GimpEevlQuantity result         = { 0, -1 };
+      gboolean         should_succeed = cases[i].should_succeed;
+      GError          *error          = NULL;
+      const gchar     *error_pos      = 0;
+
+      success = gimp_eevl_evaluate (test,
+                                    test_units,
+                                    &result,
+                                    NULL,
+                                    &error_pos,
+                                    &error);
+
+      g_print ("%s = %lg (%d): ", test, result.value, result.dimension);
+      if (error || error_pos)
+        {
+          if (should_succeed)
+            {
+              failed++;
+              g_print ("evaluation failed ");
+              if (error)
+                {
+                  g_print ("with: %s, ", error->message);
+                }
+              else
+                {
+                  g_print ("without reason, ");
+                }
+              if (error_pos)
+                {
+                  if (*error_pos) g_print ("'%s'.", error_pos);
+                  else g_print ("at end of input.");
+                }
+              else
+                {
+                  g_print ("but didn't say where.");
+                }
+              g_print ("\n");
+            }
+          else
+            {
+              g_print ("OK (failure test case)\n");
+              succeeded++;
+            }
+        }
+      else if (!should_succeed)
+        {
+          g_print ("evaluation should've failed, but didn't.\n");
+          failed++;
+        }
+      else if (should.value != result.value || should.dimension != result.dimension)
+        {
+          g_print ("results don't match, should be: %lg (%d)\n",
+                    should.value, should.dimension);
+          failed++;
+        }
+      else
+        {
+          g_print ("OK\n");
+          succeeded++;
+        }
+    }
+
+  g_print ("\n");
+  if (!failed)
+    g_print ("All OK. ");
+  else
+    g_print ("Test failed! ");
+
+  g_print ("(%d/%d) %lg%%\n\n", succeeded, succeeded+failed,
+            100*succeeded/(gdouble)(succeeded+failed));
+
+  return failed;
+}



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]