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