roundabout,
created on Monday, 23 June 2025, 14:16:51 (1750688211),
received on Monday, 23 June 2025, 14:16:54 (1750688214)
Author identity: vlad <vlad.muntoiu@gmail.com>
22da812df1c2ba4b4ab383b97c8ee90e9cf4be3b
panthera-www.cc
@@ -271,173 +271,52 @@ public:
};
class PantheraWww : public Gtk::Application {
Gtk::ApplicationWindow *window = Gtk::make_managed<Gtk::ApplicationWindow>();
protected:
std::shared_ptr<gPanthera::LayoutManager> layout_manager;
private:
Gtk::ApplicationWindow* window;
std::shared_ptr<gPanthera::ContentManager> content_manager;
Gtk::Entry *url_bar = nullptr;
std::string cookie_file = "cookies.txt";
std::string cookie_file;
std::vector<SearchEngine> search_engines;
SearchEngine *default_search_engine = nullptr;
SearchEngine* default_search_engine;
std::shared_ptr<HistoryManager> history_manager;
HistoryViewer *history_viewer = nullptr;
HistoryViewer* history_viewer;
Glib::RefPtr<Gio::Menu> main_menu;
std::string new_tab_page;
static void load_change_callback(WebKitWebView *object, WebKitLoadEvent load_event, gpointer data) {
if(auto self = static_cast<PantheraWww*>(data)) {
if(load_event == WEBKIT_LOAD_COMMITTED) {
if(webkit_web_view_get_title(WEBKIT_WEB_VIEW(object)) == nullptr) {
self->history_manager->log_url(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(object)), "");
} else {
self->history_manager->log_url(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(object)),
webkit_web_view_get_title(WEBKIT_WEB_VIEW(object)));
}
if(self->history_viewer->get_visible()) {
self->history_viewer->reload_history();
}
}
}
}
static void notify_callback(GObject *object, GParamSpec *pspec, gpointer data) {
if(!gtk_widget_get_parent(GTK_WIDGET(object))) {
return;
}
if(auto self = static_cast<PantheraWww*>(data)) {
auto parent = gtk_widget_get_parent(gtk_widget_get_parent(GTK_WIDGET(object)));
if(auto page = dynamic_cast<gPanthera::ContentPage*>(Glib::wrap(parent))) {
if(g_strcmp0(pspec->name, "title") == 0) {
if(auto label = dynamic_cast<Gtk::Label*>(page->tab_widget->get_last_child())) {
if(strlen(webkit_web_view_get_title(WEBKIT_WEB_VIEW(object))) == 0) {
label->set_label("Untitled");
} else {
label->set_label(webkit_web_view_get_title(WEBKIT_WEB_VIEW(object)));
}
}
} else if(g_strcmp0(pspec->name, "favicon") == 0) {
// Update favicons
if(auto image = dynamic_cast<Gtk::Image*>(page->tab_widget->get_first_child())) {
image->set_from_icon_name("image-loading-symbolic");
if(auto favicon = webkit_web_view_get_favicon(WEBKIT_WEB_VIEW(object))) {
gtk_image_set_from_paintable(image->gobj(), GDK_PAINTABLE(favicon));
}
}
}
}
}
}
static void notify_focused_callback(GObject *object, GParamSpec *pspec, gpointer data) {
if(!gtk_widget_get_parent(GTK_WIDGET(object))) {
return;
}
auto this_ = static_cast<PantheraWww*>(data);
auto parent = gtk_widget_get_parent(gtk_widget_get_parent(GTK_WIDGET(object)));
if(auto page = dynamic_cast<gPanthera::ContentPage*>(Glib::wrap(parent))) {
if(g_strcmp0(pspec->name, "uri") == 0) {
this_->url_bar->set_text(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(object)));
}
}
}
static void on_back_pressed(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) {
WebKitWebView *webview = WEBKIT_WEB_VIEW(user_data);
webkit_web_view_go_back(webview);
}
static void load_change_callback(WebKitWebView* object, WebKitLoadEvent load_event, gpointer data);
static void notify_callback(GObject* object, GParamSpec* pspec, gpointer data);
static void notify_focused_callback(GObject* object, GParamSpec* pspec, gpointer data);
static void on_back_pressed(GtkGestureClick* gesture, int n_press, double x, double y, gpointer user_data);
static void on_forward_pressed(GtkGestureClick* gesture, int n_press, double x, double y, gpointer user_data);
static gboolean on_decide_policy(WebKitWebView* source, WebKitPolicyDecision* decision, WebKitPolicyDecisionType type, gpointer user_data);
static void on_forward_pressed(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) {
WebKitWebView *webview = WEBKIT_WEB_VIEW(user_data);
webkit_web_view_go_forward(webview);
}
static gboolean on_decide_policy(WebKitWebView *source, WebKitPolicyDecision *decision, WebKitPolicyDecisionType type, gpointer user_data) {
if(auto self = static_cast<PantheraWww*>(user_data)) {
if(type == WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION) {
auto action = webkit_navigation_policy_decision_get_navigation_action(WEBKIT_NAVIGATION_POLICY_DECISION(decision));
Glib::ustring url = Glib::ustring(webkit_uri_request_get_uri(webkit_navigation_action_get_request(action)));
if(!starts_with(url, "about:") && url.find("://") == Glib::ustring::npos) {
// It is a special scheme (mailto:, tel: etc.)
try {
Gio::AppInfo::launch_default_for_uri(url);
} catch(const Glib::Error &exception) {
std::cerr << "Failed to launch special URI: " << exception.what() << std::endl;
}
webkit_policy_decision_ignore(decision);
return true;
}
if(webkit_navigation_action_get_mouse_button(action) == 2) {
// Middle-click opens in a new window
self->on_new_tab(nullptr, url, false);
webkit_policy_decision_ignore(decision);
return true;
}
} else if(type == WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION) {
auto action = webkit_navigation_policy_decision_get_navigation_action(WEBKIT_NAVIGATION_POLICY_DECISION(decision));
self->on_new_tab(nullptr, webkit_uri_request_get_uri(webkit_navigation_action_get_request(action)), false, true);
return true;
}
}
return false;
}
std::string new_tab_page;
protected:
void on_startup() override;
void on_activate() override;
void on_new_tab(gPanthera::ContentStack *stack, const Glib::ustring &url = "", bool focus = true, bool new_window = false) {
if(!stack) {
// Find the current area
stack = content_manager->get_last_operated_page()->get_stack();
}
Glib::ustring url_ = url;
if(url.empty()) {
url_ = new_tab_page;
}
public:
PantheraWww();
friend class PantheraWindow;
void on_new_tab(gPanthera::ContentStack* stack, const Glib::ustring& url = "", bool focus = true, bool new_window = false);
void set_search_engines_from_json(const std::string& json_string);
static Glib::RefPtr<PantheraWww> create();
std::shared_ptr<gPanthera::ContentManager> get_content_manager();
void load_plugin(const std::string& plugin_path);
};
WebKitWebView *webview = WEBKIT_WEB_VIEW(webkit_web_view_new());
gtk_widget_set_hexpand(GTK_WIDGET(webview), true);
gtk_widget_set_vexpand(GTK_WIDGET(webview), true);
auto page_content = Gtk::make_managed<Gtk::Box>();
gtk_box_append(page_content->gobj(), GTK_WIDGET(webview));
auto page_tab = new Gtk::Box();
page_tab->set_orientation(Gtk::Orientation::HORIZONTAL);
auto initial_icon = Gtk::make_managed<Gtk::Image>(Gio::Icon::create("image-loading-symbolic"));
page_tab->append(*initial_icon);
page_tab->append(*Gtk::make_managed<Gtk::Label>("Untitled"));
page_tab->set_spacing(4);
auto page = Gtk::make_managed<gPanthera::ContentPage>(content_manager, stack, page_content, page_tab);
g_signal_connect(webview, "notify", G_CALLBACK(notify_callback), this);
g_signal_connect(webview, "load-changed", G_CALLBACK(load_change_callback), this);
g_signal_connect(webview, "decide-policy", G_CALLBACK(on_decide_policy), this);
webkit_web_view_load_uri(webview, url_.data());
auto cookie_manager = webkit_network_session_get_cookie_manager(webkit_web_view_get_network_session(webview));
webkit_cookie_manager_set_persistent_storage(cookie_manager, cookie_file.c_str(), WEBKIT_COOKIE_PERSISTENT_STORAGE_TEXT);
webkit_cookie_manager_set_accept_policy(cookie_manager, WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS);
auto website_data_manager = webkit_network_session_get_website_data_manager(webkit_web_view_get_network_session(webview));
webkit_website_data_manager_set_favicons_enabled(website_data_manager, true);
GtkEventController *click_controller_back = GTK_EVENT_CONTROLLER(gtk_gesture_click_new());
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click_controller_back), 8);
g_signal_connect(click_controller_back, "pressed", G_CALLBACK(on_back_pressed), webview);
gtk_widget_add_controller(GTK_WIDGET(webview), click_controller_back);
GtkEventController *click_controller_forward = GTK_EVENT_CONTROLLER(gtk_gesture_click_new());
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click_controller_forward), 9);
g_signal_connect(click_controller_forward, "pressed", G_CALLBACK(on_forward_pressed), webview);
gtk_widget_add_controller(GTK_WIDGET(webview), click_controller_forward);
stack->add_page(*page);
if(focus) {
stack->set_visible_child(*page);
content_manager->set_last_operated_page(page);
}
if(new_window) {
bool result = stack->signal_detach.emit(page);
class PantheraWindow : public Gtk::ApplicationWindow {
private:
std::shared_ptr<gPanthera::LayoutManager> layout_manager;
Gtk::Entry *url_bar;
public:
friend class PantheraWww;
explicit PantheraWindow(Glib::RefPtr<Gtk::Application> const &application) : Gtk::ApplicationWindow(application) {
auto panthera = dynamic_cast<PantheraWww*>(application.get());
if(!panthera) {
throw std::runtime_error("Application is not a PantheraWww instance");
}
}
void on_startup() override {
Gtk::Application::on_startup();
add_window(*window);
window->set_default_size(600, 400);
this->set_default_size(800, 600);
layout_manager = std::make_shared<gPanthera::LayoutManager>();
auto dock_stack_1 = Gtk::make_managed<gPanthera::DockStack>(layout_manager, "One", "one");
auto switcher_1 = Gtk::make_managed<gPanthera::DockStackSwitcher>(dock_stack_1, Gtk::Orientation::HORIZONTAL);
auto dock_stack_2 = Gtk::make_managed<gPanthera::DockStack>(layout_manager, "Two", "two");
@@ -445,8 +324,8 @@ protected:
auto pane_1_content = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0);
auto debug_button = Gtk::make_managed<Gtk::Button>("Debug");
auto print_history_button = Gtk::make_managed<Gtk::Button>("Print history");
print_history_button->signal_clicked().connect([this]() {
auto history = history_manager->get_history();
print_history_button->signal_clicked().connect([this, panthera]() {
auto history = panthera->history_manager->get_history();
for(const auto &entry : history) {
std::cout << "entry " << entry.id << ", url " << entry.url << ", unix time " << entry.timestamp << std::endl;
// TODO: exclude redirecting URLs
@@ -471,7 +350,7 @@ protected:
outer_paned->set_start_child(*dock_stack_2);
auto inner_paned = Gtk::make_managed<Gtk::Paned>(Gtk::Orientation::VERTICAL);
auto content = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0);
content_manager = std::make_shared<gPanthera::ContentManager>();
panthera->content_manager = std::make_shared<gPanthera::ContentManager>();
std::function<bool(gPanthera::ContentPage*)> detach_handler;
detach_handler = [](gPanthera::ContentPage *widget) {
auto new_stack = Gtk::make_managed<gPanthera::ContentStack>(widget->content_manager, widget->get_stack()->get_detach_handler());
@@ -487,18 +366,18 @@ protected:
return true;
};
auto return_extra_child = [this](gPanthera::ContentTabBar *switcher) {
auto return_extra_child = [this, panthera](gPanthera::ContentTabBar *switcher) {
auto new_tab_button = Gtk::make_managed<Gtk::Button>();
new_tab_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("list-add-symbolic")));
new_tab_button->set_tooltip_text("New tab");
new_tab_button->signal_clicked().connect([this, switcher]() {
on_new_tab(switcher->get_stack());
new_tab_button->signal_clicked().connect([this, switcher, panthera]() {
panthera->on_new_tab(switcher->get_stack());
});
return new_tab_button;
};
auto content_stack = Gtk::make_managed<gPanthera::ContentStack>(content_manager, detach_handler);
auto content_stack = Gtk::make_managed<gPanthera::ContentStack>(panthera->content_manager, detach_handler);
auto content_stack_switcher = Gtk::make_managed<gPanthera::ContentTabBar>(content_stack, Gtk::Orientation::HORIZONTAL, return_extra_child);
content_manager->add_stack(content_stack);
panthera->content_manager->add_stack(content_stack);
WebKitWebView *webview = WEBKIT_WEB_VIEW(webkit_web_view_new()); // for some reason, this has to be created
content->set_name("content_box");
auto content_notebook = Gtk::make_managed<gPanthera::ContentNotebook>(content_stack, content_stack_switcher, Gtk::PositionType::TOP);
@@ -507,28 +386,14 @@ protected:
inner_paned->set_end_child(*dock_stack_1);
outer_paned->set_end_child(*inner_paned);
outer_grid->attach(*outer_paned, 1, 2, 1, 1);
// Get search engines
std::ifstream search_engines_file_in("search_engines.json");
if(search_engines_file_in) {
std::string search_engines_json((std::istreambuf_iterator<char>(search_engines_file_in)), std::istreambuf_iterator<char>());
search_engines_file_in.close();
set_search_engines_from_json(search_engines_json);
} else {
search_engines_file_in.close();
auto empty_json = nlohmann::json::array();
std::ofstream search_engines_file_out("search_engines.json");
search_engines_file_out << empty_json.dump(4);
}
// Create the toolbar
auto main_toolbar = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 8);
// URL bar
url_bar = Gtk::make_managed<Gtk::Entry>();
url_bar->set_placeholder_text("Enter URL");
url_bar->set_hexpand(true);
auto load_url_callback = [this]() {
auto page = content_manager->get_last_operated_page();
auto load_url_callback = [this, panthera]() {
auto page = panthera->content_manager->get_last_operated_page();
bool has_protocol = url_bar->get_text().find("://") != std::string::npos;
if(!has_protocol) {
url_bar->set_text("http://" + url_bar->get_text());
@@ -543,7 +408,7 @@ protected:
auto go_button = Gtk::make_managed<Gtk::Button>("Go");
go_button->signal_clicked().connect(load_url_callback);
url_bar->signal_activate().connect(load_url_callback);
content_manager->signal_page_operated.connect([this](gPanthera::ContentPage *page) {
panthera->content_manager->signal_page_operated.connect([this, panthera](gPanthera::ContentPage *page) {
if(!page->get_child()) {
return;
}
@@ -551,7 +416,7 @@ protected:
return;
}
url_bar->set_text(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())));
guint url_update_handler = g_signal_connect(page->get_child()->get_first_child()->gobj(), "notify", G_CALLBACK(notify_focused_callback), this);
guint url_update_handler = g_signal_connect(page->get_child()->get_first_child()->gobj(), "notify", G_CALLBACK(panthera->notify_focused_callback), this);
std::shared_ptr<sigc::connection> control_signal_handler = std::make_shared<sigc::connection>();
*control_signal_handler = page->signal_control_status_changed.connect([this, page, control_signal_handler, url_update_handler](bool controlled) {
if(!controlled) {
@@ -562,10 +427,10 @@ protected:
}
});
});
history_manager = std::make_shared<HistoryManager>("history.db");
history_viewer = Gtk::make_managed<HistoryViewer>(layout_manager, history_manager);
history_viewer->signal_open_url.connect([this](const std::string &url) {
on_new_tab(nullptr, url, true);
panthera->history_manager = std::make_shared<HistoryManager>("history.db");
panthera->history_viewer = Gtk::make_managed<HistoryViewer>(layout_manager, panthera->history_manager);
panthera->history_viewer->signal_open_url.connect([this, panthera](const std::string &url) {
panthera->on_new_tab(nullptr, url, true);
});
// Back, forward, reload
auto back_button = Gtk::make_managed<Gtk::Button>();
@@ -577,24 +442,24 @@ protected:
auto reload_button = Gtk::make_managed<Gtk::Button>();
reload_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("view-refresh-symbolic")));
reload_button->set_tooltip_text("Reload");
back_button->signal_clicked().connect([this]() {
auto page = content_manager->get_last_operated_page();
back_button->signal_clicked().connect([this, panthera]() {
auto page = panthera->content_manager->get_last_operated_page();
if(page) {
if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) {
webkit_web_view_go_back(webview);
}
}
});
forward_button->signal_clicked().connect([this]() {
auto page = content_manager->get_last_operated_page();
forward_button->signal_clicked().connect([this, panthera]() {
auto page = panthera->content_manager->get_last_operated_page();
if(page) {
if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) {
webkit_web_view_go_forward(webview);
}
}
});
reload_button->signal_clicked().connect([this]() {
auto page = content_manager->get_last_operated_page();
reload_button->signal_clicked().connect([this, panthera]() {
auto page = panthera->content_manager->get_last_operated_page();
if(page) {
if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) {
webkit_web_view_reload(webview);
@@ -606,28 +471,29 @@ protected:
auto search_bar = Gtk::make_managed<Gtk::Entry>();
search_bar->set_placeholder_text("Search");
search_bar->set_hexpand(true);
if(search_engines.empty()) {
if(panthera->search_engines.empty()) {
search_bar->set_sensitive(false);
search_bar->set_placeholder_text("No search");
}
auto search_callback = [this, search_bar, content_stack]() {
auto search_callback = [this, search_bar, content_stack, panthera]() {
// Create a new tab with the search results
if(content_manager->get_last_operated_page()) {
on_new_tab(nullptr);
if(panthera->content_manager->get_last_operated_page()) {
panthera->on_new_tab(nullptr);
} else {
on_new_tab(content_stack);
panthera->on_new_tab(content_stack);
}
auto page = content_manager->get_last_operated_page();
auto page = panthera->content_manager->get_last_operated_page();
if(page) {
if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) {
auto search_url = default_search_engine->get_search_url(search_bar->get_text());
auto search_url = panthera->default_search_engine->get_search_url(search_bar->get_text());
webkit_web_view_load_uri(webview, search_url.c_str());
}
}
};
if(default_search_engine) {
if(panthera->default_search_engine) {
search_bar->signal_activate().connect(search_callback);
}
// Assemble the toolbar
main_toolbar->append(*back_button);
main_toolbar->append(*forward_button);
@@ -635,16 +501,16 @@ protected:
main_toolbar->append(*url_bar);
main_toolbar->append(*go_button);
main_toolbar->append(*search_bar);
if(default_search_engine) {
auto search_button = Gtk::make_managed<Gtk::Button>(default_search_engine->name);
if(panthera->default_search_engine) {
auto search_button = Gtk::make_managed<Gtk::Button>(panthera->default_search_engine->name);
search_button->signal_clicked().connect(search_callback);
main_toolbar->append(*search_button);
}
outer_grid->attach(*main_toolbar, 0, 1, 2, 1);
window->set_child(*outer_grid);
debug_button->signal_clicked().connect([this]() {
if(content_manager->get_last_operated_page()) {
std::cout << "Last operated page: " << content_manager->get_last_operated_page()->get_name() << std::endl;
this->set_child(*outer_grid);
debug_button->signal_clicked().connect([this, panthera]() {
if(panthera->content_manager->get_last_operated_page()) {
std::cout << "Last operated page: " << panthera->content_manager->get_last_operated_page()->get_name() << std::endl;
} else {
std::cout << "No page operated!" << std::endl;
}
@@ -661,140 +527,303 @@ protected:
layout_file_in.close();
dock_stack_1->add_pane(*pane_1);
dock_stack_1->add_pane(*history_viewer);
dock_stack_1->add_pane(*panthera->history_viewer);
std::ofstream layout_file_out("layout.json");
layout_file_out << layout_manager->get_layout_as_json();
layout_file_out.close();
}
// Save the layout when changed
layout_manager->signal_pane_moved.connect([this](gPanthera::DockablePane *pane) {
layout_manager->signal_pane_moved.connect([this, panthera](gPanthera::DockablePane *pane) {
std::ofstream layout_file_out("layout.json");
layout_file_out << layout_manager->get_layout_as_json();
layout_file_out.close();
std::cout << "Layout changed: " << layout_manager->get_layout_as_json() << std::endl;
});
}
};
// Load settings
new_tab_page = "about:blank";
std::ifstream settings_file_in("settings.json");
if(settings_file_in) {
std::string settings_json((std::istreambuf_iterator<char>(settings_file_in)), std::istreambuf_iterator<char>());
settings_file_in.close();
auto json = nlohmann::json::parse(settings_json);
if(json.contains("ntp")) {
new_tab_page = json["ntp"].get<std::string>();
void PantheraWww::load_change_callback(WebKitWebView *object, WebKitLoadEvent load_event, gpointer data) {
if(auto self = static_cast<PantheraWww*>(data)) {
if(load_event == WEBKIT_LOAD_COMMITTED) {
if(webkit_web_view_get_title(WEBKIT_WEB_VIEW(object)) == nullptr) {
self->history_manager->log_url(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(object)), "");
} else {
self->history_manager->log_url(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(object)),
webkit_web_view_get_title(WEBKIT_WEB_VIEW(object)));
}
if(self->history_viewer->get_visible()) {
self->history_viewer->reload_history();
}
} else {
settings_file_in.close();
auto empty_json = nlohmann::json::object();
empty_json["ntp"] = "about:blank";
std::ofstream settings_file_out("settings.json");
settings_file_out << empty_json.dump(4);
settings_file_out.close();
}
}
}
// Load plugins
std::ifstream plugin_file_in("plugins.json");
if(plugin_file_in) {
std::string plugin_json((std::istreambuf_iterator<char>(plugin_file_in)), std::istreambuf_iterator<char>());
plugin_file_in.close();
auto json = nlohmann::json::parse(plugin_json);
for(auto &plugin : json) {
auto plugin_path = plugin["path"].get<std::string>();
load_plugin(plugin_path);
void PantheraWww::notify_callback(GObject *object, GParamSpec *pspec, gpointer data) {
if(!gtk_widget_get_parent(GTK_WIDGET(object))) {
return;
}
if(auto self = static_cast<PantheraWww*>(data)) {
auto parent = gtk_widget_get_parent(gtk_widget_get_parent(GTK_WIDGET(object)));
if(auto page = dynamic_cast<gPanthera::ContentPage*>(Glib::wrap(parent))) {
if(g_strcmp0(pspec->name, "title") == 0) {
if(auto label = dynamic_cast<Gtk::Label*>(page->tab_widget->get_last_child())) {
if(strlen(webkit_web_view_get_title(WEBKIT_WEB_VIEW(object))) == 0) {
label->set_label("Untitled");
} else {
label->set_label(webkit_web_view_get_title(WEBKIT_WEB_VIEW(object)));
}
}
} else if(g_strcmp0(pspec->name, "favicon") == 0) {
// Update favicons
if(auto image = dynamic_cast<Gtk::Image*>(page->tab_widget->get_first_child())) {
image->set_from_icon_name("image-loading-symbolic");
if(auto favicon = webkit_web_view_get_favicon(WEBKIT_WEB_VIEW(object))) {
gtk_image_set_from_paintable(image->gobj(), GDK_PAINTABLE(favicon));
}
}
}
} else {
plugin_file_in.close();
auto empty_json = nlohmann::json::array();
std::ofstream plugin_file_out("plugins.json");
plugin_file_out << empty_json.dump(4);
plugin_file_out.close();
}
}
}
window->set_show_menubar(true);
// Known bug <https://gitlab.gnome.org/GNOME/epiphany/-/issues/2714>:
// JS can't veto the application's accelerators using `preventDefault()`. This is
// not possible in WebKitGTK, or at least I can't find a way to do it.
main_menu = Gio::Menu::create();
// File
auto file_menu = Gio::Menu::create();
// New tab
auto new_tab_action = Gio::SimpleAction::create("new_tab");
new_tab_action->signal_activate().connect([this](const Glib::VariantBase&) {
on_new_tab(nullptr);
});
add_action(new_tab_action);
set_accels_for_action("app.new_tab", {"<Primary>T"});
file_menu->append("New tab", "app.new_tab");
// Close tab
auto close_tab_action = Gio::SimpleAction::create("close_tab");
close_tab_action->signal_activate().connect([this](const Glib::VariantBase&) {
auto page = content_manager->get_last_operated_page();
if(page) {
page->close();
void PantheraWww::notify_focused_callback(GObject *object, GParamSpec *pspec, gpointer data) {
if(!gtk_widget_get_parent(GTK_WIDGET(object))) {
return;
}
auto this_ = static_cast<PantheraWww*>(data);
auto parent = gtk_widget_get_parent(gtk_widget_get_parent(GTK_WIDGET(object)));
if(auto page = dynamic_cast<gPanthera::ContentPage*>(Glib::wrap(parent))) {
if(g_strcmp0(pspec->name, "uri") == 0) {
if(auto main_window = dynamic_cast<PantheraWindow*>(page->get_root())) {
main_window->url_bar->set_text(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(object)));
}
});
add_action(close_tab_action);
set_accels_for_action("app.close_tab", {"<Primary>W"});
file_menu->append("Close tab", "app.close_tab");
main_menu->append_submenu("File", file_menu);
set_menubar(main_menu);
}
}
}
void on_activate() override {
window->present();
}
void PantheraWww::on_back_pressed(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) {
WebKitWebView *webview = WEBKIT_WEB_VIEW(user_data);
webkit_web_view_go_back(webview);
}
void PantheraWww::on_forward_pressed(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) {
WebKitWebView *webview = WEBKIT_WEB_VIEW(user_data);
webkit_web_view_go_forward(webview);
}
void set_search_engines_from_json(const std::string &json_string) {
auto json = nlohmann::json::parse(json_string);
Glib::ustring default_search_engine_name;
for(auto &engine : json) {
SearchEngine search_engine;
search_engine.name = engine["name"].get<std::string>();
search_engine.url = engine["url"].get<std::string>();
search_engines.push_back(search_engine);
if(engine.contains("default") && engine["default"].get<bool>()) {
default_search_engine_name = engine["name"].get<std::string>();
gboolean PantheraWww::on_decide_policy(WebKitWebView *source, WebKitPolicyDecision *decision, WebKitPolicyDecisionType type, gpointer user_data) {
if(auto self = static_cast<PantheraWww*>(user_data)) {
if(type == WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION) {
auto action = webkit_navigation_policy_decision_get_navigation_action(WEBKIT_NAVIGATION_POLICY_DECISION(decision));
Glib::ustring url = Glib::ustring(webkit_uri_request_get_uri(webkit_navigation_action_get_request(action)));
if(!starts_with(url, "about:") && url.find("://") == Glib::ustring::npos) {
// It is a special scheme (mailto:, tel: etc.)
try {
Gio::AppInfo::launch_default_for_uri(url);
} catch(const Glib::Error &exception) {
std::cerr << "Failed to launch special URI: " << exception.what() << std::endl;
}
webkit_policy_decision_ignore(decision);
return true;
}
}
for(auto &search_engine : search_engines) {
if(search_engine.name == default_search_engine_name) {
default_search_engine = &search_engine;
break;
if(webkit_navigation_action_get_mouse_button(action) == 2) {
// Middle-click opens in a new window
self->on_new_tab(nullptr, url, false);
webkit_policy_decision_ignore(decision);
return true;
}
} else if(type == WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION) {
auto action = webkit_navigation_policy_decision_get_navigation_action(WEBKIT_NAVIGATION_POLICY_DECISION(decision));
self->on_new_tab(nullptr, webkit_uri_request_get_uri(webkit_navigation_action_get_request(action)), false, true);
return true;
}
}
public:
static Glib::RefPtr<PantheraWww> create() {
return Glib::make_refptr_for_instance<PantheraWww>(new PantheraWww());
return false;
}
void PantheraWww::on_new_tab(gPanthera::ContentStack *stack, const Glib::ustring &url, bool focus, bool new_window) {
if(!stack) {
// Find the current area
stack = content_manager->get_last_operated_page()->get_stack();
}
Glib::ustring url_ = url;
if(url.empty()) {
url_ = new_tab_page;
}
WebKitWebView *webview = WEBKIT_WEB_VIEW(webkit_web_view_new());
gtk_widget_set_hexpand(GTK_WIDGET(webview), true);
gtk_widget_set_vexpand(GTK_WIDGET(webview), true);
auto page_content = Gtk::make_managed<Gtk::Box>();
gtk_box_append(page_content->gobj(), GTK_WIDGET(webview));
auto page_tab = new Gtk::Box();
page_tab->set_orientation(Gtk::Orientation::HORIZONTAL);
auto initial_icon = Gtk::make_managed<Gtk::Image>(Gio::Icon::create("image-loading-symbolic"));
page_tab->append(*initial_icon);
page_tab->append(*Gtk::make_managed<Gtk::Label>("Untitled"));
page_tab->set_spacing(4);
auto page = Gtk::make_managed<gPanthera::ContentPage>(content_manager, stack, page_content, page_tab);
g_signal_connect(webview, "notify", G_CALLBACK(notify_callback), this);
g_signal_connect(webview, "load-changed", G_CALLBACK(load_change_callback), this);
g_signal_connect(webview, "decide-policy", G_CALLBACK(on_decide_policy), this);
webkit_web_view_load_uri(webview, url_.data());
auto cookie_manager = webkit_network_session_get_cookie_manager(webkit_web_view_get_network_session(webview));
webkit_cookie_manager_set_persistent_storage(cookie_manager, cookie_file.c_str(), WEBKIT_COOKIE_PERSISTENT_STORAGE_TEXT);
webkit_cookie_manager_set_accept_policy(cookie_manager, WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS);
auto website_data_manager = webkit_network_session_get_website_data_manager(webkit_web_view_get_network_session(webview));
webkit_website_data_manager_set_favicons_enabled(website_data_manager, true);
GtkEventController *click_controller_back = GTK_EVENT_CONTROLLER(gtk_gesture_click_new());
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click_controller_back), 8);
g_signal_connect(click_controller_back, "pressed", G_CALLBACK(on_back_pressed), webview);
gtk_widget_add_controller(GTK_WIDGET(webview), click_controller_back);
GtkEventController *click_controller_forward = GTK_EVENT_CONTROLLER(gtk_gesture_click_new());
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click_controller_forward), 9);
g_signal_connect(click_controller_forward, "pressed", G_CALLBACK(on_forward_pressed), webview);
gtk_widget_add_controller(GTK_WIDGET(webview), click_controller_forward);
stack->add_page(*page);
if(focus) {
stack->set_visible_child(*page);
content_manager->set_last_operated_page(page);
}
if(new_window) {
bool result = stack->signal_detach.emit(page);
}
}
std::shared_ptr<gPanthera::LayoutManager> get_layout_manager() {
return layout_manager;
void PantheraWww::on_startup() {
Gtk::Application::on_startup();
Glib::RefPtr<Gtk::Application> self_ref = Glib::make_refptr_for_instance<Gtk::Application>(this);
window = new PantheraWindow(self_ref);
add_window(*window);
// Get search engines
std::ifstream search_engines_file_in("search_engines.json");
if(search_engines_file_in) {
std::string search_engines_json((std::istreambuf_iterator<char>(search_engines_file_in)), std::istreambuf_iterator<char>());
search_engines_file_in.close();
set_search_engines_from_json(search_engines_json);
} else {
search_engines_file_in.close();
auto empty_json = nlohmann::json::array();
std::ofstream search_engines_file_out("search_engines.json");
search_engines_file_out << empty_json.dump(4);
}
// Load settings
new_tab_page = "about:blank";
std::ifstream settings_file_in("settings.json");
if(settings_file_in) {
std::string settings_json((std::istreambuf_iterator<char>(settings_file_in)), std::istreambuf_iterator<char>());
settings_file_in.close();
auto json = nlohmann::json::parse(settings_json);
if(json.contains("ntp")) {
new_tab_page = json["ntp"].get<std::string>();
}
} else {
settings_file_in.close();
auto empty_json = nlohmann::json::object();
empty_json["ntp"] = "about:blank";
std::ofstream settings_file_out("settings.json");
settings_file_out << empty_json.dump(4);
settings_file_out.close();
}
std::shared_ptr<gPanthera::ContentManager> get_content_manager() {
return content_manager;
// Load plugins
std::ifstream plugin_file_in("plugins.json");
if(plugin_file_in) {
std::string plugin_json((std::istreambuf_iterator<char>(plugin_file_in)), std::istreambuf_iterator<char>());
plugin_file_in.close();
auto json = nlohmann::json::parse(plugin_json);
for(auto &plugin : json) {
auto plugin_path = plugin["path"].get<std::string>();
load_plugin(plugin_path);
}
} else {
plugin_file_in.close();
auto empty_json = nlohmann::json::array();
std::ofstream plugin_file_out("plugins.json");
plugin_file_out << empty_json.dump(4);
plugin_file_out.close();
}
void load_plugin(const std::string &plugin_path) {
void *handle = dlopen(plugin_path.c_str(), RTLD_LAZY | RTLD_GLOBAL);
if(!handle) {
std::cerr << "Failed to load plugin: " << dlerror() << std::endl;
return;
window->set_show_menubar(true);
// Known bug <https://gitlab.gnome.org/GNOME/epiphany/-/issues/2714>:
// JS can't veto the application's accelerators using `preventDefault()`. This is
// not possible in WebKitGTK, or at least I can't find a way to do it.
main_menu = Gio::Menu::create();
// File
auto file_menu = Gio::Menu::create();
// New tab
auto new_tab_action = Gio::SimpleAction::create("new_tab");
new_tab_action->signal_activate().connect([this](const Glib::VariantBase&) {
on_new_tab(nullptr);
});
add_action(new_tab_action);
set_accels_for_action("app.new_tab", {"<Primary>T"});
file_menu->append("New tab", "app.new_tab");
// Close tab
auto close_tab_action = Gio::SimpleAction::create("close_tab");
close_tab_action->signal_activate().connect([this](const Glib::VariantBase&) {
auto page = content_manager->get_last_operated_page();
if(page) {
page->close();
}
});
add_action(close_tab_action);
set_accels_for_action("app.close_tab", {"<Primary>W"});
file_menu->append("Close tab", "app.close_tab");
main_menu->append_submenu("File", file_menu);
set_menubar(main_menu);
}
void PantheraWww::on_activate() {
window->present();
}
void PantheraWww::set_search_engines_from_json(const std::string &json_string) {
auto json = nlohmann::json::parse(json_string);
Glib::ustring default_search_engine_name;
for(auto &engine : json) {
SearchEngine search_engine;
search_engine.name = engine["name"].get<std::string>();
search_engine.url = engine["url"].get<std::string>();
search_engines.push_back(search_engine);
if(engine.contains("default") && engine["default"].get<bool>()) {
default_search_engine_name = engine["name"].get<std::string>();
}
auto entrypoint = (plugin_entrypoint_type)dlsym(handle, "plugin_init");
if(!entrypoint) {
std::cerr << "Failed to find entrypoint in plugin: " << dlerror() << std::endl;
dlclose(handle);
return;
}
for(auto &search_engine : search_engines) {
if(search_engine.name == default_search_engine_name) {
default_search_engine = &search_engine;
break;
}
entrypoint();
}
};
}
PantheraWww::PantheraWww() : Gtk::Application("com.roundabout_host.roundabout.panthera_www", Gio::Application::Flags::NONE) {
}
Glib::RefPtr<PantheraWww> PantheraWww::create() {
return Glib::make_refptr_for_instance<PantheraWww>(new PantheraWww());
}
void PantheraWww::load_plugin(const std::string &plugin_path) {
void *handle = dlopen(plugin_path.c_str(), RTLD_LAZY | RTLD_GLOBAL);
if(!handle) {
std::cerr << "Failed to load plugin: " << dlerror() << std::endl;
return;
}
auto entrypoint = (plugin_entrypoint_type)dlsym(handle, "plugin_init");
if(!entrypoint) {
std::cerr << "Failed to find entrypoint in plugin: " << dlerror() << std::endl;
dlclose(handle);
return;
}
entrypoint();
}
Glib::RefPtr<PantheraWww> app = nullptr;
@@ -808,8 +837,9 @@ extern "C" {
auto icon_cc = Glib::wrap(icon);
auto label_cc = Glib::ustring(label);
auto id_cc = Glib::ustring(id);
auto new_pane = Gtk::make_managed<gPanthera::DockablePane>(app->get_layout_manager(), *pane_child, id_cc, label_cc, icon_cc);
app->get_layout_manager()->add_pane(new_pane);
//auto new_pane = Gtk::make_managed<gPanthera::DockablePane>(app->get_layout_manager(), *pane_child, id_cc, label_cc, icon_cc);
// TODO: Port to multi-window
//app->get_layout_manager()->add_pane(new_pane);
}
}