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.67 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
#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
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
HistoryViewer *history_viewer;
313
gPanthera::ContentPage *controlled_page = nullptr;
314
public:
315
friend class PantheraWww;
316
explicit PantheraWindow(Glib::RefPtr<Gtk::Application> const &application) : Gtk::ApplicationWindow(application) {
317
auto panthera = dynamic_cast<PantheraWww*>(application.get());
318
if(!panthera) {
319
throw std::runtime_error("Application is not a PantheraWww instance");
320
}
321
this->set_default_size(800, 600);
322
layout_manager = std::make_shared<gPanthera::LayoutManager>();
323
324
auto dock_stack_1 = Gtk::make_managed<gPanthera::DockStack>(layout_manager, _("One"), "one");
325
auto switcher_1 = Gtk::make_managed<gPanthera::DockStackSwitcher>(dock_stack_1, Gtk::Orientation::HORIZONTAL);
326
auto dock_stack_2 = Gtk::make_managed<gPanthera::DockStack>(layout_manager, _("Two"), "two");
327
auto switcher_2 = Gtk::make_managed<gPanthera::DockStackSwitcher>(dock_stack_2, Gtk::Orientation::VERTICAL);
328
auto pane_1_content = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0);
329
auto debug_button = Gtk::make_managed<Gtk::Button>("Debug");
330
auto print_history_button = Gtk::make_managed<Gtk::Button>("Print history");
331
print_history_button->signal_clicked().connect([this, panthera]() {
332
auto history = panthera->history_manager->get_history();
333
for(const auto &entry : history) {
334
std::cout << "entry " << entry.id << ", url " << entry.url << ", unix time " << entry.timestamp << std::endl;
335
// TODO: exclude redirecting URLs
336
}
337
});
338
pane_1_content->append(*debug_button);
339
pane_1_content->append(*print_history_button);
340
auto pane_1_icon = Gtk::make_managed<Gtk::Image>(Gio::Icon::create("help-about-symbolic"));
341
auto pane_1 = Gtk::make_managed<gPanthera::DockablePane>(layout_manager, *pane_1_content, "debug", _("Debugging options"), pane_1_icon);
342
343
dock_stack_1->set_transition_type(Gtk::StackTransitionType::SLIDE_LEFT_RIGHT);
344
dock_stack_1->set_transition_duration(125);
345
dock_stack_1->set_expand(true);
346
dock_stack_2->set_transition_type(Gtk::StackTransitionType::SLIDE_UP_DOWN);
347
dock_stack_2->set_transition_duration(125);
348
dock_stack_2->set_expand(true);
349
350
auto outer_grid = Gtk::make_managed<Gtk::Grid>();
351
outer_grid->attach(*switcher_2, 0, 2, 1, 1);
352
outer_grid->attach(*switcher_1, 1, 3, 1, 1);
353
auto outer_paned = Gtk::make_managed<Gtk::Paned>(Gtk::Orientation::HORIZONTAL);
354
outer_paned->set_start_child(*dock_stack_2);
355
auto inner_paned = Gtk::make_managed<Gtk::Paned>(Gtk::Orientation::VERTICAL);
356
auto content = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0);
357
panthera->content_manager = std::make_shared<gPanthera::ContentManager>();
358
std::function<bool(gPanthera::ContentPage*)> detach_handler;
359
detach_handler = [](gPanthera::ContentPage *widget) {
360
auto new_stack = Gtk::make_managed<gPanthera::ContentStack>(widget->content_manager, widget->get_stack()->get_detach_handler());
361
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());
362
auto new_notebook = Gtk::make_managed<gPanthera::ContentNotebook>(new_stack, new_switcher);
363
auto window = new gPanthera::ContentWindow(new_notebook);
364
widget->redock(new_stack);
365
window->present();
366
new_stack->signal_leave_empty.connect([window]() {
367
window->close();
368
delete window;
369
});
370
return true;
371
};
372
373
auto return_extra_child = [this, panthera](gPanthera::ContentTabBar *switcher) {
374
auto new_tab_button = Gtk::make_managed<Gtk::Button>();
375
new_tab_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("list-add-symbolic")));
376
new_tab_button->set_tooltip_text("New tab");
377
new_tab_button->signal_clicked().connect([this, switcher, panthera]() {
378
panthera->on_new_tab(switcher->get_stack());
379
});
380
return new_tab_button;
381
};
382
auto content_stack = Gtk::make_managed<gPanthera::ContentStack>(panthera->content_manager, detach_handler);
383
auto content_stack_switcher = Gtk::make_managed<gPanthera::ContentTabBar>(content_stack, Gtk::Orientation::HORIZONTAL, return_extra_child);
384
panthera->content_manager->add_stack(content_stack);
385
WebKitWebView *webview = WEBKIT_WEB_VIEW(webkit_web_view_new()); // for some reason, this has to be created
386
content->set_name("content_box");
387
auto content_notebook = Gtk::make_managed<gPanthera::ContentNotebook>(content_stack, content_stack_switcher, Gtk::PositionType::TOP);
388
content->append(*content_notebook);
389
inner_paned->set_start_child(*content);
390
inner_paned->set_end_child(*dock_stack_1);
391
outer_paned->set_end_child(*inner_paned);
392
outer_grid->attach(*outer_paned, 1, 2, 1, 1);
393
// Create the toolbar
394
auto main_toolbar = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 8);
395
// URL bar
396
url_bar = Gtk::make_managed<Gtk::Entry>();
397
url_bar->set_placeholder_text(_("Enter URL"));
398
url_bar->set_hexpand(true);
399
auto load_url_callback = [this, panthera]() {
400
bool has_protocol = url_bar->get_text().find("://") != std::string::npos;
401
if(!has_protocol) {
402
url_bar->set_text("http://" + url_bar->get_text());
403
}
404
if(controlled_page) {
405
if(auto webview = WEBKIT_WEB_VIEW(controlled_page->get_child()->get_first_child()->gobj())) {
406
webkit_web_view_load_uri(webview, url_bar->get_text().c_str());
407
}
408
}
409
};
410
// Go
411
auto go_button = Gtk::make_managed<Gtk::Button>("Go");
412
go_button->signal_clicked().connect(load_url_callback);
413
url_bar->signal_activate().connect(load_url_callback);
414
panthera->content_manager->signal_page_operated.connect([this, panthera](gPanthera::ContentPage *page) {
415
if(!page->get_child()) {
416
return;
417
}
418
if(!page->get_child()->get_first_child()) {
419
return;
420
}
421
if(page->get_root() != this) {
422
// The page is in some other window
423
return;
424
}
425
controlled_page = page;
426
url_bar->set_text(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())));
427
guint url_update_handler = g_signal_connect(page->get_child()->get_first_child()->gobj(), "notify", G_CALLBACK(panthera->notify_focused_callback), this);
428
std::shared_ptr<sigc::connection> control_signal_handler = std::make_shared<sigc::connection>();
429
*control_signal_handler = page->signal_control_status_changed.connect([this, page, control_signal_handler, url_update_handler](bool controlled) {
430
if(!controlled) {
431
control_signal_handler->disconnect();
432
if(page->get_child() && page->get_child()->get_first_child() && WEBKIT_IS_WEB_VIEW(page->get_child()->get_first_child()->gobj())) {
433
g_signal_handler_disconnect(page->get_child()->get_first_child()->gobj(), url_update_handler);
434
}
435
}
436
});
437
});
438
history_viewer = Gtk::make_managed<HistoryViewer>(layout_manager, panthera->history_manager);
439
history_viewer->signal_open_url.connect([this, panthera](const std::string &url) {
440
panthera->on_new_tab(nullptr, url, true);
441
});
442
// Back, forward, reload
443
auto back_button = Gtk::make_managed<Gtk::Button>();
444
back_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-previous-symbolic")));
445
back_button->set_tooltip_text("Back");
446
auto forward_button = Gtk::make_managed<Gtk::Button>();
447
forward_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-next-symbolic")));
448
forward_button->set_tooltip_text("Forward");
449
auto reload_button = Gtk::make_managed<Gtk::Button>();
450
reload_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("view-refresh-symbolic")));
451
reload_button->set_tooltip_text("Reload");
452
back_button->signal_clicked().connect([this, panthera]() {
453
if(controlled_page) {
454
if(auto webview = WEBKIT_WEB_VIEW(controlled_page->get_child()->get_first_child()->gobj())) {
455
webkit_web_view_go_back(webview);
456
}
457
}
458
});
459
forward_button->signal_clicked().connect([this, panthera]() {
460
if(controlled_page) {
461
if(auto webview = WEBKIT_WEB_VIEW(controlled_page->get_child()->get_first_child()->gobj())) {
462
webkit_web_view_go_forward(webview);
463
}
464
}
465
});
466
reload_button->signal_clicked().connect([this, panthera]() {
467
if(controlled_page) {
468
if(auto webview = WEBKIT_WEB_VIEW(controlled_page->get_child()->get_first_child()->gobj())) {
469
webkit_web_view_reload(webview);
470
}
471
}
472
});
473
// Search bar
474
// TODO: provide history, provide a menu button with the other engines
475
auto search_bar = Gtk::make_managed<Gtk::Entry>();
476
search_bar->set_placeholder_text(_("Search"));
477
search_bar->set_hexpand(true);
478
if(panthera->search_engines.empty()) {
479
search_bar->set_sensitive(false);
480
search_bar->set_placeholder_text(_("No search"));
481
}
482
auto search_callback = [this, search_bar, content_stack, panthera]() {
483
// Create a new tab with the search results
484
if(panthera->content_manager->get_last_operated_page()) {
485
panthera->on_new_tab(nullptr);
486
} else {
487
panthera->on_new_tab(content_stack);
488
}
489
auto page = panthera->content_manager->get_last_operated_page();
490
if(page) {
491
if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) {
492
auto search_url = panthera->default_search_engine->get_search_url(search_bar->get_text());
493
webkit_web_view_load_uri(webview, search_url.c_str());
494
}
495
}
496
};
497
if(panthera->default_search_engine) {
498
search_bar->signal_activate().connect(search_callback);
499
}
500
501
// Assemble the toolbar
502
main_toolbar->append(*back_button);
503
main_toolbar->append(*forward_button);
504
main_toolbar->append(*reload_button);
505
main_toolbar->append(*url_bar);
506
main_toolbar->append(*go_button);
507
main_toolbar->append(*search_bar);
508
if(panthera->default_search_engine) {
509
auto search_button = Gtk::make_managed<Gtk::Button>(panthera->default_search_engine->name);
510
search_button->signal_clicked().connect(search_callback);
511
main_toolbar->append(*search_button);
512
}
513
outer_grid->attach(*main_toolbar, 0, 1, 2, 1);
514
this->set_child(*outer_grid);
515
debug_button->signal_clicked().connect([this, panthera]() {
516
if(panthera->content_manager->get_last_operated_page()) {
517
std::cout << "Last operated page: " << panthera->content_manager->get_last_operated_page()->get_name() << std::endl;
518
} else {
519
std::cout << "No page operated!" << std::endl;
520
}
521
});
522
// TODO: Use the last operated page and allow opening tabs next to the last operated page using certain panes
523
// Load the existing layout, if it exists
524
std::ifstream layout_file_in("layout.json");
525
if(layout_file_in) {
526
std::string layout_json((std::istreambuf_iterator<char>(layout_file_in)), std::istreambuf_iterator<char>());
527
layout_file_in.close();
528
layout_manager->restore_json_layout(layout_json);
529
} else {
530
// Create a new layout if the file doesn't exist
531
layout_file_in.close();
532
533
dock_stack_1->add_pane(*pane_1);
534
dock_stack_1->add_pane(*history_viewer);
535
536
std::ofstream layout_file_out("layout.json");
537
layout_file_out << layout_manager->get_layout_as_json();
538
layout_file_out.close();
539
}
540
// Save the layout when changed
541
layout_manager->signal_pane_moved.connect([this, panthera](gPanthera::DockablePane *pane) {
542
std::ofstream layout_file_out("layout.json");
543
layout_file_out << layout_manager->get_layout_as_json();
544
layout_file_out.close();
545
std::cout << "Layout changed: " << layout_manager->get_layout_as_json() << std::endl;
546
});
547
set_show_menubar(true);
548
}
549
};
550
551
void PantheraWww::load_change_callback(WebKitWebView *object, WebKitLoadEvent load_event, gpointer data) {
552
if(auto self = static_cast<PantheraWww*>(data)) {
553
if(load_event == WEBKIT_LOAD_COMMITTED) {
554
if(webkit_web_view_get_title(WEBKIT_WEB_VIEW(object)) == nullptr) {
555
self->history_manager->log_url(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(object)), "");
556
} else {
557
self->history_manager->log_url(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(object)),
558
webkit_web_view_get_title(WEBKIT_WEB_VIEW(object)));
559
}
560
// TODO: reload visible history viewers
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
bindtextdomain("panthera-www", "./locales");
708
textdomain("panthera-www");
709
Gtk::Application::on_startup();
710
// Get search engines
711
std::ifstream search_engines_file_in("search_engines.json");
712
if(search_engines_file_in) {
713
std::string search_engines_json((std::istreambuf_iterator<char>(search_engines_file_in)), std::istreambuf_iterator<char>());
714
search_engines_file_in.close();
715
set_search_engines_from_json(search_engines_json);
716
} else {
717
search_engines_file_in.close();
718
auto empty_json = nlohmann::json::array();
719
std::ofstream search_engines_file_out("search_engines.json");
720
search_engines_file_out << empty_json.dump(4);
721
}
722
723
// Load settings
724
new_tab_page = "about:blank";
725
std::ifstream settings_file_in("settings.json");
726
if(settings_file_in) {
727
std::string settings_json((std::istreambuf_iterator<char>(settings_file_in)), std::istreambuf_iterator<char>());
728
settings_file_in.close();
729
auto json = nlohmann::json::parse(settings_json);
730
if(json.contains("ntp")) {
731
new_tab_page = json["ntp"].get<std::string>();
732
}
733
} else {
734
settings_file_in.close();
735
auto empty_json = nlohmann::json::object();
736
empty_json["ntp"] = "about:blank";
737
std::ofstream settings_file_out("settings.json");
738
settings_file_out << empty_json.dump(4);
739
settings_file_out.close();
740
}
741
742
// Load plugins
743
std::ifstream plugin_file_in("plugins.json");
744
if(plugin_file_in) {
745
std::string plugin_json((std::istreambuf_iterator<char>(plugin_file_in)), std::istreambuf_iterator<char>());
746
plugin_file_in.close();
747
auto json = nlohmann::json::parse(plugin_json);
748
for(auto &plugin : json) {
749
auto plugin_path = plugin["path"].get<std::string>();
750
load_plugin(plugin_path);
751
}
752
} else {
753
plugin_file_in.close();
754
auto empty_json = nlohmann::json::array();
755
std::ofstream plugin_file_out("plugins.json");
756
plugin_file_out << empty_json.dump(4);
757
plugin_file_out.close();
758
}
759
history_manager = std::make_shared<HistoryManager>("history.db");
760
761
// Known bug <https://gitlab.gnome.org/GNOME/epiphany/-/issues/2714>:
762
// JS can't veto the application's accelerators using `preventDefault()`. This is
763
// not possible in WebKitGTK, or at least I can't find a way to do it.
764
765
main_menu = Gio::Menu::create();
766
// File
767
auto file_menu = Gio::Menu::create();
768
// New tab
769
auto new_tab_action = Gio::SimpleAction::create("new_tab");
770
new_tab_action->signal_activate().connect([this](const Glib::VariantBase&) {
771
on_new_tab(nullptr);
772
});
773
add_action(new_tab_action);
774
set_accels_for_action("app.new_tab", {"<Primary>T"});
775
file_menu->append(_("New tab"), "app.new_tab");
776
// Close tab
777
auto close_tab_action = Gio::SimpleAction::create("close_tab");
778
close_tab_action->signal_activate().connect([this](const Glib::VariantBase&) {
779
auto page = content_manager->get_last_operated_page();
780
if(page) {
781
page->close();
782
}
783
});
784
add_action(close_tab_action);
785
set_accels_for_action("app.close_tab", {"<Primary>W"});
786
file_menu->append(_("Close tab"), "app.close_tab");
787
// New window
788
auto new_window_action = Gio::SimpleAction::create("new_window");
789
new_window_action->signal_activate().connect([this](const Glib::VariantBase&) {
790
auto window = make_window();
791
window->present();
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::set_search_engines_from_json(const std::string &json_string) {
808
auto json = nlohmann::json::parse(json_string);
809
Glib::ustring default_search_engine_name;
810
for(auto &engine : json) {
811
SearchEngine search_engine;
812
search_engine.name = engine["name"].get<std::string>();
813
search_engine.url = engine["url"].get<std::string>();
814
search_engines.push_back(search_engine);
815
if(engine.contains("default") && engine["default"].get<bool>()) {
816
default_search_engine_name = engine["name"].get<std::string>();
817
}
818
}
819
for(auto &search_engine : search_engines) {
820
if(search_engine.name == default_search_engine_name) {
821
default_search_engine = &search_engine;
822
break;
823
}
824
}
825
}
826
827
PantheraWww::PantheraWww() : Gtk::Application("com.roundabout_host.roundabout.PantheraWww", Gio::Application::Flags::NONE) {
828
}
829
830
Glib::RefPtr<PantheraWww> PantheraWww::create() {
831
return Glib::make_refptr_for_instance<PantheraWww>(new PantheraWww());
832
}
833
834
void PantheraWww::load_plugin(const std::string &plugin_path) {
835
void *handle = dlopen(plugin_path.c_str(), RTLD_LAZY | RTLD_GLOBAL);
836
if(!handle) {
837
std::cerr << "Failed to load plugin: " << dlerror() << std::endl;
838
return;
839
}
840
auto entrypoint = (plugin_entrypoint_type)dlsym(handle, "plugin_init");
841
if(!entrypoint) {
842
std::cerr << "Failed to find entrypoint in plugin: " << dlerror() << std::endl;
843
dlclose(handle);
844
return;
845
}
846
entrypoint();
847
}
848
849
Glib::RefPtr<PantheraWww> app = nullptr;
850
851
extern "C" {
852
void panthera_log(const char *message) {
853
std::cerr << message << std::endl;
854
}
855
856
void panthera_add_pane(const char *id, const char *label, GtkImage *icon, GtkWidget *pane) {
857
auto pane_child = Glib::wrap(pane);
858
auto icon_cc = Glib::wrap(icon);
859
auto label_cc = Glib::ustring(label);
860
auto id_cc = Glib::ustring(id);
861
//auto new_pane = Gtk::make_managed<gPanthera::DockablePane>(app->get_layout_manager(), *pane_child, id_cc, label_cc, icon_cc);
862
// TODO: Port to multi-window
863
//app->get_layout_manager()->add_pane(new_pane);
864
}
865
}
866
867
int main(int argc, char *argv[]) {
868
gPanthera::init();
869
app = PantheraWww::create();
870
return app->run(argc, argv);
871
}
872