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