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++ • 21.8 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
17
std::string url_encode(const std::string &value) {
18
// Thanks https://stackoverflow.com/a/17708801
19
std::ostringstream escaped;
20
escaped.fill('0');
21
escaped << std::hex;
22
23
for(char c : value) {
24
// Keep alphanumeric and other accepted characters intact
25
if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
26
escaped << c;
27
continue;
28
}
29
30
// Any other characters are percent-encoded
31
escaped << std::uppercase;
32
escaped << '%' << std::setw(2) << int((unsigned char) c);
33
escaped << std::nouppercase;
34
}
35
36
return escaped.str();
37
}
38
39
struct SearchEngine {
40
Glib::ustring name;
41
Glib::ustring url;
42
Glib::ustring get_search_url(const std::string &query) {
43
auto pos = url.find("%s");
44
if(pos == Glib::ustring::npos) {
45
throw std::runtime_error("Invalid search engine URL: missing '%s' placeholder");
46
}
47
auto new_url = url.substr(0, pos);
48
new_url.replace(pos, 2, url_encode(query));
49
return new_url;
50
}
51
};
52
53
class PantheraWww : public Gtk::Application {
54
Gtk::Window *window = Gtk::make_managed<Gtk::Window>();
55
protected:
56
std::shared_ptr<gPanthera::LayoutManager> layout_manager;
57
std::shared_ptr<gPanthera::ContentManager> content_manager;
58
Gtk::Entry *url_bar = nullptr;
59
std::string cookie_file = "cookies.txt";
60
std::vector<SearchEngine> search_engines;
61
SearchEngine *default_search_engine = nullptr;
62
63
static void notify_callback(GObject *object, GParamSpec *pspec, gpointer data) {
64
if(!gtk_widget_get_parent(GTK_WIDGET(object))) {
65
return;
66
}
67
auto parent = gtk_widget_get_parent(gtk_widget_get_parent(GTK_WIDGET(object)));
68
if(auto page = dynamic_cast<gPanthera::ContentPage*>(Glib::wrap(parent))) {
69
if(g_strcmp0(pspec->name, "title") == 0) {
70
if(auto label = dynamic_cast<Gtk::Label*>(page->tab_widget)) {
71
label->set_label(webkit_web_view_get_title(WEBKIT_WEB_VIEW(object)));
72
}
73
}
74
}
75
}
76
77
static void notify_focused_callback(GObject *object, GParamSpec *pspec, gpointer data) {
78
if(!gtk_widget_get_parent(GTK_WIDGET(object))) {
79
return;
80
}
81
auto this_ = static_cast<PantheraWww*>(data);
82
auto parent = gtk_widget_get_parent(gtk_widget_get_parent(GTK_WIDGET(object)));
83
if(auto page = dynamic_cast<gPanthera::ContentPage*>(Glib::wrap(parent))) {
84
if(g_strcmp0(pspec->name, "uri") == 0) {
85
this_->url_bar->set_text(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(object)));
86
}
87
}
88
}
89
90
static void on_back_pressed(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) {
91
WebKitWebView *webview = WEBKIT_WEB_VIEW(user_data);
92
webkit_web_view_go_back(webview);
93
}
94
95
static void on_forward_pressed(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) {
96
WebKitWebView *webview = WEBKIT_WEB_VIEW(user_data);
97
webkit_web_view_go_forward(webview);
98
}
99
100
static gboolean on_decide_policy(WebKitWebView *source, WebKitPolicyDecision *decision, WebKitPolicyDecisionType type, gpointer user_data) {
101
// Middle-click opens in a new window
102
if(auto self = static_cast<PantheraWww*>(user_data)) {
103
std::cout << type << '\n';
104
if(type == WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION) {
105
auto action = webkit_navigation_policy_decision_get_navigation_action(WEBKIT_NAVIGATION_POLICY_DECISION(decision));
106
if(webkit_navigation_action_get_mouse_button(action) == 2) {
107
Glib::ustring url = Glib::ustring(webkit_uri_request_get_uri(webkit_navigation_action_get_request(action)));
108
self->on_new_tab(nullptr, url, false);
109
webkit_policy_decision_ignore(decision);
110
return true;
111
}
112
} else if(type == WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION) {
113
auto action = webkit_navigation_policy_decision_get_navigation_action(WEBKIT_NAVIGATION_POLICY_DECISION(decision));
114
self->on_new_tab(nullptr, webkit_uri_request_get_uri(webkit_navigation_action_get_request(action)), false, true);
115
return true;
116
}
117
}
118
return false;
119
}
120
121
void on_new_tab(gPanthera::ContentStack *stack, const Glib::ustring &url = "about:blank", bool focus = true, bool new_window = false) {
122
if(!stack) {
123
// Find the current area
124
stack = content_manager->get_last_operated_page()->get_stack();
125
}
126
127
WebKitWebView *webview = WEBKIT_WEB_VIEW(webkit_web_view_new());
128
gtk_widget_set_hexpand(GTK_WIDGET(webview), true);
129
gtk_widget_set_vexpand(GTK_WIDGET(webview), true);
130
auto page_content = Gtk::make_managed<Gtk::Box>();
131
gtk_box_append(page_content->gobj(), GTK_WIDGET(webview));
132
auto page_tab = new Gtk::Label("Untitled");
133
auto page = Gtk::make_managed<gPanthera::ContentPage>(content_manager, stack, page_content, page_tab);
134
g_signal_connect(webview, "notify", G_CALLBACK(notify_callback), page->gobj());
135
g_signal_connect(webview, "decide-policy", G_CALLBACK(on_decide_policy), this);
136
webkit_web_view_load_uri(webview, url.data());
137
auto cookie_manager = webkit_network_session_get_cookie_manager(webkit_web_view_get_network_session(webview));
138
webkit_cookie_manager_set_persistent_storage(cookie_manager, cookie_file.c_str(), WEBKIT_COOKIE_PERSISTENT_STORAGE_TEXT);
139
webkit_cookie_manager_set_accept_policy(cookie_manager, WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS);
140
GtkEventController *click_controller_back = GTK_EVENT_CONTROLLER(gtk_gesture_click_new());
141
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click_controller_back), 8);
142
g_signal_connect(click_controller_back, "pressed", G_CALLBACK(on_back_pressed), webview);
143
gtk_widget_add_controller(GTK_WIDGET(webview), click_controller_back);
144
145
GtkEventController *click_controller_forward = GTK_EVENT_CONTROLLER(gtk_gesture_click_new());
146
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click_controller_forward), 9);
147
g_signal_connect(click_controller_forward, "pressed", G_CALLBACK(on_forward_pressed), webview);
148
gtk_widget_add_controller(GTK_WIDGET(webview), click_controller_forward);
149
150
stack->add_page(*page);
151
if(focus) {
152
stack->set_visible_child(*page);
153
content_manager->set_last_operated_page(page);
154
}
155
if(new_window) {
156
bool result = stack->signal_detach.emit(page);
157
}
158
}
159
160
void on_startup() override {
161
Gtk::Application::on_startup();
162
add_window(*window);
163
window->set_default_size(600, 400);
164
layout_manager = std::make_shared<gPanthera::LayoutManager>();
165
auto dock_stack_1 = Gtk::make_managed<gPanthera::DockStack>(layout_manager, "One", "one");
166
auto switcher_1 = Gtk::make_managed<gPanthera::DockStackSwitcher>(dock_stack_1, Gtk::Orientation::HORIZONTAL);
167
auto dock_stack_2 = Gtk::make_managed<gPanthera::DockStack>(layout_manager, "Two", "two");
168
auto switcher_2 = Gtk::make_managed<gPanthera::DockStackSwitcher>(dock_stack_2, Gtk::Orientation::VERTICAL);
169
auto pane_1_content = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0);
170
auto debug_button = Gtk::make_managed<Gtk::Button>("Debug");
171
pane_1_content->append(*debug_button);
172
auto pane_2_content = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0);
173
auto test_notebook = Gtk::make_managed<Gtk::Notebook>();
174
test_notebook->append_page(*Gtk::make_managed<Gtk::Label>("Test 1"), *Gtk::make_managed<Gtk::Label>("Test 1"));
175
pane_2_content->append(*test_notebook);
176
auto pane_3_content = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0);
177
pane_3_content->append(*Gtk::make_managed<Gtk::Label>("Pane 3 content"));
178
auto pane_1_icon = Gtk::make_managed<Gtk::Image>();
179
pane_1_icon->set_from_icon_name("go-home-symbolic");
180
auto pane_2_icon = Gtk::make_managed<Gtk::Image>();
181
pane_2_icon->set_from_icon_name("folder-symbolic");
182
auto pane_3_icon = Gtk::make_managed<Gtk::Image>();
183
pane_3_icon->set_from_icon_name("network-transmit-receive-symbolic");
184
auto pane_1 = Gtk::make_managed<gPanthera::DockablePane>(layout_manager, *pane_1_content, "pane1", "Pane 1", pane_1_icon);
185
auto pane_2 = Gtk::make_managed<gPanthera::DockablePane>(layout_manager, *pane_2_content, "pane2", "Pane 2", pane_2_icon);
186
auto pane_3 = Gtk::make_managed<gPanthera::DockablePane>(layout_manager, *pane_3_content, "pane3", "Pane 3", pane_3_icon);
187
188
dock_stack_1->set_transition_type(Gtk::StackTransitionType::SLIDE_LEFT_RIGHT);
189
dock_stack_1->set_transition_duration(125);
190
dock_stack_1->set_expand(true);
191
dock_stack_2->set_transition_type(Gtk::StackTransitionType::SLIDE_UP_DOWN);
192
dock_stack_2->set_transition_duration(125);
193
dock_stack_2->set_expand(true);
194
195
auto outer_grid = Gtk::make_managed<Gtk::Grid>();
196
outer_grid->attach(*switcher_2, 0, 1, 1, 1);
197
outer_grid->attach(*switcher_1, 1, 2, 1, 1);
198
auto outer_paned = Gtk::make_managed<Gtk::Paned>(Gtk::Orientation::HORIZONTAL);
199
outer_paned->set_start_child(*dock_stack_2);
200
auto inner_paned = Gtk::make_managed<Gtk::Paned>(Gtk::Orientation::VERTICAL);
201
auto content = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0);
202
content_manager = std::make_shared<gPanthera::ContentManager>();
203
std::function<bool(gPanthera::ContentPage*)> detach_handler;
204
detach_handler = [](gPanthera::ContentPage *widget) {
205
auto new_stack = Gtk::make_managed<gPanthera::ContentStack>(widget->content_manager, widget->get_stack()->get_detach_handler());
206
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());
207
auto new_notebook = Gtk::make_managed<gPanthera::ContentNotebook>(new_stack, new_switcher);
208
auto window = new gPanthera::ContentWindow(new_notebook);
209
widget->redock(new_stack);
210
window->present();
211
new_stack->signal_leave_empty.connect([window]() {
212
window->close();
213
delete window;
214
});
215
return true;
216
};
217
218
auto return_extra_child = [this](gPanthera::ContentTabBar *switcher) {
219
auto new_tab_button = Gtk::make_managed<Gtk::Button>();
220
new_tab_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("list-add-symbolic")));
221
new_tab_button->set_tooltip_text("New tab");
222
new_tab_button->signal_clicked().connect([this, switcher]() {
223
on_new_tab(switcher->get_stack());
224
});
225
return new_tab_button;
226
};
227
auto content_stack = Gtk::make_managed<gPanthera::ContentStack>(content_manager, detach_handler);
228
auto content_stack_switcher = Gtk::make_managed<gPanthera::ContentTabBar>(content_stack, Gtk::Orientation::HORIZONTAL, return_extra_child);
229
content_manager->add_stack(content_stack);
230
WebKitWebView *webview = WEBKIT_WEB_VIEW(webkit_web_view_new()); // for some reason, this has to be created
231
content->set_name("content_box");
232
auto content_notebook = Gtk::make_managed<gPanthera::ContentNotebook>(content_stack, content_stack_switcher, Gtk::PositionType::TOP);
233
content->append(*content_notebook);
234
inner_paned->set_start_child(*content);
235
inner_paned->set_end_child(*dock_stack_1);
236
outer_paned->set_end_child(*inner_paned);
237
outer_grid->attach(*outer_paned, 1, 1, 1, 1);
238
239
// Get search engines
240
std::ifstream search_engines_file_in("search_engines.json");
241
if(search_engines_file_in) {
242
std::string search_engines_json((std::istreambuf_iterator<char>(search_engines_file_in)), std::istreambuf_iterator<char>());
243
search_engines_file_in.close();
244
set_search_engines_from_json(search_engines_json);
245
} else {
246
search_engines_file_in.close();
247
auto empty_json = nlohmann::json::array();
248
std::ofstream search_engines_file_out("search_engines.json");
249
search_engines_file_out << empty_json.dump(4);
250
}
251
252
// Create the toolbar
253
auto main_toolbar = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 8);
254
// URL bar
255
url_bar = Gtk::make_managed<Gtk::Entry>();
256
url_bar->set_placeholder_text("Enter URL");
257
url_bar->set_hexpand(true);
258
auto load_url_callback = [this]() {
259
auto page = content_manager->get_last_operated_page();
260
if(page) {
261
if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) {
262
webkit_web_view_load_uri(webview, url_bar->get_text().c_str());
263
}
264
}
265
};
266
// Go
267
auto go_button = Gtk::make_managed<Gtk::Button>("Go");
268
go_button->signal_clicked().connect(load_url_callback);
269
url_bar->signal_activate().connect(load_url_callback);
270
content_manager->signal_page_operated.connect([this](gPanthera::ContentPage *page) {
271
if(!page->get_child()) {
272
return;
273
}
274
if(!page->get_child()->get_first_child()) {
275
return;
276
}
277
url_bar->set_text(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())));
278
guint url_update_handler = g_signal_connect(page->get_child()->get_first_child()->gobj(), "notify", G_CALLBACK(notify_focused_callback), this);
279
std::shared_ptr<sigc::connection> control_signal_handler = std::make_shared<sigc::connection>();
280
*control_signal_handler = page->signal_control_status_changed.connect([this, page, control_signal_handler, url_update_handler](bool controlled) {
281
if(!controlled) {
282
control_signal_handler->disconnect();
283
if(page->get_child() && page->get_child()->get_first_child() && WEBKIT_IS_WEB_VIEW(page->get_child()->get_first_child()->gobj())) {
284
g_signal_handler_disconnect(page->get_child()->get_first_child()->gobj(), url_update_handler);
285
}
286
}
287
});
288
});
289
// Back, forward, reload
290
auto back_button = Gtk::make_managed<Gtk::Button>();
291
back_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-previous-symbolic")));
292
back_button->set_tooltip_text("Back");
293
auto forward_button = Gtk::make_managed<Gtk::Button>();
294
forward_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-next-symbolic")));
295
forward_button->set_tooltip_text("Forward");
296
auto reload_button = Gtk::make_managed<Gtk::Button>();
297
reload_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("view-refresh-symbolic")));
298
reload_button->set_tooltip_text("Reload");
299
back_button->signal_clicked().connect([this]() {
300
auto page = content_manager->get_last_operated_page();
301
if(page) {
302
if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) {
303
webkit_web_view_go_back(webview);
304
}
305
}
306
});
307
forward_button->signal_clicked().connect([this]() {
308
auto page = content_manager->get_last_operated_page();
309
if(page) {
310
if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) {
311
webkit_web_view_go_forward(webview);
312
}
313
}
314
});
315
reload_button->signal_clicked().connect([this]() {
316
auto page = content_manager->get_last_operated_page();
317
if(page) {
318
if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) {
319
webkit_web_view_reload(webview);
320
}
321
}
322
});
323
// Search bar
324
// TODO: provide history, provide a menu button with the other engines
325
auto search_bar = Gtk::make_managed<Gtk::Entry>();
326
search_bar->set_placeholder_text("Search");
327
search_bar->set_hexpand(true);
328
if(search_engines.empty()) {
329
search_bar->set_sensitive(false);
330
search_bar->set_placeholder_text("No search");
331
}
332
auto search_callback = [this, search_bar, content_stack]() {
333
// Create a new tab with the search results
334
if(content_manager->get_last_operated_page()) {
335
on_new_tab(nullptr);
336
} else {
337
on_new_tab(content_stack);
338
}
339
auto page = content_manager->get_last_operated_page();
340
if(page) {
341
if(auto webview = WEBKIT_WEB_VIEW(page->get_child()->get_first_child()->gobj())) {
342
auto search_url = default_search_engine->get_search_url(search_bar->get_text());
343
webkit_web_view_load_uri(webview, search_url.c_str());
344
}
345
}
346
};
347
if(default_search_engine) {
348
search_bar->signal_activate().connect(search_callback);
349
}
350
// Assemble the toolbar
351
main_toolbar->append(*back_button);
352
main_toolbar->append(*forward_button);
353
main_toolbar->append(*reload_button);
354
main_toolbar->append(*url_bar);
355
main_toolbar->append(*go_button);
356
main_toolbar->append(*search_bar);
357
if(default_search_engine) {
358
auto search_button = Gtk::make_managed<Gtk::Button>(default_search_engine->name);
359
search_button->signal_clicked().connect(search_callback);
360
main_toolbar->append(*search_button);
361
}
362
outer_grid->attach(*main_toolbar, 0, 0, 2, 1);
363
window->set_child(*outer_grid);
364
debug_button->signal_clicked().connect([this]() {
365
if(content_manager->get_last_operated_page()) {
366
std::cout << "Last operated page: " << content_manager->get_last_operated_page()->get_name() << std::endl;
367
} else {
368
std::cout << "No page operated!" << std::endl;
369
}
370
});
371
// TODO: Use the last operated page and allow opening tabs next to the last operated page using certain panes
372
// Load the existing layout, if it exists
373
std::ifstream layout_file_in("layout.json");
374
if(layout_file_in) {
375
std::string layout_json((std::istreambuf_iterator<char>(layout_file_in)), std::istreambuf_iterator<char>());
376
layout_file_in.close();
377
layout_manager->restore_json_layout(layout_json);
378
} else {
379
// Create a new layout if the file doesn't exist
380
layout_file_in.close();
381
382
dock_stack_1->add_pane(*pane_1);
383
dock_stack_1->add_pane(*pane_3);
384
dock_stack_2->add_pane(*pane_2);
385
386
std::ofstream layout_file_out("layout.json");
387
layout_file_out << layout_manager->get_layout_as_json();
388
layout_file_out.close();
389
}
390
// Save the layout when changed
391
layout_manager->signal_pane_moved.connect([this](gPanthera::DockablePane *pane) {
392
std::ofstream layout_file_out("layout.json");
393
layout_file_out << layout_manager->get_layout_as_json();
394
layout_file_out.close();
395
std::cout << "Layout changed: " << layout_manager->get_layout_as_json() << std::endl;
396
});
397
398
auto new_tab_action = Gio::SimpleAction::create("new_tab");
399
new_tab_action->signal_activate().connect([this](const Glib::VariantBase&) {
400
on_new_tab(nullptr);
401
});
402
add_action(new_tab_action);
403
set_accels_for_action("app.new_tab", {"<Primary>T"});
404
auto close_tab_action = Gio::SimpleAction::create("close_tab");
405
close_tab_action->signal_activate().connect([this](const Glib::VariantBase&) {
406
auto page = content_manager->get_last_operated_page();
407
if(page) {
408
page->close();
409
}
410
});
411
add_action(close_tab_action);
412
set_accels_for_action("app.close_tab", {"<Primary>W"});
413
}
414
415
void on_activate() override {
416
window->present();
417
}
418
419
void set_search_engines_from_json(const std::string &json_string) {
420
auto json = nlohmann::json::parse(json_string);
421
Glib::ustring default_search_engine_name;
422
for(auto &engine : json) {
423
SearchEngine search_engine;
424
search_engine.name = engine["name"].get<std::string>();
425
search_engine.url = engine["url"].get<std::string>();
426
search_engines.push_back(search_engine);
427
if(engine.contains("default") && engine["default"].get<bool>()) {
428
default_search_engine_name = engine["name"].get<std::string>();
429
}
430
}
431
for(auto &search_engine : search_engines) {
432
if(search_engine.name == default_search_engine_name) {
433
default_search_engine = &search_engine;
434
break;
435
}
436
}
437
}
438
public:
439
static Glib::RefPtr<PantheraWww> create() {
440
return Glib::make_refptr_for_instance<PantheraWww>(new PantheraWww());
441
}
442
};
443
444
int main(int argc, char *argv[]) {
445
gPanthera::init();
446
auto app = PantheraWww::create();
447
return app->run(argc, argv);
448
}