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