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