GTK docking interfaces and more

Important information: Google announced that, from September 2026, Android devices will require ALL apps to be signed by Google, effectively leading to an iOS situation. Value your right to a computer that does what you want; do not tolerate this monopolistic practice! Contact me if you don't understand why it is bad. Click to learn more.

 panthera-www.cc

View raw Download
text/x-c++ • 28.48 kiB
C++ source, ASCII text
        
            
1
#include "gpanthera.hh"
2
#include <gtkmm.h>
3
#include <glibmm.h>
4
#include <glibmm/ustring.h>
5
#include <iostream>
6
#include <memory>
7
#include <libintl.h>
8
#include <locale.h>
9
#include <gtk/gtk.h>
10
#include <gdk/gdk.h>
11
#include <webkit/webkit.h>
12
#include <fstream>
13
#include <nlohmann/json.hpp>
14
#include <iomanip>
15
#include <sstream>
16
#include <dlfcn.h>
17
#include "external/sqlite_orm/sqlite_orm.h"
18
#include "plugin_api.h"
19
20
using plugin_entrypoint_type = void(*)(void);
21
22
std::string url_encode(const std::string &value) {
23
// Thanks https://stackoverflow.com/a/17708801
24
std::ostringstream escaped;
25
escaped.fill('0');
26
escaped << std::hex;
27
28
for(char c : value) {
29
// Keep alphanumeric and other accepted characters intact
30
if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
31
escaped << c;
32
continue;
33
}
34
35
// Any other characters are percent-encoded
36
escaped << std::uppercase;
37
escaped << '%' << std::setw(2) << int((unsigned char) c);
38
escaped << std::nouppercase;
39
}
40
41
return escaped.str();
42
}
43
44
struct SearchEngine {
45
Glib::ustring name;
46
Glib::ustring url;
47
Glib::ustring get_search_url(const std::string &query) {
48
auto pos = url.find("%s");
49
if(pos == Glib::ustring::npos) {
50
throw std::runtime_error("Invalid search engine URL: missing '%s' placeholder");
51
}
52
auto new_url = url;
53
new_url.replace(pos, 2, url_encode(query));
54
return new_url;
55
}
56
};
57
58
struct HistoryEntry {
59
int id;
60
std::string url;
61
int64_t timestamp;
62
};
63
64
using StorageType = decltype(sqlite_orm::make_storage("",
65
sqlite_orm::make_table("history",
66
sqlite_orm::make_column("id", &HistoryEntry::id, sqlite_orm::primary_key()),
67
sqlite_orm::make_column("url", &HistoryEntry::url),
68
sqlite_orm::make_column("timestamp", &HistoryEntry::timestamp)
69
)
70
));
71
72
class HistoryManager {
73
static auto make_storage(const std::string& path) {
74
return sqlite_orm::make_storage(path,
75
sqlite_orm::make_table("history",
76
sqlite_orm::make_column("id", &HistoryEntry::id, sqlite_orm::primary_key()),
77
sqlite_orm::make_column("url", &HistoryEntry::url),
78
sqlite_orm::make_column("timestamp", &HistoryEntry::timestamp)
79
)
80
);
81
}
82
StorageType storage;
83
public:
84
explicit HistoryManager(const std::string &db_path) : storage(make_storage(db_path)) {
85
storage.sync_schema();
86
}
87
88
void log_url(const std::string &url, const int64_t timestamp = std::chrono::system_clock::now().time_since_epoch().count()) {
89
storage.insert(HistoryEntry{-1, url, timestamp});
90
}
91
92
std::vector<HistoryEntry> get_history() {
93
return storage.get_all<HistoryEntry>();
94
}
95
};
96
97
class PantheraWww : public Gtk::Application {
98
Gtk::Window *window = Gtk::make_managed<Gtk::Window>();
99
protected:
100
std::shared_ptr<gPanthera::LayoutManager> layout_manager;
101
std::shared_ptr<gPanthera::ContentManager> content_manager;
102
Gtk::Entry *url_bar = nullptr;
103
std::string cookie_file = "cookies.txt";
104
std::vector<SearchEngine> search_engines;
105
SearchEngine *default_search_engine = nullptr;
106
std::shared_ptr<HistoryManager> history_manager;
107
108
static void notify_callback(GObject *object, GParamSpec *pspec, gpointer data) {
109
if(!gtk_widget_get_parent(GTK_WIDGET(object))) {
110
return;
111
}
112
if(auto self = static_cast<PantheraWww*>(data)) {
113
auto parent = gtk_widget_get_parent(gtk_widget_get_parent(GTK_WIDGET(object)));
114
if(auto page = dynamic_cast<gPanthera::ContentPage*>(Glib::wrap(parent))) {
115
if(g_strcmp0(pspec->name, "title") == 0) {
116
if(auto label = dynamic_cast<Gtk::Label*>(page->tab_widget->get_last_child())) {
117
if(strlen(webkit_web_view_get_title(WEBKIT_WEB_VIEW(object))) == 0) {
118
label->set_label("Untitled");
119
} else {
120
label->set_label(webkit_web_view_get_title(WEBKIT_WEB_VIEW(object)));
121
}
122
}
123
} else if(g_strcmp0(pspec->name, "favicon") == 0) {
124
// Update favicons
125
if(auto image = dynamic_cast<Gtk::Image*>(page->tab_widget->get_first_child())) {
126
image->set_from_icon_name("image-loading-symbolic");
127
if(auto favicon = webkit_web_view_get_favicon(WEBKIT_WEB_VIEW(object))) {
128
gtk_image_set_from_paintable(image->gobj(), GDK_PAINTABLE(favicon));
129
}
130
}
131
} else if(g_strcmp0(pspec->name, "uri") == 0) {
132
self->history_manager->log_url(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(object)));
133
}
134
}
135
}
136
}
137
138
static void notify_focused_callback(GObject *object, GParamSpec *pspec, gpointer data) {
139
if(!gtk_widget_get_parent(GTK_WIDGET(object))) {
140
return;
141
}
142
auto this_ = static_cast<PantheraWww*>(data);
143
auto parent = gtk_widget_get_parent(gtk_widget_get_parent(GTK_WIDGET(object)));
144
if(auto page = dynamic_cast<gPanthera::ContentPage*>(Glib::wrap(parent))) {
145
if(g_strcmp0(pspec->name, "uri") == 0) {
146
this_->url_bar->set_text(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(object)));
147
}
148
}
149
}
150
151
static void on_back_pressed(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) {
152
WebKitWebView *webview = WEBKIT_WEB_VIEW(user_data);
153
webkit_web_view_go_back(webview);
154
}
155
156
static void on_forward_pressed(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) {
157
WebKitWebView *webview = WEBKIT_WEB_VIEW(user_data);
158
webkit_web_view_go_forward(webview);
159
}
160
161
static gboolean on_decide_policy(WebKitWebView *source, WebKitPolicyDecision *decision, WebKitPolicyDecisionType type, gpointer user_data) {
162
// Middle-click opens in a new window
163
if(auto self = static_cast<PantheraWww*>(user_data)) {
164
if(type == WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION) {
165
auto action = webkit_navigation_policy_decision_get_navigation_action(WEBKIT_NAVIGATION_POLICY_DECISION(decision));
166
if(webkit_navigation_action_get_mouse_button(action) == 2) {
167
Glib::ustring url = Glib::ustring(webkit_uri_request_get_uri(webkit_navigation_action_get_request(action)));
168
self->on_new_tab(nullptr, url, false);
169
webkit_policy_decision_ignore(decision);
170
return true;
171
}
172
} else if(type == WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION) {
173
auto action = webkit_navigation_policy_decision_get_navigation_action(WEBKIT_NAVIGATION_POLICY_DECISION(decision));
174
self->on_new_tab(nullptr, webkit_uri_request_get_uri(webkit_navigation_action_get_request(action)), false, true);
175
return true;
176
}
177
}
178
return false;
179
}
180
181
std::string new_tab_page;
182
183
void on_new_tab(gPanthera::ContentStack *stack, const Glib::ustring &url = "", bool focus = true, bool new_window = false) {
184
if(!stack) {
185
// Find the current area
186
stack = content_manager->get_last_operated_page()->get_stack();
187
}
188
Glib::ustring url_ = url;
189
if(url.empty()) {
190
url_ = new_tab_page;
191
}
192
193
WebKitWebView *webview = WEBKIT_WEB_VIEW(webkit_web_view_new());
194
gtk_widget_set_hexpand(GTK_WIDGET(webview), true);
195
gtk_widget_set_vexpand(GTK_WIDGET(webview), true);
196
auto page_content = Gtk::make_managed<Gtk::Box>();
197
gtk_box_append(page_content->gobj(), GTK_WIDGET(webview));
198
auto page_tab = new Gtk::Box();
199
page_tab->set_orientation(Gtk::Orientation::HORIZONTAL);
200
auto initial_icon = Gtk::make_managed<Gtk::Image>(Gio::Icon::create("image-loading-symbolic"));
201
page_tab->append(*initial_icon);
202
page_tab->append(*Gtk::make_managed<Gtk::Label>("Untitled"));
203
page_tab->set_spacing(4);
204
auto page = Gtk::make_managed<gPanthera::ContentPage>(content_manager, stack, page_content, page_tab);
205
g_signal_connect(webview, "notify", G_CALLBACK(notify_callback), this);
206
g_signal_connect(webview, "decide-policy", G_CALLBACK(on_decide_policy), this);
207
webkit_web_view_load_uri(webview, url_.data());
208
auto cookie_manager = webkit_network_session_get_cookie_manager(webkit_web_view_get_network_session(webview));
209
webkit_cookie_manager_set_persistent_storage(cookie_manager, cookie_file.c_str(), WEBKIT_COOKIE_PERSISTENT_STORAGE_TEXT);
210
webkit_cookie_manager_set_accept_policy(cookie_manager, WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS);
211
auto website_data_manager = webkit_network_session_get_website_data_manager(webkit_web_view_get_network_session(webview));
212
webkit_website_data_manager_set_favicons_enabled(website_data_manager, true);
213
GtkEventController *click_controller_back = GTK_EVENT_CONTROLLER(gtk_gesture_click_new());
214
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click_controller_back), 8);
215
g_signal_connect(click_controller_back, "pressed", G_CALLBACK(on_back_pressed), webview);
216
gtk_widget_add_controller(GTK_WIDGET(webview), click_controller_back);
217
218
GtkEventController *click_controller_forward = GTK_EVENT_CONTROLLER(gtk_gesture_click_new());
219
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click_controller_forward), 9);
220
g_signal_connect(click_controller_forward, "pressed", G_CALLBACK(on_forward_pressed), webview);
221
gtk_widget_add_controller(GTK_WIDGET(webview), click_controller_forward);
222
223
stack->add_page(*page);
224
if(focus) {
225
stack->set_visible_child(*page);
226
content_manager->set_last_operated_page(page);
227
}
228
if(new_window) {
229
bool result = stack->signal_detach.emit(page);
230
}
231
}
232
233
void on_startup() override {
234
Gtk::Application::on_startup();
235
add_window(*window);
236
window->set_default_size(600, 400);
237
layout_manager = std::make_shared<gPanthera::LayoutManager>();
238
auto dock_stack_1 = Gtk::make_managed<gPanthera::DockStack>(layout_manager, "One", "one");
239
auto switcher_1 = Gtk::make_managed<gPanthera::DockStackSwitcher>(dock_stack_1, Gtk::Orientation::HORIZONTAL);
240
auto dock_stack_2 = Gtk::make_managed<gPanthera::DockStack>(layout_manager, "Two", "two");
241
auto switcher_2 = Gtk::make_managed<gPanthera::DockStackSwitcher>(dock_stack_2, Gtk::Orientation::VERTICAL);
242
auto pane_1_content = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0);
243
auto debug_button = Gtk::make_managed<Gtk::Button>("Debug");
244
auto print_history_button = Gtk::make_managed<Gtk::Button>("Print history");
245
print_history_button->signal_clicked().connect([this]() {
246
auto history = history_manager->get_history();
247
for(const auto &entry : history) {
248
std::cout << "entry " << entry.id << ", url " << entry.url << ", unix time " << entry.timestamp << std::endl;
249
// TODO: exclude redirecting URLs
250
}
251
});
252
pane_1_content->append(*debug_button);
253
pane_1_content->append(*print_history_button);
254
auto pane_2_content = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0);
255
auto test_notebook = Gtk::make_managed<Gtk::Notebook>();
256
test_notebook->append_page(*Gtk::make_managed<Gtk::Label>("Test 1"), *Gtk::make_managed<Gtk::Label>("Test 1"));
257
pane_2_content->append(*test_notebook);
258
auto pane_3_content = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0);
259
pane_3_content->append(*Gtk::make_managed<Gtk::Label>("Pane 3 content"));
260
auto pane_1_icon = Gtk::make_managed<Gtk::Image>();
261
pane_1_icon->set_from_icon_name("go-home-symbolic");
262
auto pane_2_icon = Gtk::make_managed<Gtk::Image>();
263
pane_2_icon->set_from_icon_name("folder-symbolic");
264
auto pane_3_icon = Gtk::make_managed<Gtk::Image>();
265
pane_3_icon->set_from_icon_name("network-transmit-receive-symbolic");
266
auto pane_1 = Gtk::make_managed<gPanthera::DockablePane>(layout_manager, *pane_1_content, "pane1", "Pane 1", pane_1_icon);
267
auto pane_2 = Gtk::make_managed<gPanthera::DockablePane>(layout_manager, *pane_2_content, "pane2", "Pane 2", pane_2_icon);
268
auto pane_3 = Gtk::make_managed<gPanthera::DockablePane>(layout_manager, *pane_3_content, "pane3", "Pane 3", pane_3_icon);
269
270
dock_stack_1->set_transition_type(Gtk::StackTransitionType::SLIDE_LEFT_RIGHT);
271
dock_stack_1->set_transition_duration(125);
272
dock_stack_1->set_expand(true);
273
dock_stack_2->set_transition_type(Gtk::StackTransitionType::SLIDE_UP_DOWN);
274
dock_stack_2->set_transition_duration(125);
275
dock_stack_2->set_expand(true);
276
277
auto outer_grid = Gtk::make_managed<Gtk::Grid>();
278
outer_grid->attach(*switcher_2, 0, 1, 1, 1);
279
outer_grid->attach(*switcher_1, 1, 2, 1, 1);
280
auto outer_paned = Gtk::make_managed<Gtk::Paned>(Gtk::Orientation::HORIZONTAL);
281
outer_paned->set_start_child(*dock_stack_2);
282
auto inner_paned = Gtk::make_managed<Gtk::Paned>(Gtk::Orientation::VERTICAL);
283
auto content = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0);
284
content_manager = std::make_shared<gPanthera::ContentManager>();
285
std::function<bool(gPanthera::ContentPage*)> detach_handler;
286
detach_handler = [](gPanthera::ContentPage *widget) {
287
auto new_stack = Gtk::make_managed<gPanthera::ContentStack>(widget->content_manager, widget->get_stack()->get_detach_handler());
288
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());
289
auto new_notebook = Gtk::make_managed<gPanthera::ContentNotebook>(new_stack, new_switcher);
290
auto window = new gPanthera::ContentWindow(new_notebook);
291
widget->redock(new_stack);
292
window->present();
293
new_stack->signal_leave_empty.connect([window]() {
294
window->close();
295
delete window;
296
});
297
return true;
298
};
299
300
auto return_extra_child = [this](gPanthera::ContentTabBar *switcher) {
301
auto new_tab_button = Gtk::make_managed<Gtk::Button>();
302
new_tab_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("list-add-symbolic")));
303
new_tab_button->set_tooltip_text("New tab");
304
new_tab_button->signal_clicked().connect([this, switcher]() {
305
on_new_tab(switcher->get_stack());
306
});
307
return new_tab_button;
308
};
309
auto content_stack = Gtk::make_managed<gPanthera::ContentStack>(content_manager, detach_handler);
310
auto content_stack_switcher = Gtk::make_managed<gPanthera::ContentTabBar>(content_stack, Gtk::Orientation::HORIZONTAL, return_extra_child);
311
content_manager->add_stack(content_stack);
312
WebKitWebView *webview = WEBKIT_WEB_VIEW(webkit_web_view_new()); // for some reason, this has to be created
313
content->set_name("content_box");
314
auto content_notebook = Gtk::make_managed<gPanthera::ContentNotebook>(content_stack, content_stack_switcher, Gtk::PositionType::TOP);
315
content->append(*content_notebook);
316
inner_paned->set_start_child(*content);
317
inner_paned->set_end_child(*dock_stack_1);
318
outer_paned->set_end_child(*inner_paned);
319
outer_grid->attach(*outer_paned, 1, 1, 1, 1);
320
321
// Get search engines
322
std::ifstream search_engines_file_in("search_engines.json");
323
if(search_engines_file_in) {
324
std::string search_engines_json((std::istreambuf_iterator<char>(search_engines_file_in)), std::istreambuf_iterator<char>());
325
search_engines_file_in.close();
326
set_search_engines_from_json(search_engines_json);
327
} else {
328
search_engines_file_in.close();
329
auto empty_json = nlohmann::json::array();
330
std::ofstream search_engines_file_out("search_engines.json");
331
search_engines_file_out << empty_json.dump(4);
332
}
333
334
// Create the toolbar
335
auto main_toolbar = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 8);
336
// URL bar
337
url_bar = Gtk::make_managed<Gtk::Entry>();
338
url_bar->set_placeholder_text("Enter URL");
339
url_bar->set_hexpand(true);
340
auto load_url_callback = [this]() {
341
auto page = content_manager->get_last_operated_page();
342
bool has_protocol = url_bar->get_text().find("://") != std::string::npos;
343
if(!has_protocol) {
344
url_bar->set_text("http://" + url_bar->get_text());
345
}
346
if(page) {
347
if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) {
348
webkit_web_view_load_uri(webview, url_bar->get_text().c_str());
349
}
350
}
351
};
352
// Go
353
auto go_button = Gtk::make_managed<Gtk::Button>("Go");
354
go_button->signal_clicked().connect(load_url_callback);
355
url_bar->signal_activate().connect(load_url_callback);
356
content_manager->signal_page_operated.connect([this](gPanthera::ContentPage *page) {
357
if(!page->get_child()) {
358
return;
359
}
360
if(!page->get_child()->get_first_child()) {
361
return;
362
}
363
url_bar->set_text(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())));
364
guint url_update_handler = g_signal_connect(page->get_child()->get_first_child()->gobj(), "notify", G_CALLBACK(notify_focused_callback), this);
365
std::shared_ptr<sigc::connection> control_signal_handler = std::make_shared<sigc::connection>();
366
*control_signal_handler = page->signal_control_status_changed.connect([this, page, control_signal_handler, url_update_handler](bool controlled) {
367
if(!controlled) {
368
control_signal_handler->disconnect();
369
if(page->get_child() && page->get_child()->get_first_child() && WEBKIT_IS_WEB_VIEW(page->get_child()->get_first_child()->gobj())) {
370
g_signal_handler_disconnect(page->get_child()->get_first_child()->gobj(), url_update_handler);
371
}
372
}
373
});
374
});
375
history_manager = std::make_shared<HistoryManager>("history.db");
376
// Back, forward, reload
377
auto back_button = Gtk::make_managed<Gtk::Button>();
378
back_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-previous-symbolic")));
379
back_button->set_tooltip_text("Back");
380
auto forward_button = Gtk::make_managed<Gtk::Button>();
381
forward_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-next-symbolic")));
382
forward_button->set_tooltip_text("Forward");
383
auto reload_button = Gtk::make_managed<Gtk::Button>();
384
reload_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("view-refresh-symbolic")));
385
reload_button->set_tooltip_text("Reload");
386
back_button->signal_clicked().connect([this]() {
387
auto page = content_manager->get_last_operated_page();
388
if(page) {
389
if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) {
390
webkit_web_view_go_back(webview);
391
}
392
}
393
});
394
forward_button->signal_clicked().connect([this]() {
395
auto page = content_manager->get_last_operated_page();
396
if(page) {
397
if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) {
398
webkit_web_view_go_forward(webview);
399
}
400
}
401
});
402
reload_button->signal_clicked().connect([this]() {
403
auto page = content_manager->get_last_operated_page();
404
if(page) {
405
if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) {
406
webkit_web_view_reload(webview);
407
}
408
}
409
});
410
// Search bar
411
// TODO: provide history, provide a menu button with the other engines
412
auto search_bar = Gtk::make_managed<Gtk::Entry>();
413
search_bar->set_placeholder_text("Search");
414
search_bar->set_hexpand(true);
415
if(search_engines.empty()) {
416
search_bar->set_sensitive(false);
417
search_bar->set_placeholder_text("No search");
418
}
419
auto search_callback = [this, search_bar, content_stack]() {
420
// Create a new tab with the search results
421
if(content_manager->get_last_operated_page()) {
422
on_new_tab(nullptr);
423
} else {
424
on_new_tab(content_stack);
425
}
426
auto page = content_manager->get_last_operated_page();
427
if(page) {
428
if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) {
429
auto search_url = default_search_engine->get_search_url(search_bar->get_text());
430
webkit_web_view_load_uri(webview, search_url.c_str());
431
}
432
}
433
};
434
if(default_search_engine) {
435
search_bar->signal_activate().connect(search_callback);
436
}
437
// Assemble the toolbar
438
main_toolbar->append(*back_button);
439
main_toolbar->append(*forward_button);
440
main_toolbar->append(*reload_button);
441
main_toolbar->append(*url_bar);
442
main_toolbar->append(*go_button);
443
main_toolbar->append(*search_bar);
444
if(default_search_engine) {
445
auto search_button = Gtk::make_managed<Gtk::Button>(default_search_engine->name);
446
search_button->signal_clicked().connect(search_callback);
447
main_toolbar->append(*search_button);
448
}
449
outer_grid->attach(*main_toolbar, 0, 0, 2, 1);
450
window->set_child(*outer_grid);
451
debug_button->signal_clicked().connect([this]() {
452
if(content_manager->get_last_operated_page()) {
453
std::cout << "Last operated page: " << content_manager->get_last_operated_page()->get_name() << std::endl;
454
} else {
455
std::cout << "No page operated!" << std::endl;
456
}
457
});
458
// TODO: Use the last operated page and allow opening tabs next to the last operated page using certain panes
459
// Load the existing layout, if it exists
460
std::ifstream layout_file_in("layout.json");
461
if(layout_file_in) {
462
std::string layout_json((std::istreambuf_iterator<char>(layout_file_in)), std::istreambuf_iterator<char>());
463
layout_file_in.close();
464
layout_manager->restore_json_layout(layout_json);
465
} else {
466
// Create a new layout if the file doesn't exist
467
layout_file_in.close();
468
469
dock_stack_1->add_pane(*pane_1);
470
dock_stack_1->add_pane(*pane_3);
471
dock_stack_2->add_pane(*pane_2);
472
473
std::ofstream layout_file_out("layout.json");
474
layout_file_out << layout_manager->get_layout_as_json();
475
layout_file_out.close();
476
}
477
// Save the layout when changed
478
layout_manager->signal_pane_moved.connect([this](gPanthera::DockablePane *pane) {
479
std::ofstream layout_file_out("layout.json");
480
layout_file_out << layout_manager->get_layout_as_json();
481
layout_file_out.close();
482
std::cout << "Layout changed: " << layout_manager->get_layout_as_json() << std::endl;
483
});
484
485
auto new_tab_action = Gio::SimpleAction::create("new_tab");
486
new_tab_action->signal_activate().connect([this](const Glib::VariantBase&) {
487
on_new_tab(nullptr);
488
});
489
add_action(new_tab_action);
490
set_accels_for_action("app.new_tab", {"<Primary>T"});
491
auto close_tab_action = Gio::SimpleAction::create("close_tab");
492
close_tab_action->signal_activate().connect([this](const Glib::VariantBase&) {
493
auto page = content_manager->get_last_operated_page();
494
if(page) {
495
page->close();
496
}
497
});
498
add_action(close_tab_action);
499
set_accels_for_action("app.close_tab", {"<Primary>W"});
500
501
// Load settings
502
new_tab_page = "about:blank";
503
std::ifstream settings_file_in("settings.json");
504
if(settings_file_in) {
505
std::string settings_json((std::istreambuf_iterator<char>(settings_file_in)), std::istreambuf_iterator<char>());
506
settings_file_in.close();
507
auto json = nlohmann::json::parse(settings_json);
508
if(json.contains("ntp")) {
509
new_tab_page = json["ntp"].get<std::string>();
510
}
511
} else {
512
settings_file_in.close();
513
auto empty_json = nlohmann::json::object();
514
empty_json["ntp"] = "about:blank";
515
std::ofstream settings_file_out("settings.json");
516
settings_file_out << empty_json.dump(4);
517
settings_file_out.close();
518
}
519
520
// Load plugins
521
std::ifstream plugin_file_in("plugins.json");
522
if(plugin_file_in) {
523
std::string plugin_json((std::istreambuf_iterator<char>(plugin_file_in)), std::istreambuf_iterator<char>());
524
plugin_file_in.close();
525
auto json = nlohmann::json::parse(plugin_json);
526
for(auto &plugin : json) {
527
auto plugin_path = plugin["path"].get<std::string>();
528
load_plugin(plugin_path);
529
}
530
} else {
531
plugin_file_in.close();
532
auto empty_json = nlohmann::json::array();
533
std::ofstream plugin_file_out("plugins.json");
534
plugin_file_out << empty_json.dump(4);
535
plugin_file_out.close();
536
}
537
}
538
539
void on_activate() override {
540
window->present();
541
}
542
543
void set_search_engines_from_json(const std::string &json_string) {
544
auto json = nlohmann::json::parse(json_string);
545
Glib::ustring default_search_engine_name;
546
for(auto &engine : json) {
547
SearchEngine search_engine;
548
search_engine.name = engine["name"].get<std::string>();
549
search_engine.url = engine["url"].get<std::string>();
550
search_engines.push_back(search_engine);
551
if(engine.contains("default") && engine["default"].get<bool>()) {
552
default_search_engine_name = engine["name"].get<std::string>();
553
}
554
}
555
for(auto &search_engine : search_engines) {
556
if(search_engine.name == default_search_engine_name) {
557
default_search_engine = &search_engine;
558
break;
559
}
560
}
561
}
562
public:
563
static Glib::RefPtr<PantheraWww> create() {
564
return Glib::make_refptr_for_instance<PantheraWww>(new PantheraWww());
565
}
566
567
std::shared_ptr<gPanthera::LayoutManager> get_layout_manager() {
568
return layout_manager;
569
}
570
571
std::shared_ptr<gPanthera::ContentManager> get_content_manager() {
572
return content_manager;
573
}
574
575
void load_plugin(const std::string &plugin_path) {
576
void *handle = dlopen(plugin_path.c_str(), RTLD_LAZY | RTLD_GLOBAL);
577
if(!handle) {
578
std::cerr << "Failed to load plugin: " << dlerror() << std::endl;
579
return;
580
}
581
auto entrypoint = (plugin_entrypoint_type)dlsym(handle, "plugin_init");
582
if(!entrypoint) {
583
std::cerr << "Failed to find entrypoint in plugin: " << dlerror() << std::endl;
584
dlclose(handle);
585
return;
586
}
587
entrypoint();
588
}
589
};
590
591
Glib::RefPtr<PantheraWww> app = nullptr;
592
593
extern "C" {
594
void panthera_log(const char *message) {
595
std::cerr << message << std::endl;
596
}
597
598
void panthera_add_pane(const char *id, const char *label, GtkImage *icon, GtkWidget *pane) {
599
auto pane_child = Glib::wrap(pane);
600
auto icon_cc = Glib::wrap(icon);
601
auto label_cc = Glib::ustring(label);
602
auto id_cc = Glib::ustring(id);
603
auto new_pane = Gtk::make_managed<gPanthera::DockablePane>(app->get_layout_manager(), *pane_child, id_cc, label_cc, icon_cc);
604
app->get_layout_manager()->add_pane(new_pane);
605
}
606
}
607
608
int main(int argc, char *argv[]) {
609
gPanthera::init();
610
app = PantheraWww::create();
611
return app->run(argc, argv);
612
}
613