diff --git a/docs/roadmap.md b/docs/roadmap.md index 26e4e4d..2f73445 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -3,10 +3,10 @@ See also [previous roadmap](./old_roadmap.md) for finished work. - [x] Back navigation with mouse 4th button (mbits-os/quick_dra#109) -- [ ] Shortcut discovery (mbits-os/quick_dra#104, As a user, I want shortcuts to popup, when I press Ctrl for 2 seconds) +- [x] Shortcut discovery (mbits-os/quick_dra#104, As a user, I want shortcuts to popup, when I press Ctrl for 2 seconds) - [x] Filter keyboard and mouse events to keep track, when to show and hide the shortcuts (mbits-os/quick_dra#107) - - [ ] Keep track of all widgets with shortcuts currently on screen (mbits-os/quick_dra#105) - - [ ] Show matching shortcuts, when Control is being held for one seconds (mbits-os/quick_dra#106) + - [x] Keep track of all widgets with shortcuts currently on screen (mbits-os/quick_dra#105) + - [x] Show matching shortcuts, when Control is being held for one seconds (mbits-os/quick_dra#106) - [ ] Update documentation up to and including Quick-DRA GUI - [ ] Automatic upload to ZUS ePłatnik - [ ] Get access to Profil Zaufany sandbox diff --git a/libs/gui/gui/CMakeLists.txt b/libs/gui/gui/CMakeLists.txt index 5ea5f44..6fb8b25 100644 --- a/libs/gui/gui/CMakeLists.txt +++ b/libs/gui/gui/CMakeLists.txt @@ -14,6 +14,7 @@ set(SRCS include/app/gui/PageStack.hpp include/app/gui/ShortcutDiscovery.hpp include/app/gui/types.hpp + include/app/gui/ToolTipLabel.hpp src/app/gui/CurrentColor.cpp src/app/gui/Globals.cpp src/app/gui/HeaderTitle.cpp @@ -21,6 +22,7 @@ set(SRCS src/app/gui/PageHeader.cpp src/app/gui/PageStack.cpp src/app/gui/ShortcutDiscovery.cpp + src/app/gui/ToolTipLabel.cpp ) set(RES res/icons.qrc) diff --git a/libs/gui/gui/include/app/gui/ShortcutDiscovery.hpp b/libs/gui/gui/include/app/gui/ShortcutDiscovery.hpp index 7718e57..d923be4 100644 --- a/libs/gui/gui/include/app/gui/ShortcutDiscovery.hpp +++ b/libs/gui/gui/include/app/gui/ShortcutDiscovery.hpp @@ -19,6 +19,8 @@ namespace ch = std::chrono; QT_FORWARD_DECLARE_CLASS(QToolButton) namespace quick_dra::gui { + class ToolTipLabel; + struct HolderPosition { QRect geometry; QWidget const* rectReference; @@ -92,6 +94,17 @@ namespace quick_dra::gui { struct PrivateTag {}; public: + struct LabelInfo { + QWidget* parent = nullptr; + ToolTipLabel* toolTip = nullptr; + QPoint origin{}; + QString text{}; + + bool operator==(LabelInfo const& rhs) const noexcept { + return parent == rhs.parent && origin == rhs.origin && text == rhs.text; + } + }; + struct Editor { static Editor* current; @@ -125,6 +138,7 @@ namespace quick_dra::gui { Editor beginHolderUpdate(); std::deque const& holders() const noexcept { return holders_; } + std::vector const& labels() const noexcept { return labels_; } public slots: void modifiersPressed(Qt::KeyboardModifiers); @@ -140,15 +154,6 @@ namespace quick_dra::gui { void labelsChanged(); private: - struct LabelInfo { - QWidget* parent = nullptr; - QWidget* toolTip = nullptr; - QPoint origin{}; - QString text{}; - - auto operator<=>(LabelInfo const&) const noexcept = default; - }; - enum class ModsState { Stable = false, Changed = true, diff --git a/libs/gui/gui/include/app/gui/ToolTipLabel.hpp b/libs/gui/gui/include/app/gui/ToolTipLabel.hpp new file mode 100644 index 0000000..5dafb41 --- /dev/null +++ b/libs/gui/gui/include/app/gui/ToolTipLabel.hpp @@ -0,0 +1,22 @@ +// Copyright (c) 2026 midnightBITS +// This code is licensed under MIT license (see LICENSE for details) + +#pragma once + +#include + +namespace quick_dra::gui { + class ToolTipLabel : public QLabel { + Q_OBJECT + + public: + ToolTipLabel(const QString& text, const QPoint& pos, QWidget* parent); + + void hideTip(); + + protected: + bool eventFilter(QObject*, QEvent*) override; + void paintEvent(QPaintEvent* e) override; + void resizeEvent(QResizeEvent* e) override; + }; +} // namespace quick_dra::gui diff --git a/libs/gui/gui/src/app/gui/ShortcutDiscovery.cpp b/libs/gui/gui/src/app/gui/ShortcutDiscovery.cpp index 29c9c67..af63210 100644 --- a/libs/gui/gui/src/app/gui/ShortcutDiscovery.cpp +++ b/libs/gui/gui/src/app/gui/ShortcutDiscovery.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -206,16 +207,35 @@ namespace quick_dra::gui { } for (auto const& label : labels_) { - if (label.toolTip) label.toolTip->deleteLater(); // GCOV_EXCL_LINE (until #106) + if (label.toolTip) label.toolTip->hideTip(); } + labels_ = std::move(labels); - for (auto const& label : labels_) { - // TODO: mbits-os/quick_dra#106 Show matching shortcuts, when Control is being held for one seconds + + for (auto& label : labels_) { + label.toolTip = new ToolTipLabel{label.text, label.origin, label.parent}; + label.toolTip->showNormal(); } labelsChanged(); } + static bool visibleInStack(QWidget const* bottom, QPoint const& pt) { + auto point = pt; + auto widget = bottom; + + while (widget) { + if (!widget->rect().contains(point)) { + return false; + } + auto parent = qobject_cast(widget->parent()); + if (!parent) break; + point = widget->mapTo(parent, point); + widget = parent; + } + return true; + } + std::vector ShortcutDiscovery::filterShortcuts() { if (modifiers_ == Qt::NoModifier) { return {}; @@ -241,7 +261,7 @@ namespace quick_dra::gui { int lineHeight{fm.ascent() + fm.descent() + fm.leading()}; int em{fm.boundingRect("m").width()}; - int vOffset = -lineHeight * 2 / 3; + int vOffset = -lineHeight * 4 / 5; for (auto const& holder : holders_) { if (!holder.isEnabled(holder.holder)) continue; @@ -253,8 +273,12 @@ namespace quick_dra::gui { auto const pos = holder.getPosition(holder.holder); auto const topLevelParent = pos.rectReference->topLevelWidget(); - auto const position = - pos.rectReference->mapTo(topLevelParent, pos.geometry.bottomLeft()) + QPoint{hOffset, vOffset}; + auto const local = pos.geometry.bottomLeft() + QPoint{hOffset, vOffset}; + if (!visibleInStack(pos.rectReference, local)) { + continue; + } + + auto const position = pos.rectReference->mapToGlobal(local); auto label = QKeySequence{mod.key() | (mod.keyboardModifiers() & ~modifiers_)}.toString(); hOffset += 2 * em + fm.boundingRect(label).width(); diff --git a/libs/gui/gui/src/app/gui/ToolTipLabel.cpp b/libs/gui/gui/src/app/gui/ToolTipLabel.cpp new file mode 100644 index 0000000..f78f81f --- /dev/null +++ b/libs/gui/gui/src/app/gui/ToolTipLabel.cpp @@ -0,0 +1,68 @@ +// Copyright (c) 2026 midnightBITS +// This code is licensed under MIT license (see LICENSE for details) + +#include +#include +#include +#include +#include +#include +#include + +namespace quick_dra::gui { + ToolTipLabel::ToolTipLabel(const QString& value, const QPoint& pos, QWidget* parent) + : QLabel{parent, Qt::ToolTip | Qt::BypassGraphicsProxyWidget} { + setFont(QFont{"QTipLabel"}); + setForegroundRole(QPalette::ToolTipText); + setBackgroundRole(QPalette::ToolTipBase); + setPalette(QToolTip::palette()); + ensurePolished(); + setMargin(1 + style()->pixelMetric(QStyle::PM_ToolTipLabelFrameWidth, nullptr, this)); + setFrameStyle(QFrame::NoFrame); + setAlignment(Qt::AlignLeft); + setIndent(1); + qApp->installEventFilter(this); + setWindowOpacity(style()->styleHint(QStyle::SH_ToolTipLabel_Opacity, nullptr, this) / 255.0); + setMouseTracking(true); + setText(value); + + QFontMetrics fm{font()}; + QSize extra{1, 0}; + + // Make it look good with the default ToolTip font on Mac, which has a small descent. + if (fm.descent() == 2 && fm.ascent() >= 11) ++extra.rheight(); + + setWordWrap(Qt::mightBeRichText(text())); + QSize sh = sizeHint(); + move(pos); + resize(sh + extra); + } + + void ToolTipLabel::hideTip() { + close(); + deleteLater(); + } + + bool ToolTipLabel::eventFilter(QObject*, QEvent*) { return false; } + + void ToolTipLabel::paintEvent(QPaintEvent* event) { + QStylePainter p(this); + QStyleOptionFrame opt; + opt.initFrom(this); + p.drawPrimitive(QStyle::PE_PanelTipLabel, opt); + p.end(); + + QLabel::paintEvent(event); + } + + void ToolTipLabel::resizeEvent(QResizeEvent* event) { + QStyleHintReturnMask frameMask; + QStyleOption option; + option.initFrom(this); + if (style()->styleHint(QStyle::SH_ToolTip_Mask, &option, this, &frameMask)) { + setMask(frameMask.region); // GCOV_EXCL_LINE + } + + QLabel::resizeEvent(event); + } +} // namespace quick_dra::gui diff --git a/libs/gui/gui/tests/qt6/ShortcutDiscovery.test.cpp b/libs/gui/gui/tests/qt6/ShortcutDiscovery.test.cpp index 5e8dc74..60c3f62 100644 --- a/libs/gui/gui/tests/qt6/ShortcutDiscovery.test.cpp +++ b/libs/gui/gui/tests/qt6/ShortcutDiscovery.test.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include "GuiTest.hpp" #include "ui_helpers.hpp" @@ -31,6 +32,8 @@ namespace { return Qt::NoModifier; } + + QString operator""_L1(char const* ptr, size_t len) { return QString::fromLatin1(ptr, static_cast(len)); } } // namespace struct HUD { @@ -297,18 +300,20 @@ void GuiTest::ShortcutDiscovery_gatherTooltips() { QWidget widget{}; HUD hud{&widget}; - auto layout = new QVBoxLayout{&widget}; + auto parent = new QWidget{&widget}; + auto layout = new QVBoxLayout{parent}; layout->setContentsMargins(5, 5, 5, 5); layout->setSpacing(5); std::vector widgets{}; widgets.reserve(std::size(controls)); - std::transform(std::begin(controls), std::end(controls), std::back_inserter(widgets), widgetMaker(&widget, layout)); + std::transform(std::begin(controls), std::end(controls), std::back_inserter(widgets), widgetMaker(parent, layout)); int height = 5; for (auto const& control : controls) { height += std::get<0>(control) + 5; } - widget.resize(200, height); + parent->resize(200, height); + widget.resize(200, height * 2 / 3); widget.show(); QVERIFY(QTest::qWaitForWindowExposed(&widget)); @@ -333,6 +338,25 @@ void GuiTest::ShortcutDiscovery_gatherTooltips() { QCOMPARE_EQ(labelsSpy.size(), 1); QCOMPARE_EQ(activeSpy.size(), 1); + auto const previous = discovery.labels(); + { + // "empty" update + discovery.beginHolderUpdate(); + } + auto const now = discovery.labels(); + auto actual = QList>{}; + actual.reserve(static_cast(now.size())); + std::transform(now.begin(), now.end(), std::back_inserter(actual), + [](ShortcutDiscovery::LabelInfo const& info) { return std::pair{info.origin, info.text}; }); + + labelsSpy.wait(100ms); + activeSpy.wait(100ms); + QCOMPARE_EQ(labelsSpy.size(), 1); + QCOMPARE_EQ(activeSpy.size(), 1); + QCOMPARE_EQ(previous, now); + QCOMPARE_EQ(actual, (QList{std::pair{QPoint{17, 49}, "Shift+E"_L1}, std::pair{QPoint{17, 108}, "Alt+Z"_L1}, + std::pair{QPoint{71, 108}, "Z"_L1}})); + { auto editor = discovery.beginHolderUpdate(); for (auto child : widgets) { @@ -375,4 +399,9 @@ void GuiTest::ShortcutDiscovery_toolButton() { button.setVisible(true); QVERIFY(Support::isEnabled(&button)); + + // call painting the tool tip label + ToolTipLabel label{"Text", QPoint{}, nullptr}; + label.showNormal(); + QVERIFY(QTest::qWaitForWindowExposed(&label)); } diff --git a/libs/gui/pages/include/app/main/MainWindow.hpp b/libs/gui/pages/include/app/main/MainWindow.hpp index 8837208..3f7c469 100644 --- a/libs/gui/pages/include/app/main/MainWindow.hpp +++ b/libs/gui/pages/include/app/main/MainWindow.hpp @@ -36,7 +36,6 @@ namespace quick_dra::gui { private slots: void updateTitle(); void reloadConfig(); - void discoveryModifiers(Qt::KeyboardModifiers); private: void setupUi(Globals* globals); diff --git a/libs/gui/pages/src/app/main/MainWindow.cpp b/libs/gui/pages/src/app/main/MainWindow.cpp index 52c30f5..5f4f4c4 100644 --- a/libs/gui/pages/src/app/main/MainWindow.cpp +++ b/libs/gui/pages/src/app/main/MainWindow.cpp @@ -25,8 +25,6 @@ namespace quick_dra::gui { pageStack->push(); QObject::connect(globals, &Globals::configModifiedChanged, messageBar, &QWidget::setVisible); - QObject::connect(&pageStack->discovery(), &ShortcutDiscovery::modifiersChanged, this, - &MainWindow::discoveryModifiers); } void MainWindow::closeEvent(QCloseEvent* event) { @@ -54,31 +52,6 @@ namespace quick_dra::gui { pageStack->globals().reloadConfig(); } - // GCOV_EXCL_START -- temporary slot to verify signals via console - // TODO: remove this slot before closing of #104 - void MainWindow::discoveryModifiers(Qt::KeyboardModifiers modifiers) { - std::string sMods{}; - if (!modifiers) { - sMods = "NoModifier"s; - } -#define CHECK_MOD(NAME, LABEL) \ - if (modifiers & Qt::NAME) { \ - modifiers &= ~Qt::NAME; \ - if (!sMods.empty()) sMods.push_back('+'); \ - sMods.append(LABEL##sv); \ - } - CHECK_MOD(ShiftModifier, "Shift"); - CHECK_MOD(ControlModifier, "Ctrl"); - CHECK_MOD(MetaModifier, "Meta"); - CHECK_MOD(AltModifier, "Alt"); - if (modifiers) { - if (!sMods.empty()) sMods.push_back('+'); - sMods.append(std::to_string(modifiers.toInt())); - } - qDebug() << "[discovery]" << sMods.c_str(); - } - // GCOV_EXCL_STOP - void MainWindow::storePosition(Globals* globals) { auto settings = globals->createSettings(); settings.beginGroup("State");