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; 282HistoryViewer* history_viewer; 283Glib::RefPtr<Gio::Menu> main_menu; 284std::string new_tab_page; 285 286static void load_change_callback(WebKitWebView* object, WebKitLoadEvent load_event, gpointer data); 287static void notify_callback(GObject* object, GParamSpec* pspec, gpointer data); 288static void notify_focused_callback(GObject* object, GParamSpec* pspec, gpointer data); 289static void on_back_pressed(GtkGestureClick* gesture, int n_press, double x, double y, gpointer user_data); 290static void on_forward_pressed(GtkGestureClick* gesture, int n_press, double x, double y, gpointer user_data); 291static gboolean on_decide_policy(WebKitWebView* source, WebKitPolicyDecision* decision, WebKitPolicyDecisionType type, gpointer user_data); 292 293protected: 294void on_startup() override; 295void on_activate() override; 296PantheraWindow *make_window(); 297 298public: 299PantheraWww(); 300friend class PantheraWindow; 301void on_new_tab(gPanthera::ContentStack* stack, const Glib::ustring& url = "", bool focus = true, bool new_window = false); 302void set_search_engines_from_json(const std::string& json_string); 303static Glib::RefPtr<PantheraWww> create(); 304std::shared_ptr<gPanthera::ContentManager> get_content_manager(); 305void load_plugin(const std::string& plugin_path); 306}; 307 308class PantheraWindow : public Gtk::ApplicationWindow { 309private: 310std::shared_ptr<gPanthera::LayoutManager> layout_manager; 311Gtk::Entry *url_bar; 312public: 313friend class PantheraWww; 314explicit PantheraWindow(Glib::RefPtr<Gtk::Application> const &application) : Gtk::ApplicationWindow(application) { 315auto panthera = dynamic_cast<PantheraWww*>(application.get()); 316if(!panthera) { 317throw std::runtime_error("Application is not a PantheraWww instance"); 318} 319this->set_default_size(800, 600); 320layout_manager = std::make_shared<gPanthera::LayoutManager>(); 321 322auto dock_stack_1 = Gtk::make_managed<gPanthera::DockStack>(layout_manager, "One", "one"); 323auto switcher_1 = Gtk::make_managed<gPanthera::DockStackSwitcher>(dock_stack_1, Gtk::Orientation::HORIZONTAL); 324auto dock_stack_2 = Gtk::make_managed<gPanthera::DockStack>(layout_manager, "Two", "two"); 325auto switcher_2 = Gtk::make_managed<gPanthera::DockStackSwitcher>(dock_stack_2, Gtk::Orientation::VERTICAL); 326auto pane_1_content = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0); 327auto debug_button = Gtk::make_managed<Gtk::Button>("Debug"); 328auto print_history_button = Gtk::make_managed<Gtk::Button>("Print history"); 329print_history_button->signal_clicked().connect([this, panthera]() { 330auto history = panthera->history_manager->get_history(); 331for(const auto &entry : history) { 332std::cout << "entry " << entry.id << ", url " << entry.url << ", unix time " << entry.timestamp << std::endl; 333// TODO: exclude redirecting URLs 334} 335}); 336pane_1_content->append(*debug_button); 337pane_1_content->append(*print_history_button); 338auto pane_1_icon = Gtk::make_managed<Gtk::Image>(Gio::Icon::create("help-about-symbolic")); 339auto pane_1 = Gtk::make_managed<gPanthera::DockablePane>(layout_manager, *pane_1_content, "debug", "Debugging options", pane_1_icon); 340 341dock_stack_1->set_transition_type(Gtk::StackTransitionType::SLIDE_LEFT_RIGHT); 342dock_stack_1->set_transition_duration(125); 343dock_stack_1->set_expand(true); 344dock_stack_2->set_transition_type(Gtk::StackTransitionType::SLIDE_UP_DOWN); 345dock_stack_2->set_transition_duration(125); 346dock_stack_2->set_expand(true); 347 348auto outer_grid = Gtk::make_managed<Gtk::Grid>(); 349outer_grid->attach(*switcher_2, 0, 2, 1, 1); 350outer_grid->attach(*switcher_1, 1, 3, 1, 1); 351auto outer_paned = Gtk::make_managed<Gtk::Paned>(Gtk::Orientation::HORIZONTAL); 352outer_paned->set_start_child(*dock_stack_2); 353auto inner_paned = Gtk::make_managed<Gtk::Paned>(Gtk::Orientation::VERTICAL); 354auto content = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0); 355panthera->content_manager = std::make_shared<gPanthera::ContentManager>(); 356std::function<bool(gPanthera::ContentPage*)> detach_handler; 357detach_handler = [](gPanthera::ContentPage *widget) { 358auto new_stack = Gtk::make_managed<gPanthera::ContentStack>(widget->content_manager, widget->get_stack()->get_detach_handler()); 359auto 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()); 360auto new_notebook = Gtk::make_managed<gPanthera::ContentNotebook>(new_stack, new_switcher); 361auto window = new gPanthera::ContentWindow(new_notebook); 362widget->redock(new_stack); 363window->present(); 364new_stack->signal_leave_empty.connect([window]() { 365window->close(); 366delete window; 367}); 368return true; 369}; 370 371auto return_extra_child = [this, panthera](gPanthera::ContentTabBar *switcher) { 372auto new_tab_button = Gtk::make_managed<Gtk::Button>(); 373new_tab_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("list-add-symbolic"))); 374new_tab_button->set_tooltip_text("New tab"); 375new_tab_button->signal_clicked().connect([this, switcher, panthera]() { 376panthera->on_new_tab(switcher->get_stack()); 377}); 378return new_tab_button; 379}; 380auto content_stack = Gtk::make_managed<gPanthera::ContentStack>(panthera->content_manager, detach_handler); 381auto content_stack_switcher = Gtk::make_managed<gPanthera::ContentTabBar>(content_stack, Gtk::Orientation::HORIZONTAL, return_extra_child); 382panthera->content_manager->add_stack(content_stack); 383WebKitWebView *webview = WEBKIT_WEB_VIEW(webkit_web_view_new()); // for some reason, this has to be created 384content->set_name("content_box"); 385auto content_notebook = Gtk::make_managed<gPanthera::ContentNotebook>(content_stack, content_stack_switcher, Gtk::PositionType::TOP); 386content->append(*content_notebook); 387inner_paned->set_start_child(*content); 388inner_paned->set_end_child(*dock_stack_1); 389outer_paned->set_end_child(*inner_paned); 390outer_grid->attach(*outer_paned, 1, 2, 1, 1); 391// Create the toolbar 392auto main_toolbar = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 8); 393// URL bar 394url_bar = Gtk::make_managed<Gtk::Entry>(); 395url_bar->set_placeholder_text("Enter URL"); 396url_bar->set_hexpand(true); 397auto load_url_callback = [this, panthera]() { 398auto page = panthera->content_manager->get_last_operated_page(); 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(page) { 404if(auto webview = WEBKIT_WEB_VIEW(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} 420url_bar->set_text(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj()))); 421guint url_update_handler = g_signal_connect(page->get_child()->get_first_child()->gobj(), "notify", G_CALLBACK(panthera->notify_focused_callback), this); 422std::shared_ptr<sigc::connection> control_signal_handler = std::make_shared<sigc::connection>(); 423*control_signal_handler = page->signal_control_status_changed.connect([this, page, control_signal_handler, url_update_handler](bool controlled) { 424if(!controlled) { 425control_signal_handler->disconnect(); 426if(page->get_child() && page->get_child()->get_first_child() && WEBKIT_IS_WEB_VIEW(page->get_child()->get_first_child()->gobj())) { 427g_signal_handler_disconnect(page->get_child()->get_first_child()->gobj(), url_update_handler); 428} 429} 430}); 431}); 432panthera->history_manager = std::make_shared<HistoryManager>("history.db"); 433panthera->history_viewer = Gtk::make_managed<HistoryViewer>(layout_manager, panthera->history_manager); 434panthera->history_viewer->signal_open_url.connect([this, panthera](const std::string &url) { 435panthera->on_new_tab(nullptr, url, true); 436}); 437// Back, forward, reload 438auto back_button = Gtk::make_managed<Gtk::Button>(); 439back_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-previous-symbolic"))); 440back_button->set_tooltip_text("Back"); 441auto forward_button = Gtk::make_managed<Gtk::Button>(); 442forward_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-next-symbolic"))); 443forward_button->set_tooltip_text("Forward"); 444auto reload_button = Gtk::make_managed<Gtk::Button>(); 445reload_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("view-refresh-symbolic"))); 446reload_button->set_tooltip_text("Reload"); 447back_button->signal_clicked().connect([this, panthera]() { 448auto page = panthera->content_manager->get_last_operated_page(); 449if(page) { 450if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) { 451webkit_web_view_go_back(webview); 452} 453} 454}); 455forward_button->signal_clicked().connect([this, panthera]() { 456auto page = panthera->content_manager->get_last_operated_page(); 457if(page) { 458if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) { 459webkit_web_view_go_forward(webview); 460} 461} 462}); 463reload_button->signal_clicked().connect([this, panthera]() { 464auto page = panthera->content_manager->get_last_operated_page(); 465if(page) { 466if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) { 467webkit_web_view_reload(webview); 468} 469} 470}); 471// Search bar 472// TODO: provide history, provide a menu button with the other engines 473auto search_bar = Gtk::make_managed<Gtk::Entry>(); 474search_bar->set_placeholder_text("Search"); 475search_bar->set_hexpand(true); 476if(panthera->search_engines.empty()) { 477search_bar->set_sensitive(false); 478search_bar->set_placeholder_text("No search"); 479} 480auto search_callback = [this, search_bar, content_stack, panthera]() { 481// Create a new tab with the search results 482if(panthera->content_manager->get_last_operated_page()) { 483panthera->on_new_tab(nullptr); 484} else { 485panthera->on_new_tab(content_stack); 486} 487auto page = panthera->content_manager->get_last_operated_page(); 488if(page) { 489if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) { 490auto search_url = panthera->default_search_engine->get_search_url(search_bar->get_text()); 491webkit_web_view_load_uri(webview, search_url.c_str()); 492} 493} 494}; 495if(panthera->default_search_engine) { 496search_bar->signal_activate().connect(search_callback); 497} 498 499// Assemble the toolbar 500main_toolbar->append(*back_button); 501main_toolbar->append(*forward_button); 502main_toolbar->append(*reload_button); 503main_toolbar->append(*url_bar); 504main_toolbar->append(*go_button); 505main_toolbar->append(*search_bar); 506if(panthera->default_search_engine) { 507auto search_button = Gtk::make_managed<Gtk::Button>(panthera->default_search_engine->name); 508search_button->signal_clicked().connect(search_callback); 509main_toolbar->append(*search_button); 510} 511outer_grid->attach(*main_toolbar, 0, 1, 2, 1); 512this->set_child(*outer_grid); 513debug_button->signal_clicked().connect([this, panthera]() { 514if(panthera->content_manager->get_last_operated_page()) { 515std::cout << "Last operated page: " << panthera->content_manager->get_last_operated_page()->get_name() << std::endl; 516} else { 517std::cout << "No page operated!" << std::endl; 518} 519}); 520// TODO: Use the last operated page and allow opening tabs next to the last operated page using certain panes 521// Load the existing layout, if it exists 522std::ifstream layout_file_in("layout.json"); 523if(layout_file_in) { 524std::string layout_json((std::istreambuf_iterator<char>(layout_file_in)), std::istreambuf_iterator<char>()); 525layout_file_in.close(); 526layout_manager->restore_json_layout(layout_json); 527} else { 528// Create a new layout if the file doesn't exist 529layout_file_in.close(); 530 531dock_stack_1->add_pane(*pane_1); 532dock_stack_1->add_pane(*panthera->history_viewer); 533 534std::ofstream layout_file_out("layout.json"); 535layout_file_out << layout_manager->get_layout_as_json(); 536layout_file_out.close(); 537} 538// Save the layout when changed 539layout_manager->signal_pane_moved.connect([this, panthera](gPanthera::DockablePane *pane) { 540std::ofstream layout_file_out("layout.json"); 541layout_file_out << layout_manager->get_layout_as_json(); 542layout_file_out.close(); 543std::cout << "Layout changed: " << layout_manager->get_layout_as_json() << std::endl; 544}); 545set_show_menubar(true); 546} 547}; 548 549void PantheraWww::load_change_callback(WebKitWebView *object, WebKitLoadEvent load_event, gpointer data) { 550if(auto self = static_cast<PantheraWww*>(data)) { 551if(load_event == WEBKIT_LOAD_COMMITTED) { 552if(webkit_web_view_get_title(WEBKIT_WEB_VIEW(object)) == nullptr) { 553self->history_manager->log_url(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(object)), ""); 554} else { 555self->history_manager->log_url(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(object)), 556webkit_web_view_get_title(WEBKIT_WEB_VIEW(object))); 557} 558if(self->history_viewer->get_visible()) { 559self->history_viewer->reload_history(); 560} 561} 562} 563} 564 565void PantheraWww::notify_callback(GObject *object, GParamSpec *pspec, gpointer data) { 566if(!gtk_widget_get_parent(GTK_WIDGET(object))) { 567return; 568} 569if(auto self = static_cast<PantheraWww*>(data)) { 570auto parent = gtk_widget_get_parent(gtk_widget_get_parent(GTK_WIDGET(object))); 571if(auto page = dynamic_cast<gPanthera::ContentPage*>(Glib::wrap(parent))) { 572if(g_strcmp0(pspec->name, "title") == 0) { 573if(auto label = dynamic_cast<Gtk::Label*>(page->tab_widget->get_last_child())) { 574if(strlen(webkit_web_view_get_title(WEBKIT_WEB_VIEW(object))) == 0) { 575label->set_label("Untitled"); 576} else { 577label->set_label(webkit_web_view_get_title(WEBKIT_WEB_VIEW(object))); 578} 579} 580} else if(g_strcmp0(pspec->name, "favicon") == 0) { 581// Update favicons 582if(auto image = dynamic_cast<Gtk::Image*>(page->tab_widget->get_first_child())) { 583image->set_from_icon_name("image-loading-symbolic"); 584if(auto favicon = webkit_web_view_get_favicon(WEBKIT_WEB_VIEW(object))) { 585gtk_image_set_from_paintable(image->gobj(), GDK_PAINTABLE(favicon)); 586} 587} 588} 589} 590} 591} 592 593void PantheraWww::notify_focused_callback(GObject *object, GParamSpec *pspec, gpointer data) { 594if(!gtk_widget_get_parent(GTK_WIDGET(object))) { 595return; 596} 597auto this_ = static_cast<PantheraWww*>(data); 598auto parent = gtk_widget_get_parent(gtk_widget_get_parent(GTK_WIDGET(object))); 599if(auto page = dynamic_cast<gPanthera::ContentPage*>(Glib::wrap(parent))) { 600if(g_strcmp0(pspec->name, "uri") == 0) { 601if(auto main_window = dynamic_cast<PantheraWindow*>(page->get_root())) { 602main_window->url_bar->set_text(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(object))); 603} 604} 605} 606} 607 608void PantheraWww::on_back_pressed(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) { 609WebKitWebView *webview = WEBKIT_WEB_VIEW(user_data); 610webkit_web_view_go_back(webview); 611} 612 613void PantheraWww::on_forward_pressed(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) { 614WebKitWebView *webview = WEBKIT_WEB_VIEW(user_data); 615webkit_web_view_go_forward(webview); 616} 617 618gboolean PantheraWww::on_decide_policy(WebKitWebView *source, WebKitPolicyDecision *decision, WebKitPolicyDecisionType type, gpointer user_data) { 619if(auto self = static_cast<PantheraWww*>(user_data)) { 620if(type == WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION) { 621auto action = webkit_navigation_policy_decision_get_navigation_action(WEBKIT_NAVIGATION_POLICY_DECISION(decision)); 622Glib::ustring url = Glib::ustring(webkit_uri_request_get_uri(webkit_navigation_action_get_request(action))); 623if(!starts_with(url, "about:") && url.find("://") == Glib::ustring::npos) { 624// It is a special scheme (mailto:, tel: etc.) 625try { 626Gio::AppInfo::launch_default_for_uri(url); 627} catch(const Glib::Error &exception) { 628std::cerr << "Failed to launch special URI: " << exception.what() << std::endl; 629} 630webkit_policy_decision_ignore(decision); 631return true; 632} 633if(webkit_navigation_action_get_mouse_button(action) == 2) { 634// Middle-click opens in a new window 635self->on_new_tab(nullptr, url, false); 636webkit_policy_decision_ignore(decision); 637return true; 638} 639} else if(type == WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION) { 640auto action = webkit_navigation_policy_decision_get_navigation_action(WEBKIT_NAVIGATION_POLICY_DECISION(decision)); 641self->on_new_tab(nullptr, webkit_uri_request_get_uri(webkit_navigation_action_get_request(action)), false, true); 642return true; 643} 644} 645return false; 646} 647 648void PantheraWww::on_new_tab(gPanthera::ContentStack *stack, const Glib::ustring &url, bool focus, bool new_window) { 649if(!stack) { 650// Find the current area 651stack = content_manager->get_last_operated_page()->get_stack(); 652} 653Glib::ustring url_ = url; 654if(url.empty()) { 655url_ = new_tab_page; 656} 657 658WebKitWebView *webview = WEBKIT_WEB_VIEW(webkit_web_view_new()); 659gtk_widget_set_hexpand(GTK_WIDGET(webview), true); 660gtk_widget_set_vexpand(GTK_WIDGET(webview), true); 661auto page_content = Gtk::make_managed<Gtk::Box>(); 662gtk_box_append(page_content->gobj(), GTK_WIDGET(webview)); 663auto page_tab = new Gtk::Box(); 664page_tab->set_orientation(Gtk::Orientation::HORIZONTAL); 665auto initial_icon = Gtk::make_managed<Gtk::Image>(Gio::Icon::create("image-loading-symbolic")); 666page_tab->append(*initial_icon); 667page_tab->append(*Gtk::make_managed<Gtk::Label>("Untitled")); 668page_tab->set_spacing(4); 669auto page = Gtk::make_managed<gPanthera::ContentPage>(content_manager, stack, page_content, page_tab); 670g_signal_connect(webview, "notify", G_CALLBACK(notify_callback), this); 671g_signal_connect(webview, "load-changed", G_CALLBACK(load_change_callback), this); 672g_signal_connect(webview, "decide-policy", G_CALLBACK(on_decide_policy), this); 673webkit_web_view_load_uri(webview, url_.data()); 674auto cookie_manager = webkit_network_session_get_cookie_manager(webkit_web_view_get_network_session(webview)); 675webkit_cookie_manager_set_persistent_storage(cookie_manager, cookie_file.c_str(), WEBKIT_COOKIE_PERSISTENT_STORAGE_TEXT); 676webkit_cookie_manager_set_accept_policy(cookie_manager, WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS); 677auto website_data_manager = webkit_network_session_get_website_data_manager(webkit_web_view_get_network_session(webview)); 678webkit_website_data_manager_set_favicons_enabled(website_data_manager, true); 679GtkEventController *click_controller_back = GTK_EVENT_CONTROLLER(gtk_gesture_click_new()); 680gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click_controller_back), 8); 681g_signal_connect(click_controller_back, "pressed", G_CALLBACK(on_back_pressed), webview); 682gtk_widget_add_controller(GTK_WIDGET(webview), click_controller_back); 683 684GtkEventController *click_controller_forward = GTK_EVENT_CONTROLLER(gtk_gesture_click_new()); 685gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click_controller_forward), 9); 686g_signal_connect(click_controller_forward, "pressed", G_CALLBACK(on_forward_pressed), webview); 687gtk_widget_add_controller(GTK_WIDGET(webview), click_controller_forward); 688 689stack->add_page(*page); 690if(focus) { 691stack->set_visible_child(*page); 692content_manager->set_last_operated_page(page); 693} 694if(new_window) { 695bool result = stack->signal_detach.emit(page); 696} 697} 698 699PantheraWindow *PantheraWww::make_window() { 700Glib::RefPtr<Gtk::Application> self_ref = Glib::make_refptr_for_instance<Gtk::Application>(this); 701auto window = new PantheraWindow(self_ref); 702add_window(*window); 703return window; 704} 705 706void PantheraWww::on_startup() { 707Gtk::Application::on_startup(); 708// Get search engines 709std::ifstream search_engines_file_in("search_engines.json"); 710if(search_engines_file_in) { 711std::string search_engines_json((std::istreambuf_iterator<char>(search_engines_file_in)), std::istreambuf_iterator<char>()); 712search_engines_file_in.close(); 713set_search_engines_from_json(search_engines_json); 714} else { 715search_engines_file_in.close(); 716auto empty_json = nlohmann::json::array(); 717std::ofstream search_engines_file_out("search_engines.json"); 718search_engines_file_out << empty_json.dump(4); 719} 720// Load settings 721new_tab_page = "about:blank"; 722std::ifstream settings_file_in("settings.json"); 723if(settings_file_in) { 724std::string settings_json((std::istreambuf_iterator<char>(settings_file_in)), std::istreambuf_iterator<char>()); 725settings_file_in.close(); 726auto json = nlohmann::json::parse(settings_json); 727if(json.contains("ntp")) { 728new_tab_page = json["ntp"].get<std::string>(); 729} 730} else { 731settings_file_in.close(); 732auto empty_json = nlohmann::json::object(); 733empty_json["ntp"] = "about:blank"; 734std::ofstream settings_file_out("settings.json"); 735settings_file_out << empty_json.dump(4); 736settings_file_out.close(); 737} 738 739// Load plugins 740std::ifstream plugin_file_in("plugins.json"); 741if(plugin_file_in) { 742std::string plugin_json((std::istreambuf_iterator<char>(plugin_file_in)), std::istreambuf_iterator<char>()); 743plugin_file_in.close(); 744auto json = nlohmann::json::parse(plugin_json); 745for(auto &plugin : json) { 746auto plugin_path = plugin["path"].get<std::string>(); 747load_plugin(plugin_path); 748} 749} else { 750plugin_file_in.close(); 751auto empty_json = nlohmann::json::array(); 752std::ofstream plugin_file_out("plugins.json"); 753plugin_file_out << empty_json.dump(4); 754plugin_file_out.close(); 755} 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"); 783main_menu->append_submenu("File", file_menu); 784set_menubar(main_menu); 785} 786 787void PantheraWww::on_activate() { 788auto window = make_window(); 789window->present(); 790} 791 792void PantheraWww::set_search_engines_from_json(const std::string &json_string) { 793auto json = nlohmann::json::parse(json_string); 794Glib::ustring default_search_engine_name; 795for(auto &engine : json) { 796SearchEngine search_engine; 797search_engine.name = engine["name"].get<std::string>(); 798search_engine.url = engine["url"].get<std::string>(); 799search_engines.push_back(search_engine); 800if(engine.contains("default") && engine["default"].get<bool>()) { 801default_search_engine_name = engine["name"].get<std::string>(); 802} 803} 804for(auto &search_engine : search_engines) { 805if(search_engine.name == default_search_engine_name) { 806default_search_engine = &search_engine; 807break; 808} 809} 810} 811 812PantheraWww::PantheraWww() : Gtk::Application("com.roundabout_host.roundabout.panthera_www", Gio::Application::Flags::NONE) { 813} 814 815Glib::RefPtr<PantheraWww> PantheraWww::create() { 816return Glib::make_refptr_for_instance<PantheraWww>(new PantheraWww()); 817} 818 819void PantheraWww::load_plugin(const std::string &plugin_path) { 820void *handle = dlopen(plugin_path.c_str(), RTLD_LAZY | RTLD_GLOBAL); 821if(!handle) { 822std::cerr << "Failed to load plugin: " << dlerror() << std::endl; 823return; 824} 825auto entrypoint = (plugin_entrypoint_type)dlsym(handle, "plugin_init"); 826if(!entrypoint) { 827std::cerr << "Failed to find entrypoint in plugin: " << dlerror() << std::endl; 828dlclose(handle); 829return; 830} 831entrypoint(); 832} 833 834Glib::RefPtr<PantheraWww> app = nullptr; 835 836extern "C" { 837void panthera_log(const char *message) { 838std::cerr << message << std::endl; 839} 840 841void panthera_add_pane(const char *id, const char *label, GtkImage *icon, GtkWidget *pane) { 842auto pane_child = Glib::wrap(pane); 843auto icon_cc = Glib::wrap(icon); 844auto label_cc = Glib::ustring(label); 845auto id_cc = Glib::ustring(id); 846//auto new_pane = Gtk::make_managed<gPanthera::DockablePane>(app->get_layout_manager(), *pane_child, id_cc, label_cc, icon_cc); 847// TODO: Port to multi-window 848//app->get_layout_manager()->add_pane(new_pane); 849} 850} 851 852int main(int argc, char *argv[]) { 853gPanthera::init(); 854app = PantheraWww::create(); 855return app->run(argc, argv); 856} 857