// -*- mode: c++; c-basic-offset: 2; -*- #include "common.hh" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "gui_about.hh" #include "gui_config.hh" #include "gui_form.hh" #include "gui_formapply.hh" #include "gui_htmlattrtext.hh" #include "gui_listmodel.hh" #include "gui_main.hh" #include "gui_menu.hh" #include "gui_message.hh" #include "gui_progress.hh" #include "gui_statusbar.hh" #include "gui_textwnd.hh" #include "looper.hh" #include "observers.hh" namespace { class SizeHintLayout : public QLayout { public: SizeHintLayout(QWidget* parent, QSize hint) : QLayout(parent), hint_(hint), item_(nullptr) { } ~SizeHintLayout() override { while (true) { auto l = takeAt(0); if (!l) break; delete l; } } void addItem(QLayoutItem *item) override { assert(!item_); item_ = item; } int count() const override { return item_ ? 1 : 0; } QLayoutItem *itemAt(int index) const override { if (item_ && index == 0) { return item_; } return nullptr; } Qt::Orientations expandingDirections() const override { return item_ ? item_->expandingDirections() : QLayout::expandingDirections(); } QSize minimumSize() const override { return item_ ? item_->minimumSize() : QLayout::minimumSize(); } bool hasHeightForWidth() const override { return item_ ? item_->hasHeightForWidth() : QLayout::hasHeightForWidth(); } void setGeometry(const QRect &rect) override { QLayout::setGeometry(rect); if (item_) { item_->setGeometry(rect); } } QSize sizeHint() const override { return hint_; } QLayoutItem *takeAt(int index) override { if (item_ && index == 0) { auto ret = item_; item_ = nullptr; return ret; } return nullptr; } private: QSize const hint_; QLayoutItem* item_; }; class QtGuiWindow : public GuiWindow { public: void set_title(std::string const& title) override { auto w = widget(); if (w) { w->setWindowTitle(QString::fromStdString(title)); } } virtual QWidget* widget() const { return nullptr; } virtual bool showWidget() { return false; } void* impl() const override { return const_cast(this); } }; class QtGuiStatusBar : public GuiStatusBar { public: QtGuiStatusBar(); void set_status(std::string const& str) override; void set_override(std::string const& str) override; void setup(QStatusBar* statusbar); private: std::string status_; std::string override_; QStatusBar* statusbar_; QLabel* label_; }; class QtChildGuiMenu; class QtCommonMenu : public GuiMenu { public: QtCommonMenu(QtCommonMenu* parent, std::string const& label) : parent_(parent), label_(label) { } QtCommonMenu() : parent_(nullptr) { } void add_item(std::string const& id, std::string const& label, Shortcut const& shortcut) override { item_.emplace_back(id, label, shortcut); } void add_separator() override { item_.emplace_back("", "", Shortcut()); } GuiMenu* add_menu(std::string const& label) override; bool enable_item(const std::string& id, bool enable) override { if (parent_) return parent_->enable_item(id, enable) || find(id); if (enable) { disabled_.erase(id); } else { disabled_.insert(id); } return find(id); } virtual void add_action(std::string const& id, QAction* action) { parent_->add_action(id, action); } std::string const& label() const { return label_; } protected: struct Entry { std::string const id; std::string const label; Shortcut const shortcut; Entry(std::string const& id, std::string const& label, Shortcut const& shortcut) : id(id), label(label), shortcut(shortcut) { } }; bool find(const std::string& id) const { for (auto const& entry : item_) { if (entry.id == id) return true; } return false; } static QKeySequence make_key_sequence(Shortcut const& shortcut) { assert(shortcut.key); int mod = 0; if (shortcut.mask & CTRL) mod |= Qt::CTRL; if (shortcut.mask & ALT) mod |= Qt::ALT; if (shortcut.mask & SHIFT) mod |= Qt::SHIFT; if (shortcut.mask & META) mod |= Qt::META; int key = 0; if (shortcut.function) { key = Qt::Key_F1 + (shortcut.key - 1); if (key > Qt::Key_F35) { return QKeySequence(); } } else { if (shortcut.key == ESC) { key = Qt::Key_Escape; } else if (shortcut.key == RETURN) { key = Qt::Key_Return; } else if (shortcut.key == UP) { key = Qt::Key_Up; } else if (shortcut.key == LEFT) { key = Qt::Key_Left; } else if (shortcut.key == RIGHT) { key = Qt::Key_Right; } else if (shortcut.key == DOWN) { key = Qt::Key_Down; } else if (shortcut.key >= 0x20 && shortcut.key <= 0x60) { key = shortcut.key; } else if (shortcut.key > 0x60 && shortcut.key < 0x7b) { key = shortcut.key - 32; } else if (shortcut.key == '\t') { key = Qt::Key_Tab; } else if (shortcut.key == 0x08) { key = Qt::Key_Backspace; } else { return QKeySequence(); } } return QKeySequence(mod + key); } QtCommonMenu* const parent_; std::string const label_; std::vector item_; std::unordered_set disabled_; std::vector> children_; }; class QtGuiMenu : public QtCommonMenu { public: QtGuiMenu(); void add_item(std::string const& id, std::string const& label, Shortcut const& shotcut) override; void add_separator() override; GuiMenu* add_menu(std::string const& label) override; bool enable_item(const std::string& id, bool enable) override; void add_listener(Listener* listener) override; void remove_listener(Listener* listener) override; void add_action(std::string const& id, QAction* action) override; void setup(QMenuBar* menubar); private: void notify_item_activated(std::string const& id); Observers observers_; QMenuBar* menubar_; std::unordered_map action_; }; class QGuiListModel : public QAbstractItemModel, GuiListModel::Listener { public: QGuiListModel(GuiListModel* model) : model_(model) { model_->add_listener(this); } ~QGuiListModel() override { model_->remove_listener(this); } void rows_added(GuiListModel*, size_t first, size_t last) override { beginInsertRows(QModelIndex(), first, last); endInsertRows(); } void rows_changed(GuiListModel*, size_t first, size_t last) override { emit dataChanged(createIndex(first, 0), createIndex(last, model_->columns())); } void rows_removed(GuiListModel*, size_t first, size_t last) override { beginRemoveRows(QModelIndex(), first, last); endRemoveRows(); } GuiListModel* model() const { return model_; } bool hasChildren(const QModelIndex& parent) const override { return !parent.isValid(); } Qt::ItemFlags flags(const QModelIndex& index) const override { auto flags = QAbstractItemModel::flags(index); if (index.isValid()) flags |= Qt::ItemNeverHasChildren; return flags; } QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const override { if (!parent.isValid() && row >= 0 && static_cast(row) < model_->rows() && column >= 0 && static_cast(column) < model_->columns()) { return createIndex(row, column); } return QModelIndex(); } QModelIndex parent(const QModelIndex&) const override { return QModelIndex(); } int rowCount(const QModelIndex& parent) const override { if (parent.isValid()) return 0; return model_->rows(); } int columnCount(const QModelIndex& parent = QModelIndex()) const override { if (parent.isValid()) return 0; return model_->columns(); } QVariant data(const QModelIndex& index, int role) const override { if (index.isValid() && role == Qt::DisplayRole && index.row() >= 0 && static_cast(index.row()) < model_->rows() && index.column() >= 0 && static_cast(index.column()) < model_->columns()) { return QVariant(QString::fromStdString(model_->data(index.row(), index.column()))); } return QVariant(); } QVariant headerData(int section, Qt::Orientation orientation, int role) const override { if (orientation == Qt::Horizontal && role == Qt::DisplayRole && section >= 0 && static_cast(section) < model_->columns()) { return QVariant(QString::fromStdString(model_->header(section))); } return QVariant(); } private: GuiListModel* const model_; }; class QtGuiConfig : public GuiConfig { public: QtGuiConfig() : settings_(new QSettings("org.the_jk", "tp.Monitor")) { } ~QtGuiConfig() override { settings_->sync(); } using Config::get; std::string const& get(std::string const& key, std::string const& fallback) override { memory_[key] = settings_->value(QString::fromStdString(key), QString::fromStdString(fallback)) .toString().toStdString(); return memory_[key]; } char const* get(std::string const& key, char const* fallback) override { auto k = QString::fromStdString(key); if (!settings_->contains(k)) return fallback; memory_[key] = settings_->value(k).toString().toStdString(); return memory_[key].c_str(); } bool is_set(std::string const& key) override { return settings_->contains(QString::fromStdString(key)); } void set(std::string const& key, std::string const& value) override { settings_->setValue(QString::fromStdString(key), QString::fromStdString(value)); } void remove(std::string const& key) override { settings_->remove(QString::fromStdString(key)); } private: std::unique_ptr settings_; std::unordered_map memory_; }; QString show_file_dialog(QWidget* parent, std::string const& title, QString const& value, uint8_t flags, std::vector const& filters) { QString filter; for (auto const& f : filters) { if (!filter.isEmpty()) filter.append(";;"); filter.append(QString::fromStdString(f.name)); if (!f.name.empty() && f.name.back() == ')') continue; filter.append(" ("); bool first = true; for (auto const& mask : f.masks) { if (first) { first = false; } else { filter.append(' '); } filter.append(QString::fromStdString(mask)); } filter.append(')'); } if (!(flags & GuiFile::FILE_SAVE)) { return QFileDialog::getOpenFileName( parent, QString::fromStdString(title), value, filter); } else { return QFileDialog::getSaveFileName( parent, QString::fromStdString(title), value, filter); } } class QtGuiMain : public GuiMain, public QtGuiWindow { public: typedef std::function RunWhenStartedCallback; QtGuiMain(std::string const& title, uint32_t width, uint32_t height) : title_(title), width_(width), height_(height), split_(0.7), menu_(nullptr), statusbar_(nullptr), config_(new QtGuiConfig()), splitter_(nullptr), top_(nullptr), bottom_package_(nullptr), bottom_content_(nullptr), bottom_(nullptr), started_(false) { } Looper* create_looper() override; void set_menu(GuiMenu* menu) override { menu_ = static_cast(menu); } GuiMenu* menu() const override { return menu_; } void set_statusbar(GuiStatusBar* statusbar) override { statusbar_ = static_cast(statusbar); } GuiStatusBar* statusbar() const override { return statusbar_; } void set_split(double split) override { split_ = std::max(0.0, std::min(split, 1.0)); if (splitter_) { QList sizes; auto total = splitter_->size().height(); sizes << round(total * split_); sizes << total - sizes.front(); splitter_->setSizes(sizes); } } double split() const override { if (splitter_) { auto sizes = splitter_->sizes(); if (!sizes.empty()) { split_ = static_cast(splitter_->size().height()) / sizes.front(); } } return split_; } void set_listmodel(GuiListModel* listmodel) override { if (listmodel) { listmodel_.reset(new QGuiListModel(listmodel)); } else { listmodel_.reset(); } } GuiListModel* listmodel() const override { return listmodel_ ? listmodel_->model() : nullptr; } void select_row(size_t row) override { if (!listmodel_ || !top_) return; QTreeView* treeview = static_cast(top_); treeview->setCurrentIndex(listmodel_->index(row, 0)); } void set_package(std::unique_ptr&& text) override { if (text) { package_ = "" + static_cast(text.get())->html() + ""; } else { package_.clear(); } if (bottom_package_) { bottom_package_->setHtml(QString::fromStdString(package_)); } } void set_content(std::string const& name, AttributedText* text) override { if (text) { content_ = "" + static_cast(text)->html() + ""; content_name_ = name; } else { content_.clear(); content_name_.clear(); } if (bottom_content_) { bottom_content_->setHtml(QString::fromStdString(content_)); bottom_->setTabText(1, QString::fromStdString(content_name_)); bottom_->setTabEnabled(1, !content_.empty()); if (text && content_last_active_) { bottom_->setCurrentIndex(1); } } } void add_listener(GuiMain::Listener* listener) override { observers_.insert(listener); } void remove_listener(GuiMain::Listener* listener) override { observers_.erase(listener); } void resize(uint32_t width, uint32_t height) override { if (width == width_ && height == height_) return; width_ = width; height_ = height; auto w = widget(); if (w) { w->resize(width_, height_); } else { notify_resize(); } } void set_title(std::string const& title) override { title_ = title; QtGuiWindow::set_title(title); } std::string const& title() const override { return title_; } void* impl() const override { return QtGuiWindow::impl(); } QWidget* widget() const override { return main_.get(); } bool run(int argc, char** argv) override; void show(GuiWindow* window) override { auto wnd = static_cast(window->impl()); auto widget = wnd->widget(); if (!widget) { if (!wnd->showWidget()) { assert(false); } return; } widget->show(); } bool exit() override { if (!notify_about_to_exit()) return false; QApplication::exit(); return true; } void add_to_clipboard(std::string const& data, std::string const& mimetype) override { auto clipboard = QApplication::clipboard(); if (mimetype.empty() || valid_utf8(data)) { clipboard->setText(QString::fromStdString(data)); return; } auto mimedata = new QMimeData(); mimedata->setData(QString::fromStdString(mimetype), QByteArray(data.data(), data.size())); clipboard->setMimeData(mimedata); } Config* config() override { return config_.get(); } std::string file_dialog(std::string const& title, std::string const& file, uint8_t flags, std::vector const& filter) override { auto ret = show_file_dialog(main_.get(), title, QString::fromStdString(file), flags, filter); return ret.isNull() ? "" : ret.toStdString(); } void run_when_started(RunWhenStartedCallback const& callback) { if (started_) { callback(this); return; } run_when_started_.emplace_back(callback); } private: static bool valid_utf8(std::string const& data) { QTextCodec::ConverterState state; QTextCodec* codec = QTextCodec::codecForName("UTF-8"); const QString text = codec->toUnicode(data.data(), data.size(), &state); return state.invalidChars == 0; } bool notify_about_to_exit() { auto it = observers_.notify(); while (it.has_next()) { if (!it.next()->about_to_exit(this)) return false; } return true; } void notify_resize() { auto it = observers_.notify(); while (it.has_next()) { it.next()->resize(width_, height_); } } void notify_selected_row(size_t row) { auto it = observers_.notify(); while (it.has_next()) { it.next()->selected_row(this, row); } } void notify_lost_selection() { auto it = observers_.notify(); while (it.has_next()) { it.next()->lost_selection(this); } } void notify_open(std::string const& file) { auto it = observers_.notify(); while (it.has_next()) { it.next()->open(this, file); } } void start() { assert(!started_); started_ = true; for (auto& callback : run_when_started_) { callback(this); } run_when_started_.clear(); } std::string title_; uint32_t width_; uint32_t height_; mutable double split_; QtGuiMenu* menu_; QtGuiStatusBar* statusbar_; std::unique_ptr listmodel_; std::string package_; std::string content_; std::string content_name_; Observers observers_; std::unique_ptr main_; std::unique_ptr layout_; std::unique_ptr center_; std::unique_ptr config_; QSplitter* splitter_; QWidget* top_; QTextEdit* bottom_package_; QTextEdit* bottom_content_; QTabWidget* bottom_; bool started_; std::vector run_when_started_; bool content_last_active_; }; class QtChildGuiMenu : public QtCommonMenu { public: QtChildGuiMenu(QtCommonMenu* parent, std::string const& label) : QtCommonMenu(parent, label), menu_(nullptr) { } void add_item(std::string const& id, std::string const& label, Shortcut const& shortcut) override { if (!menu_) { QtCommonMenu::add_item(id, label, shortcut); return; } auto action = menu_->addAction(QString::fromStdString(label)); if (shortcut.key != 0) { action->setShortcut(make_key_sequence(shortcut)); } parent_->add_action(id, action); } void add_separator() override { if (!menu_) { QtCommonMenu::add_separator(); return; } menu_->addSeparator(); } GuiMenu* add_menu(std::string const& label) override { auto menu = static_cast(QtCommonMenu::add_menu(label)); if (menu_) { menu->setup(menu_->addMenu(QString::fromStdString(menu->label()))); } return menu; } void add_listener(Listener* listener) override { parent_->add_listener(listener); } void remove_listener(Listener* listener) override { parent_->remove_listener(listener); } void setup(QMenu* menu) { assert(!menu_); assert(menu); menu_ = menu; for (auto const& entry : item_) { if (entry.id.empty() && entry.label.empty()) { menu_->addSeparator(); continue; } auto action = menu_->addAction(QString::fromStdString(entry.label)); if (entry.shortcut.key) { action->setShortcut(make_key_sequence(entry.shortcut)); } parent_->add_action(entry.id, action); } item_.clear(); for (auto& child : children_) { child->setup(menu_->addMenu(QString::fromStdString(child->label()))); } } private: QMenu* menu_; }; GuiMenu* QtCommonMenu::add_menu(std::string const& label) { auto child = new QtChildGuiMenu(this, label); children_.emplace_back(child); return child; } QtGuiMenu::QtGuiMenu() : QtCommonMenu(), menubar_(nullptr) { } void QtGuiMenu::add_item(std::string const& id, std::string const& label, Shortcut const& shortcut) { if (!menubar_) { QtCommonMenu::add_item(id, label, shortcut); return; } auto action = menubar_->addAction(QString::fromStdString(label)); if (shortcut.key != 0) { action->setShortcut(make_key_sequence(shortcut)); } add_action(id, action); } void QtGuiMenu::add_separator() { if (!menubar_) { QtCommonMenu::add_separator(); return; } menubar_->addSeparator(); } GuiMenu* QtGuiMenu::add_menu(std::string const& label) { auto menu = static_cast(QtCommonMenu::add_menu(label)); if (menubar_) { menu->setup(menubar_->addMenu(QString::fromStdString(label))); } return menu; } bool QtGuiMenu::enable_item(const std::string& id, bool enable) { if (!menubar_) { return QtCommonMenu::enable_item(id, enable); } auto pair = action_.find(id); if (pair == action_.end()) return false; pair->second->setEnabled(enable); return true; } void QtGuiMenu::add_listener(Listener* listener) { observers_.insert(listener); } void QtGuiMenu::remove_listener(Listener* listener) { observers_.erase(listener); } void QtGuiMenu::notify_item_activated(std::string const& id) { auto it = observers_.notify(); while (it.has_next()) { it.next()->item_activated(id); } } void QtGuiMenu::add_action(std::string const& id, QAction* action) { action_.emplace(id, action); QObject::connect(action, &QAction::triggered, [=](bool) { notify_item_activated(id); }); } void QtGuiMenu::setup(QMenuBar* menubar) { assert(menubar); assert(!menubar_); menubar_ = menubar; for (auto const& entry : item_) { auto action = menubar_->addAction(QString::fromStdString(entry.label)); if (entry.shortcut.key) { } add_action(entry.id, action); } item_.clear(); for (auto& child : children_) { child->setup(menubar_->addMenu(QString::fromStdString(child->label()))); } for (auto const& id : disabled_) { auto it = action_.find(id); if (it == action_.end()) { assert(false); continue; } it->second->setEnabled(false); } disabled_.clear(); } QtGuiStatusBar::QtGuiStatusBar() : statusbar_(nullptr), label_(nullptr) { } void QtGuiStatusBar::setup(QStatusBar* statusbar) { if (!statusbar) { statusbar_ = nullptr; label_ = nullptr; return; } label_ = new QLabel(); label_->setText(QString::fromStdString(status_)); statusbar_ = statusbar; statusbar_->addWidget(label_, 1); if (override_.empty()) { statusbar_->clearMessage(); } else { statusbar_->showMessage(QString::fromStdString(override_)); } } void QtGuiStatusBar::set_status(std::string const& str) { status_ = str; if (label_) label_->setText(QString::fromStdString(str)); } void QtGuiStatusBar::set_override(std::string const& str) { override_ = str; if (statusbar_) { if (override_.empty()) { statusbar_->clearMessage(); } else { statusbar_->showMessage(QString::fromStdString(override_)); } } } class QtGuiAbout : public virtual GuiAbout, public QtGuiWindow { public: QtGuiAbout(std::string const& title, std::string const& version, std::vector const& authors) : title_(title + " " + version) { auto it = authors.begin(); while (it != authors.end()) { text_ += *it++; if (!it->empty()) { text_.push_back(' '); text_ += *it; } ++it; text_.push_back('\n'); } } void add_listener(GuiAbout::Listener* listener) override { observers_.insert(listener); } void remove_listener(GuiAbout::Listener* listener) override { observers_.erase(listener); } void set_title(std::string const& title) override { title_ = title; } std::string const& title() const override { return title_; } void* impl() const override { return QtGuiWindow::impl(); } bool showWidget() override { QMessageBox::about(QApplication::activeWindow(), QString::fromStdString(title_), QString::fromStdString(text_)); return true; } private: std::string title_; std::string text_; Observers observers_; }; class OptionalCloseDialog : public QDialog { public: class Delegate { public: virtual ~Delegate() {} virtual bool about_to_close() = 0; protected: Delegate() {} }; OptionalCloseDialog(QWidget* parent, Delegate* delegate) : QDialog(parent), delegate_(delegate) { } void done(int result) override { if (result == QDialog::Accepted) { if (!delegate_->about_to_close()) return; } QDialog::done(result); } private: Delegate* const delegate_; }; class QtGuiForm : public virtual GuiForm, public QtGuiWindow, public virtual OptionalCloseDialog::Delegate { public: QtGuiForm(std::string const& title, std::string const& text) : title_(title), text_(text), dialog_(nullptr), error_(nullptr), buttons_(nullptr) { } ~QtGuiForm() { assert(!dialog_); } void add_listener(GuiForm::Listener* listener) override { observers_.insert(listener); } void remove_listener(GuiForm::Listener* listener) override { observers_.erase(listener); } void set_title(std::string const& title) override { title_ = title; QtGuiWindow::set_title(title); } std::string const& title() const override { return title_; } QWidget* widget() const override { return dialog_; } void* impl() const override { return QtGuiWindow::impl(); } bool showWidget() override { show(QApplication::activeWindow()); return true; } bool show(GuiWindow* parent) override { auto wnd = static_cast(parent->impl()); return show(wnd->widget()); } void add_string(std::string const& id, std::string const& label, std::string const& value, std::string const& description) override { values_.emplace_back(new StringValue(id, label, description, value)); } void add_number(std::string const& id, std::string const& label, uint64_t value, std::string const& description) override { values_.emplace_back(new NumberValue(id, label, description, value)); } void add_file(std::string const& id, std::string const& label, std::string const& value, std::string const& description, uint8_t flags, std::vector const& filter) override { values_.emplace_back(new FileValue(id, label, description, value, flags, filter)); } void add_bool(std::string const& id, std::string const& label, bool value, std::string const& description) override { values_.emplace_back(new BoolValue(id, label, description, value)); } void enable(std::string const& id, bool enable) override { for (auto const& value : values_) { if (value->id_ == id) { value->enable_ = enable; if (value->edit_) { value->edit_->setEnabled(enable); } switch (value->type_) { case STRING: case NUMBER: break; case FILE: { auto v = static_cast(value.get()); if (v->button_) v->button_->setEnabled(enable); break; } case BOOLEAN: { auto v = static_cast(value.get()); if (v->check_) v->check_->setEnabled(enable); break; } } } } } std::string get_string(std::string const& id) const override { for (auto const& value : values_) { if (value->id_ == id && value->type_ == STRING) { if (value->edit_) { return value->edit_->text().toStdString(); } return static_cast(value.get())->value_; } } assert(false); return ""; } uint64_t get_number(std::string const& id) const override { for (auto const& value : values_) { if (value->id_ == id && value->type_ == NUMBER) { if (value->edit_) { uint64_t tmp; if (get_number(value->edit_, &tmp)) { return tmp; } } return static_cast(value.get())->value_; } } assert(false); return 0; } std::string get_file(std::string const& id) const override { for (auto const& value : values_) { if (value->id_ == id && value->type_ == FILE) { if (value->edit_) { return value->edit_->text().toStdString(); } return static_cast(value.get())->value_; } } assert(false); return ""; } bool get_bool(std::string const& id) const override { for (auto const& value : values_) { if (value->id_ == id && value->type_ == BOOLEAN) { auto v = static_cast(value.get()); if (v->check_) { return v->check_->isChecked(); } return v->value_; } } assert(false); return ""; } void set_error(std::string const& error) override { if (!error_) { assert(false); return; } if (error.empty()) { error_->hide(); } else { error_->setText(QString::fromStdString(error)); error_->show(); } } bool about_to_close() override { return notify_about_to_close(); } protected: enum Type { STRING, NUMBER, FILE, BOOLEAN, }; struct Value { Type const type_; std::string const id_; std::string const label_; std::string const description_; bool enable_; QLineEdit* edit_; Value(Type type, std::string const& id, std::string const& label, std::string const& description) : type_(type), id_(id), label_(label), description_(description), enable_(true), edit_(nullptr) { } }; struct StringValue : public Value { std::string value_; StringValue(std::string const& id, std::string const& label, std::string const& description, std::string const& value) : Value(STRING, id, label, description), value_(value) { } }; struct NumberValue : public Value { uint64_t value_; NumberValue(std::string const& id, std::string const& label, std::string const& description, uint64_t value) : Value(NUMBER, id, label, description), value_(value) { } }; struct FileValue : public Value { std::string value_; uint8_t flags_; std::vector filter_; QPushButton* button_; FileValue(std::string const& id, std::string const& label, std::string const& description, std::string const& value, uint8_t flags, std::vector const& filter) : Value(FILE, id, label, description), value_(value), flags_(flags), filter_(filter), button_(nullptr) { } }; struct BoolValue : public Value { bool value_; QCheckBox* check_; BoolValue(std::string const& id, std::string const& label, std::string const& description, bool value) : Value(BOOLEAN, id, label, description), value_(value), check_(nullptr) { } }; static bool get_number(QLineEdit* edit, uint64_t* number) { assert(edit && number); auto text = edit->text().toStdString(); errno = 0; char* end = nullptr; auto tmp = strtoull(text.c_str(), &end, 10); if (errno && end && !*end) { if (number) *number = tmp; return true; } return false; } bool show(QWidget* parent) { dialog_ = new OptionalCloseDialog(parent, this); dialog_->setWindowTitle(QString::fromStdString(title_)); auto layout = new QGridLayout(); int row = 0; if (!text_.empty()) { auto text = new QLabel(QString::fromStdString(text_)); text->setWordWrap(true); layout->addWidget(text, row++, 0, 1, 3); } for (auto& value : values_) { QWidget* label; QLineEdit* edit; if (value->type_ != BOOLEAN) { label = new QLabel(QString::fromStdString(value->label_)); edit = new QLineEdit(); value->edit_ = edit; } else { label = nullptr; edit = nullptr; } switch (value->type_) { case STRING: edit->setText(QString::fromStdString( static_cast(value.get())->value_)); break; case NUMBER: { char tmp[20]; snprintf(tmp, sizeof(tmp), "%llu", static_cast( static_cast(value.get())->value_)); edit->setText(tmp); edit->setValidator(new QIntValidator( 0, std::numeric_limits::max(), edit)); break; } case FILE: edit->setText(QString::fromStdString( static_cast(value.get())->value_)); break; case BOOLEAN: { auto v = static_cast(value.get()); assert(!label); v->check_ = new QCheckBox(QString::fromStdString(v->label_)); label = v->check_; v->check_->setChecked(v->value_); v->check_->setEnabled(v->enable_); v->check_->connect(v->check_, &QCheckBox::stateChanged, [=](int) { notify_changed(v->id_); }); break; } } auto vp = value.get(); if (edit) { edit->connect(edit, &QLineEdit::textChanged, [=](QString const&) { notify_changed(vp->id_); }); edit->setEnabled(value->enable_); } if (value->type_ == FILE) { auto v = static_cast(vp); v->button_ = new QPushButton("..."); v->button_->setEnabled(v->enable_); edit->setReadOnly(true); dialog_->connect(v->button_, &QPushButton::clicked, [=](bool) { showFileDialog(v); }); layout->addWidget(label, row, 0); layout->addWidget(edit, row, 1); layout->addWidget(v->button_, row, 2); } else if (edit) { layout->addWidget(label, row, 0); layout->addWidget(edit, row, 1, 1, 2); } else { layout->addWidget(label, row, 0, 1, 3); } ++row; if (!value->description_.empty()) { auto desc = new QLabel( QString::fromStdString("" + value->description_ + "")); desc->setAlignment(Qt::AlignTrailing); desc->setWordWrap(true); layout->addWidget(desc, row++, 0, 1, 3); } } error_ = new QLabel(); layout->addWidget(error_, row++, 0, 1, 3); error_->hide(); buttons_ = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); dialog_->connect(buttons_, &QDialogButtonBox::accepted, dialog_, &QDialog::accept); dialog_->connect(buttons_, &QDialogButtonBox::rejected, dialog_, &QDialog::reject); layout->addWidget(buttons_, row++, 0, 1, 3); add_extra_widgets(layout, row, 3); dialog_->setLayout(layout); auto ret = dialog_->exec(); for (auto& value : values_) { switch (value->type_) { case STRING: static_cast(value.get())->value_ = value->edit_->text().toStdString(); break; case NUMBER: get_number(value->edit_, &static_cast(value.get())->value_); break; case FILE: static_cast(value.get())->value_ = value->edit_->text().toStdString(); break; case BOOLEAN: { auto v = static_cast(value.get()); v->value_ = v->check_->isChecked(); v->check_ = nullptr; break; } } value->edit_ = nullptr; } delete dialog_; reset_extra_widgets(); dialog_ = nullptr; error_ = nullptr; buttons_ = nullptr; return ret == QDialog::Accepted; } virtual bool notify_about_to_close() { auto it = observers_.notify(); while (it.has_next()) { if (!it.next()->about_to_close(this)) return false; } return true; } void notify_changed(std::string const& id) { auto it = observers_.notify(); while (it.has_next()) { it.next()->changed(this, id); } } virtual int add_extra_widgets(QGridLayout*, int row, int) { return row; } virtual void reset_extra_widgets() { } void showFileDialog(FileValue* value) { auto file = show_file_dialog(dialog_, value->label_, value->edit_->text(), value->flags_, value->filter_); if (!file.isNull()) { value->edit_->setText(file); } } std::string title_; std::string text_; Observers observers_; std::vector> values_; QDialog* dialog_; QLabel* error_; QDialogButtonBox* buttons_; }; class QtGuiFormApply : public QtGuiForm, public virtual GuiFormApply { public: QtGuiFormApply(std::string const& title, std::string const& text, std::string const& apply_button, GuiFormApply::Delegate* delegate) : QtGuiForm(title, text), apply_button_(apply_button), delegate_(delegate), progress_(nullptr), applied_(false) { } void applied(bool success) override { assert(progress_); if (progress_) { progress_->hide(); } assert(buttons_); if (buttons_) { auto ok = buttons_->button(QDialogButtonBox::Ok); ok->setEnabled(true); } if (success) { applied_ = true; dialog_->done(QDialog::Accepted); } } private: int add_extra_widgets(QGridLayout* layout, int row, int columns) override { assert(buttons_); if (buttons_) { auto ok = buttons_->button(QDialogButtonBox::Ok); ok->setText(QString::fromStdString(apply_button_)); } assert(!progress_); progress_ = new QProgressBar(); progress_->setRange(0, 0); layout->addWidget(progress_, row++, 0, 1, columns); progress_->hide(); return row; } void reset_extra_widgets() override { progress_ = nullptr; } bool notify_about_to_close() override { if (applied_) { applied_ = false; return true; } if (!QtGuiForm::notify_about_to_close()) return false; assert(progress_); if (progress_) { progress_->show(); } assert(buttons_); if (buttons_) { auto ok = buttons_->button(QDialogButtonBox::Ok); ok->setEnabled(false); } delegate_->apply(this); return false; } std::string apply_button_; GuiFormApply::Delegate* const delegate_; QProgressBar* progress_; bool applied_; }; class QtGuiMessage : public virtual GuiMessage, public QtGuiWindow { public: QtGuiMessage(Type type, std::string const& title, std::string const& text) : type_(type), title_(title), text_(text) { } void add_listener(GuiMessage::Listener* listener) override { observers_.insert(listener); } void remove_listener(GuiMessage::Listener* listener) override { observers_.erase(listener); } void set_title(std::string const& title) override { title_ = title; } std::string const& title() const override { return title_; } void* impl() const override { return QtGuiWindow::impl(); } bool showWidget() override { show(QApplication::activeWindow()); return true; } void show(GuiWindow* parent) override { auto wnd = static_cast(parent->impl()); show(wnd->widget()); } protected: void show(QWidget* parent) { switch (type_) { case ERROR: QMessageBox::critical(parent, QString::fromStdString(title_), QString::fromStdString(text_)); break; case OTHER: QMessageBox::information(parent, QString::fromStdString(title_), QString::fromStdString(text_)); break; } } Type type_; std::string title_; std::string text_; Observers observers_; }; class OptionalCloseWidget : public QWidget { public: class Delegate { public: virtual ~Delegate() {} virtual bool about_to_close() = 0; protected: Delegate() {} }; OptionalCloseWidget(QWidget* parent, Delegate* delegate) : QWidget(parent), delegate_(delegate) { } protected: void closeEvent(QCloseEvent* event) override { if (!delegate_->about_to_close()) { event->ignore(); return; } this->QWidget::closeEvent(event); } private: Delegate* const delegate_; }; class QtGuiTextWindow : public virtual GuiTextWindow, public QtGuiWindow, public virtual OptionalCloseWidget::Delegate, public virtual HtmlAttributedText::Listener { public: QtGuiTextWindow(std::string const& title, uint32_t width, uint32_t height, AttributedText const* text) : title_(title), width_(width), height_(height), text_(static_cast(text)), widget_(nullptr), view_(nullptr) { } ~QtGuiTextWindow() override { } void set_text(AttributedText const* text) override { if (!text) { assert(false); if (text_own_) text_own_->reset(); return; } disconnect_buffer(); text_ = static_cast(text); connect_buffer(); text_own_.reset(); } void set_text(std::unique_ptr&& text) override { if (!text) { assert(false); if (text_own_) text_own_->reset(); return; } disconnect_buffer(); text_ = static_cast(text.get()); connect_buffer(); text_own_.swap(text); } AttributedText const* text() const override { return text_; } void add_listener(GuiTextWindow::Listener* listener) override { observers_.insert(listener); } void remove_listener(GuiTextWindow::Listener* listener) override { observers_.erase(listener); } void set_title(std::string const& title) override { title_ = title; QtGuiWindow::set_title(title); } std::string const& title() const override { return title_; } QWidget* widget() const override { return widget_.get(); } void* impl() const override { return QtGuiWindow::impl(); } bool showWidget() override { show(); return true; } void show(GuiWindow*) override { if (widget_) { focus(); return; } show(); } void focus() override { if (!widget_) { assert(false); show(); return; } widget_->raise(); widget_->activateWindow(); } private: void show() { widget_.reset(new OptionalCloseWidget(nullptr, this)); widget_->setWindowTitle(QString::fromStdString(title_)); layout_.reset(new SizeHintLayout(widget_.get(), QSize(width_, height_))); view_ = new QTextEdit(); view_->setReadOnly(true); connect_buffer(); if (text_) view_->setHtml(QString::fromStdString(text_->html())); widget_->layout()->addWidget(view_); widget_->show(); } bool about_to_close() override { disconnect_buffer(); auto view = view_; view_ = nullptr; auto widget = widget_.release(); auto it = observers_.notify(); while (it.has_next()) { if (!it.next()->about_to_close(this)) { widget_.reset(widget); view_ = view; connect_buffer(); return false; } } return true; } void changed(HtmlAttributedText* text) override { assert(text == text_); if (view_) { view_->setHtml(QString::fromStdString(text->html())); view_->moveCursor(QTextCursor::End); view_->ensureCursorVisible(); } } void connect_buffer() { if (!text_ || !view_) return; const_cast(text_)->add_listener(this); } void disconnect_buffer() { if (!text_ || !view_) return; const_cast(text_)->remove_listener(this); } std::string title_; uint32_t width_; uint32_t height_; HtmlAttributedText const* text_; std::unique_ptr text_own_; Observers observers_; std::unique_ptr layout_; std::unique_ptr widget_; QTextEdit* view_; }; class QtGuiProgress : public virtual GuiProgress, public QtGuiWindow, public virtual OptionalCloseDialog::Delegate { private: static const int UNIT = 100000; public: QtGuiProgress(std::string const& title, std::string const& text, float min, float max) : title_(title), text_(text), min_(min), max_(max), value_(min), progress_(nullptr) { } ~QtGuiProgress() override { } void add_listener(GuiProgress::Listener* listener) override { observers_.insert(listener); } void remove_listener(GuiProgress::Listener* listener) override { observers_.erase(listener); } void set_title(std::string const& title) override { title_ = title; QtGuiWindow::set_title(title); } std::string const& title() const override { return title_; } void set_progress(float value) override { value_ = value; if (progress_) progress_->setValue(value_ * UNIT); } QWidget* widget() const override { return dialog_.get(); } void* impl() const override { return QtGuiWindow::impl(); } bool showWidget() override { show(QApplication::activeWindow()); return true; } void show(GuiWindow* parent) override { auto wnd = static_cast(parent->impl()); show(wnd->widget()); } bool about_to_close() override { return notify_about_to_close(); } private: void show(QWidget* parent) { if (dialog_) { assert(false); dialog_->raise(); dialog_->activateWindow(); return; } progress_ = new QProgressBar(); progress_->setRange(min_ * UNIT, max_ * UNIT); progress_->setValue(value_ * UNIT); dialog_.reset(new OptionalCloseDialog(parent, this)); dialog_->setWindowTitle(QString::fromStdString(title_)); auto layout = new QVBoxLayout(); auto text = new QLabel(QString::fromStdString(text_)); text->setWordWrap(true); layout->addWidget(text); layout->addWidget(progress_); dialog_->setLayout(layout); dialog_->setModal(true); dialog_->show(); } bool notify_about_to_close() { auto it = observers_.notify(); while (it.has_next()) { if (!it.next()->about_to_close(this)) return false; } return true; } std::string title_; std::string text_; float min_; float max_; float value_; Observers observers_; std::unique_ptr dialog_; QProgressBar* progress_; }; class QNotifyResizeMainWindow : public QMainWindow { public: typedef std::function ResizeCallback; explicit QNotifyResizeMainWindow(ResizeCallback const& callback) : callback_(callback) { } void resizeEvent(QResizeEvent* event) override { callback_(event->size().width(), event->size().height()); } private: ResizeCallback const callback_; }; bool QtGuiMain::run(int argc, char** argv) { QApplication app(argc, argv); app.setApplicationName(QString::fromStdString(title_)); app.setApplicationVersion(VERSION); start(); QCommandLineParser parser; parser.addHelpOption(); parser.addVersionOption(); parser.addPositionalArgument("FILE", "Package capture to load"); parser.process(app); app.setStyleSheet("QStatusBar::item { border: 0px }"); main_.reset(new QNotifyResizeMainWindow( [=](uint32_t width, uint32_t height) { width_ = width; height_ = height; notify_resize(); })); main_->setWindowTitle(QString::fromStdString(title_)); center_.reset(new QWidget()); layout_.reset(new SizeHintLayout(center_.get(), QSize(width_, height_))); splitter_ = new QSplitter(Qt::Vertical, main_.get()); center_->layout()->addWidget(splitter_); if (listmodel_) { auto tree = new QTreeView(); tree->setModel(listmodel_.get()); tree->setUniformRowHeights(true); tree->setSelectionMode(QAbstractItemView::SingleSelection); QObject::connect(tree->selectionModel(), &QItemSelectionModel::selectionChanged, [=](QItemSelection const& selected, QItemSelection const&) { if (selected.size() == 1) { notify_selected_row(selected.indexes()[0].row()); } else { notify_lost_selection(); } }); tree->header()->setSectionsMovable(false); auto columns = listmodel_->columnCount(); for (int i = 0; i < columns; ++i) { tree->header()->setSectionResizeMode( i, i == columns - 1 ? QHeaderView::Stretch : QHeaderView::ResizeToContents); } top_ = tree; } else { top_ = new QWidget(); } splitter_->addWidget(top_); bottom_package_ = new QTextEdit(); bottom_package_->setReadOnly(true); bottom_package_->setHtml(QString::fromStdString(package_)); bottom_content_ = new QTextEdit(); bottom_content_->setReadOnly(true); bottom_content_->setHtml(QString::fromStdString(content_)); bottom_ = new QTabWidget(); bottom_->addTab(bottom_package_, "Package"); bottom_->addTab(bottom_content_, QString::fromStdString(content_name_)); bottom_->setTabEnabled(1, !content_.empty()); content_last_active_ = false; QObject::connect(bottom_, &QTabWidget::tabBarClicked, [=](int index) { if (index < 0) return; content_last_active_ = index == 1; }); splitter_->addWidget(bottom_); main_->setCentralWidget(center_.get()); if (menu_) { menu_->setup(main_->menuBar()); } if (statusbar_) { statusbar_->setup(main_->statusBar()); } main_->show(); set_split(split_); auto args = parser.positionalArguments(); if (args.size() > 0) { notify_open(args[0].toStdString()); } auto ret = app.exec() == 0; layout_.reset(); top_ = nullptr; bottom_ = nullptr; splitter_ = nullptr; center_.reset(); main_.reset(); // Make sure QMainWindow destructor is run before // QApplication's destructor return ret; } class QtLooper : public Looper { public: QtLooper(QtGuiMain* main) : init_(false) { main->run_when_started([=](GuiMain*) { init(); }); } void add(int fd, uint8_t events, FdCallback const& callback) override { auto& handle = fds_[fd]; handle.reset(new Fd(fd, callback)); handle->events_ = events; if (init_) { handle->init(); } } void modify(int fd, uint8_t events) override { auto it = fds_.find(fd); if (it == fds_.end()) { assert(false); return; } auto& handle = it->second; if (init_) { handle->read_->setEnabled(events & EVENT_READ); handle->write_->setEnabled(events & EVENT_WRITE); } else { handle->events_ = events; } } void remove(int fd) override { auto it = fds_.find(fd); if (it == fds_.end()) { assert(false); return; } auto& handle = it->second; if (init_ && handle->in_callback_) { handle.release()->delayed_delete_ = true; } fds_.erase(fd); } void* schedule(float delay_s, ScheduleCallback const& callback) override { auto timer = new QTimer(); timer->setSingleShot(true); timers_.insert(timer); QObject::connect(timer, &QTimer::timeout, [=]() { timers_.erase(timer); callback(timer); timer->deleteLater(); }); timer->start(delay_s * 1000); return timer; } void cancel(void* handle) override { auto timer = reinterpret_cast(handle); if (timers_.erase(timer)) { delete timer; } } bool run() override { assert(false); return false; } void quit() override { assert(false); } clock::time_point now() const override { return clock::now(); } private: struct Fd { int fd_; uint8_t events_; FdCallback callback_; std::unique_ptr read_; std::unique_ptr write_; bool in_callback_; bool delayed_delete_; Fd(int fd, FdCallback const& fd_callback) : fd_(fd), events_(0), callback_(fd_callback), in_callback_(false), delayed_delete_(false) { } void init() { read_.reset(new QSocketNotifier(fd_, QSocketNotifier::Read)); write_.reset(new QSocketNotifier(fd_, QSocketNotifier::Write)); QObject::connect(read_.get(), &QSocketNotifier::activated, [=](int fd) { callback(fd, EVENT_READ); }); QObject::connect(write_.get(), &QSocketNotifier::activated, [=](int fd) { callback(fd, EVENT_WRITE); }); read_->setEnabled(events_ & EVENT_READ); write_->setEnabled(events_ & EVENT_WRITE); } void callback(int fd, uint8_t events) { assert(!in_callback_); in_callback_ = true; callback_(fd, events); in_callback_ = false; if (delayed_delete_) { read_->setEnabled(false); read_.release()->deleteLater(); write_->setEnabled(false); write_.release()->deleteLater(); delete this; } } }; void init() { init_ = true; for (auto& pair : fds_) { pair.second->init(); } } bool init_; std::unordered_map> fds_; std::unordered_set timers_; }; Looper* QtGuiMain::create_looper() { return new QtLooper(this); } } // namespace // static GuiMain* GuiMain::create(std::string const& title, uint32_t width, uint32_t height) { return new QtGuiMain(title, width, height); } // static GuiMenu* GuiMenu::create() { return new QtGuiMenu(); } // static const uint32_t GuiMenu::CTRL = 1 << 0; const uint32_t GuiMenu::ALT = 1 << 1; const uint32_t GuiMenu::SHIFT = 1 << 2; const uint32_t GuiMenu::META = 1 << 3; // static const char GuiMenu::ESC = 0x1b; const char GuiMenu::UP = 1; const char GuiMenu::LEFT = 2; const char GuiMenu::RIGHT = 3; const char GuiMenu::DOWN = 4; const char GuiMenu::RETURN = '\n'; // static GuiStatusBar* GuiStatusBar::create() { return new QtGuiStatusBar(); } // static GuiAbout* GuiAbout::create(std::string const& title, std::string const& version, const char* author_name, const char* author_email, ...) { std::vector authors; authors.push_back(author_name); authors.push_back(author_email); va_list args; va_start(args, author_email); while (true) { auto name = va_arg(args, char const*); if (!name) break; auto email = va_arg(args, char const*); authors.push_back(name); authors.push_back(email); } va_end(args); return new QtGuiAbout(title, version, authors); } // static GuiForm* GuiForm::create(std::string const& title, std::string const& text) { return new QtGuiForm(title, text); } // static uint8_t const GuiFile::FILE_OPEN = 0; uint8_t const GuiFile::FILE_SAVE = 1; // static GuiFormApply* GuiFormApply::create(std::string const& title, std::string const& text, std::string const& apply_button, Delegate* delegate) { return new QtGuiFormApply(title, text, apply_button, delegate); } // static GuiMessage* GuiMessage::create(Type type, std::string const& title, std::string const& text) { return new QtGuiMessage(type, title, text); } // static AttributedText* AttributedText::create() { return HtmlAttributedText::create(); } // static GuiTextWindow* GuiTextWindow::create(std::string const& title, uint32_t width, uint32_t height, AttributedText const* text) { return new QtGuiTextWindow(title, width, height, text); } // static GuiProgress* GuiProgress::create(std::string const& title, std::string const& text, float min, float max) { return new QtGuiProgress(title, text, min, max); }