#define POPUP_USING_WDIALOG__OTHERWISE_USING_WMESSAGEBOX 0 #pragma GCC diagnostic ignored "-Wdeprecated-declarations" #include #pragma GCC diagnostic pop #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; using namespace Wt; using namespace Wt::cpp17; #define SET_BS5THEME 1 // Always apply theme before creating widgets! class CThemedApplication : public WApplication { public: CThemedApplication(const WEnvironment& rcEnv) : WApplication{rcEnv} { #if SET_BS5THEME setTheme(make_shared()); #endif } }; class CFixedTableModel : public WAbstractTableModel { public: CFixedTableModel(const int rowCount, const int colCount) : _rowCount{rowCount} , _colCount{colCount} { } any headerData(const int section, const Orientation orientation, const ItemDataRole role) const { if (orientation != Orientation::Horizontal) throw invalid_argument("orientation"); switch (role.value()) { default: return {}; case ItemDataRole::Display: return format("C{}", section); } } any data(const WModelIndex& index, const ItemDataRole role) const override { if (!index.isValid()) return {}; switch (role.value()) { default: return {}; case ItemDataRole::Display: return format("{}, {}", index.row(), index.column()); } } //! Returns the WModelIndex moved by delta. [[nodiscard]] WModelIndex move(const WModelIndex& index, int rowDelta, int colDelta) const { const int row{clamp(index.row() + rowDelta, 0, _rowCount - 1)}; const int col{clamp(index.column() + colDelta, 0, _colCount - 1)}; return createIndex(row, col, nullptr); } int columnCount(const WModelIndex& index) const override { return index.isValid() ? 0 : _colCount; } int rowCount (const WModelIndex& index) const override { return index.isValid() ? 0 : _rowCount; } const int _rowCount; const int _colCount; }; template constexpr inline std::unique_ptr create(std::function&& fInitialize, TArgs&&... args) { std::unique_ptr Ptr{std::make_unique(std::forward(args)...)}; fInitialize(*Ptr); return Ptr; } inline void setAccessKey(WInteractWidget& widget, const char mnemonic) { widget.setAttributeValue("accesskey", string(1, tolower(mnemonic))); } template <> class std::formatter> : public formatter { public: auto format(const WFlags& rcModifiers, format_context& rCtx) const { return formatter::format(std::format("0x{:02x}", rcModifiers.value()), rCtx); } }; template <> class std::formatter : public formatter { public: auto format(const WKeyEvent& rcKeyEvent, format_context& rCtx) const { return formatter::format(std::format("{{ key: '{}' {} 0x{:02x}, modifiers: {}, charCode: '{}' {} 0x{:02x} }}" , static_cast(rcKeyEvent.key()), static_cast(rcKeyEvent.key()), static_cast(rcKeyEvent.key()) , rcKeyEvent.modifiers() , static_cast(rcKeyEvent.charCode()), rcKeyEvent.charCode(), rcKeyEvent.charCode() ) , rCtx ); } }; class CApplication final : public CThemedApplication { public: CApplication(const WEnvironment& rcEnv); void showMessageBox(const WString& rcsMessage, const WString& rcsCaption = WString::Empty, const Icon ceIcon = Icon::None, const WFlags ceButtons = StandardButton::Ok, const StandardButton ceDefaultButton = StandardButton::Ok) { #if POPUP_USING_WDIALOG__OTHERWISE_USING_WMESSAGEBOX _dlgMessage.setWindowTitle(rcsMessage); _dlgMessage.setClosable(true); _dlgMessage.show(); _dlgMessage.keyWentDown().connect([this] { _dlgMessage.reject(); }); suppressBrowserContextMenu(_dlgMessage); (void)rcsCaption; (void)ceIcon; (void)ceButtons; (void)ceDefaultButton; #else const WString& title{rcsCaption.empty() ? WApplication::title() : rcsCaption}; WMessageBox& messagebox{*_cntRoot.addChild(make_unique(title, rcsMessage, ceIcon, ceButtons))}; if (ceDefaultButton != StandardButton::Ok) { messagebox.setDefaultButton(ceDefaultButton); } messagebox.buttonClicked().connect([&messagebox, this] { _cntRoot.removeChild(&messagebox); }); messagebox.show(); #endif } static void suppressBrowserContextMenu(WWidget& rWidget) { rWidget.setAttributeValue("oncontextmenu", "event.cancelBubble = true; event.returnValue = false; return false;"); } private: WServer& _Server; //!< The associated WServer instance, needed for async operations. WContainerWidget& _cntRoot{*root()}; WBoxLayout& _lytRoot{*_cntRoot.setLayout(make_unique())}; unique_ptr _mnuContext{make_unique()}; WMenuItem& _miContext{*_mnuContext->addItem("context")}; unique_ptr _mnuSub{make_unique()}; WMenuItem& _miSubLong{*_mnuSub->addItem("subitem with longer label 1111111111111111111 1111111111111111111 1111111111111111111111111 1111111111111111111111")}; unique_ptr _mnuMain{make_unique()}; WMenuItem& _miMainItem{*_mnuMain->addItem("item")}; WMenuItem& _miMainSubmenu{[this] -> WMenuItem& { WMenuItem& item{*_mnuMain->addItem("submenu")}; item.setMenu(move(_mnuSub)); return item; }()}; WMenu& _mainMenu{*_lytRoot.addWidget(make_unique())}; WMenuItem& _miMain{[this] -> WMenuItem& { WMenuItem& item{*_mainMenu.addItem("main")}; item.setMenu(move(_mnuMain)); return item; }()}; WLineEdit& _ed{*_lytRoot.addWidget(make_unique())}; WPushButton& _btnP{*_lytRoot.addWidget(create([this](auto& btnP) { btnP.clicked().connect([this] { _tvA.setRowHeaderCount(1); }); btnP.setTextFormat(TextFormat::UnsafeXHTML); setAccessKey(btnP, 'F'); }, "Fix first column of A - by means of 'setRowHeaderCount(1)'"))}; WTabWidget& _tbwMain{*_lytRoot.addWidget(make_unique(), /*stretch*/ 100)}; WMenuItem& _tabConnecting{*_tbwMain.addTab(make_unique(), "connecting...", ContentLoading::Eager)}; WMenuItem& _tabA{*_tbwMain.addTab(make_unique(), "A", ContentLoading::Eager)}; WTableView& _tvA{dynamic_cast(*_tbwMain.widget(_tbwMain.count() - 1))}; WMenuItem& _tabB{*_tbwMain.addTab(make_unique(), "B", ContentLoading::Eager)}; WTableView& _tvB{dynamic_cast(*_tbwMain.widget(_tbwMain.count() - 1))}; WPanel& _pnlBottom{*_lytRoot.addWidget(make_unique())}; WContainerWidget& _cntBottom{*_pnlBottom.setCentralWidget(make_unique())}; WBoxLayout& _lytBottom{*_cntBottom.setLayout(make_unique())}; WPushButton& _btnUp{*_lytBottom.addWidget(create([this](auto& btnUp) { btnUp.clicked().connect([this] { _tvA.select(_mdlAPtr->move(*_tvA.selectedIndexes().begin(), -1, 0)); }); btnUp.setTextFormat(TextFormat::UnsafeXHTML); setAccessKey(btnUp, 'U'); }, "Up A"))}; WPushButton& _btnDown{*_lytBottom.addWidget(create([this](auto& btnDown) { btnDown.clicked().connect([this] { _tvA.select(_mdlAPtr->move(*_tvA.selectedIndexes().begin(), +1, 0)); }); btnDown.setTextFormat(TextFormat::UnsafeXHTML); setAccessKey(btnDown, 'D'); }, "Down A"))}; WPushButton& _btnLeft{*_lytBottom.addWidget(create([this](auto& btnLeft) { btnLeft.clicked().connect([this] { _tvA.select(_mdlAPtr->move(*_tvA.selectedIndexes().begin(), 0, -1)); }); btnLeft.setTextFormat(TextFormat::UnsafeXHTML); setAccessKey(btnLeft, 'L'); }, "Left A"))}; WPushButton& _btnRight{*_lytBottom.addWidget(create([this](auto& btnRight) { btnRight.clicked().connect([this] { _tvA.select(_mdlAPtr->move(*_tvA.selectedIndexes().begin(), 0, +1)); }); btnRight.setTextFormat(TextFormat::UnsafeXHTML); setAccessKey(btnRight, 'R'); }, "Right A"))}; WPushButton& _btnPopup{*_lytBottom.addWidget(create([this](auto& btnPopup) { btnPopup.clicked().connect([this] { showMessageBox("popup on button press"); }); btnPopup.setTextFormat(TextFormat::UnsafeXHTML); setAccessKey(btnPopup, 'P'); }, "Popup"))}; shared_ptr _mdlAPtr{make_shared(100, 50)}; shared_ptr _mdlBPtr{make_shared(200, 100)}; const vector> _appWidgets{_tabA, _tabB, _pnlBottom}; #if POPUP_USING_WDIALOG__OTHERWISE_USING_WMESSAGEBOX WDialog _dlgMessage{}; #endif }; CApplication::CApplication(const WEnvironment& rcEnv) : CThemedApplication{rcEnv} , _Server{*environment().server()} { setTitle("bug repro " WT_VERSION_STR); suppressBrowserContextMenu(_cntRoot); suppressBrowserContextMenu(*_mnuContext); globalKeyWentDown().connect([this](const WKeyEvent& e) { log("trace") << format("globalKeyWentDown({})", e); }); globalKeyWentUp().connect([this](const WKeyEvent& e) { log("trace") << format("globalKeyWentUp({})", e); if (e.key() == Key::M) showMessageBox("popup on globalKeyWentUp with 'M'"); }); const auto initTable = [this](WTableView& tv, const shared_ptr& mdlPtr) { //tv.setRowHeaderCount(1); // causes double scrollbar with 4.12.2 tv.setSelectionBehavior(SelectionBehavior::Items); tv.setSelectionMode(SelectionMode::Single); tv.setSortingEnabled(false); tv.setCanReceiveFocus(true); tv.setEditTriggers(EditTrigger::None); tv.setAlternatingRowColors(true); tv.setModel(mdlPtr); tv.selectionChanged().connect([&tv] { const WModelIndexSet selectedIndexes{tv.selectionModel()->selectedIndexes()}; switch (selectedIndexes.size()) { case 0: break; case 1: tv.scrollTo(*selectedIndexes.cbegin()); break; default: throw logic_error("multi-selection unexpected"); } }); tv.select(mdlPtr->move(WModelIndex{}, 0, 1)); tv.keyWentDown().connect([&tv, this](const WKeyEvent& rcKeyEvent) { const WModelIndex selected{*tv.selectedIndexes().cbegin()}; const CFixedTableModel& model{dynamic_cast(*tv.model())}; const int ciMax{0x3FFFFFFF}; switch (rcKeyEvent.modifiers()) { case KeyboardModifier::None: switch (rcKeyEvent.key()) { case Key::Up: return tv.select(model.move(selected, -1, 0)); case Key::Down: return tv.select(model.move(selected, +1, 0)); case Key::Left: return tv.select(model.move(selected, 0, -1)); case Key::Right: return tv.select(model.move(selected, 0, +1)); case Key::Home: return tv.select(model.move(selected, 0, -ciMax)); case Key::End: return tv.select(model.move(selected, 0, +ciMax)); case Key::PageUp: return tv.select(model.move(selected, -20, 0)); case Key::PageDown: return tv.select(model.move(selected, +20, 0)); default: return; } case KeyboardModifier::Control: switch (rcKeyEvent.key()) { case Key::Home: return tv.select(model.move(selected, -ciMax, -ciMax)); case Key::End: return tv.select(model.move(selected, +ciMax, +ciMax)); case Key::PageUp: log("trace") << "Ctrl+PageUp would work in this browser"; return; // caught by browser case Key::PageDown: log("trace") << "Ctrl+PageDn would work in this browser"; return; // caught by browser default: return; } case KeyboardModifier::Shift: switch (rcKeyEvent.key()) { case Key::Home: return tv.select(model.move(selected, 0, -ciMax)); case Key::End: return tv.select(model.move(selected, 0, +ciMax)); case Key::PageUp: return tv.select(model.move(selected, -ciMax, 0)); case Key::PageDown: return tv.select(model.move(selected, +ciMax, 0)); default: return; } default: return; } }); tv.mouseWentDown().connect([&tv, this](const WModelIndex& index, const WMouseEvent& e) { if (e.button() != MouseButton::Right) return; _mnuContext->popup(tv.itemWidget(index)); }); tv.mouseWentUp().connect([&tv, this](const WModelIndex& index, const WMouseEvent& e) { if (e.button() != MouseButton::Middle) return; (void)index; showMessageBox("popup on mouseWentUp"); }); }; initTable(_tvA, _mdlAPtr); initTable(_tvB, _mdlBPtr); _tvB.setHeaderHeight(0); _tvB.setRowHeaderCount(1); _ed.mouseWentUp().connect([this](const WMouseEvent& e) { if (e.button() != MouseButton::Right) return; showMessageBox("popup on mouseWentUp"); }); _lytBottom.setContentsMargins(0, 0, 0, 0); _lytBottom.addStretch(100); const auto showAppWidgets{[this](const bool show) { for (WWidget& appWidget : _appWidgets) { appWidget.setHidden(!show); } }}; showAppWidgets(false); _Server.schedule(300ms, sessionId(), [this, showAppWidgets = move(showAppWidgets)] { showAppWidgets(true); _tabConnecting.hide(); log("trace") << "canReceiveFocus " << _tvA.canReceiveFocus(); // is true (by default) _tabA.setFirstFocus(); // Why doesn't this have any effect?!? _tvA.setFocus(); triggerUpdate(); }); // final action after creating the UI enableUpdates(); } int main(int argc, char* argv[]) { return WRun(argc, argv, [](const WEnvironment& rcEnv) { try { return make_unique(rcEnv); } catch (const std::exception& rcException) { cerr << "exception " << typeid(remove_cvref::type).name() << ": " << rcException.what() << endl; #if DEBUG exit(-1); #endif throw; } }); }