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