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