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.88 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
~PantheraWww();
302
friend class PantheraWindow;
303
void on_new_tab(gPanthera::ContentStack* stack, const Glib::ustring& url = "", bool focus = true, bool new_window = false);
304
void set_search_engines_from_json(const std::string& json_string);
305
static Glib::RefPtr<PantheraWww> create();
306
std::shared_ptr<gPanthera::ContentManager> get_content_manager();
307
void load_plugin(const std::string& plugin_path);
308
};
309
310
class PantheraWindow : public Gtk::ApplicationWindow {
311
private:
312
std::shared_ptr<gPanthera::LayoutManager> layout_manager;
313
Gtk::Entry *url_bar;
314
HistoryViewer *history_viewer;
315
gPanthera::ContentPage *controlled_page = nullptr;
316
public:
317
friend class PantheraWww;
318
explicit PantheraWindow(Glib::RefPtr<Gtk::Application> const &application) : Gtk::ApplicationWindow(application) {
319
auto panthera = dynamic_cast<PantheraWww*>(application.get());
320
if(!panthera) {
321
throw std::runtime_error("Application is not a PantheraWww instance");
322
}
323
this->set_default_size(800, 600);
324
layout_manager = std::make_shared<gPanthera::LayoutManager>();
325
326
auto dock_stack_1 = Gtk::make_managed<gPanthera::DockStack>(layout_manager, _("One"), "one");
327
auto switcher_1 = Gtk::make_managed<gPanthera::DockStackSwitcher>(dock_stack_1, Gtk::Orientation::HORIZONTAL);
328
auto dock_stack_2 = Gtk::make_managed<gPanthera::DockStack>(layout_manager, _("Two"), "two");
329
auto switcher_2 = Gtk::make_managed<gPanthera::DockStackSwitcher>(dock_stack_2, Gtk::Orientation::VERTICAL);
330
auto pane_1_content = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0);
331
auto debug_button = Gtk::make_managed<Gtk::Button>("Debug");
332
auto print_history_button = Gtk::make_managed<Gtk::Button>("Print history");
333
print_history_button->signal_clicked().connect([this, panthera]() {
334
auto history = panthera->history_manager->get_history();
335
for(const auto &entry : history) {
336
std::cout << "entry " << entry.id << ", url " << entry.url << ", unix time " << entry.timestamp << std::endl;
337
// TODO: exclude redirecting URLs
338
}
339
});
340
pane_1_content->append(*debug_button);
341
pane_1_content->append(*print_history_button);
342
auto pane_1_icon = Gtk::make_managed<Gtk::Image>(Gio::Icon::create("help-about-symbolic"));
343
auto pane_1 = Gtk::make_managed<gPanthera::DockablePane>(layout_manager, *pane_1_content, "debug", _("Debugging options"), pane_1_icon);
344
345
dock_stack_1->set_transition_type(Gtk::StackTransitionType::SLIDE_LEFT_RIGHT);
346
dock_stack_1->set_transition_duration(125);
347
dock_stack_1->set_expand(true);
348
dock_stack_2->set_transition_type(Gtk::StackTransitionType::SLIDE_UP_DOWN);
349
dock_stack_2->set_transition_duration(125);
350
dock_stack_2->set_expand(true);
351
352
auto outer_grid = Gtk::make_managed<Gtk::Grid>();
353
outer_grid->attach(*switcher_2, 0, 2, 1, 1);
354
outer_grid->attach(*switcher_1, 1, 3, 1, 1);
355
auto outer_paned = Gtk::make_managed<Gtk::Paned>(Gtk::Orientation::HORIZONTAL);
356
outer_paned->set_start_child(*dock_stack_2);
357
auto inner_paned = Gtk::make_managed<Gtk::Paned>(Gtk::Orientation::VERTICAL);
358
auto content = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0);
359
panthera->content_manager = std::make_shared<gPanthera::ContentManager>();
360
std::function<bool(gPanthera::ContentPage*)> detach_handler;
361
detach_handler = [](gPanthera::ContentPage *widget) {
362
auto new_stack = Gtk::make_managed<gPanthera::ContentStack>(widget->content_manager, widget->get_stack()->get_detach_handler());
363
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());
364
auto new_notebook = Gtk::make_managed<gPanthera::ContentNotebook>(new_stack, new_switcher);
365
auto window = new gPanthera::ContentWindow(new_notebook);
366
widget->redock(new_stack);
367
window->present();
368
new_stack->signal_leave_empty.connect([window]() {
369
window->close();
370
delete window;
371
});
372
return true;
373
};
374
375
auto return_extra_child = [this, panthera](gPanthera::ContentTabBar *switcher) {
376
auto new_tab_button = Gtk::make_managed<Gtk::Button>();
377
new_tab_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("list-add-symbolic")));
378
new_tab_button->set_tooltip_text("New tab");
379
new_tab_button->signal_clicked().connect([this, switcher, panthera]() {
380
panthera->on_new_tab(switcher->get_stack());
381
});
382
return new_tab_button;
383
};
384
auto content_stack = Gtk::make_managed<gPanthera::ContentStack>(panthera->content_manager, detach_handler);
385
auto content_stack_switcher = Gtk::make_managed<gPanthera::ContentTabBar>(content_stack, Gtk::Orientation::HORIZONTAL, return_extra_child);
386
panthera->content_manager->add_stack(content_stack);
387
WebKitWebView *webview = WEBKIT_WEB_VIEW(webkit_web_view_new()); // for some reason, this has to be created
388
content->set_name("content_box");
389
auto content_notebook = Gtk::make_managed<gPanthera::ContentNotebook>(content_stack, content_stack_switcher, Gtk::PositionType::TOP);
390
content->append(*content_notebook);
391
inner_paned->set_start_child(*content);
392
inner_paned->set_end_child(*dock_stack_1);
393
outer_paned->set_end_child(*inner_paned);
394
outer_grid->attach(*outer_paned, 1, 2, 1, 1);
395
// Create the toolbar
396
auto main_toolbar = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 8);
397
// URL bar
398
url_bar = Gtk::make_managed<Gtk::Entry>();
399
url_bar->set_placeholder_text(_("Enter URL"));
400
url_bar->set_hexpand(true);
401
auto load_url_callback = [this, panthera]() {
402
bool has_protocol = url_bar->get_text().find("://") != std::string::npos;
403
if(!has_protocol) {
404
url_bar->set_text("http://" + url_bar->get_text());
405
}
406
if(controlled_page) {
407
if(auto webview = WEBKIT_WEB_VIEW(controlled_page->get_child()->get_first_child()->gobj())) {
408
webkit_web_view_load_uri(webview, url_bar->get_text().c_str());
409
}
410
}
411
};
412
// Go
413
auto go_button = Gtk::make_managed<Gtk::Button>(_("Go"));
414
go_button->signal_clicked().connect(load_url_callback);
415
url_bar->signal_activate().connect(load_url_callback);
416
panthera->content_manager->signal_page_operated.connect([this, panthera](gPanthera::ContentPage *page) {
417
if(!page->get_child()) {
418
return;
419
}
420
if(!page->get_child()->get_first_child()) {
421
return;
422
}
423
if(page->get_root() != this) {
424
// The page is in some other window
425
return;
426
}
427
controlled_page = page;
428
url_bar->set_text(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())));
429
guint url_update_handler = g_signal_connect(page->get_child()->get_first_child()->gobj(), "notify", G_CALLBACK(panthera->notify_focused_callback), this);
430
std::shared_ptr<sigc::connection> control_signal_handler = std::make_shared<sigc::connection>();
431
*control_signal_handler = page->signal_control_status_changed.connect([this, page, control_signal_handler, url_update_handler](bool controlled) {
432
if(!controlled) {
433
control_signal_handler->disconnect();
434
if(page->get_child() && page->get_child()->get_first_child() && WEBKIT_IS_WEB_VIEW(page->get_child()->get_first_child()->gobj())) {
435
g_signal_handler_disconnect(page->get_child()->get_first_child()->gobj(), url_update_handler);
436
}
437
}
438
});
439
});
440
history_viewer = Gtk::make_managed<HistoryViewer>(layout_manager, panthera->history_manager);
441
history_viewer->signal_open_url.connect([this, panthera](const std::string &url) {
442
panthera->on_new_tab(nullptr, url, true);
443
});
444
// Back, forward, reload
445
auto back_button = Gtk::make_managed<Gtk::Button>();
446
back_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-previous-symbolic")));
447
back_button->set_tooltip_text("Back");
448
auto forward_button = Gtk::make_managed<Gtk::Button>();
449
forward_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-next-symbolic")));
450
forward_button->set_tooltip_text("Forward");
451
auto reload_button = Gtk::make_managed<Gtk::Button>();
452
reload_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("view-refresh-symbolic")));
453
reload_button->set_tooltip_text("Reload");
454
back_button->signal_clicked().connect([this, panthera]() {
455
if(controlled_page) {
456
if(auto webview = WEBKIT_WEB_VIEW(controlled_page->get_child()->get_first_child()->gobj())) {
457
webkit_web_view_go_back(webview);
458
}
459
}
460
});
461
forward_button->signal_clicked().connect([this, panthera]() {
462
if(controlled_page) {
463
if(auto webview = WEBKIT_WEB_VIEW(controlled_page->get_child()->get_first_child()->gobj())) {
464
webkit_web_view_go_forward(webview);
465
}
466
}
467
});
468
reload_button->signal_clicked().connect([this, panthera]() {
469
if(controlled_page) {
470
if(auto webview = WEBKIT_WEB_VIEW(controlled_page->get_child()->get_first_child()->gobj())) {
471
webkit_web_view_reload(webview);
472
}
473
}
474
});
475
// Search bar
476
// TODO: provide history, provide a menu button with the other engines
477
auto search_bar = Gtk::make_managed<Gtk::Entry>();
478
search_bar->set_placeholder_text(_("Search"));
479
search_bar->set_hexpand(true);
480
if(panthera->search_engines.empty()) {
481
search_bar->set_sensitive(false);
482
search_bar->set_placeholder_text(_("No search"));
483
}
484
auto search_callback = [this, search_bar, content_stack, panthera]() {
485
// Create a new tab with the search results
486
if(panthera->content_manager->get_last_operated_page()) {
487
panthera->on_new_tab(nullptr);
488
} else {
489
panthera->on_new_tab(content_stack);
490
}
491
auto page = panthera->content_manager->get_last_operated_page();
492
if(page) {
493
if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) {
494
auto search_url = panthera->default_search_engine->get_search_url(search_bar->get_text());
495
webkit_web_view_load_uri(webview, search_url.c_str());
496
}
497
}
498
};
499
if(panthera->default_search_engine) {
500
search_bar->signal_activate().connect(search_callback);
501
}
502
503
// Assemble the toolbar
504
main_toolbar->append(*back_button);
505
main_toolbar->append(*forward_button);
506
main_toolbar->append(*reload_button);
507
main_toolbar->append(*url_bar);
508
main_toolbar->append(*go_button);
509
main_toolbar->append(*search_bar);
510
if(panthera->default_search_engine) {
511
auto search_button = Gtk::make_managed<Gtk::Button>(panthera->default_search_engine->name);
512
search_button->signal_clicked().connect(search_callback);
513
main_toolbar->append(*search_button);
514
}
515
outer_grid->attach(*main_toolbar, 0, 1, 2, 1);
516
this->set_child(*outer_grid);
517
debug_button->signal_clicked().connect([this, panthera]() {
518
if(panthera->content_manager->get_last_operated_page()) {
519
std::cout << "Last operated page: " << panthera->content_manager->get_last_operated_page()->get_name() << std::endl;
520
} else {
521
std::cout << "No page operated!" << std::endl;
522
}
523
});
524
// TODO: Use the last operated page and allow opening tabs next to the last operated page using certain panes
525
// Load the existing layout, if it exists
526
std::ifstream layout_file_in("layout.json");
527
if(layout_file_in) {
528
std::string layout_json((std::istreambuf_iterator<char>(layout_file_in)), std::istreambuf_iterator<char>());
529
layout_file_in.close();
530
layout_manager->restore_json_layout(layout_json);
531
} else {
532
// Create a new layout if the file doesn't exist
533
layout_file_in.close();
534
535
dock_stack_1->add_pane(*pane_1);
536
dock_stack_1->add_pane(*history_viewer);
537
538
std::ofstream layout_file_out("layout.json");
539
layout_file_out << layout_manager->get_layout_as_json();
540
layout_file_out.close();
541
}
542
// Save the layout when changed
543
layout_manager->signal_pane_moved.connect([this, panthera](gPanthera::DockablePane *pane) {
544
std::ofstream layout_file_out("layout.json");
545
layout_file_out << layout_manager->get_layout_as_json();
546
layout_file_out.close();
547
std::cout << "Layout changed: " << layout_manager->get_layout_as_json() << std::endl;
548
});
549
set_show_menubar(true);
550
}
551
~PantheraWindow() override = default;
552
};
553
554
void PantheraWww::load_change_callback(WebKitWebView *object, WebKitLoadEvent load_event, gpointer data) {
555
if(auto self = static_cast<PantheraWww*>(data)) {
556
if(load_event == WEBKIT_LOAD_COMMITTED) {
557
if(webkit_web_view_get_title(WEBKIT_WEB_VIEW(object)) == nullptr) {
558
self->history_manager->log_url(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(object)), "");
559
} else {
560
self->history_manager->log_url(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(object)),
561
webkit_web_view_get_title(WEBKIT_WEB_VIEW(object)));
562
}
563
// TODO: reload visible history viewers
564
}
565
}
566
}
567
568
void PantheraWww::notify_callback(GObject *object, GParamSpec *pspec, gpointer data) {
569
if(!gtk_widget_get_parent(GTK_WIDGET(object))) {
570
return;
571
}
572
if(auto self = static_cast<PantheraWww*>(data)) {
573
auto parent = gtk_widget_get_parent(gtk_widget_get_parent(GTK_WIDGET(object)));
574
if(auto page = dynamic_cast<gPanthera::ContentPage*>(Glib::wrap(parent))) {
575
if(g_strcmp0(pspec->name, "title") == 0) {
576
if(auto label = dynamic_cast<Gtk::Label*>(page->tab_widget->get_last_child())) {
577
if(strlen(webkit_web_view_get_title(WEBKIT_WEB_VIEW(object))) == 0) {
578
label->set_label(_("Untitled"));
579
} else {
580
label->set_label(webkit_web_view_get_title(WEBKIT_WEB_VIEW(object)));
581
}
582
}
583
} else if(g_strcmp0(pspec->name, "favicon") == 0) {
584
// Update favicons
585
if(auto image = dynamic_cast<Gtk::Image*>(page->tab_widget->get_first_child())) {
586
image->set_from_icon_name("image-loading-symbolic");
587
if(auto favicon = webkit_web_view_get_favicon(WEBKIT_WEB_VIEW(object))) {
588
gtk_image_set_from_paintable(image->gobj(), GDK_PAINTABLE(favicon));
589
}
590
}
591
}
592
}
593
}
594
}
595
596
void PantheraWww::notify_focused_callback(GObject *object, GParamSpec *pspec, gpointer data) {
597
if(!gtk_widget_get_parent(GTK_WIDGET(object))) {
598
return;
599
}
600
auto this_ = static_cast<PantheraWww*>(data);
601
auto parent = gtk_widget_get_parent(gtk_widget_get_parent(GTK_WIDGET(object)));
602
if(auto page = dynamic_cast<gPanthera::ContentPage*>(Glib::wrap(parent))) {
603
if(g_strcmp0(pspec->name, "uri") == 0) {
604
if(auto main_window = dynamic_cast<PantheraWindow*>(page->get_root())) {
605
main_window->url_bar->set_text(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(object)));
606
}
607
}
608
}
609
}
610
611
void PantheraWww::on_back_pressed(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) {
612
WebKitWebView *webview = WEBKIT_WEB_VIEW(user_data);
613
webkit_web_view_go_back(webview);
614
}
615
616
void PantheraWww::on_forward_pressed(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) {
617
WebKitWebView *webview = WEBKIT_WEB_VIEW(user_data);
618
webkit_web_view_go_forward(webview);
619
}
620
621
gboolean PantheraWww::on_decide_policy(WebKitWebView *source, WebKitPolicyDecision *decision, WebKitPolicyDecisionType type, gpointer user_data) {
622
if(auto self = static_cast<PantheraWww*>(user_data)) {
623
if(type == WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION) {
624
auto action = webkit_navigation_policy_decision_get_navigation_action(WEBKIT_NAVIGATION_POLICY_DECISION(decision));
625
Glib::ustring url = Glib::ustring(webkit_uri_request_get_uri(webkit_navigation_action_get_request(action)));
626
if(!starts_with(url, "about:") && url.find("://") == Glib::ustring::npos) {
627
// It is a special scheme (mailto:, tel: etc.)
628
try {
629
Gio::AppInfo::launch_default_for_uri(url);
630
} catch(const Glib::Error &exception) {
631
std::cerr << "Failed to launch special URI: " << exception.what() << std::endl;
632
}
633
webkit_policy_decision_ignore(decision);
634
return true;
635
}
636
if(webkit_navigation_action_get_mouse_button(action) == 2) {
637
// Middle-click opens in a new window
638
self->on_new_tab(nullptr, url, false);
639
webkit_policy_decision_ignore(decision);
640
return true;
641
}
642
} else if(type == WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION) {
643
auto action = webkit_navigation_policy_decision_get_navigation_action(WEBKIT_NAVIGATION_POLICY_DECISION(decision));
644
self->on_new_tab(nullptr, webkit_uri_request_get_uri(webkit_navigation_action_get_request(action)), false, true);
645
return true;
646
}
647
}
648
return false;
649
}
650
651
void PantheraWww::on_new_tab(gPanthera::ContentStack *stack, const Glib::ustring &url, bool focus, bool new_window) {
652
if(!stack) {
653
// Find the current area
654
stack = content_manager->get_last_operated_page()->get_stack();
655
}
656
Glib::ustring url_ = url;
657
if(url.empty()) {
658
url_ = new_tab_page;
659
}
660
661
WebKitWebView *webview = WEBKIT_WEB_VIEW(webkit_web_view_new());
662
gtk_widget_set_hexpand(GTK_WIDGET(webview), true);
663
gtk_widget_set_vexpand(GTK_WIDGET(webview), true);
664
auto page_content = Gtk::make_managed<Gtk::Box>();
665
gtk_box_append(page_content->gobj(), GTK_WIDGET(webview));
666
auto page_tab = new Gtk::Box();
667
page_tab->set_orientation(Gtk::Orientation::HORIZONTAL);
668
auto initial_icon = Gtk::make_managed<Gtk::Image>(Gio::Icon::create("image-loading-symbolic"));
669
page_tab->append(*initial_icon);
670
page_tab->append(*Gtk::make_managed<Gtk::Label>("Untitled"));
671
page_tab->set_spacing(4);
672
auto page = Gtk::make_managed<gPanthera::ContentPage>(content_manager, stack, page_content, page_tab);
673
g_signal_connect(webview, "notify", G_CALLBACK(notify_callback), this);
674
g_signal_connect(webview, "load-changed", G_CALLBACK(load_change_callback), this);
675
g_signal_connect(webview, "decide-policy", G_CALLBACK(on_decide_policy), this);
676
webkit_web_view_load_uri(webview, url_.data());
677
auto cookie_manager = webkit_network_session_get_cookie_manager(webkit_web_view_get_network_session(webview));
678
webkit_cookie_manager_set_persistent_storage(cookie_manager, cookie_file.c_str(), WEBKIT_COOKIE_PERSISTENT_STORAGE_TEXT);
679
webkit_cookie_manager_set_accept_policy(cookie_manager, WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS);
680
auto website_data_manager = webkit_network_session_get_website_data_manager(webkit_web_view_get_network_session(webview));
681
webkit_website_data_manager_set_favicons_enabled(website_data_manager, true);
682
GtkEventController *click_controller_back = GTK_EVENT_CONTROLLER(gtk_gesture_click_new());
683
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click_controller_back), 8);
684
g_signal_connect(click_controller_back, "pressed", G_CALLBACK(on_back_pressed), webview);
685
gtk_widget_add_controller(GTK_WIDGET(webview), click_controller_back);
686
687
GtkEventController *click_controller_forward = GTK_EVENT_CONTROLLER(gtk_gesture_click_new());
688
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click_controller_forward), 9);
689
g_signal_connect(click_controller_forward, "pressed", G_CALLBACK(on_forward_pressed), webview);
690
gtk_widget_add_controller(GTK_WIDGET(webview), click_controller_forward);
691
692
stack->add_page(*page);
693
if(focus) {
694
stack->set_visible_child(*page);
695
content_manager->set_last_operated_page(page);
696
}
697
if(new_window) {
698
bool result = stack->signal_detach.emit(page);
699
}
700
}
701
702
PantheraWindow *PantheraWww::make_window() {
703
Glib::RefPtr<Gtk::Application> self_ref = Glib::make_refptr_for_instance<Gtk::Application>(this);
704
auto window = Gtk::make_managed<PantheraWindow>(self_ref);
705
return window;
706
}
707
708
void PantheraWww::on_startup() {
709
bindtextdomain("panthera-www", "./locales");
710
textdomain("panthera-www");
711
Gtk::Application::on_startup();
712
// Get search engines
713
std::ifstream search_engines_file_in("search_engines.json");
714
if(search_engines_file_in) {
715
std::string search_engines_json((std::istreambuf_iterator<char>(search_engines_file_in)), std::istreambuf_iterator<char>());
716
search_engines_file_in.close();
717
set_search_engines_from_json(search_engines_json);
718
} else {
719
search_engines_file_in.close();
720
auto empty_json = nlohmann::json::array();
721
std::ofstream search_engines_file_out("search_engines.json");
722
search_engines_file_out << empty_json.dump(4);
723
}
724
725
// Load settings
726
new_tab_page = "about:blank";
727
std::ifstream settings_file_in("settings.json");
728
if(settings_file_in) {
729
std::string settings_json((std::istreambuf_iterator<char>(settings_file_in)), std::istreambuf_iterator<char>());
730
settings_file_in.close();
731
auto json = nlohmann::json::parse(settings_json);
732
if(json.contains("ntp")) {
733
new_tab_page = json["ntp"].get<std::string>();
734
}
735
} else {
736
settings_file_in.close();
737
auto empty_json = nlohmann::json::object();
738
empty_json["ntp"] = "about:blank";
739
std::ofstream settings_file_out("settings.json");
740
settings_file_out << empty_json.dump(4);
741
settings_file_out.close();
742
}
743
744
// Load plugins
745
std::ifstream plugin_file_in("plugins.json");
746
if(plugin_file_in) {
747
std::string plugin_json((std::istreambuf_iterator<char>(plugin_file_in)), std::istreambuf_iterator<char>());
748
plugin_file_in.close();
749
auto json = nlohmann::json::parse(plugin_json);
750
for(auto &plugin : json) {
751
auto plugin_path = plugin["path"].get<std::string>();
752
//load_plugin(plugin_path);
753
}
754
} else {
755
plugin_file_in.close();
756
auto empty_json = nlohmann::json::array();
757
std::ofstream plugin_file_out("plugins.json");
758
plugin_file_out << empty_json.dump(4);
759
plugin_file_out.close();
760
}
761
history_manager = std::make_shared<HistoryManager>("history.db");
762
763
// Known bug <https://gitlab.gnome.org/GNOME/epiphany/-/issues/2714>:
764
// JS can't veto the application's accelerators using `preventDefault()`. This is
765
// not possible in WebKitGTK, or at least I can't find a way to do it.
766
767
main_menu = Gio::Menu::create();
768
// File
769
auto file_menu = Gio::Menu::create();
770
// New tab
771
auto new_tab_action = Gio::SimpleAction::create("new_tab");
772
new_tab_action->signal_activate().connect([this](const Glib::VariantBase&) {
773
on_new_tab(nullptr);
774
});
775
add_action(new_tab_action);
776
set_accels_for_action("app.new_tab", {"<Primary>T"});
777
file_menu->append(_("New tab"), "app.new_tab");
778
// Close tab
779
auto close_tab_action = Gio::SimpleAction::create("close_tab");
780
close_tab_action->signal_activate().connect([this](const Glib::VariantBase&) {
781
auto page = content_manager->get_last_operated_page();
782
if(page) {
783
page->close();
784
}
785
});
786
add_action(close_tab_action);
787
set_accels_for_action("app.close_tab", {"<Primary>W"});
788
file_menu->append(_("Close tab"), "app.close_tab");
789
// New window
790
auto new_window_action = Gio::SimpleAction::create("new_window");
791
new_window_action->signal_activate().connect([this](const Glib::VariantBase&) {
792
on_activate();
793
});
794
add_action(new_window_action);
795
set_accels_for_action("app.new_window", {"<Primary>N"});
796
file_menu->append(_("New window"), "app.new_window");
797
798
main_menu->append_submenu(_("File"), file_menu);
799
800
set_menubar(main_menu);
801
}
802
803
void PantheraWww::on_activate() {
804
auto window = make_window();
805
window->present();
806
}
807
808
void PantheraWww::on_shutdown() {
809
Gtk::Application::on_shutdown();
810
}
811
812
void PantheraWww::set_search_engines_from_json(const std::string &json_string) {
813
auto json = nlohmann::json::parse(json_string);
814
Glib::ustring default_search_engine_name;
815
for(auto &engine : json) {
816
SearchEngine search_engine;
817
search_engine.name = engine["name"].get<std::string>();
818
search_engine.url = engine["url"].get<std::string>();
819
search_engines.push_back(search_engine);
820
if(engine.contains("default") && engine["default"].get<bool>()) {
821
default_search_engine_name = engine["name"].get<std::string>();
822
}
823
}
824
for(auto &search_engine : search_engines) {
825
if(search_engine.name == default_search_engine_name) {
826
default_search_engine = &search_engine;
827
break;
828
}
829
}
830
}
831
832
PantheraWww::PantheraWww() : Gtk::Application("com.roundabout_host.roundabout.PantheraWww", Gio::Application::Flags::NONE) {
833
}
834
835
Glib::RefPtr<PantheraWww> PantheraWww::create() {
836
return Glib::make_refptr_for_instance<PantheraWww>(new PantheraWww());
837
}
838
839
void PantheraWww::load_plugin(const std::string &plugin_path) {
840
// TODO: needs to be reworked, to work with multi-window
841
void *handle = dlopen(plugin_path.c_str(), RTLD_LAZY | RTLD_GLOBAL);
842
if(!handle) {
843
std::cerr << "Failed to load plugin: " << dlerror() << std::endl;
844
return;
845
}
846
auto entrypoint = (plugin_entrypoint_type)dlsym(handle, "plugin_init");
847
if(!entrypoint) {
848
std::cerr << "Failed to find entrypoint in plugin: " << dlerror() << std::endl;
849
dlclose(handle);
850
return;
851
}
852
entrypoint();
853
}
854
855
PantheraWww::~PantheraWww() = default;
856
857
/*
858
extern "C" {
859
void panthera_log(const char *message) {
860
std::cerr << message << std::endl;
861
}
862
863
void panthera_add_pane(const char *id, const char *label, GtkImage *icon, GtkWidget *pane) {
864
auto pane_child = Glib::wrap(pane);
865
auto icon_cc = Glib::wrap(icon);
866
auto label_cc = Glib::ustring(label);
867
auto id_cc = Glib::ustring(id);
868
//auto new_pane = Gtk::make_managed<gPanthera::DockablePane>(app->get_layout_manager(), *pane_child, id_cc, label_cc, icon_cc);
869
// TODO: Port to multi-window
870
//app->get_layout_manager()->add_pane(new_pane);
871
}
872
}
873
*/
874
875
int main(int argc, char *argv[]) {
876
gPanthera::init();
877
878
auto app = PantheraWww::create();
879
880
return app->run(argc, argv);
881
}
882