|
|
@@ -0,0 +1,169 @@
|
|
|
+#include "TagManagerDialog.hpp"
|
|
|
+#include <glibmm/markup.h>
|
|
|
+
|
|
|
+TagManagerDialog::TagManagerDialog(Gtk::Window& parent, DatabaseManager& db)
|
|
|
+ : Gtk::Dialog("Manage Tags", parent, true),
|
|
|
+ m_db(db) {
|
|
|
+
|
|
|
+ set_default_size(400, 450);
|
|
|
+ set_modal(true);
|
|
|
+
|
|
|
+ // Content area
|
|
|
+ m_contentBox.set_spacing(8);
|
|
|
+ m_contentBox.set_margin_top(12);
|
|
|
+ m_contentBox.set_margin_bottom(12);
|
|
|
+ m_contentBox.set_margin_start(12);
|
|
|
+ m_contentBox.set_margin_end(12);
|
|
|
+
|
|
|
+ // Empty state label
|
|
|
+ m_emptyLabel.set_markup("<span size='large'>No tags</span>\n<small>Tags will appear here when you add them to books.</small>");
|
|
|
+ m_emptyLabel.set_halign(Gtk::Align::CENTER);
|
|
|
+ m_emptyLabel.set_valign(Gtk::Align::CENTER);
|
|
|
+ m_emptyLabel.set_vexpand(true);
|
|
|
+
|
|
|
+ // Scrolled list
|
|
|
+ m_scrolled.set_policy(Gtk::PolicyType::NEVER, Gtk::PolicyType::AUTOMATIC);
|
|
|
+ m_scrolled.set_vexpand(true);
|
|
|
+ m_tagList.set_selection_mode(Gtk::SelectionMode::NONE);
|
|
|
+ m_tagList.add_css_class("boxed-list");
|
|
|
+ m_scrolled.set_child(m_tagList);
|
|
|
+
|
|
|
+ // Cleanup button
|
|
|
+ m_cleanupButton.set_label("Clean Up Unused");
|
|
|
+ m_cleanupButton.set_tooltip_text("Remove tags that aren't assigned to any books");
|
|
|
+ m_cleanupButton.signal_clicked().connect(sigc::mem_fun(*this, &TagManagerDialog::on_cleanup_clicked));
|
|
|
+ m_buttonBox.set_halign(Gtk::Align::END);
|
|
|
+ m_buttonBox.append(m_cleanupButton);
|
|
|
+
|
|
|
+ m_contentBox.append(m_scrolled);
|
|
|
+ m_contentBox.append(m_buttonBox);
|
|
|
+
|
|
|
+ get_content_area()->append(m_contentBox);
|
|
|
+
|
|
|
+ // Setup rename popover
|
|
|
+ m_renameBox.set_spacing(6);
|
|
|
+ m_renameBox.set_margin_top(6);
|
|
|
+ m_renameBox.set_margin_bottom(6);
|
|
|
+ m_renameBox.set_margin_start(6);
|
|
|
+ m_renameBox.set_margin_end(6);
|
|
|
+
|
|
|
+ m_renameEntry.set_placeholder_text("New tag name...");
|
|
|
+ m_renameEntry.signal_activate().connect([this]() {
|
|
|
+ m_renameApplyButton.activate();
|
|
|
+ });
|
|
|
+
|
|
|
+ m_renameApplyButton.set_label("Rename");
|
|
|
+ m_renameApplyButton.add_css_class("suggested-action");
|
|
|
+ m_renameApplyButton.signal_clicked().connect([this]() {
|
|
|
+ const auto new_name = m_renameEntry.get_text();
|
|
|
+ if (new_name.empty() || m_renameOldName.empty()) return;
|
|
|
+
|
|
|
+ if (m_db.rename_tag(m_renameOldName, new_name.raw())) {
|
|
|
+ m_renamePopover.popdown();
|
|
|
+ refresh_tag_list();
|
|
|
+ m_signalTagsModified.emit();
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ m_renameBox.append(m_renameEntry);
|
|
|
+ m_renameBox.append(m_renameApplyButton);
|
|
|
+ m_renamePopover.set_child(m_renameBox);
|
|
|
+
|
|
|
+ // Close button
|
|
|
+ add_button("Close", Gtk::ResponseType::CLOSE);
|
|
|
+
|
|
|
+ signal_response().connect([this](int) {
|
|
|
+ hide();
|
|
|
+ });
|
|
|
+
|
|
|
+ refresh_tag_list();
|
|
|
+}
|
|
|
+
|
|
|
+void TagManagerDialog::refresh_tag_list() {
|
|
|
+ // Remove all children
|
|
|
+ while (auto* child = m_tagList.get_first_child()) {
|
|
|
+ m_tagList.remove(*child);
|
|
|
+ }
|
|
|
+
|
|
|
+ auto tags = m_db.get_all_tags();
|
|
|
+
|
|
|
+ if (tags.empty()) {
|
|
|
+ m_scrolled.set_visible(false);
|
|
|
+ m_buttonBox.set_visible(false);
|
|
|
+ m_contentBox.append(m_emptyLabel);
|
|
|
+ m_emptyLabel.set_visible(true);
|
|
|
+ } else {
|
|
|
+ m_emptyLabel.set_visible(false);
|
|
|
+ if (m_emptyLabel.get_parent())
|
|
|
+ m_contentBox.remove(m_emptyLabel);
|
|
|
+ m_scrolled.set_visible(true);
|
|
|
+ m_buttonBox.set_visible(true);
|
|
|
+
|
|
|
+ for (const auto& tag : tags) {
|
|
|
+ auto* row = create_tag_row(tag);
|
|
|
+ m_tagList.append(*row);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+Gtk::Widget* TagManagerDialog::create_tag_row(const DatabaseManager::TagInfo& tag) {
|
|
|
+ auto* row = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 8);
|
|
|
+ row->set_margin_top(8);
|
|
|
+ row->set_margin_bottom(8);
|
|
|
+ row->set_margin_start(12);
|
|
|
+ row->set_margin_end(12);
|
|
|
+
|
|
|
+ auto* name_label = Gtk::make_managed<Gtk::Label>(tag.name);
|
|
|
+ name_label->set_hexpand(true);
|
|
|
+ name_label->set_xalign(0);
|
|
|
+
|
|
|
+ auto* count_label = Gtk::make_managed<Gtk::Label>(
|
|
|
+ Glib::ustring::compose("%1 book%2", tag.usage_count, tag.usage_count == 1 ? "" : "s"));
|
|
|
+ count_label->add_css_class("dim-label");
|
|
|
+
|
|
|
+ auto* rename_btn = Gtk::make_managed<Gtk::Button>();
|
|
|
+ rename_btn->set_icon_name("document-edit-symbolic");
|
|
|
+ rename_btn->add_css_class("flat");
|
|
|
+ rename_btn->set_tooltip_text("Rename tag");
|
|
|
+ rename_btn->signal_clicked().connect([this, tag, rename_btn]() {
|
|
|
+ on_rename_tag(tag.name);
|
|
|
+ m_renamePopover.set_parent(*rename_btn);
|
|
|
+ m_renamePopover.popup();
|
|
|
+ });
|
|
|
+
|
|
|
+ auto* delete_btn = Gtk::make_managed<Gtk::Button>();
|
|
|
+ delete_btn->set_icon_name("user-trash-symbolic");
|
|
|
+ delete_btn->add_css_class("flat");
|
|
|
+ delete_btn->set_tooltip_text("Delete tag");
|
|
|
+ delete_btn->signal_clicked().connect([this, tag]() {
|
|
|
+ on_delete_tag(tag.name);
|
|
|
+ });
|
|
|
+
|
|
|
+ row->append(*name_label);
|
|
|
+ row->append(*count_label);
|
|
|
+ row->append(*rename_btn);
|
|
|
+ row->append(*delete_btn);
|
|
|
+
|
|
|
+ return row;
|
|
|
+}
|
|
|
+
|
|
|
+void TagManagerDialog::on_rename_tag(const std::string& old_name) {
|
|
|
+ m_renameOldName = old_name;
|
|
|
+ m_renameEntry.set_text(old_name);
|
|
|
+ m_renameEntry.select_region(0, -1);
|
|
|
+}
|
|
|
+
|
|
|
+void TagManagerDialog::on_delete_tag(const std::string& tag_name) {
|
|
|
+ if (m_db.delete_tag(tag_name)) {
|
|
|
+ refresh_tag_list();
|
|
|
+ m_signalTagsModified.emit();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void TagManagerDialog::on_cleanup_clicked() {
|
|
|
+ int removed = m_db.delete_unused_tags();
|
|
|
+ if (removed > 0) {
|
|
|
+ refresh_tag_list();
|
|
|
+ m_signalTagsModified.emit();
|
|
|
+ }
|
|
|
+}
|