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