GTK docking interfaces and more

By using this site, you agree to have cookies stored on your device, strictly for functional purposes, such as storing your session and preferences.

Dismiss

 panthera-www.cc

View raw Download
text/x-c++ • 39.25 kiB
C++ source, ASCII text, with very long lines (379)
        
            
1
#include "gpanthera.hh"
2
#include <gtkmm.h>
3
#include <glibmm.h>
4
#include <glibmm/ustring.h>
5
#include <iostream>
6
#include <memory>
7
#include <libintl.h>
8
#include <locale.h>
9
#include <gtk/gtk.h>
10
#include <gdk/gdk.h>
11
#include <webkit/webkit.h>
12
#include <fstream>
13
#include <nlohmann/json.hpp>
14
#include <iomanip>
15
#include <sstream>
16
#include <dlfcn.h>
17
#include "external/sqlite_orm/sqlite_orm.h"
18
#include "plugin_api.h"
19
20
using plugin_entrypoint_type = void(*)(void);
21
22
std::string url_encode(const std::string &value) {
23
// Thanks https://stackoverflow.com/a/17708801
24
std::ostringstream escaped;
25
escaped.fill('0');
26
escaped << std::hex;
27
28
for(char c : value) {
29
// Keep alphanumeric and other accepted characters intact
30
if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
31
escaped << c;
32
continue;
33
}
34
35
// Any other characters are percent-encoded
36
escaped << std::uppercase;
37
escaped << '%' << std::setw(2) << int((unsigned char) c);
38
escaped << std::nouppercase;
39
}
40
41
return escaped.str();
42
}
43
44
bool starts_with(const Glib::ustring &str, const Glib::ustring &prefix) {
45
return str.size() >= prefix.size() && str.compare(0, prefix.size(), prefix) == 0;
46
}
47
48
bool ends_with(const Glib::ustring &str, const Glib::ustring &suffix) {
49
return str.size() >= suffix.size() && str.compare(str.size() - suffix.size(), suffix.size(), suffix) == 0;
50
}
51
52
struct SearchEngine {
53
Glib::ustring name;
54
Glib::ustring url;
55
Glib::ustring get_search_url(const std::string &query) {
56
auto pos = url.find("%s");
57
if(pos == Glib::ustring::npos) {
58
throw std::runtime_error("Invalid search engine URL: missing '%s' placeholder");
59
}
60
auto new_url = url;
61
new_url.replace(pos, 2, url_encode(query));
62
return new_url;
63
}
64
};
65
66
struct HistoryEntry {
67
int id;
68
std::string url, title;
69
int64_t timestamp;
70
};
71
72
using StorageType = decltype(sqlite_orm::make_storage("",
73
sqlite_orm::make_table("history",
74
sqlite_orm::make_column("id", &HistoryEntry::id, sqlite_orm::primary_key()),
75
sqlite_orm::make_column("url", &HistoryEntry::url),
76
sqlite_orm::make_column("title", &HistoryEntry::title),
77
sqlite_orm::make_column("timestamp", &HistoryEntry::timestamp)
78
)
79
));
80
81
class HistoryManager {
82
public:
83
static auto make_storage(const std::string& path) {
84
return sqlite_orm::make_storage(path,
85
sqlite_orm::make_table("history",
86
sqlite_orm::make_column("id", &HistoryEntry::id, sqlite_orm::primary_key()),
87
sqlite_orm::make_column("url", &HistoryEntry::url),
88
sqlite_orm::make_column("title", &HistoryEntry::title),
89
sqlite_orm::make_column("timestamp", &HistoryEntry::timestamp)
90
)
91
);
92
}
93
StorageType storage;
94
explicit HistoryManager(const std::string &db_path) : storage(make_storage(db_path)) {
95
storage.sync_schema();
96
}
97
98
void log_url(const std::string &url, const std::string title, const int64_t timestamp = Glib::DateTime::create_now_local().to_unix()) {
99
storage.insert(HistoryEntry{-1, url, title, timestamp});
100
}
101
102
std::vector<HistoryEntry> get_history() {
103
return storage.get_all<HistoryEntry>();
104
}
105
106
std::vector<HistoryEntry> get_history_between(int64_t start_time, int64_t end_time) {
107
using namespace sqlite_orm;
108
return storage.get_all<HistoryEntry>(
109
where(c(&HistoryEntry::timestamp) >= start_time and c(&HistoryEntry::timestamp) < end_time),
110
order_by(&HistoryEntry::timestamp).desc()
111
);
112
}
113
};
114
115
class HistoryViewer : public gPanthera::DockablePane {
116
protected:
117
std::shared_ptr<HistoryManager> history_manager;
118
Gtk::Calendar *calendar;
119
Gtk::Label *date_label;
120
Gtk::Box *box_place;
121
public:
122
sigc::signal<void(const std::string &url)> signal_open_url;
123
HistoryViewer(std::shared_ptr<gPanthera::LayoutManager> layout_manager, std::shared_ptr<HistoryManager> history_manager) : gPanthera::DockablePane(layout_manager, *Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0), "history", "History", Gtk::make_managed<Gtk::Image>(Gio::Icon::create("document-open-recent-symbolic"))), history_manager(std::move(history_manager)) {
124
auto history_toolbar = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 0);
125
auto history_button_previous = Gtk::make_managed<Gtk::Button>();
126
history_button_previous->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-previous-symbolic")));
127
history_button_previous->set_tooltip_text("Previous day");
128
auto history_button_next = Gtk::make_managed<Gtk::Button>();
129
history_button_next->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-next-symbolic")));
130
history_button_next->set_tooltip_text("Next day");
131
132
date_label = Gtk::make_managed<Gtk::Label>("");
133
auto date_button = Gtk::make_managed<Gtk::MenuButton>();
134
date_button->set_child(*date_label);
135
date_button->set_tooltip_text("Select date");
136
date_button->set_direction(Gtk::ArrowType::NONE);
137
auto date_popover = Gtk::make_managed<Gtk::Popover>();
138
calendar = Gtk::make_managed<Gtk::Calendar>();
139
date_popover->set_child(*calendar);
140
date_button->set_popover(*date_popover);
141
date_button->set_has_frame(false);
142
date_button->set_hexpand(true);
143
144
history_toolbar->append(*history_button_previous);
145
history_toolbar->append(*date_button);
146
history_toolbar->append(*history_button_next);
147
auto box = dynamic_cast<Gtk::Box*>(this->child);
148
box->append(*history_toolbar);
149
150
history_button_next->signal_clicked().connect([this]() {
151
Glib::DateTime calendar_day = calendar->get_date();
152
Glib::DateTime datetime = Glib::DateTime::create_local(
153
calendar_day.get_year(),
154
calendar_day.get_month(),
155
calendar_day.get_day_of_month(),
156
0, 0, 0
157
);
158
auto next_entries = this->history_manager->storage.get_all<HistoryEntry>(
159
sqlite_orm::where(sqlite_orm::c(&HistoryEntry::timestamp) >= datetime.to_unix() + 86400),
160
sqlite_orm::order_by(&HistoryEntry::timestamp).asc()
161
);
162
if(next_entries.empty()) {
163
return;
164
}
165
auto last_entry = next_entries.front();
166
Glib::DateTime next_day = Glib::DateTime::create_now_local(last_entry.timestamp);
167
Glib::DateTime new_date = Glib::DateTime::create_local(
168
next_day.get_year(),
169
next_day.get_month(),
170
next_day.get_day_of_month(),
171
0, 0, 0
172
);
173
calendar->select_day(new_date);
174
reload_history();
175
});
176
177
history_button_previous->signal_clicked().connect([this]() {
178
Glib::DateTime calendar_day = calendar->get_date();
179
Glib::DateTime datetime = Glib::DateTime::create_local(
180
calendar_day.get_year(),
181
calendar_day.get_month(),
182
calendar_day.get_day_of_month(),
183
0, 0, 0
184
);
185
auto previous_entries = this->history_manager->storage.get_all<HistoryEntry>(
186
sqlite_orm::where(sqlite_orm::c(&HistoryEntry::timestamp) < datetime.to_unix()),
187
sqlite_orm::order_by(&HistoryEntry::timestamp).desc()
188
);
189
if(previous_entries.empty()) {
190
return;
191
}
192
auto last_entry = previous_entries.front();
193
Glib::DateTime previous_day = Glib::DateTime::create_now_local(last_entry.timestamp);
194
Glib::DateTime new_date = Glib::DateTime::create_local(
195
previous_day.get_year(),
196
previous_day.get_month(),
197
previous_day.get_day_of_month(),
198
0, 0, 0
199
);
200
calendar->select_day(new_date);
201
reload_history();
202
});
203
204
calendar->signal_day_selected().connect(sigc::mem_fun(*this, &HistoryViewer::reload_history));
205
calendar->signal_prev_month().connect(sigc::mem_fun(*this, &HistoryViewer::reload_history));
206
calendar->signal_next_month().connect(sigc::mem_fun(*this, &HistoryViewer::reload_history));
207
calendar->signal_prev_year().connect(sigc::mem_fun(*this, &HistoryViewer::reload_history));
208
calendar->signal_next_year().connect(sigc::mem_fun(*this, &HistoryViewer::reload_history));
209
Glib::DateTime datetime = Glib::DateTime::create_now_local();
210
calendar->select_day(datetime);
211
212
this->signal_pane_shown.connect([this]() {
213
// Load the current date
214
reload_history();
215
});
216
217
auto scrolled_window = Gtk::make_managed<Gtk::ScrolledWindow>();
218
box_place = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0);
219
auto viewport = Gtk::make_managed<Gtk::Viewport>(nullptr, nullptr);
220
viewport->set_child(*box_place);
221
scrolled_window->set_child(*viewport);
222
scrolled_window->set_policy(Gtk::PolicyType::AUTOMATIC, Gtk::PolicyType::AUTOMATIC);
223
scrolled_window->set_vexpand(true);
224
scrolled_window->set_hexpand(true);
225
box->append(*scrolled_window);
226
227
// TODO: allow deleting entries
228
}
229
230
void reload_history() {
231
Glib::DateTime calendar_day = calendar->get_date();
232
Glib::DateTime datetime = Glib::DateTime::create_local(
233
calendar_day.get_year(),
234
calendar_day.get_month(),
235
calendar_day.get_day_of_month(),
236
0, 0, 0
237
);
238
date_label->set_text(datetime.format("%x"));
239
auto box = Gtk::make_managed<Gtk::ListBox>();
240
auto history_entries = history_manager->get_history_between(datetime.to_unix(), datetime.to_unix() + 86400);
241
for(const auto &entry : history_entries) {
242
auto row = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 4);
243
auto title = Gtk::make_managed<Gtk::Label>(entry.title.empty() ? "Untitled" : entry.title);
244
title->set_halign(Gtk::Align::START);
245
title->set_ellipsize(Pango::EllipsizeMode::MIDDLE);
246
auto label = Gtk::make_managed<Gtk::Label>(entry.url);
247
label->set_halign(Gtk::Align::START);
248
label->set_ellipsize(Pango::EllipsizeMode::MIDDLE);
249
row->append(*title);
250
row->append(*label);
251
if(auto time = Glib::DateTime::create_now_local(entry.timestamp)) {
252
label->set_tooltip_text(time.format("%c"));
253
box->append(*row);
254
}
255
}
256
if(box_place->get_first_child()) {
257
box_place->remove(*box_place->get_first_child());
258
}
259
box->signal_row_activated().connect([this](Gtk::ListBoxRow *row) {
260
if(auto box_row = dynamic_cast<Gtk::Box*>(row->get_child())) {
261
if(auto label = dynamic_cast<Gtk::Label*>(box_row->get_last_child())) {
262
auto url = label->get_text();
263
if(!url.empty()) {
264
signal_open_url.emit(url);
265
}
266
}
267
}
268
});
269
box_place->append(*box);
270
}
271
};
272
273
class PantheraWindow;
274
275
class PantheraWww : public Gtk::Application {
276
private:
277
std::shared_ptr<gPanthera::ContentManager> content_manager;
278
std::string cookie_file;
279
std::vector<SearchEngine> search_engines;
280
SearchEngine* default_search_engine;
281
std::shared_ptr<HistoryManager> history_manager;
282
HistoryViewer* history_viewer;
283
Glib::RefPtr<Gio::Menu> main_menu;
284
std::string new_tab_page;
285
286
static void load_change_callback(WebKitWebView* object, WebKitLoadEvent load_event, gpointer data);
287
static void notify_callback(GObject* object, GParamSpec* pspec, gpointer data);
288
static void notify_focused_callback(GObject* object, GParamSpec* pspec, gpointer data);
289
static void on_back_pressed(GtkGestureClick* gesture, int n_press, double x, double y, gpointer user_data);
290
static void on_forward_pressed(GtkGestureClick* gesture, int n_press, double x, double y, gpointer user_data);
291
static gboolean on_decide_policy(WebKitWebView* source, WebKitPolicyDecision* decision, WebKitPolicyDecisionType type, gpointer user_data);
292
293
protected:
294
void on_startup() override;
295
void on_activate() override;
296
PantheraWindow *make_window();
297
298
public:
299
PantheraWww();
300
friend class PantheraWindow;
301
void on_new_tab(gPanthera::ContentStack* stack, const Glib::ustring& url = "", bool focus = true, bool new_window = false);
302
void set_search_engines_from_json(const std::string& json_string);
303
static Glib::RefPtr<PantheraWww> create();
304
std::shared_ptr<gPanthera::ContentManager> get_content_manager();
305
void load_plugin(const std::string& plugin_path);
306
};
307
308
class PantheraWindow : public Gtk::ApplicationWindow {
309
private:
310
std::shared_ptr<gPanthera::LayoutManager> layout_manager;
311
Gtk::Entry *url_bar;
312
public:
313
friend class PantheraWww;
314
explicit PantheraWindow(Glib::RefPtr<Gtk::Application> const &application) : Gtk::ApplicationWindow(application) {
315
auto panthera = dynamic_cast<PantheraWww*>(application.get());
316
if(!panthera) {
317
throw std::runtime_error("Application is not a PantheraWww instance");
318
}
319
this->set_default_size(800, 600);
320
layout_manager = std::make_shared<gPanthera::LayoutManager>();
321
322
auto dock_stack_1 = Gtk::make_managed<gPanthera::DockStack>(layout_manager, "One", "one");
323
auto switcher_1 = Gtk::make_managed<gPanthera::DockStackSwitcher>(dock_stack_1, Gtk::Orientation::HORIZONTAL);
324
auto dock_stack_2 = Gtk::make_managed<gPanthera::DockStack>(layout_manager, "Two", "two");
325
auto switcher_2 = Gtk::make_managed<gPanthera::DockStackSwitcher>(dock_stack_2, Gtk::Orientation::VERTICAL);
326
auto pane_1_content = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0);
327
auto debug_button = Gtk::make_managed<Gtk::Button>("Debug");
328
auto print_history_button = Gtk::make_managed<Gtk::Button>("Print history");
329
print_history_button->signal_clicked().connect([this, panthera]() {
330
auto history = panthera->history_manager->get_history();
331
for(const auto &entry : history) {
332
std::cout << "entry " << entry.id << ", url " << entry.url << ", unix time " << entry.timestamp << std::endl;
333
// TODO: exclude redirecting URLs
334
}
335
});
336
pane_1_content->append(*debug_button);
337
pane_1_content->append(*print_history_button);
338
auto pane_1_icon = Gtk::make_managed<Gtk::Image>(Gio::Icon::create("help-about-symbolic"));
339
auto pane_1 = Gtk::make_managed<gPanthera::DockablePane>(layout_manager, *pane_1_content, "debug", "Debugging options", pane_1_icon);
340
341
dock_stack_1->set_transition_type(Gtk::StackTransitionType::SLIDE_LEFT_RIGHT);
342
dock_stack_1->set_transition_duration(125);
343
dock_stack_1->set_expand(true);
344
dock_stack_2->set_transition_type(Gtk::StackTransitionType::SLIDE_UP_DOWN);
345
dock_stack_2->set_transition_duration(125);
346
dock_stack_2->set_expand(true);
347
348
auto outer_grid = Gtk::make_managed<Gtk::Grid>();
349
outer_grid->attach(*switcher_2, 0, 2, 1, 1);
350
outer_grid->attach(*switcher_1, 1, 3, 1, 1);
351
auto outer_paned = Gtk::make_managed<Gtk::Paned>(Gtk::Orientation::HORIZONTAL);
352
outer_paned->set_start_child(*dock_stack_2);
353
auto inner_paned = Gtk::make_managed<Gtk::Paned>(Gtk::Orientation::VERTICAL);
354
auto content = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0);
355
panthera->content_manager = std::make_shared<gPanthera::ContentManager>();
356
std::function<bool(gPanthera::ContentPage*)> detach_handler;
357
detach_handler = [](gPanthera::ContentPage *widget) {
358
auto new_stack = Gtk::make_managed<gPanthera::ContentStack>(widget->content_manager, widget->get_stack()->get_detach_handler());
359
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());
360
auto new_notebook = Gtk::make_managed<gPanthera::ContentNotebook>(new_stack, new_switcher);
361
auto window = new gPanthera::ContentWindow(new_notebook);
362
widget->redock(new_stack);
363
window->present();
364
new_stack->signal_leave_empty.connect([window]() {
365
window->close();
366
delete window;
367
});
368
return true;
369
};
370
371
auto return_extra_child = [this, panthera](gPanthera::ContentTabBar *switcher) {
372
auto new_tab_button = Gtk::make_managed<Gtk::Button>();
373
new_tab_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("list-add-symbolic")));
374
new_tab_button->set_tooltip_text("New tab");
375
new_tab_button->signal_clicked().connect([this, switcher, panthera]() {
376
panthera->on_new_tab(switcher->get_stack());
377
});
378
return new_tab_button;
379
};
380
auto content_stack = Gtk::make_managed<gPanthera::ContentStack>(panthera->content_manager, detach_handler);
381
auto content_stack_switcher = Gtk::make_managed<gPanthera::ContentTabBar>(content_stack, Gtk::Orientation::HORIZONTAL, return_extra_child);
382
panthera->content_manager->add_stack(content_stack);
383
WebKitWebView *webview = WEBKIT_WEB_VIEW(webkit_web_view_new()); // for some reason, this has to be created
384
content->set_name("content_box");
385
auto content_notebook = Gtk::make_managed<gPanthera::ContentNotebook>(content_stack, content_stack_switcher, Gtk::PositionType::TOP);
386
content->append(*content_notebook);
387
inner_paned->set_start_child(*content);
388
inner_paned->set_end_child(*dock_stack_1);
389
outer_paned->set_end_child(*inner_paned);
390
outer_grid->attach(*outer_paned, 1, 2, 1, 1);
391
// Create the toolbar
392
auto main_toolbar = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 8);
393
// URL bar
394
url_bar = Gtk::make_managed<Gtk::Entry>();
395
url_bar->set_placeholder_text("Enter URL");
396
url_bar->set_hexpand(true);
397
auto load_url_callback = [this, panthera]() {
398
auto page = panthera->content_manager->get_last_operated_page();
399
bool has_protocol = url_bar->get_text().find("://") != std::string::npos;
400
if(!has_protocol) {
401
url_bar->set_text("http://" + url_bar->get_text());
402
}
403
if(page) {
404
if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) {
405
webkit_web_view_load_uri(webview, url_bar->get_text().c_str());
406
}
407
}
408
};
409
// Go
410
auto go_button = Gtk::make_managed<Gtk::Button>("Go");
411
go_button->signal_clicked().connect(load_url_callback);
412
url_bar->signal_activate().connect(load_url_callback);
413
panthera->content_manager->signal_page_operated.connect([this, panthera](gPanthera::ContentPage *page) {
414
if(!page->get_child()) {
415
return;
416
}
417
if(!page->get_child()->get_first_child()) {
418
return;
419
}
420
url_bar->set_text(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())));
421
guint url_update_handler = g_signal_connect(page->get_child()->get_first_child()->gobj(), "notify", G_CALLBACK(panthera->notify_focused_callback), this);
422
std::shared_ptr<sigc::connection> control_signal_handler = std::make_shared<sigc::connection>();
423
*control_signal_handler = page->signal_control_status_changed.connect([this, page, control_signal_handler, url_update_handler](bool controlled) {
424
if(!controlled) {
425
control_signal_handler->disconnect();
426
if(page->get_child() && page->get_child()->get_first_child() && WEBKIT_IS_WEB_VIEW(page->get_child()->get_first_child()->gobj())) {
427
g_signal_handler_disconnect(page->get_child()->get_first_child()->gobj(), url_update_handler);
428
}
429
}
430
});
431
});
432
panthera->history_manager = std::make_shared<HistoryManager>("history.db");
433
panthera->history_viewer = Gtk::make_managed<HistoryViewer>(layout_manager, panthera->history_manager);
434
panthera->history_viewer->signal_open_url.connect([this, panthera](const std::string &url) {
435
panthera->on_new_tab(nullptr, url, true);
436
});
437
// Back, forward, reload
438
auto back_button = Gtk::make_managed<Gtk::Button>();
439
back_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-previous-symbolic")));
440
back_button->set_tooltip_text("Back");
441
auto forward_button = Gtk::make_managed<Gtk::Button>();
442
forward_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-next-symbolic")));
443
forward_button->set_tooltip_text("Forward");
444
auto reload_button = Gtk::make_managed<Gtk::Button>();
445
reload_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("view-refresh-symbolic")));
446
reload_button->set_tooltip_text("Reload");
447
back_button->signal_clicked().connect([this, panthera]() {
448
auto page = panthera->content_manager->get_last_operated_page();
449
if(page) {
450
if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) {
451
webkit_web_view_go_back(webview);
452
}
453
}
454
});
455
forward_button->signal_clicked().connect([this, panthera]() {
456
auto page = panthera->content_manager->get_last_operated_page();
457
if(page) {
458
if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) {
459
webkit_web_view_go_forward(webview);
460
}
461
}
462
});
463
reload_button->signal_clicked().connect([this, panthera]() {
464
auto page = panthera->content_manager->get_last_operated_page();
465
if(page) {
466
if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) {
467
webkit_web_view_reload(webview);
468
}
469
}
470
});
471
// Search bar
472
// TODO: provide history, provide a menu button with the other engines
473
auto search_bar = Gtk::make_managed<Gtk::Entry>();
474
search_bar->set_placeholder_text("Search");
475
search_bar->set_hexpand(true);
476
if(panthera->search_engines.empty()) {
477
search_bar->set_sensitive(false);
478
search_bar->set_placeholder_text("No search");
479
}
480
auto search_callback = [this, search_bar, content_stack, panthera]() {
481
// Create a new tab with the search results
482
if(panthera->content_manager->get_last_operated_page()) {
483
panthera->on_new_tab(nullptr);
484
} else {
485
panthera->on_new_tab(content_stack);
486
}
487
auto page = panthera->content_manager->get_last_operated_page();
488
if(page) {
489
if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) {
490
auto search_url = panthera->default_search_engine->get_search_url(search_bar->get_text());
491
webkit_web_view_load_uri(webview, search_url.c_str());
492
}
493
}
494
};
495
if(panthera->default_search_engine) {
496
search_bar->signal_activate().connect(search_callback);
497
}
498
499
// Assemble the toolbar
500
main_toolbar->append(*back_button);
501
main_toolbar->append(*forward_button);
502
main_toolbar->append(*reload_button);
503
main_toolbar->append(*url_bar);
504
main_toolbar->append(*go_button);
505
main_toolbar->append(*search_bar);
506
if(panthera->default_search_engine) {
507
auto search_button = Gtk::make_managed<Gtk::Button>(panthera->default_search_engine->name);
508
search_button->signal_clicked().connect(search_callback);
509
main_toolbar->append(*search_button);
510
}
511
outer_grid->attach(*main_toolbar, 0, 1, 2, 1);
512
this->set_child(*outer_grid);
513
debug_button->signal_clicked().connect([this, panthera]() {
514
if(panthera->content_manager->get_last_operated_page()) {
515
std::cout << "Last operated page: " << panthera->content_manager->get_last_operated_page()->get_name() << std::endl;
516
} else {
517
std::cout << "No page operated!" << std::endl;
518
}
519
});
520
// TODO: Use the last operated page and allow opening tabs next to the last operated page using certain panes
521
// Load the existing layout, if it exists
522
std::ifstream layout_file_in("layout.json");
523
if(layout_file_in) {
524
std::string layout_json((std::istreambuf_iterator<char>(layout_file_in)), std::istreambuf_iterator<char>());
525
layout_file_in.close();
526
layout_manager->restore_json_layout(layout_json);
527
} else {
528
// Create a new layout if the file doesn't exist
529
layout_file_in.close();
530
531
dock_stack_1->add_pane(*pane_1);
532
dock_stack_1->add_pane(*panthera->history_viewer);
533
534
std::ofstream layout_file_out("layout.json");
535
layout_file_out << layout_manager->get_layout_as_json();
536
layout_file_out.close();
537
}
538
// Save the layout when changed
539
layout_manager->signal_pane_moved.connect([this, panthera](gPanthera::DockablePane *pane) {
540
std::ofstream layout_file_out("layout.json");
541
layout_file_out << layout_manager->get_layout_as_json();
542
layout_file_out.close();
543
std::cout << "Layout changed: " << layout_manager->get_layout_as_json() << std::endl;
544
});
545
set_show_menubar(true);
546
}
547
};
548
549
void PantheraWww::load_change_callback(WebKitWebView *object, WebKitLoadEvent load_event, gpointer data) {
550
if(auto self = static_cast<PantheraWww*>(data)) {
551
if(load_event == WEBKIT_LOAD_COMMITTED) {
552
if(webkit_web_view_get_title(WEBKIT_WEB_VIEW(object)) == nullptr) {
553
self->history_manager->log_url(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(object)), "");
554
} else {
555
self->history_manager->log_url(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(object)),
556
webkit_web_view_get_title(WEBKIT_WEB_VIEW(object)));
557
}
558
if(self->history_viewer->get_visible()) {
559
self->history_viewer->reload_history();
560
}
561
}
562
}
563
}
564
565
void PantheraWww::notify_callback(GObject *object, GParamSpec *pspec, gpointer data) {
566
if(!gtk_widget_get_parent(GTK_WIDGET(object))) {
567
return;
568
}
569
if(auto self = static_cast<PantheraWww*>(data)) {
570
auto parent = gtk_widget_get_parent(gtk_widget_get_parent(GTK_WIDGET(object)));
571
if(auto page = dynamic_cast<gPanthera::ContentPage*>(Glib::wrap(parent))) {
572
if(g_strcmp0(pspec->name, "title") == 0) {
573
if(auto label = dynamic_cast<Gtk::Label*>(page->tab_widget->get_last_child())) {
574
if(strlen(webkit_web_view_get_title(WEBKIT_WEB_VIEW(object))) == 0) {
575
label->set_label("Untitled");
576
} else {
577
label->set_label(webkit_web_view_get_title(WEBKIT_WEB_VIEW(object)));
578
}
579
}
580
} else if(g_strcmp0(pspec->name, "favicon") == 0) {
581
// Update favicons
582
if(auto image = dynamic_cast<Gtk::Image*>(page->tab_widget->get_first_child())) {
583
image->set_from_icon_name("image-loading-symbolic");
584
if(auto favicon = webkit_web_view_get_favicon(WEBKIT_WEB_VIEW(object))) {
585
gtk_image_set_from_paintable(image->gobj(), GDK_PAINTABLE(favicon));
586
}
587
}
588
}
589
}
590
}
591
}
592
593
void PantheraWww::notify_focused_callback(GObject *object, GParamSpec *pspec, gpointer data) {
594
if(!gtk_widget_get_parent(GTK_WIDGET(object))) {
595
return;
596
}
597
auto this_ = static_cast<PantheraWww*>(data);
598
auto parent = gtk_widget_get_parent(gtk_widget_get_parent(GTK_WIDGET(object)));
599
if(auto page = dynamic_cast<gPanthera::ContentPage*>(Glib::wrap(parent))) {
600
if(g_strcmp0(pspec->name, "uri") == 0) {
601
if(auto main_window = dynamic_cast<PantheraWindow*>(page->get_root())) {
602
main_window->url_bar->set_text(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(object)));
603
}
604
}
605
}
606
}
607
608
void PantheraWww::on_back_pressed(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) {
609
WebKitWebView *webview = WEBKIT_WEB_VIEW(user_data);
610
webkit_web_view_go_back(webview);
611
}
612
613
void PantheraWww::on_forward_pressed(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) {
614
WebKitWebView *webview = WEBKIT_WEB_VIEW(user_data);
615
webkit_web_view_go_forward(webview);
616
}
617
618
gboolean PantheraWww::on_decide_policy(WebKitWebView *source, WebKitPolicyDecision *decision, WebKitPolicyDecisionType type, gpointer user_data) {
619
if(auto self = static_cast<PantheraWww*>(user_data)) {
620
if(type == WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION) {
621
auto action = webkit_navigation_policy_decision_get_navigation_action(WEBKIT_NAVIGATION_POLICY_DECISION(decision));
622
Glib::ustring url = Glib::ustring(webkit_uri_request_get_uri(webkit_navigation_action_get_request(action)));
623
if(!starts_with(url, "about:") && url.find("://") == Glib::ustring::npos) {
624
// It is a special scheme (mailto:, tel: etc.)
625
try {
626
Gio::AppInfo::launch_default_for_uri(url);
627
} catch(const Glib::Error &exception) {
628
std::cerr << "Failed to launch special URI: " << exception.what() << std::endl;
629
}
630
webkit_policy_decision_ignore(decision);
631
return true;
632
}
633
if(webkit_navigation_action_get_mouse_button(action) == 2) {
634
// Middle-click opens in a new window
635
self->on_new_tab(nullptr, url, false);
636
webkit_policy_decision_ignore(decision);
637
return true;
638
}
639
} else if(type == WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION) {
640
auto action = webkit_navigation_policy_decision_get_navigation_action(WEBKIT_NAVIGATION_POLICY_DECISION(decision));
641
self->on_new_tab(nullptr, webkit_uri_request_get_uri(webkit_navigation_action_get_request(action)), false, true);
642
return true;
643
}
644
}
645
return false;
646
}
647
648
void PantheraWww::on_new_tab(gPanthera::ContentStack *stack, const Glib::ustring &url, bool focus, bool new_window) {
649
if(!stack) {
650
// Find the current area
651
stack = content_manager->get_last_operated_page()->get_stack();
652
}
653
Glib::ustring url_ = url;
654
if(url.empty()) {
655
url_ = new_tab_page;
656
}
657
658
WebKitWebView *webview = WEBKIT_WEB_VIEW(webkit_web_view_new());
659
gtk_widget_set_hexpand(GTK_WIDGET(webview), true);
660
gtk_widget_set_vexpand(GTK_WIDGET(webview), true);
661
auto page_content = Gtk::make_managed<Gtk::Box>();
662
gtk_box_append(page_content->gobj(), GTK_WIDGET(webview));
663
auto page_tab = new Gtk::Box();
664
page_tab->set_orientation(Gtk::Orientation::HORIZONTAL);
665
auto initial_icon = Gtk::make_managed<Gtk::Image>(Gio::Icon::create("image-loading-symbolic"));
666
page_tab->append(*initial_icon);
667
page_tab->append(*Gtk::make_managed<Gtk::Label>("Untitled"));
668
page_tab->set_spacing(4);
669
auto page = Gtk::make_managed<gPanthera::ContentPage>(content_manager, stack, page_content, page_tab);
670
g_signal_connect(webview, "notify", G_CALLBACK(notify_callback), this);
671
g_signal_connect(webview, "load-changed", G_CALLBACK(load_change_callback), this);
672
g_signal_connect(webview, "decide-policy", G_CALLBACK(on_decide_policy), this);
673
webkit_web_view_load_uri(webview, url_.data());
674
auto cookie_manager = webkit_network_session_get_cookie_manager(webkit_web_view_get_network_session(webview));
675
webkit_cookie_manager_set_persistent_storage(cookie_manager, cookie_file.c_str(), WEBKIT_COOKIE_PERSISTENT_STORAGE_TEXT);
676
webkit_cookie_manager_set_accept_policy(cookie_manager, WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS);
677
auto website_data_manager = webkit_network_session_get_website_data_manager(webkit_web_view_get_network_session(webview));
678
webkit_website_data_manager_set_favicons_enabled(website_data_manager, true);
679
GtkEventController *click_controller_back = GTK_EVENT_CONTROLLER(gtk_gesture_click_new());
680
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click_controller_back), 8);
681
g_signal_connect(click_controller_back, "pressed", G_CALLBACK(on_back_pressed), webview);
682
gtk_widget_add_controller(GTK_WIDGET(webview), click_controller_back);
683
684
GtkEventController *click_controller_forward = GTK_EVENT_CONTROLLER(gtk_gesture_click_new());
685
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click_controller_forward), 9);
686
g_signal_connect(click_controller_forward, "pressed", G_CALLBACK(on_forward_pressed), webview);
687
gtk_widget_add_controller(GTK_WIDGET(webview), click_controller_forward);
688
689
stack->add_page(*page);
690
if(focus) {
691
stack->set_visible_child(*page);
692
content_manager->set_last_operated_page(page);
693
}
694
if(new_window) {
695
bool result = stack->signal_detach.emit(page);
696
}
697
}
698
699
PantheraWindow *PantheraWww::make_window() {
700
Glib::RefPtr<Gtk::Application> self_ref = Glib::make_refptr_for_instance<Gtk::Application>(this);
701
auto window = new PantheraWindow(self_ref);
702
add_window(*window);
703
return window;
704
}
705
706
void PantheraWww::on_startup() {
707
Gtk::Application::on_startup();
708
// Get search engines
709
std::ifstream search_engines_file_in("search_engines.json");
710
if(search_engines_file_in) {
711
std::string search_engines_json((std::istreambuf_iterator<char>(search_engines_file_in)), std::istreambuf_iterator<char>());
712
search_engines_file_in.close();
713
set_search_engines_from_json(search_engines_json);
714
} else {
715
search_engines_file_in.close();
716
auto empty_json = nlohmann::json::array();
717
std::ofstream search_engines_file_out("search_engines.json");
718
search_engines_file_out << empty_json.dump(4);
719
}
720
// Load settings
721
new_tab_page = "about:blank";
722
std::ifstream settings_file_in("settings.json");
723
if(settings_file_in) {
724
std::string settings_json((std::istreambuf_iterator<char>(settings_file_in)), std::istreambuf_iterator<char>());
725
settings_file_in.close();
726
auto json = nlohmann::json::parse(settings_json);
727
if(json.contains("ntp")) {
728
new_tab_page = json["ntp"].get<std::string>();
729
}
730
} else {
731
settings_file_in.close();
732
auto empty_json = nlohmann::json::object();
733
empty_json["ntp"] = "about:blank";
734
std::ofstream settings_file_out("settings.json");
735
settings_file_out << empty_json.dump(4);
736
settings_file_out.close();
737
}
738
739
// Load plugins
740
std::ifstream plugin_file_in("plugins.json");
741
if(plugin_file_in) {
742
std::string plugin_json((std::istreambuf_iterator<char>(plugin_file_in)), std::istreambuf_iterator<char>());
743
plugin_file_in.close();
744
auto json = nlohmann::json::parse(plugin_json);
745
for(auto &plugin : json) {
746
auto plugin_path = plugin["path"].get<std::string>();
747
load_plugin(plugin_path);
748
}
749
} else {
750
plugin_file_in.close();
751
auto empty_json = nlohmann::json::array();
752
std::ofstream plugin_file_out("plugins.json");
753
plugin_file_out << empty_json.dump(4);
754
plugin_file_out.close();
755
}
756
757
// Known bug <https://gitlab.gnome.org/GNOME/epiphany/-/issues/2714>:
758
// JS can't veto the application's accelerators using `preventDefault()`. This is
759
// not possible in WebKitGTK, or at least I can't find a way to do it.
760
761
main_menu = Gio::Menu::create();
762
// File
763
auto file_menu = Gio::Menu::create();
764
// New tab
765
auto new_tab_action = Gio::SimpleAction::create("new_tab");
766
new_tab_action->signal_activate().connect([this](const Glib::VariantBase&) {
767
on_new_tab(nullptr);
768
});
769
add_action(new_tab_action);
770
set_accels_for_action("app.new_tab", {"<Primary>T"});
771
file_menu->append("New tab", "app.new_tab");
772
// Close tab
773
auto close_tab_action = Gio::SimpleAction::create("close_tab");
774
close_tab_action->signal_activate().connect([this](const Glib::VariantBase&) {
775
auto page = content_manager->get_last_operated_page();
776
if(page) {
777
page->close();
778
}
779
});
780
add_action(close_tab_action);
781
set_accels_for_action("app.close_tab", {"<Primary>W"});
782
file_menu->append("Close tab", "app.close_tab");
783
main_menu->append_submenu("File", file_menu);
784
set_menubar(main_menu);
785
}
786
787
void PantheraWww::on_activate() {
788
auto window = make_window();
789
window->present();
790
}
791
792
void PantheraWww::set_search_engines_from_json(const std::string &json_string) {
793
auto json = nlohmann::json::parse(json_string);
794
Glib::ustring default_search_engine_name;
795
for(auto &engine : json) {
796
SearchEngine search_engine;
797
search_engine.name = engine["name"].get<std::string>();
798
search_engine.url = engine["url"].get<std::string>();
799
search_engines.push_back(search_engine);
800
if(engine.contains("default") && engine["default"].get<bool>()) {
801
default_search_engine_name = engine["name"].get<std::string>();
802
}
803
}
804
for(auto &search_engine : search_engines) {
805
if(search_engine.name == default_search_engine_name) {
806
default_search_engine = &search_engine;
807
break;
808
}
809
}
810
}
811
812
PantheraWww::PantheraWww() : Gtk::Application("com.roundabout_host.roundabout.panthera_www", Gio::Application::Flags::NONE) {
813
}
814
815
Glib::RefPtr<PantheraWww> PantheraWww::create() {
816
return Glib::make_refptr_for_instance<PantheraWww>(new PantheraWww());
817
}
818
819
void PantheraWww::load_plugin(const std::string &plugin_path) {
820
void *handle = dlopen(plugin_path.c_str(), RTLD_LAZY | RTLD_GLOBAL);
821
if(!handle) {
822
std::cerr << "Failed to load plugin: " << dlerror() << std::endl;
823
return;
824
}
825
auto entrypoint = (plugin_entrypoint_type)dlsym(handle, "plugin_init");
826
if(!entrypoint) {
827
std::cerr << "Failed to find entrypoint in plugin: " << dlerror() << std::endl;
828
dlclose(handle);
829
return;
830
}
831
entrypoint();
832
}
833
834
Glib::RefPtr<PantheraWww> app = nullptr;
835
836
extern "C" {
837
void panthera_log(const char *message) {
838
std::cerr << message << std::endl;
839
}
840
841
void panthera_add_pane(const char *id, const char *label, GtkImage *icon, GtkWidget *pane) {
842
auto pane_child = Glib::wrap(pane);
843
auto icon_cc = Glib::wrap(icon);
844
auto label_cc = Glib::ustring(label);
845
auto id_cc = Glib::ustring(id);
846
//auto new_pane = Gtk::make_managed<gPanthera::DockablePane>(app->get_layout_manager(), *pane_child, id_cc, label_cc, icon_cc);
847
// TODO: Port to multi-window
848
//app->get_layout_manager()->add_pane(new_pane);
849
}
850
}
851
852
int main(int argc, char *argv[]) {
853
gPanthera::init();
854
app = PantheraWww::create();
855
return app->run(argc, argv);
856
}
857