roundabout,
created on Wednesday, 28 May 2025, 19:32:54 (1748460774),
received on Wednesday, 28 May 2025, 19:32:57 (1748460777)
Author identity: vlad <vlad.muntoiu@gmail.com>
c0e8b6372cce674f16237e61cf4b05cc4f5671a2
gpanthera.cc
@@ -59,6 +59,9 @@ namespace gPanthera {
auto new_image = Gtk::make_managed<Gtk::Image>();
new_image->set_from_icon_name(image->get_icon_name());
return new_image;
} else if(image->get_storage_type() == Gtk::Image::Type::GICON) {
auto new_image = Gtk::make_managed<Gtk::Image>(image->get_gicon());
return new_image;
} else {
return nullptr;
}
@@ -197,6 +200,7 @@ namespace gPanthera {
this->header->get_style_context()->add_class("gpanthera-dock-titlebar-popout");
}
this->window->present();
this->signal_pane_shown.emit();
layout->signal_pane_moved.emit(this);
}
@@ -246,16 +250,24 @@ namespace gPanthera {
// Hide the stack when no child is visible
this->property_visible_child_name().signal_changed().connect([this]() {
if(last_pane) {
last_pane->signal_pane_hidden.emit();
}
if(this->get_visible_child_name() == "") {
this->hide();
} else {
this->show();
}
last_pane = dynamic_cast<DockablePane*>(this->get_visible_child());
if(last_pane) {
last_pane->signal_pane_shown.emit();
}
});
// Also hide when the visible child is removed
this->signal_child_removed.connect([this](Gtk::Widget* const &child) {
if(this->get_visible_child_name() == "") {
last_pane = nullptr;
this->hide();
}
});
@@ -309,7 +321,9 @@ namespace gPanthera {
DockButton::DockButton(DockablePane *pane) : Gtk::ToggleButton(), pane(pane) {
if(pane->get_icon()) {
this->set_child(*copy_image(pane->get_icon()));
if(auto icon_copy = copy_image(pane->get_icon())) {
this->set_child(*icon_copy);
}
}
this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB);
this->set_tooltip_text(pane->get_label()->get_text());
gpanthera.hh
@@ -12,20 +12,22 @@ namespace gPanthera {
void init();
// Utility function to create a signal that fires when a context menu is requested
sigc::signal<void(double, double)> add_context_menu(Gtk::Widget &widget);
std::vector<Gtk::Widget*> collect_children(Gtk::Widget &widget);
Gtk::Image *copy_image(Gtk::Image *image);
class DockStack;
class DockWindow;
class LayoutManager;
class DockablePane : public Gtk::Box {
private:
protected:
Gtk::Widget *child;
Gtk::Label label;
Glib::ustring name;
Gtk::Image *icon;
DockStack *stack = nullptr;
DockWindow *window = nullptr;
std::unique_ptr<Gtk::HeaderBar> header;
Gtk::Widget *child;
Glib::RefPtr<Gio::SimpleActionGroup> action_group;
public:
DockWindow *get_window() const;
@@ -40,6 +42,8 @@ namespace gPanthera {
void redock(DockStack *stack);
void pop_out();
Gtk::Widget *get_child() const;
sigc::signal<void()> signal_pane_shown;
sigc::signal<void()> signal_pane_hidden;
};
class DockWindow : public Gtk::Window {
@@ -84,6 +88,7 @@ namespace gPanthera {
std::shared_ptr<LayoutManager> layout;
Glib::ustring name;
std::string id;
DockablePane *last_pane = nullptr;
explicit DockStack(std::shared_ptr<LayoutManager> layout, const Glib::ustring &name, const std::string &id);
void add_pane(DockablePane &child);
panthera-www.cc
@@ -57,7 +57,7 @@ struct SearchEngine {
struct HistoryEntry {
int id;
std::string url;
std::string url, title;
int64_t timestamp;
};
@@ -65,33 +65,137 @@ using StorageType = decltype(sqlite_orm::make_storage("",
sqlite_orm::make_table("history",
sqlite_orm::make_column("id", &HistoryEntry::id, sqlite_orm::primary_key()),
sqlite_orm::make_column("url", &HistoryEntry::url),
sqlite_orm::make_column("title", &HistoryEntry::title),
sqlite_orm::make_column("timestamp", &HistoryEntry::timestamp)
)
));
class HistoryManager {
public:
static auto make_storage(const std::string& path) {
return sqlite_orm::make_storage(path,
sqlite_orm::make_table("history",
sqlite_orm::make_column("id", &HistoryEntry::id, sqlite_orm::primary_key()),
sqlite_orm::make_column("url", &HistoryEntry::url),
sqlite_orm::make_column("title", &HistoryEntry::title),
sqlite_orm::make_column("timestamp", &HistoryEntry::timestamp)
)
);
}
StorageType storage;
public:
explicit HistoryManager(const std::string &db_path) : storage(make_storage(db_path)) {
storage.sync_schema();
}
void log_url(const std::string &url, const int64_t timestamp = std::chrono::system_clock::now().time_since_epoch().count()) {
storage.insert(HistoryEntry{-1, url, timestamp});
void log_url(const std::string &url, const std::string title, const int64_t timestamp = Glib::DateTime::create_now_local().to_unix()) {
storage.insert(HistoryEntry{-1, url, title, timestamp});
}
std::vector<HistoryEntry> get_history() {
return storage.get_all<HistoryEntry>();
}
std::vector<HistoryEntry> get_history_between(int64_t start_time, int64_t end_time) {
using namespace sqlite_orm;
return storage.get_all<HistoryEntry>(
where(c(&HistoryEntry::timestamp) >= start_time and c(&HistoryEntry::timestamp) < end_time),
order_by(&HistoryEntry::timestamp).desc()
);
}
};
class HistoryViewer : public gPanthera::DockablePane {
protected:
std::shared_ptr<HistoryManager> history_manager;
Gtk::Calendar *calendar;
Gtk::Label *date_label;
Gtk::Box *box_place;
public:
sigc::signal<void(const std::string &url)> signal_open_url;
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)) {
auto history_toolbar = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 0);
auto history_button_previous = Gtk::make_managed<Gtk::Button>();
history_button_previous->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-previous-symbolic")));
history_button_previous->set_tooltip_text("Previous day");
auto history_button_next = Gtk::make_managed<Gtk::Button>();
history_button_next->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-next-symbolic")));
history_button_next->set_tooltip_text("Next day");
date_label = Gtk::make_managed<Gtk::Label>("");
auto date_button = Gtk::make_managed<Gtk::MenuButton>();
date_button->set_child(*date_label);
date_button->set_tooltip_text("Select date");
date_button->set_direction(Gtk::ArrowType::NONE);
auto date_popover = Gtk::make_managed<Gtk::Popover>();
calendar = Gtk::make_managed<Gtk::Calendar>();
date_popover->set_child(*calendar);
date_button->set_popover(*date_popover);
date_button->set_has_frame(false);
date_button->set_hexpand(true);
history_toolbar->append(*history_button_previous);
history_toolbar->append(*date_button);
history_toolbar->append(*history_button_next);
auto box = dynamic_cast<Gtk::Box*>(this->child);
box->append(*history_toolbar);
calendar->signal_day_selected().connect(sigc::mem_fun(*this, &HistoryViewer::reload_history));
Glib::DateTime datetime = Glib::DateTime::create_now_local();
calendar->select_day(datetime);
this->signal_pane_shown.connect([this]() {
// Load the current date
reload_history();
});
box_place = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0);
box->append(*box_place);
// TODO: make the arrows work, they should move to the previous/next day with any entries
// TODO: allow deleting entries
}
void reload_history() {
Glib::DateTime calendar_day = calendar->get_date();
Glib::DateTime datetime = Glib::DateTime::create_local(
calendar_day.get_year(),
calendar_day.get_month(),
calendar_day.get_day_of_month(),
0, 0, 0
);
date_label->set_text(datetime.format("%x"));
auto box = Gtk::make_managed<Gtk::ListBox>();
auto history_entries = history_manager->get_history_between(datetime.to_unix(), datetime.to_unix() + 86400);
for(const auto &entry : history_entries) {
auto row = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 4);
auto title = Gtk::make_managed<Gtk::Label>(entry.title.empty() ? "Untitled" : entry.title);
title->set_halign(Gtk::Align::START);
title->set_ellipsize(Pango::EllipsizeMode::MIDDLE);
auto label = Gtk::make_managed<Gtk::Label>(entry.url);
label->set_halign(Gtk::Align::START);
label->set_ellipsize(Pango::EllipsizeMode::MIDDLE);
row->append(*title);
row->append(*label);
if(auto time = Glib::DateTime::create_now_local(entry.timestamp)) {
label->set_tooltip_text(time.format("%c"));
box->append(*row);
}
}
if(box_place->get_first_child()) {
box_place->remove(*box_place->get_first_child());
}
box->signal_row_activated().connect([this](Gtk::ListBoxRow *row) {
if(auto box_row = dynamic_cast<Gtk::Box*>(row->get_child())) {
if(auto label = dynamic_cast<Gtk::Label*>(box_row->get_last_child())) {
auto url = label->get_text();
if(!url.empty()) {
signal_open_url.emit(url);
}
}
}
});
box_place->append(*box);
}
};
class PantheraWww : public Gtk::Application {
@@ -104,6 +208,7 @@ protected:
std::vector<SearchEngine> search_engines;
SearchEngine *default_search_engine = nullptr;
std::shared_ptr<HistoryManager> history_manager;
HistoryViewer *history_viewer = nullptr;
static void notify_callback(GObject *object, GParamSpec *pspec, gpointer data) {
if(!gtk_widget_get_parent(GTK_WIDGET(object))) {
@@ -129,7 +234,15 @@ protected:
}
}
} else if(g_strcmp0(pspec->name, "uri") == 0) {
self->history_manager->log_url(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(object)));
if(webkit_web_view_get_title(WEBKIT_WEB_VIEW(object)) == nullptr) {
self->history_manager->log_url(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(object)), "");
} else {
self->history_manager->log_url(webkit_web_view_get_uri(WEBKIT_WEB_VIEW(object)),
webkit_web_view_get_title(WEBKIT_WEB_VIEW(object)));
}
if(self->history_viewer->get_visible()) {
self->history_viewer->reload_history();
}
}
}
}
@@ -251,21 +364,8 @@ protected:
});
pane_1_content->append(*debug_button);
pane_1_content->append(*print_history_button);
auto pane_2_content = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0);
auto test_notebook = Gtk::make_managed<Gtk::Notebook>();
test_notebook->append_page(*Gtk::make_managed<Gtk::Label>("Test 1"), *Gtk::make_managed<Gtk::Label>("Test 1"));
pane_2_content->append(*test_notebook);
auto pane_3_content = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0);
pane_3_content->append(*Gtk::make_managed<Gtk::Label>("Pane 3 content"));
auto pane_1_icon = Gtk::make_managed<Gtk::Image>();
pane_1_icon->set_from_icon_name("go-home-symbolic");
auto pane_2_icon = Gtk::make_managed<Gtk::Image>();
pane_2_icon->set_from_icon_name("folder-symbolic");
auto pane_3_icon = Gtk::make_managed<Gtk::Image>();
pane_3_icon->set_from_icon_name("network-transmit-receive-symbolic");
auto pane_1 = Gtk::make_managed<gPanthera::DockablePane>(layout_manager, *pane_1_content, "pane1", "Pane 1", pane_1_icon);
auto pane_2 = Gtk::make_managed<gPanthera::DockablePane>(layout_manager, *pane_2_content, "pane2", "Pane 2", pane_2_icon);
auto pane_3 = Gtk::make_managed<gPanthera::DockablePane>(layout_manager, *pane_3_content, "pane3", "Pane 3", pane_3_icon);
auto pane_1_icon = Gtk::make_managed<Gtk::Image>(Gio::Icon::create("help-about-symbolic"));
auto pane_1 = Gtk::make_managed<gPanthera::DockablePane>(layout_manager, *pane_1_content, "debug", "Debugging options", pane_1_icon);
dock_stack_1->set_transition_type(Gtk::StackTransitionType::SLIDE_LEFT_RIGHT);
dock_stack_1->set_transition_duration(125);
@@ -373,6 +473,10 @@ protected:
});
});
history_manager = std::make_shared<HistoryManager>("history.db");
history_viewer = Gtk::make_managed<HistoryViewer>(layout_manager, history_manager);
history_viewer->signal_open_url.connect([this](const std::string &url) {
on_new_tab(nullptr, url, true);
});
// Back, forward, reload
auto back_button = Gtk::make_managed<Gtk::Button>();
back_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-previous-symbolic")));
@@ -467,8 +571,7 @@ protected:
layout_file_in.close();
dock_stack_1->add_pane(*pane_1);
dock_stack_1->add_pane(*pane_3);
dock_stack_2->add_pane(*pane_2);
dock_stack_1->add_pane(*history_viewer);
std::ofstream layout_file_out("layout.json");
layout_file_out << layout_manager->get_layout_as_json();