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