panthera-www.cc
C++ source, ASCII text
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 17std::string url_encode(const std::string &value) { 18// Thanks https://stackoverflow.com/a/17708801 19std::ostringstream escaped; 20escaped.fill('0'); 21escaped << std::hex; 22 23for(char c : value) { 24// Keep alphanumeric and other accepted characters intact 25if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') { 26escaped << c; 27continue; 28} 29 30// Any other characters are percent-encoded 31escaped << std::uppercase; 32escaped << '%' << std::setw(2) << int((unsigned char) c); 33escaped << std::nouppercase; 34} 35 36return escaped.str(); 37} 38 39struct SearchEngine { 40Glib::ustring name; 41Glib::ustring url; 42Glib::ustring get_search_url(const std::string &query) { 43auto pos = url.find("%s"); 44if(pos == Glib::ustring::npos) { 45throw std::runtime_error("Invalid search engine URL: missing '%s' placeholder"); 46} 47auto new_url = url.substr(0, pos); 48new_url.replace(pos, 2, url_encode(query)); 49return new_url; 50} 51}; 52 53class PantheraWww : public Gtk::Application { 54Gtk::Window *window = Gtk::make_managed<Gtk::Window>(); 55protected: 56std::shared_ptr<gPanthera::LayoutManager> layout_manager; 57std::shared_ptr<gPanthera::ContentManager> content_manager; 58Gtk::Entry *url_bar = nullptr; 59std::string cookie_file = "cookies.txt"; 60std::vector<SearchEngine> search_engines; 61SearchEngine *default_search_engine = nullptr; 62 63static void notify_callback(GObject *object, GParamSpec *pspec, gpointer data) { 64if(!gtk_widget_get_parent(GTK_WIDGET(object))) { 65return; 66} 67auto parent = gtk_widget_get_parent(gtk_widget_get_parent(GTK_WIDGET(object))); 68if(auto page = dynamic_cast<gPanthera::ContentPage*>(Glib::wrap(parent))) { 69if(g_strcmp0(pspec->name, "title") == 0) { 70if(auto label = dynamic_cast<Gtk::Label*>(page->tab_widget)) { 71label->set_label(webkit_web_view_get_title(WEBKIT_WEB_VIEW(object))); 72} 73} 74} 75} 76 77static void notify_focused_callback(GObject *object, GParamSpec *pspec, gpointer data) { 78if(!gtk_widget_get_parent(GTK_WIDGET(object))) { 79return; 80} 81auto this_ = static_cast<PantheraWww*>(data); 82auto parent = gtk_widget_get_parent(gtk_widget_get_parent(GTK_WIDGET(object))); 83if(auto page = dynamic_cast<gPanthera::ContentPage*>(Glib::wrap(parent))) { 84if(g_strcmp0(pspec->name, "uri") == 0) { 85this_->url_bar->set_text(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(object))); 86} 87} 88} 89 90static void on_back_pressed(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) { 91WebKitWebView *webview = WEBKIT_WEB_VIEW(user_data); 92webkit_web_view_go_back(webview); 93} 94 95static void on_forward_pressed(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) { 96WebKitWebView *webview = WEBKIT_WEB_VIEW(user_data); 97webkit_web_view_go_forward(webview); 98} 99 100static gboolean on_decide_policy(WebKitWebView *source, WebKitPolicyDecision *decision, WebKitPolicyDecisionType type, gpointer user_data) { 101// Middle-click opens in a new window 102if(auto self = static_cast<PantheraWww*>(user_data)) { 103std::cout << type << '\n'; 104if(type == WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION) { 105auto action = webkit_navigation_policy_decision_get_navigation_action(WEBKIT_NAVIGATION_POLICY_DECISION(decision)); 106if(webkit_navigation_action_get_mouse_button(action) == 2) { 107Glib::ustring url = Glib::ustring(webkit_uri_request_get_uri(webkit_navigation_action_get_request(action))); 108self->on_new_tab(nullptr, url, false); 109webkit_policy_decision_ignore(decision); 110return true; 111} 112} else if(type == WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION) { 113auto action = webkit_navigation_policy_decision_get_navigation_action(WEBKIT_NAVIGATION_POLICY_DECISION(decision)); 114self->on_new_tab(nullptr, webkit_uri_request_get_uri(webkit_navigation_action_get_request(action)), false, true); 115return true; 116} 117} 118return false; 119} 120 121void on_new_tab(gPanthera::ContentStack *stack, const Glib::ustring &url = "about:blank", bool focus = true, bool new_window = false) { 122if(!stack) { 123// Find the current area 124stack = content_manager->get_last_operated_page()->get_stack(); 125} 126 127WebKitWebView *webview = WEBKIT_WEB_VIEW(webkit_web_view_new()); 128gtk_widget_set_hexpand(GTK_WIDGET(webview), true); 129gtk_widget_set_vexpand(GTK_WIDGET(webview), true); 130auto page_content = Gtk::make_managed<Gtk::Box>(); 131gtk_box_append(page_content->gobj(), GTK_WIDGET(webview)); 132auto page_tab = new Gtk::Label("Untitled"); 133auto page = Gtk::make_managed<gPanthera::ContentPage>(content_manager, stack, page_content, page_tab); 134g_signal_connect(webview, "notify", G_CALLBACK(notify_callback), page->gobj()); 135g_signal_connect(webview, "decide-policy", G_CALLBACK(on_decide_policy), this); 136webkit_web_view_load_uri(webview, url.data()); 137auto cookie_manager = webkit_network_session_get_cookie_manager(webkit_web_view_get_network_session(webview)); 138webkit_cookie_manager_set_persistent_storage(cookie_manager, cookie_file.c_str(), WEBKIT_COOKIE_PERSISTENT_STORAGE_TEXT); 139webkit_cookie_manager_set_accept_policy(cookie_manager, WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS); 140GtkEventController *click_controller_back = GTK_EVENT_CONTROLLER(gtk_gesture_click_new()); 141gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click_controller_back), 8); 142g_signal_connect(click_controller_back, "pressed", G_CALLBACK(on_back_pressed), webview); 143gtk_widget_add_controller(GTK_WIDGET(webview), click_controller_back); 144 145GtkEventController *click_controller_forward = GTK_EVENT_CONTROLLER(gtk_gesture_click_new()); 146gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click_controller_forward), 9); 147g_signal_connect(click_controller_forward, "pressed", G_CALLBACK(on_forward_pressed), webview); 148gtk_widget_add_controller(GTK_WIDGET(webview), click_controller_forward); 149 150stack->add_page(*page); 151if(focus) { 152stack->set_visible_child(*page); 153content_manager->set_last_operated_page(page); 154} 155if(new_window) { 156bool result = stack->signal_detach.emit(page); 157} 158} 159 160void on_startup() override { 161Gtk::Application::on_startup(); 162add_window(*window); 163window->set_default_size(600, 400); 164layout_manager = std::make_shared<gPanthera::LayoutManager>(); 165auto dock_stack_1 = Gtk::make_managed<gPanthera::DockStack>(layout_manager, "One", "one"); 166auto switcher_1 = Gtk::make_managed<gPanthera::DockStackSwitcher>(dock_stack_1, Gtk::Orientation::HORIZONTAL); 167auto dock_stack_2 = Gtk::make_managed<gPanthera::DockStack>(layout_manager, "Two", "two"); 168auto switcher_2 = Gtk::make_managed<gPanthera::DockStackSwitcher>(dock_stack_2, Gtk::Orientation::VERTICAL); 169auto pane_1_content = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0); 170auto debug_button = Gtk::make_managed<Gtk::Button>("Debug"); 171pane_1_content->append(*debug_button); 172auto pane_2_content = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0); 173auto test_notebook = Gtk::make_managed<Gtk::Notebook>(); 174test_notebook->append_page(*Gtk::make_managed<Gtk::Label>("Test 1"), *Gtk::make_managed<Gtk::Label>("Test 1")); 175pane_2_content->append(*test_notebook); 176auto pane_3_content = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0); 177pane_3_content->append(*Gtk::make_managed<Gtk::Label>("Pane 3 content")); 178auto pane_1_icon = Gtk::make_managed<Gtk::Image>(); 179pane_1_icon->set_from_icon_name("go-home-symbolic"); 180auto pane_2_icon = Gtk::make_managed<Gtk::Image>(); 181pane_2_icon->set_from_icon_name("folder-symbolic"); 182auto pane_3_icon = Gtk::make_managed<Gtk::Image>(); 183pane_3_icon->set_from_icon_name("network-transmit-receive-symbolic"); 184auto pane_1 = Gtk::make_managed<gPanthera::DockablePane>(layout_manager, *pane_1_content, "pane1", "Pane 1", pane_1_icon); 185auto pane_2 = Gtk::make_managed<gPanthera::DockablePane>(layout_manager, *pane_2_content, "pane2", "Pane 2", pane_2_icon); 186auto pane_3 = Gtk::make_managed<gPanthera::DockablePane>(layout_manager, *pane_3_content, "pane3", "Pane 3", pane_3_icon); 187 188dock_stack_1->set_transition_type(Gtk::StackTransitionType::SLIDE_LEFT_RIGHT); 189dock_stack_1->set_transition_duration(125); 190dock_stack_1->set_expand(true); 191dock_stack_2->set_transition_type(Gtk::StackTransitionType::SLIDE_UP_DOWN); 192dock_stack_2->set_transition_duration(125); 193dock_stack_2->set_expand(true); 194 195auto outer_grid = Gtk::make_managed<Gtk::Grid>(); 196outer_grid->attach(*switcher_2, 0, 1, 1, 1); 197outer_grid->attach(*switcher_1, 1, 2, 1, 1); 198auto outer_paned = Gtk::make_managed<Gtk::Paned>(Gtk::Orientation::HORIZONTAL); 199outer_paned->set_start_child(*dock_stack_2); 200auto inner_paned = Gtk::make_managed<Gtk::Paned>(Gtk::Orientation::VERTICAL); 201auto content = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0); 202content_manager = std::make_shared<gPanthera::ContentManager>(); 203std::function<bool(gPanthera::ContentPage*)> detach_handler; 204detach_handler = [](gPanthera::ContentPage *widget) { 205auto new_stack = Gtk::make_managed<gPanthera::ContentStack>(widget->content_manager, widget->get_stack()->get_detach_handler()); 206auto 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()); 207auto new_notebook = Gtk::make_managed<gPanthera::ContentNotebook>(new_stack, new_switcher); 208auto window = new gPanthera::ContentWindow(new_notebook); 209widget->redock(new_stack); 210window->present(); 211new_stack->signal_leave_empty.connect([window]() { 212window->close(); 213delete window; 214}); 215return true; 216}; 217 218auto return_extra_child = [this](gPanthera::ContentTabBar *switcher) { 219auto new_tab_button = Gtk::make_managed<Gtk::Button>(); 220new_tab_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("list-add-symbolic"))); 221new_tab_button->set_tooltip_text("New tab"); 222new_tab_button->signal_clicked().connect([this, switcher]() { 223on_new_tab(switcher->get_stack()); 224}); 225return new_tab_button; 226}; 227auto content_stack = Gtk::make_managed<gPanthera::ContentStack>(content_manager, detach_handler); 228auto content_stack_switcher = Gtk::make_managed<gPanthera::ContentTabBar>(content_stack, Gtk::Orientation::HORIZONTAL, return_extra_child); 229content_manager->add_stack(content_stack); 230WebKitWebView *webview = WEBKIT_WEB_VIEW(webkit_web_view_new()); // for some reason, this has to be created 231content->set_name("content_box"); 232auto content_notebook = Gtk::make_managed<gPanthera::ContentNotebook>(content_stack, content_stack_switcher, Gtk::PositionType::TOP); 233content->append(*content_notebook); 234inner_paned->set_start_child(*content); 235inner_paned->set_end_child(*dock_stack_1); 236outer_paned->set_end_child(*inner_paned); 237outer_grid->attach(*outer_paned, 1, 1, 1, 1); 238 239// Get search engines 240std::ifstream search_engines_file_in("search_engines.json"); 241if(search_engines_file_in) { 242std::string search_engines_json((std::istreambuf_iterator<char>(search_engines_file_in)), std::istreambuf_iterator<char>()); 243search_engines_file_in.close(); 244set_search_engines_from_json(search_engines_json); 245} else { 246search_engines_file_in.close(); 247auto empty_json = nlohmann::json::array(); 248std::ofstream search_engines_file_out("search_engines.json"); 249search_engines_file_out << empty_json.dump(4); 250} 251 252// Create the toolbar 253auto main_toolbar = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 8); 254// URL bar 255url_bar = Gtk::make_managed<Gtk::Entry>(); 256url_bar->set_placeholder_text("Enter URL"); 257url_bar->set_hexpand(true); 258auto load_url_callback = [this]() { 259auto page = content_manager->get_last_operated_page(); 260if(page) { 261if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) { 262webkit_web_view_load_uri(webview, url_bar->get_text().c_str()); 263} 264} 265}; 266// Go 267auto go_button = Gtk::make_managed<Gtk::Button>("Go"); 268go_button->signal_clicked().connect(load_url_callback); 269url_bar->signal_activate().connect(load_url_callback); 270content_manager->signal_page_operated.connect([this](gPanthera::ContentPage *page) { 271if(!page->get_child()) { 272return; 273} 274if(!page->get_child()->get_first_child()) { 275return; 276} 277url_bar->set_text(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj()))); 278guint url_update_handler = g_signal_connect(page->get_child()->get_first_child()->gobj(), "notify", G_CALLBACK(notify_focused_callback), this); 279std::shared_ptr<sigc::connection> control_signal_handler = std::make_shared<sigc::connection>(); 280*control_signal_handler = page->signal_control_status_changed.connect([this, page, control_signal_handler, url_update_handler](bool controlled) { 281if(!controlled) { 282control_signal_handler->disconnect(); 283if(page->get_child() && page->get_child()->get_first_child() && WEBKIT_IS_WEB_VIEW(page->get_child()->get_first_child()->gobj())) { 284g_signal_handler_disconnect(page->get_child()->get_first_child()->gobj(), url_update_handler); 285} 286} 287}); 288}); 289// Back, forward, reload 290auto back_button = Gtk::make_managed<Gtk::Button>(); 291back_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-previous-symbolic"))); 292back_button->set_tooltip_text("Back"); 293auto forward_button = Gtk::make_managed<Gtk::Button>(); 294forward_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-next-symbolic"))); 295forward_button->set_tooltip_text("Forward"); 296auto reload_button = Gtk::make_managed<Gtk::Button>(); 297reload_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("view-refresh-symbolic"))); 298reload_button->set_tooltip_text("Reload"); 299back_button->signal_clicked().connect([this]() { 300auto page = content_manager->get_last_operated_page(); 301if(page) { 302if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) { 303webkit_web_view_go_back(webview); 304} 305} 306}); 307forward_button->signal_clicked().connect([this]() { 308auto page = content_manager->get_last_operated_page(); 309if(page) { 310if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) { 311webkit_web_view_go_forward(webview); 312} 313} 314}); 315reload_button->signal_clicked().connect([this]() { 316auto page = content_manager->get_last_operated_page(); 317if(page) { 318if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) { 319webkit_web_view_reload(webview); 320} 321} 322}); 323// Search bar 324// TODO: provide history, provide a menu button with the other engines 325auto search_bar = Gtk::make_managed<Gtk::Entry>(); 326search_bar->set_placeholder_text("Search"); 327search_bar->set_hexpand(true); 328if(search_engines.empty()) { 329search_bar->set_sensitive(false); 330search_bar->set_placeholder_text("No search"); 331} 332auto search_callback = [this, search_bar, content_stack]() { 333// Create a new tab with the search results 334if(content_manager->get_last_operated_page()) { 335on_new_tab(nullptr); 336} else { 337on_new_tab(content_stack); 338} 339auto page = content_manager->get_last_operated_page(); 340if(page) { 341if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) { 342auto search_url = default_search_engine->get_search_url(search_bar->get_text()); 343webkit_web_view_load_uri(webview, search_url.c_str()); 344} 345} 346}; 347if(default_search_engine) { 348search_bar->signal_activate().connect(search_callback); 349} 350// Assemble the toolbar 351main_toolbar->append(*back_button); 352main_toolbar->append(*forward_button); 353main_toolbar->append(*reload_button); 354main_toolbar->append(*url_bar); 355main_toolbar->append(*go_button); 356main_toolbar->append(*search_bar); 357if(default_search_engine) { 358auto search_button = Gtk::make_managed<Gtk::Button>(default_search_engine->name); 359search_button->signal_clicked().connect(search_callback); 360main_toolbar->append(*search_button); 361} 362outer_grid->attach(*main_toolbar, 0, 0, 2, 1); 363window->set_child(*outer_grid); 364debug_button->signal_clicked().connect([this]() { 365if(content_manager->get_last_operated_page()) { 366std::cout << "Last operated page: " << content_manager->get_last_operated_page()->get_name() << std::endl; 367} else { 368std::cout << "No page operated!" << std::endl; 369} 370}); 371// TODO: Use the last operated page and allow opening tabs next to the last operated page using certain panes 372// Load the existing layout, if it exists 373std::ifstream layout_file_in("layout.json"); 374if(layout_file_in) { 375std::string layout_json((std::istreambuf_iterator<char>(layout_file_in)), std::istreambuf_iterator<char>()); 376layout_file_in.close(); 377layout_manager->restore_json_layout(layout_json); 378} else { 379// Create a new layout if the file doesn't exist 380layout_file_in.close(); 381 382dock_stack_1->add_pane(*pane_1); 383dock_stack_1->add_pane(*pane_3); 384dock_stack_2->add_pane(*pane_2); 385 386std::ofstream layout_file_out("layout.json"); 387layout_file_out << layout_manager->get_layout_as_json(); 388layout_file_out.close(); 389} 390// Save the layout when changed 391layout_manager->signal_pane_moved.connect([this](gPanthera::DockablePane *pane) { 392std::ofstream layout_file_out("layout.json"); 393layout_file_out << layout_manager->get_layout_as_json(); 394layout_file_out.close(); 395std::cout << "Layout changed: " << layout_manager->get_layout_as_json() << std::endl; 396}); 397 398auto new_tab_action = Gio::SimpleAction::create("new_tab"); 399new_tab_action->signal_activate().connect([this](const Glib::VariantBase&) { 400on_new_tab(nullptr); 401}); 402add_action(new_tab_action); 403set_accels_for_action("app.new_tab", {"<Primary>T"}); 404auto close_tab_action = Gio::SimpleAction::create("close_tab"); 405close_tab_action->signal_activate().connect([this](const Glib::VariantBase&) { 406auto page = content_manager->get_last_operated_page(); 407if(page) { 408page->close(); 409} 410}); 411add_action(close_tab_action); 412set_accels_for_action("app.close_tab", {"<Primary>W"}); 413} 414 415void on_activate() override { 416window->present(); 417} 418 419void set_search_engines_from_json(const std::string &json_string) { 420auto json = nlohmann::json::parse(json_string); 421Glib::ustring default_search_engine_name; 422for(auto &engine : json) { 423SearchEngine search_engine; 424search_engine.name = engine["name"].get<std::string>(); 425search_engine.url = engine["url"].get<std::string>(); 426search_engines.push_back(search_engine); 427if(engine.contains("default") && engine["default"].get<bool>()) { 428default_search_engine_name = engine["name"].get<std::string>(); 429} 430} 431for(auto &search_engine : search_engines) { 432if(search_engine.name == default_search_engine_name) { 433default_search_engine = &search_engine; 434break; 435} 436} 437} 438public: 439static Glib::RefPtr<PantheraWww> create() { 440return Glib::make_refptr_for_instance<PantheraWww>(new PantheraWww()); 441} 442}; 443 444int main(int argc, char *argv[]) { 445gPanthera::init(); 446auto app = PantheraWww::create(); 447return app->run(argc, argv); 448}