LibreWeb-Browser/src/main-window.cc

2363 lines
86 KiB
C++

#include "main-window.h"
#include "menu.h"
#include "project_config.h"
#include <cstdint>
#include <gdk-pixbuf/gdk-pixbuf.h>
#include <giomm/file.h>
#include <giomm/notification.h>
#include <giomm/settingsschemasource.h>
#include <giomm/themedicon.h>
#include <glibmm/convert.h>
#include <glibmm/fileutils.h>
#include <glibmm/miscutils.h>
#include <gtkmm/image.h>
#include <gtkmm/listboxrow.h>
#include <gtkmm/menuitem.h>
#include <gtkmm/settings.h>
#include <iostream>
#include <nlohmann/json.hpp>
#include <sstream>
#include <string.h>
#include <whereami.h>
#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("<b>" + networkStatus + "</b>");
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<std::string> 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<Gtk::WrapMode>(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<Gtk::Label*>(icon_theme_button.get_child());
iconThemeButtonlabel->set_xalign(0.0);
Gtk::Label* aboutButtonLabel = dynamic_cast<Gtk::Label*>(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 <b>empty</b> 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<Glib::RefPtr<Gtk::TextMark>> headings)
{
Gtk::TreeRow heading1Row, heading2Row, heading3Row, heading4Row, heading5Row;
int previousLevel = 1; // Default heading 1
for (const Glib::RefPtr<Gtk::TextMark>& headerMark : headings)
{
Glib::ustring heading = static_cast<char*>(headerMark->get_data("name"));
auto level = reinterpret_cast<std::intptr_t>(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<int>(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<std::string> 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<std::string> 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<char*>(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();
}