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