panthera-www.cc
C++ source, ASCII text, with very long lines (379)
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, title; 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("title", &HistoryEntry::title), 69sqlite_orm::make_column("timestamp", &HistoryEntry::timestamp) 70) 71)); 72 73class HistoryManager { 74public: 75static auto make_storage(const std::string& path) { 76return sqlite_orm::make_storage(path, 77sqlite_orm::make_table("history", 78sqlite_orm::make_column("id", &HistoryEntry::id, sqlite_orm::primary_key()), 79sqlite_orm::make_column("url", &HistoryEntry::url), 80sqlite_orm::make_column("title", &HistoryEntry::title), 81sqlite_orm::make_column("timestamp", &HistoryEntry::timestamp) 82) 83); 84} 85StorageType storage; 86explicit HistoryManager(const std::string &db_path) : storage(make_storage(db_path)) { 87storage.sync_schema(); 88} 89 90void log_url(const std::string &url, const std::string title, const int64_t timestamp = Glib::DateTime::create_now_local().to_unix()) { 91storage.insert(HistoryEntry{-1, url, title, timestamp}); 92} 93 94std::vector<HistoryEntry> get_history() { 95return storage.get_all<HistoryEntry>(); 96} 97 98std::vector<HistoryEntry> get_history_between(int64_t start_time, int64_t end_time) { 99using namespace sqlite_orm; 100return storage.get_all<HistoryEntry>( 101where(c(&HistoryEntry::timestamp) >= start_time and c(&HistoryEntry::timestamp) < end_time), 102order_by(&HistoryEntry::timestamp).desc() 103); 104} 105}; 106 107class HistoryViewer : public gPanthera::DockablePane { 108protected: 109std::shared_ptr<HistoryManager> history_manager; 110Gtk::Calendar *calendar; 111Gtk::Label *date_label; 112Gtk::Box *box_place; 113public: 114sigc::signal<void(const std::string &url)> signal_open_url; 115HistoryViewer(std::shared_ptr<gPanthera::LayoutManager> layout_manager, std::shared_ptr<HistoryManager> history_manager) : gPanthera::DockablePane(layout_manager, *Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0), "history", "History", Gtk::make_managed<Gtk::Image>(Gio::Icon::create("document-open-recent-symbolic"))), history_manager(std::move(history_manager)) { 116auto history_toolbar = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 0); 117auto history_button_previous = Gtk::make_managed<Gtk::Button>(); 118history_button_previous->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-previous-symbolic"))); 119history_button_previous->set_tooltip_text("Previous day"); 120auto history_button_next = Gtk::make_managed<Gtk::Button>(); 121history_button_next->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-next-symbolic"))); 122history_button_next->set_tooltip_text("Next day"); 123 124date_label = Gtk::make_managed<Gtk::Label>(""); 125auto date_button = Gtk::make_managed<Gtk::MenuButton>(); 126date_button->set_child(*date_label); 127date_button->set_tooltip_text("Select date"); 128date_button->set_direction(Gtk::ArrowType::NONE); 129auto date_popover = Gtk::make_managed<Gtk::Popover>(); 130calendar = Gtk::make_managed<Gtk::Calendar>(); 131date_popover->set_child(*calendar); 132date_button->set_popover(*date_popover); 133date_button->set_has_frame(false); 134date_button->set_hexpand(true); 135 136history_toolbar->append(*history_button_previous); 137history_toolbar->append(*date_button); 138history_toolbar->append(*history_button_next); 139auto box = dynamic_cast<Gtk::Box*>(this->child); 140box->append(*history_toolbar); 141 142calendar->signal_day_selected().connect(sigc::mem_fun(*this, &HistoryViewer::reload_history)); 143Glib::DateTime datetime = Glib::DateTime::create_now_local(); 144calendar->select_day(datetime); 145 146this->signal_pane_shown.connect([this]() { 147// Load the current date 148reload_history(); 149}); 150 151box_place = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0); 152box->append(*box_place); 153 154// TODO: make the arrows work, they should move to the previous/next day with any entries 155// TODO: allow deleting entries 156} 157 158void reload_history() { 159Glib::DateTime calendar_day = calendar->get_date(); 160Glib::DateTime datetime = Glib::DateTime::create_local( 161calendar_day.get_year(), 162calendar_day.get_month(), 163calendar_day.get_day_of_month(), 1640, 0, 0 165); 166date_label->set_text(datetime.format("%x")); 167auto box = Gtk::make_managed<Gtk::ListBox>(); 168auto history_entries = history_manager->get_history_between(datetime.to_unix(), datetime.to_unix() + 86400); 169for(const auto &entry : history_entries) { 170auto row = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 4); 171auto title = Gtk::make_managed<Gtk::Label>(entry.title.empty() ? "Untitled" : entry.title); 172title->set_halign(Gtk::Align::START); 173title->set_ellipsize(Pango::EllipsizeMode::MIDDLE); 174auto label = Gtk::make_managed<Gtk::Label>(entry.url); 175label->set_halign(Gtk::Align::START); 176label->set_ellipsize(Pango::EllipsizeMode::MIDDLE); 177row->append(*title); 178row->append(*label); 179if(auto time = Glib::DateTime::create_now_local(entry.timestamp)) { 180label->set_tooltip_text(time.format("%c")); 181box->append(*row); 182} 183} 184if(box_place->get_first_child()) { 185box_place->remove(*box_place->get_first_child()); 186} 187box->signal_row_activated().connect([this](Gtk::ListBoxRow *row) { 188if(auto box_row = dynamic_cast<Gtk::Box*>(row->get_child())) { 189if(auto label = dynamic_cast<Gtk::Label*>(box_row->get_last_child())) { 190auto url = label->get_text(); 191if(!url.empty()) { 192signal_open_url.emit(url); 193} 194} 195} 196}); 197box_place->append(*box); 198} 199}; 200 201class PantheraWww : public Gtk::Application { 202Gtk::Window *window = Gtk::make_managed<Gtk::Window>(); 203protected: 204std::shared_ptr<gPanthera::LayoutManager> layout_manager; 205std::shared_ptr<gPanthera::ContentManager> content_manager; 206Gtk::Entry *url_bar = nullptr; 207std::string cookie_file = "cookies.txt"; 208std::vector<SearchEngine> search_engines; 209SearchEngine *default_search_engine = nullptr; 210std::shared_ptr<HistoryManager> history_manager; 211HistoryViewer *history_viewer = nullptr; 212 213static void notify_callback(GObject *object, GParamSpec *pspec, gpointer data) { 214if(!gtk_widget_get_parent(GTK_WIDGET(object))) { 215return; 216} 217if(auto self = static_cast<PantheraWww*>(data)) { 218auto parent = gtk_widget_get_parent(gtk_widget_get_parent(GTK_WIDGET(object))); 219if(auto page = dynamic_cast<gPanthera::ContentPage*>(Glib::wrap(parent))) { 220if(g_strcmp0(pspec->name, "title") == 0) { 221if(auto label = dynamic_cast<Gtk::Label*>(page->tab_widget->get_last_child())) { 222if(strlen(webkit_web_view_get_title(WEBKIT_WEB_VIEW(object))) == 0) { 223label->set_label("Untitled"); 224} else { 225label->set_label(webkit_web_view_get_title(WEBKIT_WEB_VIEW(object))); 226} 227} 228} else if(g_strcmp0(pspec->name, "favicon") == 0) { 229// Update favicons 230if(auto image = dynamic_cast<Gtk::Image*>(page->tab_widget->get_first_child())) { 231image->set_from_icon_name("image-loading-symbolic"); 232if(auto favicon = webkit_web_view_get_favicon(WEBKIT_WEB_VIEW(object))) { 233gtk_image_set_from_paintable(image->gobj(), GDK_PAINTABLE(favicon)); 234} 235} 236} else if(g_strcmp0(pspec->name, "uri") == 0) { 237if(webkit_web_view_get_title(WEBKIT_WEB_VIEW(object)) == nullptr) { 238self->history_manager->log_url(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(object)), ""); 239} else { 240self->history_manager->log_url(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(object)), 241webkit_web_view_get_title(WEBKIT_WEB_VIEW(object))); 242} 243if(self->history_viewer->get_visible()) { 244self->history_viewer->reload_history(); 245} 246} 247} 248} 249} 250 251static void notify_focused_callback(GObject *object, GParamSpec *pspec, gpointer data) { 252if(!gtk_widget_get_parent(GTK_WIDGET(object))) { 253return; 254} 255auto this_ = static_cast<PantheraWww*>(data); 256auto parent = gtk_widget_get_parent(gtk_widget_get_parent(GTK_WIDGET(object))); 257if(auto page = dynamic_cast<gPanthera::ContentPage*>(Glib::wrap(parent))) { 258if(g_strcmp0(pspec->name, "uri") == 0) { 259this_->url_bar->set_text(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(object))); 260} 261} 262} 263 264static void on_back_pressed(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) { 265WebKitWebView *webview = WEBKIT_WEB_VIEW(user_data); 266webkit_web_view_go_back(webview); 267} 268 269static void on_forward_pressed(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) { 270WebKitWebView *webview = WEBKIT_WEB_VIEW(user_data); 271webkit_web_view_go_forward(webview); 272} 273 274static gboolean on_decide_policy(WebKitWebView *source, WebKitPolicyDecision *decision, WebKitPolicyDecisionType type, gpointer user_data) { 275// Middle-click opens in a new window 276if(auto self = static_cast<PantheraWww*>(user_data)) { 277if(type == WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION) { 278auto action = webkit_navigation_policy_decision_get_navigation_action(WEBKIT_NAVIGATION_POLICY_DECISION(decision)); 279if(webkit_navigation_action_get_mouse_button(action) == 2) { 280Glib::ustring url = Glib::ustring(webkit_uri_request_get_uri(webkit_navigation_action_get_request(action))); 281self->on_new_tab(nullptr, url, false); 282webkit_policy_decision_ignore(decision); 283return true; 284} 285} else if(type == WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION) { 286auto action = webkit_navigation_policy_decision_get_navigation_action(WEBKIT_NAVIGATION_POLICY_DECISION(decision)); 287self->on_new_tab(nullptr, webkit_uri_request_get_uri(webkit_navigation_action_get_request(action)), false, true); 288return true; 289} 290} 291return false; 292} 293 294std::string new_tab_page; 295 296void on_new_tab(gPanthera::ContentStack *stack, const Glib::ustring &url = "", bool focus = true, bool new_window = false) { 297if(!stack) { 298// Find the current area 299stack = content_manager->get_last_operated_page()->get_stack(); 300} 301Glib::ustring url_ = url; 302if(url.empty()) { 303url_ = new_tab_page; 304} 305 306WebKitWebView *webview = WEBKIT_WEB_VIEW(webkit_web_view_new()); 307gtk_widget_set_hexpand(GTK_WIDGET(webview), true); 308gtk_widget_set_vexpand(GTK_WIDGET(webview), true); 309auto page_content = Gtk::make_managed<Gtk::Box>(); 310gtk_box_append(page_content->gobj(), GTK_WIDGET(webview)); 311auto page_tab = new Gtk::Box(); 312page_tab->set_orientation(Gtk::Orientation::HORIZONTAL); 313auto initial_icon = Gtk::make_managed<Gtk::Image>(Gio::Icon::create("image-loading-symbolic")); 314page_tab->append(*initial_icon); 315page_tab->append(*Gtk::make_managed<Gtk::Label>("Untitled")); 316page_tab->set_spacing(4); 317auto page = Gtk::make_managed<gPanthera::ContentPage>(content_manager, stack, page_content, page_tab); 318g_signal_connect(webview, "notify", G_CALLBACK(notify_callback), this); 319g_signal_connect(webview, "decide-policy", G_CALLBACK(on_decide_policy), this); 320webkit_web_view_load_uri(webview, url_.data()); 321auto cookie_manager = webkit_network_session_get_cookie_manager(webkit_web_view_get_network_session(webview)); 322webkit_cookie_manager_set_persistent_storage(cookie_manager, cookie_file.c_str(), WEBKIT_COOKIE_PERSISTENT_STORAGE_TEXT); 323webkit_cookie_manager_set_accept_policy(cookie_manager, WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS); 324auto website_data_manager = webkit_network_session_get_website_data_manager(webkit_web_view_get_network_session(webview)); 325webkit_website_data_manager_set_favicons_enabled(website_data_manager, true); 326GtkEventController *click_controller_back = GTK_EVENT_CONTROLLER(gtk_gesture_click_new()); 327gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click_controller_back), 8); 328g_signal_connect(click_controller_back, "pressed", G_CALLBACK(on_back_pressed), webview); 329gtk_widget_add_controller(GTK_WIDGET(webview), click_controller_back); 330 331GtkEventController *click_controller_forward = GTK_EVENT_CONTROLLER(gtk_gesture_click_new()); 332gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click_controller_forward), 9); 333g_signal_connect(click_controller_forward, "pressed", G_CALLBACK(on_forward_pressed), webview); 334gtk_widget_add_controller(GTK_WIDGET(webview), click_controller_forward); 335 336stack->add_page(*page); 337if(focus) { 338stack->set_visible_child(*page); 339content_manager->set_last_operated_page(page); 340} 341if(new_window) { 342bool result = stack->signal_detach.emit(page); 343} 344} 345 346void on_startup() override { 347Gtk::Application::on_startup(); 348add_window(*window); 349window->set_default_size(600, 400); 350layout_manager = std::make_shared<gPanthera::LayoutManager>(); 351auto dock_stack_1 = Gtk::make_managed<gPanthera::DockStack>(layout_manager, "One", "one"); 352auto switcher_1 = Gtk::make_managed<gPanthera::DockStackSwitcher>(dock_stack_1, Gtk::Orientation::HORIZONTAL); 353auto dock_stack_2 = Gtk::make_managed<gPanthera::DockStack>(layout_manager, "Two", "two"); 354auto switcher_2 = Gtk::make_managed<gPanthera::DockStackSwitcher>(dock_stack_2, Gtk::Orientation::VERTICAL); 355auto pane_1_content = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0); 356auto debug_button = Gtk::make_managed<Gtk::Button>("Debug"); 357auto print_history_button = Gtk::make_managed<Gtk::Button>("Print history"); 358print_history_button->signal_clicked().connect([this]() { 359auto history = history_manager->get_history(); 360for(const auto &entry : history) { 361std::cout << "entry " << entry.id << ", url " << entry.url << ", unix time " << entry.timestamp << std::endl; 362// TODO: exclude redirecting URLs 363} 364}); 365pane_1_content->append(*debug_button); 366pane_1_content->append(*print_history_button); 367auto pane_1_icon = Gtk::make_managed<Gtk::Image>(Gio::Icon::create("help-about-symbolic")); 368auto pane_1 = Gtk::make_managed<gPanthera::DockablePane>(layout_manager, *pane_1_content, "debug", "Debugging options", pane_1_icon); 369 370dock_stack_1->set_transition_type(Gtk::StackTransitionType::SLIDE_LEFT_RIGHT); 371dock_stack_1->set_transition_duration(125); 372dock_stack_1->set_expand(true); 373dock_stack_2->set_transition_type(Gtk::StackTransitionType::SLIDE_UP_DOWN); 374dock_stack_2->set_transition_duration(125); 375dock_stack_2->set_expand(true); 376 377auto outer_grid = Gtk::make_managed<Gtk::Grid>(); 378outer_grid->attach(*switcher_2, 0, 1, 1, 1); 379outer_grid->attach(*switcher_1, 1, 2, 1, 1); 380auto outer_paned = Gtk::make_managed<Gtk::Paned>(Gtk::Orientation::HORIZONTAL); 381outer_paned->set_start_child(*dock_stack_2); 382auto inner_paned = Gtk::make_managed<Gtk::Paned>(Gtk::Orientation::VERTICAL); 383auto content = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0); 384content_manager = std::make_shared<gPanthera::ContentManager>(); 385std::function<bool(gPanthera::ContentPage*)> detach_handler; 386detach_handler = [](gPanthera::ContentPage *widget) { 387auto new_stack = Gtk::make_managed<gPanthera::ContentStack>(widget->content_manager, widget->get_stack()->get_detach_handler()); 388auto 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()); 389auto new_notebook = Gtk::make_managed<gPanthera::ContentNotebook>(new_stack, new_switcher); 390auto window = new gPanthera::ContentWindow(new_notebook); 391widget->redock(new_stack); 392window->present(); 393new_stack->signal_leave_empty.connect([window]() { 394window->close(); 395delete window; 396}); 397return true; 398}; 399 400auto return_extra_child = [this](gPanthera::ContentTabBar *switcher) { 401auto new_tab_button = Gtk::make_managed<Gtk::Button>(); 402new_tab_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("list-add-symbolic"))); 403new_tab_button->set_tooltip_text("New tab"); 404new_tab_button->signal_clicked().connect([this, switcher]() { 405on_new_tab(switcher->get_stack()); 406}); 407return new_tab_button; 408}; 409auto content_stack = Gtk::make_managed<gPanthera::ContentStack>(content_manager, detach_handler); 410auto content_stack_switcher = Gtk::make_managed<gPanthera::ContentTabBar>(content_stack, Gtk::Orientation::HORIZONTAL, return_extra_child); 411content_manager->add_stack(content_stack); 412WebKitWebView *webview = WEBKIT_WEB_VIEW(webkit_web_view_new()); // for some reason, this has to be created 413content->set_name("content_box"); 414auto content_notebook = Gtk::make_managed<gPanthera::ContentNotebook>(content_stack, content_stack_switcher, Gtk::PositionType::TOP); 415content->append(*content_notebook); 416inner_paned->set_start_child(*content); 417inner_paned->set_end_child(*dock_stack_1); 418outer_paned->set_end_child(*inner_paned); 419outer_grid->attach(*outer_paned, 1, 1, 1, 1); 420 421// Get search engines 422std::ifstream search_engines_file_in("search_engines.json"); 423if(search_engines_file_in) { 424std::string search_engines_json((std::istreambuf_iterator<char>(search_engines_file_in)), std::istreambuf_iterator<char>()); 425search_engines_file_in.close(); 426set_search_engines_from_json(search_engines_json); 427} else { 428search_engines_file_in.close(); 429auto empty_json = nlohmann::json::array(); 430std::ofstream search_engines_file_out("search_engines.json"); 431search_engines_file_out << empty_json.dump(4); 432} 433 434// Create the toolbar 435auto main_toolbar = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 8); 436// URL bar 437url_bar = Gtk::make_managed<Gtk::Entry>(); 438url_bar->set_placeholder_text("Enter URL"); 439url_bar->set_hexpand(true); 440auto load_url_callback = [this]() { 441auto page = content_manager->get_last_operated_page(); 442bool has_protocol = url_bar->get_text().find("://") != std::string::npos; 443if(!has_protocol) { 444url_bar->set_text("http://" + url_bar->get_text()); 445} 446if(page) { 447if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) { 448webkit_web_view_load_uri(webview, url_bar->get_text().c_str()); 449} 450} 451}; 452// Go 453auto go_button = Gtk::make_managed<Gtk::Button>("Go"); 454go_button->signal_clicked().connect(load_url_callback); 455url_bar->signal_activate().connect(load_url_callback); 456content_manager->signal_page_operated.connect([this](gPanthera::ContentPage *page) { 457if(!page->get_child()) { 458return; 459} 460if(!page->get_child()->get_first_child()) { 461return; 462} 463url_bar->set_text(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj()))); 464guint url_update_handler = g_signal_connect(page->get_child()->get_first_child()->gobj(), "notify", G_CALLBACK(notify_focused_callback), this); 465std::shared_ptr<sigc::connection> control_signal_handler = std::make_shared<sigc::connection>(); 466*control_signal_handler = page->signal_control_status_changed.connect([this, page, control_signal_handler, url_update_handler](bool controlled) { 467if(!controlled) { 468control_signal_handler->disconnect(); 469if(page->get_child() && page->get_child()->get_first_child() && WEBKIT_IS_WEB_VIEW(page->get_child()->get_first_child()->gobj())) { 470g_signal_handler_disconnect(page->get_child()->get_first_child()->gobj(), url_update_handler); 471} 472} 473}); 474}); 475history_manager = std::make_shared<HistoryManager>("history.db"); 476history_viewer = Gtk::make_managed<HistoryViewer>(layout_manager, history_manager); 477history_viewer->signal_open_url.connect([this](const std::string &url) { 478on_new_tab(nullptr, url, true); 479}); 480// Back, forward, reload 481auto back_button = Gtk::make_managed<Gtk::Button>(); 482back_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-previous-symbolic"))); 483back_button->set_tooltip_text("Back"); 484auto forward_button = Gtk::make_managed<Gtk::Button>(); 485forward_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-next-symbolic"))); 486forward_button->set_tooltip_text("Forward"); 487auto reload_button = Gtk::make_managed<Gtk::Button>(); 488reload_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("view-refresh-symbolic"))); 489reload_button->set_tooltip_text("Reload"); 490back_button->signal_clicked().connect([this]() { 491auto page = content_manager->get_last_operated_page(); 492if(page) { 493if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) { 494webkit_web_view_go_back(webview); 495} 496} 497}); 498forward_button->signal_clicked().connect([this]() { 499auto page = content_manager->get_last_operated_page(); 500if(page) { 501if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) { 502webkit_web_view_go_forward(webview); 503} 504} 505}); 506reload_button->signal_clicked().connect([this]() { 507auto page = content_manager->get_last_operated_page(); 508if(page) { 509if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) { 510webkit_web_view_reload(webview); 511} 512} 513}); 514// Search bar 515// TODO: provide history, provide a menu button with the other engines 516auto search_bar = Gtk::make_managed<Gtk::Entry>(); 517search_bar->set_placeholder_text("Search"); 518search_bar->set_hexpand(true); 519if(search_engines.empty()) { 520search_bar->set_sensitive(false); 521search_bar->set_placeholder_text("No search"); 522} 523auto search_callback = [this, search_bar, content_stack]() { 524// Create a new tab with the search results 525if(content_manager->get_last_operated_page()) { 526on_new_tab(nullptr); 527} else { 528on_new_tab(content_stack); 529} 530auto page = content_manager->get_last_operated_page(); 531if(page) { 532if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) { 533auto search_url = default_search_engine->get_search_url(search_bar->get_text()); 534webkit_web_view_load_uri(webview, search_url.c_str()); 535} 536} 537}; 538if(default_search_engine) { 539search_bar->signal_activate().connect(search_callback); 540} 541// Assemble the toolbar 542main_toolbar->append(*back_button); 543main_toolbar->append(*forward_button); 544main_toolbar->append(*reload_button); 545main_toolbar->append(*url_bar); 546main_toolbar->append(*go_button); 547main_toolbar->append(*search_bar); 548if(default_search_engine) { 549auto search_button = Gtk::make_managed<Gtk::Button>(default_search_engine->name); 550search_button->signal_clicked().connect(search_callback); 551main_toolbar->append(*search_button); 552} 553outer_grid->attach(*main_toolbar, 0, 0, 2, 1); 554window->set_child(*outer_grid); 555debug_button->signal_clicked().connect([this]() { 556if(content_manager->get_last_operated_page()) { 557std::cout << "Last operated page: " << content_manager->get_last_operated_page()->get_name() << std::endl; 558} else { 559std::cout << "No page operated!" << std::endl; 560} 561}); 562// TODO: Use the last operated page and allow opening tabs next to the last operated page using certain panes 563// Load the existing layout, if it exists 564std::ifstream layout_file_in("layout.json"); 565if(layout_file_in) { 566std::string layout_json((std::istreambuf_iterator<char>(layout_file_in)), std::istreambuf_iterator<char>()); 567layout_file_in.close(); 568layout_manager->restore_json_layout(layout_json); 569} else { 570// Create a new layout if the file doesn't exist 571layout_file_in.close(); 572 573dock_stack_1->add_pane(*pane_1); 574dock_stack_1->add_pane(*history_viewer); 575 576std::ofstream layout_file_out("layout.json"); 577layout_file_out << layout_manager->get_layout_as_json(); 578layout_file_out.close(); 579} 580// Save the layout when changed 581layout_manager->signal_pane_moved.connect([this](gPanthera::DockablePane *pane) { 582std::ofstream layout_file_out("layout.json"); 583layout_file_out << layout_manager->get_layout_as_json(); 584layout_file_out.close(); 585std::cout << "Layout changed: " << layout_manager->get_layout_as_json() << std::endl; 586}); 587 588auto new_tab_action = Gio::SimpleAction::create("new_tab"); 589new_tab_action->signal_activate().connect([this](const Glib::VariantBase&) { 590on_new_tab(nullptr); 591}); 592add_action(new_tab_action); 593set_accels_for_action("app.new_tab", {"<Primary>T"}); 594auto close_tab_action = Gio::SimpleAction::create("close_tab"); 595close_tab_action->signal_activate().connect([this](const Glib::VariantBase&) { 596auto page = content_manager->get_last_operated_page(); 597if(page) { 598page->close(); 599} 600}); 601add_action(close_tab_action); 602set_accels_for_action("app.close_tab", {"<Primary>W"}); 603 604// Load settings 605new_tab_page = "about:blank"; 606std::ifstream settings_file_in("settings.json"); 607if(settings_file_in) { 608std::string settings_json((std::istreambuf_iterator<char>(settings_file_in)), std::istreambuf_iterator<char>()); 609settings_file_in.close(); 610auto json = nlohmann::json::parse(settings_json); 611if(json.contains("ntp")) { 612new_tab_page = json["ntp"].get<std::string>(); 613} 614} else { 615settings_file_in.close(); 616auto empty_json = nlohmann::json::object(); 617empty_json["ntp"] = "about:blank"; 618std::ofstream settings_file_out("settings.json"); 619settings_file_out << empty_json.dump(4); 620settings_file_out.close(); 621} 622 623// Load plugins 624std::ifstream plugin_file_in("plugins.json"); 625if(plugin_file_in) { 626std::string plugin_json((std::istreambuf_iterator<char>(plugin_file_in)), std::istreambuf_iterator<char>()); 627plugin_file_in.close(); 628auto json = nlohmann::json::parse(plugin_json); 629for(auto &plugin : json) { 630auto plugin_path = plugin["path"].get<std::string>(); 631load_plugin(plugin_path); 632} 633} else { 634plugin_file_in.close(); 635auto empty_json = nlohmann::json::array(); 636std::ofstream plugin_file_out("plugins.json"); 637plugin_file_out << empty_json.dump(4); 638plugin_file_out.close(); 639} 640} 641 642void on_activate() override { 643window->present(); 644} 645 646void set_search_engines_from_json(const std::string &json_string) { 647auto json = nlohmann::json::parse(json_string); 648Glib::ustring default_search_engine_name; 649for(auto &engine : json) { 650SearchEngine search_engine; 651search_engine.name = engine["name"].get<std::string>(); 652search_engine.url = engine["url"].get<std::string>(); 653search_engines.push_back(search_engine); 654if(engine.contains("default") && engine["default"].get<bool>()) { 655default_search_engine_name = engine["name"].get<std::string>(); 656} 657} 658for(auto &search_engine : search_engines) { 659if(search_engine.name == default_search_engine_name) { 660default_search_engine = &search_engine; 661break; 662} 663} 664} 665public: 666static Glib::RefPtr<PantheraWww> create() { 667return Glib::make_refptr_for_instance<PantheraWww>(new PantheraWww()); 668} 669 670std::shared_ptr<gPanthera::LayoutManager> get_layout_manager() { 671return layout_manager; 672} 673 674std::shared_ptr<gPanthera::ContentManager> get_content_manager() { 675return content_manager; 676} 677 678void load_plugin(const std::string &plugin_path) { 679void *handle = dlopen(plugin_path.c_str(), RTLD_LAZY | RTLD_GLOBAL); 680if(!handle) { 681std::cerr << "Failed to load plugin: " << dlerror() << std::endl; 682return; 683} 684auto entrypoint = (plugin_entrypoint_type)dlsym(handle, "plugin_init"); 685if(!entrypoint) { 686std::cerr << "Failed to find entrypoint in plugin: " << dlerror() << std::endl; 687dlclose(handle); 688return; 689} 690entrypoint(); 691} 692}; 693 694Glib::RefPtr<PantheraWww> app = nullptr; 695 696extern "C" { 697void panthera_log(const char *message) { 698std::cerr << message << std::endl; 699} 700 701void panthera_add_pane(const char *id, const char *label, GtkImage *icon, GtkWidget *pane) { 702auto pane_child = Glib::wrap(pane); 703auto icon_cc = Glib::wrap(icon); 704auto label_cc = Glib::ustring(label); 705auto id_cc = Glib::ustring(id); 706auto new_pane = Gtk::make_managed<gPanthera::DockablePane>(app->get_layout_manager(), *pane_child, id_cc, label_cc, icon_cc); 707app->get_layout_manager()->add_pane(new_pane); 708} 709} 710 711int main(int argc, char *argv[]) { 712gPanthera::init(); 713app = PantheraWww::create(); 714return app->run(argc, argv); 715} 716