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