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