1/*
2 * Copyright (C) 2008-2012 Robert Ancell
3 *
4 * This program is free software: you can redistribute it and/or modify it under
5 * the terms of the GNU General Public License as published by the Free Software
6 * Foundation, either version 3 of the License, or (at your option) any later
7 * version. See http://www.gnu.org/copyleft/gpl.html the full text of the
8 * license.
9 */
10
11public class MathDisplay : Gtk.Box
12{
13    /* Equation being displayed */
14    private MathEquation _equation;
15    public MathEquation equation { get { return _equation; } }
16    private HistoryView history;
17
18    /* Display widget */
19    Gtk.SourceView source_view;
20
21    /* Buffer that shows errors etc */
22    Gtk.TextBuffer info_buffer;
23
24    /* Spinner widget that shows if we're calculating a response */
25    Gtk.Spinner spinner;
26    public bool completion_visible { get; set;}
27    public bool completion_selected { get; set;}
28
29    Regex only_variable_name = /^_*\p{L}+(_|\p{L})*$/;
30    Regex only_function_definition = /^[a-zA-Z0-9 ]*\(([a-zA-z0-9;]*)?\)[ ]*$/;
31
32    static construct {
33        set_css_name ("mathdisplay");
34    }
35
36    public MathDisplay (MathEquation equation)
37    {
38        _equation = equation;
39        _equation.history_signal.connect (this.update_history);
40        orientation = Gtk.Orientation.VERTICAL;
41
42        history = new HistoryView ();
43        history.answer_clicked.connect ((ans) => { insert_text (ans); });
44        history.equation_clicked.connect ((eq) => { display_text (eq); });
45        history.set_serializer (equation.serializer);
46        _equation.display_changed.connect (history.set_serializer);
47        add (history);
48        show_all ();
49
50        var scrolled_window = new Gtk.ScrolledWindow (null, null);
51        var style_context = scrolled_window.get_style_context ();
52        style_context.add_class ("display-scrolled");
53
54        scrolled_window.set_policy (Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.NEVER);
55        source_view = new Gtk.SourceView.with_buffer (equation);
56        source_view.set_accepts_tab (false);
57        source_view.set_left_margin (14);
58        source_view.set_pixels_above_lines (8);
59        source_view.set_pixels_below_lines (2);
60        source_view.set_justification (Gtk.Justification.LEFT);
61
62        set_enable_osk (false);
63
64        source_view.set_name ("displayitem");
65        source_view.set_size_request (20, 20);
66        source_view.get_accessible ().set_role (Atk.Role.EDITBAR);
67        //FIXME:<property name="AtkObject::accessible-description" translatable="yes" comments="Accessible description for the area in which results are displayed">Result Region</property>
68        source_view.key_press_event.connect (key_press_cb);
69        create_autocompletion ();
70        completion_visible = false;
71        completion_selected = false;
72
73        pack_start (scrolled_window, false, false, 0);
74        scrolled_window.add (source_view); /* Adds ScrolledWindow to source_view for displaying long equations */
75        scrolled_window.show ();
76
77        var info_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6);
78        pack_start (info_box, false, true, 0);
79
80        var info_view = new Gtk.TextView ();
81        info_view.set_wrap_mode (Gtk.WrapMode.WORD);
82        info_view.set_can_focus (false);
83        info_view.set_editable (false);
84        info_view.set_left_margin (12);
85        info_view.set_right_margin (12);
86        info_box.pack_start (info_view, true, true, 0);
87        info_buffer = info_view.get_buffer ();
88
89        style_context = info_view.get_style_context ();
90        style_context.add_class ("info-view");
91
92        spinner = new Gtk.Spinner ();
93        info_box.pack_end (spinner, false, false, 0);
94
95        info_box.show ();
96        info_view.show ();
97        source_view.show ();
98
99        equation.notify["status"].connect ((pspec) => { status_changed_cb (); });
100        status_changed_cb ();
101
102        equation.notify["error-token-end"].connect ((pspec) => { error_status_changed_cb (); });
103    }
104
105    public void set_enable_osk (bool enable_osk)
106    {
107        const Gtk.InputHints hints = Gtk.InputHints.NO_EMOJI | Gtk.InputHints.NO_SPELLCHECK;
108        source_view.set_input_hints (enable_osk ? hints : hints | Gtk.InputHints.INHIBIT_OSK);
109    }
110
111    public void grabfocus () /* Editbar grabs focus when an instance of gnome-calculator is created */
112    {
113        source_view.grab_focus ();
114    }
115
116    public void update_history (string answer, Number number, int number_base, uint representation_base)
117    {
118        /* Recieves signal emitted by a MathEquation object for updating history-view */
119        history.insert_entry (answer, number, number_base, representation_base); /* Sends current equation and answer for updating History-View */
120    }
121
122    public void display_text (string prev_eq)
123    {
124        _equation.display_selected (prev_eq);
125    }
126
127    public void clear_history ()
128    {
129        history.clear ();
130    }
131
132    public void insert_text (string answer)
133    {
134        _equation.insert_selected (answer);
135    }
136
137    private void create_autocompletion ()
138    {
139        Gtk.SourceCompletion completion = source_view.get_completion ();
140        completion.show.connect ((completion) => { this.completion_visible = true; this.completion_selected = false;} );
141        completion.hide.connect ((completion) => { this.completion_visible = false; this.completion_selected = false; } );
142        completion.move_cursor.connect ((completion) => {this.completion_selected = true;});
143        completion.select_on_show = false;
144        try
145        {
146            completion.add_provider (new FunctionCompletionProvider ());
147            completion.add_provider (new VariableCompletionProvider (equation));
148        }
149        catch (Error e)
150        {
151            warning ("Could not add CompletionProvider to source-view");
152        }
153    }
154
155    protected override bool key_press_event (Gdk.EventKey event)
156    {
157        return source_view.key_press_event (event);
158    }
159
160    private bool key_press_cb (Gdk.EventKey event)
161    {
162        /* Clear on escape */
163        var state = event.state & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.MOD1_MASK);
164
165        if ((event.keyval == Gdk.Key.Escape && state == 0 && !completion_visible) ||
166            (event.keyval == Gdk.Key.Delete && (event.state & Gdk.ModifierType.CONTROL_MASK) == Gdk.ModifierType.CONTROL_MASK))
167        {
168            equation.clear ();
169            status_changed_cb ();
170            return true;
171        } else if (event.keyval == Gdk.Key.Escape && state == 0 && completion_visible)
172        /* If completion window is shown and escape is pressed, hide it */
173        {
174            Gtk.SourceCompletion completion = source_view.get_completion ();
175            completion.hide ();
176            return true;
177        } else if (state == Gdk.ModifierType.MOD1_MASK && (event.keyval == Gdk.Key.Left || event.keyval == Gdk.Key.Right))
178        {
179            switch (event.keyval)
180            {
181            case Gdk.Key.Left:
182                history.current -= 1;
183                break;
184            case Gdk.Key.Right:
185                history.current += 1;
186                break;
187            }
188            HistoryEntry? entry = history.get_entry_at (history.current);
189            if (entry != null) {
190                equation.clear();
191                insert_text (entry.answer_label.get_text ());
192            }
193            return true;
194        }
195
196        /* Ignore keypresses while calculating */
197        if (equation.in_solve)
198            return true;
199
200        /* Treat keypad keys as numbers even when numlock is off */
201        uint new_keyval = 0;
202        switch (event.keyval)
203        {
204        case Gdk.Key.KP_Insert:
205            new_keyval = Gdk.Key.@0;
206            break;
207        case Gdk.Key.KP_End:
208            new_keyval = Gdk.Key.@1;
209            break;
210        case Gdk.Key.KP_Down:
211            new_keyval = Gdk.Key.@2;
212            break;
213        case Gdk.Key.KP_Page_Down:
214            new_keyval = Gdk.Key.@3;
215            break;
216        case Gdk.Key.KP_Left:
217            new_keyval = Gdk.Key.@4;
218            break;
219        case Gdk.Key.KP_Begin: /* This is apparently what "5" does when numlock is off. */
220            new_keyval = Gdk.Key.@5;
221            break;
222        case Gdk.Key.KP_Right:
223            new_keyval = Gdk.Key.@6;
224            break;
225        case Gdk.Key.KP_Home:
226            new_keyval = Gdk.Key.@7;
227            break;
228        case Gdk.Key.KP_Up:
229            new_keyval = Gdk.Key.@8;
230            break;
231        case Gdk.Key.KP_Page_Up:
232            new_keyval = Gdk.Key.@9;
233            break;
234        case Gdk.Key.KP_Delete:
235            new_keyval = Gdk.Key.period;
236            break;
237        }
238
239        if (new_keyval != 0)
240        {
241            var new_event = event; // FIXME: Does this copy?
242            new_event.keyval = new_keyval;
243            return key_press_event (new_event);
244        }
245
246        var c = Gdk.keyval_to_unicode (event.keyval);
247
248        /* Solve on [=] if the input is not a variable name */
249        if (event.keyval == Gdk.Key.equal || event.keyval == Gdk.Key.KP_Equal)
250        {
251            if (!(only_variable_name.match((string) equation.equation)
252                || only_function_definition.match((string) equation.equation)))
253            {
254                event.keyval = Gdk.Key.KP_Enter;
255            }
256        }
257
258        /* Solve on enter */
259        if (event.keyval == Gdk.Key.Return || event.keyval == Gdk.Key.KP_Enter)
260        {
261            if (completion_visible && completion_selected)
262                return false;
263            equation.solve ();
264            return true;
265        }
266
267        /* Numeric keypad will insert '.' or ',' depending on layout */
268        if ((event.keyval == Gdk.Key.KP_Decimal) ||
269            (event.keyval == Gdk.Key.KP_Separator) ||
270            (event.keyval == Gdk.Key.period) ||
271            (event.keyval == Gdk.Key.decimalpoint) ||
272            (event.keyval == Gdk.Key.comma))
273        {
274            equation.insert_numeric_point ();
275            return true;
276        }
277
278        /* Substitute */
279        if (state == 0)
280        {
281            if (c == '*')
282            {
283                equation.insert ("×");
284                return true;
285            }
286            if (c == '>')
287            {
288                equation.insert (">");
289                return true;
290            }
291            if (c == '<')
292            {
293                equation.insert ("<");
294                return true;
295            }
296            if (c == '/')
297            {
298                equation.insert ("÷");
299                return true;
300            }
301            if (c == '-')
302            {
303                equation.insert_subtract ();
304                return true;
305            }
306        }
307
308        /* Shortcuts */
309        if (state == Gdk.ModifierType.CONTROL_MASK)
310        {
311            switch (event.keyval)
312            {
313            case Gdk.Key.bracketleft:
314                equation.insert ("⌈");
315                return true;
316            case Gdk.Key.bracketright:
317                equation.insert ("⌉");
318                return true;
319            case Gdk.Key.e:
320                equation.insert_exponent ();
321                return true;
322            case Gdk.Key.f:
323                equation.factorize ();
324                return true;
325            case Gdk.Key.i:
326                equation.insert ("⁻¹");
327                return true;
328            case Gdk.Key.p:
329                equation.insert ("π");
330                return true;
331    	    case Gdk.Key.t:
332                equation.insert ("τ");
333                return true;
334            case Gdk.Key.r:
335                equation.insert ("√");
336                return true;
337            case Gdk.Key.o:
338                equation.insert("˚");
339                return true;
340            case Gdk.Key.u:
341                equation.insert ("µ");
342                return true;
343            case Gdk.Key.minus:
344                 equation.insert ("⁻");
345                 return true;
346            case Gdk.Key.apostrophe:
347                 equation.insert ("°");
348                 return true;
349            }
350        }
351        if (state == Gdk.ModifierType.MOD1_MASK)
352        {
353            switch (event.keyval)
354            {
355            case Gdk.Key.bracketleft:
356                equation.insert ("⌊");
357                return true;
358            case Gdk.Key.bracketright:
359                equation.insert ("⌋");
360                return true;
361            }
362        }
363
364        if (state == Gdk.ModifierType.CONTROL_MASK || equation.number_mode == NumberMode.SUPERSCRIPT)
365        {
366            if (!equation.has_selection)
367                equation.remove_trailing_spaces ();
368            switch (event.keyval)
369            {
370            case Gdk.Key.@0:
371            case Gdk.Key.KP_0:
372                equation.insert ("⁰");
373                return true;
374            case Gdk.Key.@1:
375            case Gdk.Key.KP_1:
376                equation.insert ("¹");
377                return true;
378            case Gdk.Key.@2:
379            case Gdk.Key.KP_2:
380                equation.insert ("²");
381                return true;
382            case Gdk.Key.@3:
383            case Gdk.Key.KP_3:
384                equation.insert ("³");
385                return true;
386            case Gdk.Key.@4:
387            case Gdk.Key.KP_4:
388                equation.insert ("⁴");
389                return true;
390            case Gdk.Key.@5:
391            case Gdk.Key.KP_5:
392                equation.insert ("⁵");
393                return true;
394            case Gdk.Key.@6:
395            case Gdk.Key.KP_6:
396                equation.insert ("⁶");
397                return true;
398            case Gdk.Key.@7:
399            case Gdk.Key.KP_7:
400                equation.insert ("⁷");
401                return true;
402            case Gdk.Key.@8:
403            case Gdk.Key.KP_8:
404                equation.insert ("⁸");
405                return true;
406            case Gdk.Key.@9:
407            case Gdk.Key.KP_9:
408                equation.insert ("⁹");
409                return true;
410            }
411        }
412        else if (state == Gdk.ModifierType.MOD1_MASK || equation.number_mode == NumberMode.SUBSCRIPT)
413        {
414            if (!equation.has_selection)
415                equation.remove_trailing_spaces ();
416            switch (event.keyval)
417            {
418            case Gdk.Key.@0:
419            case Gdk.Key.KP_0:
420                equation.insert ("₀");
421                return true;
422            case Gdk.Key.@1:
423            case Gdk.Key.KP_1:
424                equation.insert ("₁");
425                return true;
426            case Gdk.Key.@2:
427            case Gdk.Key.KP_2:
428                equation.insert ("₂");
429                return true;
430            case Gdk.Key.@3:
431            case Gdk.Key.KP_3:
432                equation.insert ("₃");
433                return true;
434            case Gdk.Key.@4:
435            case Gdk.Key.KP_4:
436                equation.insert ("₄");
437                return true;
438            case Gdk.Key.@5:
439            case Gdk.Key.KP_5:
440                equation.insert ("₅");
441                return true;
442            case Gdk.Key.@6:
443            case Gdk.Key.KP_6:
444                equation.insert ("₆");
445                return true;
446            case Gdk.Key.@7:
447            case Gdk.Key.KP_7:
448                equation.insert ("₇");
449                return true;
450            case Gdk.Key.@8:
451            case Gdk.Key.KP_8:
452                equation.insert ("₈");
453                return true;
454            case Gdk.Key.@9:
455            case Gdk.Key.KP_9:
456                equation.insert ("₉");
457                return true;
458            }
459        }
460
461        return false;
462    }
463
464    private void status_changed_cb ()
465    {
466        info_buffer.set_text (equation.status, -1);
467        if (equation.in_solve && !spinner.get_visible ())
468        {
469            spinner.show ();
470            spinner.start ();
471        }
472        else if (!equation.in_solve && spinner.get_visible ())
473        {
474            spinner.hide ();
475            spinner.stop ();
476        }
477    }
478
479    private void error_status_changed_cb ()
480    {
481        /* If both start and end location of error token are the same, no need to select anything. */
482        if (equation.error_token_end - equation.error_token_start == 0)
483            return;
484
485        Gtk.TextIter start, end;
486        equation.get_start_iter (out start);
487        equation.get_start_iter (out end);
488
489        start.set_offset ((int) equation.error_token_start);
490        end.set_offset ((int) equation.error_token_end);
491
492        equation.select_range (start, end);
493    }
494
495    public new void grab_focus ()
496    {
497        source_view.grab_focus ();
498    }
499}
500
501public class CompletionProvider : GLib.Object, Gtk.SourceCompletionProvider
502{
503    public virtual string get_name ()
504    {
505        return "";
506    }
507
508    public virtual Gtk.SourceCompletionItem create_proposal (string label, string text, string details)
509    {
510        var proposal = new Gtk.SourceCompletionItem ();
511        proposal.label = label;
512        proposal.text = text;
513        proposal.info = details;
514        return proposal;
515    }
516
517    public static void move_iter_to_name_start (ref Gtk.TextIter iter)
518    {
519        while (iter.backward_char ())
520        {
521            unichar current_char = iter.get_char ();
522            if (!current_char.isalpha ())
523            {
524                iter.forward_char ();
525                break;
526            }
527        }
528    }
529
530    public virtual bool get_start_iter (Gtk.SourceCompletionContext context, Gtk.SourceCompletionProposal proposal, out Gtk.TextIter iter)
531    {
532        iter = {};
533        return false;
534    }
535
536    public virtual bool activate_proposal (Gtk.SourceCompletionProposal proposal, Gtk.TextIter iter)
537    {
538        string proposed_string = proposal.get_text ();
539        Gtk.TextBuffer buffer = iter.get_buffer ();
540
541        Gtk.TextIter start_iter, end;
542        buffer.get_iter_at_offset (out start_iter, iter.get_offset ());
543        move_iter_to_name_start (ref start_iter);
544
545        buffer.place_cursor (start_iter);
546        buffer.delete_range (start_iter, iter);
547        buffer.insert_at_cursor (proposed_string, proposed_string.length);
548        if (proposed_string.contains ("()"))
549        {
550            buffer.get_iter_at_mark (out end, buffer.get_insert ());
551            end.backward_chars (1);
552            buffer.place_cursor (end);
553        }
554        return true;
555    }
556
557    public virtual void populate (Gtk.SourceCompletionContext context) {}
558}
559
560public class FunctionCompletionProvider : CompletionProvider
561{
562    public override string get_name ()
563    {
564        return _("Defined Functions");
565    }
566
567    public static MathFunction[] get_matches_for_completion_at_cursor (Gtk.TextBuffer text_buffer)
568    {
569        Gtk.TextIter start_iter, end_iter;
570        text_buffer.get_iter_at_mark (out end_iter, text_buffer.get_insert ());
571        text_buffer.get_iter_at_mark (out start_iter, text_buffer.get_insert ());
572        CompletionProvider.move_iter_to_name_start (ref start_iter);
573
574        string search_pattern = text_buffer.get_slice (start_iter, end_iter, false);
575
576        FunctionManager function_manager = FunctionManager.get_default_function_manager ();
577        MathFunction[] functions = function_manager.functions_eligible_for_autocompletion_for_text (search_pattern);
578        return functions;
579    }
580
581    public override void populate (Gtk.SourceCompletionContext context)
582    {
583        Gtk.TextIter iter1;
584        if (!context.get_iter (out iter1))
585            return;
586
587        Gtk.TextBuffer text_buffer = iter1.get_buffer ();
588        MathFunction[] functions = get_matches_for_completion_at_cursor (text_buffer);
589
590        List<Gtk.SourceCompletionItem>? proposals = null;
591        if (functions.length > 0)
592        {
593            proposals = new List<Gtk.SourceCompletionItem> ();
594            foreach (var function in functions)
595            {
596                string display_text = "%s(%s)".printf (function.name, string.joinv (";", function.arguments));
597                string details_text = "%s".printf (function.description);
598                string label_text = function.name + "()";
599                if (function.is_custom_function ())
600                    details_text = "%s(%s)=%s\n%s".printf (function.name, string.joinv (";", function.arguments),
601                                                           function.expression, function.description);
602
603                proposals.append (create_proposal (display_text, label_text, details_text));
604            }
605        }
606        context.add_proposals (this, proposals, true);
607    }
608}
609
610public class VariableCompletionProvider : CompletionProvider
611{
612    private MathEquation _equation;
613
614    public VariableCompletionProvider (MathEquation equation)
615    {
616        _equation = equation;
617    }
618
619    public override string get_name ()
620    {
621        return _("Defined Variables");
622    }
623
624    public static string[] get_matches_for_completion_at_cursor (Gtk.TextBuffer text_buffer, MathVariables variables )
625    {
626        Gtk.TextIter start_iter, end_iter;
627        text_buffer.get_iter_at_mark (out end_iter, text_buffer.get_insert ());
628        text_buffer.get_iter_at_mark (out start_iter, text_buffer.get_insert ());
629        CompletionProvider.move_iter_to_name_start (ref start_iter);
630
631        string search_pattern = text_buffer.get_slice (start_iter, end_iter, false);
632        string[] math_variables = variables.variables_eligible_for_autocompletion (search_pattern);
633        return math_variables;
634    }
635
636    public override void populate (Gtk.SourceCompletionContext context)
637    {
638        Gtk.TextIter iter1;
639        if (!context.get_iter (out iter1))
640            return;
641
642        Gtk.TextBuffer text_buffer = iter1.get_buffer ();
643        string[] variables = get_matches_for_completion_at_cursor (text_buffer, _equation.variables);
644
645        List<Gtk.SourceCompletionItem>? proposals = null;
646        if (variables.length > 0)
647        {
648            proposals = new List<Gtk.SourceCompletionItem> ();
649            foreach (var variable in variables)
650            {
651                string display_text = "%s".printf (variable);
652                string details_text = _equation.serializer.to_string (_equation.variables.get (variable));
653                string label_text = variable;
654
655                proposals.append (create_proposal (display_text, label_text, details_text));
656            }
657        }
658        context.add_proposals (this, proposals, true);
659    }
660}
661