panthera-www.cc
C++ source, ASCII text, with very long lines (382)
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#define _(STRING) gettext(STRING) 20 21using plugin_entrypoint_type = void(*)(void); 22 23std::string url_encode(const std::string &value) { 24// Thanks https://stackoverflow.com/a/17708801 25std::ostringstream escaped; 26escaped.fill('0'); 27escaped << std::hex; 28 29for(char c : value) { 30// Keep alphanumeric and other accepted characters intact 31if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') { 32escaped << c; 33continue; 34} 35 36// Any other characters are percent-encoded 37escaped << std::uppercase; 38escaped << '%' << std::setw(2) << int((unsigned char) c); 39escaped << std::nouppercase; 40} 41 42return escaped.str(); 43} 44 45bool starts_with(const Glib::ustring &str, const Glib::ustring &prefix) { 46return str.size() >= prefix.size() && str.compare(0, prefix.size(), prefix) == 0; 47} 48 49bool ends_with(const Glib::ustring &str, const Glib::ustring &suffix) { 50return str.size() >= suffix.size() && str.compare(str.size() - suffix.size(), suffix.size(), suffix) == 0; 51} 52 53struct SearchEngine { 54Glib::ustring name; 55Glib::ustring url; 56Glib::ustring get_search_url(const std::string &query) { 57auto pos = url.find("%s"); 58if(pos == Glib::ustring::npos) { 59throw std::runtime_error("Invalid search engine URL: missing '%s' placeholder"); 60} 61auto new_url = url; 62new_url.replace(pos, 2, url_encode(query)); 63return new_url; 64} 65}; 66 67struct HistoryEntry { 68int id; 69std::string url, title; 70int64_t timestamp; 71}; 72 73using StorageType = decltype(sqlite_orm::make_storage("", 74sqlite_orm::make_table("history", 75sqlite_orm::make_column("id", &HistoryEntry::id, sqlite_orm::primary_key()), 76sqlite_orm::make_column("url", &HistoryEntry::url), 77sqlite_orm::make_column("title", &HistoryEntry::title), 78sqlite_orm::make_column("timestamp", &HistoryEntry::timestamp) 79) 80)); 81 82class HistoryManager { 83public: 84static auto make_storage(const std::string& path) { 85return sqlite_orm::make_storage(path, 86sqlite_orm::make_table("history", 87sqlite_orm::make_column("id", &HistoryEntry::id, sqlite_orm::primary_key()), 88sqlite_orm::make_column("url", &HistoryEntry::url), 89sqlite_orm::make_column("title", &HistoryEntry::title), 90sqlite_orm::make_column("timestamp", &HistoryEntry::timestamp) 91) 92); 93} 94StorageType storage; 95explicit HistoryManager(const std::string &db_path) : storage(make_storage(db_path)) { 96storage.sync_schema(); 97} 98 99void log_url(const std::string &url, const std::string title, const int64_t timestamp = Glib::DateTime::create_now_local().to_unix()) { 100storage.insert(HistoryEntry{-1, url, title, timestamp}); 101} 102 103std::vector<HistoryEntry> get_history() { 104return storage.get_all<HistoryEntry>(); 105} 106 107std::vector<HistoryEntry> get_history_between(int64_t start_time, int64_t end_time) { 108using namespace sqlite_orm; 109return storage.get_all<HistoryEntry>( 110where(c(&HistoryEntry::timestamp) >= start_time and c(&HistoryEntry::timestamp) < end_time), 111order_by(&HistoryEntry::timestamp).desc() 112); 113} 114}; 115 116std::vector<WebKitWebView*> collect_webviews(Gtk::Widget const &root) { 117std::vector<WebKitWebView*> result; 118std::function<void(const Gtk::Widget&)> recurse = [&](const Gtk::Widget &widget) { 119gpointer gobj = G_OBJECT(widget.gobj()); 120if(WEBKIT_IS_WEB_VIEW(gobj)) { 121result.push_back(WEBKIT_WEB_VIEW(gobj)); 122} else for(auto *child = widget.get_first_child(); child; child = child->get_next_sibling()) { 123recurse(*child); 124} 125}; 126recurse(root); 127return result; 128} 129 130bool has_webview(Gtk::Widget const &root) { 131if(WEBKIT_IS_WEB_VIEW(G_OBJECT(root.gobj()))) { 132return true; 133} 134for(auto *child = root.get_first_child(); child; child = child->get_next_sibling()) { 135if(has_webview(*child)) { 136return true; 137} 138} 139return false; 140} 141 142class HistoryViewer : public gPanthera::DockablePane { 143protected: 144std::shared_ptr<HistoryManager> history_manager; 145Gtk::Calendar *calendar; 146Gtk::Label *date_label; 147Gtk::Box *box_place; 148public: 149sigc::signal<void(const std::string &url)> signal_open_url; 150HistoryViewer(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)) { 151auto history_toolbar = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 0); 152auto history_button_previous = Gtk::make_managed<Gtk::Button>(); 153history_button_previous->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-previous-symbolic"))); 154history_button_previous->set_tooltip_text(_("Previous day")); 155auto history_button_next = Gtk::make_managed<Gtk::Button>(); 156history_button_next->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-next-symbolic"))); 157history_button_next->set_tooltip_text(_("Next day")); 158 159date_label = Gtk::make_managed<Gtk::Label>(""); 160auto date_button = Gtk::make_managed<Gtk::MenuButton>(); 161date_button->set_child(*date_label); 162date_button->set_tooltip_text(_("Select date")); 163date_button->set_direction(Gtk::ArrowType::NONE); 164auto date_popover = Gtk::make_managed<Gtk::Popover>(); 165calendar = Gtk::make_managed<Gtk::Calendar>(); 166date_popover->set_child(*calendar); 167date_button->set_popover(*date_popover); 168date_button->set_has_frame(false); 169date_button->set_hexpand(true); 170 171history_toolbar->append(*history_button_previous); 172history_toolbar->append(*date_button); 173history_toolbar->append(*history_button_next); 174auto box = dynamic_cast<Gtk::Box*>(this->child); 175box->append(*history_toolbar); 176 177history_button_next->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 next_entries = this->history_manager->storage.get_all<HistoryEntry>( 186sqlite_orm::where(sqlite_orm::c(&HistoryEntry::timestamp) >= datetime.to_unix() + 86400), 187sqlite_orm::order_by(&HistoryEntry::timestamp).asc() 188); 189if(next_entries.empty()) { 190return; 191} 192auto last_entry = next_entries.front(); 193Glib::DateTime next_day = Glib::DateTime::create_now_local(last_entry.timestamp); 194Glib::DateTime new_date = Glib::DateTime::create_local( 195next_day.get_year(), 196next_day.get_month(), 197next_day.get_day_of_month(), 1980, 0, 0 199); 200calendar->select_day(new_date); 201reload_history(); 202}); 203 204history_button_previous->signal_clicked().connect([this]() { 205Glib::DateTime calendar_day = calendar->get_date(); 206Glib::DateTime datetime = Glib::DateTime::create_local( 207calendar_day.get_year(), 208calendar_day.get_month(), 209calendar_day.get_day_of_month(), 2100, 0, 0 211); 212auto previous_entries = this->history_manager->storage.get_all<HistoryEntry>( 213sqlite_orm::where(sqlite_orm::c(&HistoryEntry::timestamp) < datetime.to_unix()), 214sqlite_orm::order_by(&HistoryEntry::timestamp).desc() 215); 216if(previous_entries.empty()) { 217return; 218} 219auto last_entry = previous_entries.front(); 220Glib::DateTime previous_day = Glib::DateTime::create_now_local(last_entry.timestamp); 221Glib::DateTime new_date = Glib::DateTime::create_local( 222previous_day.get_year(), 223previous_day.get_month(), 224previous_day.get_day_of_month(), 2250, 0, 0 226); 227calendar->select_day(new_date); 228reload_history(); 229}); 230 231calendar->signal_day_selected().connect(sigc::mem_fun(*this, &HistoryViewer::reload_history)); 232calendar->signal_prev_month().connect(sigc::mem_fun(*this, &HistoryViewer::reload_history)); 233calendar->signal_next_month().connect(sigc::mem_fun(*this, &HistoryViewer::reload_history)); 234calendar->signal_prev_year().connect(sigc::mem_fun(*this, &HistoryViewer::reload_history)); 235calendar->signal_next_year().connect(sigc::mem_fun(*this, &HistoryViewer::reload_history)); 236Glib::DateTime datetime = Glib::DateTime::create_now_local(); 237calendar->select_day(datetime); 238 239this->signal_pane_shown.connect([this]() { 240// Load the current date 241reload_history(); 242}); 243 244auto scrolled_window = Gtk::make_managed<Gtk::ScrolledWindow>(); 245box_place = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0); 246auto viewport = Gtk::make_managed<Gtk::Viewport>(nullptr, nullptr); 247viewport->set_child(*box_place); 248scrolled_window->set_child(*viewport); 249scrolled_window->set_policy(Gtk::PolicyType::AUTOMATIC, Gtk::PolicyType::AUTOMATIC); 250scrolled_window->set_vexpand(true); 251scrolled_window->set_hexpand(true); 252box->append(*scrolled_window); 253 254// TODO: allow deleting entries 255} 256 257void reload_history() { 258Glib::DateTime calendar_day = calendar->get_date(); 259Glib::DateTime datetime = Glib::DateTime::create_local( 260calendar_day.get_year(), 261calendar_day.get_month(), 262calendar_day.get_day_of_month(), 2630, 0, 0 264); 265date_label->set_text(datetime.format("%x")); 266auto box = Gtk::make_managed<Gtk::ListBox>(); 267auto history_entries = history_manager->get_history_between(datetime.to_unix(), datetime.to_unix() + 86400); 268for(const auto &entry : history_entries) { 269auto row = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 4); 270auto title = Gtk::make_managed<Gtk::Label>(entry.title.empty() ? "Untitled" : entry.title); 271title->set_halign(Gtk::Align::START); 272title->set_ellipsize(Pango::EllipsizeMode::MIDDLE); 273auto label = Gtk::make_managed<Gtk::Label>(entry.url); 274label->set_halign(Gtk::Align::START); 275label->set_ellipsize(Pango::EllipsizeMode::MIDDLE); 276row->append(*title); 277row->append(*label); 278if(auto time = Glib::DateTime::create_now_local(entry.timestamp)) { 279label->set_tooltip_text(time.format("%c")); 280box->append(*row); 281} 282} 283if(box_place->get_first_child()) { 284box_place->remove(*box_place->get_first_child()); 285} 286box->signal_row_activated().connect([this](Gtk::ListBoxRow *row) { 287if(auto box_row = dynamic_cast<Gtk::Box*>(row->get_child())) { 288if(auto label = dynamic_cast<Gtk::Label*>(box_row->get_last_child())) { 289auto url = label->get_text(); 290if(!url.empty()) { 291signal_open_url.emit(url); 292} 293} 294} 295}); 296box_place->append(*box); 297} 298}; 299 300class PantheraWindow; 301 302class PantheraWww : public Gtk::Application { 303private: 304std::shared_ptr<gPanthera::ContentManager> content_manager; 305std::string cookie_file; 306std::vector<SearchEngine> search_engines; 307SearchEngine *default_search_engine; 308std::shared_ptr<HistoryManager> history_manager; 309Glib::RefPtr<Gio::Menu> main_menu; 310std::string new_tab_page; 311gPanthera::ContentStack *controlled_stack = nullptr; 312 313static void load_change_callback(WebKitWebView* object, WebKitLoadEvent load_event, gpointer data); 314static void notify_callback(GObject* object, GParamSpec* pspec, gpointer data); 315static void notify_focused_callback(GObject* object, GParamSpec* pspec, gpointer data); 316static void on_back_pressed(GtkGestureClick* gesture, int n_press, double x, double y, gpointer user_data); 317static void on_forward_pressed(GtkGestureClick* gesture, int n_press, double x, double y, gpointer user_data); 318static gboolean on_decide_policy(WebKitWebView* source, WebKitPolicyDecision* decision, WebKitPolicyDecisionType type, gpointer user_data); 319static void close_callback(WebKitWebView *source, gpointer user_data); 320 321protected: 322void on_startup() override; 323void on_activate() override; 324void on_shutdown() override; 325PantheraWindow *make_window(); 326 327public: 328PantheraWww(); 329~PantheraWww(); 330friend class PantheraWindow; 331void on_new_tab(gPanthera::ContentStack* stack, const Glib::ustring& url = "", bool focus = true, bool new_window = false); 332void set_search_engines_from_json(const std::string& json_string); 333static Glib::RefPtr<PantheraWww> create(); 334std::shared_ptr<gPanthera::ContentManager> get_content_manager(); 335void load_plugin(const std::string& plugin_path); 336}; 337 338class PantheraWindow : public Gtk::ApplicationWindow { 339private: 340std::shared_ptr<gPanthera::LayoutManager> layout_manager; 341Gtk::Entry *url_bar; 342HistoryViewer *history_viewer; 343gPanthera::ContentPage *controlled_page = nullptr; 344gPanthera::ContentStack *controlled_stack = nullptr; 345Glib::RefPtr<Gio::SimpleAction> back_action, forward_action, reload_action; 346Gtk::Button *go_button; 347 348void enable_controls() { 349back_action->set_enabled(true); 350forward_action->set_enabled(true); 351reload_action->set_enabled(true); 352url_bar->set_sensitive(true); 353go_button->set_sensitive(true); 354if(controlled_page) { 355g_object_notify(G_OBJECT(controlled_page->get_child()->get_first_child()->gobj()), "uri"); 356} 357} 358 359void disable_controls() { 360back_action->set_enabled(false); 361forward_action->set_enabled(false); 362reload_action->set_enabled(false); 363url_bar->set_sensitive(false); 364url_bar->set_text(""); 365go_button->set_sensitive(false); 366} 367 368sigc::connection moving_connection, moved_connection, closing_connection, closed_connection; 369public: 370friend class PantheraWww; 371 372explicit PantheraWindow(Gtk::Application *application) : Gtk::ApplicationWindow() { 373// There is a constructor with Glib::RefPtr<Gtk::Application>, but it is not appropriate 374// because the window will own the application, creating a cycle 375auto panthera = dynamic_cast<PantheraWww*>(application); 376if(!panthera) { 377throw std::runtime_error("Application is not a PantheraWww instance"); 378} 379panthera->add_window(*this); 380this->set_default_size(800, 600); 381layout_manager = std::make_shared<gPanthera::LayoutManager>(); 382 383auto dock_stack_1 = Gtk::make_managed<gPanthera::DockStack>(layout_manager, _("One"), "one"); 384auto switcher_1 = Gtk::make_managed<gPanthera::DockStackSwitcher>(dock_stack_1, Gtk::Orientation::HORIZONTAL); 385auto dock_stack_2 = Gtk::make_managed<gPanthera::DockStack>(layout_manager, _("Two"), "two"); 386auto switcher_2 = Gtk::make_managed<gPanthera::DockStackSwitcher>(dock_stack_2, Gtk::Orientation::VERTICAL); 387auto pane_1_content = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0); 388auto debug_button = Gtk::make_managed<Gtk::Button>("Debug"); 389auto print_history_button = Gtk::make_managed<Gtk::Button>("Print history"); 390print_history_button->signal_clicked().connect([this, panthera]() { 391auto history = panthera->history_manager->get_history(); 392for(const auto &entry : history) { 393std::cout << "entry " << entry.id << ", url " << entry.url << ", unix time " << entry.timestamp << std::endl; 394// TODO: exclude redirecting URLs 395} 396}); 397pane_1_content->append(*debug_button); 398pane_1_content->append(*print_history_button); 399auto pane_1_icon = Gtk::make_managed<Gtk::Image>(Gio::Icon::create("help-about-symbolic")); 400auto pane_1 = Gtk::make_managed<gPanthera::DockablePane>(layout_manager, *pane_1_content, "debug", _("Debugging options"), pane_1_icon); 401 402dock_stack_1->set_transition_type(Gtk::StackTransitionType::SLIDE_LEFT_RIGHT); 403dock_stack_1->set_transition_duration(125); 404dock_stack_1->set_expand(true); 405dock_stack_2->set_transition_type(Gtk::StackTransitionType::SLIDE_UP_DOWN); 406dock_stack_2->set_transition_duration(125); 407dock_stack_2->set_expand(true); 408 409auto outer_grid = Gtk::make_managed<Gtk::Grid>(); 410outer_grid->attach(*switcher_2, 0, 2, 1, 1); 411outer_grid->attach(*switcher_1, 1, 3, 1, 1); 412auto outer_paned = Gtk::make_managed<Gtk::Paned>(Gtk::Orientation::HORIZONTAL); 413outer_paned->set_start_child(*dock_stack_2); 414auto inner_paned = Gtk::make_managed<Gtk::Paned>(Gtk::Orientation::VERTICAL); 415auto content = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0); 416std::function<bool(gPanthera::ContentPage*)> detach_handler; 417 418detach_handler = [this](gPanthera::ContentPage *widget) { 419auto new_stack = Gtk::make_managed<gPanthera::ContentStack>(widget->content_manager, widget->get_stack()->get_detach_handler()); 420auto 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()); 421auto new_notebook = Gtk::make_managed<gPanthera::ContentNotebook>(new_stack, new_switcher); 422auto window = new gPanthera::ContentWindow(new_notebook); 423widget->redock(new_stack); 424window->present(); 425window->signal_close_request().connect([window]() { 426auto webviews = collect_webviews(*window); 427for(auto view : webviews) { 428webkit_web_view_try_close(view); 429} 430if(!has_webview(*window)) { 431// All documents have been closed safely, the window can be closed 432return false; 433} 434return true; 435}, false); 436controlled_page = nullptr; 437controlled_stack = nullptr; 438disable_controls(); 439return true; 440}; 441 442auto return_extra_child = [this, panthera](gPanthera::ContentTabBar *switcher) { 443auto new_tab_button = Gtk::make_managed<Gtk::Button>(); 444new_tab_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("list-add-symbolic"))); 445new_tab_button->set_tooltip_text(_("New Tab")); 446new_tab_button->signal_clicked().connect([this, switcher, panthera]() { 447panthera->on_new_tab(switcher->get_stack()); 448}); 449return new_tab_button; 450}; 451controlled_stack = Gtk::make_managed<gPanthera::ContentStack>(panthera->content_manager, detach_handler); 452panthera->controlled_stack = controlled_stack; 453auto content_stack_switcher = Gtk::make_managed<gPanthera::ContentTabBar>(controlled_stack, Gtk::Orientation::HORIZONTAL, return_extra_child); 454panthera->content_manager->add_stack(controlled_stack); 455WebKitWebView *webview = WEBKIT_WEB_VIEW(webkit_web_view_new()); // for some reason, this has to be created 456content->set_name("content_box"); 457auto content_notebook = Gtk::make_managed<gPanthera::ContentNotebook>(controlled_stack, content_stack_switcher, Gtk::PositionType::TOP); 458content->append(*content_notebook); 459inner_paned->set_start_child(*content); 460inner_paned->set_end_child(*dock_stack_1); 461outer_paned->set_end_child(*inner_paned); 462outer_grid->attach(*outer_paned, 1, 2, 1, 1); 463// Create the toolbar 464auto main_toolbar = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 8); 465// URL bar 466url_bar = Gtk::make_managed<Gtk::Entry>(); 467url_bar->set_placeholder_text(_("Enter URL")); 468url_bar->set_hexpand(true); 469auto load_url_callback = [this, panthera]() { 470bool has_protocol = url_bar->get_text().find("://") != std::string::npos; 471if(!has_protocol) { 472url_bar->set_text("http://" + url_bar->get_text()); 473} 474if(controlled_page) { 475if(auto webview = WEBKIT_WEB_VIEW(controlled_page->get_child()->get_first_child()->gobj())) { 476webkit_web_view_load_uri(webview, url_bar->get_text().c_str()); 477} 478} 479}; 480// Go 481go_button = Gtk::make_managed<Gtk::Button>(_("Go")); 482go_button->signal_clicked().connect(load_url_callback); 483url_bar->signal_activate().connect(load_url_callback); 484panthera->content_manager->signal_page_operated.connect([this, panthera](gPanthera::ContentPage *page) { 485if(!page) { 486return; 487} 488if(!page->get_child()) { 489return; 490} 491if(!page->get_child()->get_first_child()) { 492return; 493} 494if(page->get_root() != this) { 495// The page is in some other window 496return; 497} 498controlled_page = page; 499controlled_stack = page->get_stack(); 500panthera->controlled_stack = page->get_stack(); 501if(WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) { 502if(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj()))) { 503url_bar->set_text(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj()))); 504} 505guint url_update_handler = g_signal_connect(page->get_child()->get_first_child()->gobj(), "notify", G_CALLBACK(panthera->notify_focused_callback), this); 506std::shared_ptr<sigc::connection> control_signal_handler = std::make_shared<sigc::connection>(); 507*control_signal_handler = page->signal_control_status_changed.connect([this, page, control_signal_handler, url_update_handler](bool controlled) { 508if(!controlled) { 509control_signal_handler->disconnect(); 510if(page->get_child() && page->get_child()->get_first_child() && WEBKIT_IS_WEB_VIEW(page->get_child()->get_first_child()->gobj())) { 511g_signal_handler_disconnect(page->get_child()->get_first_child()->gobj(), url_update_handler); 512} 513} 514}); 515} 516 517enable_controls(); 518}); 519history_viewer = Gtk::make_managed<HistoryViewer>(layout_manager, panthera->history_manager); 520history_viewer->signal_open_url.connect([this, panthera](const std::string &url) { 521panthera->on_new_tab(nullptr, url, true); 522}); 523// Back, forward, reload 524auto back_button = Gtk::make_managed<Gtk::Button>(); 525back_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-previous-symbolic"))); 526back_button->set_tooltip_text(_("Back")); 527auto forward_button = Gtk::make_managed<Gtk::Button>(); 528forward_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-next-symbolic"))); 529forward_button->set_tooltip_text(_("Forward")); 530auto reload_button = Gtk::make_managed<Gtk::Button>(); 531reload_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("view-refresh-symbolic"))); 532reload_button->set_tooltip_text(_("Reload")); 533 534back_action = Gio::SimpleAction::create("go_back"); 535add_action(back_action); 536back_action->signal_activate().connect([this, panthera](const Glib::VariantBase&) { 537if(controlled_page) { 538if(auto webview = WEBKIT_WEB_VIEW(controlled_page->get_child()->get_first_child()->gobj())) { 539webkit_web_view_go_back(webview); 540} 541} 542}); 543back_button->set_action_name("win.go_back"); 544 545forward_action = Gio::SimpleAction::create("go_forward"); 546add_action(forward_action); 547forward_action->signal_activate().connect([this, panthera](const Glib::VariantBase&) { 548if(controlled_page) { 549if(auto webview = WEBKIT_WEB_VIEW(controlled_page->get_child()->get_first_child()->gobj())) { 550webkit_web_view_go_forward(webview); 551} 552} 553}); 554forward_button->set_action_name("win.go_forward"); 555 556reload_action = Gio::SimpleAction::create("reload"); 557add_action(reload_action); 558reload_action->signal_activate().connect([this, panthera](const Glib::VariantBase&) { 559if(controlled_page) { 560if(auto webview = WEBKIT_WEB_VIEW(controlled_page->get_child()->get_first_child()->gobj())) { 561webkit_web_view_reload(webview); 562} 563} 564}); 565reload_button->set_action_name("win.reload"); 566 567// Search bar 568// TODO: provide history, provide a menu button with the other engines 569auto search_bar = Gtk::make_managed<Gtk::Entry>(); 570search_bar->set_placeholder_text(_("Search")); 571search_bar->set_hexpand(true); 572if(panthera->search_engines.empty()) { 573search_bar->set_sensitive(false); 574search_bar->set_placeholder_text(_("No search")); 575} 576auto search_callback = [this, search_bar, panthera]() { 577// Create a new tab with the search results 578panthera->on_new_tab(controlled_stack); 579auto page = panthera->content_manager->get_last_operated_page(); 580if(page) { 581if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) { 582auto search_url = panthera->default_search_engine->get_search_url(search_bar->get_text()); 583webkit_web_view_load_uri(webview, search_url.c_str()); 584} 585} 586}; 587if(panthera->default_search_engine) { 588search_bar->signal_activate().connect(search_callback); 589} 590 591// Assemble the toolbar 592main_toolbar->append(*back_button); 593main_toolbar->append(*forward_button); 594main_toolbar->append(*reload_button); 595main_toolbar->append(*url_bar); 596main_toolbar->append(*go_button); 597main_toolbar->append(*search_bar); 598if(panthera->default_search_engine) { 599auto search_button = Gtk::make_managed<Gtk::Button>(panthera->default_search_engine->name); 600search_button->signal_clicked().connect(search_callback); 601main_toolbar->append(*search_button); 602} 603outer_grid->attach(*main_toolbar, 0, 1, 2, 1); 604this->set_child(*outer_grid); 605debug_button->signal_clicked().connect([this, panthera]() { 606if(panthera->content_manager->get_last_operated_page()) { 607std::cout << "Last operated page: " << panthera->content_manager->get_last_operated_page()->get_name() << std::endl; 608} else { 609std::cout << "No page operated!" << std::endl; 610} 611}); 612// TODO: Use the last operated page and allow opening tabs next to the last operated page using certain panes 613// Load the existing layout, if it exists 614std::ifstream layout_file_in("layout.json"); 615if(layout_file_in) { 616std::string layout_json((std::istreambuf_iterator<char>(layout_file_in)), std::istreambuf_iterator<char>()); 617layout_file_in.close(); 618layout_manager->restore_json_layout(layout_json); 619} else { 620// Create a new layout if the file doesn't exist 621layout_file_in.close(); 622 623dock_stack_1->add_pane(*pane_1); 624dock_stack_1->add_pane(*history_viewer); 625 626std::ofstream layout_file_out("layout.json"); 627layout_file_out << layout_manager->get_layout_as_json(); 628layout_file_out.close(); 629} 630// Save the layout when changed 631layout_manager->signal_pane_moved.connect([this, panthera](gPanthera::DockablePane *pane) { 632std::ofstream layout_file_out("layout.json"); 633layout_file_out << layout_manager->get_layout_as_json(); 634layout_file_out.close(); 635std::cout << "Layout changed: " << layout_manager->get_layout_as_json() << std::endl; 636}); 637set_show_menubar(true); 638 639// As the window is initially empty, disable most actions 640disable_controls(); 641 642moving_connection = panthera->content_manager->signal_page_moving.connect([this, panthera](gPanthera::ContentPage *page) { 643if(page->get_root() == this) { 644controlled_page = nullptr; 645controlled_stack = nullptr; 646disable_controls(); 647} 648}); 649moved_connection = panthera->content_manager->signal_page_moved.connect([this, panthera](gPanthera::ContentPage *page) { 650if(page->get_root() == this) { 651panthera->content_manager->set_last_operated_page(page); 652page->get_stack()->set_visible_child(*page); 653enable_controls(); 654} 655}); 656 657signal_close_request().connect([this]() { 658auto webviews = collect_webviews(*this); 659for(auto view : webviews) { 660webkit_web_view_try_close(view); 661} 662if(!has_webview(*this)) { 663// All documents have been closed safely, the window can be closed 664return false; 665} 666return true; 667}, false); 668 669closing_connection = panthera->content_manager->signal_page_closing.connect([this](gPanthera::ContentPage *page) { 670if(page->get_root() != this) { 671return; 672} 673disable_controls(); 674}); 675closed_connection = panthera->content_manager->signal_page_closed.connect([this](gPanthera::ContentPage *page) { 676if(has_webview(*this)) { 677enable_controls(); 678} 679}); 680} 681~PantheraWindow() override { 682moving_connection.disconnect(); 683moved_connection.disconnect(); 684closed_connection.disconnect(); 685closing_connection.disconnect(); 686} 687}; 688 689void PantheraWww::load_change_callback(WebKitWebView *object, WebKitLoadEvent load_event, gpointer data) { 690if(auto self = static_cast<PantheraWww*>(data)) { 691if(load_event == WEBKIT_LOAD_COMMITTED) { 692if(webkit_web_view_get_title(WEBKIT_WEB_VIEW(object)) == nullptr) { 693self->history_manager->log_url(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(object)), ""); 694} else { 695self->history_manager->log_url(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(object)), 696webkit_web_view_get_title(WEBKIT_WEB_VIEW(object))); 697} 698// TODO: reload visible history viewers 699} 700} 701} 702 703void PantheraWww::notify_callback(GObject *object, GParamSpec *pspec, gpointer data) { 704if(!gtk_widget_get_parent(GTK_WIDGET(object))) { 705return; 706} 707if(auto self = static_cast<PantheraWww*>(data)) { 708auto parent = gtk_widget_get_parent(gtk_widget_get_parent(GTK_WIDGET(object))); 709if(auto page = dynamic_cast<gPanthera::ContentPage*>(Glib::wrap(parent))) { 710if(g_strcmp0(pspec->name, "title") == 0) { 711if(auto label = dynamic_cast<Gtk::Label*>(page->tab_widget->get_last_child())) { 712if(strlen(webkit_web_view_get_title(WEBKIT_WEB_VIEW(object))) == 0) { 713label->set_label(_("Untitled")); 714} else { 715label->set_label(webkit_web_view_get_title(WEBKIT_WEB_VIEW(object))); 716} 717} 718} else if(g_strcmp0(pspec->name, "favicon") == 0) { 719// Update favicons 720if(auto image = dynamic_cast<Gtk::Image*>(page->tab_widget->get_first_child())) { 721image->set_from_icon_name("image-loading-symbolic"); 722if(auto favicon = webkit_web_view_get_favicon(WEBKIT_WEB_VIEW(object))) { 723gtk_image_set_from_paintable(image->gobj(), GDK_PAINTABLE(favicon)); 724} 725} 726} 727} 728} 729} 730 731void PantheraWww::notify_focused_callback(GObject *object, GParamSpec *pspec, gpointer data) { 732if(!gtk_widget_get_parent(GTK_WIDGET(object))) { 733return; 734} 735auto this_ = static_cast<PantheraWww*>(data); 736auto parent = gtk_widget_get_parent(gtk_widget_get_parent(GTK_WIDGET(object))); 737if(auto page = dynamic_cast<gPanthera::ContentPage*>(Glib::wrap(parent))) { 738if(g_strcmp0(pspec->name, "uri") == 0) { 739if(auto main_window = dynamic_cast<PantheraWindow*>(page->get_root())) { 740if(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(object))) { 741main_window->url_bar->set_text(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(object))); 742} 743} 744} else if(g_strcmp0(pspec->name, "title") == 0) { 745if(auto window = dynamic_cast<Gtk::Window*>(page->get_root())) { 746if(webkit_web_view_get_title(WEBKIT_WEB_VIEW(object))) { 747window->set_title(webkit_web_view_get_title(WEBKIT_WEB_VIEW(object))); 748} else { 749window->set_title(_("Untitled")); 750} 751} 752} 753} 754} 755 756void PantheraWww::on_back_pressed(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) { 757WebKitWebView *webview = WEBKIT_WEB_VIEW(user_data); 758webkit_web_view_go_back(webview); 759} 760 761void PantheraWww::on_forward_pressed(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) { 762WebKitWebView *webview = WEBKIT_WEB_VIEW(user_data); 763webkit_web_view_go_forward(webview); 764} 765 766gboolean PantheraWww::on_decide_policy(WebKitWebView *source, WebKitPolicyDecision *decision, WebKitPolicyDecisionType type, gpointer user_data) { 767if(auto self = static_cast<PantheraWww*>(user_data)) { 768if(type == WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION) { 769auto action = webkit_navigation_policy_decision_get_navigation_action(WEBKIT_NAVIGATION_POLICY_DECISION(decision)); 770Glib::ustring url = Glib::ustring(webkit_uri_request_get_uri(webkit_navigation_action_get_request(action))); 771if(!starts_with(url, "about:") && url.find("://") == Glib::ustring::npos) { 772// It is a special scheme (mailto:, tel: etc.) 773try { 774Gio::AppInfo::launch_default_for_uri(url); 775} catch(const Glib::Error &exception) { 776std::cerr << "Failed to launch special URI: " << exception.what() << std::endl; 777} 778webkit_policy_decision_ignore(decision); 779return true; 780} 781if(webkit_navigation_action_get_mouse_button(action) == 2) { 782// Middle-click opens in a new window 783self->on_new_tab(nullptr, url, false); 784webkit_policy_decision_ignore(decision); 785return true; 786} 787} else if(type == WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION) { 788auto action = webkit_navigation_policy_decision_get_navigation_action(WEBKIT_NAVIGATION_POLICY_DECISION(decision)); 789self->on_new_tab(nullptr, webkit_uri_request_get_uri(webkit_navigation_action_get_request(action)), false, true); 790return true; 791} 792} 793return false; 794} 795 796void PantheraWww::close_callback(WebKitWebView *source, gpointer user_data) { 797if(auto self = static_cast<PantheraWww*>(user_data)) { 798auto parent = gtk_widget_get_parent(gtk_widget_get_parent(GTK_WIDGET(source))); 799if(auto page = dynamic_cast<gPanthera::ContentPage*>(Glib::wrap(parent))) { 800if(page->get_next_sibling()) { 801page->content_manager->set_last_operated_page(static_cast<gPanthera::ContentPage*>(page->get_next_sibling())); 802page->get_stack()->set_visible_child(*page->get_next_sibling()); 803} else if(page->get_prev_sibling()) { 804page->content_manager->set_last_operated_page(static_cast<gPanthera::ContentPage*>(page->get_prev_sibling())); 805page->get_stack()->set_visible_child(*page->get_prev_sibling()); 806} 807webkit_web_view_terminate_web_process(source); 808if(page->content_manager->get_last_operated_page() == page) { 809page->content_manager->set_last_operated_page(nullptr); 810} 811page->redock(nullptr); 812self->content_manager->signal_page_closed.emit(page); 813} 814} 815} 816 817void PantheraWww::on_new_tab(gPanthera::ContentStack *stack, const Glib::ustring &url, bool focus, bool new_window) { 818if(!stack) { 819// Find the current area 820stack = controlled_stack; 821} 822Glib::ustring url_ = url; 823if(url.empty()) { 824url_ = new_tab_page; 825} 826 827WebKitWebView *webview = WEBKIT_WEB_VIEW(webkit_web_view_new()); 828gtk_widget_set_hexpand(GTK_WIDGET(webview), true); 829gtk_widget_set_vexpand(GTK_WIDGET(webview), true); 830auto page_content = Gtk::make_managed<Gtk::Box>(); 831gtk_box_append(page_content->gobj(), GTK_WIDGET(webview)); 832auto page_tab = new Gtk::Box(); 833page_tab->set_orientation(Gtk::Orientation::HORIZONTAL); 834auto initial_icon = Gtk::make_managed<Gtk::Image>(Gio::Icon::create("image-loading-symbolic")); 835page_tab->append(*initial_icon); 836page_tab->append(*Gtk::make_managed<Gtk::Label>(_("Untitled"))); 837page_tab->set_spacing(4); 838auto page = Gtk::make_managed<gPanthera::ContentPage>(content_manager, stack, page_content, page_tab); 839page->signal_close.connect([webview]() { 840webkit_web_view_try_close(WEBKIT_WEB_VIEW(webview)); 841return true; 842}); 843g_signal_connect(webview, "close", G_CALLBACK(close_callback), this); 844g_signal_connect(webview, "notify", G_CALLBACK(notify_callback), this); 845g_signal_connect(webview, "load-changed", G_CALLBACK(load_change_callback), this); 846g_signal_connect(webview, "decide-policy", G_CALLBACK(on_decide_policy), this); 847webkit_web_view_load_uri(webview, url_.data()); 848auto cookie_manager = webkit_network_session_get_cookie_manager(webkit_web_view_get_network_session(webview)); 849webkit_cookie_manager_set_persistent_storage(cookie_manager, cookie_file.c_str(), WEBKIT_COOKIE_PERSISTENT_STORAGE_TEXT); 850webkit_cookie_manager_set_accept_policy(cookie_manager, WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS); 851auto website_data_manager = webkit_network_session_get_website_data_manager(webkit_web_view_get_network_session(webview)); 852webkit_website_data_manager_set_favicons_enabled(website_data_manager, true); 853GtkEventController *click_controller_back = GTK_EVENT_CONTROLLER(gtk_gesture_click_new()); 854gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click_controller_back), 8); 855g_signal_connect(click_controller_back, "pressed", G_CALLBACK(on_back_pressed), webview); 856gtk_widget_add_controller(GTK_WIDGET(webview), click_controller_back); 857 858GtkEventController *click_controller_forward = GTK_EVENT_CONTROLLER(gtk_gesture_click_new()); 859gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click_controller_forward), 9); 860g_signal_connect(click_controller_forward, "pressed", G_CALLBACK(on_forward_pressed), webview); 861gtk_widget_add_controller(GTK_WIDGET(webview), click_controller_forward); 862 863stack->add_page(*page); 864if(focus) { 865stack->set_visible_child(*page); 866content_manager->set_last_operated_page(page); 867} 868if(new_window) { 869bool result = stack->signal_detach.emit(page); 870} 871} 872 873PantheraWindow *PantheraWww::make_window() { 874auto window = Gtk::make_managed<PantheraWindow>(this); 875return window; 876} 877 878void PantheraWww::on_startup() { 879bindtextdomain("panthera-www", "./locales"); 880textdomain("panthera-www"); 881content_manager = std::make_shared<gPanthera::ContentManager>(); 882Gtk::Application::on_startup(); 883// Get search engines 884std::ifstream search_engines_file_in("search_engines.json"); 885if(search_engines_file_in) { 886std::string search_engines_json((std::istreambuf_iterator<char>(search_engines_file_in)), std::istreambuf_iterator<char>()); 887search_engines_file_in.close(); 888set_search_engines_from_json(search_engines_json); 889} else { 890search_engines_file_in.close(); 891auto empty_json = nlohmann::json::array(); 892std::ofstream search_engines_file_out("search_engines.json"); 893search_engines_file_out << empty_json.dump(4); 894} 895 896// Set window titles on tab switch 897content_manager->signal_page_operated.connect([this](gPanthera::ContentPage *page) { 898if(!page) { 899return; 900} 901if(auto window = dynamic_cast<Gtk::Window*>(page->get_root())) { 902if(webkit_web_view_get_title(WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj()))) { 903window->set_title(webkit_web_view_get_title(WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj()))); 904} else { 905window->set_title(_("Untitled")); 906} 907} 908}); 909 910// Load settings 911new_tab_page = "about:blank"; 912std::ifstream settings_file_in("settings.json"); 913if(settings_file_in) { 914std::string settings_json((std::istreambuf_iterator<char>(settings_file_in)), std::istreambuf_iterator<char>()); 915settings_file_in.close(); 916auto json = nlohmann::json::parse(settings_json); 917if(json.contains("ntp")) { 918new_tab_page = json["ntp"].get<std::string>(); 919} 920} else { 921settings_file_in.close(); 922auto empty_json = nlohmann::json::object(); 923empty_json["ntp"] = "about:blank"; 924std::ofstream settings_file_out("settings.json"); 925settings_file_out << empty_json.dump(4); 926settings_file_out.close(); 927} 928 929// Load plugins 930std::ifstream plugin_file_in("plugins.json"); 931if(plugin_file_in) { 932std::string plugin_json((std::istreambuf_iterator<char>(plugin_file_in)), std::istreambuf_iterator<char>()); 933plugin_file_in.close(); 934auto json = nlohmann::json::parse(plugin_json); 935for(auto &plugin : json) { 936auto plugin_path = plugin["path"].get<std::string>(); 937//load_plugin(plugin_path); 938} 939} else { 940plugin_file_in.close(); 941auto empty_json = nlohmann::json::array(); 942std::ofstream plugin_file_out("plugins.json"); 943plugin_file_out << empty_json.dump(4); 944plugin_file_out.close(); 945} 946history_manager = std::make_shared<HistoryManager>("history.db"); 947 948// Known bug <https://gitlab.gnome.org/GNOME/epiphany/-/issues/2714>: 949// JS can't veto the application's accelerators using `preventDefault()`. This is 950// not possible in WebKitGTK, or at least I can't find a way to do it. 951 952main_menu = Gio::Menu::create(); 953// File 954auto file_menu = Gio::Menu::create(); 955// New tab 956auto new_tab_action = Gio::SimpleAction::create("new_tab"); 957new_tab_action->signal_activate().connect([this](const Glib::VariantBase&) { 958on_new_tab(nullptr); 959}); 960add_action(new_tab_action); 961set_accels_for_action("app.new_tab", {"<Primary>T"}); 962file_menu->append(_("New _Tab"), "app.new_tab"); 963// Close tab 964auto close_tab_action = Gio::SimpleAction::create("close_tab"); 965close_tab_action->signal_activate().connect([this](const Glib::VariantBase&) { 966auto page = content_manager->get_last_operated_page(); 967if(page) { 968page->close(); 969} 970}); 971add_action(close_tab_action); 972set_accels_for_action("app.close_tab", {"<Primary>W"}); 973file_menu->append(_("_Close Tab"), "app.close_tab"); 974// New window 975auto new_window_action = Gio::SimpleAction::create("new_window"); 976new_window_action->signal_activate().connect([this](const Glib::VariantBase&) { 977on_activate(); 978}); 979add_action(new_window_action); 980set_accels_for_action("app.new_window", {"<Primary>N"}); 981file_menu->append(_("_New Window"), "app.new_window"); 982// Quit 983auto quit_action = Gio::SimpleAction::create("quit"); 984quit_action->signal_activate().connect([this](const Glib::VariantBase&) { 985quit(); 986}); 987add_action(quit_action); 988set_accels_for_action("app.quit", {"<Primary>Q"}); 989file_menu->append(_("_Quit"), "app.quit"); 990 991main_menu->append_submenu(_("_File"), file_menu); 992 993// View 994auto view_menu = Gio::Menu::create(); 995// Reload 996set_accels_for_action("win.reload", {"F5", "<Primary>R"}); 997view_menu->append(_("_Reload"), "win.reload"); 998 999main_menu->append_submenu(_("_View"), view_menu); 1000 1001// Go 1002auto go_menu = Gio::Menu::create(); 1003// Back 1004set_accels_for_action("win.go_back", {"<Alt>Left"}); 1005go_menu->append(_("_Back"), "win.go_back"); 1006// Forward 1007set_accels_for_action("win.go_forward", {"<Alt>Right"}); 1008go_menu->append(_("_Forward"), "win.go_forward"); 1009 1010main_menu->append_submenu(_("_Go"), go_menu); 1011 1012set_menubar(main_menu); 1013} 1014 1015void PantheraWww::on_activate() { 1016auto window = make_window(); 1017window->present(); 1018} 1019 1020void PantheraWww::on_shutdown() { 1021Gtk::Application::on_shutdown(); 1022} 1023 1024void PantheraWww::set_search_engines_from_json(const std::string &json_string) { 1025auto json = nlohmann::json::parse(json_string); 1026Glib::ustring default_search_engine_name; 1027for(auto &engine : json) { 1028SearchEngine search_engine; 1029search_engine.name = engine["name"].get<std::string>(); 1030search_engine.url = engine["url"].get<std::string>(); 1031search_engines.push_back(search_engine); 1032if(engine.contains("default") && engine["default"].get<bool>()) { 1033default_search_engine_name = engine["name"].get<std::string>(); 1034} 1035} 1036for(auto &search_engine : search_engines) { 1037if(search_engine.name == default_search_engine_name) { 1038default_search_engine = &search_engine; 1039break; 1040} 1041} 1042} 1043 1044PantheraWww::PantheraWww() : Gtk::Application("com.roundabout_host.roundabout.PantheraWww", Gio::Application::Flags::NONE) { 1045} 1046 1047Glib::RefPtr<PantheraWww> PantheraWww::create() { 1048return Glib::make_refptr_for_instance<PantheraWww>(new PantheraWww()); 1049} 1050 1051void PantheraWww::load_plugin(const std::string &plugin_path) { 1052// TODO: needs to be reworked, to work with multi-window 1053void *handle = dlopen(plugin_path.c_str(), RTLD_LAZY | RTLD_GLOBAL); 1054if(!handle) { 1055std::cerr << "Failed to load plugin: " << dlerror() << std::endl; 1056return; 1057} 1058auto entrypoint = (plugin_entrypoint_type)dlsym(handle, "plugin_init"); 1059if(!entrypoint) { 1060std::cerr << "Failed to find entrypoint in plugin: " << dlerror() << std::endl; 1061dlclose(handle); 1062return; 1063} 1064entrypoint(); 1065} 1066 1067PantheraWww::~PantheraWww() = default; 1068 1069/* 1070extern "C" { 1071void panthera_log(const char *message) { 1072std::cerr << message << std::endl; 1073} 1074 1075void panthera_add_pane(const char *id, const char *label, GtkImage *icon, GtkWidget *pane) { 1076auto pane_child = Glib::wrap(pane); 1077auto icon_cc = Glib::wrap(icon); 1078auto label_cc = Glib::ustring(label); 1079auto id_cc = Glib::ustring(id); 1080//auto new_pane = Gtk::make_managed<gPanthera::DockablePane>(app->get_layout_manager(), *pane_child, id_cc, label_cc, icon_cc); 1081// TODO: Port to multi-window 1082//app->get_layout_manager()->add_pane(new_pane); 1083} 1084} 1085*/ 1086 1087int main(int argc, char *argv[]) { 1088gPanthera::init(); 1089 1090auto app = PantheraWww::create(); 1091 1092return app->run(argc, argv); 1093} 1094