/************************************************************************** Copyright: (C) 2008 - 2018 Alexander Shaduri License: See LICENSE_gsmartcontrol.txt ***************************************************************************/ /// \file /// \author Alexander Shaduri /// \ingroup gsc /// \weakgroup gsc /// @{ // TODO Remove this in gtkmm4. #include "local_glibmm.h" #include #include #include // GDK_KEY_Escape #include // better use vector, it's needed by others too #include // std::min, std::max #include "hz/scoped_ptr.h" #include "hz/string_num.h" // number_to_string #include "hz/string_sprintf.h" // string_sprintf #include "hz/string_algo.h" // string_join #include "hz/fs.h" #include "hz/format_unit.h" // format_time_length #include "rconfig/config.h" // rconfig::* #include "applib/app_gtkmm_utils.h" // app_gtkmm_* #include "applib/storage_property_colors.h" #include "applib/gui_utils.h" // gui_show_error_dialog #include "applib/smartctl_executor_gui.h" #include "gsc_text_window.h" #include "gsc_info_window.h" #include "gsc_executor_error_dialog.h" #include "gsc_startup_settings.h" using namespace std::literals; /// A label for StorageProperty struct PropertyLabel { /// Constructor PropertyLabel(std::string label_, const StorageProperty* prop, bool markup_ = false) : label(std::move(label_)), property(prop), markup(markup_) { } std::string label; ///< Label text const StorageProperty* property = nullptr; ///< Storage property bool markup = false; ///< Whether the label text uses markup }; namespace { /// Set "top" labels - the generic text at the top of each tab page. inline void app_set_top_labels(Gtk::Box* vbox, const std::vector& label_strings) { if (!vbox) return; // remove all first for (auto& w : vbox->get_children()) { vbox->remove(*w); delete w; // since it's without parent anymore, it won't be auto-deleted. } vbox->set_visible(!label_strings.empty()); if (label_strings.empty()) { // add one label only // Gtk::Label* label = Gtk::manage(new Gtk::Label("No data available", Gtk::ALIGN_START)); // label->set_padding(6, 0); // vbox->pack_start(*label, false, false); } else { // add one label per element for (const auto& label_string : label_strings) { std::string label_text = (label_string.markup ? Glib::ustring(label_string.label) : Glib::Markup::escape_text( label_string.label)); Gtk::Label* label = Gtk::manage(new Gtk::Label()); label->set_markup(label_text); label->set_padding(6, 0); label->set_alignment(Gtk::ALIGN_START); // label->set_ellipsize(Pango::ELLIPSIZE_END); label->set_selectable(true); label->set_can_focus(false); std::string fg; if (app_property_get_label_highlight_color(label_string.property->warning, fg)) label->set_markup("" + label_text + ""); vbox->pack_start(*label, false, false); // set it after packing, else the old tooltips api won't have anything to attach them to. app_gtkmm_set_widget_tooltip(*label, // label_text + "\n\n" + // add label text too, in case it's ellipsized label_string.property->get_description(), true); // already markupped label->show(); } } vbox->show_all_children(true); } /// Cell renderer functions for list cells inline void app_list_cell_renderer_func(Gtk::CellRenderer* cr, const Gtk::TreeModel::iterator& iter, Gtk::TreeModelColumn storage_column) { const StorageProperty* p = (*iter)[storage_column]; if (auto* crt = dynamic_cast(cr)) { std::string fg, bg; if (app_property_get_row_highlight_colors(p->warning, fg, bg)) { // Note: property_cell_background makes horizontal tree lines disappear around it, // but property_background doesn't play nice with sorted column color. crt->property_cell_background() = bg; crt->property_foreground() = fg; } else { // this is needed because cellrenderer is shared in column, so the previous call // may set the color for all subsequent cells. crt->property_cell_background().reset_value(); crt->property_foreground().reset_value(); } if (p->is_value_type() && p->get_value().is_header) { crt->property_weight() = Pango::WEIGHT_BOLD; } else { crt->property_weight().reset_value(); } } } /// Highlight a tab label according to \c warning inline void app_highlight_tab_label(Gtk::Widget* label_widget, WarningLevel warning, const Glib::ustring& original_label) { auto* label = dynamic_cast(label_widget); if (!label) return; if (warning == WarningLevel::none) { label->set_markup_with_mnemonic(original_label); return; } std::string fg; if (app_property_get_label_highlight_color(warning, fg)) label->set_markup_with_mnemonic("" + original_label + ""); } /// Scroll to appropriate error in text when row is selected in tree. inline void on_error_log_treeview_row_selected(GscInfoWindow* window, Gtk::TreeModelColumn mark_name_column) { auto* treeview = window->lookup_widget("error_log_treeview"); auto* textview = window->lookup_widget("error_log_textview"); Glib::RefPtr buffer; if (treeview && textview && (buffer = textview->get_buffer())) { Gtk::TreeModel::iterator iter = treeview->get_selection()->get_selected(); if (iter) { Glib::RefPtr mark = buffer->get_mark((*iter)[mark_name_column]); if (mark) textview->scroll_to(mark, 0., 0., 0.); } } } } GscInfoWindow::GscInfoWindow(BaseObjectType* gtkcobj, Glib::RefPtr ui) : AppBuilderWidget(gtkcobj, std::move(ui)) { // Size { int def_size_w = rconfig::get_data("gui/info_window/default_size_w"); int def_size_h = rconfig::get_data("gui/info_window/default_size_h"); if (def_size_w > 0 && def_size_h > 0) { set_default_size(def_size_w, def_size_h); } } // Create missing widgets auto* device_name_hbox = lookup_widget("device_name_label_hbox"); if (device_name_hbox) { device_name_label = Gtk::manage(new Gtk::Label(_("No data available"), Gtk::ALIGN_START)); device_name_label->set_selectable(true); device_name_label->show(); device_name_hbox->pack_start(*device_name_label, true, true); } // Connect callbacks Gtk::Button* refresh_info_button = nullptr; APP_BUILDER_AUTO_CONNECT(refresh_info_button, clicked); Gtk::Button* view_output_button = nullptr; APP_BUILDER_AUTO_CONNECT(view_output_button, clicked); Gtk::Button* save_info_button = nullptr; APP_BUILDER_AUTO_CONNECT(save_info_button, clicked); Gtk::Button* close_window_button = nullptr; APP_BUILDER_AUTO_CONNECT(close_window_button, clicked); Gtk::ComboBox* test_type_combo = nullptr; APP_BUILDER_AUTO_CONNECT(test_type_combo, changed); Gtk::Button* test_execute_button = nullptr; APP_BUILDER_AUTO_CONNECT(test_execute_button, clicked); Gtk::Button* test_stop_button = nullptr; APP_BUILDER_AUTO_CONNECT(test_stop_button, clicked); // Accelerators if (close_window_button) { close_window_button->add_accelerator("clicked", this->get_accel_group(), GDK_KEY_Escape, Gdk::ModifierType(0), Gtk::AccelFlags(0)); } // Context menu in treeviews { static const std::vector treeview_names { "attributes_treeview", "statistics_treeview", "selftest_log_treeview" }; for (const auto& treeview_name : treeview_names) { auto* treeview = lookup_widget(treeview_name); treeview_menus[treeview_name] = new Gtk::Menu(); // deleted in window destructor treeview->signal_button_press_event().connect( sigc::bind(sigc::bind(sigc::mem_fun(*this, &GscInfoWindow::on_treeview_button_press_event), treeview), treeview_menus[treeview_name]), false); // before Gtk::MenuItem* item = Gtk::manage(new Gtk::MenuItem(_("Copy Selected Data"), true)); item->signal_activate().connect( sigc::bind(sigc::mem_fun(*this, &GscInfoWindow::on_treeview_menu_copy_clicked), treeview) ); treeview_menus[treeview_name]->append(*item); treeview_menus[treeview_name]->show_all(); // Show all menu items when the menu pops up } } // --------------- // Set default texts on TextView-s, because glade's "text" property doesn't work // on them in gtkbuilder. if (auto* textview = lookup_widget("error_log_textview")) { Glib::RefPtr buffer = textview->get_buffer(); buffer->set_text("\n"s + _("No data available")); } if (auto* textview = lookup_widget("selective_selftest_log_textview")) { Glib::RefPtr buffer = textview->get_buffer(); buffer->set_text("\n"s + _("No data available")); } if (auto* textview = lookup_widget("temperature_log_textview")) { Glib::RefPtr buffer = textview->get_buffer(); buffer->set_text("\n"s + _("No data available")); } if (auto* textview = lookup_widget("erc_log_textview")) { Glib::RefPtr buffer = textview->get_buffer(); buffer->set_text("\n"s + _("No data available")); } if (auto* textview = lookup_widget("phy_log_textview")) { Glib::RefPtr buffer = textview->get_buffer(); buffer->set_text("\n"s + _("No data available")); } if (auto* textview = lookup_widget("directory_log_textview")) { Glib::RefPtr buffer = textview->get_buffer(); buffer->set_text("\n"s + _("No data available")); } // Save their original texts so that we can apply markup to them. Gtk::Label* tab_label = nullptr; tab_label = lookup_widget("general_tab_label"); tab_identity_name = (tab_label ? tab_label->get_label() : ""); tab_label = lookup_widget("attributes_tab_label"); tab_attributes_name = (tab_label ? tab_label->get_label() : ""); tab_label = lookup_widget("statistics_tab_label"); tab_statistics_name = (tab_label ? tab_label->get_label() : ""); tab_label = lookup_widget("test_tab_label"); tab_test_name = (tab_label ? tab_label->get_label() : ""); tab_label = lookup_widget("error_log_tab_label"); tab_error_log_name = (tab_label ? tab_label->get_label() : ""); tab_label = lookup_widget("temperature_log_tab_label"); tab_temperature_name = (tab_label ? tab_label->get_label() : ""); tab_label = lookup_widget("advanced_tab_label"); tab_advanced_name = (tab_label ? tab_label->get_label() : ""); tab_label = lookup_widget("capabilities_tab_label"); tab_capabilities_name = (tab_label ? tab_label->get_label() : ""); tab_label = lookup_widget("erc_tab_label"); tab_erc_name = (tab_label ? tab_label->get_label() : ""); tab_label = lookup_widget("selective_selftest_tab_label"); tab_selective_selftest_name = (tab_label ? tab_label->get_label() : ""); tab_label = lookup_widget("phy_tab_label"); tab_phy_name = (tab_label ? tab_label->get_label() : ""); tab_label = lookup_widget("directory_tab_label"); tab_directory_name = (tab_label ? tab_label->get_label() : ""); // show(); // don't show here, removing tabs later is ugly. } GscInfoWindow::~GscInfoWindow() { // Store window size. We don't store position to avoid overlaps. { int window_w = 0, window_h = 0; get_size(window_w, window_h); rconfig::set_data("gui/info_window/default_size_w", window_w); rconfig::set_data("gui/info_window/default_size_h", window_h); } for (auto& iter : treeview_menus) { delete iter.second; } } void GscInfoWindow::set_drive(StorageDevicePtr d) { if (drive) // if an old drive is present, disconnect our callback from it. drive_changed_connection.disconnect(); drive = std::move(d); drive_changed_connection = drive->signal_changed.connect(sigc::mem_fun(this, &GscInfoWindow::on_drive_changed)); } void GscInfoWindow::fill_ui_with_info(bool scan, bool clear_ui, bool clear_tests) { debug_out_info("app", DBG_FUNC_MSG << "Scan " << (scan ? "" : "not ") << "requested.\n"); if (clear_ui) clear_ui_info(clear_tests); if (!drive->get_is_virtual()) { // fetch all smartctl info, even if it already has it (to refresh it). if (scan) { std::shared_ptr ex(new SmartctlExecutorGui()); ex->create_running_dialog(this, Glib::ustring::compose(_("Running {command} on %1..."), drive->get_device_with_type())); std::string error_msg = drive->fetch_data_and_parse(ex); // run it with GUI support if (!error_msg.empty()) { gsc_executor_error_dialog_show(_("Cannot retrieve SMART data"), error_msg, this); return; } } } // disable refresh button if virtual if (drive->get_is_virtual()) { auto* b = lookup_widget("refresh_info_button"); if (b) { b->set_sensitive(false); app_gtkmm_set_widget_tooltip(*b, _("Cannot re-read information from virtual drive")); } } // hide all tabs except the first if smart is disabled, because they may contain // completely random data (smartctl does that sometimes). if (get_startup_settings().hide_tabs_on_smart_disabled) { bool smart_enabled = (drive->get_smart_status() == StorageDevice::Status::enabled); Gtk::Widget* note_page_box = nullptr; if ((note_page_box = lookup_widget("attributes_tab_vbox")) != nullptr) { if (smart_enabled) { note_page_box->show(); } else { note_page_box->hide(); } } if ((note_page_box = lookup_widget("statistics_tab_vbox")) != nullptr) { if (smart_enabled) { note_page_box->show(); } else { note_page_box->hide(); } } if ((note_page_box = lookup_widget("test_tab_vbox")) != nullptr) { if (smart_enabled) { note_page_box->show(); } else { note_page_box->hide(); } } if ((note_page_box = lookup_widget("error_log_tab_vbox")) != nullptr) { if (smart_enabled) { note_page_box->show(); } else { note_page_box->hide(); } } if ((note_page_box = lookup_widget("temperature_log_tab_vbox")) != nullptr) { if (smart_enabled) { note_page_box->show(); } else { note_page_box->hide(); } } if ((note_page_box = lookup_widget("advanced_tab_vbox")) != nullptr) { if (smart_enabled) { note_page_box->show(); } else { note_page_box->hide(); } } if (auto* notebook = lookup_widget("main_notebook")) { notebook->set_show_tabs(smart_enabled); } } // Top label - short device information { std::string device = Glib::Markup::escape_text(drive->get_device_with_type()); std::string model = Glib::Markup::escape_text(drive->get_model_name().empty() ? _("Unknown model") : drive->get_model_name()); std::string drive_letters = Glib::Markup::escape_text(drive->format_drive_letters(false)); /// Translators: %1 is device name, %2 is device model. this->set_title(Glib::ustring::compose(_("Device Information - %1: %2 - GSmartControl"), device, model)); // Gtk::Label* device_name_label = lookup_widget("device_name_label"); if (device_name_label) { /// Translators: %1 is device name, %2 is drive letters (if not empty), %3 is device model. device_name_label->set_markup(Glib::ustring::compose(_("Device: %1%2 Model: %3"), device, (drive_letters.empty() ? "" : (" (" + drive_letters + ")")), model)); } } // Fill the tabs with info // we need reference here - we take addresses of the elements const auto& props = drive->get_properties(); // it's a vector fill_ui_general(props); fill_ui_attributes(props); fill_ui_statistics(props); if (clear_tests) { fill_ui_self_test_info(); } fill_ui_self_test_log(props); fill_ui_error_log(props); fill_ui_temperature_log(props); // Advanced tab WarningLevel caps_warning_level = fill_ui_capabilities(props); WarningLevel errc_warning_level = fill_ui_error_recovery(props); WarningLevel selective_warning_level = fill_ui_selective_self_test_log(props); WarningLevel dir_warning_level = fill_ui_directory(props); WarningLevel phy_warning_level = fill_ui_physical(props); WarningLevel max_advanced_tab_warning = std::max({ caps_warning_level, errc_warning_level, selective_warning_level, dir_warning_level, phy_warning_level }); // Advanced tab label app_highlight_tab_label(lookup_widget("advanced_tab_label"), max_advanced_tab_warning, tab_advanced_name); } void GscInfoWindow::clear_ui_info(bool clear_tests_too) { // Note: We do NOT show/hide the notebook tabs here. // fill_ui_with_info() will do it all by itself. { this->set_title(_("Device Information - GSmartControl")); // Gtk::Label* device_name_label = lookup_widget("device_name_label"); if (device_name_label) device_name_label->set_text(_("No data available")); } { auto* identity_table = lookup_widget("identity_table"); if (identity_table) { // manually remove all children. without this visual corruption occurs. std::vector children = identity_table->get_children(); for (auto& widget : children) { identity_table->remove(*widget); } } // tab label app_highlight_tab_label(lookup_widget("general_tab_label"), WarningLevel::none, tab_identity_name); } { auto* label_vbox = lookup_widget("attributes_label_vbox"); app_set_top_labels(label_vbox, std::vector()); if (auto* treeview = lookup_widget("attributes_treeview")) { // Glib::RefPtr model = Glib::RefPtr::cast_dynamic(treeview->get_model()); // if (model) // model->clear(); treeview->remove_all_columns(); treeview->unset_model(); } // tab label app_highlight_tab_label(lookup_widget("attributes_tab_label"), WarningLevel::none, tab_attributes_name); } { auto* label_vbox = lookup_widget("statistics_label_vbox"); app_set_top_labels(label_vbox, std::vector()); if (auto treeview = lookup_widget("statistics_treeview")) { treeview->remove_all_columns(); treeview->unset_model(); } // tab label app_highlight_tab_label(lookup_widget("statistics_tab_label"), WarningLevel::none, tab_statistics_name); } { auto* label_vbox = lookup_widget("selftest_log_label_vbox"); app_set_top_labels(label_vbox, std::vector()); if (auto* treeview = lookup_widget("selftest_log_treeview")) { // Glib::RefPtr model = Glib::RefPtr::cast_dynamic(treeview->get_model()); // if (model) // model->clear(); treeview->remove_all_columns(); treeview->unset_model(); } if (clear_tests_too) { auto* test_type_combo = lookup_widget("test_type_combo"); if (test_type_combo) { test_type_combo->set_sensitive(false); // true if testing is possible and not active. // test_type_combo->clear(); // clear cellrenderers if (test_combo_model) test_combo_model->clear(); } if (auto* min_duration_label = lookup_widget("min_duration_label")) min_duration_label->set_text("N/A"); // set on test selection if (auto* test_execute_button = lookup_widget("test_execute_button")) test_execute_button->set_sensitive(false); // true if testing is possible and not active auto* test_description_textview = lookup_widget("test_description_textview"); if (test_description_textview && test_description_textview->get_buffer()) test_description_textview->get_buffer()->set_text(""); // set on test selection if (auto* test_completion_progressbar = lookup_widget("test_completion_progressbar")) { test_completion_progressbar->set_text(""); // set when test is run or completed test_completion_progressbar->set_sensitive(false); // set when test is run or completed test_completion_progressbar->hide(); } if (auto* test_stop_button = lookup_widget("test_stop_button")) { test_stop_button->set_sensitive(false); // true when test is active test_stop_button->hide(); } if (auto* test_result_hbox = lookup_widget("test_result_hbox")) test_result_hbox->hide(); // hide by default. show when test is completed. } // tab label app_highlight_tab_label(lookup_widget("test_tab_label"), WarningLevel::none, tab_test_name); } { auto* label_vbox = lookup_widget("error_log_label_vbox"); app_set_top_labels(label_vbox, std::vector()); auto* treeview = lookup_widget("error_log_treeview"); if (treeview) { // Glib::RefPtr model = Glib::RefPtr::cast_dynamic(treeview->get_model()); // if (model) // model->clear(); treeview->remove_all_columns(); treeview->unset_model(); } auto* textview = lookup_widget("error_log_textview"); if (textview) { // we re-create the buffer to get rid of all the Marks textview->set_buffer(Gtk::TextBuffer::create()); textview->get_buffer()->set_text("\n"s + _("No data available")); } // tab label app_highlight_tab_label(lookup_widget("error_log_tab_label"), WarningLevel::none, tab_error_log_name); } { auto* textview = lookup_widget("temperature_log_textview"); if (textview) { Glib::RefPtr buffer = textview->get_buffer(); buffer->set_text("\n"s + _("No data available")); } // tab label app_highlight_tab_label(lookup_widget("temperature_log_tab_label"), WarningLevel::none, tab_temperature_name); } // tab label app_highlight_tab_label(lookup_widget("advanced_tab_label"), WarningLevel::none, tab_advanced_name); { if (auto* treeview = lookup_widget("capabilities_treeview")) { // It's better to clear the model rather than unset it. If we unset it, we'll have // to deattach the callbacks too. But if we clear it, we have to remember column vars. // Glib::RefPtr model = Glib::RefPtr::cast_dynamic(treeview->get_model()); // if (model) // model->clear(); treeview->remove_all_columns(); treeview->unset_model(); } // tab label app_highlight_tab_label(lookup_widget("capabilities_tab_label"), WarningLevel::none, tab_capabilities_name); } { if (auto* textview = lookup_widget("erc_log_textview")) { textview->get_buffer()->set_text("\n"s + _("No data available")); } // tab label app_highlight_tab_label(lookup_widget("erc_tab_label"), WarningLevel::none, tab_erc_name); } { if (auto* textview = lookup_widget("selective_selftest_log_textview")) { textview->get_buffer()->set_text("\n"s + _("No data available")); } // tab label app_highlight_tab_label(lookup_widget("selective_selftest_tab_label"), WarningLevel::none, tab_selective_selftest_name); } { if (auto* textview = lookup_widget("phy_log_textview")) { textview->get_buffer()->set_text("\n"s + _("No data available")); } // tab label app_highlight_tab_label(lookup_widget("phy_tab_label"), WarningLevel::none, tab_phy_name); } { if (auto* textview = lookup_widget("directory_log_textview")) { textview->get_buffer()->set_text("\n"s + _("No data available")); } // tab label app_highlight_tab_label(lookup_widget("directory_tab_label"), WarningLevel::none, tab_directory_name); } } void GscInfoWindow::refresh_info(bool clear_tests_too) { this->set_sensitive(false); // make insensitive until filled. helps with pressed F5 problem. // this->clear_ui_info(); // no need, fill_ui_with_info() will call it. this->fill_ui_with_info(true, true, clear_tests_too); this->set_sensitive(true); // make sensitive again. } void GscInfoWindow::show_tests() { if (auto* book = lookup_widget("main_notebook")) book->set_current_page(3); // the Tests tab } bool GscInfoWindow::on_delete_event([[maybe_unused]] GdkEventAny* e) { on_close_window_button_clicked(); return true; // event handled } void GscInfoWindow::on_refresh_info_button_clicked() { this->refresh_info(); } void GscInfoWindow::on_view_output_button_clicked() { GscTextWindow* win = GscTextWindow::create(); // make save visible and enable monospace font std::string output = this->drive->get_full_output(); if (output.empty()) { output = this->drive->get_info_output(); } win->set_text(_("Smartctl Output"), output, true, true); std::string filename = drive->get_save_filename(); if (!filename.empty()) win->set_save_filename(filename); win->show(); } void GscInfoWindow::on_save_info_button_clicked() { static std::string last_dir; if (last_dir.empty()) { last_dir = rconfig::get_data("gui/drive_data_open_save_dir"); } int result = 0; std::string filename = drive->get_save_filename(); 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) hz::scoped_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()); if (!filename.empty()) gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog.get()), filename.c_str()); 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); if (!filename.empty()) dialog.set_current_name(filename); // 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"; } std::string data = this->drive->get_full_output(); if (data.empty()) { data = this->drive->get_info_output(); } std::error_code ec = hz::fs_file_put_contents(hz::fs::u8path(file), data); if (ec) { gui_show_error_dialog(_("Cannot save SMART 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 GscInfoWindow::on_close_window_button_clicked() { if (drive && drive->get_test_is_active()) { // disallow close if test is active. gui_show_warn_dialog(_("Please wait until all tests are finished."), this); } else { destroy(this); // deletes this object and nullifies instance } } void GscInfoWindow::on_test_type_combo_changed() { auto* test_type_combo = lookup_widget("test_type_combo"); Gtk::TreeRow row = *(test_type_combo->get_active()); if (row) { std::shared_ptr test = row[test_combo_col_self_test]; //debug_out_error("app", test->get_min_duration_seconds() << "\n"); if (auto* min_duration_label = lookup_widget("min_duration_label")) { auto duration = test->get_min_duration_seconds(); min_duration_label->set_text(duration == std::chrono::seconds(-1) ? C_("duration", "N/A") : (duration.count() == 0 ? C_("duration", "Unknown") : hz::format_time_length(duration))); } auto* test_description_textview = lookup_widget("test_description_textview"); if (test_description_textview && test_description_textview->get_buffer()) test_description_textview->get_buffer()->set_text(row[test_combo_col_description]); } } void GscInfoWindow::fill_ui_general(const std::vector& props) { // filter out some properties std::vector id_props, version_props, health_props; for (auto&& p : props) { if (p.section == StorageProperty::Section::info) { if (p.generic_name == "smartctl_version_full") { version_props.push_back(p); } else if (p.generic_name == "smartctl_version") { continue; // we use the full version string instead. } else { id_props.push_back(p); } } else if (p.section == StorageProperty::Section::data && p.subsection == StorageProperty::SubSection::health) { health_props.push_back(p); } } // put version after all the info for (auto&& p : version_props) id_props.push_back(p); // health is the last one for (auto&& p : health_props) id_props.push_back(p); auto* identity_table = lookup_widget("identity_table"); identity_table->hide(); WarningLevel max_tab_warning = WarningLevel::none; int row = 0; for (auto&& p : id_props) { if (!p.show_in_ui) { continue; // hide debug messages from smartctl } if (p.generic_name == "overall_health") { // a little distance for this one Gtk::Label* empty_label = Gtk::manage(new Gtk::Label()); empty_label->set_can_focus(false); identity_table->attach(*empty_label, 0, row, 2, 1); ++row; } Gtk::Label* name = Gtk::manage(new Gtk::Label()); // name->set_ellipsize(Pango::ELLIPSIZE_END); name->set_alignment(Gtk::ALIGN_END); // right-align name->set_selectable(true); name->set_can_focus(false); name->set_markup("" + Glib::Markup::escape_text(p.displayable_name) + ""); // If the above is Label, then this has to be Label too, else it will shrink // and "name" will take most of the horizontal space. If "name" is set to shrink, // then it stops being right-aligned. Gtk::Label* value = Gtk::manage(new Gtk::Label()); // value->set_ellipsize(Pango::ELLIPSIZE_END); value->set_alignment(Gtk::ALIGN_START); // left-align value->set_selectable(true); value->set_can_focus(false); value->set_markup(Glib::Markup::escape_text(p.format_value())); std::string fg; if (app_property_get_label_highlight_color(p.warning, fg)) { name->set_markup("" + name->get_label() + ""); value->set_markup("" + value->get_label() + ""); } identity_table->attach(*name, 0, row, 1, 1); identity_table->attach(*value, 1, row, 1, 1); app_gtkmm_set_widget_tooltip(*name, p.get_description(), true); app_gtkmm_set_widget_tooltip(*value, // value->get_label() + "\n\n" + p.get_description(), true); if (int(p.warning) > int(max_tab_warning)) max_tab_warning = p.warning; ++row; } identity_table->show_all(); // tab label app_highlight_tab_label(lookup_widget("general_tab_label"), max_tab_warning, tab_identity_name); } void GscInfoWindow::fill_ui_attributes(const std::vector& props) { auto* treeview = lookup_widget("attributes_treeview"); Gtk::TreeModelColumnRecord model_columns; int num_tree_cols = 0; // ID (int), Name, Flag (hex), Normalized Value (uint8), Worst (uint8), Thresh (uint8), Raw (int64), Type (string), // Updated (string), When Failed (string) Gtk::TreeModelColumn col_id; model_columns.add(col_id); // we can use the column variable by value after this. num_tree_cols = app_gtkmm_create_tree_view_column(col_id, *treeview, _("ID"), _("Attribute ID"), true); Gtk::TreeModelColumn col_name; model_columns.add(col_name); num_tree_cols = app_gtkmm_create_tree_view_column(col_name, *treeview, _("Name"), _("Attribute name (this is deduced from ID by smartctl and may be incorrect, as it's highly vendor-specific)"), true); treeview->set_search_column(col_name.index()); auto* cr_name = dynamic_cast(treeview->get_column_cell_renderer(num_tree_cols - 1)); if (cr_name) cr_name->property_weight() = Pango::WEIGHT_BOLD; Gtk::TreeModelColumn col_failed; model_columns.add(col_failed); num_tree_cols = app_gtkmm_create_tree_view_column(col_failed, *treeview, _("Failed"), _("When failed (that is, the normalized value became equal to or less than threshold)"), true, true); Gtk::TreeModelColumn col_value; model_columns.add(col_value); num_tree_cols = app_gtkmm_create_tree_view_column(col_value, *treeview, C_("value", "Normalized"), _("Normalized value (highly vendor-specific; converted from Raw value by the drive's firmware)"), false); Gtk::TreeModelColumn col_worst; model_columns.add(col_worst); num_tree_cols = app_gtkmm_create_tree_view_column(col_worst, *treeview, C_("value", "Worst"), _("The worst normalized value recorded for this attribute during the drive's lifetime (with SMART enabled)"), false); Gtk::TreeModelColumn col_threshold; model_columns.add(col_threshold); num_tree_cols = app_gtkmm_create_tree_view_column(col_threshold, *treeview, C_("value", "Threshold"), _("Threshold for normalized value. Normalized value should be greater than threshold (unless vendor thinks otherwise)."), false); Gtk::TreeModelColumn col_raw; model_columns.add(col_raw); num_tree_cols = app_gtkmm_create_tree_view_column(col_raw, *treeview, _("Raw value"), _("Raw value as reported by drive. May or may not be sensible."), false); Gtk::TreeModelColumn col_type; model_columns.add(col_type); num_tree_cols = app_gtkmm_create_tree_view_column(col_type, *treeview, _("Type"), _("Alarm condition is reached when normalized value becomes less than or equal to threshold. Type indicates whether it's a signal of drive's pre-failure time or just an old age."), false, true); // Doesn't carry that much info. Advanced users can look at the flags. // Gtk::TreeModelColumn col_updated; // model_columns.add(col_updated); // num_tree_cols = app_gtkmm_create_tree_view_column(col_updated, *treeview, // "Updated", "The attribute is usually updated continuously, or during Offline Data Collection only. This column indicates that.", true); Gtk::TreeModelColumn col_flag_value; model_columns.add(col_flag_value); num_tree_cols = app_gtkmm_create_tree_view_column(col_flag_value, *treeview, _("Flags"), _("Flags") + "\n\n"s + Glib::ustring::compose(_("If given in %1 format, the presence of each letter indicates that the flag is on."), "POSRCK+") + "\n" + _("P: pre-failure attribute (if the attribute failed, the drive is failing)") + "\n" + _("O: updated continuously (as opposed to updated on offline data collection)") + "\n" + _("S: speed / performance attribute") + "\n" + _("R: error rate") + "\n" + _("C: event count") + "\n" + _("K: auto-keep") + "\n" + _("+: undocumented bits present"), false); Gtk::TreeModelColumn col_tooltip; model_columns.add(col_tooltip); treeview->set_tooltip_column(col_tooltip.index()); Gtk::TreeModelColumn col_storage; model_columns.add(col_storage); // create a TreeModel (ListStore) Glib::RefPtr list_store = Gtk::ListStore::create(model_columns); list_store->set_sort_column(col_id, Gtk::SORT_ASCENDING); // default sort treeview->set_model(list_store); for (int i = 0; i < num_tree_cols; ++i) { Gtk::TreeViewColumn* tcol = treeview->get_column(i); tcol->set_cell_data_func(*(tcol->get_first_cell()), sigc::bind(sigc::ptr_fun(app_list_cell_renderer_func), col_storage)); } WarningLevel max_tab_warning = WarningLevel::none; std::vector label_strings; // outside-of-tree properties for (const auto& p : props) { if (p.section != StorageProperty::Section::data || p.subsection != StorageProperty::SubSection::attributes) continue; // add non-attribute-type properties to label above if (!p.is_value_type()) { label_strings.emplace_back(p.displayable_name + ": " + p.format_value(), &p); if (int(p.warning) > int(max_tab_warning)) max_tab_warning = p.warning; continue; } const auto& attr = p.get_value(); std::string attr_type = StorageAttribute::get_attr_type_name(attr.attr_type); if (attr.attr_type == StorageAttribute::AttributeType::prefail) attr_type = "" + attr_type + ""; std::string fail_time = StorageAttribute::get_fail_time_name(attr.when_failed); if (attr.when_failed != StorageAttribute::FailTime::none) fail_time = "" + fail_time + ""; Gtk::TreeRow row = *(list_store->append()); row[col_id] = attr.id; row[col_name] = p.displayable_name; row[col_flag_value] = attr.flag; // it's a string, not int. row[col_value] = (attr.value.has_value() ? hz::number_to_string_locale(attr.value.value()) : "-"); row[col_worst] = (attr.worst.has_value() ? hz::number_to_string_locale(attr.worst.value()) : "-"); row[col_threshold] = (attr.threshold.has_value() ? hz::number_to_string_locale(attr.threshold.value()) : "-"); row[col_raw] = attr.format_raw_value(); row[col_type] = attr_type; // row[col_updated] = StorageAttribute::get_update_type_name(attr.update_type); row[col_failed] = fail_time; row[col_tooltip] = p.get_description(); row[col_storage] = &p; if (int(p.warning) > int(max_tab_warning)) max_tab_warning = p.warning; } auto* label_vbox = lookup_widget("attributes_label_vbox"); app_set_top_labels(label_vbox, label_strings); // tab label app_highlight_tab_label(lookup_widget("attributes_tab_label"), max_tab_warning, tab_attributes_name); } void GscInfoWindow::fill_ui_statistics(const std::vector& props) { auto* treeview = lookup_widget("statistics_treeview"); Gtk::TreeModelColumnRecord model_columns; int num_tree_cols = 0; Gtk::TreeModelColumn col_description; model_columns.add(col_description); num_tree_cols = app_gtkmm_create_tree_view_column(col_description, *treeview, _("Description"), _("Entry description"), true); treeview->set_search_column(col_description.index()); // Gtk::CellRendererText* cr_name = dynamic_cast(treeview->get_column_cell_renderer(num_tree_cols - 1)); // if (cr_name) // cr_name->property_weight() = Pango::WEIGHT_BOLD ; Gtk::TreeModelColumn col_value; model_columns.add(col_value); num_tree_cols = app_gtkmm_create_tree_view_column(col_value, *treeview, _("Value"), Glib::ustring::compose(_("Value (can be normalized if '%1' flag is present)"), "N"), false); Gtk::TreeModelColumn col_flags; model_columns.add(col_flags); num_tree_cols = app_gtkmm_create_tree_view_column(col_flags, *treeview, _("Flags"), _("Flags") + "\n\n"s + _("N: value is normalized") + "\n" + _("D: supports Device Statistics Notification (DSN)") + "\n" + _("C: monitored condition met") + "\n" // Related to DSN? From the specification, it looks like something user-controllable. + _("+: undocumented bits present"), false); Gtk::TreeModelColumn col_page_offset; model_columns.add(col_page_offset); num_tree_cols = app_gtkmm_create_tree_view_column(col_page_offset, *treeview, _("Page, Offset"), _("Page and offset of the entry"), false); Gtk::TreeModelColumn col_tooltip; model_columns.add(col_tooltip); treeview->set_tooltip_column(col_tooltip.index()); Gtk::TreeModelColumn col_storage; model_columns.add(col_storage); // create a TreeModel (ListStore) Glib::RefPtr list_store = Gtk::ListStore::create(model_columns); treeview->set_model(list_store); // No sorting (we don't want to screw up the headers). for (int i = 0; i < num_tree_cols; ++i) { Gtk::TreeViewColumn* tcol = treeview->get_column(i); tcol->set_cell_data_func(*(tcol->get_first_cell()), sigc::bind(sigc::ptr_fun(app_list_cell_renderer_func), col_storage)); } WarningLevel max_tab_warning = WarningLevel::none; std::vector label_strings; // outside-of-tree properties for (const auto& p : props) { if (p.section != StorageProperty::Section::data || p.subsection != StorageProperty::SubSection::devstat) continue; // add non-entry-type properties to label above if (!p.is_value_type()) { label_strings.emplace_back(p.displayable_name + ": " + p.format_value(), &p); if (int(p.warning) > int(max_tab_warning)) max_tab_warning = p.warning; continue; } Gtk::TreeRow row = *(list_store->append()); const auto& st = p.get_value(); row[col_description] = (st.is_header ? p.displayable_name : (" " + p.displayable_name)); row[col_value] = st.format_value(); row[col_flags] = st.flags; // it's a string, not int. row[col_page_offset] = (st.is_header ? std::string() : hz::string_sprintf("0x%02x, 0x%03x", int(st.page), int(st.offset))); row[col_tooltip] = p.get_description(); row[col_storage] = &p; if (int(p.warning) > int(max_tab_warning)) max_tab_warning = p.warning; } auto* label_vbox = lookup_widget("statistics_label_vbox"); app_set_top_labels(label_vbox, label_strings); // tab label app_highlight_tab_label(lookup_widget("statistics_tab_label"), max_tab_warning, tab_statistics_name); } void GscInfoWindow::fill_ui_self_test_info() { auto* test_type_combo = lookup_widget("test_type_combo"); // don't check with get_model(), it comes pre-modeled from glade. if (!test_combo_model) { Gtk::TreeModelColumnRecord model_columns; // Test name, [description], [selftest_obj] model_columns.add(test_combo_col_name); // we can use the column variable by value after this. model_columns.add(test_combo_col_description); model_columns.add(test_combo_col_self_test); test_combo_model = Gtk::ListStore::create(model_columns); test_type_combo->set_model(test_combo_model); // visible columns test_type_combo->clear(); // clear old (glade) cellrenderers test_type_combo->pack_start(test_combo_col_name); } // add possible tests Gtk::TreeModel::Row row; auto test_ioffline = std::make_shared(drive, SelfTest::TestType::immediate_offline); if (test_ioffline->is_supported()) { row = *(test_combo_model->append()); row[test_combo_col_name] = SelfTest::get_test_displayable_name(SelfTest::TestType::immediate_offline); row[test_combo_col_description] = _("Immediate Offline Test (also known as Immediate Offline Data Collection)" " is the manual version of Automatic Offline Data Collection, which, if enabled, is automatically run" " every four hours. If an error occurs during this test, it will be reported in Error Log. Besides that," " its effects are visible only in that it updates the \"Offline\" Attribute values."); row[test_combo_col_self_test] = test_ioffline; } auto test_short = std::make_shared(drive, SelfTest::TestType::short_test); if (test_short->is_supported()) { row = *(test_combo_model->append()); row[test_combo_col_name] = SelfTest::get_test_displayable_name(SelfTest::TestType::short_test); row[test_combo_col_description] = _("Short self-test consists of a collection of test routines that have the highest chance" " of detecting drive problems. Its result is reported in the Self-Test Log." " Note that this test is in no way comprehensive. Its main purpose is to detect totally damaged" " drives without running a full surface scan." "\nNote: On some drives this actually runs several consequent tests, which may" " cause the program to display the test progress incorrectly."); // seagate multi-pass test on 7200.11. row[test_combo_col_self_test] = test_short; } auto test_long = std::make_shared(drive, SelfTest::TestType::long_test); if (test_long->is_supported()) { row = *(test_combo_model->append()); row[test_combo_col_name] = SelfTest::get_test_displayable_name(SelfTest::TestType::long_test); row[test_combo_col_description] = _("Extended self-test examines complete disk surface and performs various test routines" " built into the drive. Its result is reported in the Self-Test Log."); row[test_combo_col_self_test] = test_long; } auto test_conveyance = std::make_shared(drive, SelfTest::TestType::conveyance); if (test_conveyance->is_supported()) { row = *(test_combo_model->append()); row[test_combo_col_name] = SelfTest::get_test_displayable_name(SelfTest::TestType::conveyance); row[test_combo_col_description] = _("Conveyance self-test is intended to identify damage incurred during transporting of the drive."); row[test_combo_col_self_test] = test_conveyance; } if (!test_combo_model->children().empty()) { test_type_combo->set_sensitive(true); test_type_combo->set_active(test_combo_model->children().begin()); // select first entry // At least one test is possible, so enable test button. // Note: we disable only Execute button on virtual drives. The combo is left // sensitive so that the user can see which tests are supported by the drive. auto* test_execute_button = lookup_widget("test_execute_button"); if (test_execute_button) test_execute_button->set_sensitive(!drive->get_is_virtual()); } } void GscInfoWindow::fill_ui_self_test_log(const std::vector& props) { auto* treeview = lookup_widget("selftest_log_treeview"); Gtk::TreeModelColumnRecord model_columns; int num_tree_cols = 0; // Test num., Type, Status, % Completed, Lifetime hours, LBA of the first error Gtk::TreeModelColumn col_num; model_columns.add(col_num); // we can use the column variable by value after this. num_tree_cols = app_gtkmm_create_tree_view_column(col_num, *treeview, _("Test #"), _("Test # (greater may mean newer or older depending on drive model)"), true); auto* cr_test_num = dynamic_cast(treeview->get_column_cell_renderer(num_tree_cols - 1)); if (cr_test_num) cr_test_num->property_weight() = Pango::WEIGHT_BOLD ; Gtk::TreeModelColumn col_type; model_columns.add(col_type); num_tree_cols = app_gtkmm_create_tree_view_column(col_type, *treeview, _("Type"), _("Type of the test performed"), true); treeview->set_search_column(col_type.index()); Gtk::TreeModelColumn col_status; model_columns.add(col_status); num_tree_cols = app_gtkmm_create_tree_view_column(col_status, *treeview, _("Status"), _("Test completion status"), true); Gtk::TreeModelColumn col_percent; model_columns.add(col_percent); num_tree_cols = app_gtkmm_create_tree_view_column(col_percent, *treeview, _("% Completed"), _("Percentage of the test completed. Instantly-aborted tests have 10%, while unsupported ones may have 100%."), true, false, true); Gtk::TreeModelColumn col_hours; model_columns.add(col_hours); num_tree_cols = app_gtkmm_create_tree_view_column(col_hours, *treeview, _("Lifetime hours"), _("During which hour of the drive's (powered on) lifetime did the test complete (or abort)"), true); Gtk::TreeModelColumn col_lba; model_columns.add(col_lba); num_tree_cols = app_gtkmm_create_tree_view_column(col_lba, *treeview, _("LBA of the first error"), _("LBA of the first error (if an LBA-related error happened)"), true); Gtk::TreeModelColumn col_tooltip; model_columns.add(col_tooltip); treeview->set_tooltip_column(col_tooltip.index()); Gtk::TreeModelColumn col_storage; model_columns.add(col_storage); // create a TreeModel (ListStore) Glib::RefPtr list_store = Gtk::ListStore::create(model_columns); list_store->set_sort_column(col_num, Gtk::SORT_ASCENDING); // default sort treeview->set_model(list_store); for (int i = 0; i < num_tree_cols; ++i) { Gtk::TreeViewColumn* tcol = treeview->get_column(i); tcol->set_cell_data_func(*(tcol->get_first_cell()), sigc::bind(sigc::ptr_fun(app_list_cell_renderer_func), col_storage)); } WarningLevel max_tab_warning = WarningLevel::none; std::vector label_strings; // outside-of-tree properties for (auto&& p : props) { if (p.section != StorageProperty::Section::data || p.subsection != StorageProperty::SubSection::selftest_log) continue; if (p.generic_name == "selftest_log") // the whole section, we don't need it continue; // add non-entry properties to label above if (!p.is_value_type()) { label_strings.emplace_back(p.displayable_name + ": " + p.format_value(), &p); if (int(p.warning) > int(max_tab_warning)) max_tab_warning = p.warning; continue; } Gtk::TreeRow row = *(list_store->append()); const auto& sse = p.get_value(); row[col_num] = sse.test_num; row[col_type] = sse.type; row[col_status] = sse.get_status_str(); row[col_percent] = hz::number_to_string_locale(100 - sse.remaining_percent) + "%"; row[col_hours] = sse.format_lifetime_hours(); row[col_lba] = sse.lba_of_first_error; // There are no descriptions in self-test log entries, so don't display // "No description available" for all of them. // row[col_tooltip] = p.get_description(); row[col_storage] = &p; if (int(p.warning) > int(max_tab_warning)) max_tab_warning = p.warning; } auto* label_vbox = lookup_widget("selftest_log_label_vbox"); app_set_top_labels(label_vbox, label_strings); // tab label app_highlight_tab_label(lookup_widget("test_tab_label"), max_tab_warning, tab_test_name); } void GscInfoWindow::fill_ui_error_log(const std::vector& props) { auto* treeview = lookup_widget("error_log_treeview"); Gtk::TreeModelColumnRecord model_columns; int num_tree_cols = 0; // Error Number, Lifetime Hours, State, Type, Details, [tooltips] Gtk::TreeModelColumn col_num; model_columns.add(col_num); // we can use the column variable by value after this. num_tree_cols = app_gtkmm_create_tree_view_column(col_num, *treeview, _("Error #"), _("Error # in the error log (greater means newer)"), true); if (auto* cr_name = dynamic_cast(treeview->get_column_cell_renderer(num_tree_cols - 1))) cr_name->property_weight() = Pango::WEIGHT_BOLD ; Gtk::TreeModelColumn col_hours; model_columns.add(col_hours); num_tree_cols = app_gtkmm_create_tree_view_column(col_hours, *treeview, _("Lifetime hours"), _("During which hour of the drive's (powered on) lifetime did the error happen."), true); Gtk::TreeModelColumn col_state; model_columns.add(col_state); num_tree_cols = app_gtkmm_create_tree_view_column(col_state, *treeview, C_("power", "State"), _("Power state of the drive when the error occurred"), false); Gtk::TreeModelColumn col_type; model_columns.add(col_type); num_tree_cols = app_gtkmm_create_tree_view_column(col_type, *treeview, _("Type"), _("Type of error"), true); Gtk::TreeModelColumn col_details; model_columns.add(col_details); num_tree_cols = app_gtkmm_create_tree_view_column(col_details, *treeview, _("Details"), _("Additional details (e.g. LBA where the error occurred, etc...)"), true); Gtk::TreeModelColumn col_tooltip; model_columns.add(col_tooltip); treeview->set_tooltip_column(col_tooltip.index()); Gtk::TreeModelColumn col_storage; model_columns.add(col_storage); Gtk::TreeModelColumn col_mark_name; model_columns.add(col_mark_name); // create a TreeModel (ListStore) Glib::RefPtr list_store = Gtk::ListStore::create(model_columns); list_store->set_sort_column(col_num, Gtk::SORT_DESCENDING); // default sort treeview->set_model(list_store); for (int i = 0; i < num_tree_cols; ++i) { Gtk::TreeViewColumn* tcol = treeview->get_column(i); tcol->set_cell_data_func(*(tcol->get_first_cell()), sigc::bind(sigc::ptr_fun(app_list_cell_renderer_func), col_storage)); } WarningLevel max_tab_warning = WarningLevel::none; std::vector label_strings; // outside-of-tree properties for (auto&& p : props) { if (p.section != StorageProperty::Section::data || p.subsection != StorageProperty::SubSection::error_log) continue; // Note: Don't use property description as a tooltip here. It won't be available if there's no property. if (p.generic_name == "error_log") { if (auto* textview = lookup_widget("error_log_textview")) { // Add complete error log to textview window. Glib::RefPtr buffer = textview->get_buffer(); if (buffer) { buffer->set_text("\n" + Glib::ustring::compose(_("Complete error log: %1"), "\n\n" + p.get_value())); // set marks so we can scroll to them if (!error_log_row_selected_conn.connected()) { // avoid double-connect error_log_row_selected_conn = treeview->get_selection()->signal_changed().connect( sigc::bind(sigc::bind(sigc::ptr_fun(on_error_log_treeview_row_selected), col_mark_name), this)); } Gtk::TextIter titer = buffer->begin(); Gtk::TextIter match_start, match_end; // TODO Change this for json while (titer.forward_search("\nError ", Gtk::TEXT_SEARCH_TEXT_ONLY, match_start, match_end)) { match_start.forward_char(); // place after newline match_end.forward_word_end(); // include error number titer = match_end; // continue searching from here Glib::ustring mark_name = match_start.get_slice(match_end); // e.g. "Error 3" buffer->create_mark(mark_name, titer); } } } // add non-tree properties to label above } else if (!p.is_value_type()) { label_strings.emplace_back(p.displayable_name + ": " + p.format_value(), &p); if (p.generic_name == "error_log_error_count") label_strings.back().label += " "s + _("(Note: The number of entries may be limited to the newest ones)"); } else { const auto& eb = p.get_value(); std::string type_details = eb.type_more_info; Gtk::TreeRow row = *(list_store->append()); row[col_num] = eb.error_num; row[col_hours] = eb.format_lifetime_hours(); row[col_state] = eb.device_state; row[col_type] = StorageErrorBlock::get_displayable_error_types(eb.reported_types); row[col_details] = (type_details.empty() ? "-" : type_details); // e.g. OBS has no details // There are no descriptions in self-test log entries, so don't display // "No description available" for all of them. row[col_tooltip] = p.get_description(); row[col_storage] = &p; row[col_mark_name] = Glib::ustring::compose(_("Error %1"), eb.error_num); } if (int(p.warning) > int(max_tab_warning)) max_tab_warning = p.warning; } auto* label_vbox = lookup_widget("error_log_label_vbox"); app_set_top_labels(label_vbox, label_strings); // inner tab label app_highlight_tab_label(lookup_widget("error_log_tab_label"), max_tab_warning, tab_error_log_name); } void GscInfoWindow::fill_ui_temperature_log(const std::vector& props) { auto* textview = lookup_widget("temperature_log_textview"); WarningLevel max_tab_warning = WarningLevel::none; std::vector label_strings; // outside-of-tree properties std::string temperature; StorageProperty temp_property; enum { temp_attr2 = 1, temp_attr1, temp_stat, temp_sct }; // less important to more important int temp_prop_source = 0; for (const auto& p : props) { // Find temperature if (temp_prop_source < temp_sct && p.generic_name == "sct_temperature_celsius") { temperature = hz::number_to_string_locale(p.get_value()); temp_property = p; temp_prop_source = temp_sct; } if (temp_prop_source < temp_stat && p.generic_name == "stat_temperature_celsius") { temperature = hz::number_to_string_locale(p.get_value().value_int); temp_property = p; temp_prop_source = temp_stat; } if (temp_prop_source < temp_attr1 && p.generic_name == "attr_temperature_celsius") { temperature = hz::number_to_string_locale(p.get_value().raw_value_int); temp_property = p; temp_prop_source = temp_attr1; } if (temp_prop_source < temp_attr2 && p.generic_name == "attr_temperature_celsius_x10") { temperature = hz::number_to_string_locale(p.get_value().raw_value_int / 10); temp_property = p; temp_prop_source = temp_attr2; } if (p.section != StorageProperty::Section::data || p.subsection != StorageProperty::SubSection::temperature_log) continue; if (p.generic_name == "sct_unsupported" && p.get_value()) { // only show if unsupported label_strings.emplace_back(_("SCT temperature commands not supported."), &p); if (int(p.warning) > int(max_tab_warning)) max_tab_warning = p.warning; continue; } // Note: Don't use property description as a tooltip here. It won't be available if there's no property. if (p.generic_name == "scttemp_log") { Glib::RefPtr buffer = textview->get_buffer(); buffer->set_text("\n" + Glib::ustring::compose(_("Complete SCT temperature log: %1"), "\n\n" + p.get_value())); } } if (temperature.empty()) { temperature = C_("value", "Unknown"); } else { temperature = Glib::ustring::compose(C_("temperature", "%1 C"), temperature); } temp_property.set_description(_("Current drive temperature in Celsius.")); // overrides attribute description label_strings.emplace_back(Glib::ustring::compose(_("Current temperature: %1"), "" + temperature + ""), &temp_property, true); if (int(temp_property.warning) > int(max_tab_warning)) max_tab_warning = temp_property.warning; auto* label_vbox = lookup_widget("temperature_log_label_vbox"); app_set_top_labels(label_vbox, label_strings); // tab label app_highlight_tab_label(lookup_widget("temperature_log_tab_label"), max_tab_warning, tab_temperature_name); } WarningLevel GscInfoWindow::fill_ui_capabilities(const std::vector& props) { auto* treeview = lookup_widget("capabilities_treeview"); Gtk::TreeModelColumnRecord model_columns; int num_tree_cols = 0; // N, Name, Flag, Capabilities, [tooltips] Gtk::TreeModelColumn col_index; model_columns.add(col_index); // we can use the column variable by value after this. num_tree_cols = app_gtkmm_create_tree_view_column(col_index, *treeview, _("#"), _("Entry #"), true); Gtk::TreeModelColumn col_name; model_columns.add(col_name); num_tree_cols = app_gtkmm_create_tree_view_column(col_name, *treeview, _("Name"), _("Name"), true); treeview->set_search_column(col_name.index()); auto cr_name = dynamic_cast(treeview->get_column_cell_renderer(num_tree_cols - 1)); if (cr_name) cr_name->property_weight() = Pango::WEIGHT_BOLD ; Gtk::TreeModelColumn col_flag_value; model_columns.add(col_flag_value); num_tree_cols = app_gtkmm_create_tree_view_column(col_flag_value, *treeview, _("Flags"), _("Flags"), false); Gtk::TreeModelColumn col_str_values; model_columns.add(col_str_values); num_tree_cols = app_gtkmm_create_tree_view_column(col_str_values, *treeview, _("Capabilities"), _("Capabilities"), false); Gtk::TreeModelColumn col_tooltip; model_columns.add(col_tooltip); treeview->set_tooltip_column(col_tooltip.index()); Gtk::TreeModelColumn col_storage; model_columns.add(col_storage); // create a TreeModel (ListStore) Glib::RefPtr list_store = Gtk::ListStore::create(model_columns); list_store->set_sort_column(col_index, Gtk::SORT_ASCENDING); // default sort treeview->set_model(list_store); for (int i = 0; i < num_tree_cols; ++i) { Gtk::TreeViewColumn* tcol = treeview->get_column(i); tcol->set_cell_data_func(*(tcol->get_first_cell()), sigc::bind(sigc::ptr_fun(app_list_cell_renderer_func), col_storage)); } WarningLevel max_tab_warning = WarningLevel::none; int index = 1; for (auto&& p : props) { if (p.section != StorageProperty::Section::data || p.subsection != StorageProperty::SubSection::capabilities) continue; Glib::ustring name = p.displayable_name; std::string flag_value; Glib::ustring str_value; if (p.is_value_type()) { flag_value = hz::number_to_string_nolocale(p.get_value().flag_value, 16); // 0xXX str_value = hz::string_join(p.get_value().strvalues, "\n"); } else { // no flag value here str_value = p.format_value(); } Gtk::TreeRow row = *(list_store->append()); row[col_index] = index; row[col_name] = name; row[col_flag_value] = (flag_value.empty() ? "-" : flag_value); row[col_str_values] = str_value; row[col_tooltip] = p.get_description(); row[col_storage] = &p; if (int(p.warning) > int(max_tab_warning)) max_tab_warning = p.warning; ++index; } // tab label app_highlight_tab_label(lookup_widget("capabilities_tab_label"), max_tab_warning, tab_capabilities_name); return max_tab_warning; } WarningLevel GscInfoWindow::fill_ui_error_recovery(const std::vector& props) { auto* textview = lookup_widget("erc_log_textview"); WarningLevel max_tab_warning = WarningLevel::none; for (auto&& p : props) { if (p.section != StorageProperty::Section::data || p.subsection != StorageProperty::SubSection::erc_log) continue; // Note: Don't use property description as a tooltip here. It won't be available if there's no property. if (p.generic_name == "scterc_log") { Glib::RefPtr buffer = textview->get_buffer(); buffer->set_text("\n" + Glib::ustring::compose(_("Complete SCT Error Recovery Control settings: %1"), "\n\n" + p.get_value())); } } // tab label app_highlight_tab_label(lookup_widget("erc_tab_label"), max_tab_warning, tab_erc_name); return max_tab_warning; } WarningLevel GscInfoWindow::fill_ui_selective_self_test_log(const std::vector& props) { auto* textview = lookup_widget("selective_selftest_log_textview"); WarningLevel max_tab_warning = WarningLevel::none; for (auto&& p : props) { if (p.section != StorageProperty::Section::data || p.subsection != StorageProperty::SubSection::selective_selftest_log) continue; // Note: Don't use property description as a tooltip here. It won't be available if there's no property. if (p.generic_name == "SubSection::selective_selftest_log") { Glib::RefPtr buffer = textview->get_buffer(); buffer->set_text("\n" + Glib::ustring::compose(_("Complete selective self-test log: %1"), "\n\n" + p.get_value())); } } // tab label app_highlight_tab_label(lookup_widget("selective_selftest_tab_label"), max_tab_warning, tab_selective_selftest_name); return max_tab_warning; } WarningLevel GscInfoWindow::fill_ui_physical(const std::vector& props) { auto* textview = lookup_widget("phy_log_textview"); WarningLevel max_tab_warning = WarningLevel::none; for (auto&& p : props) { if (p.section != StorageProperty::Section::data || p.subsection != StorageProperty::SubSection::phy_log) continue; // Note: Don't use property description as a tooltip here. It won't be available if there's no property. if (p.generic_name == "sataphy_log") { Glib::RefPtr buffer = textview->get_buffer(); buffer->set_text("\n" + Glib::ustring::compose(_("Complete phy log: %1"), "\n\n" + p.get_value())); } } // tab label app_highlight_tab_label(lookup_widget("phy_tab_label"), max_tab_warning, tab_phy_name); return max_tab_warning; } WarningLevel GscInfoWindow::fill_ui_directory(const std::vector& props) { auto* textview = lookup_widget("directory_log_textview"); WarningLevel max_tab_warning = WarningLevel::none; for (auto&& p : props) { if (p.section != StorageProperty::Section::data || p.subsection != StorageProperty::SubSection::directory_log) continue; // Note: Don't use property description as a tooltip here. It won't be available if there's no property. if (p.generic_name == "directory_log") { Glib::RefPtr buffer = textview->get_buffer(); buffer->set_text("\n" + Glib::ustring::compose(_("Complete directory log: %1"), "\n\n" + p.get_value())); } } // tab label app_highlight_tab_label(lookup_widget("directory_tab_label"), max_tab_warning, tab_directory_name); return max_tab_warning; } // Note: Another loop like this may run inside it for another drive. gboolean GscInfoWindow::test_idle_callback(void* data) { auto* self = static_cast(data); DBG_ASSERT(self); if (!self->current_test) // shouldn't happen return false; // stop auto* test_completion_progressbar = self->lookup_widget("test_completion_progressbar"); bool active = true; do { // goto if (!self->current_test->is_active()) { // check status active = false; break; } int8_t rem_percent = self->current_test->get_remaining_percent(); std::string rem_percent_str = (rem_percent == -1 ? C_("value", "Unknown") : hz::number_to_string_locale(100 - rem_percent)); auto poll_in = self->current_test->get_poll_in_seconds(); // sec // One update() is performed by start(), so do the timeout first. // Wait until next poll (up to several minutes). Meanwhile, interpolate // the remaining time, update the progressbar, etc... if (self->test_timer_poll.elapsed() < static_cast(poll_in.count())) { // elapsed() is seconds in double. // Update progress bar right after poll, plus every 5 seconds. if (self->test_force_bar_update || self->test_timer_bar.elapsed() >= 5.) { auto rem_seconds = self->current_test->get_remaining_seconds(); if (test_completion_progressbar) { std::string rem_seconds_str = (rem_seconds == std::chrono::seconds(-1) ? C_("duration", "Unknown") : hz::format_time_length(rem_seconds)); Glib::ustring bar_str; if (self->test_error_msg.empty()) { bar_str = Glib::ustring::compose(_("Test completion: %1%%; ETA: %2"), rem_percent_str, rem_seconds_str); } else { bar_str = self->test_error_msg; // better than popup every few seconds } test_completion_progressbar->set_text(bar_str); test_completion_progressbar->set_fraction(std::max(0., std::min(1., 1. - (rem_percent / 100.)))); } self->test_force_bar_update = false; self->test_timer_bar.start(); // restart } if (!self->current_test->is_active()) { // check status active = false; break; } } else { // it's poll time if (!self->current_test->is_active()) { // the inner loop stopped, stop this one too active = false; break; } std::shared_ptr ex(new SmartctlExecutorGui()); ex->create_running_dialog(self); self->test_error_msg = self->current_test->update(ex); if (!self->test_error_msg.empty()) { // gui_show_error_dialog("Cannot monitor test progress", self->test_error_msg, this); // better show in progressbar. self->current_test->force_stop(ex); // what else can we do? active = false; break; } self->test_timer_poll.start(); // restart it self->test_force_bar_update = true; // force progressbar / ETA update on the next tick } } while (false); if (active) { return true; // continue the idle callback } // Test is finished, clean up self->test_timer_poll.stop(); // just in case self->test_timer_bar.stop(); // just in case StorageSelftestEntry::Status status = self->current_test->get_status(); bool aborted = false; StorageSelftestEntry::StatusSeverity severity = StorageSelftestEntry::StatusSeverity::none; std::string result_msg; if (!self->test_error_msg.empty()) { aborted = true; severity = StorageSelftestEntry::StatusSeverity::error; result_msg = Glib::ustring::compose(_("Test aborted: %1"), self->test_error_msg); } else { severity = StorageSelftestEntry::get_status_severity(status); if (status == StorageSelftestEntry::Status::aborted_by_host) { aborted = true; result_msg = ""s + _("Test was manually aborted.") + ""; // it's a StatusSeverity::none message } else { result_msg = Glib::ustring::compose(_("Test result: %1."), StorageSelftestEntry::get_status_displayable_name(status)); // It may not reach 100% somehow, so do it manually. if (test_completion_progressbar) test_completion_progressbar->set_fraction(1.); // yes, we're at 100% anyway (at least logically). } } if (severity != StorageSelftestEntry::StatusSeverity::none) { result_msg += "\n"s + _("Check the Self-Test Log for more information."); } if (auto* test_type_combo = self->lookup_widget("test_type_combo")) test_type_combo->set_sensitive(true); if (auto* test_execute_button = self->lookup_widget("test_execute_button")) test_execute_button->set_sensitive(true); if (test_completion_progressbar) test_completion_progressbar->set_text(aborted ? _("Test aborted") : _("Test completed")); if (auto* test_stop_button = self->lookup_widget("test_stop_button")) test_stop_button->set_sensitive(false); Gtk::StockID stock_id = Gtk::Stock::DIALOG_ERROR; if (severity == StorageSelftestEntry::StatusSeverity::none) { stock_id = Gtk::Stock::DIALOG_INFO; } else if (severity == StorageSelftestEntry::StatusSeverity::warning) { stock_id = Gtk::Stock::DIALOG_WARNING; } // we use large icon size here because the icons we use are from dialogs. // unfortunately, there are no non-dialog icons of this sort. if (auto* test_result_image = self->lookup_widget("test_result_image")) test_result_image->set(stock_id, Gtk::ICON_SIZE_DND); if (auto* test_result_label = self->lookup_widget("test_result_label")) test_result_label->set_markup(result_msg); if (auto* test_result_hbox = self->lookup_widget("test_result_hbox")) test_result_hbox->show(); self->refresh_info(false); // don't clear the tests tab return false; // stop idle callback } void GscInfoWindow::on_test_execute_button_clicked() { auto* test_type_combo = lookup_widget("test_type_combo"); Gtk::TreeRow row = *(test_type_combo->get_active()); if (!row) return; std::shared_ptr test_from_combo = row[test_combo_col_self_test]; auto test = std::make_shared(drive, test_from_combo->get_test_type()); if (!test) return; // hide previous test results from GUI if (auto* test_result_hbox = this->lookup_widget("test_result_hbox")) test_result_hbox->hide(); std::shared_ptr ex(new SmartctlExecutorGui()); ex->create_running_dialog(this); std::string error_msg = test->start(ex); // this runs update() too. if (!error_msg.empty()) { /// Translators: %1 is test name gui_show_error_dialog(Glib::ustring::compose(_("Cannot run %1"), SelfTest::get_test_displayable_name(test->get_test_type())), error_msg, this); return; } current_test = test; // Switch GUI to "running test" mode if (test_type_combo) test_type_combo->set_sensitive(false); if (auto* test_execute_button = lookup_widget("test_execute_button")) test_execute_button->set_sensitive(false); if (auto* test_completion_progressbar = lookup_widget("test_completion_progressbar")) { test_completion_progressbar->set_text(""); test_completion_progressbar->set_sensitive(true); test_completion_progressbar->show(); } if (auto* test_stop_button = lookup_widget("test_stop_button")) { test_stop_button->set_sensitive(true); // true while test is active test_stop_button->show(); } // reset these test_error_msg.clear(); test_timer_poll.start(); test_timer_bar.start(); test_force_bar_update = true; // We don't use idle function here, because it has the following problem: // CmdexSync::execute() (which is called on force_stop()) calls g_main_context_pending(), // which returns true EVERY time, until the idle callback returns false. // So, force_stop() exits its "execute abort" command only when the // idle callback polls the drive on the next timeout and sees that the test // has been actually aborted. // So, we use timeout callback - and hope that there are no usleeps // with 300ms so that g_main_context_pending() returns false at least once, // to escape the execute() loop. // g_idle_add(test_idle_callback, this); g_timeout_add(300, test_idle_callback, this); // every 300ms } void GscInfoWindow::on_test_stop_button_clicked() { if (!current_test) return; std::shared_ptr ex(new SmartctlExecutorGui()); ex->create_running_dialog(this); std::string error_msg = current_test->force_stop(ex); if (!error_msg.empty()) { /// Translators: %1 is test name gui_show_error_dialog(Glib::ustring::compose(_("Cannot stop %1"), SelfTest::get_test_displayable_name(current_test->get_test_type())), error_msg, this); return; } // nothing else to do - the cleanup is performed by the idle callback. } // Callback attached to StorageDevice. // We don't refresh automatically (that would make it impossible to do // several same-drive info window comparisons side by side). // But we need to look for testing status change, to avoid aborting it. void GscInfoWindow::on_drive_changed([[maybe_unused]] StorageDevice* pdrive) { if (!drive) return; bool test_active = drive->get_test_is_active(); // disable refresh button if test is active or if it's a virtual drive if (auto* refresh_info_button = lookup_widget("refresh_info_button")) refresh_info_button->set_sensitive(!test_active && !drive->get_is_virtual()); // disallow close. usually modal dialogs are used for this, but we can't have // per-drive modal dialogs. if (auto* close_window_button = lookup_widget("close_window_button")) close_window_button->set_sensitive(!test_active); // test_active is also checked in delete_event handler, because this call may not // succeed - the window manager may refuse to do it. this->set_deletable(!test_active); } bool GscInfoWindow::on_treeview_button_press_event(GdkEventButton* button_event, Gtk::Menu* menu, Gtk::TreeView* treeview) { if (button_event->type == GDK_BUTTON_PRESS && button_event->button == 3) { bool selection_empty = treeview->get_selection()->get_selected_rows().empty(); std::vector children = menu->get_children(); for (auto& child : children) { child->set_sensitive(!selection_empty); } menu->popup(button_event->button, button_event->time); return true; } return false; } void GscInfoWindow::on_treeview_menu_copy_clicked(Gtk::TreeView* treeview) { std::string text; int num_cols = static_cast(treeview->get_n_columns()); std::vector col_texts; for (int i = 0; i < num_cols; ++i) { Gtk::TreeViewColumn* tcol = treeview->get_column(i); col_texts.push_back("\"" + hz::string_replace_copy(tcol->get_title(), "\"", "\"\"") + "\""); } text += hz::string_join(col_texts, ',') + "\n"; auto selection = treeview->get_selection()->get_selected_rows(); auto list_store = Glib::RefPtr::cast_dynamic(treeview->get_model()); for (const auto& path : selection) { std::vector cell_texts; Gtk::TreeRow row = *(list_store->get_iter(path)); for (int j = 0; j < num_cols; ++j) { // gather data only from tree columns, not model columns (like tooltips and helper data) GType type = list_store->get_column_type(j); if (type == G_TYPE_INT) { int32_t value = 0; row.get_value(j, value); cell_texts.push_back(hz::number_to_string_nolocale(value)); // no locale for export } else if (type == G_TYPE_STRING) { std::string value; row.get_value(j, value); cell_texts.push_back("\"" + hz::string_replace_copy(value, "\"", "\"\"") + "\""); } } text += hz::string_join(cell_texts, ',') + "\n"; } if (auto clipboard = Gtk::Clipboard::get()) { clipboard->set_text(text); } } /// @}