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
void on_new_tab(gPanthera::ContentStack *stack) {
101
if(!stack) {
102
// Find the current area
103
stack = content_manager->get_last_operated_page()->get_stack();
104
}
105
106
WebKitWebView *webview = WEBKIT_WEB_VIEW(webkit_web_view_new());
107
gtk_widget_set_hexpand(GTK_WIDGET(webview), true);
108
gtk_widget_set_vexpand(GTK_WIDGET(webview), true);
109
auto page_content = Gtk::make_managed<Gtk::Box>();
110
gtk_box_append(page_content->gobj(), GTK_WIDGET(webview));
111
auto page_tab = new Gtk::Label("Untitled");
112
auto page = Gtk::make_managed<gPanthera::ContentPage>(content_manager, stack, page_content, page_tab);
113
g_signal_connect(webview, "notify", G_CALLBACK(notify_callback), page->gobj());
114
webkit_web_view_load_uri(webview, "about:blank");
115
auto cookie_manager = webkit_network_session_get_cookie_manager(webkit_web_view_get_network_session(webview));
116
webkit_cookie_manager_set_persistent_storage(cookie_manager, cookie_file.c_str(), WEBKIT_COOKIE_PERSISTENT_STORAGE_TEXT);
117
webkit_cookie_manager_set_accept_policy(cookie_manager, WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS);
118
GtkEventController *click_controller_back = GTK_EVENT_CONTROLLER(gtk_gesture_click_new());
119
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click_controller_back), 8);
120
g_signal_connect(click_controller_back, "pressed", G_CALLBACK(on_back_pressed), webview);
121
gtk_widget_add_controller(GTK_WIDGET(webview), click_controller_back);
122
123
GtkEventController *click_controller_forward = GTK_EVENT_CONTROLLER(gtk_gesture_click_new());
124
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click_controller_forward), 9);
125
g_signal_connect(click_controller_forward, "pressed", G_CALLBACK(on_forward_pressed), webview);
126
gtk_widget_add_controller(GTK_WIDGET(webview), click_controller_forward);
127
128
stack->add_page(*page);
129
stack->set_visible_child(*page);
130
content_manager->set_last_operated_page(page);
131
}
132
133
void on_startup() override {
134
Gtk::Application::on_startup();
135
add_window(*window);
136
window->set_default_size(600, 400);
137
layout_manager = std::make_shared<gPanthera::LayoutManager>();
138
auto dock_stack_1 = Gtk::make_managed<gPanthera::DockStack>(layout_manager, "One", "one");
139
auto switcher_1 = Gtk::make_managed<gPanthera::DockStackSwitcher>(dock_stack_1, Gtk::Orientation::HORIZONTAL);
140
auto dock_stack_2 = Gtk::make_managed<gPanthera::DockStack>(layout_manager, "Two", "two");
141
auto switcher_2 = Gtk::make_managed<gPanthera::DockStackSwitcher>(dock_stack_2, Gtk::Orientation::VERTICAL);
142
auto pane_1_content = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0);
143
auto debug_button = Gtk::make_managed<Gtk::Button>("Debug");
144
pane_1_content->append(*debug_button);
145
auto pane_2_content = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0);
146
auto test_notebook = Gtk::make_managed<Gtk::Notebook>();
147
test_notebook->append_page(*Gtk::make_managed<Gtk::Label>("Test 1"), *Gtk::make_managed<Gtk::Label>("Test 1"));
148
pane_2_content->append(*test_notebook);
149
auto pane_3_content = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0);
150
pane_3_content->append(*Gtk::make_managed<Gtk::Label>("Pane 3 content"));
151
auto pane_1_icon = Gtk::make_managed<Gtk::Image>();
152
pane_1_icon->set_from_icon_name("go-home-symbolic");
153
auto pane_2_icon = Gtk::make_managed<Gtk::Image>();
154
pane_2_icon->set_from_icon_name("folder-symbolic");
155
auto pane_3_icon = Gtk::make_managed<Gtk::Image>();
156
pane_3_icon->set_from_icon_name("network-transmit-receive-symbolic");
157
auto pane_1 = Gtk::make_managed<gPanthera::DockablePane>(layout_manager, *pane_1_content, "pane1", "Pane 1", pane_1_icon);
158
auto pane_2 = Gtk::make_managed<gPanthera::DockablePane>(layout_manager, *pane_2_content, "pane2", "Pane 2", pane_2_icon);
159
auto pane_3 = Gtk::make_managed<gPanthera::DockablePane>(layout_manager, *pane_3_content, "pane3", "Pane 3", pane_3_icon);
160
161
dock_stack_1->set_transition_type(Gtk::StackTransitionType::SLIDE_LEFT_RIGHT);
162
dock_stack_1->set_transition_duration(125);
163
dock_stack_1->set_expand(true);
164
dock_stack_2->set_transition_type(Gtk::StackTransitionType::SLIDE_UP_DOWN);
165
dock_stack_2->set_transition_duration(125);
166
dock_stack_2->set_expand(true);
167
168
auto outer_grid = Gtk::make_managed<Gtk::Grid>();
169
outer_grid->attach(*switcher_2, 0, 1, 1, 1);
170
outer_grid->attach(*switcher_1, 1, 2, 1, 1);
171
auto outer_paned = Gtk::make_managed<Gtk::Paned>(Gtk::Orientation::HORIZONTAL);
172
outer_paned->set_start_child(*dock_stack_2);
173
auto inner_paned = Gtk::make_managed<Gtk::Paned>(Gtk::Orientation::VERTICAL);
174
auto content = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0);
175
content_manager = std::make_shared<gPanthera::ContentManager>();
176
std::function<bool(gPanthera::ContentPage*)> detach_handler;
177
detach_handler = [](gPanthera::ContentPage *widget) {
178
auto new_stack = Gtk::make_managed<gPanthera::ContentStack>(widget->content_manager, widget->get_stack()->get_detach_handler());
179
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());
180
auto new_notebook = Gtk::make_managed<gPanthera::ContentNotebook>(new_stack, new_switcher);
181
auto window = new gPanthera::ContentWindow(new_notebook);
182
widget->redock(new_stack);
183
window->present();
184
new_stack->signal_leave_empty.connect([window]() {
185
window->close();
186
delete window;
187
});
188
return true;
189
};
190
191
auto return_extra_child = [this](gPanthera::ContentTabBar *switcher) {
192
auto new_tab_button = Gtk::make_managed<Gtk::Button>();
193
new_tab_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("list-add-symbolic")));
194
new_tab_button->set_tooltip_text("New tab");
195
new_tab_button->signal_clicked().connect([this, switcher]() {
196
on_new_tab(switcher->get_stack());
197
});
198
return new_tab_button;
199
};
200
auto content_stack = Gtk::make_managed<gPanthera::ContentStack>(content_manager, detach_handler);
201
auto content_stack_switcher = Gtk::make_managed<gPanthera::ContentTabBar>(content_stack, Gtk::Orientation::HORIZONTAL, return_extra_child);
202
content_manager->add_stack(content_stack);
203
WebKitWebView *webview = WEBKIT_WEB_VIEW(webkit_web_view_new()); // for some reason, this has to be created
204
content->set_name("content_box");
205
auto content_notebook = Gtk::make_managed<gPanthera::ContentNotebook>(content_stack, content_stack_switcher, Gtk::PositionType::TOP);
206
content->append(*content_notebook);
207
inner_paned->set_start_child(*content);
208
inner_paned->set_end_child(*dock_stack_1);
209
outer_paned->set_end_child(*inner_paned);
210
outer_grid->attach(*outer_paned, 1, 1, 1, 1);
211
212
// Get search engines
213
std::ifstream search_engines_file_in("search_engines.json");
214
if(search_engines_file_in) {
215
std::string search_engines_json((std::istreambuf_iterator<char>(search_engines_file_in)), std::istreambuf_iterator<char>());
216
search_engines_file_in.close();
217
set_search_engines_from_json(search_engines_json);
218
} else {
219
search_engines_file_in.close();
220
auto empty_json = nlohmann::json::array();
221
std::ofstream search_engines_file_out("search_engines.json");
222
search_engines_file_out << empty_json.dump(4);
223
}
224
225
// Create the toolbar
226
auto main_toolbar = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 8);
227
// URL bar
228
url_bar = Gtk::make_managed<Gtk::Entry>();
229
url_bar->set_placeholder_text("Enter URL");
230
url_bar->set_hexpand(true);
231
auto load_url_callback = [this]() {
232
auto page = content_manager->get_last_operated_page();
233
if(page) {
234
if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) {
235
webkit_web_view_load_uri(webview, url_bar->get_text().c_str());
236
}
237
}
238
};
239
// Go
240
auto go_button = Gtk::make_managed<Gtk::Button>("Go");
241
go_button->signal_clicked().connect(load_url_callback);
242
url_bar->signal_activate().connect(load_url_callback);
243
content_manager->signal_page_operated.connect([this](gPanthera::ContentPage *page) {
244
if(!page->get_child()) {
245
return;
246
}
247
if(!page->get_child()->get_first_child()) {
248
return;
249
}
250
url_bar->set_text(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())));
251
guint url_update_handler = g_signal_connect(page->get_child()->get_first_child()->gobj(), "notify", G_CALLBACK(notify_focused_callback), this);
252
std::shared_ptr<sigc::connection> control_signal_handler = std::make_shared<sigc::connection>();
253
*control_signal_handler = page->signal_control_status_changed.connect([this, page, control_signal_handler, url_update_handler](bool controlled) {
254
if(!controlled) {
255
control_signal_handler->disconnect();
256
g_signal_handler_disconnect(page->get_child()->get_first_child()->gobj(), url_update_handler);
257
}
258
});
259
});
260
// Back, forward, reload
261
auto back_button = Gtk::make_managed<Gtk::Button>();
262
back_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-previous-symbolic")));
263
back_button->set_tooltip_text("Back");
264
auto forward_button = Gtk::make_managed<Gtk::Button>();
265
forward_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-next-symbolic")));
266
forward_button->set_tooltip_text("Forward");
267
auto reload_button = Gtk::make_managed<Gtk::Button>();
268
reload_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("view-refresh-symbolic")));
269
reload_button->set_tooltip_text("Reload");
270
back_button->signal_clicked().connect([this]() {
271
auto page = content_manager->get_last_operated_page();
272
if(page) {
273
if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) {
274
webkit_web_view_go_back(webview);
275
}
276
}
277
});
278
forward_button->signal_clicked().connect([this]() {
279
auto page = content_manager->get_last_operated_page();
280
if(page) {
281
if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) {
282
webkit_web_view_go_forward(webview);
283
}
284
}
285
});
286
reload_button->signal_clicked().connect([this]() {
287
auto page = content_manager->get_last_operated_page();
288
if(page) {
289
if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) {
290
webkit_web_view_reload(webview);
291
}
292
}
293
});
294
// Search bar
295
// TODO: provide history, provide a menu button with the other engines
296
auto search_bar = Gtk::make_managed<Gtk::Entry>();
297
search_bar->set_placeholder_text("Search");
298
search_bar->set_hexpand(true);
299
if(search_engines.empty()) {
300
search_bar->set_sensitive(false);
301
search_bar->set_placeholder_text("No search");
302
}
303
auto search_callback = [this, search_bar, content_stack]() {
304
// Create a new tab with the search results
305
if(content_manager->get_last_operated_page()) {
306
on_new_tab(nullptr);
307
} else {
308
on_new_tab(content_stack);
309
}
310
auto page = content_manager->get_last_operated_page();
311
if(page) {
312
if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) {
313
auto search_url = default_search_engine->get_search_url(search_bar->get_text());
314
webkit_web_view_load_uri(webview, search_url.c_str());
315
}
316
}
317
};
318
if(default_search_engine) {
319
search_bar->signal_activate().connect(search_callback);
320
}
321
// Assemble the toolbar
322
main_toolbar->append(*back_button);
323
main_toolbar->append(*forward_button);
324
main_toolbar->append(*reload_button);
325
main_toolbar->append(*url_bar);
326
main_toolbar->append(*go_button);
327
main_toolbar->append(*search_bar);
328
if(default_search_engine) {
329
auto search_button = Gtk::make_managed<Gtk::Button>(default_search_engine->name);
330
search_button->signal_clicked().connect(search_callback);
331
main_toolbar->append(*search_button);
332
}
333
outer_grid->attach(*main_toolbar, 0, 0, 2, 1);
334
window->set_child(*outer_grid);
335
debug_button->signal_clicked().connect([this]() {
336
if(content_manager->get_last_operated_page()) {
337
std::cout << "Last operated page: " << content_manager->get_last_operated_page()->get_name() << std::endl;
338
} else {
339
std::cout << "No page operated!" << std::endl;
340
}
341
});
342
// TODO: Use the last operated page and allow opening tabs next to the last operated page using certain panes
343
// Load the existing layout, if it exists
344
std::ifstream layout_file_in("layout.json");
345
if(layout_file_in) {
346
std::string layout_json((std::istreambuf_iterator<char>(layout_file_in)), std::istreambuf_iterator<char>());
347
layout_file_in.close();
348
layout_manager->restore_json_layout(layout_json);
349
} else {
350
// Create a new layout if the file doesn't exist
351
layout_file_in.close();
352
353
dock_stack_1->add_pane(*pane_1);
354
dock_stack_1->add_pane(*pane_3);
355
dock_stack_2->add_pane(*pane_2);
356
357
std::ofstream layout_file_out("layout.json");
358
layout_file_out << layout_manager->get_layout_as_json();
359
layout_file_out.close();
360
}
361
// Save the layout when changed
362
layout_manager->signal_pane_moved.connect([this](gPanthera::DockablePane *pane) {
363
std::ofstream layout_file_out("layout.json");
364
layout_file_out << layout_manager->get_layout_as_json();
365
layout_file_out.close();
366
std::cout << "Layout changed: " << layout_manager->get_layout_as_json() << std::endl;
367
});
368
369
auto new_tab_action = Gio::SimpleAction::create("new_tab");
370
new_tab_action->signal_activate().connect([this](const Glib::VariantBase&) {
371
on_new_tab(nullptr);
372
});
373
add_action(new_tab_action);
374
set_accels_for_action("app.new_tab", {"<Primary>T"});
375
auto close_tab_action = Gio::SimpleAction::create("close_tab");
376
close_tab_action->signal_activate().connect([this](const Glib::VariantBase&) {
377
auto page = content_manager->get_last_operated_page();
378
if(page) {
379
page->close();
380
}
381
});
382
add_action(close_tab_action);
383
set_accels_for_action("app.close_tab", {"<Primary>W"});
384
}
385
386
void on_activate() override {
387
window->present();
388
}
389
390
void set_search_engines_from_json(const std::string &json_string) {
391
auto json = nlohmann::json::parse(json_string);
392
Glib::ustring default_search_engine_name;
393
for(auto &engine : json) {
394
SearchEngine search_engine;
395
search_engine.name = engine["name"].get<std::string>();
396
search_engine.url = engine["url"].get<std::string>();
397
search_engines.push_back(search_engine);
398
if(engine.contains("default") && engine["default"].get<bool>()) {
399
default_search_engine_name = engine["name"].get<std::string>();
400
}
401
}
402
for(auto &search_engine : search_engines) {
403
if(search_engine.name == default_search_engine_name) {
404
default_search_engine = &search_engine;
405
break;
406
}
407
}
408
}
409
public:
410
static Glib::RefPtr<PantheraWww> create() {
411
return Glib::make_refptr_for_instance<PantheraWww>(new PantheraWww());
412
}
413
};
414
415
int main(int argc, char *argv[]) {
416
gPanthera::init();
417
auto app = PantheraWww::create();
418
return app->run(argc, argv);
419
}
420