|
#define POPUP_USING_WDIALOG__OTHERWISE_USING_WMESSAGEBOX 0
|
|
|
|
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
|
#include <Wt/WAbstractTableModel.h>
|
|
#pragma GCC diagnostic pop
|
|
#include <Wt/WApplication.h>
|
|
#include <Wt/WBootstrap5Theme.h>
|
|
#include <Wt/WContainerWidget.h>
|
|
#include <Wt/WDialog.h>
|
|
#include <Wt/WEvent.h>
|
|
#include <Wt/WGridLayout.h>
|
|
#include <Wt/WHBoxLayout.h>
|
|
#include <Wt/WLineEdit.h>
|
|
#include <Wt/WMenuItem.h>
|
|
#include <Wt/WMessageBox.h>
|
|
#include <Wt/WPanel.h>
|
|
#include <Wt/WPopupMenu.h>
|
|
#include <Wt/WPushButton.h>
|
|
#include <Wt/WServer.h>
|
|
#include <Wt/WTableView.h>
|
|
#include <Wt/WTabWidget.h>
|
|
#include <Wt/WText.h>
|
|
#include <Wt/WVBoxLayout.h>
|
|
|
|
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<WBootstrap5Theme>());
|
|
#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<typename T, typename... TArgs>
|
|
constexpr inline
|
|
std::unique_ptr<T> create(std::function<void (T&)>&& fInitialize, TArgs&&... args)
|
|
{
|
|
std::unique_ptr<T> Ptr{std::make_unique<T>(std::forward<TArgs>(args)...)};
|
|
fInitialize(*Ptr);
|
|
return Ptr;
|
|
}
|
|
|
|
inline void setAccessKey(WInteractWidget& widget, const char mnemonic) { widget.setAttributeValue("accesskey", string(1, tolower(mnemonic))); }
|
|
|
|
template <>
|
|
class std::formatter<WFlags<KeyboardModifier>> : public formatter<string>
|
|
{
|
|
public: auto format(const WFlags<KeyboardModifier>& rcModifiers, format_context& rCtx) const
|
|
{
|
|
return formatter<string>::format(std::format("0x{:02x}", rcModifiers.value()), rCtx);
|
|
}
|
|
};
|
|
|
|
template <>
|
|
class std::formatter<WKeyEvent> : public formatter<string>
|
|
{
|
|
public: auto format(const WKeyEvent& rcKeyEvent, format_context& rCtx) const
|
|
{
|
|
return formatter<string>::format(std::format("{{ key: '{}' {} 0x{:02x}, modifiers: {}, charCode: '{}' {} 0x{:02x} }}"
|
|
, static_cast<char>(rcKeyEvent.key()), static_cast<int>(rcKeyEvent.key()), static_cast<int>(rcKeyEvent.key())
|
|
, rcKeyEvent.modifiers()
|
|
, static_cast<char>(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<StandardButton> 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<WMessageBox>(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<WVBoxLayout>())};
|
|
|
|
unique_ptr<WPopupMenu> _mnuContext{make_unique<WPopupMenu>()};
|
|
WMenuItem& _miContext{*_mnuContext->addItem("context")};
|
|
|
|
unique_ptr<WPopupMenu> _mnuSub{make_unique<WPopupMenu>()};
|
|
WMenuItem& _miSubLong{*_mnuSub->addItem("subitem with longer label 1111111111111111111 1111111111111111111 1111111111111111111111111 1111111111111111111111")};
|
|
|
|
unique_ptr<WPopupMenu> _mnuMain{make_unique<WPopupMenu>()};
|
|
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<WMenu>())};
|
|
WMenuItem& _miMain{[this] -> WMenuItem& { WMenuItem& item{*_mainMenu.addItem("main")}; item.setMenu(move(_mnuMain)); return item; }()};
|
|
|
|
WLineEdit& _ed{*_lytRoot.addWidget(make_unique<WLineEdit>())};
|
|
|
|
WPushButton& _btnP{*_lytRoot.addWidget(create<WPushButton>([this](auto& btnP)
|
|
{
|
|
btnP.clicked().connect([this] { _tvA.setRowHeaderCount(1); });
|
|
btnP.setTextFormat(TextFormat::UnsafeXHTML);
|
|
setAccessKey(btnP, 'F');
|
|
}, "<u>F</u>ix <i>first column</i> of A - by means of 'set<i>RowHeader</i>Count(1)'"))};
|
|
|
|
WTabWidget& _tbwMain{*_lytRoot.addWidget(make_unique<WTabWidget>(), /*stretch*/ 100)};
|
|
WMenuItem& _tabConnecting{*_tbwMain.addTab(make_unique<WText>(), "connecting...", ContentLoading::Eager)};
|
|
WMenuItem& _tabA{*_tbwMain.addTab(make_unique<WTableView>(), "A", ContentLoading::Eager)};
|
|
WTableView& _tvA{dynamic_cast<WTableView&>(*_tbwMain.widget(_tbwMain.count() - 1))};
|
|
WMenuItem& _tabB{*_tbwMain.addTab(make_unique<WTableView>(), "B", ContentLoading::Eager)};
|
|
WTableView& _tvB{dynamic_cast<WTableView&>(*_tbwMain.widget(_tbwMain.count() - 1))};
|
|
|
|
WPanel& _pnlBottom{*_lytRoot.addWidget(make_unique<WPanel>())};
|
|
WContainerWidget& _cntBottom{*_pnlBottom.setCentralWidget(make_unique<WContainerWidget>())};
|
|
WBoxLayout& _lytBottom{*_cntBottom.setLayout(make_unique<WHBoxLayout>())};
|
|
|
|
WPushButton& _btnUp{*_lytBottom.addWidget(create<WPushButton>([this](auto& btnUp)
|
|
{
|
|
btnUp.clicked().connect([this] { _tvA.select(_mdlAPtr->move(*_tvA.selectedIndexes().begin(), -1, 0)); });
|
|
btnUp.setTextFormat(TextFormat::UnsafeXHTML);
|
|
setAccessKey(btnUp, 'U');
|
|
}, "<u>U</u>p A"))};
|
|
WPushButton& _btnDown{*_lytBottom.addWidget(create<WPushButton>([this](auto& btnDown)
|
|
{
|
|
btnDown.clicked().connect([this] { _tvA.select(_mdlAPtr->move(*_tvA.selectedIndexes().begin(), +1, 0)); });
|
|
btnDown.setTextFormat(TextFormat::UnsafeXHTML);
|
|
setAccessKey(btnDown, 'D');
|
|
}, "<u>D</u>own A"))};
|
|
WPushButton& _btnLeft{*_lytBottom.addWidget(create<WPushButton>([this](auto& btnLeft)
|
|
{
|
|
btnLeft.clicked().connect([this] { _tvA.select(_mdlAPtr->move(*_tvA.selectedIndexes().begin(), 0, -1)); });
|
|
btnLeft.setTextFormat(TextFormat::UnsafeXHTML);
|
|
setAccessKey(btnLeft, 'L');
|
|
}, "<u>L</u>eft A"))};
|
|
WPushButton& _btnRight{*_lytBottom.addWidget(create<WPushButton>([this](auto& btnRight)
|
|
{
|
|
btnRight.clicked().connect([this] { _tvA.select(_mdlAPtr->move(*_tvA.selectedIndexes().begin(), 0, +1)); });
|
|
btnRight.setTextFormat(TextFormat::UnsafeXHTML);
|
|
setAccessKey(btnRight, 'R');
|
|
}, "<u>R</u>ight A"))};
|
|
WPushButton& _btnPopup{*_lytBottom.addWidget(create<WPushButton>([this](auto& btnPopup)
|
|
{
|
|
btnPopup.clicked().connect([this] { showMessageBox("popup on button press"); });
|
|
btnPopup.setTextFormat(TextFormat::UnsafeXHTML);
|
|
setAccessKey(btnPopup, 'P');
|
|
}, "<u>P</u>opup"))};
|
|
|
|
shared_ptr<CFixedTableModel> _mdlAPtr{make_shared<CFixedTableModel>(100, 50)};
|
|
shared_ptr<CFixedTableModel> _mdlBPtr{make_shared<CFixedTableModel>(200, 100)};
|
|
|
|
const vector<reference_wrapper<WWidget>> _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<CFixedTableModel>& 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<CFixedTableModel&>(*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<CApplication>(rcEnv);
|
|
}
|
|
catch (const std::exception& rcException)
|
|
{
|
|
cerr << "exception " << typeid(remove_cvref<decltype(rcException)>::type).name() << ": " << rcException.what() << endl;
|
|
|
|
#if DEBUG
|
|
exit(-1);
|
|
#endif
|
|
throw;
|
|
}
|
|
});
|
|
}
|