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