/****************************************************************************** License: GNU General Public License v3.0 only Copyright: (C) 2008 - 2021 Alexander Shaduri ******************************************************************************/ /// \file /// \author Alexander Shaduri /// \ingroup gsc /// \weakgroup gsc /// @{ #include "local_glibmm.h" #include #include // GDK_KEY_Escape #include #include // std::size_t #include #include "applib/app_gtkmm_tools.h" // app_gtkmm_create_tree_view_column #include "hz/fs.h" #include "rconfig/rconfig.h" #include "gsc_executor_log_window.h" #include "gsc_init.h" // app_get_debug_buffer_str() GscExecutorLogWindow::GscExecutorLogWindow(BaseObjectType* gtkcobj, Glib::RefPtr ui) : AppBuilderWidget(gtkcobj, std::move(ui)) { // Connect callbacks Gtk::Button* window_close_button = nullptr; APP_BUILDER_AUTO_CONNECT(window_close_button, clicked); Gtk::Button* window_save_current_button = nullptr; APP_BUILDER_AUTO_CONNECT(window_save_current_button, clicked); Gtk::Button* window_save_all_button = nullptr; APP_BUILDER_AUTO_CONNECT(window_save_all_button, clicked); Gtk::Button* clear_command_list_button = nullptr; APP_BUILDER_AUTO_CONNECT(clear_command_list_button, clicked); // Accelerators Glib::RefPtr accel_group = this->get_accel_group(); if (window_close_button) { window_close_button->add_accelerator("clicked", accel_group, GDK_KEY_Escape, Gdk::ModifierType(0), Gtk::AccelFlags(0)); } // --------------- Make a treeview auto* treeview = this->lookup_widget("command_list_treeview"); if (treeview) { Gtk::TreeModelColumnRecord model_columns; // #, Command + parameters, [EntryPtr] model_columns.add(col_num); app_gtkmm_create_tree_view_column(col_num, *treeview, "#", _("# of executed command"), true); // sortable model_columns.add(col_command); app_gtkmm_create_tree_view_column(col_command, *treeview, _("Command"), _("Command with parameters"), true); // sortable model_columns.add(col_entry); // create a TreeModel (ListStore) list_store = Gtk::ListStore::create(model_columns); // list_store->set_sort_column(col_num, Gtk::SORT_DESCENDING); // default sort treeview->set_model(list_store); selection = treeview->get_selection(); selection->signal_changed().connect(sigc::mem_fun(*this, &GscExecutorLogWindow::on_tree_selection_changed) ); } // Hide command text entry in win32. // Setting text on this entry segfaults under win32, (utf8 conversion doesn't // help, not that it should). Seems to be connected to non-english locale. // Surprisingly, the treeview column text still works. // The problem seems to have disappeared (new compiler/runtime?) // if constexpr(BuildEnv::is_kernel_family_windows()) { // Gtk::Box* command_hbox = this->lookup_widget("command_hbox"); // if (command_hbox) // command_hbox->hide(); // } // --------------- // Connect to CommandExecutor signal cmdex_sync_signal_execute_finish().connect(sigc::mem_fun(*this, &GscExecutorLogWindow::on_command_output_received)); // show(); } void GscExecutorLogWindow::show_last() { auto* treeview = this->lookup_widget("command_list_treeview"); if (treeview != nullptr && !list_store->children().empty()) { // Gtk::TreeRow row = *(list_store->children().rbegin()); // this causes invalid read error in valgrind Gtk::TreeRow row = *(--(list_store->children().end())); selection->select(row); // you would think that scroll_to_row would accept a TreeRow for a change (shock!) treeview->scroll_to_row(list_store->get_path(row)); } show(); } void GscExecutorLogWindow::clear_view_widgets() { auto* window_save_current_button = this->lookup_widget("window_save_current_button"); if (window_save_current_button) window_save_current_button->set_sensitive(false); auto* output_textview = this->lookup_widget("output_textview"); if (output_textview) { Glib::RefPtr buffer = output_textview->get_buffer(); buffer->set_text(""); } auto* command_entry = this->lookup_widget("command_entry"); if (command_entry) command_entry->set_text(""); } void GscExecutorLogWindow::on_command_output_received(const CommandExecutorResult& info) { auto entry = std::make_shared(info); entries.push_back(entry); // update tree model Gtk::TreeRow row = *(list_store->append()); row[col_num] = entries.size(); row[col_command] = info.command + " " + info.parameters; row[col_entry] = entry; // if visible, set the selection to it if (auto* treeview = this->lookup_widget("command_list_treeview")) { selection->select(row); treeview->scroll_to_row(list_store->get_path(row)); } } bool GscExecutorLogWindow::on_delete_event([[maybe_unused]] GdkEventAny* e) { on_window_close_button_clicked(); return true; // event handled } void GscExecutorLogWindow::on_window_close_button_clicked() { this->hide(); // hide only, don't destroy } void GscExecutorLogWindow::on_window_save_current_button_clicked() { if (selection->count_selected_rows() == 0) return; Gtk::TreeIter iter = selection->get_selected(); std::shared_ptr entry = (*iter)[col_entry]; static std::string last_dir; if (last_dir.empty()) { last_dir = rconfig::get_data("gui/drive_data_open_save_dir"); } int result = 0; Glib::RefPtr specific_filter = Gtk::FileFilter::create(); specific_filter->set_name(_("Text Files")); specific_filter->add_pattern("*.txt"); Glib::RefPtr all_filter = Gtk::FileFilter::create(); all_filter->set_name(_("All Files")); all_filter->add_pattern("*"); #if GTK_CHECK_VERSION(3, 20, 0) std::unique_ptr dialog(gtk_file_chooser_native_new( _("Save Data As..."), this->gobj(), GTK_FILE_CHOOSER_ACTION_SAVE, nullptr, nullptr), &g_object_unref); gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dialog.get()), TRUE); gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog.get()), specific_filter->gobj()); gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog.get()), all_filter->gobj()); if (!last_dir.empty()) gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog.get()), last_dir.c_str()); gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog.get()), ".txt"); result = gtk_native_dialog_run(GTK_NATIVE_DIALOG(dialog.get())); #else Gtk::FileChooserDialog dialog(*this, _("Save Data As..."), Gtk::FILE_CHOOSER_ACTION_SAVE); // Add response buttons the the dialog dialog.add_button(Gtk::Stock::CANCEL, Gtk::RESPONSE_CANCEL); dialog.add_button(Gtk::Stock::SAVE, Gtk::RESPONSE_ACCEPT); dialog.set_do_overwrite_confirmation(true); dialog.add_filter(specific_filter); dialog.add_filter(all_filter); if (!last_dir.empty()) dialog.set_current_folder(last_dir); dialog.set_current_name(".txt"); // Show the dialog and wait for a user response result = dialog.run(); // the main cycle blocks here #endif // Handle the response switch (result) { case Gtk::RESPONSE_ACCEPT: { std::string file; #if GTK_CHECK_VERSION(3, 20, 0) file = app_ustring_from_gchar(gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog.get()))); last_dir = hz::fs::u8path(file).parent_path().u8string(); #else file = dialog.get_filename(); // in fs encoding last_dir = dialog.get_current_folder(); // save for the future #endif rconfig::set_data("gui/drive_data_open_save_dir", last_dir); if (file.rfind(".txt") != (file.size() - std::strlen(".txt"))) { file += ".txt"; } auto ec = hz::fs_file_put_contents(hz::fs::u8path(file), entry->std_output); if (ec) { gui_show_error_dialog(_("Cannot save data to file"), ec.message(), this); } break; } case Gtk::RESPONSE_CANCEL: case Gtk::RESPONSE_DELETE_EVENT: // nothing, the dialog is closed already break; default: debug_out_error("app", DBG_FUNC_MSG << "Unknown dialog response code: " << result << ".\n"); break; } } void GscExecutorLogWindow::on_window_save_all_button_clicked() { // complete libdebug output + execution logs std::ostringstream exss; exss << "\n------------------------- LIBDEBUG LOG -------------------------\n\n\n"; exss << app_get_debug_buffer_str() << "\n\n\n"; exss << "\n\n\n------------------------- EXECUTION LOG -------------------------\n\n\n"; for (std::size_t i = 0; i < entries.size(); ++i) { exss << "\n\n\n------------------------- EXECUTED COMMAND " << (i+1) << " -------------------------\n\n"; exss << "\n---------------" << "Command" << "---------------\n"; exss << entries[i]->command << "\n"; exss << "\n---------------" << "Parameters" << "---------------\n"; exss << entries[i]->parameters << "\n"; exss << "\n---------------" << "STDOUT" << "---------------\n"; exss << entries[i]->std_output << "\n\n"; exss << "\n---------------" << "STDERR" << "---------------\n"; exss << entries[i]->std_error << "\n\n"; exss << "\n---------------" << "Error Message" << "---------------\n"; exss << entries[i]->error_message << "\n\n"; } static std::string last_dir; if (last_dir.empty()) { last_dir = rconfig::get_data("gui/drive_data_open_save_dir"); } int result = 0; Glib::RefPtr specific_filter = Gtk::FileFilter::create(); specific_filter->set_name(_("Text Files")); specific_filter->add_pattern("*.txt"); Glib::RefPtr all_filter = Gtk::FileFilter::create(); all_filter->set_name(_("All Files")); all_filter->add_pattern("*"); #if GTK_CHECK_VERSION(3, 20, 0) std::unique_ptr dialog(gtk_file_chooser_native_new( _("Save Data As..."), this->gobj(), GTK_FILE_CHOOSER_ACTION_SAVE, nullptr, nullptr), &g_object_unref); gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dialog.get()), TRUE); gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog.get()), specific_filter->gobj()); gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog.get()), all_filter->gobj()); if (!last_dir.empty()) gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog.get()), last_dir.c_str()); gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog.get()), ".txt"); result = gtk_native_dialog_run(GTK_NATIVE_DIALOG(dialog.get())); #else Gtk::FileChooserDialog dialog(*this, _("Save Data As..."), Gtk::FILE_CHOOSER_ACTION_SAVE); // Add response buttons the the dialog dialog.add_button(Gtk::Stock::CANCEL, Gtk::RESPONSE_CANCEL); dialog.add_button(Gtk::Stock::SAVE, Gtk::RESPONSE_ACCEPT); dialog.set_do_overwrite_confirmation(true); dialog.add_filter(specific_filter); dialog.add_filter(all_filter); if (!last_dir.empty()) dialog.set_current_folder(last_dir); dialog.set_current_name(".txt"); // Show the dialog and wait for a user response result = dialog.run(); // the main cycle blocks here #endif // Handle the response switch (result) { case Gtk::RESPONSE_ACCEPT: { std::string file; #if GTK_CHECK_VERSION(3, 20, 0) file = app_ustring_from_gchar(gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog.get()))); last_dir = hz::fs::u8path(file).parent_path().u8string(); #else file = dialog.get_filename(); // in fs encoding last_dir = dialog.get_current_folder(); // save for the future #endif rconfig::set_data("gui/drive_data_open_save_dir", last_dir); if (file.rfind(".txt") != (file.size() - std::strlen(".txt"))) { file += ".txt"; } auto ec = hz::fs_file_put_contents(hz::fs::u8path(file), exss.str()); if (ec) { gui_show_error_dialog(_("Cannot save data to file"), ec.message(), this); } break; } case Gtk::RESPONSE_CANCEL: case Gtk::RESPONSE_DELETE_EVENT: // nothing, the dialog is closed already break; default: debug_out_error("app", DBG_FUNC_MSG << "Unknown dialog response code: " << result << ".\n"); break; } } void GscExecutorLogWindow::on_clear_command_list_button_clicked() { entries.clear(); list_store->clear(); // this will unselect & clear widgets too. } void GscExecutorLogWindow::on_tree_selection_changed() { this->clear_view_widgets(); if (selection->count_selected_rows() > 0) { Gtk::TreeIter iter = selection->get_selected(); Gtk::TreeRow row = *iter; std::shared_ptr entry = row[col_entry]; if (auto* output_textview = this->lookup_widget("output_textview")) { Glib::RefPtr buffer = output_textview->get_buffer(); if (buffer) { buffer->set_text(app_make_valid_utf8_from_command_output(entry->std_output)); Glib::RefPtr tag; Glib::RefPtr table = buffer->get_tag_table(); if (table) tag = table->lookup("font"); if (!tag) tag = buffer->create_tag("font"); tag->property_family() = "Monospace"; buffer->apply_tag(tag, buffer->begin(), buffer->end()); } } if (auto* command_entry = this->lookup_widget("command_entry")) { std::string cmd_text = entry->command + " " + entry->parameters; command_entry->set_text(app_make_valid_utf8_from_command_output(cmd_text)); } if (auto* window_save_current_button = this->lookup_widget("window_save_current_button")) window_save_current_button->set_sensitive(true); } } /// @}