panthera-www.cc
C++ source, ASCII text
1#include "gpanthera.hh" 2#include <gtkmm.h> 3#include <glibmm.h> 4#include <glibmm/ustring.h> 5#include <iostream> 6#include <memory> 7#include <libintl.h> 8#include <locale.h> 9#include <gtk/gtk.h> 10#include <gdk/gdk.h> 11#include <webkit/webkit.h> 12#include <fstream> 13#include <nlohmann/json.hpp> 14#include <iomanip> 15#include <sstream> 16#include <dlfcn.h> 17#include "external/sqlite_orm/sqlite_orm.h" 18#include "plugin_api.h" 19 20using plugin_entrypoint_type = void(*)(void); 21 22std::string url_encode(const std::string &value) { 23// Thanks https://stackoverflow.com/a/17708801 24std::ostringstream escaped; 25escaped.fill('0'); 26escaped << std::hex; 27 28for(char c : value) { 29// Keep alphanumeric and other accepted characters intact 30if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') { 31escaped << c; 32continue; 33} 34 35// Any other characters are percent-encoded 36escaped << std::uppercase; 37escaped << '%' << std::setw(2) << int((unsigned char) c); 38escaped << std::nouppercase; 39} 40 41return escaped.str(); 42} 43 44struct SearchEngine { 45Glib::ustring name; 46Glib::ustring url; 47Glib::ustring get_search_url(const std::string &query) { 48auto pos = url.find("%s"); 49if(pos == Glib::ustring::npos) { 50throw std::runtime_error("Invalid search engine URL: missing '%s' placeholder"); 51} 52auto new_url = url; 53new_url.replace(pos, 2, url_encode(query)); 54return new_url; 55} 56}; 57 58struct HistoryEntry { 59int id; 60std::string url; 61int64_t timestamp; 62}; 63 64using StorageType = decltype(sqlite_orm::make_storage("", 65sqlite_orm::make_table("history", 66sqlite_orm::make_column("id", &HistoryEntry::id, sqlite_orm::primary_key()), 67sqlite_orm::make_column("url", &HistoryEntry::url), 68sqlite_orm::make_column("timestamp", &HistoryEntry::timestamp) 69) 70)); 71 72class HistoryManager { 73static auto make_storage(const std::string& path) { 74return sqlite_orm::make_storage(path, 75sqlite_orm::make_table("history", 76sqlite_orm::make_column("id", &HistoryEntry::id, sqlite_orm::primary_key()), 77sqlite_orm::make_column("url", &HistoryEntry::url), 78sqlite_orm::make_column("timestamp", &HistoryEntry::timestamp) 79) 80); 81} 82StorageType storage; 83public: 84explicit HistoryManager(const std::string &db_path) : storage(make_storage(db_path)) { 85storage.sync_schema(); 86} 87 88void log_url(const std::string &url, const int64_t timestamp = std::chrono::system_clock::now().time_since_epoch().count()) { 89storage.insert(HistoryEntry{-1, url, timestamp}); 90} 91 92std::vector<HistoryEntry> get_history() { 93return storage.get_all<HistoryEntry>(); 94} 95}; 96 97class PantheraWww : public Gtk::Application { 98Gtk::Window *window = Gtk::make_managed<Gtk::Window>(); 99protected: 100std::shared_ptr<gPanthera::LayoutManager> layout_manager; 101std::shared_ptr<gPanthera::ContentManager> content_manager; 102Gtk::Entry *url_bar = nullptr; 103std::string cookie_file = "cookies.txt"; 104std::vector<SearchEngine> search_engines; 105SearchEngine *default_search_engine = nullptr; 106std::shared_ptr<HistoryManager> history_manager; 107 108static void notify_callback(GObject *object, GParamSpec *pspec, gpointer data) { 109if(!gtk_widget_get_parent(GTK_WIDGET(object))) { 110return; 111} 112if(auto self = static_cast<PantheraWww*>(data)) { 113auto parent = gtk_widget_get_parent(gtk_widget_get_parent(GTK_WIDGET(object))); 114if(auto page = dynamic_cast<gPanthera::ContentPage*>(Glib::wrap(parent))) { 115if(g_strcmp0(pspec->name, "title") == 0) { 116if(auto label = dynamic_cast<Gtk::Label*>(page->tab_widget->get_last_child())) { 117if(strlen(webkit_web_view_get_title(WEBKIT_WEB_VIEW(object))) == 0) { 118label->set_label("Untitled"); 119} else { 120label->set_label(webkit_web_view_get_title(WEBKIT_WEB_VIEW(object))); 121} 122} 123} else if(g_strcmp0(pspec->name, "favicon") == 0) { 124// Update favicons 125if(auto image = dynamic_cast<Gtk::Image*>(page->tab_widget->get_first_child())) { 126image->set_from_icon_name("image-loading-symbolic"); 127if(auto favicon = webkit_web_view_get_favicon(WEBKIT_WEB_VIEW(object))) { 128gtk_image_set_from_paintable(image->gobj(), GDK_PAINTABLE(favicon)); 129} 130} 131} else if(g_strcmp0(pspec->name, "uri") == 0) { 132self->history_manager->log_url(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(object))); 133} 134} 135} 136} 137 138static void notify_focused_callback(GObject *object, GParamSpec *pspec, gpointer data) { 139if(!gtk_widget_get_parent(GTK_WIDGET(object))) { 140return; 141} 142auto this_ = static_cast<PantheraWww*>(data); 143auto parent = gtk_widget_get_parent(gtk_widget_get_parent(GTK_WIDGET(object))); 144if(auto page = dynamic_cast<gPanthera::ContentPage*>(Glib::wrap(parent))) { 145if(g_strcmp0(pspec->name, "uri") == 0) { 146this_->url_bar->set_text(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(object))); 147} 148} 149} 150 151static void on_back_pressed(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) { 152WebKitWebView *webview = WEBKIT_WEB_VIEW(user_data); 153webkit_web_view_go_back(webview); 154} 155 156static void on_forward_pressed(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) { 157WebKitWebView *webview = WEBKIT_WEB_VIEW(user_data); 158webkit_web_view_go_forward(webview); 159} 160 161static gboolean on_decide_policy(WebKitWebView *source, WebKitPolicyDecision *decision, WebKitPolicyDecisionType type, gpointer user_data) { 162// Middle-click opens in a new window 163if(auto self = static_cast<PantheraWww*>(user_data)) { 164if(type == WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION) { 165auto action = webkit_navigation_policy_decision_get_navigation_action(WEBKIT_NAVIGATION_POLICY_DECISION(decision)); 166if(webkit_navigation_action_get_mouse_button(action) == 2) { 167Glib::ustring url = Glib::ustring(webkit_uri_request_get_uri(webkit_navigation_action_get_request(action))); 168self->on_new_tab(nullptr, url, false); 169webkit_policy_decision_ignore(decision); 170return true; 171} 172} else if(type == WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION) { 173auto action = webkit_navigation_policy_decision_get_navigation_action(WEBKIT_NAVIGATION_POLICY_DECISION(decision)); 174self->on_new_tab(nullptr, webkit_uri_request_get_uri(webkit_navigation_action_get_request(action)), false, true); 175return true; 176} 177} 178return false; 179} 180 181std::string new_tab_page; 182 183void on_new_tab(gPanthera::ContentStack *stack, const Glib::ustring &url = "", bool focus = true, bool new_window = false) { 184if(!stack) { 185// Find the current area 186stack = content_manager->get_last_operated_page()->get_stack(); 187} 188Glib::ustring url_ = url; 189if(url.empty()) { 190url_ = new_tab_page; 191} 192 193WebKitWebView *webview = WEBKIT_WEB_VIEW(webkit_web_view_new()); 194gtk_widget_set_hexpand(GTK_WIDGET(webview), true); 195gtk_widget_set_vexpand(GTK_WIDGET(webview), true); 196auto page_content = Gtk::make_managed<Gtk::Box>(); 197gtk_box_append(page_content->gobj(), GTK_WIDGET(webview)); 198auto page_tab = new Gtk::Box(); 199page_tab->set_orientation(Gtk::Orientation::HORIZONTAL); 200auto initial_icon = Gtk::make_managed<Gtk::Image>(Gio::Icon::create("image-loading-symbolic")); 201page_tab->append(*initial_icon); 202page_tab->append(*Gtk::make_managed<Gtk::Label>("Untitled")); 203page_tab->set_spacing(4); 204auto page = Gtk::make_managed<gPanthera::ContentPage>(content_manager, stack, page_content, page_tab); 205g_signal_connect(webview, "notify", G_CALLBACK(notify_callback), this); 206g_signal_connect(webview, "decide-policy", G_CALLBACK(on_decide_policy), this); 207webkit_web_view_load_uri(webview, url_.data()); 208auto cookie_manager = webkit_network_session_get_cookie_manager(webkit_web_view_get_network_session(webview)); 209webkit_cookie_manager_set_persistent_storage(cookie_manager, cookie_file.c_str(), WEBKIT_COOKIE_PERSISTENT_STORAGE_TEXT); 210webkit_cookie_manager_set_accept_policy(cookie_manager, WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS); 211auto website_data_manager = webkit_network_session_get_website_data_manager(webkit_web_view_get_network_session(webview)); 212webkit_website_data_manager_set_favicons_enabled(website_data_manager, true); 213GtkEventController *click_controller_back = GTK_EVENT_CONTROLLER(gtk_gesture_click_new()); 214gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click_controller_back), 8); 215g_signal_connect(click_controller_back, "pressed", G_CALLBACK(on_back_pressed), webview); 216gtk_widget_add_controller(GTK_WIDGET(webview), click_controller_back); 217 218GtkEventController *click_controller_forward = GTK_EVENT_CONTROLLER(gtk_gesture_click_new()); 219gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click_controller_forward), 9); 220g_signal_connect(click_controller_forward, "pressed", G_CALLBACK(on_forward_pressed), webview); 221gtk_widget_add_controller(GTK_WIDGET(webview), click_controller_forward); 222 223stack->add_page(*page); 224if(focus) { 225stack->set_visible_child(*page); 226content_manager->set_last_operated_page(page); 227} 228if(new_window) { 229bool result = stack->signal_detach.emit(page); 230} 231} 232 233void on_startup() override { 234Gtk::Application::on_startup(); 235add_window(*window); 236window->set_default_size(600, 400); 237layout_manager = std::make_shared<gPanthera::LayoutManager>(); 238auto dock_stack_1 = Gtk::make_managed<gPanthera::DockStack>(layout_manager, "One", "one"); 239auto switcher_1 = Gtk::make_managed<gPanthera::DockStackSwitcher>(dock_stack_1, Gtk::Orientation::HORIZONTAL); 240auto dock_stack_2 = Gtk::make_managed<gPanthera::DockStack>(layout_manager, "Two", "two"); 241auto switcher_2 = Gtk::make_managed<gPanthera::DockStackSwitcher>(dock_stack_2, Gtk::Orientation::VERTICAL); 242auto pane_1_content = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0); 243auto debug_button = Gtk::make_managed<Gtk::Button>("Debug"); 244auto print_history_button = Gtk::make_managed<Gtk::Button>("Print history"); 245print_history_button->signal_clicked().connect([this]() { 246auto history = history_manager->get_history(); 247for(const auto &entry : history) { 248std::cout << "entry " << entry.id << ", url " << entry.url << ", unix time " << entry.timestamp << std::endl; 249// TODO: exclude redirecting URLs 250} 251}); 252pane_1_content->append(*debug_button); 253pane_1_content->append(*print_history_button); 254auto pane_2_content = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0); 255auto test_notebook = Gtk::make_managed<Gtk::Notebook>(); 256test_notebook->append_page(*Gtk::make_managed<Gtk::Label>("Test 1"), *Gtk::make_managed<Gtk::Label>("Test 1")); 257pane_2_content->append(*test_notebook); 258auto pane_3_content = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0); 259pane_3_content->append(*Gtk::make_managed<Gtk::Label>("Pane 3 content")); 260auto pane_1_icon = Gtk::make_managed<Gtk::Image>(); 261pane_1_icon->set_from_icon_name("go-home-symbolic"); 262auto pane_2_icon = Gtk::make_managed<Gtk::Image>(); 263pane_2_icon->set_from_icon_name("folder-symbolic"); 264auto pane_3_icon = Gtk::make_managed<Gtk::Image>(); 265pane_3_icon->set_from_icon_name("network-transmit-receive-symbolic"); 266auto pane_1 = Gtk::make_managed<gPanthera::DockablePane>(layout_manager, *pane_1_content, "pane1", "Pane 1", pane_1_icon); 267auto pane_2 = Gtk::make_managed<gPanthera::DockablePane>(layout_manager, *pane_2_content, "pane2", "Pane 2", pane_2_icon); 268auto pane_3 = Gtk::make_managed<gPanthera::DockablePane>(layout_manager, *pane_3_content, "pane3", "Pane 3", pane_3_icon); 269 270dock_stack_1->set_transition_type(Gtk::StackTransitionType::SLIDE_LEFT_RIGHT); 271dock_stack_1->set_transition_duration(125); 272dock_stack_1->set_expand(true); 273dock_stack_2->set_transition_type(Gtk::StackTransitionType::SLIDE_UP_DOWN); 274dock_stack_2->set_transition_duration(125); 275dock_stack_2->set_expand(true); 276 277auto outer_grid = Gtk::make_managed<Gtk::Grid>(); 278outer_grid->attach(*switcher_2, 0, 1, 1, 1); 279outer_grid->attach(*switcher_1, 1, 2, 1, 1); 280auto outer_paned = Gtk::make_managed<Gtk::Paned>(Gtk::Orientation::HORIZONTAL); 281outer_paned->set_start_child(*dock_stack_2); 282auto inner_paned = Gtk::make_managed<Gtk::Paned>(Gtk::Orientation::VERTICAL); 283auto content = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0); 284content_manager = std::make_shared<gPanthera::ContentManager>(); 285std::function<bool(gPanthera::ContentPage*)> detach_handler; 286detach_handler = [](gPanthera::ContentPage *widget) { 287auto new_stack = Gtk::make_managed<gPanthera::ContentStack>(widget->content_manager, widget->get_stack()->get_detach_handler()); 288auto new_switcher = Gtk::make_managed<gPanthera::ContentTabBar>(new_stack, Gtk::Orientation::HORIZONTAL, dynamic_cast<gPanthera::ContentTabBar*>(widget->get_stack()->get_parent()->get_first_child())->get_extra_child_function()); 289auto new_notebook = Gtk::make_managed<gPanthera::ContentNotebook>(new_stack, new_switcher); 290auto window = new gPanthera::ContentWindow(new_notebook); 291widget->redock(new_stack); 292window->present(); 293new_stack->signal_leave_empty.connect([window]() { 294window->close(); 295delete window; 296}); 297return true; 298}; 299 300auto return_extra_child = [this](gPanthera::ContentTabBar *switcher) { 301auto new_tab_button = Gtk::make_managed<Gtk::Button>(); 302new_tab_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("list-add-symbolic"))); 303new_tab_button->set_tooltip_text("New tab"); 304new_tab_button->signal_clicked().connect([this, switcher]() { 305on_new_tab(switcher->get_stack()); 306}); 307return new_tab_button; 308}; 309auto content_stack = Gtk::make_managed<gPanthera::ContentStack>(content_manager, detach_handler); 310auto content_stack_switcher = Gtk::make_managed<gPanthera::ContentTabBar>(content_stack, Gtk::Orientation::HORIZONTAL, return_extra_child); 311content_manager->add_stack(content_stack); 312WebKitWebView *webview = WEBKIT_WEB_VIEW(webkit_web_view_new()); // for some reason, this has to be created 313content->set_name("content_box"); 314auto content_notebook = Gtk::make_managed<gPanthera::ContentNotebook>(content_stack, content_stack_switcher, Gtk::PositionType::TOP); 315content->append(*content_notebook); 316inner_paned->set_start_child(*content); 317inner_paned->set_end_child(*dock_stack_1); 318outer_paned->set_end_child(*inner_paned); 319outer_grid->attach(*outer_paned, 1, 1, 1, 1); 320 321// Get search engines 322std::ifstream search_engines_file_in("search_engines.json"); 323if(search_engines_file_in) { 324std::string search_engines_json((std::istreambuf_iterator<char>(search_engines_file_in)), std::istreambuf_iterator<char>()); 325search_engines_file_in.close(); 326set_search_engines_from_json(search_engines_json); 327} else { 328search_engines_file_in.close(); 329auto empty_json = nlohmann::json::array(); 330std::ofstream search_engines_file_out("search_engines.json"); 331search_engines_file_out << empty_json.dump(4); 332} 333 334// Create the toolbar 335auto main_toolbar = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 8); 336// URL bar 337url_bar = Gtk::make_managed<Gtk::Entry>(); 338url_bar->set_placeholder_text("Enter URL"); 339url_bar->set_hexpand(true); 340auto load_url_callback = [this]() { 341auto page = content_manager->get_last_operated_page(); 342bool has_protocol = url_bar->get_text().find("://") != std::string::npos; 343if(!has_protocol) { 344url_bar->set_text("http://" + url_bar->get_text()); 345} 346if(page) { 347if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) { 348webkit_web_view_load_uri(webview, url_bar->get_text().c_str()); 349} 350} 351}; 352// Go 353auto go_button = Gtk::make_managed<Gtk::Button>("Go"); 354go_button->signal_clicked().connect(load_url_callback); 355url_bar->signal_activate().connect(load_url_callback); 356content_manager->signal_page_operated.connect([this](gPanthera::ContentPage *page) { 357if(!page->get_child()) { 358return; 359} 360if(!page->get_child()->get_first_child()) { 361return; 362} 363url_bar->set_text(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj()))); 364guint url_update_handler = g_signal_connect(page->get_child()->get_first_child()->gobj(), "notify", G_CALLBACK(notify_focused_callback), this); 365std::shared_ptr<sigc::connection> control_signal_handler = std::make_shared<sigc::connection>(); 366*control_signal_handler = page->signal_control_status_changed.connect([this, page, control_signal_handler, url_update_handler](bool controlled) { 367if(!controlled) { 368control_signal_handler->disconnect(); 369if(page->get_child() && page->get_child()->get_first_child() && WEBKIT_IS_WEB_VIEW(page->get_child()->get_first_child()->gobj())) { 370g_signal_handler_disconnect(page->get_child()->get_first_child()->gobj(), url_update_handler); 371} 372} 373}); 374}); 375history_manager = std::make_shared<HistoryManager>("history.db"); 376// Back, forward, reload 377auto back_button = Gtk::make_managed<Gtk::Button>(); 378back_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-previous-symbolic"))); 379back_button->set_tooltip_text("Back"); 380auto forward_button = Gtk::make_managed<Gtk::Button>(); 381forward_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-next-symbolic"))); 382forward_button->set_tooltip_text("Forward"); 383auto reload_button = Gtk::make_managed<Gtk::Button>(); 384reload_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("view-refresh-symbolic"))); 385reload_button->set_tooltip_text("Reload"); 386back_button->signal_clicked().connect([this]() { 387auto page = content_manager->get_last_operated_page(); 388if(page) { 389if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) { 390webkit_web_view_go_back(webview); 391} 392} 393}); 394forward_button->signal_clicked().connect([this]() { 395auto page = content_manager->get_last_operated_page(); 396if(page) { 397if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) { 398webkit_web_view_go_forward(webview); 399} 400} 401}); 402reload_button->signal_clicked().connect([this]() { 403auto page = content_manager->get_last_operated_page(); 404if(page) { 405if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) { 406webkit_web_view_reload(webview); 407} 408} 409}); 410// Search bar 411// TODO: provide history, provide a menu button with the other engines 412auto search_bar = Gtk::make_managed<Gtk::Entry>(); 413search_bar->set_placeholder_text("Search"); 414search_bar->set_hexpand(true); 415if(search_engines.empty()) { 416search_bar->set_sensitive(false); 417search_bar->set_placeholder_text("No search"); 418} 419auto search_callback = [this, search_bar, content_stack]() { 420// Create a new tab with the search results 421if(content_manager->get_last_operated_page()) { 422on_new_tab(nullptr); 423} else { 424on_new_tab(content_stack); 425} 426auto page = content_manager->get_last_operated_page(); 427if(page) { 428if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) { 429auto search_url = default_search_engine->get_search_url(search_bar->get_text()); 430webkit_web_view_load_uri(webview, search_url.c_str()); 431} 432} 433}; 434if(default_search_engine) { 435search_bar->signal_activate().connect(search_callback); 436} 437// Assemble the toolbar 438main_toolbar->append(*back_button); 439main_toolbar->append(*forward_button); 440main_toolbar->append(*reload_button); 441main_toolbar->append(*url_bar); 442main_toolbar->append(*go_button); 443main_toolbar->append(*search_bar); 444if(default_search_engine) { 445auto search_button = Gtk::make_managed<Gtk::Button>(default_search_engine->name); 446search_button->signal_clicked().connect(search_callback); 447main_toolbar->append(*search_button); 448} 449outer_grid->attach(*main_toolbar, 0, 0, 2, 1); 450window->set_child(*outer_grid); 451debug_button->signal_clicked().connect([this]() { 452if(content_manager->get_last_operated_page()) { 453std::cout << "Last operated page: " << content_manager->get_last_operated_page()->get_name() << std::endl; 454} else { 455std::cout << "No page operated!" << std::endl; 456} 457}); 458// TODO: Use the last operated page and allow opening tabs next to the last operated page using certain panes 459// Load the existing layout, if it exists 460std::ifstream layout_file_in("layout.json"); 461if(layout_file_in) { 462std::string layout_json((std::istreambuf_iterator<char>(layout_file_in)), std::istreambuf_iterator<char>()); 463layout_file_in.close(); 464layout_manager->restore_json_layout(layout_json); 465} else { 466// Create a new layout if the file doesn't exist 467layout_file_in.close(); 468 469dock_stack_1->add_pane(*pane_1); 470dock_stack_1->add_pane(*pane_3); 471dock_stack_2->add_pane(*pane_2); 472 473std::ofstream layout_file_out("layout.json"); 474layout_file_out << layout_manager->get_layout_as_json(); 475layout_file_out.close(); 476} 477// Save the layout when changed 478layout_manager->signal_pane_moved.connect([this](gPanthera::DockablePane *pane) { 479std::ofstream layout_file_out("layout.json"); 480layout_file_out << layout_manager->get_layout_as_json(); 481layout_file_out.close(); 482std::cout << "Layout changed: " << layout_manager->get_layout_as_json() << std::endl; 483}); 484 485auto new_tab_action = Gio::SimpleAction::create("new_tab"); 486new_tab_action->signal_activate().connect([this](const Glib::VariantBase&) { 487on_new_tab(nullptr); 488}); 489add_action(new_tab_action); 490set_accels_for_action("app.new_tab", {"<Primary>T"}); 491auto close_tab_action = Gio::SimpleAction::create("close_tab"); 492close_tab_action->signal_activate().connect([this](const Glib::VariantBase&) { 493auto page = content_manager->get_last_operated_page(); 494if(page) { 495page->close(); 496} 497}); 498add_action(close_tab_action); 499set_accels_for_action("app.close_tab", {"<Primary>W"}); 500 501// Load settings 502new_tab_page = "about:blank"; 503std::ifstream settings_file_in("settings.json"); 504if(settings_file_in) { 505std::string settings_json((std::istreambuf_iterator<char>(settings_file_in)), std::istreambuf_iterator<char>()); 506settings_file_in.close(); 507auto json = nlohmann::json::parse(settings_json); 508if(json.contains("ntp")) { 509new_tab_page = json["ntp"].get<std::string>(); 510} 511} else { 512settings_file_in.close(); 513auto empty_json = nlohmann::json::object(); 514empty_json["ntp"] = "about:blank"; 515std::ofstream settings_file_out("settings.json"); 516settings_file_out << empty_json.dump(4); 517settings_file_out.close(); 518} 519 520// Load plugins 521std::ifstream plugin_file_in("plugins.json"); 522if(plugin_file_in) { 523std::string plugin_json((std::istreambuf_iterator<char>(plugin_file_in)), std::istreambuf_iterator<char>()); 524plugin_file_in.close(); 525auto json = nlohmann::json::parse(plugin_json); 526for(auto &plugin : json) { 527auto plugin_path = plugin["path"].get<std::string>(); 528load_plugin(plugin_path); 529} 530} else { 531plugin_file_in.close(); 532auto empty_json = nlohmann::json::array(); 533std::ofstream plugin_file_out("plugins.json"); 534plugin_file_out << empty_json.dump(4); 535plugin_file_out.close(); 536} 537} 538 539void on_activate() override { 540window->present(); 541} 542 543void set_search_engines_from_json(const std::string &json_string) { 544auto json = nlohmann::json::parse(json_string); 545Glib::ustring default_search_engine_name; 546for(auto &engine : json) { 547SearchEngine search_engine; 548search_engine.name = engine["name"].get<std::string>(); 549search_engine.url = engine["url"].get<std::string>(); 550search_engines.push_back(search_engine); 551if(engine.contains("default") && engine["default"].get<bool>()) { 552default_search_engine_name = engine["name"].get<std::string>(); 553} 554} 555for(auto &search_engine : search_engines) { 556if(search_engine.name == default_search_engine_name) { 557default_search_engine = &search_engine; 558break; 559} 560} 561} 562public: 563static Glib::RefPtr<PantheraWww> create() { 564return Glib::make_refptr_for_instance<PantheraWww>(new PantheraWww()); 565} 566 567std::shared_ptr<gPanthera::LayoutManager> get_layout_manager() { 568return layout_manager; 569} 570 571std::shared_ptr<gPanthera::ContentManager> get_content_manager() { 572return content_manager; 573} 574 575void load_plugin(const std::string &plugin_path) { 576void *handle = dlopen(plugin_path.c_str(), RTLD_LAZY | RTLD_GLOBAL); 577if(!handle) { 578std::cerr << "Failed to load plugin: " << dlerror() << std::endl; 579return; 580} 581auto entrypoint = (plugin_entrypoint_type)dlsym(handle, "plugin_init"); 582if(!entrypoint) { 583std::cerr << "Failed to find entrypoint in plugin: " << dlerror() << std::endl; 584dlclose(handle); 585return; 586} 587entrypoint(); 588} 589}; 590 591Glib::RefPtr<PantheraWww> app = nullptr; 592 593extern "C" { 594void panthera_log(const char *message) { 595std::cerr << message << std::endl; 596} 597 598void panthera_add_pane(const char *id, const char *label, GtkImage *icon, GtkWidget *pane) { 599auto pane_child = Glib::wrap(pane); 600auto icon_cc = Glib::wrap(icon); 601auto label_cc = Glib::ustring(label); 602auto id_cc = Glib::ustring(id); 603auto new_pane = Gtk::make_managed<gPanthera::DockablePane>(app->get_layout_manager(), *pane_child, id_cc, label_cc, icon_cc); 604app->get_layout_manager()->add_pane(new_pane); 605} 606} 607 608int main(int argc, char *argv[]) { 609gPanthera::init(); 610app = PantheraWww::create(); 611return app->run(argc, argv); 612} 613