/* Copyright (c) 1987-2008 Sun Microsystems, Inc. All Rights Reserved. * Copyright (c) 2008-2009 Robert Ancell * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2, or (at your option) * any later version. * * This program 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 * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. */ #include #include #include #include #include #include #include #include #include #include "math-equation.h" #include "mp.h" #include "mp-equation.h" #include "currency.h" enum { PROP_0, PROP_STATUS, PROP_DISPLAY, PROP_EQUATION, PROP_NUMBER_MODE, PROP_ACCURACY, PROP_SHOW_THOUSANDS_SEPARATORS, PROP_SHOW_TRAILING_ZEROES, PROP_NUMBER_FORMAT, PROP_BASE, PROP_WORD_SIZE, PROP_ANGLE_UNITS, PROP_SOURCE_CURRENCY, PROP_TARGET_CURRENCY }; static GType number_mode_type, number_format_type, angle_unit_type; #define MAX_DIGITS 512 /* Expression mode state */ typedef struct { MPNumber ans; /* Previously calculated answer */ gchar *expression; /* Expression entered by user */ gint ans_start, ans_end; /* Start and end characters for ans variable in expression */ gint cursor; /* ??? */ NumberMode number_mode; /* ??? */ gboolean can_super_minus; /* TRUE if entering minus can generate a superscript minus */ gboolean entered_multiply; /* Last insert was a multiply character */ gchar *status; /* Equation status */ } MathEquationState; struct MathEquationPrivate { GtkTextTag *ans_tag; gint show_tsep; /* Set if the thousands separator should be shown. */ gint show_zeroes; /* Set if trailing zeroes should be shown. */ DisplayFormat format; /* Number display mode. */ gint accuracy; /* Number of digits to show */ gint word_size; /* Word size in bits */ MPAngleUnit angle_units; /* Units for trigonometric functions */ char *source_currency; char *target_currency; gint base; /* Numeric base */ NumberMode number_mode; /* ??? */ gboolean can_super_minus; /* TRUE if entering minus can generate a superscript minus */ const char *digits[16]; /* Localized digit values */ const char *radix; /* Locale specific radix string. */ const char *tsep; /* Locale specific thousands separator. */ gint tsep_count; /* Number of digits between separator. */ GtkTextMark *ans_start, *ans_end; MathEquationState state; /* Equation state */ GList *undo_stack; /* History of expression mode states */ GList *redo_stack; gboolean in_undo_operation; gboolean in_reformat; gboolean in_delete; MathVariables *variables; }; G_DEFINE_TYPE (MathEquation, math_equation, GTK_TYPE_TEXT_BUFFER); MathEquation * math_equation_new() { return g_object_new (math_equation_get_type(), NULL); } MathVariables * math_equation_get_variables(MathEquation *equation) { return equation->priv->variables; } static void get_ans_offsets(MathEquation *equation, gint *start, gint *end) { GtkTextIter iter; if (!equation->priv->ans_start) { *start = *end = -1; return; } gtk_text_buffer_get_iter_at_mark(GTK_TEXT_BUFFER(equation), &iter, equation->priv->ans_start); *start = gtk_text_iter_get_offset(&iter); gtk_text_buffer_get_iter_at_mark(GTK_TEXT_BUFFER(equation), &iter, equation->priv->ans_end); *end = gtk_text_iter_get_offset(&iter); } static void reformat_ans(MathEquation *equation) { if (!equation->priv->ans_start) return; gchar *orig_ans_text; gchar ans_text[MAX_DIGITS]; GtkTextIter ans_start, ans_end; gtk_text_buffer_get_iter_at_mark(GTK_TEXT_BUFFER(equation), &ans_start, equation->priv->ans_start); gtk_text_buffer_get_iter_at_mark(GTK_TEXT_BUFFER(equation), &ans_end, equation->priv->ans_end); orig_ans_text = gtk_text_buffer_get_text(GTK_TEXT_BUFFER(equation), &ans_start, &ans_end, FALSE); display_make_number(equation, ans_text, MAX_DIGITS, &equation->priv->state.ans); if (strcmp(orig_ans_text, ans_text) != 0) { gint start; equation->priv->in_undo_operation = TRUE; equation->priv->in_reformat = TRUE; start = gtk_text_iter_get_offset(&ans_start); gtk_text_buffer_delete(GTK_TEXT_BUFFER(equation), &ans_start, &ans_end); gtk_text_buffer_insert_with_tags(GTK_TEXT_BUFFER(equation), &ans_end, ans_text, -1, equation->priv->ans_tag, NULL); /* There seems to be a bug in the marks as they alternate being the correct and incorrect ways. Reset them */ gtk_text_buffer_get_iter_at_offset(GTK_TEXT_BUFFER(equation), &ans_start, start); gtk_text_buffer_move_mark(GTK_TEXT_BUFFER(equation), equation->priv->ans_start, &ans_start); gtk_text_buffer_move_mark(GTK_TEXT_BUFFER(equation), equation->priv->ans_end, &ans_end); equation->priv->in_reformat = FALSE; equation->priv->in_undo_operation = FALSE; } gtk_text_buffer_get_iter_at_mark(GTK_TEXT_BUFFER(equation), &ans_start, equation->priv->ans_start); gtk_text_buffer_get_iter_at_mark(GTK_TEXT_BUFFER(equation), &ans_end, equation->priv->ans_end); g_free(orig_ans_text); } /* NOTE: Not efficent but easy to write */ // FIXME: This is just a lexer - use the same lexer as the solver static void reformat_base(MathEquation *equation, gint old_base) { gunichar sub_zero, sub_nine; gchar *text, *read_iter; gboolean in_number = FALSE, have_radix = FALSE; gint offset = 0, offset_step = 0, max_digit = 0, base = -1, base_offset = 0; gint ans_start, ans_end; if (equation->priv->base == old_base) return; sub_zero = g_utf8_get_char("₀"); sub_nine = g_utf8_get_char("₉"); read_iter = text = math_equation_get_display(equation); get_ans_offsets(equation, &ans_start, &ans_end); while (TRUE) { gunichar c; gint digit = -1, sub_digit = -1; /* See what digit this character is */ c = g_utf8_get_char(read_iter); if (c >= sub_zero && c <= sub_nine) sub_digit = c - sub_zero; else if (c >= 'a' && c <= 'z') digit = c - 'a'; else if (c >= 'A' && c <= 'Z') digit = c - 'A'; else digit = g_unichar_digit_value(c); /* Don't mess with ans */ if (offset >= ans_start && offset <= ans_end) { digit = -1; sub_digit = -1; } if (in_number && digit >= 0) { if (digit > max_digit) max_digit = digit; } else if (in_number && sub_digit >= 0) { if (base < 0) { base_offset = offset; base = 0; } base = base * 10 + sub_digit; } else if (in_number) { /* Allow one radix inside a number */ if (!have_radix && base < 0 && strncmp(read_iter, equation->priv->radix, strlen(equation->priv->radix)) == 0) { have_radix = TRUE; read_iter += strlen(equation->priv->radix); offset += g_utf8_strlen(equation->priv->radix, -1); continue; } /* If had no base then insert it */ if (base < 0) { GtkTextIter iter; gint multiplier = 1; gint b = old_base; const char *digits[] = {"₀", "₁", "₂", "₃", "₄", "₅", "₆", "₇", "₈", "₉"}; equation->priv->in_undo_operation = TRUE; equation->priv->in_reformat = TRUE; while (b / multiplier != 0) multiplier *= 10; while (multiplier != 1) { int d; multiplier /= 10; d = b / multiplier; gtk_text_buffer_get_iter_at_offset(GTK_TEXT_BUFFER(equation), &iter, offset + offset_step); gtk_text_buffer_insert(GTK_TEXT_BUFFER(equation), &iter, digits[d], -1); offset_step++; b -= d * multiplier; } equation->priv->in_reformat = FALSE; equation->priv->in_undo_operation = FALSE; } /* Remove the base if the current value */ else if (max_digit < base && base == equation->priv->base) { GtkTextIter start, end; equation->priv->in_undo_operation = TRUE; equation->priv->in_reformat = TRUE; gtk_text_buffer_get_iter_at_offset(GTK_TEXT_BUFFER(equation), &start, base_offset + offset_step); gtk_text_buffer_get_iter_at_offset(GTK_TEXT_BUFFER(equation), &end, offset + offset_step); gtk_text_buffer_delete(GTK_TEXT_BUFFER(equation), &start, &end); offset_step -= offset - base_offset; equation->priv->in_reformat = FALSE; equation->priv->in_undo_operation = FALSE; } in_number = FALSE; } else if (digit >= 0) { in_number = TRUE; have_radix = FALSE; base = -1; max_digit = digit; } if (c == '\0') break; read_iter = g_utf8_next_char(read_iter); offset++; } g_free(text); } static void reformat_separators(MathEquation *equation) { #if 0 gchar *text, *read_iter; gboolean in_number = FALSE, in_fraction = FALSE; text = math_equation_get_display(equation); /* Find numbers in display, and modify if necessary */ read_iter = text; while(*read_iter != '\0') { gunichar c; c = g_utf8_get_char(read_iter); if (strncmp(read_iter, equation->priv->tsep, strlen(equation->priv->tsep)) == 0) ; read_iter = g_utf8_next_char(read_iter); } g_free(text); #endif } static void reformat_display(MathEquation *equation, gint old_base) { /* Change ans */ reformat_ans(equation); /* Add/remove base suffixes if have changed base */ reformat_base(equation, old_base); /* Add/remove thousands separators */ reformat_separators(equation); } static MathEquationState * get_current_state(MathEquation *equation) { MathEquationState *state; gint ans_start = -1, ans_end = -1; state = g_malloc0(sizeof(MathEquationState)); if (equation->priv->ans_start) { GtkTextIter iter; gtk_text_buffer_get_iter_at_mark(GTK_TEXT_BUFFER(equation), &iter, equation->priv->ans_start); ans_start = gtk_text_iter_get_offset(&iter); gtk_text_buffer_get_iter_at_mark(GTK_TEXT_BUFFER(equation), &iter, equation->priv->ans_end); ans_end = gtk_text_iter_get_offset(&iter); } mp_set_from_mp(&equation->priv->state.ans, &state->ans); state->expression = math_equation_get_display(equation); state->ans_start = ans_start; state->ans_end = ans_end; g_object_get(G_OBJECT(equation), "cursor-position", &state->cursor, NULL); state->number_mode = equation->priv->number_mode; state->can_super_minus = equation->priv->can_super_minus; state->entered_multiply = equation->priv->state.entered_multiply; state->status = g_strdup(equation->priv->state.status); return state; } static void free_state(MathEquationState *state) { g_free(state->expression); g_free(state->status); g_free(state); } static void math_equation_push_undo_stack(MathEquation *equation) { GList *link; MathEquationState *state; if (equation->priv->in_undo_operation) return; math_equation_set_status(equation, ""); /* Can't redo anymore */ for (link = equation->priv->redo_stack; link; link = link->next) { state = link->data; free_state(state); } g_list_free(equation->priv->redo_stack); equation->priv->redo_stack = NULL; state = get_current_state(equation); equation->priv->undo_stack = g_list_prepend(equation->priv->undo_stack, state); } static void clear_ans(MathEquation *equation, gboolean remove_tag) { if (!equation->priv->ans_start) return; if (remove_tag) { GtkTextIter start, end; gtk_text_buffer_get_iter_at_mark(GTK_TEXT_BUFFER(equation), &start, equation->priv->ans_start); gtk_text_buffer_get_iter_at_mark(GTK_TEXT_BUFFER(equation), &end, equation->priv->ans_end); gtk_text_buffer_remove_tag(GTK_TEXT_BUFFER(equation), equation->priv->ans_tag, &start, &end); } gtk_text_buffer_delete_mark(GTK_TEXT_BUFFER(equation), equation->priv->ans_start); gtk_text_buffer_delete_mark(GTK_TEXT_BUFFER(equation), equation->priv->ans_end); equation->priv->ans_start = NULL; equation->priv->ans_end = NULL; } static void apply_state(MathEquation *equation, MathEquationState *state) { GtkTextIter cursor; /* Disable undo detection */ equation->priv->in_undo_operation = TRUE; mp_set_from_mp(&state->ans, &equation->priv->state.ans); gtk_text_buffer_set_text(GTK_TEXT_BUFFER(equation), state->expression, -1); gtk_text_buffer_get_iter_at_offset(GTK_TEXT_BUFFER(equation), &cursor, state->cursor); gtk_text_buffer_place_cursor(GTK_TEXT_BUFFER(equation), &cursor); clear_ans(equation, FALSE); if (state->ans_start >= 0) { GtkTextIter start, end; gtk_text_buffer_get_iter_at_offset(GTK_TEXT_BUFFER(equation), &start, state->ans_start); equation->priv->ans_start = gtk_text_buffer_create_mark(GTK_TEXT_BUFFER(equation), NULL, &start, FALSE); gtk_text_buffer_get_iter_at_offset(GTK_TEXT_BUFFER(equation), &end, state->ans_end); equation->priv->ans_end = gtk_text_buffer_create_mark(GTK_TEXT_BUFFER(equation), NULL, &end, TRUE); gtk_text_buffer_apply_tag(GTK_TEXT_BUFFER(equation), equation->priv->ans_tag, &start, &end); } math_equation_set_number_mode(equation, state->number_mode); equation->priv->can_super_minus = state->can_super_minus; equation->priv->state.entered_multiply = state->entered_multiply; math_equation_set_status(equation, state->status); equation->priv->in_undo_operation = FALSE; } void math_equation_copy(MathEquation *equation) { GtkTextIter start, end; gchar *text; if (!gtk_text_buffer_get_selection_bounds(GTK_TEXT_BUFFER(equation), &start, &end)) gtk_text_buffer_get_bounds(GTK_TEXT_BUFFER(equation), &start, &end); text = gtk_text_buffer_get_text(GTK_TEXT_BUFFER(equation), &start, &end, FALSE); gtk_clipboard_set_text(gtk_clipboard_get(GDK_NONE), text, -1); g_free (text); } static void on_paste(GtkClipboard *clipboard, const gchar *text, gpointer data) { MathEquation *equation = data; if (text != NULL) math_equation_insert(equation, text); } void math_equation_paste(MathEquation *equation) { gtk_clipboard_request_text(gtk_clipboard_get(GDK_NONE), on_paste, equation); } void math_equation_undo(MathEquation *equation) { GList *link; MathEquationState *state; if (!equation->priv->undo_stack) { math_equation_set_status(equation, /* Error shown when trying to undo with no undo history */ _("No undo history")); return; } link = equation->priv->undo_stack; equation->priv->undo_stack = g_list_remove_link(equation->priv->undo_stack, link); state = link->data; g_list_free(link); equation->priv->redo_stack = g_list_prepend(equation->priv->redo_stack, get_current_state(equation)); apply_state(equation, state); free_state(state); } void math_equation_redo(MathEquation *equation) { GList *link; MathEquationState *state; if (!equation->priv->redo_stack) { math_equation_set_status(equation, /* Error shown when trying to redo with no redo history */ _("No redo history")); return; } link = equation->priv->redo_stack; equation->priv->redo_stack = g_list_remove_link(equation->priv->redo_stack, link); state = link->data; g_list_free(link); equation->priv->undo_stack = g_list_prepend(equation->priv->undo_stack, get_current_state(equation)); apply_state(equation, state); free_state(state); } const gchar * math_equation_get_digit_text(MathEquation *equation, guint digit) { return equation->priv->digits[digit]; } const gchar * math_equation_get_numeric_point_text(MathEquation *equation) { return equation->priv->radix; } const gchar *math_equation_get_thousands_separator_text(MathEquation *equation) { return equation->priv->tsep; } void math_equation_set_accuracy(MathEquation *equation, gint accuracy) { if (equation->priv->accuracy == accuracy) return; equation->priv->accuracy = accuracy; reformat_display(equation, equation->priv->base); g_object_notify(G_OBJECT(equation), "accuracy"); } gint math_equation_get_accuracy(MathEquation *equation) { return equation->priv->accuracy; } void math_equation_set_show_thousands_separators(MathEquation *equation, gboolean visible) { if ((equation->priv->show_tsep && visible) || (!equation->priv->show_tsep && !visible)) return; equation->priv->show_tsep = visible; reformat_display(equation, equation->priv->base); g_object_notify(G_OBJECT(equation), "show-thousands-separators"); } gboolean math_equation_get_show_thousands_separators(MathEquation *equation) { return equation->priv->show_tsep; } void math_equation_set_show_trailing_zeroes(MathEquation *equation, gboolean visible) { if ((equation->priv->show_zeroes && visible) || (!equation->priv->show_zeroes && !visible)) return; equation->priv->show_zeroes = visible; reformat_display(equation, equation->priv->base); g_object_notify(G_OBJECT(equation), "show-trailing-zeroes"); } gboolean math_equation_get_show_trailing_zeroes(MathEquation *equation) { return equation->priv->show_zeroes; } void math_equation_set_number_format(MathEquation *equation, DisplayFormat format) { if (equation->priv->format == format) return; equation->priv->format = format; reformat_display(equation, equation->priv->base); g_object_notify(G_OBJECT(equation), "number-format"); } DisplayFormat math_equation_get_number_format(MathEquation *equation) { return equation->priv->format; } void math_equation_set_base(MathEquation *equation, gint base) { gint old_base; if (equation->priv->base == base) return; old_base = equation->priv->base; equation->priv->base = base; reformat_display(equation, old_base); g_object_notify(G_OBJECT(equation), "base"); } gint math_equation_get_base(MathEquation *equation) { return equation->priv->base; } void math_equation_set_word_size(MathEquation *equation, gint word_size) { if (equation->priv->word_size == word_size) return; equation->priv->word_size = word_size; g_object_notify(G_OBJECT(equation), "word-size"); } gint math_equation_get_word_size(MathEquation *equation) { return equation->priv->word_size; } void math_equation_set_angle_units(MathEquation *equation, MPAngleUnit angle_units) { if (equation->priv->angle_units == angle_units) return; equation->priv->angle_units = angle_units; g_object_notify(G_OBJECT(equation), "angle-units"); } MPAngleUnit math_equation_get_angle_units(MathEquation *equation) { return equation->priv->angle_units; } void math_equation_set_source_currency(MathEquation *equation, const gchar *currency) { // FIXME: Pick based on locale if (!currency || currency[0] == '\0') currency = currency_names[0].short_name; if (strcmp(equation->priv->source_currency, currency) == 0) return; g_free(equation->priv->source_currency); equation->priv->source_currency = g_strdup(currency); g_object_notify(G_OBJECT(equation), "source-currency"); } const gchar * math_equation_get_source_currency(MathEquation *equation) { return equation->priv->source_currency; } void math_equation_set_target_currency(MathEquation *equation, const gchar *currency) { // FIXME: Pick based on locale if (!currency || currency[0] == '\0') currency = currency_names[0].short_name; if (strcmp(equation->priv->target_currency, currency) == 0) return; g_free(equation->priv->target_currency); equation->priv->target_currency = g_strdup(currency); g_object_notify(G_OBJECT(equation), "target-currency"); } const gchar * math_equation_get_target_currency(MathEquation *equation) { return equation->priv->target_currency; } void math_equation_set_status(MathEquation *equation, const gchar *status) { if (strcmp(equation->priv->state.status, status) == 0) return; g_free(equation->priv->state.status); equation->priv->state.status = g_strdup(status); g_object_notify(G_OBJECT(equation), "status"); } const gchar * math_equation_get_status(MathEquation *equation) { return equation->priv->state.status; } gboolean math_equation_is_empty(MathEquation *equation) { return gtk_text_buffer_get_char_count(GTK_TEXT_BUFFER(equation)) == 0; } gboolean math_equation_is_result(MathEquation *equation) { char *text; gboolean result; text = math_equation_get_equation(equation); result = strcmp(text, "ans") == 0; g_free(text); return result; } gchar * math_equation_get_display(MathEquation *equation) { GtkTextIter start, end; gtk_text_buffer_get_bounds(GTK_TEXT_BUFFER(equation), &start, &end); return gtk_text_buffer_get_text(GTK_TEXT_BUFFER(equation), &start, &end, FALSE); } gchar * math_equation_get_equation(MathEquation *equation) { char *text, *t; gint ans_start, ans_end; text = math_equation_get_display(equation); /* No ans to substitute */ if(!equation->priv->ans_start) return text; get_ans_offsets(equation, &ans_start, &ans_end); t = g_strdup_printf("%.*sans%s", (int)(g_utf8_offset_to_pointer(text, ans_start) - text), text, g_utf8_offset_to_pointer(text, ans_end)); g_free(text); return t; } gboolean math_equation_get_number(MathEquation *equation, MPNumber *z) { gchar *text; gboolean result; text = math_equation_get_display(equation); result = !mp_set_from_string(text, equation->priv->base, z); g_free (text); return result; } void math_equation_set_number_mode(MathEquation *equation, NumberMode mode) { if (equation->priv->number_mode == mode) return; equation->priv->can_super_minus = mode == SUPERSCRIPT; equation->priv->number_mode = mode; g_object_notify(G_OBJECT(equation), "number-mode"); } NumberMode math_equation_get_number_mode(MathEquation *equation) { return equation->priv->number_mode; } const MPNumber * math_equation_get_answer(MathEquation *equation) { return &equation->priv->state.ans; } void math_equation_store(MathEquation *equation, const gchar *name) { MPNumber t; if (!math_equation_get_number(equation, &t)) math_equation_set_status(equation, _("No sane value to store")); else math_variables_set_value(equation->priv->variables, name, &t); } void math_equation_recall(MathEquation *equation, const gchar *name) { math_equation_insert(equation, name); } void math_equation_set(MathEquation *equation, const gchar *text) { gtk_text_buffer_set_text(GTK_TEXT_BUFFER(equation), text, -1); clear_ans(equation, FALSE); } void math_equation_set_number(MathEquation *equation, const MPNumber *x) { char text[MAX_DIGITS]; GtkTextIter start, end; /* Show the number in the user chosen format */ display_make_number(equation, text, MAX_DIGITS, x); gtk_text_buffer_set_text(GTK_TEXT_BUFFER(equation), text, -1); mp_set_from_mp(x, &equation->priv->state.ans); /* Mark this text as the answer variable */ gtk_text_buffer_get_bounds(GTK_TEXT_BUFFER(equation), &start, &end); clear_ans(equation, FALSE); equation->priv->ans_start = gtk_text_buffer_create_mark(GTK_TEXT_BUFFER(equation), NULL, &start, FALSE); equation->priv->ans_end = gtk_text_buffer_create_mark(GTK_TEXT_BUFFER(equation), NULL, &end, TRUE); gtk_text_buffer_apply_tag(GTK_TEXT_BUFFER(equation), equation->priv->ans_tag, &start, &end); } void math_equation_insert(MathEquation *equation, const gchar *text) { /* Replace ** with ^ (not on all keyboards) */ if (!gtk_text_buffer_get_has_selection(GTK_TEXT_BUFFER(equation)) && strcmp(text, "×") == 0 && equation->priv->state.entered_multiply) { GtkTextIter iter; gtk_text_buffer_get_iter_at_mark(GTK_TEXT_BUFFER(equation), &iter, gtk_text_buffer_get_insert(GTK_TEXT_BUFFER(equation))); gtk_text_buffer_backspace(GTK_TEXT_BUFFER(equation), &iter, TRUE, TRUE); gtk_text_buffer_insert_at_cursor(GTK_TEXT_BUFFER(equation), "^", -1); return; } /* Start new equation when entering digits after existing result */ if(math_equation_is_result(equation) && g_unichar_isdigit(g_utf8_get_char(text))) gtk_text_buffer_set_text(GTK_TEXT_BUFFER(equation), "", -1); /* Can't enter superscript minus after entering digits */ if (strstr("⁰¹²³⁴⁵⁶⁷⁸⁹", text) != NULL || strcmp("⁻", text) == 0) equation->priv->can_super_minus = FALSE; /* Disable super/subscript mode when finished entering */ if (strstr("⁻⁰¹²³⁴⁵⁶⁷⁸⁹₀₁₂₃₄₅₆₇₈₉", text) == NULL) math_equation_set_number_mode(equation, NORMAL); // FIXME: Add thousands separators gtk_text_buffer_delete_selection(GTK_TEXT_BUFFER(equation), FALSE, FALSE); gtk_text_buffer_insert_at_cursor(GTK_TEXT_BUFFER(equation), text, -1); } void math_equation_insert_digit(MathEquation *equation, guint digit) { static const char *subscript_digits[] = {"₀", "₁", "₂", "₃", "₄", "₅", "₆", "₇", "₈", "₉", NULL}; static const char *superscript_digits[] = {"⁰", "¹", "²", "³", "⁴", "⁵", "⁶", "⁷", "⁸", "⁹", NULL}; if (equation->priv->number_mode == NORMAL || digit >= 10) math_equation_insert(equation, math_equation_get_digit_text(equation, digit)); else if (equation->priv->number_mode == SUPERSCRIPT) math_equation_insert(equation, superscript_digits[digit]); else if (equation->priv->number_mode == SUBSCRIPT) math_equation_insert(equation, subscript_digits[digit]); } void math_equation_insert_numeric_point(MathEquation *equation) { math_equation_insert(equation, math_equation_get_numeric_point_text(equation)); } void math_equation_insert_number(MathEquation *equation, const MPNumber *x) { char text[MAX_DIGITS]; display_make_number(equation, text, MAX_DIGITS, x); math_equation_insert(equation, text); } void math_equation_insert_exponent(MathEquation *equation) { math_equation_insert(equation, "×10"); math_equation_set_number_mode(equation, SUPERSCRIPT); } void math_equation_insert_subtract(MathEquation *equation) { if (equation->priv->number_mode == SUPERSCRIPT && equation->priv->can_super_minus) { math_equation_insert(equation, "⁻"); equation->priv->can_super_minus = FALSE; } else { math_equation_insert(equation, "−"); math_equation_set_number_mode(equation, NORMAL); } } static int variable_is_defined(const char *name, void *data) { MathEquation *equation = data; char *c, *lower_name; lower_name = strdup(name); for (c = lower_name; *c; c++) *c = tolower(*c); if (strcmp(lower_name, "rand") == 0 || strcmp(lower_name, "ans") == 0) { g_free (lower_name); return 1; } g_free (lower_name); return math_variables_get_value(equation->priv->variables, name) != NULL; } static int get_variable(const char *name, MPNumber *z, void *data) { char *c, *lower_name; int result = 1; MathEquation *equation = data; MPNumber *t; lower_name = strdup(name); for (c = lower_name; *c; c++) *c = tolower(*c); if (strcmp(lower_name, "rand") == 0) mp_set_from_random(z); else if (strcmp(lower_name, "ans") == 0) mp_set_from_mp(&equation->priv->state.ans, z); else { t = math_variables_get_value(equation->priv->variables, name); if (t) mp_set_from_mp(t, z); else result = 0; } free(lower_name); return result; } static void set_variable(const char *name, const MPNumber *x, void *data) { MathEquation *equation = data; /* FIXME: Don't allow writing to built-in variables, e.g. ans, rand, sin, ... */ math_variables_set_value(equation->priv->variables, name, x); } static int convert(const MPNumber *x, const char *x_units, const char *z_units, MPNumber *z, void *data) { return currency_convert(x, x_units, z_units, z); } static int parse(MathEquation *equation, const char *text, MPNumber *z, char **error_token) { MPEquationOptions options; memset(&options, 0, sizeof(options)); options.base = equation->priv->base; options.wordlen = equation->priv->word_size; options.angle_units = equation->priv->angle_units; options.variable_is_defined = variable_is_defined; options.get_variable = get_variable; options.set_variable = set_variable; options.convert = convert; options.callback_data = equation; return mp_equation_parse(text, &options, z, error_token); } void math_equation_solve(MathEquation *equation) { MPNumber z; gint result, n_brackets = 0; gchar *c, *text, *error_token = NULL, *message = NULL; GString *equation_text; if (math_equation_is_empty(equation)) return; /* If showing a result return to the equation that caused it */ // FIXME: Result may not be here due to solve (i.e. the user may have entered "ans") if (math_equation_is_result(equation)) { math_equation_undo(equation); return; } math_equation_set_number_mode(equation, NORMAL); text = math_equation_get_equation(equation); equation_text = g_string_new(text); g_free(text); /* Count the number of brackets and automatically add missing closing brackets */ for (c = equation_text->str; *c; c++) { if (*c == '(') n_brackets++; else if (*c == ')') n_brackets--; } while (n_brackets > 0) { g_string_append_c(equation_text, ')'); n_brackets--; } result = parse(equation, equation_text->str, &z, &error_token); g_string_free(equation_text, TRUE); switch (result) { case PARSER_ERR_NONE: math_equation_set_number(equation, &z); break; case PARSER_ERR_OVERFLOW: message = g_strdup(/* Error displayed to user when they perform a bitwise operation on numbers greater than the current word */ _("Overflow. Try a bigger word size")); break; case PARSER_ERR_UNKNOWN_VARIABLE: message = g_strdup_printf(/* Error displayed to user when they an unknown variable is entered */ _("Unknown variable '%s'"), error_token); break; case PARSER_ERR_UNKNOWN_FUNCTION: message = g_strdup_printf(/* Error displayed to user when an unknown function is entered */ _("Function '%s' is not defined"), error_token); break; case PARSER_ERR_UNKNOWN_CONVERSION: message = g_strdup(/* Error displayed to user when an conversion with unknown units is attempted */ _("Unknown conversion")); break; case PARSER_ERR_MP: message = g_strdup(mp_get_error()); break; default: message = g_strdup(/* Error displayed to user when they enter an invalid calculation */ _("Malformed expression")); break; } if (error_token) free(error_token); if (message) { math_equation_set_status(equation, message); g_free(message); } } void math_equation_factorize(MathEquation *equation) { MPNumber x; GList *factors, *factor; GString *text; if (!math_equation_get_number(equation, &x) || !mp_is_integer(&x)) { /* Error displayed when trying to factorize a non-integer value */ math_equation_set_status(equation, _("Need an integer to factorize")); return; } factors = mp_factorize(&x); text = g_string_new(""); for (factor = factors; factor; factor = factor->next) { gchar temp[MAX_DIGITS]; MPNumber *n; n = factor->data; display_make_number(equation, temp, MAX_DIGITS, n); g_string_append(text, temp); if (factor->next) g_string_append(text, "×"); g_slice_free(MPNumber, n); } g_list_free(factors); math_equation_set(equation, text->str); g_string_free(text, TRUE); } void math_equation_delete(MathEquation *equation) { gint cursor; GtkTextIter start, end; g_object_get(G_OBJECT(equation), "cursor-position", &cursor, NULL); if (cursor >= gtk_text_buffer_get_char_count(GTK_TEXT_BUFFER(equation))) return; gtk_text_buffer_get_iter_at_offset(GTK_TEXT_BUFFER(equation), &start, cursor); gtk_text_buffer_get_iter_at_offset(GTK_TEXT_BUFFER(equation), &end, cursor+1); gtk_text_buffer_delete(GTK_TEXT_BUFFER(equation), &start, &end); } void math_equation_backspace(MathEquation *equation) { /* Can't delete empty display */ if (math_equation_is_empty(equation)) return; if (gtk_text_buffer_get_has_selection(GTK_TEXT_BUFFER(equation))) gtk_text_buffer_delete_selection(GTK_TEXT_BUFFER(equation), FALSE, FALSE); else { GtkTextIter iter; gtk_text_buffer_get_iter_at_mark(GTK_TEXT_BUFFER(equation), &iter, gtk_text_buffer_get_insert(GTK_TEXT_BUFFER(equation))); gtk_text_buffer_backspace(GTK_TEXT_BUFFER(equation), &iter, TRUE, TRUE); } } void math_equation_clear(MathEquation *equation) { math_equation_set_number_mode(equation, NORMAL); gtk_text_buffer_set_text(GTK_TEXT_BUFFER(equation), "", -1); clear_ans(equation, FALSE); } void math_equation_shift(MathEquation *equation, gint count) { MPNumber z; if (!math_equation_get_number(equation, &z)) { math_equation_set_status(equation, /* This message is displayed in the status bar when a bit shift operation is performed and the display does not contain a number */ _("No sane value to bitwise shift")); return; } mp_shift(&z, count, &z); math_equation_set_number(equation, &z); } void math_equation_toggle_bit(MathEquation *equation, guint bit) { MPNumber x; guint64 bits; gboolean result; result = math_equation_get_number(equation, &x); if (result) { MPNumber max; mp_set_from_unsigned_integer(G_MAXUINT64, &max); if (mp_is_negative(&x) || mp_is_greater_than(&x, &max)) result = FALSE; else bits = mp_cast_to_unsigned_int(&x); } if (!result) { math_equation_set_status(equation, /* Message displayed when cannot toggle bit in display*/ _("Displayed value not an integer")); return; } bits ^= (1LL << (63 - bit)); mp_set_from_unsigned_integer(bits, &x); // FIXME: Only do this if in ans format, otherwise set text in same format as previous number math_equation_set_number(equation, &x); } /* Convert MP number to character string. */ //FIXME: What to do with this? void display_make_number(MathEquation *equation, char *target, int target_len, const MPNumber *x) { switch(equation->priv->format) { case FIX: mp_cast_to_string(x, equation->priv->base, equation->priv->base, equation->priv->accuracy, !equation->priv->show_zeroes, target, target_len); break; case SCI: mp_cast_to_exponential_string(x, equation->priv->base, equation->priv->base, equation->priv->accuracy, !equation->priv->show_zeroes, false, target, target_len); break; case ENG: mp_cast_to_exponential_string(x, equation->priv->base, equation->priv->base, equation->priv->accuracy, !equation->priv->show_zeroes, true, target, target_len); break; } } static void math_equation_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { MathEquation *self; self = MATH_EQUATION (object); switch (prop_id) { case PROP_STATUS: math_equation_set_status(self, g_value_get_string(value)); break; case PROP_DISPLAY: math_equation_set(self, g_value_get_string(value)); break; case PROP_NUMBER_MODE: math_equation_set_number_mode(self, g_value_get_int(value)); break; case PROP_ACCURACY: math_equation_set_accuracy(self, g_value_get_int(value)); break; case PROP_SHOW_THOUSANDS_SEPARATORS: math_equation_set_show_thousands_separators(self, g_value_get_boolean(value)); break; case PROP_SHOW_TRAILING_ZEROES: math_equation_set_show_trailing_zeroes(self, g_value_get_boolean(value)); break; case PROP_NUMBER_FORMAT: math_equation_set_number_format(self, g_value_get_int(value)); break; case PROP_BASE: math_equation_set_base(self, g_value_get_int(value)); break; case PROP_WORD_SIZE: math_equation_set_word_size(self, g_value_get_int(value)); break; case PROP_ANGLE_UNITS: math_equation_set_angle_units(self, g_value_get_int(value)); break; case PROP_SOURCE_CURRENCY: math_equation_set_source_currency(self, g_value_get_string(value)); break; case PROP_TARGET_CURRENCY: math_equation_set_target_currency(self, g_value_get_string(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void math_equation_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { MathEquation *self; gchar *text; self = MATH_EQUATION (object); switch (prop_id) { case PROP_STATUS: g_value_set_string(value, self->priv->state.status); break; case PROP_DISPLAY: text = math_equation_get_display(self); g_value_set_string(value, text); g_free(text); break; case PROP_EQUATION: text = math_equation_get_equation(self); g_value_set_string(value, text); g_free(text); break; case PROP_NUMBER_MODE: g_value_set_enum(value, self->priv->number_mode); break; case PROP_ACCURACY: g_value_set_int(value, self->priv->accuracy); break; case PROP_SHOW_THOUSANDS_SEPARATORS: g_value_set_boolean(value, self->priv->show_tsep); break; case PROP_SHOW_TRAILING_ZEROES: g_value_set_boolean(value, self->priv->show_zeroes); break; case PROP_NUMBER_FORMAT: g_value_set_enum(value, self->priv->format); break; case PROP_BASE: g_value_set_int(value, math_equation_get_base(self)); break; case PROP_WORD_SIZE: g_value_set_int(value, self->priv->word_size); break; case PROP_ANGLE_UNITS: g_value_set_enum(value, self->priv->angle_units); break; case PROP_SOURCE_CURRENCY: g_value_set_string(value, self->priv->source_currency); break; case PROP_TARGET_CURRENCY: g_value_set_string(value, self->priv->target_currency); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void math_equation_class_init (MathEquationClass *klass) { static GEnumValue number_mode_values[] = { {NORMAL, "normal", "normal"}, {SUPERSCRIPT, "superscript", "superscript"}, {SUBSCRIPT, "subscript", "subscript"}, {0, NULL, NULL} }; static GEnumValue number_format_values[] = { {FIX, "fixed-point", "fixed-point"}, {SCI, "scientific", "scientific"}, {ENG, "engineering", "engineering"}, {0, NULL, NULL} }; static GEnumValue angle_unit_values[] = { {MP_RADIANS, "radians", "radians"}, {MP_DEGREES, "degrees", "degrees"}, {MP_GRADIANS, "gradians", "gradians"}, {0, NULL, NULL} }; GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->get_property = math_equation_get_property; object_class->set_property = math_equation_set_property; g_type_class_add_private (klass, sizeof (MathEquationPrivate)); number_mode_type = g_enum_register_static("NumberMode", number_mode_values); number_format_type = g_enum_register_static("DisplayFormat", number_format_values); angle_unit_type = g_enum_register_static("AngleUnit", angle_unit_values); g_object_class_install_property(object_class, PROP_STATUS, g_param_spec_string("status", "status", "Equation status", "", G_PARAM_READWRITE)); g_object_class_install_property(object_class, PROP_DISPLAY, g_param_spec_string("display", "display", "Displayed equation text", "", G_PARAM_READWRITE)); g_object_class_install_property(object_class, PROP_EQUATION, g_param_spec_string("equation", "equation", "Equation text", "", G_PARAM_READABLE)); g_object_class_install_property(object_class, PROP_NUMBER_MODE, g_param_spec_enum("number-mode", "number-mode", "Input number mode", number_mode_type, NORMAL, G_PARAM_READWRITE)); g_object_class_install_property(object_class, PROP_ACCURACY, g_param_spec_int("accuracy", "accuracy", "Display accuracy", 0, 20, 9, G_PARAM_READWRITE)); g_object_class_install_property(object_class, PROP_SHOW_THOUSANDS_SEPARATORS, g_param_spec_boolean("show-thousands-separators", "show-thousands-separators", "Show thousands separators", TRUE, G_PARAM_READWRITE)); g_object_class_install_property(object_class, PROP_SHOW_TRAILING_ZEROES, g_param_spec_boolean("show-trailing-zeroes", "show-trailing-zeroes", "Show trailing zeroes", FALSE, G_PARAM_READWRITE)); g_object_class_install_property(object_class, PROP_NUMBER_FORMAT, g_param_spec_enum("number-format", "number-format", "Display format", number_format_type, FIX, G_PARAM_READWRITE)); g_object_class_install_property(object_class, PROP_BASE, g_param_spec_int("base", "base", "Default number base (derived from number-format)", 2, 16, 10, G_PARAM_READWRITE)); g_object_class_install_property(object_class, PROP_WORD_SIZE, g_param_spec_int("word-size", "word-size", "Word size in bits", 8, 64, 64, G_PARAM_READWRITE)); g_object_class_install_property(object_class, PROP_ANGLE_UNITS, g_param_spec_enum("angle-units", "angle-units", "Angle units", angle_unit_type, MP_DEGREES, G_PARAM_READWRITE)); g_object_class_install_property(object_class, PROP_SOURCE_CURRENCY, g_param_spec_string("source-currency", "source-currency", "Source Currency", "", G_PARAM_READWRITE)); g_object_class_install_property(object_class, PROP_TARGET_CURRENCY, g_param_spec_string("target-currency", "target-currency", "target Currency", "", G_PARAM_READWRITE)); } static void pre_insert_text_cb (MathEquation *equation, GtkTextIter *location, gchar *text, gint len, gpointer user_data) { gunichar c; if (equation->priv->in_reformat) return; /* If following a delete then have already pushed undo stack (GtkTextBuffer doesn't indicate replace operations so we have to infer them) */ if (!equation->priv->in_delete) math_equation_push_undo_stack(equation); /* Clear result on next digit entered if cursor at end of line */ // FIXME Cursor c = g_utf8_get_char(text); if (g_unichar_isdigit(c) && math_equation_is_result(equation)) { gtk_text_buffer_set_text(GTK_TEXT_BUFFER(equation), "", -1); clear_ans(equation, FALSE); gtk_text_buffer_get_end_iter(GTK_TEXT_BUFFER(equation), location); } if (equation->priv->ans_start) { gint ans_start, ans_end; gint offset; offset = gtk_text_iter_get_offset(location); get_ans_offsets(equation, &ans_start, &ans_end); /* Inserted inside ans */ if (offset > ans_start && offset < ans_end) clear_ans(equation, TRUE); } } static gboolean on_delete(MathEquation *equation) { equation->priv->in_delete = FALSE; return FALSE; } static void pre_delete_range_cb (MathEquation *equation, GtkTextIter *start, GtkTextIter *end, gpointer user_data) { if (equation->priv->in_reformat) return; math_equation_push_undo_stack(equation); equation->priv->in_delete = TRUE; g_idle_add((GSourceFunc)on_delete, equation); if (equation->priv->ans_start) { gint ans_start, ans_end; gint start_offset, end_offset; start_offset = gtk_text_iter_get_offset(start); end_offset = gtk_text_iter_get_offset(end); get_ans_offsets(equation, &ans_start, &ans_end); /* Deleted part of ans */ if (start_offset < ans_end && end_offset > ans_start) clear_ans(equation, TRUE); } } static void insert_text_cb (MathEquation *equation, GtkTextIter *location, gchar *text, gint len, gpointer user_data) { if (equation->priv->in_reformat) return; equation->priv->state.entered_multiply = strcmp(text, "×") == 0; g_object_notify(G_OBJECT(equation), "display"); } static void delete_range_cb (MathEquation *equation, GtkTextIter *start, GtkTextIter *end, gpointer user_data) { if (equation->priv->in_reformat) return; // FIXME: A replace will emit this both for delete-range and insert-text, can it be avoided? g_object_notify(G_OBJECT(equation), "display"); } static void math_equation_init(MathEquation *equation) { /* Digits localized for the given language */ const char *digit_values = _("0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F"); const char *default_digits[] = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"}; gchar *radix, *tsep; gchar **digits; gboolean use_default_digits = FALSE; int i; equation->priv = G_TYPE_INSTANCE_GET_PRIVATE (equation, math_equation_get_type(), MathEquationPrivate); // FIXME: Causes error // (process:18573): Gtk-CRITICAL **: set_table: assertion buffer->tag_table == NULL' failed equation->priv->ans_tag = gtk_text_buffer_create_tag(GTK_TEXT_BUFFER(equation), NULL, "weight", PANGO_WEIGHT_BOLD, NULL); g_signal_connect(equation, "insert-text", G_CALLBACK(pre_insert_text_cb), equation); g_signal_connect(equation, "delete-range", G_CALLBACK(pre_delete_range_cb), equation); g_signal_connect_after(equation, "insert-text", G_CALLBACK(insert_text_cb), equation); g_signal_connect_after(equation, "delete-range", G_CALLBACK(delete_range_cb), equation); digits = g_strsplit(digit_values, ",", -1); for (i = 0; i < 16; i++) { if (use_default_digits || digits[i] == NULL) { use_default_digits = TRUE; equation->priv->digits[i] = strdup(default_digits[i]); } else equation->priv->digits[i] = strdup(digits[i]); } g_strfreev(digits); setlocale(LC_NUMERIC, ""); radix = nl_langinfo(RADIXCHAR); equation->priv->radix = radix ? g_locale_to_utf8(radix, -1, NULL, NULL, NULL) : g_strdup("."); tsep = nl_langinfo(THOUSEP); equation->priv->tsep = tsep ? g_locale_to_utf8(tsep, -1, NULL, NULL, NULL) : g_strdup(","); equation->priv->tsep_count = 3; equation->priv->variables = math_variables_new(); equation->priv->state.status = g_strdup(""); equation->priv->show_zeroes = FALSE; equation->priv->show_tsep = FALSE; equation->priv->format = FIX; equation->priv->accuracy = 9; equation->priv->word_size = 32; equation->priv->angle_units = MP_DEGREES; // FIXME: Pick based on locale equation->priv->source_currency = g_strdup(currency_names[0].short_name); equation->priv->target_currency = g_strdup(currency_names[0].short_name); equation->priv->base = 10; mp_set_from_integer(0, &equation->priv->state.ans); }