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