#include "main-window.h" #include "menu.h" #include "project_config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(__APPLE__) static void osx_will_quit_cb(__attribute__((unused)) GtkosxApplication* app, __attribute__((unused)) gpointer data) { gtk_main_quit(); } #endif MainWindow::MainWindow(const std::string& timeout) : accel_group(Gtk::AccelGroup::create()), settings(), brightness_adjustment(Gtk::Adjustment::create(1.0, 0.0, 1.0, 0.05, 0.1)), max_content_width_adjustment(Gtk::Adjustment::create(700, 0, 99999, 10, 20)), spacing_adjustment(Gtk::Adjustment::create(0.0, -50.0, 50.0, 0.2, 0.1)), margins_adjustment(Gtk::Adjustment::create(10, 0, 1000, 10, 20)), indent_adjustment(Gtk::Adjustment::create(0, 0, 1000, 5, 10)), draw_css_provider(Gtk::CssProvider::create()), menu(accel_group), draw_primary(middleware_), draw_secondary(middleware_), about(*this), vbox_main(Gtk::ORIENTATION_VERTICAL, 0), vbox_toc(Gtk::ORIENTATION_VERTICAL), vbox_search(Gtk::ORIENTATION_VERTICAL), vbox_status(Gtk::ORIENTATION_VERTICAL), vbox_settings(Gtk::ORIENTATION_VERTICAL), vbox_icon_theme(Gtk::ORIENTATION_VERTICAL), search_match_case("Match _Case", true), wrap_none(wrapping_group, "None"), wrap_char(wrapping_group, "Char"), wrap_word(wrapping_group, "Word"), wrap_word_char(wrapping_group, "Word+Char"), close_toc_window_button("Close table of contents"), open_toc_button("Show table of contents (Ctrl+Shift+T)", true), back_button("Go back one page (Alt+Left arrow)", true), forward_button("Go forward one page (Alt+Right arrow)", true), refresh_button("Reload current page (Ctrl+R)", true), home_button("Home page (Alt+Home)", true), open_button("Open document (Ctrl+O)"), save_button("Save document (Ctrl+S)"), publish_button("Publish document... (Ctrl+P)"), cut_button("Cut (Ctrl+X)"), copy_button("Copy (Ctrl+C)"), paste_button("Paste (Ctrl+V)"), undo_button("Undo text (Ctrl+Z)"), redo_button("Redo text (Ctrl+Y)"), bold_button("Add bold text"), italic_button("Add italic text"), strikethrough_button("Add strikethrough text"), super_button("Add superscript text"), sub_button("Add subscript text"), link_button("Add a link"), image_button("Add an image"), emoji_button("Insert emoji"), quote_button("Insert a quote"), code_button("Insert code"), bullet_list_button("Add a bullet list"), numbered_list_button("Add a numbered list"), highlight_button("Add highlight text"), table_of_contents_label("Table of Contents"), network_heading_label("IPFS Network"), network_rate_heading_label("Network rate"), connectivity_label("Status:"), peers_label("Connected peers:"), repo_size_label("Repo size:"), repo_path_label("Repo path:"), ipfs_version_label("IPFS version:"), network_incoming_label("Incoming"), network_outgoing_label("Outgoing"), network_kilo_bytes_label("Kilobytes/s"), font_label("Font"), max_content_width_label("Content width"), spacing_label("Spacing"), margins_label("Margins"), indent_label("Indent"), text_wrapping_label("Wrapping"), theme_label("Dark Theme"), reader_view_label("Reader View"), icon_theme_label("Active Theme"), // Private members middleware_(*this, timeout), app_name_("LibreWeb Browser"), use_current_gtk_icon_theme_(false), // Use LibreWeb icon theme or the GTK icons icon_theme_("flat"), // Default is flat built-in theme icon_size_(18), font_family_("Sans"), default_font_size_(10), current_font_size_(10), position_divider_draw_(-1), content_margin_(20), content_max_width_(700), font_spacing_(0.0), indent_(0), wrap_mode_(Gtk::WRAP_WORD_CHAR), brightness_scale_(1.0), use_dark_theme_(false), is_reader_view_enabled_(true), current_history_index_(0) { set_title(app_name_); set_default_size(1000, 800); set_position(Gtk::WIN_POS_CENTER); add_accel_group(accel_group); load_stored_settings(); load_icons(); init_toolbar_buttons(); set_theme(); init_search_popover(); init_status_popover(); init_settings_popover(); init_table_of_contents(); init_signals(); init_mac_os(); // Add custom CSS Provider to draw textviews auto stylePrimary = draw_primary.get_style_context(); auto styleSecondary = draw_secondary.get_style_context(); stylePrimary->add_provider(draw_css_provider, GTK_STYLE_PROVIDER_PRIORITY_USER); styleSecondary->add_provider(draw_css_provider, GTK_STYLE_PROVIDER_PRIORITY_USER); // Load the default font family and font size update_css(); // Primary drawing area scrolled_window_primary.add(draw_primary); scrolled_window_primary.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); // Secondary drawing area draw_secondary.set_view_source_menu_item(false); scrolled_window_secondary.add(draw_secondary); scrolled_window_secondary.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); paned_draw.pack1(scrolled_window_primary, true, false); paned_draw.pack2(scrolled_window_secondary, true, true); // Left the vbox for the table of contents, // right the drawing paned windows (primary/secondary). paned_root.pack1(vbox_toc, true, false); paned_root.pack2(paned_draw, true, false); // Main virtual box vbox_main.pack_start(menu, false, false, 0); vbox_main.pack_start(hbox_browser_toolbar, false, false, 6); vbox_main.pack_start(hbox_standard_editor_toolbar, false, false, 6); vbox_main.pack_start(hbox_formatting_editor_toolbar, false, false, 6); vbox_main.pack_start(paned_root, true, true, 0); add(vbox_main); show_all_children(); // Hide by default the table of contents, secondary textview, replace entry and editor toolbars vbox_toc.hide(); scrolled_window_secondary.hide(); search_replace_entry.hide(); hbox_standard_editor_toolbar.hide(); hbox_formatting_editor_toolbar.hide(); // Grap focus to input field by default address_bar.grab_focus(); // Show homepage if debugging is disabled #ifdef NDEBUG go_home(); #else std::cout << "INFO: Running as Debug mode, opening test.md." << std::endl; // Load test file during development middleware_.do_request("file://../../test.md"); #endif } /** * \brief Called before the requests begins. * \param path File path (on disk or IPFS) that needs to be processed. * \param title Application title * \param is_set_address_bar If true update the address bar with the file path * \param is_history_request Set to true if this is an history request call: back/forward * \param is_disable_editor If true the editor will be disabled if needed */ void MainWindow::pre_request( const std::string& path, const std::string& title, bool is_set_address_bar, bool is_history_request, bool is_disable_editor) { if (is_set_address_bar) address_bar.set_text(path); if (!title.empty()) set_title(title + " - " + app_name_); else set_title(app_name_); if (is_disable_editor && is_editor_enabled()) disable_edit(); // Do not insert history back/forward calls into the history (again) if (!is_history_request && !path.empty()) { if (history_.empty()) { history_.push_back(path); current_history_index_ = history_.size() - 1; } else if (history_.back().compare(path) != 0) { history_.push_back(path); current_history_index_ = history_.size() - 1; } } // Enable back/forward buttons when possible back_button.set_sensitive(current_history_index_ > 0); menu.set_back_menu_sensitive(current_history_index_ > 0); forward_button.set_sensitive(current_history_index_ < history_.size() - 1); menu.set_forward_menu_sensitive(current_history_index_ < history_.size() - 1); // Clear table of contents (ToC) toc_tree_model->clear(); } /** * \brief Called after file is written to disk. */ void MainWindow::post_write(const std::string& path, const std::string& title, bool is_set_address_and_title) { if (is_set_address_and_title) { address_bar.set_text(path); set_title(title + " - " + app_name_); } } /** * \brief Called when request started (from thread). */ void MainWindow::started_request() { // Start spinning icon refresh_icon.get_style_context()->add_class("spinning"); } /** * \brief Called when request is finished (from thread). */ void MainWindow::finished_request() { // Stop spinning icon refresh_icon.get_style_context()->remove_class("spinning"); } /** * \brief Refresh the current page */ void MainWindow::refresh_request() { // Only allow refresh if editor is disabled (doesn't make sense otherwise to refresh) if (!is_editor_enabled()) // Reload existing file, don't need to update the address bar, don't disable the editor middleware_.do_request("", false, false, false); } /** * \brief Show home page */ void MainWindow::show_homepage() { draw_primary.show_homepage(); } /** * \brief Set plain text * \param content content string */ void MainWindow::set_text(const Glib::ustring& content) { draw_primary.set_text(content); } /** * \brief Set markdown document (common mark) on primary window. cmark_node pointer will be freed automatically. * And set the ToC. * \param root_node cmark root data struct */ void MainWindow::set_document(cmark_node* root_node) { draw_primary.set_document(root_node); set_table_of_contents(draw_primary.get_headings()); } /** * \brief Set message with optionally additional details * \param message Message string * \param details Details string */ void MainWindow::set_message(const Glib::ustring& message, const Glib::ustring& details) { draw_primary.set_message(message, details); } /** * \brief Update all status fields in status pop-over menu + status icon */ void MainWindow::update_status_popover_and_icon() { std::string networkStatus; std::size_t nrOfPeers = middleware_.get_ipfs_number_of_peers(); // Update status icon if (nrOfPeers > 0) { networkStatus = "Connected"; if (use_current_gtk_icon_theme_) { status_icon.set_from_icon_name("network-wired-symbolic", Gtk::IconSize(Gtk::ICON_SIZE_MENU)); } else { status_icon.set(status_online_icon); } } else { networkStatus = "Disconnected"; if (use_current_gtk_icon_theme_) { status_icon.set_from_icon_name("network-wired-disconnected-symbolic", Gtk::IconSize(Gtk::ICON_SIZE_MENU)); } else { status_icon.set(status_offline_icon); } } connectivity_status_label.set_markup("" + networkStatus + ""); peers_status_label.set_text(std::to_string(nrOfPeers)); repo_size_status_label.set_text(std::to_string(middleware_.get_ipfs_repo_size()) + " MB"); repo_path_status_label.set_text(middleware_.get_ipfs_repo_path()); network_incoming_status_label.set_text(middleware_.get_ipfs_incoming_rate()); network_outgoing_status_label.set_text(middleware_.get_ipfs_outgoing_rate()); ipfs_version_status_label.set_text(middleware_.get_ipfs_version()); } /** * Load stored settings from GSettings scheme file */ void MainWindow::load_stored_settings() { // Set additional schema directory, when browser is not yet installed if (!is_installed()) { // Relative to the binary path std::vector relativePath{".."}; std::string schemaDir = Glib::build_path(G_DIR_SEPARATOR_S, relativePath); std::cout << "INFO: Binary not installed. Try to find the gschema file one directory up (..)." << std::endl; Glib::setenv("GSETTINGS_SCHEMA_DIR", schemaDir); } // Load schema settings file auto schemaSource = Gio::SettingsSchemaSource::get_default()->lookup("org.libreweb.browser", true); if (schemaSource) { settings = Gio::Settings::create("org.libreweb.browser"); // Apply global settings set_default_size(settings->get_int("width"), settings->get_int("height")); if (settings->get_boolean("maximized")) maximize(); position_divider_draw_ = settings->get_int("position-divider-draw"); paned_draw.set_position(position_divider_draw_); font_family_ = settings->get_string("font-family"); current_font_size_ = default_font_size_ = settings->get_int("font-size"); font_button.set_font_name(font_family_ + " " + std::to_string(current_font_size_)); content_max_width_ = settings->get_int("max-content-width"); font_spacing_ = settings->get_double("spacing"); content_margin_ = settings->get_int("margins"); indent_ = settings->get_int("indent"); wrap_mode_ = static_cast(settings->get_enum("wrap-mode")); max_content_width_adjustment->set_value(content_max_width_); spacing_adjustment->set_value(font_spacing_); margins_adjustment->set_value(content_margin_); indent_adjustment->set_value(indent_); draw_primary.set_indent(indent_); int tocDividerPosition = settings->get_int("position-divider-toc"); paned_root.set_position(tocDividerPosition); icon_theme_ = settings->get_string("icon-theme"); use_current_gtk_icon_theme_ = settings->get_boolean("icon-gtk-theme"); brightness_scale_ = settings->get_double("brightness"); use_dark_theme_ = settings->get_boolean("dark-theme"); is_reader_view_enabled_ = settings->get_boolean("reader-view"); switch (wrap_mode_) { case Gtk::WRAP_NONE: wrap_none.set_active(true); break; case Gtk::WRAP_CHAR: wrap_char.set_active(true); break; case Gtk::WRAP_WORD: wrap_word.set_active(true); break; case Gtk::WRAP_WORD_CHAR: wrap_word_char.set_active(true); break; default: wrap_word_char.set_active(true); } } else { std::cerr << "ERROR: Gsettings schema file could not be found!" << std::endl; // Select default fallback wrap mode wrap_word_char.set_active(true); // Fallback adjustment controls max_content_width_adjustment->set_value(content_max_width_); spacing_adjustment->set_value(font_spacing_); margins_adjustment->set_value(content_max_width_); indent_adjustment->set_value(indent_); // Fallback ToC paned divider paned_root.set_position(300); } // Apply settings that needs to be applied now // Note: margins are getting automatically applied (on resize), // and some other attributes are part of CSS. draw_primary.set_indent(indent_); draw_primary.set_wrap_mode(wrap_mode_); } /** * \brief set GTK Icons */ void MainWindow::set_gtk_icons() { // Toolbox buttons toc_icon.set_from_icon_name("view-list-symbolic", Gtk::IconSize(Gtk::ICON_SIZE_MENU)); back_icon.set_from_icon_name("go-previous", Gtk::IconSize(Gtk::ICON_SIZE_MENU)); forward_icon.set_from_icon_name("go-next", Gtk::IconSize(Gtk::ICON_SIZE_MENU)); refresh_icon.set_from_icon_name("view-refresh", Gtk::IconSize(Gtk::ICON_SIZE_MENU)); home_icon.set_from_icon_name("go-home", Gtk::IconSize(Gtk::ICON_SIZE_MENU)); search_icon.set_from_icon_name("edit-find-symbolic", Gtk::IconSize(Gtk::ICON_SIZE_MENU)); settings_icon.set_from_icon_name("open-menu-symbolic", Gtk::IconSize(Gtk::ICON_SIZE_MENU)); // Settings pop-over buttons zoom_out_image.set_from_icon_name("zoom-out-symbolic", Gtk::IconSize(Gtk::ICON_SIZE_MENU)); zoom_in_image.set_from_icon_name("zoom-in-symbolic", Gtk::IconSize(Gtk::ICON_SIZE_MENU)); brightness_image.set_from_icon_name("display-brightness-symbolic", Gtk::IconSize(Gtk::ICON_SIZE_MENU)); } /** * Load all icon images from theme/disk. Or reload them. */ void MainWindow::load_icons() { try { // Editor buttons open_icon.set(Gdk::Pixbuf::create_from_file(get_icon_image_from_theme("open_folder", "folders"), icon_size_, icon_size_)); save_icon.set(Gdk::Pixbuf::create_from_file(get_icon_image_from_theme("floppy_disk", "basic"), icon_size_, icon_size_)); publish_icon.set(Gdk::Pixbuf::create_from_file(get_icon_image_from_theme("upload", "basic"), icon_size_, icon_size_)); cut_icon.set(Gdk::Pixbuf::create_from_file(get_icon_image_from_theme("cut", "editor"), icon_size_, icon_size_)); copy_icon.set(Gdk::Pixbuf::create_from_file(get_icon_image_from_theme("copy", "editor"), icon_size_, icon_size_)); paste_icon.set(Gdk::Pixbuf::create_from_file(get_icon_image_from_theme("clipboard", "editor"), icon_size_, icon_size_)); undo_icon.set(Gdk::Pixbuf::create_from_file(get_icon_image_from_theme("undo", "editor"), icon_size_, icon_size_)); redo_icon.set(Gdk::Pixbuf::create_from_file(get_icon_image_from_theme("redo", "editor"), icon_size_, icon_size_)); bold_icon.set(Gdk::Pixbuf::create_from_file(get_icon_image_from_theme("bold", "editor"), icon_size_, icon_size_)); italic_icon.set(Gdk::Pixbuf::create_from_file(get_icon_image_from_theme("italic", "editor"), icon_size_, icon_size_)); strikethrough_icon.set(Gdk::Pixbuf::create_from_file(get_icon_image_from_theme("strikethrough", "editor"), icon_size_, icon_size_)); super_icon.set(Gdk::Pixbuf::create_from_file(get_icon_image_from_theme("superscript", "editor"), icon_size_, icon_size_)); sub_icon.set(Gdk::Pixbuf::create_from_file(get_icon_image_from_theme("subscript", "editor"), icon_size_, icon_size_)); link_icon.set(Gdk::Pixbuf::create_from_file(get_icon_image_from_theme("link", "editor"), icon_size_, icon_size_)); image_icon.set(Gdk::Pixbuf::create_from_file(get_icon_image_from_theme("shapes", "editor"), icon_size_, icon_size_)); emoji_icon.set(Gdk::Pixbuf::create_from_file(get_icon_image_from_theme("smile", "smiley"), icon_size_, icon_size_)); quote_icon.set(Gdk::Pixbuf::create_from_file(get_icon_image_from_theme("quote", "editor"), icon_size_, icon_size_)); code_icon.set(Gdk::Pixbuf::create_from_file(get_icon_image_from_theme("code", "editor"), icon_size_, icon_size_)); bullet_list_icon.set(Gdk::Pixbuf::create_from_file(get_icon_image_from_theme("bullet_list", "editor"), icon_size_, icon_size_)); numbered_list_icon.set(Gdk::Pixbuf::create_from_file(get_icon_image_from_theme("number_list", "editor"), icon_size_, icon_size_)); hightlight_icon.set(Gdk::Pixbuf::create_from_file(get_icon_image_from_theme("highlighter", "editor"), icon_size_, icon_size_)); if (use_current_gtk_icon_theme_) { set_gtk_icons(); } else { // Toolbox buttons toc_icon.set(Gdk::Pixbuf::create_from_file(get_icon_image_from_theme("square_list", "editor"), icon_size_, icon_size_)); back_icon.set(Gdk::Pixbuf::create_from_file(get_icon_image_from_theme("right_arrow_1", "arrows"), icon_size_, icon_size_)->flip()); forward_icon.set(Gdk::Pixbuf::create_from_file(get_icon_image_from_theme("right_arrow_1", "arrows"), icon_size_, icon_size_)); refresh_icon.set(Gdk::Pixbuf::create_from_file(get_icon_image_from_theme("reload_centered", "arrows"), icon_size_ * 1.13, icon_size_)); home_icon.set(Gdk::Pixbuf::create_from_file(get_icon_image_from_theme("home", "basic"), icon_size_, icon_size_)); search_icon.set(Gdk::Pixbuf::create_from_file(get_icon_image_from_theme("search", "basic"), icon_size_, icon_size_)); settings_icon.set(Gdk::Pixbuf::create_from_file(get_icon_image_from_theme("menu", "basic"), icon_size_, icon_size_)); // Settings pop-over buttons zoom_out_image.set(Gdk::Pixbuf::create_from_file(get_icon_image_from_theme("zoom_out", "basic"), icon_size_, icon_size_)); zoom_in_image.set(Gdk::Pixbuf::create_from_file(get_icon_image_from_theme("zoom_in", "basic"), icon_size_, icon_size_)); brightness_image.set(Gdk::Pixbuf::create_from_file(get_icon_image_from_theme("brightness", "basic"), icon_size_, icon_size_)); status_offline_icon = Gdk::Pixbuf::create_from_file(get_icon_image_from_theme("network_disconnected", "network"), icon_size_, icon_size_); status_online_icon = Gdk::Pixbuf::create_from_file(get_icon_image_from_theme("network_connected", "network"), icon_size_, icon_size_); } } catch (const Glib::FileError& error) { std::cerr << "ERROR: Icon could not be loaded, file error: " << error.what() << ".\nContinue nevertheless, with GTK icons as fallback..." << std::endl; set_gtk_icons(); use_current_gtk_icon_theme_ = true; } catch (const Gdk::PixbufError& error) { std::cerr << "ERROR: Icon could not be loaded, pixbuf error: " << error.what() << ".\nContinue nevertheless, with GTK icons as fallback..." << std::endl; set_gtk_icons(); use_current_gtk_icon_theme_ = true; } } /** * Init all buttons / comboboxes from the toolbars */ void MainWindow::init_toolbar_buttons() { // Add icons to the toolbar editor buttons open_button.add(open_icon); save_button.add(save_icon); publish_button.add(publish_icon); cut_button.add(cut_icon); copy_button.add(copy_icon); paste_button.add(paste_icon); undo_button.add(undo_icon); redo_button.add(redo_icon); bold_button.add(bold_icon); italic_button.add(italic_icon); strikethrough_button.add(strikethrough_icon); super_button.add(super_icon); sub_button.add(sub_icon); link_button.add(link_icon); image_button.add(image_icon); emoji_button.add(emoji_icon); quote_button.add(quote_icon); code_button.add(code_icon); bullet_list_button.add(bullet_list_icon); numbered_list_button.add(numbered_list_icon); highlight_button.add(hightlight_icon); // Disable focus the other buttons as well search_match_case.set_can_focus(false); headings_combo_box.set_can_focus(false); headings_combo_box.set_focus_on_click(false); // Populate the heading comboboxtext headings_combo_box.append("", "Select Heading"); headings_combo_box.append("1", "Heading 1"); headings_combo_box.append("2", "Heading 2"); headings_combo_box.append("3", "Heading 3"); headings_combo_box.append("4", "Heading 4"); headings_combo_box.append("5", "Heading 5"); headings_combo_box.append("6", "Heading 6"); headings_combo_box.set_active(0); // Horizontal bar back_button.get_style_context()->add_class("circular"); forward_button.get_style_context()->add_class("circular"); refresh_button.get_style_context()->add_class("circular"); search_button.set_popover(search_popover); status_button.set_popover(status_popover); settings_button.set_popover(settings_popover); search_button.set_relief(Gtk::RELIEF_NONE); status_button.set_relief(Gtk::RELIEF_NONE); settings_button.set_relief(Gtk::RELIEF_NONE); // Add icons to the toolbar buttons open_toc_button.add(toc_icon); back_button.add(back_icon); forward_button.add(forward_icon); refresh_button.add(refresh_icon); home_button.add(home_icon); search_button.add(search_icon); status_button.add(status_icon); settings_button.add(settings_icon); // Add spinning CSS class to refresh icon auto cssProvider = Gtk::CssProvider::create(); auto screen = Gdk::Screen::get_default(); std::string spinningCSS = "@keyframes spin { to { -gtk-icon-transform: rotate(1turn); }} .spinning { animation-name: spin; " "animation-duration: 1s; animation-timing-function: linear; animation-iteration-count: infinite;}"; if (!cssProvider->load_from_data(spinningCSS)) { std::cerr << "ERROR: CSS data parsing went wrong." << std::endl; } auto refreshIconStyle = refresh_icon.get_style_context(); refreshIconStyle->add_provider_for_screen(screen, cssProvider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); // Add tooltips to the toolbar buttons search_button.set_tooltip_text("Find"); status_button.set_tooltip_text("IPFS Network Status"); settings_button.set_tooltip_text("Settings"); // Disable back/forward buttons on start-up back_button.set_sensitive(false); forward_button.set_sensitive(false); /* * Adding the buttons to the boxes */ // Browser Toolbar open_toc_button.set_margin_left(6); hbox_browser_toolbar.pack_start(open_toc_button, false, false, 0); hbox_browser_toolbar.pack_start(back_button, false, false, 0); hbox_browser_toolbar.pack_start(forward_button, false, false, 0); hbox_browser_toolbar.pack_start(refresh_button, false, false, 0); hbox_browser_toolbar.pack_start(home_button, false, false, 0); hbox_browser_toolbar.pack_start(address_bar, true, true, 4); hbox_browser_toolbar.pack_start(search_button, false, false, 0); hbox_browser_toolbar.pack_start(status_button, false, false, 0); hbox_browser_toolbar.pack_start(settings_button, false, false, 0); // Standard editor toolbar headings_combo_box.set_margin_left(4); hbox_standard_editor_toolbar.pack_start(open_button, false, false, 2); hbox_standard_editor_toolbar.pack_start(save_button, false, false, 2); hbox_standard_editor_toolbar.pack_start(publish_button, false, false, 2); hbox_standard_editor_toolbar.pack_start(separator1, false, false, 0); hbox_standard_editor_toolbar.pack_start(cut_button, false, false, 2); hbox_standard_editor_toolbar.pack_start(copy_button, false, false, 2); hbox_standard_editor_toolbar.pack_start(paste_button, false, false, 2); hbox_standard_editor_toolbar.pack_start(separator2, false, false, 0); hbox_standard_editor_toolbar.pack_start(undo_button, false, false, 2); hbox_standard_editor_toolbar.pack_start(redo_button, false, false, 2); // Formatting toolbar headings_combo_box.set_margin_left(4); hbox_formatting_editor_toolbar.pack_start(headings_combo_box, false, false, 2); hbox_formatting_editor_toolbar.pack_start(bold_button, false, false, 2); hbox_formatting_editor_toolbar.pack_start(italic_button, false, false, 2); hbox_formatting_editor_toolbar.pack_start(strikethrough_button, false, false, 2); hbox_formatting_editor_toolbar.pack_start(super_button, false, false, 2); hbox_formatting_editor_toolbar.pack_start(sub_button, false, false, 2); hbox_formatting_editor_toolbar.pack_start(separator3, false, false, 0); hbox_formatting_editor_toolbar.pack_start(link_button, false, false, 2); hbox_formatting_editor_toolbar.pack_start(image_button, false, false, 2); hbox_formatting_editor_toolbar.pack_start(emoji_button, false, false, 2); hbox_formatting_editor_toolbar.pack_start(separator4, false, false, 0); hbox_formatting_editor_toolbar.pack_start(quote_button, false, false, 2); hbox_formatting_editor_toolbar.pack_start(code_button, false, false, 2); hbox_formatting_editor_toolbar.pack_start(bullet_list_button, false, false, 2); hbox_formatting_editor_toolbar.pack_start(numbered_list_button, false, false, 2); hbox_formatting_editor_toolbar.pack_start(highlight_button, false, false, 2); } /** * \brief Prefer dark or light theme */ void MainWindow::set_theme() { auto settings = Gtk::Settings::get_default(); settings->property_gtk_application_prefer_dark_theme().set_value(use_dark_theme_); } /** * \brief Popover search bar */ void MainWindow::init_search_popover() { search_entry.set_placeholder_text("Find"); search_replace_entry.set_placeholder_text("Replace"); search.connect_entry(search_entry); search_replace.connect_entry(search_replace_entry); search_entry.set_size_request(250, -1); search_replace_entry.set_size_request(250, -1); vbox_search.set_margin_left(8); vbox_search.set_margin_right(8); vbox_search.set_spacing(8); hbox_search.set_spacing(8); hbox_search.pack_start(search_entry, false, false); hbox_search.pack_start(search_match_case, false, false); vbox_search.pack_start(hbox_search, false, false, 4); vbox_search.pack_end(search_replace_entry, false, false, 4); search_popover.set_position(Gtk::POS_BOTTOM); search_popover.set_size_request(300, 50); search_popover.add(vbox_search); search_popover.show_all_children(); } /** * Init the IPFS status pop-over */ void MainWindow::init_status_popover() { connectivity_label.set_xalign(0.0); peers_label.set_xalign(0.0); repo_size_label.set_xalign(0.0); repo_path_label.set_xalign(0.0); ipfs_version_label.set_xalign(0.0); connectivity_status_label.set_xalign(1.0); peers_status_label.set_xalign(1.0); repo_size_status_label.set_xalign(1.0); repo_path_status_label.set_xalign(1.0); ipfs_version_status_label.set_xalign(1.0); connectivity_label.get_style_context()->add_class("dim-label"); peers_label.get_style_context()->add_class("dim-label"); repo_size_label.get_style_context()->add_class("dim-label"); repo_path_label.get_style_context()->add_class("dim-label"); ipfs_version_label.get_style_context()->add_class("dim-label"); // Status popover grid status_grid.set_column_homogeneous(true); status_grid.set_margin_start(6); status_grid.set_margin_top(6); status_grid.set_margin_bottom(6); status_grid.set_margin_end(12); status_grid.set_row_spacing(10); status_grid.set_column_spacing(6); status_grid.attach(connectivity_label, 0, 0); status_grid.attach(connectivity_status_label, 1, 0); status_grid.attach(peers_label, 0, 1); status_grid.attach(peers_status_label, 1, 1); status_grid.attach(repo_size_label, 0, 2); status_grid.attach(repo_size_status_label, 1, 2); status_grid.attach(repo_path_label, 0, 3); status_grid.attach(repo_path_status_label, 1, 3); status_grid.attach(ipfs_version_label, 0, 4); status_grid.attach(ipfs_version_status_label, 1, 4); // IPFS Network activity status grid network_kilo_bytes_label.get_style_context()->add_class("dim-label"); activity_status_grid.set_column_homogeneous(true); activity_status_grid.set_margin_start(6); activity_status_grid.set_margin_top(6); activity_status_grid.set_margin_bottom(6); activity_status_grid.set_margin_end(6); activity_status_grid.set_row_spacing(10); activity_status_grid.set_column_spacing(6); activity_status_grid.attach(network_incoming_label, 1, 0); activity_status_grid.attach(network_outgoing_label, 2, 0); activity_status_grid.attach(network_kilo_bytes_label, 0, 1); activity_status_grid.attach(network_incoming_status_label, 1, 1); activity_status_grid.attach(network_outgoing_status_label, 2, 1); network_heading_label.get_style_context()->add_class("dim-label"); network_rate_heading_label.get_style_context()->add_class("dim-label"); // Copy ID & public key buttons copy_id_button.set_label("Copy your ID"); copy_public_key_button.set_label("Copy Public Key"); copy_id_button.set_margin_start(6); copy_id_button.set_margin_end(6); copy_public_key_button.set_margin_start(6); copy_public_key_button.set_margin_end(6); // Add all items to status box & status popover vbox_status.set_margin_start(10); vbox_status.set_margin_end(10); vbox_status.set_margin_top(10); vbox_status.set_margin_bottom(10); vbox_status.set_spacing(6); vbox_status.add(network_heading_label); vbox_status.add(status_grid); vbox_status.add(separator9); vbox_status.add(network_rate_heading_label); vbox_status.add(activity_status_grid); vbox_status.add(separator10); vbox_status.add(copy_public_key_button); vbox_status.add(copy_id_button); status_popover.set_position(Gtk::POS_BOTTOM); status_popover.set_size_request(100, 250); status_popover.set_margin_end(2); status_popover.add(vbox_status); status_popover.show_all_children(); // Set fallback values for all status fields + status icon update_status_popover_and_icon(); } /** * \brief Init table of contents window (left side-panel) */ void MainWindow::init_table_of_contents() { close_toc_window_button.set_image_from_icon_name("window-close-symbolic", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); table_of_contents_label.set_margin_start(6); hbox_toc.pack_start(table_of_contents_label, false, false); hbox_toc.pack_end(close_toc_window_button, false, false); toc_tree_view.append_column("Level", toc_columns.col_level); toc_tree_view.append_column("Name", toc_columns.col_heading); toc_tree_view.set_activate_on_single_click(true); toc_tree_view.set_headers_visible(false); toc_tree_view.set_tooltip_column(2); scrolled_toc.add(toc_tree_view); scrolled_toc.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); toc_tree_model = Gtk::TreeStore::create(toc_columns); toc_tree_view.set_model(toc_tree_model); vbox_toc.pack_start(hbox_toc, Gtk::PackOptions::PACK_SHRINK); vbox_toc.pack_end(scrolled_toc); } /** * \brief Init the settings pop-over */ void MainWindow::init_settings_popover() { // Toolbar buttons / images zoom_out_button.add(zoom_out_image); zoom_in_button.add(zoom_in_image); brightness_image.set_tooltip_text("Brightness"); brightness_image.set_margin_start(2); brightness_image.set_margin_end(2); brightness_image.set_margin_top(1); brightness_image.set_margin_bottom(1); // Zoom buttons auto hboxZoomStyleContext = hbox_setings_zoom.get_style_context(); hboxZoomStyleContext->add_class("linked"); zoom_restore_button.set_sensitive(false); // By default restore button disabled zoom_restore_button.set_label("100%"); zoom_out_button.set_tooltip_text("Zoom out"); zoom_restore_button.set_tooltip_text("Restore zoom"); zoom_in_button.set_tooltip_text("Zoom in"); hbox_setings_zoom.set_size_request(-1, 40); hbox_setings_zoom.set_margin_bottom(6); hbox_setings_zoom.pack_start(zoom_out_button); hbox_setings_zoom.pack_start(zoom_restore_button); hbox_setings_zoom.pack_end(zoom_in_button); // Brightness slider brightness_adjustment->set_value(brightness_scale_); // Override with current loaded brightness setting scale_settings_brightness.set_adjustment(brightness_adjustment); scale_settings_brightness.add_mark(0.5, Gtk::PositionType::POS_BOTTOM, ""); scale_settings_brightness.set_draw_value(false); scale_settings_brightness.signal_value_changed().connect(sigc::mem_fun(this, &MainWindow::on_brightness_changed)); hbox_setings_brightness.pack_start(brightness_image, false, false); hbox_setings_brightness.pack_end(scale_settings_brightness); // Settings labels / buttons font_label.set_tooltip_text("Font familiy"); max_content_width_label.set_tooltip_text("Max content width"); spacing_label.set_tooltip_text("Text spacing"); margins_label.set_tooltip_text("Text margins"); indent_label.set_tooltip_text("Text indentation"); text_wrapping_label.set_tooltip_text("Text wrapping"); wrap_none.set_tooltip_text("No wrapping"); wrap_char.set_tooltip_text("Character wrapping"); wrap_word.set_tooltip_text("Word wrapping"); wrap_word_char.set_tooltip_text("Word wrapping (+ character)"); max_content_width_spin_button.set_adjustment(max_content_width_adjustment); spacing_spin_button.set_adjustment(spacing_adjustment); spacing_spin_button.set_digits(1); margins_spin_button.set_adjustment(margins_adjustment); indent_spin_button.set_adjustment(indent_adjustment); font_label.set_xalign(1); max_content_width_label.set_xalign(1); spacing_label.set_xalign(1); margins_label.set_xalign(1); indent_label.set_xalign(1); text_wrapping_label.set_xalign(1); theme_label.set_xalign(1); reader_view_label.set_xalign(1); font_label.get_style_context()->add_class("dim-label"); max_content_width_label.get_style_context()->add_class("dim-label"); spacing_label.get_style_context()->add_class("dim-label"); margins_label.get_style_context()->add_class("dim-label"); indent_label.get_style_context()->add_class("dim-label"); text_wrapping_label.get_style_context()->add_class("dim-label"); theme_label.get_style_context()->add_class("dim-label"); reader_view_label.get_style_context()->add_class("dim-label"); // Dark theme switch theme_switch.set_active(use_dark_theme_); // Override with current dark theme preference // Reader view switch reader_view_switch.set_active(is_reader_view_enabled_); // Settings grid settings_grid.set_margin_start(6); settings_grid.set_margin_top(6); settings_grid.set_margin_bottom(6); settings_grid.set_row_spacing(10); settings_grid.set_column_spacing(10); settings_grid.attach(font_label, 0, 0, 1); settings_grid.attach(font_button, 1, 0, 2); settings_grid.attach(max_content_width_label, 0, 1, 1); settings_grid.attach(max_content_width_spin_button, 1, 1, 2); settings_grid.attach(spacing_label, 0, 2, 1); settings_grid.attach(spacing_spin_button, 1, 2, 2); settings_grid.attach(margins_label, 0, 3, 1); settings_grid.attach(margins_spin_button, 1, 3, 2); settings_grid.attach(indent_label, 0, 4, 1); settings_grid.attach(indent_spin_button, 1, 4, 2); settings_grid.attach(text_wrapping_label, 0, 5, 1); settings_grid.attach(wrap_none, 1, 5, 1); settings_grid.attach(wrap_char, 2, 5, 1); settings_grid.attach(wrap_word, 1, 6, 1); settings_grid.attach(wrap_word_char, 2, 6, 1); settings_grid.attach(theme_label, 0, 7, 1); settings_grid.attach(theme_switch, 1, 7, 2); settings_grid.attach(reader_view_label, 0, 8, 1); settings_grid.attach(reader_view_switch, 1, 8, 2); // Icon theme (+ submenu) icon_theme_button.set_label("Icon Theme"); icon_theme_button.property_menu_name() = "icon-theme"; about_button.set_label("About LibreWeb"); Gtk::Label* iconThemeButtonlabel = dynamic_cast(icon_theme_button.get_child()); iconThemeButtonlabel->set_xalign(0.0); Gtk::Label* aboutButtonLabel = dynamic_cast(about_button.get_child()); iconThemeButtonlabel->set_xalign(0.0); aboutButtonLabel->set_xalign(0.0); // Add Settings vbox to popover menu vbox_settings.set_margin_start(10); vbox_settings.set_margin_end(10); vbox_settings.set_margin_top(10); vbox_settings.set_margin_bottom(10); vbox_settings.set_spacing(8); vbox_settings.add(hbox_setings_zoom); vbox_settings.add(hbox_setings_brightness); vbox_settings.add(separator5); vbox_settings.add(settings_grid); vbox_settings.add(separator6); vbox_settings.add(icon_theme_button); vbox_settings.add(separator7); vbox_settings.pack_end(about_button, false, false); settings_popover.set_position(Gtk::POS_BOTTOM); settings_popover.set_size_request(200, 300); settings_popover.set_margin_end(2); settings_popover.add(vbox_settings); // Add Theme vbox to popover menu icon_theme_back_button.set_label("Icon Theme"); icon_theme_back_button.property_menu_name() = "main"; icon_theme_back_button.property_inverted() = true; // List of themes in list box Gtk::Label* iconTheme1 = Gtk::manage(new Gtk::Label("Flat theme")); Gtk::ListBoxRow* row1 = Gtk::manage(new Gtk::ListBoxRow()); row1->add(*iconTheme1); row1->set_data("value", (void*)"flat"); Gtk::Label* iconTheme2 = Gtk::manage(new Gtk::Label("Filled theme")); Gtk::ListBoxRow* row2 = Gtk::manage(new Gtk::ListBoxRow()); row2->add(*iconTheme2); row2->set_data("value", (void*)"filled"); Gtk::Label* iconTheme3 = Gtk::manage(new Gtk::Label("Gtk default theme")); Gtk::ListBoxRow* row3 = Gtk::manage(new Gtk::ListBoxRow()); row3->add(*iconTheme3); row3->set_data("value", (void*)"none"); icon_theme_list_box.add(*row1); icon_theme_list_box.add(*row2); icon_theme_list_box.add(*row3); // Select the correct row by default if (use_current_gtk_icon_theme_) icon_theme_list_box.select_row(*row3); else if (icon_theme_ == "flat") icon_theme_list_box.select_row(*row1); else if (icon_theme_ == "filled") icon_theme_list_box.select_row(*row2); else icon_theme_list_box.select_row(*row1); // flat is fallback icon_theme_list_scrolled_window.property_height_request() = 200; icon_theme_list_scrolled_window.add(icon_theme_list_box); icon_theme_label.get_style_context()->add_class("dim-label"); vbox_icon_theme.add(icon_theme_back_button); vbox_icon_theme.add(separator8); vbox_icon_theme.add(icon_theme_label); vbox_icon_theme.add(icon_theme_list_scrolled_window); settings_popover.add(vbox_icon_theme); settings_popover.child_property_submenu(vbox_icon_theme) = "icon-theme"; settings_popover.show_all_children(); } /** * \brief Init all signals and connect them to functions */ void MainWindow::init_signals() { // Window signals signal_delete_event().connect(sigc::mem_fun(this, &MainWindow::delete_window)); draw_primary.signal_size_allocate().connect(sigc::mem_fun(this, &MainWindow::on_size_alloc)); // Table of contents close_toc_window_button.signal_clicked().connect(sigc::mem_fun(vbox_toc, &Gtk::Widget::hide)); toc_tree_view.signal_row_activated().connect(sigc::mem_fun(this, &MainWindow::on_toc_row_activated)); // Menu & toolbar signals menu.new_doc.connect(sigc::mem_fun(this, &MainWindow::new_doc)); /*!< Menu item for new document */ menu.open.connect(sigc::mem_fun(this, &MainWindow::open)); /*!< Menu item for opening existing document */ menu.open_edit.connect(sigc::mem_fun(this, &MainWindow::open_and_edit)); /*!< Menu item for opening & editing existing document */ menu.edit.connect(sigc::mem_fun(this, &MainWindow::edit)); /*!< Menu item for editing current open document */ menu.save.connect(sigc::mem_fun(this, &MainWindow::save)); /*!< Menu item for save document */ menu.save_as.connect(sigc::mem_fun(this, &MainWindow::save_as)); /*!< Menu item for save document as */ menu.publish.connect(sigc::mem_fun(this, &MainWindow::publish)); /*!< Menu item for publishing */ menu.quit.connect(sigc::mem_fun(this, &MainWindow::close)); /*!< close main window and therefor closes the app */ menu.undo.connect(sigc::mem_fun(draw_primary, &Draw::undo)); /*!< Menu item for undo text */ menu.redo.connect(sigc::mem_fun(draw_primary, &Draw::redo)); /*!< Menu item for redo text */ menu.cut.connect(sigc::mem_fun(this, &MainWindow::cut)); /*!< Menu item for cut text */ menu.copy.connect(sigc::mem_fun(this, &MainWindow::copy)); /*!< Menu item for copy text */ menu.paste.connect(sigc::mem_fun(this, &MainWindow::paste)); /*!< Menu item for paste text */ menu.del.connect(sigc::mem_fun(this, &MainWindow::del)); /*!< Menu item for deleting selected text */ menu.select_all.connect(sigc::mem_fun(this, &MainWindow::selectAll)); /*!< Menu item for selecting all text */ menu.find.connect(sigc::bind(sigc::mem_fun(this, &MainWindow::show_search), false)); /*!< Menu item for finding text */ menu.replace.connect(sigc::bind(sigc::mem_fun(this, &MainWindow::show_search), true)); /*!< Menu item for replacing text */ menu.back.connect(sigc::mem_fun(this, &MainWindow::back)); /*!< Menu item for previous page */ menu.forward.connect(sigc::mem_fun(this, &MainWindow::forward)); /*!< Menu item for next page */ menu.reload.connect(sigc::mem_fun(this, &MainWindow::refresh_request)); /*!< Menu item for reloading the page */ menu.home.connect(sigc::mem_fun(this, &MainWindow::go_home)); /*!< Menu item for home page */ menu.toc.connect(sigc::mem_fun(this, &MainWindow::show_toc)); /*!< Menu item for table of contents */ menu.source_code.connect(sigc::mem_fun(this, &MainWindow::show_source_code_dialog)); /*!< Source code dialog */ source_code_dialog.signal_response().connect(sigc::mem_fun(source_code_dialog, &SourceCodeDialog::hide_dialog)); /*!< Close source code dialog */ menu.about.connect(sigc::mem_fun(about, &About::show_about)); /*!< Display about dialog */ draw_primary.source_code.connect(sigc::mem_fun(this, &MainWindow::show_source_code_dialog)); /*!< Open source code dialog */ about.signal_response().connect(sigc::mem_fun(about, &About::hide_about)); /*!< Close about dialog */ address_bar.signal_activate().connect(sigc::mem_fun(this, &MainWindow::address_bar_activate)); /*!< User pressed enter the address bar */ open_toc_button.signal_clicked().connect(sigc::mem_fun(this, &MainWindow::show_toc)); /*!< Button for showing Table of Contents */ back_button.signal_clicked().connect(sigc::mem_fun(this, &MainWindow::back)); /*!< Button for previous page */ forward_button.signal_clicked().connect(sigc::mem_fun(this, &MainWindow::forward)); /*!< Button for next page */ refresh_button.signal_clicked().connect(sigc::mem_fun(this, &MainWindow::refresh_request)); /*!< Button for reloading the page */ home_button.signal_clicked().connect(sigc::mem_fun(this, &MainWindow::go_home)); /*!< Button for home page */ search_entry.signal_activate().connect(sigc::mem_fun(this, &MainWindow::on_search)); /*!< Execute the text search */ search_replace_entry.signal_activate().connect(sigc::mem_fun(this, &MainWindow::on_replace)); /*!< Execute the text replace */ // Editor toolbar buttons open_button.signal_clicked().connect(sigc::mem_fun(this, &MainWindow::open_and_edit)); save_button.signal_clicked().connect(sigc::mem_fun(this, &MainWindow::save)); publish_button.signal_clicked().connect(sigc::mem_fun(this, &MainWindow::publish)); cut_button.signal_clicked().connect(sigc::mem_fun(this, &MainWindow::cut)); copy_button.signal_clicked().connect(sigc::mem_fun(this, &MainWindow::copy)); paste_button.signal_clicked().connect(sigc::mem_fun(this, &MainWindow::paste)); undo_button.signal_clicked().connect(sigc::mem_fun(draw_primary, &Draw::undo)); redo_button.signal_clicked().connect(sigc::mem_fun(draw_primary, &Draw::redo)); headings_combo_box.signal_changed().connect(sigc::mem_fun(this, &MainWindow::get_heading)); bold_button.signal_clicked().connect(sigc::mem_fun(draw_primary, &Draw::make_bold)); italic_button.signal_clicked().connect(sigc::mem_fun(draw_primary, &Draw::make_italic)); strikethrough_button.signal_clicked().connect(sigc::mem_fun(draw_primary, &Draw::make_strikethrough)); super_button.signal_clicked().connect(sigc::mem_fun(draw_primary, &Draw::make_super)); sub_button.signal_clicked().connect(sigc::mem_fun(draw_primary, &Draw::make_sub)); link_button.signal_clicked().connect(sigc::mem_fun(draw_primary, &Draw::insert_link)); image_button.signal_clicked().connect(sigc::mem_fun(draw_primary, &Draw::insert_image)); emoji_button.signal_clicked().connect(sigc::mem_fun(this, &MainWindow::insert_emoji)); quote_button.signal_clicked().connect(sigc::mem_fun(draw_primary, &Draw::make_quote)); code_button.signal_clicked().connect(sigc::mem_fun(draw_primary, &Draw::make_code)); bullet_list_button.signal_clicked().connect(sigc::mem_fun(draw_primary, &Draw::insert_bullet_list)); numbered_list_button.signal_clicked().connect(sigc::mem_fun(draw_primary, &Draw::insert_numbered_list)); highlight_button.signal_clicked().connect(sigc::mem_fun(draw_primary, &Draw::make_highlight)); // Status pop-over buttons copy_id_button.signal_clicked().connect(sigc::mem_fun(this, &MainWindow::copy_client_id)); copy_public_key_button.signal_clicked().connect(sigc::mem_fun(this, &MainWindow::copy_client_public_key)); // Settings pop-over buttons zoom_out_button.signal_clicked().connect(sigc::mem_fun(this, &MainWindow::on_zoom_out)); zoom_restore_button.signal_clicked().connect(sigc::mem_fun(this, &MainWindow::on_zoom_restore)); zoom_in_button.signal_clicked().connect(sigc::mem_fun(this, &MainWindow::on_zoom_in)); font_button.signal_font_set().connect(sigc::mem_fun(this, &MainWindow::on_font_set)); max_content_width_spin_button.signal_value_changed().connect(sigc::mem_fun(this, &MainWindow::on_max_content_width_changed)); spacing_spin_button.signal_value_changed().connect(sigc::mem_fun(this, &MainWindow::on_spacing_changed)); margins_spin_button.signal_value_changed().connect(sigc::mem_fun(this, &MainWindow::on_margins_changed)); indent_spin_button.signal_value_changed().connect(sigc::mem_fun(this, &MainWindow::on_indent_changed)); wrap_none.signal_toggled().connect(sigc::bind(sigc::mem_fun(this, &MainWindow::on_wrap_toggled), Gtk::WrapMode::WRAP_NONE)); wrap_char.signal_toggled().connect(sigc::bind(sigc::mem_fun(this, &MainWindow::on_wrap_toggled), Gtk::WrapMode::WRAP_CHAR)); wrap_word.signal_toggled().connect(sigc::bind(sigc::mem_fun(this, &MainWindow::on_wrap_toggled), Gtk::WrapMode::WRAP_WORD)); wrap_word_char.signal_toggled().connect(sigc::bind(sigc::mem_fun(this, &MainWindow::on_wrap_toggled), Gtk::WrapMode::WRAP_WORD_CHAR)); theme_switch.property_active().signal_changed().connect(sigc::mem_fun(this, &MainWindow::on_theme_changed)); reader_view_switch.property_active().signal_changed().connect(sigc::mem_fun(this, &MainWindow::on_reader_view_changed)); icon_theme_list_box.signal_row_activated().connect(sigc::mem_fun(this, &MainWindow::on_icon_theme_activated)); about_button.signal_clicked().connect(sigc::mem_fun(about, &About::show_about)); } void MainWindow::init_mac_os() { #if defined(__APPLE__) { osx_app = (GtkosxApplication*)g_object_new(GTKOSX_TYPE_APPLICATION, NULL); // TODO: Should I implement those terminate signals. Sinse I disabled quartz accelerators MainWindow* mainWindow = this; g_signal_connect(osx_app, "NSApplicationWillTerminate", G_CALLBACK(osx_will_quit_cb), mainWindow); // TODO: Open file callback? // g_signal_connect (osx_app, "NSApplicationOpenFile", G_CALLBACK (osx_open_file_cb), mainWindow); menu.hide(); GtkWidget* menubar = (GtkWidget*)menu.gobj(); gtkosx_application_set_menu_bar(osx_app, GTK_MENU_SHELL(menubar)); // Use GTK accelerators gtkosx_application_set_use_quartz_accelerators(osx_app, FALSE); gtkosx_application_ready(osx_app); } #endif } /** * \brief Called when Window is closed/exited */ bool MainWindow::delete_window(GdkEventAny* any_event __attribute__((unused))) { if (settings) { // Save the schema settings settings->set_int("width", get_width()); settings->set_int("height", get_height()); settings->set_boolean("maximized", is_maximized()); if (paned_root.get_position() > 0) settings->set_int("position-divider-toc", paned_root.get_position()); // Only store a divider value bigger than zero, // because the secondary draw window is hidden by default, resulting into a zero value. if (paned_draw.get_position() > 0) settings->set_int("position-divider-draw", paned_draw.get_position()); // Fullscreen will be availible with gtkmm-4.0 // settings->set_boolean("fullscreen", is_fullscreen()); settings->set_string("font-family", font_family_); settings->set_int("font-size", current_font_size_); settings->set_int("max-content-width", content_max_width_); settings->set_double("spacing", font_spacing_); settings->set_int("margins", content_margin_); settings->set_int("indent", indent_); settings->set_enum("wrap-mode", wrap_mode_); settings->set_string("icon-theme", icon_theme_); settings->set_boolean("icon-gtk-theme", use_current_gtk_icon_theme_); settings->set_double("brightness", brightness_scale_); settings->set_boolean("dark-theme", use_dark_theme_); settings->set_boolean("reader-view", is_reader_view_enabled_); } return false; } /** * \brief Cut/copy/paste/delete/select all keybindings */ void MainWindow::cut() { if (draw_primary.has_focus()) { draw_primary.cut(); } else if (draw_secondary.has_focus()) { draw_secondary.cut(); } else if (address_bar.has_focus()) { address_bar.cut_clipboard(); } else if (search_entry.has_focus()) { search_entry.cut_clipboard(); } else if (search_replace_entry.has_focus()) { search_replace_entry.cut_clipboard(); } } void MainWindow::copy() { if (draw_primary.has_focus()) { draw_primary.copy(); } else if (draw_secondary.has_focus()) { draw_secondary.copy(); } else if (address_bar.has_focus()) { address_bar.copy_clipboard(); } else if (search_entry.has_focus()) { search_entry.copy_clipboard(); } else if (search_replace_entry.has_focus()) { search_replace_entry.copy_clipboard(); } } void MainWindow::paste() { if (draw_primary.has_focus()) { draw_primary.paste(); } else if (draw_secondary.has_focus()) { draw_secondary.paste(); } else if (address_bar.has_focus()) { address_bar.paste_clipboard(); } else if (search_entry.has_focus()) { search_entry.paste_clipboard(); } else if (search_replace_entry.has_focus()) { search_replace_entry.paste_clipboard(); } } void MainWindow::del() { if (draw_primary.has_focus()) { draw_primary.del(); } else if (draw_secondary.has_focus()) { draw_secondary.del(); } else if (address_bar.has_focus()) { int start, end; if (address_bar.get_selection_bounds(start, end)) { address_bar.delete_text(start, end); } else { ++end; address_bar.delete_text(start, end); } } else if (search_entry.has_focus()) { int start, end; if (search_entry.get_selection_bounds(start, end)) { search_entry.delete_text(start, end); } else { ++end; search_entry.delete_text(start, end); } } else if (search_replace_entry.has_focus()) { int start, end; if (search_replace_entry.get_selection_bounds(start, end)) { search_replace_entry.delete_text(start, end); } else { ++end; search_replace_entry.delete_text(start, end); } } } void MainWindow::selectAll() { if (draw_primary.has_focus()) { draw_primary.select_all(); } else if (draw_secondary.has_focus()) { draw_secondary.select_all(); } else if (address_bar.has_focus()) { address_bar.select_region(0, -1); } else if (search_entry.has_focus()) { search_entry.select_region(0, -1); } else if (search_replace_entry.has_focus()) { search_replace_entry.select_region(0, -1); } } /** * \brief Triggers when the textview widget changes size */ void MainWindow::on_size_alloc(__attribute__((unused)) Gdk::Rectangle& allocation) { if (!is_editor_enabled()) update_margins(); } /** * \brief Triggered when user clicked on the column in ToC */ void MainWindow::on_toc_row_activated(const Gtk::TreeModel::Path& path, __attribute__((unused)) Gtk::TreeViewColumn* column) { const auto iter = toc_tree_model->get_iter(path); if (iter) { const auto row = *iter; if (row[toc_columns.col_valid]) { Gtk::TextIter textIter = row[toc_columns.col_iter]; // Scroll to to mark iterator if (is_editor_enabled()) draw_secondary.scroll_to(textIter); else draw_primary.scroll_to(textIter); } } } /** * \brief Trigger when user selected 'new document' from menu item */ void MainWindow::new_doc() { // Clear content & path middleware_.reset_content_and_path(); // Enable editing mode enable_edit(); // Change address bar address_bar.set_text("file://unsaved"); // Set new title set_title("Untitled * - " + app_name_); } /** * \brief Triggered when user selected 'open...' from menu item / toolbar */ void MainWindow::open() { auto dialog = new Gtk::FileChooserDialog("Open", Gtk::FILE_CHOOSER_ACTION_OPEN); dialog->set_transient_for(*this); dialog->set_modal(true); dialog->signal_response().connect(sigc::bind(sigc::mem_fun(*this, &MainWindow::on_open_dialog_response), dialog)); dialog->add_button("_Cancel", Gtk::ResponseType::RESPONSE_CANCEL); dialog->add_button("_Open", Gtk::ResponseType::RESPONSE_OK); // Add filters, so that only certain file types can be selected: #ifdef __linux__ auto filterMarkdown = Gtk::FileFilter::create(); filterMarkdown->set_name("All Markdown files"); filterMarkdown->add_mime_type("text/markdown"); dialog->add_filter(filterMarkdown); #endif auto filterMarkdownExt = Gtk::FileFilter::create(); filterMarkdownExt->set_name("All Markdown files extension (*.md)"); filterMarkdownExt->add_pattern("*.md"); dialog->add_filter(filterMarkdownExt); auto filterTextFiles = Gtk::FileFilter::create(); filterTextFiles->set_name("All text files"); filterTextFiles->add_mime_type("text/plain"); dialog->add_filter(filterTextFiles); auto filterAny = Gtk::FileFilter::create(); filterAny->set_name("Any files"); filterAny->add_pattern("*"); dialog->add_filter(filterAny); dialog->show(); // Finally, show the open dialog } /** * \brief Triggered when user selected 'open & edit...' from menu item */ void MainWindow::open_and_edit() { auto dialog = new Gtk::FileChooserDialog("Open & Edit", Gtk::FILE_CHOOSER_ACTION_OPEN); dialog->set_transient_for(*this); dialog->set_modal(true); dialog->signal_response().connect(sigc::bind(sigc::mem_fun(*this, &MainWindow::on_open_edit_dialog_response), dialog)); dialog->add_button("_Cancel", Gtk::ResponseType::RESPONSE_CANCEL); dialog->add_button("_Open", Gtk::ResponseType::RESPONSE_OK); // Add filters, so that only certain file types can be selected: #ifdef __linux__ auto filterMarkdown = Gtk::FileFilter::create(); filterMarkdown->set_name("All Markdown files"); filterMarkdown->add_mime_type("text/markdown"); dialog->add_filter(filterMarkdown); #endif auto filterMarkdownExt = Gtk::FileFilter::create(); filterMarkdownExt->set_name("All Markdown files extension (*.md)"); filterMarkdownExt->add_pattern("*.md"); dialog->add_filter(filterMarkdownExt); auto filterTextFiles = Gtk::FileFilter::create(); filterTextFiles->set_name("All text files"); filterTextFiles->add_mime_type("text/plain"); dialog->add_filter(filterTextFiles); auto filterAny = Gtk::FileFilter::create(); filterAny->set_name("Any files"); filterAny->add_pattern("*"); dialog->add_filter(filterAny); dialog->show(); // Finally, show the open & edit dialog } /** * \brief Signal response when 'open' dialog is closed */ void MainWindow::on_open_dialog_response(int response_id, Gtk::FileChooserDialog* dialog) { switch (response_id) { case Gtk::ResponseType::RESPONSE_OK: { auto filePath = dialog->get_file()->get_path(); // Open file, set address bar & disable editor if needed middleware_.do_request("file://" + filePath); break; } case Gtk::ResponseType::RESPONSE_CANCEL: { break; } default: { std::cerr << "WARN: Unexpected button clicked." << std::endl; break; } } delete dialog; } /** * \brief Signal response when 'open & edit' dialog is closed */ void MainWindow::on_open_edit_dialog_response(int response_id, Gtk::FileChooserDialog* dialog) { switch (response_id) { case Gtk::ResponseType::RESPONSE_OK: { // Enable editor if needed if (!is_editor_enabled()) enable_edit(); auto filePath = dialog->get_file()->get_path(); std::string path = "file://" + filePath; // Open file and set address bar, but do not parse the content or the disable editor middleware_.do_request(path, true, false, false, false); // Set current file path for the 'save' feature current_file_saved_path_ = filePath; break; } case Gtk::ResponseType::RESPONSE_CANCEL: { break; } default: { std::cerr << "WARN: Unexpected button clicked." << std::endl; break; } } delete dialog; } /** * \brief Triggered when user selected 'edit' from menu item */ void MainWindow::edit() { if (!is_editor_enabled()) enable_edit(); draw_primary.set_text(middleware_.get_content()); // Set title set_title("Untitled * - " + app_name_); } /** * \brief Triggered when user selected 'save' from menu item / toolbar */ void MainWindow::save() { if (current_file_saved_path_.empty()) { save_as(); } else { if (is_editor_enabled()) { try { middleware_.do_write(current_file_saved_path_); } catch (std::ios_base::failure& error) { std::cerr << "ERROR: Could not write file: " << current_file_saved_path_ << ". Message: " << error.what() << ".\nError code: " << error.code() << std::endl; } } else { std::cerr << "ERROR: Saving while \"file saved path\" is filled and editor is disabled should not happen!?" << std::endl; } } } /** * \brief Triggered when 'save as..' menu item is selected or the user saves the file for the first time via 'save' */ void MainWindow::save_as() { auto dialog = new Gtk::FileChooserDialog("Save", Gtk::FILE_CHOOSER_ACTION_SAVE); dialog->set_transient_for(*this); dialog->set_modal(true); dialog->set_do_overwrite_confirmation(true); dialog->signal_response().connect(sigc::bind(sigc::mem_fun(*this, &MainWindow::on_save_as_dialog_response), dialog)); dialog->add_button("_Cancel", Gtk::ResponseType::RESPONSE_CANCEL); dialog->add_button("_Save", Gtk::ResponseType::RESPONSE_OK); // Add filters, so that only certain file types can be selected: auto filterMarkdownExt = Gtk::FileFilter::create(); filterMarkdownExt->set_name("All Markdown files"); filterMarkdownExt->add_pattern("*.md"); dialog->add_filter(filterMarkdownExt); auto filterTextFiles = Gtk::FileFilter::create(); filterTextFiles->set_name("All text files"); filterTextFiles->add_mime_type("text/plain"); dialog->add_filter(filterTextFiles); auto filterAny = Gtk::FileFilter::create(); filterAny->set_name("Any files"); filterAny->add_pattern("*"); dialog->add_filter(filterAny); // If user is saving as an existing file, set the current uri path if (!current_file_saved_path_.empty()) { try { dialog->set_uri(Glib::filename_to_uri(current_file_saved_path_)); } catch (Glib::Error& error) { std::cerr << "ERROR: Incorrect filename most likely. Message: " << error.what() << ". Error Code: " << error.code() << std::endl; } } dialog->show(); // Finally, show save as dialog } /** * \brief Signal response when 'save as' dialog is closed */ void MainWindow::on_save_as_dialog_response(int response_id, Gtk::FileChooserDialog* dialog) { switch (response_id) { case Gtk::ResponseType::RESPONSE_OK: { auto filePath = dialog->get_file()->get_path(); if (!filePath.ends_with(".md")) filePath.append(".md"); // Save current content to file path try { middleware_.do_write(filePath, is_editor_enabled()); // Only update address & title, when editor mode is enabled // Only if editor mode is enabled if (is_editor_enabled()) { // Set/update the current file saved path variable (used for the 'save' feature) current_file_saved_path_ = filePath; } } catch (std::ios_base::failure& error) { std::cerr << "ERROR: Could not write file: " << filePath << ". Message: " << error.what() << ".\nError code: " << error.code() << std::endl; } break; } case Gtk::ResponseType::RESPONSE_CANCEL: { break; } default: { std::cerr << "ERROR: Unexpected button clicked." << std::endl; break; } } delete dialog; } /** * \brief Triggered when user selected the 'Publish...' menu item or publish button in the toolbar */ void MainWindow::publish() { int result = Gtk::RESPONSE_YES; // By default continue if (middleware_.get_content().empty()) { Gtk::MessageDialog dialog(*this, "Are you sure you want to publish empty content?", true, Gtk::MESSAGE_QUESTION, Gtk::BUTTONS_YES_NO); dialog.set_title("Are you sure?"); dialog.set_default_response(Gtk::RESPONSE_NO); result = dialog.run(); } // Continue ... if (result == Gtk::RESPONSE_YES) { std::string path = "new_file.md"; // Retrieve filename from saved file (if present) if (!current_file_saved_path_.empty()) { path = current_file_saved_path_; } else { // TODO: path is not defined yet. however, this may change anyway once we try to build more complex // websites, needing to use directory structures. } try { // Add content to IPFS std::string cid = middleware_.do_add(path); if (cid.empty()) { throw std::runtime_error("CID hash is empty."); } // Show dialog content_published_dialog.reset(new Gtk::MessageDialog(*this, "File is successfully added to IPFS!")); content_published_dialog->set_secondary_text("The content is now available on the decentralized web, via:"); // Add custom label Gtk::Label* label = Gtk::manage(new Gtk::Label("ipfs://" + cid)); label->set_selectable(true); Gtk::Box* box = content_published_dialog->get_content_area(); box->pack_end(*label); content_published_dialog->set_modal(true); // content_published_dialog->set_hide_on_close(true); available in gtk-4.0 content_published_dialog->signal_response().connect(sigc::hide(sigc::mem_fun(*content_published_dialog, &Gtk::Widget::hide))); content_published_dialog->show_all(); } catch (const std::runtime_error& error) { content_published_dialog.reset(new Gtk::MessageDialog(*this, "File could not be added to IPFS", false, Gtk::MESSAGE_ERROR)); content_published_dialog->set_secondary_text("Error message: " + std::string(error.what())); content_published_dialog->set_modal(true); // content_published_dialog->set_hide_on_close(true); available in gtk-4.0 content_published_dialog->signal_response().connect(sigc::hide(sigc::mem_fun(*content_published_dialog, &Gtk::Widget::hide))); content_published_dialog->show(); } } } /** * \brief Show homepage */ void MainWindow::go_home() { middleware_.do_request("about:home", true, false, true); } /** * \brief Show/hide table of contents */ void MainWindow::show_toc() { if (vbox_toc.is_visible()) vbox_toc.hide(); else vbox_toc.show(); } /** * \brief Copy the IPFS Client ID to clipboard */ void MainWindow::copy_client_id() { if (!middleware_.get_ipfs_client_id().empty()) { get_clipboard("CLIPBOARD")->set_text(middleware_.get_ipfs_client_id()); show_notification("Copied to clipboard", "Your client ID is now copied to your clipboard."); } else { std::cerr << "WARNING: IPFS client ID has not been set yet. Skip clipboard action." << std::endl; } } /** * \brief Copy IPFS Client public key to clipboard */ void MainWindow::copy_client_public_key() { if (!middleware_.get_ipfs_client_public_key().empty()) { get_clipboard("CLIPBOARD")->set_text(middleware_.get_ipfs_client_public_key()); show_notification("Copied to clipboard", "Your client public key is now copied to your clipboard."); } else { std::cerr << "WARNING: IPFS client public key has not been set yet. Skip clipboard action." << std::endl; } } /** * \brief Trigger when pressed enter in the search entry */ void MainWindow::on_search() { // Forward search, find and select std::string text = search_entry.get_text(); auto buffer = draw_primary.get_buffer(); Gtk::TextBuffer::iterator iter = buffer->get_iter_at_mark(buffer->get_mark("insert")); Gtk::TextBuffer::iterator start, end; bool matchCase = search_match_case.get_active(); Gtk::TextSearchFlags flags = Gtk::TextSearchFlags::TEXT_SEARCH_TEXT_ONLY; if (!matchCase) { flags |= Gtk::TextSearchFlags::TEXT_SEARCH_CASE_INSENSITIVE; } if (iter.forward_search(text, flags, start, end)) { buffer->select_range(end, start); draw_primary.scroll_to(start); } else { buffer->place_cursor(buffer->begin()); // Try another search directly from the top Gtk::TextBuffer::iterator secondIter = buffer->get_iter_at_mark(buffer->get_mark("insert")); if (secondIter.forward_search(text, flags, start, end)) { buffer->select_range(end, start); draw_primary.scroll_to(start); } } } /** * \brief Trigger when user pressed enter in the replace entry */ void MainWindow::on_replace() { if (draw_primary.get_editable()) { auto buffer = draw_primary.get_buffer(); Gtk::TextBuffer::iterator startIter = buffer->get_iter_at_mark(buffer->get_mark("insert")); Gtk::TextBuffer::iterator endIter = buffer->get_iter_at_mark(buffer->get_mark("selection_bound")); if (startIter != endIter) { // replace std::string replace = search_replace_entry.get_text(); buffer->begin_user_action(); buffer->erase(startIter, endIter); buffer->insert_at_cursor(replace); buffer->end_user_action(); } on_search(); } } /** * \brief Triggers when pressed enter in the address bar */ void MainWindow::address_bar_activate() { middleware_.do_request(address_bar.get_text(), false); // When user actually entered the address bar, focus on the primary draw draw_primary.grab_focus(); } /** * \brief Triggers when user tries to search or replace text */ void MainWindow::show_search(bool replace) { if (search_popover.is_visible() && search_replace_entry.is_visible()) { if (replace) { search_popover.hide(); address_bar.grab_focus(); search_replace_entry.hide(); } else { search_entry.grab_focus(); search_replace_entry.hide(); } } else if (search_popover.is_visible()) { if (replace) { search_replace_entry.show(); } else { search_popover.hide(); address_bar.grab_focus(); search_replace_entry.hide(); } } else { search_popover.show(); search_entry.grab_focus(); if (replace) { search_replace_entry.show(); } else { search_replace_entry.hide(); } } } void MainWindow::back() { if (current_history_index_ > 0) { current_history_index_--; middleware_.do_request(history_.at(current_history_index_), true, true); } } void MainWindow::forward() { if (current_history_index_ < history_.size() - 1) { current_history_index_++; middleware_.do_request(history_.at(current_history_index_), true, true); } } /** * \brief Fill-in table of contents and show */ void MainWindow::set_table_of_contents(std::vector> headings) { Gtk::TreeRow heading1Row, heading2Row, heading3Row, heading4Row, heading5Row; int previousLevel = 1; // Default heading 1 for (const Glib::RefPtr& headerMark : headings) { Glib::ustring heading = static_cast(headerMark->get_data("name")); auto level = reinterpret_cast(headerMark->get_data("level")); switch (level) { case 1: { heading1Row = *(toc_tree_model->append()); heading1Row[toc_columns.col_iter] = headerMark->get_iter(); heading1Row[toc_columns.col_level] = level; heading1Row[toc_columns.col_heading] = heading; heading1Row[toc_columns.col_valid] = true; // Reset if (previousLevel > 1) { heading2Row = Gtk::TreeRow(); heading3Row = Gtk::TreeRow(); heading4Row = Gtk::TreeRow(); heading5Row = Gtk::TreeRow(); } break; } case 2: { if (heading1Row->get_model_gobject() == nullptr) { // Add missing heading as top-level heading1Row = *(toc_tree_model->append()); heading1Row[toc_columns.col_level] = 1; heading1Row[toc_columns.col_heading] = "-Missing heading-"; heading1Row[toc_columns.col_valid] = false; } heading2Row = *(toc_tree_model->append(heading1Row.children())); heading2Row[toc_columns.col_iter] = headerMark->get_iter(); heading2Row[toc_columns.col_level] = level; heading2Row[toc_columns.col_heading] = heading; heading2Row[toc_columns.col_valid] = true; // Reset if (previousLevel > 2) { heading3Row = Gtk::TreeRow(); heading4Row = Gtk::TreeRow(); heading5Row = Gtk::TreeRow(); } break; } case 3: { if (heading2Row->get_model_gobject() == nullptr) { // Add missing heading as top-level heading2Row = *(toc_tree_model->append(heading1Row.children())); heading2Row[toc_columns.col_level] = 2; heading2Row[toc_columns.col_heading] = "-Missing heading-"; heading2Row[toc_columns.col_valid] = false; } heading3Row = *(toc_tree_model->append(heading2Row.children())); heading3Row[toc_columns.col_iter] = headerMark->get_iter(); heading3Row[toc_columns.col_level] = level; heading3Row[toc_columns.col_heading] = heading; heading3Row[toc_columns.col_valid] = true; // Reset if (previousLevel > 3) { heading4Row = Gtk::TreeRow(); heading5Row = Gtk::TreeRow(); } break; } case 4: { if (heading3Row->get_model_gobject() == nullptr) { // Add missing heading as top-level heading3Row = *(toc_tree_model->append(heading2Row.children())); heading3Row[toc_columns.col_level] = 3; heading3Row[toc_columns.col_heading] = "-Missing heading-"; heading3Row[toc_columns.col_valid] = false; } heading4Row = *(toc_tree_model->append(heading3Row.children())); heading4Row[toc_columns.col_iter] = headerMark->get_iter(); heading4Row[toc_columns.col_level] = level; heading4Row[toc_columns.col_heading] = heading; heading4Row[toc_columns.col_valid] = true; // Reset if (previousLevel > 4) { heading5Row = Gtk::TreeRow(); } break; } case 5: { if (heading4Row->get_model_gobject() == nullptr) { // Add missing heading as top-level heading4Row = *(toc_tree_model->append(heading3Row.children())); heading4Row[toc_columns.col_level] = 4; heading4Row[toc_columns.col_heading] = "-Missing heading-"; heading4Row[toc_columns.col_valid] = false; } heading5Row = *(toc_tree_model->append(heading4Row.children())); heading5Row[toc_columns.col_iter] = headerMark->get_iter(); heading5Row[toc_columns.col_level] = level; heading5Row[toc_columns.col_heading] = heading; heading5Row[toc_columns.col_valid] = true; break; } case 6: { if (heading5Row->get_model_gobject() == nullptr) { // Add missing heading as top-level heading5Row = *(toc_tree_model->append(heading4Row.children())); heading5Row[toc_columns.col_level] = 5; heading5Row[toc_columns.col_heading] = "- Missing heading -"; heading5Row[toc_columns.col_valid] = false; } auto heading6Row = *(toc_tree_model->append(heading5Row.children())); heading6Row[toc_columns.col_iter] = headerMark->get_iter(); heading6Row[toc_columns.col_level] = level; heading6Row[toc_columns.col_heading] = heading; heading6Row[toc_columns.col_valid] = true; break; } default: std::cerr << "ERROR: Out of range heading level detected." << std::endl; break; } previousLevel = level; } toc_tree_view.columns_autosize(); toc_tree_view.expand_all(); } /** * \brief Determing if browser is installed to the installation directory at runtime * \return true if the current running process is installed (to the installed prefix path) */ bool MainWindow::is_installed() { char* path = NULL; int length; length = wai_getExecutablePath(NULL, 0, NULL); if (length > 0) { path = (char*)malloc(length + 1); if (!path) { std::cerr << "ERROR: Couldn't create executable path." << std::endl; } else { bool is_installed = true; wai_getExecutablePath(path, length, NULL); path[length] = '\0'; #if defined(_WIN32) // Does the executable path starts with "C:\Program"? const char* windowsPrefix = "C:\\Program"; is_installed = (strncmp(path, windowsPrefix, strlen(windowsPrefix)) == 0); #elif defined(_APPLE_) // Does the executable path contains "Applications"? const char* macOsNeedle = "Applications"; is_installed = (strstr(path, macOsNeedle) != NULL); #elif defined(__linux__) // Does the executable path starts with "/usr/local"? is_installed = (strncmp(path, INSTALL_PREFIX, strlen(INSTALL_PREFIX)) == 0); #endif free(path); return is_installed; } } return true; // fallback; assume always installed } /** * \brief Enable editor mode. Allowing to create or edit existing documents */ void MainWindow::enable_edit() { // Inform the Draw class that we are creating a new document, // will apply change some textview setting changes draw_primary.new_document(); // Show editor toolbars hbox_standard_editor_toolbar.show(); hbox_formatting_editor_toolbar.show(); // Enable monospace in editor draw_primary.set_monospace(true); // Apply some settings from primary to secondary window draw_secondary.set_indent(indent_); draw_secondary.set_wrap_mode(wrap_mode_); draw_secondary.set_left_margin(content_margin_); draw_secondary.set_right_margin(content_margin_); // Determine position of divider between the primary and secondary windows int currentWidth = get_width(); int maxWidth = currentWidth - 40; // Recalculate the position divider if it's too big, // or position_divider_draw_ is still on default value if ((paned_draw.get_position() >= maxWidth) || position_divider_draw_ == -1) { int proposedPosition = position_divider_draw_; // Try to first use the gsettings if ((proposedPosition == -1) || (proposedPosition >= maxWidth)) { proposedPosition = static_cast(currentWidth / 2.0); } paned_draw.set_position(proposedPosition); } // Enabled secondary text view (on the right) scrolled_window_secondary.show(); // Disable "view source" menu item draw_primary.set_view_source_menu_item(false); // Connect changed signal text_changed_signal_handler_ = draw_primary.get_buffer()->signal_changed().connect(sigc::mem_fun(this, &MainWindow::editor_changed_text)); // Enable publish menu item menu.set_publish_menu_sensitive(true); // Disable edit menu item (you are already editing) menu.set_edit_menu_sensitive(false); // Just to be sure, disable the spinning animation refresh_icon.get_style_context()->remove_class("spinning"); } /** * \brief Disable editor mode */ void MainWindow::disable_edit() { if (is_editor_enabled()) { hbox_standard_editor_toolbar.hide(); hbox_formatting_editor_toolbar.hide(); scrolled_window_secondary.hide(); // Disconnect text changed signal text_changed_signal_handler_.disconnect(); // Disable monospace draw_primary.set_monospace(false); // Re-apply settings on primary window draw_primary.set_indent(indent_); draw_primary.set_wrap_mode(wrap_mode_); // Show "view source" menu item again draw_primary.set_view_source_menu_item(true); draw_secondary.clear(); // Disable publish menu item menu.set_publish_menu_sensitive(false); // Enable edit menu item menu.set_edit_menu_sensitive(true); // Empty current file saved path current_file_saved_path_ = ""; } } /** * \brief Check if editor is enabled * \return true if enabled, otherwise false */ bool MainWindow::is_editor_enabled() { return hbox_standard_editor_toolbar.is_visible(); } /** * \brief Retrieve image path from icon theme location * \param icon_name Icon name (.png is added default) * \param typeof_icon Type of the icon is the sub-folder within the icons directory (eg. "editor", "arrows" or "basic") * \return full path of the icon PNG image */ std::string MainWindow::get_icon_image_from_theme(const std::string& icon_name, const std::string& typeof_icon) { // Use data directory first, used when LibreWeb is installed (Linux or Windows) for (std::string data_dir : Glib::get_system_data_dirs()) { std::vector path_builder{data_dir, "libreweb", "images", "icons", icon_theme_, typeof_icon, icon_name + ".png"}; std::string file_path = Glib::build_path(G_DIR_SEPARATOR_S, path_builder); if (Glib::file_test(file_path, Glib::FileTest::FILE_TEST_IS_REGULAR)) { return file_path; } } // Try local path if the images are not (yet) installed // When working directory is in the build/bin folder (relative path) std::vector path_builder{"..", "..", "images", "icons", icon_theme_, typeof_icon, icon_name + ".png"}; std::string file_path = Glib::build_path(G_DIR_SEPARATOR_S, path_builder); if (Glib::file_test(file_path, Glib::FileTest::FILE_TEST_IS_REGULAR)) { return file_path; } else { return ""; } } /** * \brief Calculate & update margins on primary draw */ void MainWindow::update_margins() { if (is_editor_enabled()) { draw_secondary.set_left_margin(content_margin_); draw_secondary.set_right_margin(content_margin_); } else { if (is_reader_view_enabled_) { int width = draw_primary.get_width(); if (width > (content_max_width_ + (2 * content_margin_))) { // Calculate margins on the fly int margin = (width - content_max_width_) / 2; draw_primary.set_left_margin(margin); draw_primary.set_right_margin(margin); } else { draw_primary.set_left_margin(content_margin_); draw_primary.set_right_margin(content_margin_); } } else { draw_primary.set_left_margin(content_margin_); draw_primary.set_right_margin(content_margin_); } } } /** * \brief Update the CSS provider data */ void MainWindow::update_css() { std::string colorCss; double darknessScale = (1.0 - brightness_scale_); std::ostringstream darknessDoubleStream; darknessDoubleStream << darknessScale; std::string darknessStr = darknessDoubleStream.str(); // If it's getting to dark, let's change the font color to white if (darknessScale >= 0.7) { double colorDouble = ((((1.0 - darknessScale) - 0.5) * (20.0 - 255.0)) / (1.0 - 0.5)) + 255.0; std::ostringstream colorStream; colorStream << colorDouble; std::string colorStr = colorStream.str(); colorCss = "color: rgba(" + colorStr + ", " + colorStr + ", " + colorStr + ", " + darknessStr + ");"; } std::stringstream streamFontSpacing; streamFontSpacing << std::fixed << std::setprecision(1) << font_spacing_; std::string letterSpacing = streamFontSpacing.str(); try { draw_css_provider->load_from_data("textview { " "font-family: \"" + font_family_ + "\";" "font-size: " + std::to_string(current_font_size_) + "pt; }" + "textview text { " + colorCss + "background-color: rgba(0, 0, 0," + darknessStr + ");" "letter-spacing: " + letterSpacing + "px; }"); } catch (const Gtk::CssProviderError& error) { std::cerr << "ERROR: Could not apply CSS format, error: " << error.what() << std::endl; } } /** * \brief Show Gio notification * \param title Title of the notification * \param message The message displayed along with the notificiation */ void MainWindow::show_notification(const Glib::ustring& title, const Glib::ustring& message) { // TODO: Report GLib-CRITICAL upstream to GTK (this is not my issue) auto notification = Gio::Notification::create(title); auto icon = Gio::ThemedIcon::create("dialog-information"); notification->set_body(message); notification->set_icon(icon); get_application()->send_notification(notification); } void MainWindow::editor_changed_text() { // TODO: Just execute the code below in a signal_idle call? // So it will never block the GUI thread. Or is this already running in another context // Clear table of contents (ToC) toc_tree_model->clear(); // Retrieve text from editor and parse the markdown contents middleware_.set_content(draw_primary.get_text()); cmark_node* doc = middleware_.parse_content(); /* // Can be enabled to show the markdown format in terminal: std::string md = Parser::render_markdown(doc); std::cout << "Markdown:\n" << md << std::endl;*/ // Show the document as a preview on the right side text-view panel draw_secondary.set_document(doc); set_table_of_contents(draw_secondary.get_headings()); } /** * \brief Show source code dialog window with the current content */ void MainWindow::show_source_code_dialog() { source_code_dialog.set_text(middleware_.get_content()); source_code_dialog.run(); } /** * \brief Retrieve selected heading from combobox. * Send to main Draw class */ void MainWindow::get_heading() { std::string active = headings_combo_box.get_active_id(); headings_combo_box.set_active(0); // Reset if (active != "") { std::string::size_type sz; try { int headingLevel = std::stoi(active, &sz, 10); draw_primary.make_heading(headingLevel); } catch (const std::invalid_argument&) { // ignore std::cerr << "ERROR: heading combobox id is invalid (not a number)." << std::endl; } catch (const std::out_of_range&) { // ignore } } } void MainWindow::insert_emoji() { // Note: The "insert-emoji" signal is not exposed in Gtkmm library (at least not in gtk3) g_signal_emit_by_name(draw_primary.gobj(), "insert-emoji"); } void MainWindow::on_zoom_out() { current_font_size_ -= 1; update_css(); zoom_restore_button.set_sensitive(current_font_size_ != default_font_size_); } void MainWindow::on_zoom_restore() { current_font_size_ = default_font_size_; // reset update_css(); zoom_restore_button.set_sensitive(false); } void MainWindow::on_zoom_in() { current_font_size_ += 1; update_css(); zoom_restore_button.set_sensitive(current_font_size_ != default_font_size_); } void MainWindow::on_font_set() { Pango::FontDescription fontDesc = Pango::FontDescription(font_button.get_font_name()); font_family_ = fontDesc.get_family(); current_font_size_ = default_font_size_ = (fontDesc.get_size_is_absolute()) ? fontDesc.get_size() : fontDesc.get_size() / PANGO_SCALE; update_css(); } void MainWindow::on_max_content_width_changed() { content_max_width_ = max_content_width_spin_button.get_value_as_int(); if (!is_editor_enabled()) update_margins(); } void MainWindow::on_spacing_changed() { font_spacing_ = spacing_spin_button.get_value(); // Letter-spacing update_css(); } void MainWindow::on_margins_changed() { content_margin_ = margins_spin_button.get_value_as_int(); update_margins(); } void MainWindow::on_indent_changed() { indent_ = indent_spin_button.get_value_as_int(); if (is_editor_enabled()) draw_secondary.set_indent(indent_); else draw_primary.set_indent(indent_); } void MainWindow::on_wrap_toggled(Gtk::WrapMode mode) { wrap_mode_ = mode; if (is_editor_enabled()) draw_secondary.set_wrap_mode(wrap_mode_); else draw_primary.set_wrap_mode(wrap_mode_); } void MainWindow::on_brightness_changed() { brightness_scale_ = scale_settings_brightness.get_value(); update_css(); } void MainWindow::on_theme_changed() { // Switch between dark or light theme preference use_dark_theme_ = theme_switch.get_active(); set_theme(); } void MainWindow::on_reader_view_changed() { is_reader_view_enabled_ = reader_view_switch.get_active(); if (!is_editor_enabled()) update_margins(); } void MainWindow::on_icon_theme_activated(Gtk::ListBoxRow* row) { std::string themeName = static_cast(row->get_data("value")); if (themeName != "none") { icon_theme_ = themeName; use_current_gtk_icon_theme_ = false; } else { use_current_gtk_icon_theme_ = true; } // Reload icons load_icons(); // Trigger IPFS status icon update_status_popover_and_icon(); }