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 44bool starts_with(const Glib::ustring &str, const Glib::ustring &prefix) { 45return str.size() >= prefix.size() && str.compare(0, prefix.size(), prefix) == 0; 46} 47 48bool ends_with(const Glib::ustring &str, const Glib::ustring &suffix) { 49return str.size() >= suffix.size() && str.compare(str.size() - suffix.size(), suffix.size(), suffix) == 0; 50} 51 52struct SearchEngine { 53Glib::ustring name; 54Glib::ustring url; 55Glib::ustring get_search_url(const std::string &query) { 56auto pos = url.find("%s"); 57if(pos == Glib::ustring::npos) { 58throw std::runtime_error("Invalid search engine URL: missing '%s' placeholder"); 59} 60auto new_url = url; 61new_url.replace(pos, 2, url_encode(query)); 62return new_url; 63} 64}; 65 66struct HistoryEntry { 67int id; 68std::string url, title; 69int64_t timestamp; 70}; 71 72using StorageType = decltype(sqlite_orm::make_storage("", 73sqlite_orm::make_table("history", 74sqlite_orm::make_column("id", &HistoryEntry::id, sqlite_orm::primary_key()), 75sqlite_orm::make_column("url", &HistoryEntry::url), 76sqlite_orm::make_column("title", &HistoryEntry::title), 77sqlite_orm::make_column("timestamp", &HistoryEntry::timestamp) 78) 79)); 80 81class HistoryManager { 82public: 83static auto make_storage(const std::string& path) { 84return sqlite_orm::make_storage(path, 85sqlite_orm::make_table("history", 86sqlite_orm::make_column("id", &HistoryEntry::id, sqlite_orm::primary_key()), 87sqlite_orm::make_column("url", &HistoryEntry::url), 88sqlite_orm::make_column("title", &HistoryEntry::title), 89sqlite_orm::make_column("timestamp", &HistoryEntry::timestamp) 90) 91); 92} 93StorageType storage; 94explicit HistoryManager(const std::string &db_path) : storage(make_storage(db_path)) { 95storage.sync_schema(); 96} 97 98void log_url(const std::string &url, const std::string title, const int64_t timestamp = Glib::DateTime::create_now_local().to_unix()) { 99storage.insert(HistoryEntry{-1, url, title, timestamp}); 100} 101 102std::vector<HistoryEntry> get_history() { 103return storage.get_all<HistoryEntry>(); 104} 105 106std::vector<HistoryEntry> get_history_between(int64_t start_time, int64_t end_time) { 107using namespace sqlite_orm; 108return storage.get_all<HistoryEntry>( 109where(c(&HistoryEntry::timestamp) >= start_time and c(&HistoryEntry::timestamp) < end_time), 110order_by(&HistoryEntry::timestamp).desc() 111); 112} 113}; 114 115class HistoryViewer : public gPanthera::DockablePane { 116protected: 117std::shared_ptr<HistoryManager> history_manager; 118Gtk::Calendar *calendar; 119Gtk::Label *date_label; 120Gtk::Box *box_place; 121public: 122sigc::signal<void(const std::string &url)> signal_open_url; 123HistoryViewer(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)) { 124auto history_toolbar = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 0); 125auto history_button_previous = Gtk::make_managed<Gtk::Button>(); 126history_button_previous->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-previous-symbolic"))); 127history_button_previous->set_tooltip_text("Previous day"); 128auto history_button_next = Gtk::make_managed<Gtk::Button>(); 129history_button_next->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-next-symbolic"))); 130history_button_next->set_tooltip_text("Next day"); 131 132date_label = Gtk::make_managed<Gtk::Label>(""); 133auto date_button = Gtk::make_managed<Gtk::MenuButton>(); 134date_button->set_child(*date_label); 135date_button->set_tooltip_text("Select date"); 136date_button->set_direction(Gtk::ArrowType::NONE); 137auto date_popover = Gtk::make_managed<Gtk::Popover>(); 138calendar = Gtk::make_managed<Gtk::Calendar>(); 139date_popover->set_child(*calendar); 140date_button->set_popover(*date_popover); 141date_button->set_has_frame(false); 142date_button->set_hexpand(true); 143 144history_toolbar->append(*history_button_previous); 145history_toolbar->append(*date_button); 146history_toolbar->append(*history_button_next); 147auto box = dynamic_cast<Gtk::Box*>(this->child); 148box->append(*history_toolbar); 149 150history_button_next->signal_clicked().connect([this]() { 151Glib::DateTime calendar_day = calendar->get_date(); 152Glib::DateTime datetime = Glib::DateTime::create_local( 153calendar_day.get_year(), 154calendar_day.get_month(), 155calendar_day.get_day_of_month(), 1560, 0, 0 157); 158auto next_entries = this->history_manager->storage.get_all<HistoryEntry>( 159sqlite_orm::where(sqlite_orm::c(&HistoryEntry::timestamp) >= datetime.to_unix() + 86400), 160sqlite_orm::order_by(&HistoryEntry::timestamp).asc() 161); 162if(next_entries.empty()) { 163return; 164} 165auto last_entry = next_entries.front(); 166Glib::DateTime next_day = Glib::DateTime::create_now_local(last_entry.timestamp); 167Glib::DateTime new_date = Glib::DateTime::create_local( 168next_day.get_year(), 169next_day.get_month(), 170next_day.get_day_of_month(), 1710, 0, 0 172); 173calendar->select_day(new_date); 174reload_history(); 175}); 176 177history_button_previous->signal_clicked().connect([this]() { 178Glib::DateTime calendar_day = calendar->get_date(); 179Glib::DateTime datetime = Glib::DateTime::create_local( 180calendar_day.get_year(), 181calendar_day.get_month(), 182calendar_day.get_day_of_month(), 1830, 0, 0 184); 185auto previous_entries = this->history_manager->storage.get_all<HistoryEntry>( 186sqlite_orm::where(sqlite_orm::c(&HistoryEntry::timestamp) < datetime.to_unix()), 187sqlite_orm::order_by(&HistoryEntry::timestamp).desc() 188); 189if(previous_entries.empty()) { 190return; 191} 192auto last_entry = previous_entries.front(); 193Glib::DateTime previous_day = Glib::DateTime::create_now_local(last_entry.timestamp); 194Glib::DateTime new_date = Glib::DateTime::create_local( 195previous_day.get_year(), 196previous_day.get_month(), 197previous_day.get_day_of_month(), 1980, 0, 0 199); 200calendar->select_day(new_date); 201reload_history(); 202}); 203 204calendar->signal_day_selected().connect(sigc::mem_fun(*this, &HistoryViewer::reload_history)); 205calendar->signal_prev_month().connect(sigc::mem_fun(*this, &HistoryViewer::reload_history)); 206calendar->signal_next_month().connect(sigc::mem_fun(*this, &HistoryViewer::reload_history)); 207calendar->signal_prev_year().connect(sigc::mem_fun(*this, &HistoryViewer::reload_history)); 208calendar->signal_next_year().connect(sigc::mem_fun(*this, &HistoryViewer::reload_history)); 209Glib::DateTime datetime = Glib::DateTime::create_now_local(); 210calendar->select_day(datetime); 211 212this->signal_pane_shown.connect([this]() { 213// Load the current date 214reload_history(); 215}); 216 217auto scrolled_window = Gtk::make_managed<Gtk::ScrolledWindow>(); 218box_place = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0); 219auto viewport = Gtk::make_managed<Gtk::Viewport>(nullptr, nullptr); 220viewport->set_child(*box_place); 221scrolled_window->set_child(*viewport); 222scrolled_window->set_policy(Gtk::PolicyType::AUTOMATIC, Gtk::PolicyType::AUTOMATIC); 223scrolled_window->set_vexpand(true); 224scrolled_window->set_hexpand(true); 225box->append(*scrolled_window); 226 227// TODO: allow deleting entries 228} 229 230void reload_history() { 231Glib::DateTime calendar_day = calendar->get_date(); 232Glib::DateTime datetime = Glib::DateTime::create_local( 233calendar_day.get_year(), 234calendar_day.get_month(), 235calendar_day.get_day_of_month(), 2360, 0, 0 237); 238date_label->set_text(datetime.format("%x")); 239auto box = Gtk::make_managed<Gtk::ListBox>(); 240auto history_entries = history_manager->get_history_between(datetime.to_unix(), datetime.to_unix() + 86400); 241for(const auto &entry : history_entries) { 242auto row = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 4); 243auto title = Gtk::make_managed<Gtk::Label>(entry.title.empty() ? "Untitled" : entry.title); 244title->set_halign(Gtk::Align::START); 245title->set_ellipsize(Pango::EllipsizeMode::MIDDLE); 246auto label = Gtk::make_managed<Gtk::Label>(entry.url); 247label->set_halign(Gtk::Align::START); 248label->set_ellipsize(Pango::EllipsizeMode::MIDDLE); 249row->append(*title); 250row->append(*label); 251if(auto time = Glib::DateTime::create_now_local(entry.timestamp)) { 252label->set_tooltip_text(time.format("%c")); 253box->append(*row); 254} 255} 256if(box_place->get_first_child()) { 257box_place->remove(*box_place->get_first_child()); 258} 259box->signal_row_activated().connect([this](Gtk::ListBoxRow *row) { 260if(auto box_row = dynamic_cast<Gtk::Box*>(row->get_child())) { 261if(auto label = dynamic_cast<Gtk::Label*>(box_row->get_last_child())) { 262auto url = label->get_text(); 263if(!url.empty()) { 264signal_open_url.emit(url); 265} 266} 267} 268}); 269box_place->append(*box); 270} 271}; 272 273class PantheraWindow; 274 275class PantheraWww : public Gtk::Application { 276private: 277std::shared_ptr<gPanthera::ContentManager> content_manager; 278std::string cookie_file; 279std::vector<SearchEngine> search_engines; 280SearchEngine* default_search_engine; 281std::shared_ptr<HistoryManager> history_manager; 282Glib::RefPtr<Gio::Menu> main_menu; 283std::string new_tab_page; 284 285static void load_change_callback(WebKitWebView* object, WebKitLoadEvent load_event, gpointer data); 286static void notify_callback(GObject* object, GParamSpec* pspec, gpointer data); 287static void notify_focused_callback(GObject* object, GParamSpec* pspec, gpointer data); 288static void on_back_pressed(GtkGestureClick* gesture, int n_press, double x, double y, gpointer user_data); 289static void on_forward_pressed(GtkGestureClick* gesture, int n_press, double x, double y, gpointer user_data); 290static gboolean on_decide_policy(WebKitWebView* source, WebKitPolicyDecision* decision, WebKitPolicyDecisionType type, gpointer user_data); 291 292protected: 293void on_startup() override; 294void on_activate() override; 295PantheraWindow *make_window(); 296 297public: 298PantheraWww(); 299friend class PantheraWindow; 300void on_new_tab(gPanthera::ContentStack* stack, const Glib::ustring& url = "", bool focus = true, bool new_window = false); 301void set_search_engines_from_json(const std::string& json_string); 302static Glib::RefPtr<PantheraWww> create(); 303std::shared_ptr<gPanthera::ContentManager> get_content_manager(); 304void load_plugin(const std::string& plugin_path); 305}; 306 307class PantheraWindow : public Gtk::ApplicationWindow { 308private: 309std::shared_ptr<gPanthera::LayoutManager> layout_manager; 310Gtk::Entry *url_bar; 311HistoryViewer *history_viewer; 312gPanthera::ContentPage *controlled_page = nullptr; 313public: 314friend class PantheraWww; 315explicit PantheraWindow(Glib::RefPtr<Gtk::Application> const &application) : Gtk::ApplicationWindow(application) { 316auto panthera = dynamic_cast<PantheraWww*>(application.get()); 317if(!panthera) { 318throw std::runtime_error("Application is not a PantheraWww instance"); 319} 320this->set_default_size(800, 600); 321layout_manager = std::make_shared<gPanthera::LayoutManager>(); 322 323auto dock_stack_1 = Gtk::make_managed<gPanthera::DockStack>(layout_manager, "One", "one"); 324auto switcher_1 = Gtk::make_managed<gPanthera::DockStackSwitcher>(dock_stack_1, Gtk::Orientation::HORIZONTAL); 325auto dock_stack_2 = Gtk::make_managed<gPanthera::DockStack>(layout_manager, "Two", "two"); 326auto switcher_2 = Gtk::make_managed<gPanthera::DockStackSwitcher>(dock_stack_2, Gtk::Orientation::VERTICAL); 327auto pane_1_content = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0); 328auto debug_button = Gtk::make_managed<Gtk::Button>("Debug"); 329auto print_history_button = Gtk::make_managed<Gtk::Button>("Print history"); 330print_history_button->signal_clicked().connect([this, panthera]() { 331auto history = panthera->history_manager->get_history(); 332for(const auto &entry : history) { 333std::cout << "entry " << entry.id << ", url " << entry.url << ", unix time " << entry.timestamp << std::endl; 334// TODO: exclude redirecting URLs 335} 336}); 337pane_1_content->append(*debug_button); 338pane_1_content->append(*print_history_button); 339auto pane_1_icon = Gtk::make_managed<Gtk::Image>(Gio::Icon::create("help-about-symbolic")); 340auto pane_1 = Gtk::make_managed<gPanthera::DockablePane>(layout_manager, *pane_1_content, "debug", "Debugging options", pane_1_icon); 341 342dock_stack_1->set_transition_type(Gtk::StackTransitionType::SLIDE_LEFT_RIGHT); 343dock_stack_1->set_transition_duration(125); 344dock_stack_1->set_expand(true); 345dock_stack_2->set_transition_type(Gtk::StackTransitionType::SLIDE_UP_DOWN); 346dock_stack_2->set_transition_duration(125); 347dock_stack_2->set_expand(true); 348 349auto outer_grid = Gtk::make_managed<Gtk::Grid>(); 350outer_grid->attach(*switcher_2, 0, 2, 1, 1); 351outer_grid->attach(*switcher_1, 1, 3, 1, 1); 352auto outer_paned = Gtk::make_managed<Gtk::Paned>(Gtk::Orientation::HORIZONTAL); 353outer_paned->set_start_child(*dock_stack_2); 354auto inner_paned = Gtk::make_managed<Gtk::Paned>(Gtk::Orientation::VERTICAL); 355auto content = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0); 356panthera->content_manager = std::make_shared<gPanthera::ContentManager>(); 357std::function<bool(gPanthera::ContentPage*)> detach_handler; 358detach_handler = [](gPanthera::ContentPage *widget) { 359auto new_stack = Gtk::make_managed<gPanthera::ContentStack>(widget->content_manager, widget->get_stack()->get_detach_handler()); 360auto 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()); 361auto new_notebook = Gtk::make_managed<gPanthera::ContentNotebook>(new_stack, new_switcher); 362auto window = new gPanthera::ContentWindow(new_notebook); 363widget->redock(new_stack); 364window->present(); 365new_stack->signal_leave_empty.connect([window]() { 366window->close(); 367delete window; 368}); 369return true; 370}; 371 372auto return_extra_child = [this, panthera](gPanthera::ContentTabBar *switcher) { 373auto new_tab_button = Gtk::make_managed<Gtk::Button>(); 374new_tab_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("list-add-symbolic"))); 375new_tab_button->set_tooltip_text("New tab"); 376new_tab_button->signal_clicked().connect([this, switcher, panthera]() { 377panthera->on_new_tab(switcher->get_stack()); 378}); 379return new_tab_button; 380}; 381auto content_stack = Gtk::make_managed<gPanthera::ContentStack>(panthera->content_manager, detach_handler); 382auto content_stack_switcher = Gtk::make_managed<gPanthera::ContentTabBar>(content_stack, Gtk::Orientation::HORIZONTAL, return_extra_child); 383panthera->content_manager->add_stack(content_stack); 384WebKitWebView *webview = WEBKIT_WEB_VIEW(webkit_web_view_new()); // for some reason, this has to be created 385content->set_name("content_box"); 386auto content_notebook = Gtk::make_managed<gPanthera::ContentNotebook>(content_stack, content_stack_switcher, Gtk::PositionType::TOP); 387content->append(*content_notebook); 388inner_paned->set_start_child(*content); 389inner_paned->set_end_child(*dock_stack_1); 390outer_paned->set_end_child(*inner_paned); 391outer_grid->attach(*outer_paned, 1, 2, 1, 1); 392// Create the toolbar 393auto main_toolbar = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 8); 394// URL bar 395url_bar = Gtk::make_managed<Gtk::Entry>(); 396url_bar->set_placeholder_text("Enter URL"); 397url_bar->set_hexpand(true); 398auto load_url_callback = [this, panthera]() { 399bool has_protocol = url_bar->get_text().find("://") != std::string::npos; 400if(!has_protocol) { 401url_bar->set_text("http://" + url_bar->get_text()); 402} 403if(controlled_page) { 404if(auto webview = WEBKIT_WEB_VIEW(controlled_page->get_child()->get_first_child()->gobj())) { 405webkit_web_view_load_uri(webview, url_bar->get_text().c_str()); 406} 407} 408}; 409// Go 410auto go_button = Gtk::make_managed<Gtk::Button>("Go"); 411go_button->signal_clicked().connect(load_url_callback); 412url_bar->signal_activate().connect(load_url_callback); 413panthera->content_manager->signal_page_operated.connect([this, panthera](gPanthera::ContentPage *page) { 414if(!page->get_child()) { 415return; 416} 417if(!page->get_child()->get_first_child()) { 418return; 419} 420if(page->get_root() != this) { 421// The page is in some other window 422return; 423} 424controlled_page = page; 425url_bar->set_text(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj()))); 426guint url_update_handler = g_signal_connect(page->get_child()->get_first_child()->gobj(), "notify", G_CALLBACK(panthera->notify_focused_callback), this); 427std::shared_ptr<sigc::connection> control_signal_handler = std::make_shared<sigc::connection>(); 428*control_signal_handler = page->signal_control_status_changed.connect([this, page, control_signal_handler, url_update_handler](bool controlled) { 429if(!controlled) { 430control_signal_handler->disconnect(); 431if(page->get_child() && page->get_child()->get_first_child() && WEBKIT_IS_WEB_VIEW(page->get_child()->get_first_child()->gobj())) { 432g_signal_handler_disconnect(page->get_child()->get_first_child()->gobj(), url_update_handler); 433} 434} 435}); 436}); 437history_viewer = Gtk::make_managed<HistoryViewer>(layout_manager, panthera->history_manager); 438history_viewer->signal_open_url.connect([this, panthera](const std::string &url) { 439panthera->on_new_tab(nullptr, url, true); 440}); 441// Back, forward, reload 442auto back_button = Gtk::make_managed<Gtk::Button>(); 443back_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-previous-symbolic"))); 444back_button->set_tooltip_text("Back"); 445auto forward_button = Gtk::make_managed<Gtk::Button>(); 446forward_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-next-symbolic"))); 447forward_button->set_tooltip_text("Forward"); 448auto reload_button = Gtk::make_managed<Gtk::Button>(); 449reload_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("view-refresh-symbolic"))); 450reload_button->set_tooltip_text("Reload"); 451back_button->signal_clicked().connect([this, panthera]() { 452if(controlled_page) { 453if(auto webview = WEBKIT_WEB_VIEW(controlled_page->get_child()->get_first_child()->gobj())) { 454webkit_web_view_go_back(webview); 455} 456} 457}); 458forward_button->signal_clicked().connect([this, panthera]() { 459if(controlled_page) { 460if(auto webview = WEBKIT_WEB_VIEW(controlled_page->get_child()->get_first_child()->gobj())) { 461webkit_web_view_go_forward(webview); 462} 463} 464}); 465reload_button->signal_clicked().connect([this, panthera]() { 466if(controlled_page) { 467if(auto webview = WEBKIT_WEB_VIEW(controlled_page->get_child()->get_first_child()->gobj())) { 468webkit_web_view_reload(webview); 469} 470} 471}); 472// Search bar 473// TODO: provide history, provide a menu button with the other engines 474auto search_bar = Gtk::make_managed<Gtk::Entry>(); 475search_bar->set_placeholder_text("Search"); 476search_bar->set_hexpand(true); 477if(panthera->search_engines.empty()) { 478search_bar->set_sensitive(false); 479search_bar->set_placeholder_text("No search"); 480} 481auto search_callback = [this, search_bar, content_stack, panthera]() { 482// Create a new tab with the search results 483if(panthera->content_manager->get_last_operated_page()) { 484panthera->on_new_tab(nullptr); 485} else { 486panthera->on_new_tab(content_stack); 487} 488auto page = panthera->content_manager->get_last_operated_page(); 489if(page) { 490if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) { 491auto search_url = panthera->default_search_engine->get_search_url(search_bar->get_text()); 492webkit_web_view_load_uri(webview, search_url.c_str()); 493} 494} 495}; 496if(panthera->default_search_engine) { 497search_bar->signal_activate().connect(search_callback); 498} 499 500// Assemble the toolbar 501main_toolbar->append(*back_button); 502main_toolbar->append(*forward_button); 503main_toolbar->append(*reload_button); 504main_toolbar->append(*url_bar); 505main_toolbar->append(*go_button); 506main_toolbar->append(*search_bar); 507if(panthera->default_search_engine) { 508auto search_button = Gtk::make_managed<Gtk::Button>(panthera->default_search_engine->name); 509search_button->signal_clicked().connect(search_callback); 510main_toolbar->append(*search_button); 511} 512outer_grid->attach(*main_toolbar, 0, 1, 2, 1); 513this->set_child(*outer_grid); 514debug_button->signal_clicked().connect([this, panthera]() { 515if(panthera->content_manager->get_last_operated_page()) { 516std::cout << "Last operated page: " << panthera->content_manager->get_last_operated_page()->get_name() << std::endl; 517} else { 518std::cout << "No page operated!" << std::endl; 519} 520}); 521// TODO: Use the last operated page and allow opening tabs next to the last operated page using certain panes 522// Load the existing layout, if it exists 523std::ifstream layout_file_in("layout.json"); 524if(layout_file_in) { 525std::string layout_json((std::istreambuf_iterator<char>(layout_file_in)), std::istreambuf_iterator<char>()); 526layout_file_in.close(); 527layout_manager->restore_json_layout(layout_json); 528} else { 529// Create a new layout if the file doesn't exist 530layout_file_in.close(); 531 532dock_stack_1->add_pane(*pane_1); 533dock_stack_1->add_pane(*history_viewer); 534 535std::ofstream layout_file_out("layout.json"); 536layout_file_out << layout_manager->get_layout_as_json(); 537layout_file_out.close(); 538} 539// Save the layout when changed 540layout_manager->signal_pane_moved.connect([this, panthera](gPanthera::DockablePane *pane) { 541std::ofstream layout_file_out("layout.json"); 542layout_file_out << layout_manager->get_layout_as_json(); 543layout_file_out.close(); 544std::cout << "Layout changed: " << layout_manager->get_layout_as_json() << std::endl; 545}); 546set_show_menubar(true); 547} 548}; 549 550void PantheraWww::load_change_callback(WebKitWebView *object, WebKitLoadEvent load_event, gpointer data) { 551if(auto self = static_cast<PantheraWww*>(data)) { 552if(load_event == WEBKIT_LOAD_COMMITTED) { 553if(webkit_web_view_get_title(WEBKIT_WEB_VIEW(object)) == nullptr) { 554self->history_manager->log_url(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(object)), ""); 555} else { 556self->history_manager->log_url(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(object)), 557webkit_web_view_get_title(WEBKIT_WEB_VIEW(object))); 558} 559// TODO: reload visible history viewers 560} 561} 562} 563 564void PantheraWww::notify_callback(GObject *object, GParamSpec *pspec, gpointer data) { 565if(!gtk_widget_get_parent(GTK_WIDGET(object))) { 566return; 567} 568if(auto self = static_cast<PantheraWww*>(data)) { 569auto parent = gtk_widget_get_parent(gtk_widget_get_parent(GTK_WIDGET(object))); 570if(auto page = dynamic_cast<gPanthera::ContentPage*>(Glib::wrap(parent))) { 571if(g_strcmp0(pspec->name, "title") == 0) { 572if(auto label = dynamic_cast<Gtk::Label*>(page->tab_widget->get_last_child())) { 573if(strlen(webkit_web_view_get_title(WEBKIT_WEB_VIEW(object))) == 0) { 574label->set_label("Untitled"); 575} else { 576label->set_label(webkit_web_view_get_title(WEBKIT_WEB_VIEW(object))); 577} 578} 579} else if(g_strcmp0(pspec->name, "favicon") == 0) { 580// Update favicons 581if(auto image = dynamic_cast<Gtk::Image*>(page->tab_widget->get_first_child())) { 582image->set_from_icon_name("image-loading-symbolic"); 583if(auto favicon = webkit_web_view_get_favicon(WEBKIT_WEB_VIEW(object))) { 584gtk_image_set_from_paintable(image->gobj(), GDK_PAINTABLE(favicon)); 585} 586} 587} 588} 589} 590} 591 592void PantheraWww::notify_focused_callback(GObject *object, GParamSpec *pspec, gpointer data) { 593if(!gtk_widget_get_parent(GTK_WIDGET(object))) { 594return; 595} 596auto this_ = static_cast<PantheraWww*>(data); 597auto parent = gtk_widget_get_parent(gtk_widget_get_parent(GTK_WIDGET(object))); 598if(auto page = dynamic_cast<gPanthera::ContentPage*>(Glib::wrap(parent))) { 599if(g_strcmp0(pspec->name, "uri") == 0) { 600if(auto main_window = dynamic_cast<PantheraWindow*>(page->get_root())) { 601main_window->url_bar->set_text(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(object))); 602} 603} 604} 605} 606 607void PantheraWww::on_back_pressed(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) { 608WebKitWebView *webview = WEBKIT_WEB_VIEW(user_data); 609webkit_web_view_go_back(webview); 610} 611 612void PantheraWww::on_forward_pressed(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) { 613WebKitWebView *webview = WEBKIT_WEB_VIEW(user_data); 614webkit_web_view_go_forward(webview); 615} 616 617gboolean PantheraWww::on_decide_policy(WebKitWebView *source, WebKitPolicyDecision *decision, WebKitPolicyDecisionType type, gpointer user_data) { 618if(auto self = static_cast<PantheraWww*>(user_data)) { 619if(type == WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION) { 620auto action = webkit_navigation_policy_decision_get_navigation_action(WEBKIT_NAVIGATION_POLICY_DECISION(decision)); 621Glib::ustring url = Glib::ustring(webkit_uri_request_get_uri(webkit_navigation_action_get_request(action))); 622if(!starts_with(url, "about:") && url.find("://") == Glib::ustring::npos) { 623// It is a special scheme (mailto:, tel: etc.) 624try { 625Gio::AppInfo::launch_default_for_uri(url); 626} catch(const Glib::Error &exception) { 627std::cerr << "Failed to launch special URI: " << exception.what() << std::endl; 628} 629webkit_policy_decision_ignore(decision); 630return true; 631} 632if(webkit_navigation_action_get_mouse_button(action) == 2) { 633// Middle-click opens in a new window 634self->on_new_tab(nullptr, url, false); 635webkit_policy_decision_ignore(decision); 636return true; 637} 638} else if(type == WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION) { 639auto action = webkit_navigation_policy_decision_get_navigation_action(WEBKIT_NAVIGATION_POLICY_DECISION(decision)); 640self->on_new_tab(nullptr, webkit_uri_request_get_uri(webkit_navigation_action_get_request(action)), false, true); 641return true; 642} 643} 644return false; 645} 646 647void PantheraWww::on_new_tab(gPanthera::ContentStack *stack, const Glib::ustring &url, bool focus, bool new_window) { 648if(!stack) { 649// Find the current area 650stack = content_manager->get_last_operated_page()->get_stack(); 651} 652Glib::ustring url_ = url; 653if(url.empty()) { 654url_ = new_tab_page; 655} 656 657WebKitWebView *webview = WEBKIT_WEB_VIEW(webkit_web_view_new()); 658gtk_widget_set_hexpand(GTK_WIDGET(webview), true); 659gtk_widget_set_vexpand(GTK_WIDGET(webview), true); 660auto page_content = Gtk::make_managed<Gtk::Box>(); 661gtk_box_append(page_content->gobj(), GTK_WIDGET(webview)); 662auto page_tab = new Gtk::Box(); 663page_tab->set_orientation(Gtk::Orientation::HORIZONTAL); 664auto initial_icon = Gtk::make_managed<Gtk::Image>(Gio::Icon::create("image-loading-symbolic")); 665page_tab->append(*initial_icon); 666page_tab->append(*Gtk::make_managed<Gtk::Label>("Untitled")); 667page_tab->set_spacing(4); 668auto page = Gtk::make_managed<gPanthera::ContentPage>(content_manager, stack, page_content, page_tab); 669g_signal_connect(webview, "notify", G_CALLBACK(notify_callback), this); 670g_signal_connect(webview, "load-changed", G_CALLBACK(load_change_callback), this); 671g_signal_connect(webview, "decide-policy", G_CALLBACK(on_decide_policy), this); 672webkit_web_view_load_uri(webview, url_.data()); 673auto cookie_manager = webkit_network_session_get_cookie_manager(webkit_web_view_get_network_session(webview)); 674webkit_cookie_manager_set_persistent_storage(cookie_manager, cookie_file.c_str(), WEBKIT_COOKIE_PERSISTENT_STORAGE_TEXT); 675webkit_cookie_manager_set_accept_policy(cookie_manager, WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS); 676auto website_data_manager = webkit_network_session_get_website_data_manager(webkit_web_view_get_network_session(webview)); 677webkit_website_data_manager_set_favicons_enabled(website_data_manager, true); 678GtkEventController *click_controller_back = GTK_EVENT_CONTROLLER(gtk_gesture_click_new()); 679gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click_controller_back), 8); 680g_signal_connect(click_controller_back, "pressed", G_CALLBACK(on_back_pressed), webview); 681gtk_widget_add_controller(GTK_WIDGET(webview), click_controller_back); 682 683GtkEventController *click_controller_forward = GTK_EVENT_CONTROLLER(gtk_gesture_click_new()); 684gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click_controller_forward), 9); 685g_signal_connect(click_controller_forward, "pressed", G_CALLBACK(on_forward_pressed), webview); 686gtk_widget_add_controller(GTK_WIDGET(webview), click_controller_forward); 687 688stack->add_page(*page); 689if(focus) { 690stack->set_visible_child(*page); 691content_manager->set_last_operated_page(page); 692} 693if(new_window) { 694bool result = stack->signal_detach.emit(page); 695} 696} 697 698PantheraWindow *PantheraWww::make_window() { 699Glib::RefPtr<Gtk::Application> self_ref = Glib::make_refptr_for_instance<Gtk::Application>(this); 700auto window = new PantheraWindow(self_ref); 701add_window(*window); 702return window; 703} 704 705void PantheraWww::on_startup() { 706Gtk::Application::on_startup(); 707// Get search engines 708std::ifstream search_engines_file_in("search_engines.json"); 709if(search_engines_file_in) { 710std::string search_engines_json((std::istreambuf_iterator<char>(search_engines_file_in)), std::istreambuf_iterator<char>()); 711search_engines_file_in.close(); 712set_search_engines_from_json(search_engines_json); 713} else { 714search_engines_file_in.close(); 715auto empty_json = nlohmann::json::array(); 716std::ofstream search_engines_file_out("search_engines.json"); 717search_engines_file_out << empty_json.dump(4); 718} 719// Load settings 720new_tab_page = "about:blank"; 721std::ifstream settings_file_in("settings.json"); 722if(settings_file_in) { 723std::string settings_json((std::istreambuf_iterator<char>(settings_file_in)), std::istreambuf_iterator<char>()); 724settings_file_in.close(); 725auto json = nlohmann::json::parse(settings_json); 726if(json.contains("ntp")) { 727new_tab_page = json["ntp"].get<std::string>(); 728} 729} else { 730settings_file_in.close(); 731auto empty_json = nlohmann::json::object(); 732empty_json["ntp"] = "about:blank"; 733std::ofstream settings_file_out("settings.json"); 734settings_file_out << empty_json.dump(4); 735settings_file_out.close(); 736} 737 738// Load plugins 739std::ifstream plugin_file_in("plugins.json"); 740if(plugin_file_in) { 741std::string plugin_json((std::istreambuf_iterator<char>(plugin_file_in)), std::istreambuf_iterator<char>()); 742plugin_file_in.close(); 743auto json = nlohmann::json::parse(plugin_json); 744for(auto &plugin : json) { 745auto plugin_path = plugin["path"].get<std::string>(); 746load_plugin(plugin_path); 747} 748} else { 749plugin_file_in.close(); 750auto empty_json = nlohmann::json::array(); 751std::ofstream plugin_file_out("plugins.json"); 752plugin_file_out << empty_json.dump(4); 753plugin_file_out.close(); 754} 755history_manager = std::make_shared<HistoryManager>("history.db"); 756 757// Known bug <https://gitlab.gnome.org/GNOME/epiphany/-/issues/2714>: 758// JS can't veto the application's accelerators using `preventDefault()`. This is 759// not possible in WebKitGTK, or at least I can't find a way to do it. 760 761main_menu = Gio::Menu::create(); 762// File 763auto file_menu = Gio::Menu::create(); 764// New tab 765auto new_tab_action = Gio::SimpleAction::create("new_tab"); 766new_tab_action->signal_activate().connect([this](const Glib::VariantBase&) { 767on_new_tab(nullptr); 768}); 769add_action(new_tab_action); 770set_accels_for_action("app.new_tab", {"<Primary>T"}); 771file_menu->append("New tab", "app.new_tab"); 772// Close tab 773auto close_tab_action = Gio::SimpleAction::create("close_tab"); 774close_tab_action->signal_activate().connect([this](const Glib::VariantBase&) { 775auto page = content_manager->get_last_operated_page(); 776if(page) { 777page->close(); 778} 779}); 780add_action(close_tab_action); 781set_accels_for_action("app.close_tab", {"<Primary>W"}); 782file_menu->append("Close tab", "app.close_tab"); 783// New window 784auto new_window_action = Gio::SimpleAction::create("new_window"); 785new_window_action->signal_activate().connect([this](const Glib::VariantBase&) { 786auto window = make_window(); 787window->present(); 788}); 789add_action(new_window_action); 790set_accels_for_action("app.new_window", {"<Primary>N"}); 791file_menu->append("New window", "app.new_window"); 792 793main_menu->append_submenu("File", file_menu); 794 795set_menubar(main_menu); 796} 797 798void PantheraWww::on_activate() { 799auto window = make_window(); 800window->present(); 801} 802 803void PantheraWww::set_search_engines_from_json(const std::string &json_string) { 804auto json = nlohmann::json::parse(json_string); 805Glib::ustring default_search_engine_name; 806for(auto &engine : json) { 807SearchEngine search_engine; 808search_engine.name = engine["name"].get<std::string>(); 809search_engine.url = engine["url"].get<std::string>(); 810search_engines.push_back(search_engine); 811if(engine.contains("default") && engine["default"].get<bool>()) { 812default_search_engine_name = engine["name"].get<std::string>(); 813} 814} 815for(auto &search_engine : search_engines) { 816if(search_engine.name == default_search_engine_name) { 817default_search_engine = &search_engine; 818break; 819} 820} 821} 822 823PantheraWww::PantheraWww() : Gtk::Application("com.roundabout_host.roundabout.panthera_www", Gio::Application::Flags::NONE) { 824} 825 826Glib::RefPtr<PantheraWww> PantheraWww::create() { 827return Glib::make_refptr_for_instance<PantheraWww>(new PantheraWww()); 828} 829 830void PantheraWww::load_plugin(const std::string &plugin_path) { 831void *handle = dlopen(plugin_path.c_str(), RTLD_LAZY | RTLD_GLOBAL); 832if(!handle) { 833std::cerr << "Failed to load plugin: " << dlerror() << std::endl; 834return; 835} 836auto entrypoint = (plugin_entrypoint_type)dlsym(handle, "plugin_init"); 837if(!entrypoint) { 838std::cerr << "Failed to find entrypoint in plugin: " << dlerror() << std::endl; 839dlclose(handle); 840return; 841} 842entrypoint(); 843} 844 845Glib::RefPtr<PantheraWww> app = nullptr; 846 847extern "C" { 848void panthera_log(const char *message) { 849std::cerr << message << std::endl; 850} 851 852void panthera_add_pane(const char *id, const char *label, GtkImage *icon, GtkWidget *pane) { 853auto pane_child = Glib::wrap(pane); 854auto icon_cc = Glib::wrap(icon); 855auto label_cc = Glib::ustring(label); 856auto id_cc = Glib::ustring(id); 857//auto new_pane = Gtk::make_managed<gPanthera::DockablePane>(app->get_layout_manager(), *pane_child, id_cc, label_cc, icon_cc); 858// TODO: Port to multi-window 859//app->get_layout_manager()->add_pane(new_pane); 860} 861} 862 863int main(int argc, char *argv[]) { 864gPanthera::init(); 865app = PantheraWww::create(); 866return app->run(argc, argv); 867} 868