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