#include #include "Actions.hh" #include "Functional.hh" #include "Cadabra.hh" #include "Config.hh" #include "InstallPrefix.hh" #include "NotebookWindow.hh" #include "DataCell.hh" #include #include #include #include #include #include #include #include #include #if GTKMM_MINOR_VERSION < 10 #include #endif #include "Snoop.hh" #include "ChooseColoursDialog.hh" #include "SelectFileDialog.hh" #include "process.hpp" #include using namespace cadabra; // #define DEBUG 1 NotebookWindow::NotebookWindow(Cadabra *c, bool ro) : DocumentThread(this) , current_cell(doc.end()) , cdbapp(c) , search_case_insensitive("Case insensitive", true) , console(sigc::mem_fun(this, &NotebookWindow::interactive_execute)) , current_canvas(0) , kernel_spinner_status(false) , progress_frac(0) , status_line(-1) , status_col(-1) , title_prefix("Cadabra: ") , modified(false) , read_only(ro) , crash_window_hidden(true) , last_configure_width(0) , follow_cell(doc.end()) , tex_running(false), tex_need_width(0) , last_find_location(doc.end(), std::string::npos) , is_configured(false) { // Connect the dispatcher. dispatcher.connect(sigc::mem_fun(*this, &NotebookWindow::process_todo_queue)); dispatch_refresh.connect(sigc::mem_fun(*this, &NotebookWindow::refresh_after_tex_engine_run)); dispatch_tex_error.connect(sigc::mem_fun(*this, &NotebookWindow::handle_thread_tex_error)); dispatch_update_status.connect(sigc::mem_fun(*this, &NotebookWindow::update_status)); // Set the window icon. set_icon_name("cadabra2-gtk"); // Query high-dpi settings. For all systems we can probe the // HiDPI scale, and for some window managers we also probe the // text scale factor. auto screen = Gdk::Screen::get_default(); scale = screen->get_monitor_scale_factor(0); display_scale = scale; #ifndef __APPLE__ const char *ds = std::getenv("DESKTOP_SESSION"); if(ds) { settings = Gio::Settings::create((strcmp(ds, "cinnamon") == 0) ? "org.cinnamon.desktop.interface" : "org.gnome.desktop.interface"); scale *= settings->get_double("text-scaling-factor"); } #endif engine.set_scale(scale, display_scale); #ifndef __APPLE__ if(ds) { settings->signal_changed().connect( sigc::mem_fun(*this, &NotebookWindow::on_text_scaling_factor_changed)); } #endif // Setup styling. Note that 'margin-left' and so on do not work; you need // to use 'padding'. However, 'padding-top' fails because it does not make the // widget larger enough... So we still do that with set_margin_top(...). css_provider = Gtk::CssProvider::create(); // padding-left: 20px; does not work on some versions of gtk, so we use margin in CodeInput // We use CSS selectors for old-style and new-style (post 3.20) simultaneously. // Run program with 'GTK_DEBUG=interactive' environment variable and press Ctrl-Shift-D // to inspect. load_css(); // auto screen = Gdk::Screen::get_default(); // std::cerr << "cadabra-client: scale = " << screen->get_monitor_scale_factor(0) << std::endl; Gtk::StyleContext::add_provider_for_screen(screen, css_provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); // Setup menu. actiongroup = Gtk::ActionGroup::create(); actiongroup->add( Gtk::Action::create("MenuFile", "_File") ); actiongroup->add( Gtk::Action::create("New", Gtk::Stock::NEW), Gtk::AccelKey("N"), sigc::mem_fun(*this, &NotebookWindow::on_file_new) ); actiongroup->add( Gtk::Action::create("Open", Gtk::Stock::OPEN), Gtk::AccelKey("O"), sigc::mem_fun(*this, &NotebookWindow::on_file_open) ); actiongroup->add( Gtk::Action::create("Close", Gtk::Stock::CLOSE), Gtk::AccelKey("W"), sigc::mem_fun(*this, &NotebookWindow::on_file_close) ); actiongroup->add( Gtk::Action::create("Save", Gtk::Stock::SAVE), Gtk::AccelKey("S"), sigc::mem_fun(*this, &NotebookWindow::on_file_save) ); actiongroup->add( Gtk::Action::create("SaveAs", Gtk::Stock::SAVE_AS), Gtk::AccelKey("S"), sigc::mem_fun(*this, &NotebookWindow::on_file_save_as) ); actiongroup->add( Gtk::Action::create("ExportAsJupyter", "Export as Jupyter notebook"), sigc::mem_fun(*this, &NotebookWindow::on_file_save_as_jupyter)); actiongroup->add( Gtk::Action::create("ExportHtml", "Export to standalone HTML"), sigc::mem_fun(*this, &NotebookWindow::on_file_export_html) ); actiongroup->add( Gtk::Action::create("ExportHtmlSegment", "Export to HTML segment"), sigc::mem_fun(*this, &NotebookWindow::on_file_export_html_segment) ); actiongroup->add( Gtk::Action::create("ExportLaTeX", "Export to standalone LaTeX"), sigc::mem_fun(*this, &NotebookWindow::on_file_export_latex) ); actiongroup->add( Gtk::Action::create("ExportPython", "Export to Python/Cadabra source"), sigc::mem_fun(*this, &NotebookWindow::on_file_export_python) ); actiongroup->add( Gtk::Action::create("Quit", Gtk::Stock::QUIT), sigc::mem_fun(*this, &NotebookWindow::on_file_quit) ); actiongroup->add( Gtk::Action::create("MenuEdit", "_Edit") ); actiongroup->add( Gtk::Action::create("EditUndo", Gtk::Stock::UNDO), Gtk::AccelKey("Z"), sigc::mem_fun(*this, &NotebookWindow::on_edit_undo) ); action_copy = Gtk::Action::create("EditCopy", Gtk::Stock::COPY); actiongroup->add( action_copy, Gtk::AccelKey("C"), sigc::mem_fun(*this, &NotebookWindow::on_edit_copy) ); action_copy->set_sensitive(false); actiongroup->add( Gtk::Action::create("EditPaste", Gtk::Stock::PASTE), Gtk::AccelKey("V"), sigc::mem_fun(*this, &NotebookWindow::on_edit_paste) ); actiongroup->add( Gtk::Action::create("EditInsertAbove", "Insert cell above"), Gtk::AccelKey("Up"), sigc::mem_fun(*this, &NotebookWindow::on_edit_insert_above) ); actiongroup->add( Gtk::Action::create("EditInsertBelow", "Insert cell below"), Gtk::AccelKey("Down"), sigc::mem_fun(*this, &NotebookWindow::on_edit_insert_below) ); actiongroup->add( Gtk::Action::create("EditDelete", "Delete cell"), Gtk::AccelKey("Delete"), sigc::mem_fun(*this, &NotebookWindow::on_edit_delete) ); actiongroup->add( Gtk::Action::create("EditSplit", "Split cell"), Gtk::AccelKey("Return"), sigc::mem_fun(*this, &NotebookWindow::on_edit_split) ); actiongroup->add( Gtk::Action::create("EditFind", Gtk::Stock::FIND), Gtk::AccelKey("F"), sigc::mem_fun(*this, &NotebookWindow::on_edit_find) ); actiongroup->add( Gtk::Action::create("EditFindNext", "Find next"), Gtk::AccelKey("G"), sigc::mem_fun(*this, &NotebookWindow::on_search_text_changed) ); actiongroup->add( Gtk::Action::create("EditMakeCellTeX", "Cell is LaTeX"), Gtk::AccelKey("L"), sigc::mem_fun(*this, &NotebookWindow::on_edit_cell_is_latex) ); actiongroup->add( Gtk::Action::create("EditMakeCellPython", "Cell is Cadabra/Python"), Gtk::AccelKey("P"), sigc::mem_fun(*this, &NotebookWindow::on_edit_cell_is_python) ); actiongroup->add( Gtk::Action::create("EditIgnoreCellOnImport", "Ignore cell on import"), Gtk::AccelKey("I"), sigc::mem_fun(*this, &NotebookWindow::on_ignore_cell_on_import) ); actiongroup->add( Gtk::Action::create("MenuView", "_View") ); actiongroup->add( Gtk::Action::create("ViewSplit", "Split view"), sigc::mem_fun(*this, &NotebookWindow::on_view_split) ); actiongroup->add( Gtk::Action::create("ViewClose", "Close view"), sigc::mem_fun(*this, &NotebookWindow::on_view_close) ); Gtk::RadioAction::Group group_cv; actiongroup->add(Gtk::Action::create("MenuConsoleVisibility", "Console")); auto cv_action_hide = Gtk::RadioAction::create(group_cv, "ConsoleHide", "Hide"); cv_action_hide->property_value() = (int)Console::Position::Hidden; actiongroup->add(cv_action_hide, sigc::bind(sigc::mem_fun(*this, &NotebookWindow::on_prefs_set_cv), Console::Position::Hidden)); cv_action_hide->set_active(); auto cv_action_dockh = Gtk::RadioAction::create(group_cv, "ConsoleDockH", "Dock (Horizontal)"); cv_action_dockh->property_value() = (int)Console::Position::DockedH; actiongroup->add(cv_action_dockh, sigc::bind(sigc::mem_fun(*this, &NotebookWindow::on_prefs_set_cv), Console::Position::DockedH)); auto cv_action_dockv = Gtk::RadioAction::create(group_cv, "ConsoleDockV", "Dock (Vertical)"); cv_action_dockv->property_value() = (int)Console::Position::DockedV; actiongroup->add(cv_action_dockv, sigc::bind(sigc::mem_fun(*this, &NotebookWindow::on_prefs_set_cv), Console::Position::DockedV)); auto cv_action_float = Gtk::RadioAction::create(group_cv, "ConsoleFloat", "Float"); cv_action_float->property_value() = (int)Console::Position::Floating; actiongroup->add(cv_action_float, sigc::bind(sigc::mem_fun(*this, &NotebookWindow::on_prefs_set_cv), Console::Position::Floating)); Gtk::RadioAction::Group group_font_size; actiongroup->add( Gtk::Action::create("MenuFontSize", "Font size") ); auto font_action0=Gtk::RadioAction::create(group_font_size, "FontSmall", "Small"); font_action0->property_value()=-1; actiongroup->add( font_action0, sigc::bind(sigc::mem_fun(*this, &NotebookWindow::on_prefs_font_size),-1 )); if(prefs.font_step==-1) font_action0->set_active(); auto font_action1=Gtk::RadioAction::create(group_font_size, "FontMedium", "Medium (default)"); font_action1->property_value()= 0; actiongroup->add( font_action1, sigc::bind(sigc::mem_fun(*this, &NotebookWindow::on_prefs_font_size), 0)); if(prefs.font_step==0) font_action1->set_active(); default_actions.push_back(font_action1); auto font_action2=Gtk::RadioAction::create(group_font_size, "FontLarge", "Large"); font_action2->property_value()= 2; actiongroup->add( font_action2, sigc::bind(sigc::mem_fun(*this, &NotebookWindow::on_prefs_font_size), 2)); if(prefs.font_step==2) font_action2->set_active(); auto font_action3=Gtk::RadioAction::create(group_font_size, "FontExtraLarge", "Extra large"); font_action3->property_value()= 4; actiongroup->add( font_action3, sigc::bind(sigc::mem_fun(*this, &NotebookWindow::on_prefs_font_size), 4)); if(prefs.font_step==4) font_action3->set_active(); Gtk::RadioAction::Group group_highlight_syntax; actiongroup->add(Gtk::Action::create("MenuHighlightSyntax", "Highlight Syntax")); auto highlight_syntax_action0 = Gtk::RadioAction::create(group_highlight_syntax, "HighlightSyntaxOff", "Off (default)"); highlight_syntax_action0->property_value() = 0; actiongroup->add(highlight_syntax_action0, sigc::bind(sigc::mem_fun(*this, &NotebookWindow::on_prefs_highlight_syntax), false)); if (prefs.highlight == false) highlight_syntax_action0->set_active(); default_actions.push_back(highlight_syntax_action0); auto highlight_syntax_action1 = Gtk::RadioAction::create(group_highlight_syntax, "HighlightSyntaxOn", "On"); highlight_syntax_action1->property_value() = 1; actiongroup->add(highlight_syntax_action1, sigc::bind(sigc::mem_fun(*this, &NotebookWindow::on_prefs_highlight_syntax), true)); if (prefs.highlight == true) highlight_syntax_action1->set_active(); actiongroup->add(Gtk::Action::create("HighlightSyntaxChoose", "Choose Colours..."), sigc::mem_fun(*this, &NotebookWindow::on_prefs_choose_colours)); actiongroup->add(Gtk::Action::create("ViewUseDefaultSettings", "Use Default Settings"), sigc::mem_fun(*this, &NotebookWindow::on_prefs_use_defaults)); actiongroup->add( Gtk::Action::create("MenuEvaluate", "_Evaluate") ); actiongroup->add( Gtk::Action::create("EvaluateCell", "Evaluate cell"), Gtk::AccelKey("Return"), sigc::mem_fun(*this, &NotebookWindow::on_run_cell) ); actiongroup->add( Gtk::Action::create("EvaluateAll", Gtk::Stock::GO_FORWARD, "Evaluate all"), sigc::mem_fun(*this, &NotebookWindow::on_run_runall) ); actiongroup->add( Gtk::Action::create("EvaluateToCursor", Gtk::Stock::GOTO_LAST, "Evaluate to cursor"), sigc::mem_fun(*this, &NotebookWindow::on_run_runtocursor) ); actiongroup->add( Gtk::Action::create("EvaluateStop", Gtk::Stock::STOP, "Stop"), Gtk::AccelKey('.', Gdk::MOD1_MASK), sigc::mem_fun(*this, &NotebookWindow::on_run_stop) ); actiongroup->add( Gtk::Action::create("MenuKernel", "_Kernel") ); actiongroup->add( Gtk::Action::create("KernelRestart", Gtk::Stock::REFRESH, "Restart"), Gtk::AccelKey("R"), sigc::mem_fun(*this, &NotebookWindow::on_kernel_restart) ); actiongroup->add(Gtk::Action::create("MenuTools", "Tools")); actiongroup->add(Gtk::Action::create("ToolsCompare", "Compare")); actiongroup->add(Gtk::Action::create("CompareFile", "Compare to file"), sigc::mem_fun(*this, &NotebookWindow::compare_to_file)); actiongroup->add(Gtk::Action::create("CompareGit", "Compare with Git")); actiongroup->add(Gtk::Action::create("CompareGitLatest", "Latest commit"), sigc::mem_fun(*this, &NotebookWindow::compare_git_latest)); actiongroup->add(Gtk::Action::create("CompareGitChoose", "Select commit from list"), sigc::mem_fun(*this, &NotebookWindow::compare_git_choose)); actiongroup->add(Gtk::Action::create("CompareGitSpecific", "Manually enter commit hash"), sigc::mem_fun(*this, &NotebookWindow::compare_git_specific)); actiongroup->add(Gtk::Action::create("CompareSelectGit", "Select Git Executable"), sigc::mem_fun(*this, &NotebookWindow::select_git_path)); actiongroup->add(Gtk::Action::create("ToolsOptions", "Options"), sigc::mem_fun(*this, &NotebookWindow::on_tools_options)); actiongroup->add(Gtk::Action::create("ToolsClearCache", "Clear Cache"), sigc::mem_fun(*this, &NotebookWindow::on_tools_clear_cache)); actiongroup->add( Gtk::Action::create("MenuHelp", "_Help") ); // actiongroup->add( Gtk::Action::create("HelpNotebook", Gtk::Stock::HELP, "How to use the notebook"), // sigc::mem_fun(*this, &NotebookWindow::on_help_notebook) ); actiongroup->add( Gtk::Action::create("HelpAbout", Gtk::Stock::ABOUT, "About Cadabra"), sigc::mem_fun(*this, &NotebookWindow::on_help_about) ); actiongroup->add( Gtk::Action::create("HelpContext", Gtk::Stock::HELP, "Contextual help"), sigc::mem_fun(*this, &NotebookWindow::on_help) ); menu_help_register = Gtk::Action::create("HelpRegister", "Register"); actiongroup->add(menu_help_register, sigc::mem_fun(*this, &NotebookWindow::on_help_register)); menu_help_register->set_sensitive(!prefs.is_registered); uimanager = Gtk::UIManager::create(); uimanager->insert_action_group(actiongroup); add_accel_group(uimanager->get_accel_group()); Glib::ustring ui_info = "" " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " "; if(!read_only) ui_info+= " " " " " " " " // " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " "; ui_info+= " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " // " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " ""; uimanager->add_ui_from_string(ui_info); uimanager->get_widget("/ToolBar/New")->set_tooltip_text("New notebook"); uimanager->get_widget("/ToolBar/Open")->set_tooltip_text("Open existing notebook"); uimanager->get_widget("/ToolBar/Save")->set_tooltip_text("Save changes"); uimanager->get_widget("/ToolBar/SaveAs")->set_tooltip_text("Save as new notebook"); uimanager->get_widget("/ToolBar/Undo")->set_tooltip_text("Undo"); uimanager->get_widget("/ToolBar/RunAll")->set_tooltip_text("Run all cells"); uimanager->get_widget("/ToolBar/EvaluateStop")->set_tooltip_text("Stop evaluation"); // Main box structure dividing the window. add(topbox); Gtk::Widget *menubar = uimanager->get_widget("/MenuBar"); topbox.pack_start(*menubar, Gtk::PACK_SHRINK); Gtk::Widget *toolbar = uimanager->get_widget("/ToolBar"); topbox.pack_start(*toolbar, Gtk::PACK_SHRINK); topbox.pack_start(supermainbox, true, true); topbox.pack_start(statusbarbox, false, false); supermainbox.pack_start(dragbox, true, true); dragbox.add1(mainbox); mainbox.pack_start(searchbar, false, false); searchbar.add(search_hbox); // searchbar.set_halign(Gtk::ALIGN_START); search_hbox.pack_start(searchentry, Gtk::PACK_EXPAND_WIDGET, 10); searchentry.set_size_request(200, -1); search_hbox.pack_start(search_case_insensitive, Gtk::PACK_SHRINK, 10); // search_hbox.pack_start(search_result, Gtk::PACK_EXPAND_WIDGET, 10); search_case_insensitive.set_active(true); // Status bar status_label.set_alignment( 0.0, 0.5 ); kernel_label.set_alignment( 0.0, 0.5 ); status_label.set_size_request(200,-1); status_label.set_justify(Gtk::JUSTIFY_LEFT); kernel_label.set_justify(Gtk::JUSTIFY_LEFT); kernel_label.set_text("Server: not connected"); statusbarbox.pack_start(status_label); statusbarbox.pack_start(kernel_label); statusbarbox.pack_start(kernel_spinner); // auto context = kernel_spinner.get_style_context(); // context->add_class("spinner"); statusbarbox.pack_start(progressbar); statusbarbox.set_name("statusbar"); progressbar.set_size_request(200,-1); progressbar.set_text("Idle"); progressbar.set_show_text(true); searchentry.signal_search_changed().connect(sigc::mem_fun(*this, &NotebookWindow::on_search_text_changed)); // The three main widgets // mainbox.pack_start(buttonbox, Gtk::PACK_SHRINK, 0); // We always have at least one canvas. canvasses.push_back(manage( new NotebookCanvas() )); mainbox.pack_start(*canvasses[0], Gtk::PACK_EXPAND_WIDGET, 0); // FIXME: need to do this for every canvas. canvasses[0]->scroll.signal_size_allocate().connect( sigc::mem_fun(*this, &NotebookWindow::on_scroll_size_allocate)); // canvasses[0]->scroll.get_vadjustment()->signal_value_changed().connect( // sigc::mem_fun(*this, &NotebookWindow::on_vscroll_changed)); canvasses[0]->scroll.get_vscrollbar()->signal_change_value().connect( sigc::mem_fun(*this, &NotebookWindow::on_vscroll_changed)); // canvasses[0]->ebox.signal_button_press_event().connect( // sigc::mem_fun(*this, &NotebookWindow::on_mouse_wheel)); canvasses[0]->scroll.signal_scroll_event().connect( sigc::mem_fun(*this, &NotebookWindow::on_scroll), false); // Window size and title, and ready to go. set_default_size(screen->get_width()/2, screen->get_height()*0.8); // FIXME: the subtraction for the margin and scrollbar made below // is estimated but should be computed. // engine.set_geometry(screen->get_width()/2 - 2*30); update_title(); show_all(); kernel_spinner.hide(); if(read_only) { statusbarbox.hide(); progressbar.hide(); toolbar->hide(); } else { // Buttons set_stop_sensitive(false); } new_document(); } NotebookWindow::~NotebookWindow() { } void NotebookWindow::load_css() { std::string text_colour = prefs.highlight ? "black" : "blue"; Glib::ustring data = ""; data += "textview text { color: "+text_colour+"; background-color: white; -GtkWidget-cursor-aspect-ratio: 0.2; }\n"; data += "GtkTextView { color: "+text_colour+"; background-color: white; -GtkWidget-cursor-aspect-ratio: 0.2; }\n"; data += "textview *:focus { background-color: #eee; }\n"; data += ".view text selection { color: #fff; background-color: #888; }\n"; data += "textview.error { background: transparent; -GtkWidget-cursor-aspect-ratio: 0.2; color: @theme_fg_color; }\n"; data += "#ImageView { transition-property: padding, background-color; transition-duration: 1s; }\n"; data += "#CodeInput { font-family: monospace; }\n"; data += "#Console { padding: 2px; }\n"; data += "spinner { background: none; opacity: 1; -gtk-icon-source: -gtk-icontheme(\"process-working-symbolic\"); }\n"; data += "spinner:checked { opacity: 1; animation: spin 1s linear infinite; }\n"; // Some of the css properties defined in gtk-cadabra.css are overridden, and so are included // here to force them to be used #ifdef _MSC_VER data += R"( .titlebar { margin: 0; padding: 0; box-shadow: inset 0 0 1px #ddd; background-image: none; background-color: #209020; color: white; } .titlebar:backdrop { background-color: #367d36; } .titlebar label:backdrop { color: white; } .titlebar .titlebutton { margin: 0px 0px 0px -8px; padding: 0px 8px; } dialog .titlebar, dialog .titlebar:backdrop { background-color: #666; } tooltip { background-color: transparent; } toolbar button { padding: 0px; margin-right: 5px; } )"; #endif // data += "scrolledwindow { kinetic-scrolling: false; }\n"; if(!css_provider->load_from_data(data)) { std::cerr << "Cannot parse internal CSS." << std::endl; throw std::logic_error("Failed to parse widget CSS information."); } } bool NotebookWindow::on_delete_event(GdkEventAny* event) { if (quit_safeguard(true)) { prefs.save(); return Gtk::Window::on_delete_event(event); } else return true; } void NotebookWindow::on_prefs_set_cv(Console::Position pos) { // Unparent from whatever we're currently a child of console.hide(); if (console.get_parent() == &dragbox) { dragbox.remove(console); } else if (console.get_parent() == console_win.get_content_area()) { console_win.get_content_area()->remove(console); console_win.hide(); } // Reparent onto something new if (pos == Console::Position::DockedH) { dragbox.set_orientation(Gtk::Orientation::ORIENTATION_HORIZONTAL); dragbox.set_position(dragbox.get_allocation().get_width() / 2); dragbox.add2(console); console.show(); } else if (pos == Console::Position::DockedV) { dragbox.set_orientation(Gtk::Orientation::ORIENTATION_VERTICAL); dragbox.set_position(dragbox.get_allocation().get_height() / 2); dragbox.add2(console); console.set_size_request(-1, 200); console.show(); } else if (pos == Console::Position::Floating) { console_win.set_transient_for(*this); console_win.set_resizable(true); console_win.set_default_size(900, 300); console_win.set_title("Interactive Console"); console_win.get_content_area()->add(console); console_win.signal_response().connect([this](int) { actiongroup->get_action("ConsoleHide")->activate(); }); console_win.show_all(); } } bool NotebookWindow::on_configure_event(GdkEventConfigure *cfg) { // std::cerr << "cadabra-client: on_configure_event " << cfg->width << " x " << cfg->height << std::endl; is_configured=true; bool ret=Gtk::Window::on_configure_event(cfg); if(cfg->width != last_configure_width) { std::lock_guard guard(tex_need_width_mutex); if(tex_running) { tex_need_width = cfg->width; } else { tex_need_width = cfg->width; // std::cerr << "Attempting to run" << std::endl; tex_run_async(); // std::cerr << "OK, running" << std::endl; } last_configure_width = cfg->width; } return ret; } void NotebookWindow::refresh_after_tex_engine_run() { for(unsigned int i=0; irefresh_all(); } void NotebookWindow::handle_thread_tex_error() { on_tex_error(tex_error_string, doc.end()); // std::cerr << "clicked away" << std::endl; tex_error_string=""; } bool NotebookWindow::on_unhandled_error(const std::exception& err) { Gtk::MessageDialog md(*this, err.what(), false, Gtk::MessageType::MESSAGE_ERROR, Gtk::ButtonsType::BUTTONS_OK, true); md.run(); return true; } void NotebookWindow::set_title_prefix(const std::string& pf) { title_prefix=pf; } void NotebookWindow::update_title() { if(name.size()>0) { if(modified) set_title(title_prefix+name+"*"); else set_title(title_prefix+name); } else { if(modified) set_title("Cadabra*"); else set_title("Cadabra"); } } void NotebookWindow::set_statusbar_message(const std::string& message, int line, int col) { { std::lock_guard guard(status_mutex); status_string = message; status_line = line; status_col = col; } dispatch_update_status.emit(); } void NotebookWindow::set_stop_sensitive(bool s) { Gtk::Widget *stop = uimanager->get_widget("/ToolBar/EvaluateStop"); stop->set_sensitive(s); stop = uimanager->get_widget("/MenuBar/MenuEvaluate/EvaluateStop"); stop->set_sensitive(s); } void NotebookWindow::process_data() { dispatcher.emit(); } void NotebookWindow::on_connect() { std::lock_guard guard(status_mutex); kernel_string = "connected"; progress_string = "Idle"; dispatcher.emit(); dispatch_update_status.emit(); console.initialize(); // prefs.python_path might end in a backslash which will raise an EOF syntax error, so we add a // semicolon to the end of it and then remove the (empty) last element of the resulting list if (!trim(prefs.python_path).empty()) console.send_input("sys.path = r'''" + escape_backslashes(prefs.python_path) + ";'''.split(';')[:-1] + sys.path"); if (!name.empty()) { console.send_input("sys.path.insert(0, '''" + escape_backslashes(name.substr(0, name.find_last_of("\\/"))) + "''')"); } } void NotebookWindow::on_disconnect(const std::string& reason) { std::lock_guard guard(status_mutex); kernel_string = reason; dispatcher.emit(); } void NotebookWindow::on_network_error() { std::lock_guard guard(status_mutex); kernel_string = "cannot reach server, retrying..."; dispatcher.emit(); } void NotebookWindow::on_kernel_runstatus(bool running) { std::lock_guard guard(status_mutex); kernel_spinner_status=running; dispatcher.emit(); } void NotebookWindow::tex_run_async() { if(tex_error_string!="") return; // Run TeX on a separate thread; it'll take a while and // we don't want to block the UI thread. // FIXME: join properly when exiting the program, otherwise // we get a 'terminate called without active exception' message. auto tex_code = [this]() { try { for(;;) { { std::lock_guard guard(tex_need_width_mutex); engine.invalidate_all(); engine.set_geometry(tex_need_width-2*30); } engine.convert_all(); dispatch_refresh.emit(); { std::lock_guard guard(tex_need_width_mutex); if(tex_need_width-2*30 == engine.get_geometry()) { tex_running=false; break; } } } } catch(TeXEngine::TeXException& ex) { // Display the error in the main thread. std::lock_guard guard(tex_need_width_mutex); tex_error_string=ex.what(); tex_running=false; dispatch_refresh.emit(); dispatch_tex_error.emit(); } }; std::lock_guard guard(tex_need_width_mutex); tex_running=true; // Join any thread which may still exist. if(tex_thread) tex_thread->join(); tex_thread = std::make_unique( tex_code ); } void NotebookWindow::process_todo_queue() { static bool running=false; // Prevent from re-entering this from the process_action_queue entered below. if(running) return; running=true; // Update the status/kernel messages into the corresponding widgets. { std::lock_guard guard(status_mutex); kernel_label.set_text("Server: " + kernel_string); if(kernel_spinner_status) { kernel_spinner.show(); kernel_spinner.start(); } else { kernel_spinner.stop(); kernel_spinner.hide(); } } // Perform any ActionBase actions. process_action_queue(); // Before we pop up any dialogs, enable queue processing again, otherwise // subsequent calls to process_todo_queue will get postponed until a // dispatcher.emit() is called when the dialog is closed. running=false; if(crash_window_hidden && kernel_string=="not connected") { crash_window_hidden=false; Gtk::MessageDialog md("Kernel crashed", false, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_OK, true); md.set_transient_for(*this); md.set_type_hint(Gdk::WINDOW_TYPE_HINT_DIALOG); md.set_secondary_text("The kernel crashed unexpectedly, and has been restarted. You will need to re-run all cells."); md.signal_response().connect(sigc::mem_fun(*this, &NotebookWindow::on_crash_window_closed)); md.run(); nlohmann::json req; req["msg_type"] = "exit"; req["header"]["from_server"] = true; on_interactive_output(req); } } void NotebookWindow::on_crash_window_closed(int) { crash_window_hidden=true; } bool NotebookWindow::on_key_press_event(GdkEventKey* event) { bool is_ctrl_up = event->keyval==GDK_KEY_Up && (event->state&Gdk::CONTROL_MASK); bool is_ctrl_down = event->keyval==GDK_KEY_Down && (event->state&Gdk::CONTROL_MASK); bool is_ctrl_home = event->keyval==GDK_KEY_Home && (event->state&Gdk::CONTROL_MASK); bool is_ctrl_end = event->keyval==GDK_KEY_End && (event->state&Gdk::CONTROL_MASK); bool is_pagedown = event->keyval==GDK_KEY_Page_Down; bool is_pageup = event->keyval==GDK_KEY_Page_Up; bool have_current_cell = current_cell != doc.end(); if(is_ctrl_up && have_current_cell) { std::shared_ptr actionpos = std::make_shared(current_cell->id(), ActionPositionCursor::Position::previous); queue_action(actionpos); process_todo_queue(); return true; } else if(is_ctrl_down && have_current_cell) { std::shared_ptr actionpos = std::make_shared(current_cell->id(), ActionPositionCursor::Position::next); queue_action(actionpos); process_todo_queue(); return true; } else if(is_ctrl_home) { // std::shared_ptr actionpos = // std::make_shared(current_cell->id(), ActionPositionCursor::Position::in); // queue_action(actionpos); // process_todo_queue(); Glib::RefPtr va=canvasses[current_canvas]->scroll.get_vadjustment(); va->set_value( va->get_lower() ); return true; } else if(is_ctrl_end) { // std::shared_ptr actionpos = // std::make_shared(current_cell->id(), ActionPositionCursor::Position::in); // queue_action(actionpos); // process_todo_queue(); Glib::RefPtr va=canvasses[current_canvas]->scroll.get_vadjustment(); va->set_value( va->get_upper() ); return true; } else if(is_pageup) { Glib::RefPtr va=canvasses[current_canvas]->scroll.get_vadjustment(); va->set_value( va->get_value()-va->get_page_increment() ); return true; } else if(is_pagedown) { Glib::RefPtr va=canvasses[current_canvas]->scroll.get_vadjustment(); va->set_value( va->get_value()+va->get_page_increment() ); return true; } else { return Gtk::Window::on_key_press_event(event); } } void NotebookWindow::add_cell(const DTree& tr, DTree::iterator it, bool visible) { // std::cerr << "Add cell for " << it->id().id << std::endl; // Add a visual cell corresponding to this document cell in // every canvas. if(compute!=0) set_stop_sensitive( compute->number_of_cells_executing()>0 ); Glib::RefPtr global_buffer; std::shared_ptr global_texrequest; for(unsigned int i=0; ivisualcells.find(&(*it))!=canvasses[i]->visualcells.end()) { #ifdef DEBUG std::cerr << "found a visualcell for cell " << &(*it) << " in canvas " << i << std::endl; #endif if(i==0 && it->cell_type==DataCell::CellType::python) { global_buffer = canvasses[i]->visualcells[&(*it)].inbox->buffer; } continue; } // Create a visual cell of the appropriate type. VisualCell newcell; Gtk::Widget *w=0; switch(it->cell_type) { case DataCell::CellType::document: newcell.document = manage( new Gtk::VBox() ); w=newcell.document; break; case DataCell::CellType::output: case DataCell::CellType::error: case DataCell::CellType::verbatim: // std::cerr << "creating verbatim cell for " << it->textbuf << std::endl; case DataCell::CellType::latex_view: { // FIXME: would be good to share the input and output of TeXView too. // Right now nothing is shared... newcell.outbox = manage( new TeXView(engine, it) ); // std::cerr << "Add widget " << newcell.outbox << " for cell " << it->id().id << std::endl; newcell.outbox->tex_error.connect( sigc::bind( sigc::mem_fun(this, &NotebookWindow::on_tex_error), it ) ); newcell.outbox->show_hide_requested.connect( sigc::bind( sigc::mem_fun(this, &NotebookWindow::cell_toggle_visibility), i ) ); #if GTKMM_MINOR_VERSION>=10 to_reveal.push_back(&newcell.outbox->rbox); #endif w=newcell.outbox; newcell.outbox->signal_button_press_event().connect( sigc::bind( sigc::mem_fun(this, &NotebookWindow::handle_outbox_select), it ) ); break; } case DataCell::CellType::python: case DataCell::CellType::latex: { CodeInput *ci; // Ensure that all CodeInput cells share the same text buffer. if(i==0) { ci = new CodeInput(it, it->textbuf,scale/display_scale,prefs); global_buffer=ci->buffer; } else ci = new CodeInput(it, global_buffer,scale/display_scale,prefs); using namespace std::placeholders; ci->relay_cursor_pos(std::bind(&NotebookWindow::set_statusbar_message, this, "", _1, _2)); if(read_only) ci->edit.set_editable(false); ci->get_style_context()->add_provider(css_provider, GTK_STYLE_PROVIDER_PRIORITY_USER); ci->edit.content_changed.connect( sigc::bind( sigc::mem_fun(this, &NotebookWindow::cell_content_changed), i ) ); ci->edit.content_insert.connect( sigc::bind( sigc::mem_fun(this, &NotebookWindow::cell_content_insert), i ) ); ci->edit.content_erase.connect( sigc::bind( sigc::mem_fun(this, &NotebookWindow::cell_content_erase), i ) ); ci->edit.content_execute.connect( sigc::bind( sigc::mem_fun(this, &NotebookWindow::cell_content_execute), i, true ) ); ci->edit.complete_request.connect( sigc::bind( sigc::mem_fun(this, &NotebookWindow::cell_complete_request), i ) ); ci->edit.cell_got_focus.connect( sigc::bind( sigc::mem_fun(this, &NotebookWindow::cell_got_focus), i ) ); newcell.inbox = manage( ci ); w=newcell.inbox; break; } case DataCell::CellType::image_png: { // FIXME: horribly memory inefficient ImageView *iv=new ImageView(); iv->set_image_from_base64(it->textbuf); newcell.imagebox = manage( iv ); w=newcell.imagebox; break; } case DataCell::CellType::input_form: // This cell is there only for cutnpaste functionality; do not display. break; default: throw std::logic_error("Unimplemented datacell type"); } if(w!=0) { canvasses[i]->visualcells[&(*it)]=newcell; // Document cells are easy; just add. They have no parent in the DTree. if(it->cell_type==DataCell::CellType::document) { canvasses[i]->scroll.add(*w); w->show_all(); // FIXME: if you drop this, the whole document remains invisible continue; } // Figure out where to store this new VisualCell in the GUI widget // tree by exploring the DTree near the new DataCell. // First determine the parent cell and the corresponding Gtk::Box // so that we can determine where to pack_start this cell. At this // stage, all cells have parents. DTree::iterator parent = DTree::parent(it); assert(tr.is_valid(parent)); VisualCell& parent_visual = canvasses[i]->visualcells[&(*parent)]; Gtk::VBox *parentbox=0; int offset=0; if(parent->cell_type==DataCell::CellType::document) parentbox=parent_visual.document; else { // FIXME: Since we are adding children of input cells to the vbox in which // the exp_input_tv widget is the 0th cell, we have to offset. Would be // cleaner to have a separate 'children' vbox in CodeInput (or in fact // every widget that can potentially contain children). offset=1; parentbox=parent_visual.inbox; } // std::cout << "adding cell to canvas " << i << std::endl; parentbox->pack_start(*w, false, false); unsigned int index =tr.index(it)+offset; unsigned int index_gui=parentbox->get_children().size()-1; // std::cout << "is index " << index << " vs " << index_gui << std::endl; if(index!=index_gui) { // std::cout << "need to re-order" << std::endl; parentbox->reorder_child(*w, index); } if(visible) { w->show_all(); w->show_now(); } } } // Connect Glib::signal_idle().connect(sigc::mem_fun(*this, &NotebookWindow::idle_handler)); // if(current_cell!=doc.end()) // setup_focus_after_allocate(it); } void NotebookWindow::on_interactive_output(const nlohmann::json& msg) { console.signal_message(msg); } void NotebookWindow::set_progress(const std::string& msg, int cur_step, int total_steps) { { std::lock_guard guard(status_mutex); if (total_steps == 0) { progress_string = msg; progress_frac = 0.0; } else { progress_frac = (double)cur_step / total_steps; progress_string = msg + " (" + std::to_string(cur_step) + "/" + std::to_string(total_steps) + ")"; } } dispatch_update_status.emit(); } void NotebookWindow::update_status() { // This should only be called from dispatch_update_status! // Update status and progress, kernel status is taken card of in process_action_queue std::lock_guard guard(status_mutex); progressbar.set_text(progress_string); progressbar.set_fraction(progress_frac); if (status_line < 0 || status_col < 0) { status_label.set_text(status_string); } else { std::string pos = " Line: " + std::to_string(status_line) + " Col: " + std::to_string(status_col); if (status_string == "") status_label.set_text(pos); else status_label.set_text(pos + " | " + status_string); } } void NotebookWindow::remove_cell(const DTree& doc, DTree::iterator it) { // Remember: this member should only remove the visual cell; the // document tree will be updated by the ActionRemove that led to this // member being called. However, we should ensure that any references // to the visual cell are removed as well; in particular, if // current_cell is pointing to this cell, we need to unset it. // Can only remove cells which have a parent (i.e. not the // top-level document cell). if(current_cell==it) current_cell=doc.end(); // Ensure we will not continue searching in this cell. if(last_find_location.first==it) { last_find_location.first=doc.end(); last_find_location.second=std::string::npos; } DTree::iterator parent = DTree::parent(it); assert(doc.is_valid(parent)); for(unsigned int i=0; ivisualcells[&(*parent)]; Gtk::VBox *parentbox=0; if(it->cell_type==DataCell::CellType::document) parentbox=parent_visual.document; else parentbox=parent_visual.inbox; auto fnd = canvasses[i]->visualcells.find(&(*it)); // It is possible that there is no visual cell corresponding to this // DTree cell (happens for instance when deleting a cell, undoing, // then re-evaluating: the DTree still has the output cells, while // they have gone from the visual tree). if(fnd==canvasses[i]->visualcells.end()) { std::cerr << "No visual cell for " << it->id().id << std::endl; continue; } VisualCell& actual = fnd->second;//canvasses[i]->visualcells[&(*it)]; // The pointers are all in a union, and Gtkmm does not care // about the precise type, so we just remove imagebox, knowing // that it may actually be an inbox or outbox. // std::cerr << "Removing " << actual.imagebox << std::endl; parentbox->remove(*actual.imagebox); // The above does not delete the Gtk widget, despite having been // wrapped in manage at construction. So we have to delete it // ourselves. Fortunately the container does not try to delete // it again in its destructor. delete actual.imagebox; // The above removes the entire Gtk tree corresponding to the cell at // 'it' and the subtree below it. In order to remove all these from // the visualcells map, we need to walk the tree explicitly. do_subtree(doc, it, [this, i](DTree::iterator rm) { #ifdef DEBUG std::cerr << "removing cell " << &(*rm) << " " << rm->textbuf << "\n from canvas " << i << std::endl; #endif auto fnd = canvasses[i]->visualcells.find(&(*rm)); if(fnd!=canvasses[i]->visualcells.end()) { canvasses[i]->visualcells.erase(fnd); } else { #ifdef DEBUG std::cerr << "no visualcell for that one" << std::endl; #endif } return rm; }); } modified=true; update_title(); } void NotebookWindow::remove_all_cells() { // Simply removing the document cell should do the trick. for(unsigned int i=0; iscroll.remove(); canvasses[i]->visualcells.clear(); } engine.checkout_all(); } void NotebookWindow::update_cell(const DTree&, DTree::iterator it) { // We just do a redraw for now, but this may require more work later. disable_stacks=true; for(unsigned int i=0; ivisualcells[&(*it)]; if(it->cell_type==DataCell::CellType::python || it->cell_type==DataCell::CellType::latex) { vc.inbox->update_buffer(); vc.inbox->queue_draw(); } } disable_stacks=false; } void NotebookWindow::position_cursor(const DTree&, DTree::iterator it, int pos) { // if(it==doc.end()) return; // std::cerr << "cadabra-client: positioning cursor at cell " << it->textbuf << std::endl; set_stop_sensitive( compute->number_of_cells_executing()>0 ); if(canvasses[current_canvas]->visualcells.find(&(*it))==canvasses[current_canvas]->visualcells.end()) { std::cerr << "cadabra-client: Cannot find cell to position cursor." << std::endl; return; } VisualCell& target = canvasses[current_canvas]->visualcells[&(*it)]; // Gtk::Allocation alloc=target.inbox->get_allocation(); target.inbox->edit.grab_focus(); if(pos>=0) { auto cursor=target.inbox->edit.get_buffer()->begin(); cursor.forward_chars(pos); target.inbox->edit.get_buffer()->place_cursor(cursor); } current_cell=it; } void NotebookWindow::select_range(const DTree&, DTree::iterator it, int start, int len) { if(it->cell_type!=DataCell::CellType::python && it->cell_type!=DataCell::CellType::latex) { std::cerr << "Warning: select_range called on cell which is not python or latex." << std::endl; return; } if(canvasses[current_canvas]->visualcells.find(&(*it))==canvasses[current_canvas]->visualcells.end()) { std::cerr << "cadabra-client: Cannot find cell to select range." << std::endl; return; } VisualCell& target = canvasses[current_canvas]->visualcells[&(*it)]; auto start_it=target.inbox->edit.get_buffer()->begin(); start_it.forward_chars(start); auto end_it=start_it; end_it.forward_chars(len); // std::cerr << start << ", " << len << std::endl; target.inbox->edit.get_buffer()->select_range(start_it, end_it); } size_t NotebookWindow::get_cursor_position(const DTree&, DTree::iterator it) { if(canvasses[current_canvas]->visualcells.find(&(*it))==canvasses[current_canvas]->visualcells.end()) { std::cerr << "cadabra-client: Cannot find cell to retrieve cursor position for." << std::endl; return -1; } VisualCell& target = canvasses[current_canvas]->visualcells[&(*it)]; size_t offset = target.inbox->buffer->get_insert()->get_iter().get_offset(); return offset; } void NotebookWindow::scroll_current_cell_into_view() { if(current_cell==doc.end()) return; scroll_cell_into_view(current_cell); } void NotebookWindow::scroll_cell_into_view(DTree::iterator cell) { // std::cerr << "-----" << std::endl; // std::cerr << "cell content to show: " << cell->textbuf << std::endl; if(current_canvas>=(int)canvasses.size()) return; if(canvasses[current_canvas]->visualcells.find(&(*cell))==canvasses[current_canvas]->visualcells.end()) return; const VisualCell& focusbox = canvasses[current_canvas]->visualcells[&(*cell)]; if(focusbox.inbox==0) return; Gtk::Allocation al; if(cell->cell_type==DataCell::CellType::python || cell->cell_type==DataCell::CellType::latex) al=focusbox.inbox->edit.get_allocation(); else if(cell->cell_type==DataCell::CellType::latex_view || cell->cell_type==DataCell::CellType::output || cell->cell_type==DataCell::CellType::verbatim) al=focusbox.outbox->get_allocation(); else if(cell->cell_type==DataCell::CellType::image_png) al=focusbox.imagebox->get_allocation(); else return; Glib::RefPtr va=canvasses[current_canvas]->scroll.get_vadjustment(); double upper_visible=va->get_value(); double page_size =va->get_page_size(); double lower_visible=va->get_value()+page_size; // When we get called, the busybox has its allocation done and size set. However, // the edit box below still has its old position (but its correct height). So we // should make sure that busybox.y+busybox.height+editbox.height is at the bottom // of the scrollbox. // std::cerr << "viewport = " << upper_visible << " - " << lower_visible << std::endl; // std::cerr << "cell to show = " << al.get_y() << " height " << al.get_height() << std::endl; double should_be_visible = al.get_y()+al.get_height()+10; double shift = should_be_visible - lower_visible; // std::cerr << "position " << should_be_visible << " should be visible" << std::endl; // std::cerr << "shift = " << shift << std::endl; if(shift>0 || (-shift)>va->get_page_size()) { va->set_value( upper_visible + shift); } } bool NotebookWindow::on_vscroll_changed(Gtk::ScrollType, double ) { // std::cerr << "vscroll changed " << std::endl; // FIXME: does not catch scroll wheel events. follow_cell=doc.end(); return false; } //bool NotebookWindow::on_mouse_wheel(GdkEventButton *b) // { // if(b->button==2) // follow_cell=doc.end(); // return false; // } bool NotebookWindow::on_scroll(GdkEventScroll *) { // We get here when the user rolls the scroll wheel inside the canvas; // need to disable auto-follow. follow_cell=doc.end(); return false; } void NotebookWindow::on_scroll_size_allocate(Gtk::Allocation& ) { // The auto-scroll logic is as follows. Whenever a cell is ran (by // user pressing shift-enter only, not by full run), we set to // auto-scroll as soon as output for that cell appears. If multiple // cells are sent to the queue, we follow output for the last one // sent. Any scrollbar event stops auto-scrolling. Perhaps only // auto-restart when scrolling to the bottom of the notebook, // though that is not of much extra use. Could have a 'follow // current' mode when running an entire notebook, which is again // stopped by scrollbar event. if(follow_cell!=doc.end()) { // std::cerr << " scroll" << std::endl; scroll_current_cell_into_view(); } } bool NotebookWindow::cell_toggle_visibility(DTree::iterator it, int ) { // Find the parent node. If that one is a latex cell, toggle visibility of // the CodeInput widget (but not anything else in its vbox). auto parent=DTree::parent(it); if(parent->cell_type==DataCell::CellType::latex) { // FIXME: we are not allowed to do this directly, all should go through // actions. parent->hidden = !parent->hidden; for(unsigned int i=0; ivisualcells.find(&(*parent)); if(vis==canvasses[i]->visualcells.end()) { throw std::logic_error("Cannot find visual cell."); } else { if(parent->hidden) { (*vis).second.inbox->edit.hide(); } else (*vis).second.inbox->edit.show(); } } } return false; } bool NotebookWindow::cell_content_changed(DTree::iterator, int /* i */) { if(disable_stacks) return false; modified=true; unselect_output_cell(); update_title(); return false; } bool NotebookWindow::cell_content_insert(const std::string& content, int pos, DTree::iterator it, int) { if(disable_stacks) return false; unselect_output_cell(); //std::cerr << "cell_content_insert" << std::endl; std::shared_ptr action = std::make_shared(it->id(), pos, content); queue_action(action); process_todo_queue(); return false; } bool NotebookWindow::cell_content_erase(int start, int end, DTree::iterator it, int ) { if(disable_stacks) return false; unselect_output_cell(); //std::cerr << "cell_content_erase" << std::endl; std::shared_ptr action = std::make_shared(it->id(), start, end); queue_action(action); process_todo_queue(); return false; } void NotebookWindow::dim_output_cells(DTree::iterator it) { // Dim the corresponding output cell, if any. auto ch=doc.begin(it); while(ch!=doc.end(it)) { if(ch->cell_type==DataCell::CellType::latex_view) { for(unsigned int i=0; ivisualcells.find(&(*ch)); if(vc!=canvasses[i]->visualcells.end()) vc->second.outbox->dim(true); } } ++ch; } } bool NotebookWindow::cell_got_focus(DTree::iterator it, int canvas_number) { current_cell=it; current_canvas=canvas_number; unselect_output_cell(); // cell_got_focus is an input cell, so output cells should not be selected anymore. return false; } void NotebookWindow::interactive_execute() { uint64_t id = 0; std::string input = console.grab_input(id); compute->execute_interactive(id, input); } bool NotebookWindow::cell_complete_request(DTree::iterator it, int pos, int canvas_number) { if (!prefs.tab_completion) return false; int cnum=0; if(undo_stack.size()>0) { auto cmp = std::dynamic_pointer_cast(undo_stack.top()); if(cmp) { // std::cerr << "Undoing last completion" << std::endl; cmp->revert(*this, *gui); pos-=cmp->length(); cnum=cmp->alternative()+1; undo_stack.pop(); } } return compute->complete(it, pos, cnum); } bool NotebookWindow::cell_content_execute(DTree::iterator it, int canvas_number, bool ) { // This callback runs on the GUI thread. The cell pointed to by 'it' is // guaranteed to be valid. // First ensure that this cell is not already running, otherwise all hell // will break loose when we try to double-remove the existing output cell etc. if(it->running) { return true; } // Ensure this cell is not empty either. if(it->textbuf.size()==0) return true; current_canvas=canvas_number; // Remove child nodes, if any. // FIXME: Does it make more sense to do this only after the // execution result comes back from the server? DTree::sibling_iterator sib=doc.begin(it); dim_output_cells(it); while(sib!=doc.end(it)) { // std::cout << "cadabra-client: scheduling output cell for removal: " << sib->id().id << std::endl; std::shared_ptr action = std::make_shared(sib->id()); queue_action(action); ++sib; } // Execute the cell. set_stop_sensitive(true); follow_cell=it; // std::cerr << "Executing cell " << it->id().id << std::endl; compute->execute_cell(it); return true; } bool NotebookWindow::on_tex_error(const std::string& str, DTree::iterator ) { // Gtk::Dialog md; Gtk::MessageDialog md("Generic TeX error", false, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_OK, true); md.set_resizable(true); // Gtk::Button ok(Gtk::Stock::OK); md.set_transient_for(*this); md.set_type_hint(Gdk::WINDOW_TYPE_HINT_DIALOG); auto box = md.get_message_area(); // md.add_button(Gtk::Stock::OK, 1); Gtk::ScrolledWindow sw; Gtk::TextView tv; auto buffer = tv.get_buffer(); buffer->set_text(str); // auto iter = buffer->get_iter_at_offset(0); // buffer->insert(iter, str); tv.set_editable(false); box->add(sw); sw.add(tv); auto context = tv.get_style_context(); context->add_class("error"); auto screen = Gdk::Screen::get_default(); sw.set_size_request(screen->get_width()/4, screen->get_width()/4); sw.show_all(); md.run(); return true; } void NotebookWindow::on_file_new() { if(quit_safeguard(false)) { doc.clear(); remove_all_cells(); new_document(); compute->restart_kernel(); position_cursor(doc, doc.begin(doc.begin()), -1); name=""; update_title(); } } void NotebookWindow::on_file_close() { if(quit_safeguard(true)) hide(); } Glib::RefPtr create_filter(const Glib::ustring& name, const Glib::ustring& pattern) { auto filter = Gtk::FileFilter::create(); filter->set_name(name); filter->add_pattern(pattern); return filter; } void NotebookWindow::on_file_open() { if(quit_safeguard(false)==false) return; Gtk::FileChooserDialog dialog("Please choose a Cadabra notebook (.cnb file) to open", Gtk::FILE_CHOOSER_ACTION_OPEN); dialog.set_transient_for(*this); dialog.add_filter(create_filter("Cadabra notebooks (*.cnb)", "*.cnb")); dialog.add_filter(create_filter("All files", "*")); dialog.add_button("_Cancel", Gtk::RESPONSE_CANCEL); dialog.add_button("Select", Gtk::RESPONSE_OK); int result=dialog.run(); switch(result) { case(Gtk::RESPONSE_OK): { name = dialog.get_filename(); snoop::log("open") << "menu" << snoop::flush; std::ifstream file(name); std::string content, line; while(std::getline(file, line)) content+=line; compute->restart_kernel(); load_file(content); break; } } } void NotebookWindow::set_name(const std::string& n) { name=n; update_title(); } void NotebookWindow::load_file(const std::string& notebook_contents) { load_from_string(notebook_contents); mainbox.show_all(); modified=false; update_title(); } void NotebookWindow::on_file_save() { // check if name known, otherwise call save_as if(name.size()>0) { std::string res=save(name); if(res.size()>0) { Gtk::MessageDialog md("Error saving notebook "+name); md.set_transient_for(*this); md.set_secondary_text(res); md.set_type_hint(Gdk::WINDOW_TYPE_HINT_DIALOG); md.run(); } else { modified=false; update_title(); } } else on_file_save_as(); } void NotebookWindow::on_file_save_as() { Gtk::FileChooserDialog dialog("Please choose a file name to save this notebook", Gtk::FILE_CHOOSER_ACTION_SAVE); dialog.set_do_overwrite_confirmation(true); dialog.set_transient_for(*this); dialog.add_button("_Cancel", Gtk::RESPONSE_CANCEL); dialog.add_button("Select", Gtk::RESPONSE_OK); int result=dialog.run(); switch(result) { case(Gtk::RESPONSE_OK): { std::string old_name = name; name = dialog.get_filename(); std::string res=save(name); if(res.size()>0) { Gtk::MessageDialog md("Error saving notebook "+name); md.set_transient_for(*this); md.set_secondary_text(res); md.set_type_hint(Gdk::WINDOW_TYPE_HINT_DIALOG); md.run(); } else { modified=false; update_title(); console.send_input("sys.path.insert(0, r'''" + escape_backslashes(name.substr(0, name.find_last_of("\\/"))) + "''')"); } break; } } } void NotebookWindow::on_file_save_as_jupyter() { Gtk::FileChooserDialog dialog("Please choose a file name to export as Jupyter notebook", Gtk::FILE_CHOOSER_ACTION_SAVE); dialog.set_do_overwrite_confirmation(true); dialog.set_transient_for(*this); dialog.add_button("_Cancel", Gtk::RESPONSE_CANCEL); dialog.add_button("Select", Gtk::RESPONSE_OK); int result=dialog.run(); switch(result) { case(Gtk::RESPONSE_OK): { std::string ipynb_name = dialog.get_filename(); std::string out = JSON_serialise(doc); auto json=nlohmann::json::parse(out); auto ipynb = cnb2ipynb(json); std::ofstream file(ipynb_name); file << ipynb.dump(3) << std::endl; // if(res.size()>0) { // Gtk::MessageDialog md("Error saving Jupyter notebook "+name); // md.set_transient_for(*this); // md.set_secondary_text(res); // md.set_type_hint(Gdk::WINDOW_TYPE_HINT_DIALOG); // md.run(); // } break; } } } void NotebookWindow::on_file_export_html() { Gtk::FileChooserDialog dialog("Please enter a file name for the HTML document", Gtk::FILE_CHOOSER_ACTION_SAVE); dialog.set_transient_for(*this); dialog.add_button("_Cancel", Gtk::RESPONSE_CANCEL); dialog.add_button("Select", Gtk::RESPONSE_OK); int result=dialog.run(); switch(result) { case(Gtk::RESPONSE_OK): { std::string name = dialog.get_filename(); std::ofstream temp(name); temp << export_as_HTML(doc); } } } void NotebookWindow::on_file_export_latex() { Gtk::FileChooserDialog dialog("Please enter a file name for the LaTeX document", Gtk::FILE_CHOOSER_ACTION_SAVE); dialog.set_transient_for(*this); dialog.add_button("_Cancel", Gtk::RESPONSE_CANCEL); dialog.add_button("Select", Gtk::RESPONSE_OK); int result=dialog.run(); switch(result) { case(Gtk::RESPONSE_OK): { std::string name = dialog.get_filename(); std::ofstream temp(name); std::size_t dotpos = name.rfind('.'); std::string base = name.substr(0, dotpos); // std::cerr << base << std::endl; temp << export_as_LaTeX(doc, base); } } } void NotebookWindow::on_file_export_python() { Gtk::FileChooserDialog dialog("Please enter a file name for the Python/Cadabra document", Gtk::FILE_CHOOSER_ACTION_SAVE); dialog.set_transient_for(*this); dialog.add_button("_Cancel", Gtk::RESPONSE_CANCEL); dialog.add_button("Select", Gtk::RESPONSE_OK); int result=dialog.run(); switch(result) { case(Gtk::RESPONSE_OK): { std::string name = dialog.get_filename(); std::ofstream temp(name); temp << export_as_python(doc); } } } void NotebookWindow::on_file_export_html_segment() { // FIXME: merge with on_file_export_html. Gtk::FileChooserDialog dialog("Please enter a file name for the HTML segment", Gtk::FILE_CHOOSER_ACTION_SAVE); dialog.set_transient_for(*this); dialog.add_button("_Cancel", Gtk::RESPONSE_CANCEL); dialog.add_button("Select", Gtk::RESPONSE_OK); int result=dialog.run(); switch(result) { case(Gtk::RESPONSE_OK): { std::string name = dialog.get_filename(); std::ofstream temp(name); temp << export_as_HTML(doc, true); } } } // FIXME: this logic can go into DocumentThread to be system independent. std::string NotebookWindow::save(const std::string& fn) const { // Make a backup first, just in case things go wrong. std::ifstream old(fn.c_str()); std::ofstream temp(std::string(fn+"~").c_str()); if(old) { // only backup if there is something to backup if(temp) { std::string ln; while(std::getline(old, ln)) { temp << ln << "\n"; if(!temp) return "Error writing backup file"; } } else { return "Failed to create backup file"; } } std::string out = JSON_serialise(doc); std::ofstream file(fn); file << out << std::endl; return ""; } bool NotebookWindow::quit_safeguard(bool quit) { if(modified) { std::string mes; if(quit) { if(name.size()>0) mes="Save changes to "+name+" before closing?"; else mes="Save changes before closing?"; } else { if(name.size()>0) mes="Save changes to "+name+" before continuing?"; else mes="Save changes before continuing?"; } Gtk::MessageDialog md(mes, false, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_NONE, true); md.set_transient_for(*this); md.set_type_hint(Gdk::WINDOW_TYPE_HINT_DIALOG); md.add_button("Save before continuing",1); md.add_button("Cancel",2); if(quit) md.add_button("No need to save, quit now",3); else md.add_button("No need to save", 3); int action=md.run(); switch(action) { case 1: on_file_save(); return true; case 2: break; case 3: return true; } } else return true; return false; } void NotebookWindow::on_file_quit() { // FIXME: this needs to not just close the current window, but also all // other ones. if (quit_safeguard(true)) hide(); } void NotebookWindow::on_edit_undo() { undo(); } void NotebookWindow::on_edit_copy() { if(selected_cell!=doc.end()) { Glib::RefPtr clipboard = Gtk::Clipboard::get(GDK_SELECTION_CLIPBOARD); on_outbox_copy(clipboard, selected_cell); } if(current_cell!=doc.end()) { // FIXME: handle other cell types. } } void NotebookWindow::on_edit_paste() { } void NotebookWindow::on_edit_insert_above() { if(current_cell==doc.end()) return; DataCell newcell(DataCell::CellType::python, ""); std::shared_ptr action = std::make_shared(newcell, current_cell->id(), ActionAddCell::Position::before); queue_action(action); if (prefs.move_into_new_cell) { std::shared_ptr action2 = std::make_shared(newcell.id(), ActionPositionCursor::Position::in); queue_action(action2); } process_data(); } void NotebookWindow::on_edit_insert_below() { if(current_cell==doc.end()) return; DataCell newcell(DataCell::CellType::python, ""); std::shared_ptr action = std::make_shared(newcell, current_cell->id(), ActionAddCell::Position::after); queue_action(action); if (prefs.move_into_new_cell) { std::shared_ptr action2 = std::make_shared(newcell.id(), ActionPositionCursor::Position::in); queue_action(action2); } process_data(); } void NotebookWindow::on_edit_delete() { if(current_cell==doc.end()) return; if(current_cell->running) return; // we are still expecting results, don't delete DTree::sibling_iterator nxt=doc.next_sibling(current_cell); if(current_cell->textbuf=="" && doc.is_valid(nxt)==false) return; // Do not delete last cell if it is empty. std::shared_ptr action = std::make_shared(current_cell->id(), ActionPositionCursor::Position::next); queue_action(action); std::shared_ptr action2 = std::make_shared(current_cell->id()); queue_action(action2); process_data(); } void NotebookWindow::on_edit_find() { searchbar.set_search_mode(true); searchbar.set_show_close_button(true); searchentry.grab_focus(); search_result.set_text(""); } void NotebookWindow::on_search_text_changed() { const auto text = searchentry.get_text(); if(text.size()==0) return; auto start = std::make_pair(doc.begin(), 0); if(last_find_location.second!=std::string::npos) { select_range(doc, last_find_location.first, 0, 0); start = last_find_location; if(last_find_string.size()==text.size()) ++start.second; } else { } auto res = find_string(start.first, start.second, text, search_case_insensitive.get_active()); last_find_location=res; last_find_string=text; if(res.second!=std::string::npos) { if(res.first->cell_type!=DataCell::CellType::python && res.first->cell_type!=DataCell::CellType::latex) { search_result.set_text("Found."); scroll_cell_into_view(res.first); } else { current_cell=res.first; search_result.set_text("Found."); scroll_current_cell_into_view(); select_range(doc, res.first, res.second, text.size()); } } else { last_find_string=""; search_result.set_text("Not found, try again to start from start."); } } void NotebookWindow::on_edit_split() { std::shared_ptr action = std::make_shared(current_cell->id()); queue_action(action); process_data(); } void NotebookWindow::on_edit_cell_is_python() { if(current_cell==doc.end()) return; if(current_cell->cell_type==DataCell::CellType::latex) { current_cell->cell_type = DataCell::CellType::python; update_cell(doc, current_cell); } refresh_highlighting(); } void NotebookWindow::on_ignore_cell_on_import() { if(current_cell==doc.end()) return; current_cell->ignore_on_import= !(current_cell->ignore_on_import); update_cell(doc, current_cell); refresh_highlighting(); } void NotebookWindow::on_edit_cell_is_latex() { if(current_cell==doc.end()) return; if(current_cell->cell_type==DataCell::CellType::python) { current_cell->cell_type = DataCell::CellType::latex; update_cell(doc, current_cell); } refresh_highlighting(); } void NotebookWindow::on_view_split() { canvasses.push_back(new NotebookCanvas()); // Add the new canvas into the bottom pane of the last visible canvas. canvasses[canvasses.size()-2]->pack2(*canvasses.back(), true, true); build_visual_representation(); canvasses.back()->show_all(); canvasses[canvasses.size()-2]->set_position(canvasses[canvasses.size()-2]->get_height()/2.0); } void NotebookWindow::on_view_close() { // FIXME: this always removes the last canvas, not the current one. if(canvasses.size()>1) { canvasses[canvasses.size()-2]->remove(*canvasses.back()); NotebookCanvas *oldcanvas = canvasses.back(); canvasses.pop_back(); delete oldcanvas; } } void NotebookWindow::on_run_cell() { if(read_only) return; // This is actually handled by the CodeInput widget, which ensures that the // DTree is up to date and then calls execute. VisualCell& actual = canvasses[current_canvas]->visualcells[&(*current_cell)]; actual.inbox->edit.shift_enter_pressed(); // cell_content_execute(current_cell, current_canvas); } void NotebookWindow::on_run_runall() { // FIXME: move to DocumentThread DTree::sibling_iterator sib=doc.begin(doc.begin()); while(sib!=doc.end(doc.begin())) { if(sib->cell_type==DataCell::CellType::python) cell_content_execute(DTree::iterator(sib), current_canvas, false); ++sib; } } void NotebookWindow::on_run_runtocursor() { // FIXME: move to DocumentThread DTree::sibling_iterator sib=doc.begin(doc.begin()); while(sib!=current_cell && sib!=doc.end(doc.begin())) { if(sib->cell_type==DataCell::CellType::python) cell_content_execute(DTree::iterator(sib), current_canvas, false); ++sib; } } void NotebookWindow::on_run_stop() { compute->stop(); } void NotebookWindow::on_kernel_restart() { // FIXME: add warnings compute->restart_kernel(); { std::lock_guard guard(status_mutex); progress_string = "Idle"; progress_frac = 0.0; } dispatch_update_status.emit(); } void NotebookWindow::on_help() const { if(current_cell==doc.end()) return; if(current_cell->cell_type!=DataCell::CellType::python) return; // Figure out the keyword under the cursor. VisualCell& actual = canvasses[current_canvas]->visualcells[&(*current_cell)]; std::string before, after; actual.inbox->slice_cell(before, after); help_t help_type; std::string help_topic; help_type_and_topic(before, after, help_type, help_topic); snoop::log("help") << help_topic << snoop::flush; bool ret=false; std::string pref = cadabra::install_prefix()+"/share/cadabra2/manual/"; if(help_type==help_t::algorithm) ret=cdbapp->open_help(pref+"algorithms/"+help_topic+".cnb", help_topic); if(help_type==help_t::property) ret=cdbapp->open_help(pref+"properties/"+help_topic+".cnb", help_topic); if(!ret) { Gtk::MessageDialog md("No help available", false, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_OK, true); // md.set_transient_for(*this); md.set_type_hint(Gdk::WINDOW_TYPE_HINT_DIALOG); md.set_secondary_text("No help available for '"+help_topic+"'.\nNot all algorithms and properties have manual pages yet, sorry."); md.run(); } } void NotebookWindow::set_compute_thread(ComputeThread* cthread) { DocumentThread::set_compute_thread(cthread); } void NotebookWindow::on_help_about() { Glib::RefPtr logo=Gdk::Pixbuf::create_from_file(cadabra::install_prefix()+"/share/cadabra2/images/cadabra2-gtk.png"); Gtk::AboutDialog about; about.set_transient_for(*this); about.set_program_name("Cadabra"); about.set_comments("A field-theory motivated approach to computer algebra"); about.set_version(std::string("Version ")+CADABRA_VERSION_MAJOR+"."+CADABRA_VERSION_MINOR +"."+CADABRA_VERSION_PATCH +" (build "+CADABRA_VERSION_BUILD+" dated "+CADABRA_VERSION_DATE+")"); std::vector authors; authors.push_back("Kasper Peeters"); about.set_authors(authors); about.set_copyright(std::string("\xC2\xA9 ")+COPYRIGHT_YEARS+std::string(" Kasper Peeters")); about.set_license_type(Gtk::License::LICENSE_GPL_3_0); about.set_website("http://cadabra.science"); about.set_website_label("cadabra.science"); about.set_logo(logo); std::vector special; special.push_back("José M. Martín-García (for the xPerm canonicalisation code)"); special.push_back("Dominic Price (for the conversion to pybind and most of the Windows port)"); special.push_back("Connor Behan (for various improvements related to index-free algorithms)"); special.push_back("James Allen (for writing much of the factoring code)"); special.push_back("Software Sustainability Institute"); special.push_back("Institute of Advanced Study (for a Christopherson/Knott fellowship)"); about.add_credit_section("Special thanks", special); about.run(); } void NotebookWindow::on_help_register() { Gtk::Dialog md("Welcome to Cadabra!", *this, Gtk::MESSAGE_WARNING); md.set_transient_for(*this); md.set_type_hint(Gdk::WINDOW_TYPE_HINT_DIALOG); Gtk::Box *box = md.get_content_area(); Gtk::Label txt; txt.set_markup("Welcome to Cadabra!\n\nWriting this software takes an incredible amount of spare time,\nand it is extremely difficult to get funding for its development.\n\nPlease show your support by registering your email address,\nso I can convince the bean-counters that this software is of interest.\n\nI will only use this address to count users and to email you,\nroughly once every half a year, with a bit of news about Cadabra.\n\nMany thanks for your support!\n\nKasper Peeters, info@cadabra.science"); txt.set_line_wrap(); txt.set_margin_top(10); txt.set_margin_left(10); txt.set_margin_right(10); txt.set_margin_bottom(10); box->pack_start(txt, Gtk::PACK_EXPAND_WIDGET); Gtk::Grid grid; grid.set_column_homogeneous(false); grid.set_hexpand(true); grid.set_margin_left(10); grid.set_margin_right(10); box->pack_start(grid, Gtk::PACK_EXPAND_WIDGET); Gtk::Label name_label("Name:"); Gtk::Entry name; name_label.set_alignment(0, 0.5); name.set_hexpand(true); grid.attach(name_label, 0, 0, 1, 1); grid.attach(name, 1, 0, 1, 1); Gtk::Label email_label("Email address:"); email_label.set_alignment(0, 0.5); Gtk::Entry email; email.set_hexpand(true); grid.attach(email_label, 0, 1, 1, 1); grid.attach(email, 1, 1, 1, 1); Gtk::Label affiliation_label("Affiliation:"); Gtk::Entry affiliation; affiliation_label.set_alignment(0, 0.5); affiliation.set_hexpand(true); grid.attach(affiliation_label, 0, 2, 1, 1); grid.attach(affiliation, 1, 2, 1, 1); Gtk::HBox hbox; box->pack_end(hbox, Gtk::PACK_SHRINK); Gtk::Button reg("Register my support"), nothanks("I prefer to stay anonymous"), alreadyset("I am already registered"); hbox.pack_end(reg, Gtk::PACK_SHRINK, 10); hbox.pack_start(nothanks, Gtk::PACK_SHRINK, 10); hbox.pack_start(alreadyset, Gtk::PACK_SHRINK, 10); reg.signal_clicked().connect([&]() { set_user_details(name.get_text(), email.get_text(), affiliation.get_text()); prefs.is_anonymous = false; prefs.is_registered = true; md.hide(); }); nothanks.signal_clicked().connect([&]() { prefs.is_anonymous = true; prefs.is_registered = false; md.hide(); }); alreadyset.signal_clicked().connect([&]() { prefs.is_registered = true; prefs.is_anonymous = false; md.hide(); }); box->show_all(); md.run(); menu_help_register->set_sensitive(!prefs.is_registered); } void NotebookWindow::on_text_scaling_factor_changed(const std::string& key) { #ifdef DEBUG std::cerr << key << std::endl; #endif if(key=="text-scaling-factor" || key=="scaling-factor") { auto screen = Gdk::Screen::get_default(); scale = settings->get_double("text-scaling-factor"); scale *= screen->get_monitor_scale_factor(0); std::cout << "cadabra-client: total scaling-factor = " << scale << std::endl; engine.set_scale(scale, screen->get_monitor_scale_factor(0)); engine.invalidate_all(); tex_run_async(); auto it=canvasses.begin(); while(it!=canvasses.end()) { (*it)->refresh_all(); ++it; } } } void NotebookWindow::select_git_path() { SelectFileDialog dialog("Select git executable", *this, true); dialog.set_transient_for(*this); dialog.set_text(prefs.git_path); if (dialog.run() == Gtk::RESPONSE_OK) prefs.git_path = dialog.get_text(); } void NotebookWindow::compare_to_file() { std::string filename; SelectFileDialog dialog("Select file to compare", *this, true); dialog.set_transient_for(*this); if (dialog.run() == Gtk::RESPONSE_OK) filename = dialog.get_text(); else return; std::ifstream a(filename); if (!a.is_open()) { Gtk::MessageDialog error_dialog(*this, "The file '" + filename + "' could not be opened for reading", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true); error_dialog.set_title("Could not open file"); error_dialog.run(); return; std::stringstream b; b << JSON_serialise(doc); diffviewer = std::make_unique(a, b, *this); //diffviewer->set_transient_for(*this); diffviewer->run_noblock(); } } std::string NotebookWindow::run_git_command(const std::string& args) { using namespace TinyProcessLib; auto split_pos = name.find_last_of("\\/"); if (split_pos == std::string::npos) throw std::runtime_error("File must be in a valid git directory"); std::string path = name.substr(0, split_pos); std::string output; Process git( prefs.git_path + " " + args, path, [&output](const char* bytes, size_t n) { output += std::string(bytes, n); }, [&output](const char* bytes, size_t n) { output += std::string(bytes, n); } ); if (git.get_exit_status()) throw std::runtime_error(output); else return output; } void NotebookWindow::compare_git(const std::string& commit_hash) { auto split_pos = name.find_last_of("\\/"); if (split_pos == std::string::npos || split_pos == name.size() - 1) throw std::runtime_error("A valid file must be open"); auto prefix = trim(run_git_command("rev-parse --show-prefix")); auto contents = run_git_command("show " + commit_hash + ":" + prefix + name.substr(split_pos + 1)); std::stringstream a, b; a << contents; b << JSON_serialise(doc); diffviewer = std::make_unique(a, b, *this); //diffviewer->set_transient_for(*this); diffviewer->run_noblock(); } void NotebookWindow::compare_git_latest() { try { // Get latest commit hash auto commit = run_git_command("log --pretty=format:%h -n 1"); compare_git(trim(commit)); } catch (const std::exception& ex) { Gtk::MessageDialog error_dialog(ex.what(), false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true); error_dialog.set_transient_for(*this); error_dialog.set_title("Git error"); error_dialog.run(); } } class GitChooseModelColumns : public Gtk::TreeModel::ColumnRecord { public: GitChooseModelColumns() { add(commit_hash); add(author); add(timestamp); add(description); } Gtk::TreeModelColumn commit_hash; Gtk::TreeModelColumn author; Gtk::TreeModelColumn timestamp; Gtk::TreeModelColumn description; }; void NotebookWindow::compare_git_choose() { try { std::string commit_hash; std::string max_entries = "15"; auto commits = string_to_vec(run_git_command("log --pretty=format:%h -n " + max_entries)); auto authors = string_to_vec(run_git_command("log --pretty=format:%an -n " + max_entries)); auto times = string_to_vec(run_git_command("log --pretty=format:%ar -n " + max_entries)); auto descriptions = string_to_vec(run_git_command("log --pretty=format:%s -n " + max_entries)); Gtk::TreeView tree_view; GitChooseModelColumns columns; Glib::RefPtr list_store = Gtk::ListStore::create(columns); for (size_t i = 0; i < commits.size(); ++i) { Gtk::TreeModel::Row row = *(list_store->append()); row[columns.commit_hash] = commits[i]; row[columns.author] = authors[i]; row[columns.timestamp] = times[i]; row[columns.description] = descriptions[i]; } tree_view.set_model(list_store); tree_view.append_column("Commit Hash", columns.commit_hash); tree_view.append_column("Author", columns.author); tree_view.append_column("Commit Time", columns.timestamp); tree_view.append_column("Description", columns.description); Gtk::ScrolledWindow scrolled_window; scrolled_window.add(tree_view); scrolled_window.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC); scrolled_window.set_min_content_height(400); Gtk::Dialog select_dialog("Select a commit to compare", *this, true); select_dialog.set_transient_for(*this); select_dialog.get_content_area()->add(scrolled_window); select_dialog.add_button("Compare", Gtk::RESPONSE_OK); select_dialog.add_button("Cancel", Gtk::RESPONSE_CANCEL); select_dialog.show_all(); if (select_dialog.run() == Gtk::RESPONSE_OK) commit_hash = tree_view.get_selection()->get_selected()->get_value(columns.commit_hash); else return; compare_git(trim(commit_hash)); } catch (const std::exception& ex) { Gtk::MessageDialog error_dialog(ex.what(), false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true); error_dialog.set_transient_for(*this); error_dialog.set_title("Git error"); error_dialog.run(); } } void NotebookWindow::compare_git_specific() { try { std::string commit_hash; Gtk::Entry entry; Gtk::Dialog entry_dialog("Enter hash of commit to compare against", *this, true); entry_dialog.set_transient_for(*this); entry_dialog.get_content_area()->add(entry); entry_dialog.add_button("Compare", Gtk::RESPONSE_OK); entry_dialog.add_button("Cancel", Gtk::RESPONSE_CANCEL); entry_dialog.show_all(); if (entry_dialog.run() == Gtk::RESPONSE_OK) commit_hash = entry.get_text(); else return; compare_git(trim(commit_hash)); } catch (const std::exception& ex) { Gtk::MessageDialog error_dialog(ex.what(), false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true); error_dialog.set_transient_for(*this); error_dialog.set_title("Git error"); error_dialog.run(); } } void NotebookWindow::on_prefs_font_size(int num) { if(prefs.font_step==num) return; prefs.font_step=num; // std::string res=save_config(); // if(res.size()>0) { // Gtk::MessageDialog md("Error"); // md.set_secondary_text(res); // md.set_type_hint(Gdk::WINDOW_TYPE_HINT_DIALOG); // md.run(); // } // std::cerr << "cadabra-client: prefs_font_size = " << num << std::endl; engine.set_font_size(12+(num*2)); if(is_configured) { // std::cerr << "cadabra-client: re-running TeX to change font size" << std::endl; // No point in running TeX on all cells if we have not yet had an // on_configure_event signal; that will come after us and then we will // have to run all again. engine.invalidate_all(); tex_run_async(); for(auto& canvas: canvasses) { for(auto& visualcell: canvas->visualcells) { if(visualcell.first->cell_type==DataCell::CellType::python || visualcell.first->cell_type==DataCell::CellType::latex) { visualcell.second.inbox->set_font_size(num); } } } for(unsigned int i=0; irefresh_all(); } // // Hack. // auto screen = Gdk::Screen::get_default(); // if(get_window()!=0) { // std::cerr << "invalidating" << std::endl; // get_window()->invalidate_rect(Gdk::Rectangle(0, 0, screen->get_width()/2, screen->get_height()*0.8),true); // queue_draw(); // } } void NotebookWindow::on_prefs_highlight_syntax(bool on) { if (prefs.highlight == on) return; prefs.highlight = on; prefs.save(); for (auto& canvas : canvasses) { for (auto& visualcell : canvas->visualcells) { // Need to be careful to only try and do this on latex or // python cells to avoid an exception being raised when // trying to edit an immutable cell type switch (visualcell.first->cell_type) { // Fallthrough case DataCell::CellType::python: case DataCell::CellType::latex: if(on) { load_css(); visualcell.second.inbox->enable_highlighting(visualcell.first->cell_type, prefs); } else { load_css(); visualcell.second.inbox->disable_highlighting(); } break; default: break; } } } } void NotebookWindow::on_prefs_choose_colours() { cadabra::ChooseColoursDialog dialog(prefs, *this); dialog.run(); prefs.save(); } void NotebookWindow::on_prefs_use_defaults() { Gtk::MessageDialog sure("Use Default settings?", false, Gtk::MESSAGE_QUESTION, Gtk::BUTTONS_YES_NO, true); sure.set_transient_for(*this); sure.set_secondary_text("This will effect font size and colour schemes."); int response = sure.run(); if (response == Gtk::RESPONSE_YES) { prefs = Prefs(true); for (auto& action : default_actions) action->activate(); refresh_highlighting(); menu_help_register->set_sensitive(!prefs.is_registered); prefs.save(); } } void NotebookWindow::on_tools_options() { // Create the dialog box Gtk::Dialog options("Options", false); options.set_transient_for(*this); options.get_content_area()->property_margin() = 5; options.add_button("Apply", Gtk::RESPONSE_APPLY); options.add_button("Cancel", Gtk::RESPONSE_CANCEL); // Helper functions for making option widgets auto add_label = [&options](const char* text, Gtk::Align alignment) { auto label = std::make_unique(); label->set_markup(text); label->set_halign(alignment); options.get_content_area()->add(*label); return label; }; auto add_sep = [&options]() { auto sep = std::make_unique(); sep->property_margin() = 5; options.get_content_area()->add(*sep); return sep; }; auto add_checkbox = [&options](const char* label, bool active) { auto cb = std::make_unique(label); cb->set_active(active); options.get_content_area()->add(*cb); return cb; }; auto add_entry = [&options](const Glib::ustring& val) { auto entry = std::make_unique(); entry->set_text(val); options.get_content_area()->add(*entry); return entry; }; // Add widgets & display auto py_opts = add_label("Cadabra/Python Options", Gtk::ALIGN_CENTER); auto pypath_label = add_label("Default Python Path (semicolon separated):", Gtk::ALIGN_START); auto pypath = add_entry(prefs.python_path); auto sep1 = add_sep(); auto gui_opts = add_label("GUI Options", Gtk::ALIGN_CENTER); auto auto_move = add_checkbox("Automatically move into a created cell", prefs.move_into_new_cell); auto tab_completion = add_checkbox("Show possible completions with TAB", prefs.tab_completion); auto sep2 = add_sep(); options.show_all(); int res = options.run(); // Options menu closed, run any updates if (res == Gtk::RESPONSE_APPLY) { if (prefs.python_path != pypath->get_text()) { prefs.python_path = pypath->get_text(); Gtk::MessageDialog md(*this, "Changes to python path won't take effect until kernel is restarted; restart now?", false, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_YES_NO, true); md.set_title("Python Path Changed"); md.set_modal(); md.set_position(Gtk::WindowPosition::WIN_POS_CENTER); int res = md.run(); if (res == Gtk::RESPONSE_YES) { on_kernel_restart(); } } prefs.move_into_new_cell = auto_move->get_active(); prefs.tab_completion = tab_completion->get_active(); } } bool remove_recursive(const gchar* path) { // If filename is a file then we have reached the end of the recursion tree if (g_file_test(path, G_FILE_TEST_IS_REGULAR)) return g_remove(path) == 0; bool success = true; auto dir = g_dir_open(path, 0, NULL); for (const gchar* child = g_dir_read_name(dir); child != NULL; child = g_dir_read_name(dir)) { gchar* fullpath = g_strconcat(path, G_DIR_SEPARATOR_S, child, NULL); success = remove_recursive(fullpath); g_free(fullpath); if (!success) break; } g_dir_close(dir); if (!success) return false; return g_remove(path) == 0; } void NotebookWindow::on_tools_clear_cache() { Gtk::MessageDialog prog("Removing cached library files, please wait...", false, Gtk::MESSAGE_INFO, Gtk::BUTTONS_OK, true); prog.set_transient_for(*this); prog.set_title("Clearing cache"); prog.show_all(); auto config_dir = Glib::ustring(g_get_user_config_dir()) + "/cadabra_packages"; bool success = remove_recursive(config_dir.c_str()); prog.set_message(success ? "Cache cleared!" : "Failed to remove cached library files. You can manually remove them by deleting the cadabra_packages folder in your config directory" ); prog.run(); } void NotebookWindow::refresh_highlighting() { if (prefs.highlight) { on_prefs_highlight_syntax(false); on_prefs_highlight_syntax(true); } } bool NotebookWindow::idle_handler() { #if GTKMM_MINOR_VERSION>=10 for(auto& reveal: to_reveal) { reveal->set_reveal_child(true); } to_reveal.clear(); #endif return false; // disconnect } void NotebookWindow::unselect_output_cell() { if(selected_cell==doc.end()) return; for(unsigned int i=0; ivisualcells.find(&(*selected_cell))!=canvasses[i]->visualcells.end()) { auto& outbox = canvasses[i]->visualcells[&(*selected_cell)].outbox; outbox->image.set_state(Gtk::STATE_NORMAL); } } selected_cell=doc.end(); action_copy->set_sensitive(false); } bool NotebookWindow::handle_outbox_select(GdkEventButton *, DTree::iterator it) { // std::cerr << "handle_outbox_select " << it->textbuf << std::endl; unselect_output_cell(); // Colour the background of the selected cell, in all canvasses. for(int i=0; i<(int)canvasses.size(); ++i) { if(canvasses[i]->visualcells.find(&(*it))!=canvasses[i]->visualcells.end()) { auto& outbox = canvasses[i]->visualcells[&(*it)].outbox; outbox->image.set_state(Gtk::STATE_SELECTED); // std::cerr << "selecting" << std::endl; // if(i==current_canvas) // outbox->grab_focus(); // FIXME: need to remove focus from any CodeInput widget; the above does not do that. } } selected_cell=it; action_copy->set_sensitive(true); Glib::RefPtr clipboard = Gtk::Clipboard::get(GDK_SELECTION_PRIMARY); on_outbox_copy(clipboard, selected_cell); return true; } void NotebookWindow::on_outbox_copy(Glib::RefPtr refClipboard, DTree::iterator it) { std::string cpystring=(*it).textbuf; // Find the child cell which contains the input_form data. auto sib=doc.begin(it); while(sib!=doc.end(it)) { if(sib->cell_type==DataCell::CellType::input_form) { clipboard_cdb = sib->textbuf; // std::cerr << "found input form " << clipboard_cdb << std::endl; break; } ++sib; } // Setup clipboard handling clipboard_txt = cpystring; std::vector listTargets; if(clipboard_cdb.size()>0) listTargets.push_back( Gtk::TargetEntry("cadabra") ); listTargets.push_back( Gtk::TargetEntry("UTF8_STRING") ); listTargets.push_back( Gtk::TargetEntry("TEXT") ); refClipboard->set( listTargets, sigc::mem_fun(this, &NotebookWindow::on_clipboard_get), sigc::mem_fun(this, &NotebookWindow::on_clipboard_clear) ); } void NotebookWindow::on_clipboard_get(Gtk::SelectionData& selection_data, guint ) { const Glib::ustring target = selection_data.get_target(); if(target == "cadabra") selection_data.set("cadabra", clipboard_cdb); else if(target == "UTF8_STRING" || target=="TEXT") { selection_data.set_text(clipboard_txt); } } void NotebookWindow::on_clipboard_clear() { }