diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2c3c424..afb2053 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,6 +27,15 @@ jobs: qmake fuzzy_clock.pro "CONFIG+=release" nmake release + - name: Тесты + run: | + New-Item -ItemType Directory tests-build | Out-Null + Set-Location tests-build + qmake ..\tests\tests.pro "CONFIG+=release" + nmake release + .\release\fuzzy_clock_tests.exe + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + - name: Развёртывание run: | New-Item -ItemType Directory deploy | Out-Null diff --git a/FuzzyClock_from KDE (original C source).cab b/FuzzyClock_from KDE (original C source).cab new file mode 100644 index 0000000..ab114dc Binary files /dev/null and b/FuzzyClock_from KDE (original C source).cab differ diff --git a/fuzzy_clock.pro b/fuzzy_clock.pro index aa0f092..981a3ba 100644 --- a/fuzzy_clock.pro +++ b/fuzzy_clock.pro @@ -16,11 +16,13 @@ win32: RC_FILE = resource.rc HEADERS += \ fuzzy_clock.h \ + fuzzy_dock.h \ fuzzy_helper.h \ fuzzy_clock_window.h SOURCES += \ fuzzy_clock.cpp \ + fuzzy_dock.cpp \ fuzzy_helper.cpp \ fuzzy_clock_window.cpp \ main.cpp diff --git a/fuzzy_clock_window.cpp b/fuzzy_clock_window.cpp index 1842e1d..eb0d381 100644 --- a/fuzzy_clock_window.cpp +++ b/fuzzy_clock_window.cpp @@ -5,6 +5,7 @@ #include #include "fuzzy_clock_window.h" +#include "fuzzy_dock.h" #include "fuzzy_helper.h" namespace { @@ -42,6 +43,9 @@ const Opacity Opacities[] = { { "10%", 0.1 }, { "50%", 0.5 }, { "100%", 1.0 } }; const char *const FuzzinessNames[] = { "Четверть пятого", "Вечер", "Середина недели" }; +// на каком расстоянии от края экрана окно к нему притягивается +constexpr int SnapDistance = 15; + QString autostartCommand() { return "\"" + QDir::toNativeSeparators(QCoreApplication::applicationFilePath()) + "\""; @@ -104,6 +108,15 @@ fuzzyClockWindow::fuzzyClockWindow() restorePosition(); setWindowOpacity(m_Settings.value("Opacity", 1.0).toDouble()); + // панель задач переместилась, сменилось разрешение - окно остаётся у своих краёв + const auto watchScreen = [this](QScreen *screen) { + connect(screen, &QScreen::availableGeometryChanged, this, &fuzzyClockWindow::applyDock); + }; + for (QScreen *screen : QGuiApplication::screens()) + watchScreen(screen); + connect(qApp, &QGuiApplication::screenAdded, this, watchScreen); + connect(qApp, &QGuiApplication::screenRemoved, this, &fuzzyClockWindow::applyDock, Qt::QueuedConnection); + /* Context menu */ createMenu(); @@ -133,6 +146,7 @@ void fuzzyClockWindow::updateTime() setToolTip(QLocale(QLocale::Russian).toString(now, "HH:mm, dddd, d MMMM")); m_label->adjustSize(); // учитывает шрифт из CSS (вызывает ensurePolished) setFixedSize(m_label->size() + QSize(10, 10)); + applyDock(); // при прилипании справа или снизу окно растёт внутрь экрана // следующее обновление - в начале следующей минуты m_timer.start(60000 - now.time().msecsSinceStartOfDay() % 60000); @@ -190,17 +204,45 @@ void fuzzyClockWindow::mousePressEvent(QMouseEvent *event) mpos = event->pos(); } -// Drag window without title +// Drag window without title; Shift отключает прилипание к краям void fuzzyClockWindow::mouseMoveEvent(QMouseEvent *event) { - if (event->buttons() & Qt::LeftButton) - move(pos() + event->pos() - mpos); + if (!(event->buttons() & Qt::LeftButton)) + return; + const QPoint p = pos() + event->pos() - mpos; + if (event->modifiers() & Qt::ShiftModifier) + move(p); + else + move(fuzzyDock::snapped(QRect(p, size()), dockArea(p), SnapDistance)); } void fuzzyClockWindow::mouseReleaseEvent(QMouseEvent *event) { - if (event->button() == Qt::LeftButton) + if (event->button() == Qt::LeftButton) { + m_dock = fuzzyDock::edges(geometry(), dockArea(pos())); savePosition(); // перетаскивание закончено + } +} + +QRect fuzzyClockWindow::dockArea(const QPoint &topLeft) const +{ + const QScreen *screen = QGuiApplication::screenAt(QRect(topLeft, size()).center()); + if (!screen) + screen = QGuiApplication::screenAt(pos()); + if (!screen) + screen = QGuiApplication::primaryScreen(); + return screen->availableGeometry(); // без панели задач +} + +void fuzzyClockWindow::applyDock() +{ + if (!m_dock) + return; + const QPoint p = fuzzyDock::docked(geometry(), dockArea(pos()), m_dock); + if (p != pos()) { + move(p); + savePosition(); + } } void fuzzyClockWindow::closeEvent(QCloseEvent *event) @@ -226,6 +268,7 @@ void fuzzyClockWindow::savePosition() m_Settings.setValue("currentPath", QDir::currentPath()); m_Settings.setValue("Top", pos().y()); m_Settings.setValue("Left", pos().x()); + m_Settings.setValue("Dock", m_dock); } void fuzzyClockWindow::restorePosition() @@ -235,6 +278,7 @@ void fuzzyClockWindow::restorePosition() if (!QGuiApplication::screenAt(p)) p = QGuiApplication::primaryScreen()->availableGeometry().center(); move(p); + m_dock = m_Settings.value("Dock", 0).toInt(); // применяется после первого расчёта размера в updateTime() } void fuzzyClockWindow::createMenu() diff --git a/fuzzy_clock_window.h b/fuzzy_clock_window.h index 3e5589c..79a1f49 100644 --- a/fuzzy_clock_window.h +++ b/fuzzy_clock_window.h @@ -48,6 +48,10 @@ private slots: void restorePosition(); void savePosition(); + // Прилипание к краям экрана (расчёты - в fuzzy_dock.h) + QRect dockArea(const QPoint &topLeft) const; // рабочая область экрана, на котором окажется окно + void applyDock(); // удержать прижатые края при смене размера окна или экрана + fuzzyClock::Fuzziness fuzziness() const; fuzzyClock m_clock; @@ -55,6 +59,7 @@ private slots: QTimer m_timer; QSettings m_Settings; QPoint mpos; + int m_dock = 0; // сочетание fuzzyDock::Edge QMenu *m_menu; QAction *m_autostartAct; diff --git a/fuzzy_dock.cpp b/fuzzy_dock.cpp new file mode 100644 index 0000000..7c94494 --- /dev/null +++ b/fuzzy_dock.cpp @@ -0,0 +1,52 @@ +#include + +#include "fuzzy_dock.h" + +namespace fuzzyDock { + +QPoint snapped(const QRect &window, const QRect &area, int distance) +{ + QPoint p = window.topLeft(); + + if (qAbs(window.left() - area.left()) <= distance) + p.setX(area.left()); + else if (qAbs(window.right() - area.right()) <= distance) + p.setX(area.right() - window.width() + 1); + + if (qAbs(window.top() - area.top()) <= distance) + p.setY(area.top()); + else if (qAbs(window.bottom() - area.bottom()) <= distance) + p.setY(area.bottom() - window.height() + 1); + + return p; +} + +int edges(const QRect &window, const QRect &area) +{ + int result = 0; + if (window.left() == area.left()) + result |= Left; + else if (window.right() == area.right()) + result |= Right; + if (window.top() == area.top()) + result |= Top; + else if (window.bottom() == area.bottom()) + result |= Bottom; + return result; +} + +QPoint docked(const QRect &window, const QRect &area, int edges) +{ + QPoint p = window.topLeft(); + if (edges & Left) + p.setX(area.left()); + else if (edges & Right) + p.setX(area.right() - window.width() + 1); + if (edges & Top) + p.setY(area.top()); + else if (edges & Bottom) + p.setY(area.bottom() - window.height() + 1); + return p; +} + +} diff --git a/fuzzy_dock.h b/fuzzy_dock.h new file mode 100644 index 0000000..053a48a --- /dev/null +++ b/fuzzy_dock.h @@ -0,0 +1,23 @@ +#ifndef FUZZY_DOCK_H +#define FUZZY_DOCK_H + +#include +#include + +// Прилипание окна к краям рабочей области экрана +namespace fuzzyDock { + +enum Edge { Left = 1, Right = 2, Top = 4, Bottom = 8 }; + +// позиция окна с притяжением к краям area, если до края не больше distance пикселей +QPoint snapped(const QRect &window, const QRect &area, int distance); + +// края area, к которым окно прижато (сочетание Edge) +int edges(const QRect &window, const QRect &area); + +// позиция, при которой окно остаётся прижатым к краям edges +QPoint docked(const QRect &window, const QRect &area, int edges); + +} + +#endif // FUZZY_DOCK_H diff --git a/tests/tests.pro b/tests/tests.pro new file mode 100644 index 0000000..28f4861 --- /dev/null +++ b/tests/tests.pro @@ -0,0 +1,18 @@ +# Тесты логики часов: фразы из fuzzy.conf и прилипание к краям экрана +QT = core testlib +CONFIG += console testcase +TARGET = fuzzy_clock_tests + +INCLUDEPATH += .. +DEFINES += SOURCE_DIR=\\\"$$PWD/..\\\" + +HEADERS += \ + ../fuzzy_clock.h \ + ../fuzzy_dock.h \ + ../fuzzy_helper.h + +SOURCES += \ + tst_fuzzy_clock.cpp \ + ../fuzzy_clock.cpp \ + ../fuzzy_dock.cpp \ + ../fuzzy_helper.cpp diff --git a/tests/tst_fuzzy_clock.cpp b/tests/tst_fuzzy_clock.cpp new file mode 100644 index 0000000..91aa920 --- /dev/null +++ b/tests/tst_fuzzy_clock.cpp @@ -0,0 +1,192 @@ +#include +#include +#include + +#include "fuzzy_clock.h" +#include "fuzzy_dock.h" +#include "fuzzy_helper.h" + +using Fuzziness = fuzzyClock::Fuzziness; + +class tst_fuzzyClock : public QObject +{ + Q_OBJECT + + fuzzyClock m_clock; + + static fuzzyClock load(const QString &dir) + { + const QString current = QDir::currentPath(); + QDir::setCurrent(dir); // fuzzy.conf читается из текущей директории + fuzzyClock clock; + fuzzyHelper::instance()->readArrays(clock); + QDir::setCurrent(current); + return clock; + } + +private slots: + void initTestCase() + { + m_clock = load(SOURCE_DIR); + QVERIFY(m_clock.isValid(Fuzziness::Minutes)); + QVERIFY(m_clock.isValid(Fuzziness::DayPart)); + QVERIFY(m_clock.isValid(Fuzziness::WeekPart)); + } + + // "Четверть пятого" + void minutes_data() + { + QTest::addColumn("time"); + QTest::addColumn("text"); + + QTest::newRow("полночь") << QTime(0, 0) << "Ровно двенадцать"; + QTest::newRow("час ночи") << QTime(1, 0) << "Ровно час"; + QTest::newRow("01:02") << QTime(1, 2) << "Ровно час"; + QTest::newRow("полдень") << QTime(12, 0) << "Ровно двенадцать"; + QTest::newRow("час дня") << QTime(13, 0) << "Ровно час"; + QTest::newRow("13:03") << QTime(13, 3) << "Пять минут второго"; + QTest::newRow("00:05") << QTime(0, 5) << "Пять минут первого"; + QTest::newRow("16:15") << QTime(16, 15) << "Четверть пятого"; + QTest::newRow("12:30") << QTime(12, 30) << "Половина первого"; + QTest::newRow("12:40") << QTime(12, 40) << "Без двадцати час"; + QTest::newRow("13:45") << QTime(13, 45) << "Без четверти два"; + QTest::newRow("11:57") << QTime(11, 57) << "Без пяти двенадцать"; + QTest::newRow("23:58") << QTime(23, 58) << "Почти двенадцать"; + } + + void minutes() + { + QFETCH(QTime, time); + QFETCH(QString, text); + QCOMPARE(m_clock.text(QDateTime(QDate(2026, 10, 12), time), Fuzziness::Minutes), text); + } + + // в каждую минуту суток - готовая фраза без незаменённых %0 и %1 + void minutesWholeDay() + { + for (int minute = 0; minute < 24 * 60; ++minute) { + const QTime time = QTime(0, 0).addSecs(minute * 60); + const QString text = m_clock.text(QDateTime(QDate(2026, 10, 12), time), Fuzziness::Minutes); + QVERIFY2(!text.isEmpty() && !text.contains('%'), qPrintable(time.toString("HH:mm") + " " + text)); + } + } + + // "Вечер" + void dayParts_data() + { + QTest::addColumn("hour"); + QTest::addColumn("text"); + + QTest::newRow("0") << 0 << "Ночь"; + QTest::newRow("4") << 4 << "Ночь"; + QTest::newRow("5") << 5 << "Раннее утро"; + QTest::newRow("8") << 8 << "Утро"; + QTest::newRow("11") << 11 << "Почти полдень"; + QTest::newRow("12") << 12 << "Полдень"; + QTest::newRow("13") << 13 << "День"; + QTest::newRow("18") << 18 << "Вечер"; + QTest::newRow("22") << 22 << "Поздний вечер"; + QTest::newRow("23") << 23 << "Поздний вечер"; + } + + void dayParts() + { + QFETCH(int, hour); + QFETCH(QString, text); + QCOMPARE(m_clock.text(QDateTime(QDate(2026, 10, 12), QTime(hour, 30)), Fuzziness::DayPart), text); + } + + // "Середина недели"; 12.10.2026 - понедельник + void weekParts_data() + { + QTest::addColumn("day"); + QTest::addColumn("text"); + + QTest::newRow("пн") << 0 << "Начало недели"; + QTest::newRow("вт") << 1 << "Середина недели"; + QTest::newRow("ср") << 2 << "Середина недели"; + QTest::newRow("чт") << 3 << "Середина недели"; + QTest::newRow("пт") << 4 << "Конец недели"; + QTest::newRow("сб") << 5 << "Выходные"; + QTest::newRow("вс") << 6 << "Выходные"; + } + + void weekParts() + { + QFETCH(int, day); + QFETCH(QString, text); + QCOMPARE(m_clock.text(QDateTime(QDate(2026, 10, 12).addDays(day), QTime(12, 0)), Fuzziness::WeekPart), text); + } + + void missingConfig() + { + QTemporaryDir empty; + const fuzzyClock clock = load(empty.path()); + QVERIFY(!clock.isValid(Fuzziness::Minutes)); + QCOMPARE(clock.text(QDateTime::currentDateTime(), Fuzziness::Minutes), QString("Ошибка в fuzzy.conf")); + } + + void brokenReference() + { + fuzzyClock clock = m_clock; + clock.vectMinuteRefer[5] = int(clock.vectMinutes.size()); // ссылка за пределы списка фраз + QVERIFY(!clock.isValid(Fuzziness::Minutes)); + + clock = m_clock; + clock.vectHourRefer.pop_back(); // 23 часа вместо 24 + QVERIFY(!clock.isValid(Fuzziness::DayPart)); + QVERIFY(clock.isValid(Fuzziness::Minutes)); + } + + // Прилипание к краям: рабочая область 0,0 - 1919,1039, окно 200x30 + void snapped_data() + { + QTest::addColumn("topLeft"); + QTest::addColumn("expected"); + + QTest::newRow("далеко от краёв") << QPoint(500, 500) << QPoint(500, 500); + QTest::newRow("к левому") << QPoint(10, 500) << QPoint(0, 500); + QTest::newRow("к левому снаружи") << QPoint(-12, 500) << QPoint(0, 500); + QTest::newRow("к правому") << QPoint(1705, 500) << QPoint(1720, 500); + QTest::newRow("к верхнему") << QPoint(500, 15) << QPoint(500, 0); + QTest::newRow("к нижнему") << QPoint(500, 1000) << QPoint(500, 1010); + QTest::newRow("в угол") << QPoint(1710, 1005) << QPoint(1720, 1010); + QTest::newRow("16 пикселей - мимо") << QPoint(16, 500) << QPoint(16, 500); + } + + void snapped() + { + QFETCH(QPoint, topLeft); + QFETCH(QPoint, expected); + const QRect area(0, 0, 1920, 1040); + QCOMPARE(fuzzyDock::snapped(QRect(topLeft, QSize(200, 30)), area, 15), expected); + } + + void edges() + { + const QRect area(0, 0, 1920, 1040); + QCOMPARE(fuzzyDock::edges(QRect(500, 500, 200, 30), area), 0); + QCOMPARE(fuzzyDock::edges(QRect(0, 500, 200, 30), area), int(fuzzyDock::Left)); + QCOMPARE(fuzzyDock::edges(QRect(1720, 1010, 200, 30), area), fuzzyDock::Right | fuzzyDock::Bottom); + QCOMPARE(fuzzyDock::edges(QRect(0, 0, 200, 30), area), fuzzyDock::Left | fuzzyDock::Top); + } + + // окно у правого края стало шире - правый край остаётся у края экрана + void dockedKeepsRightEdge() + { + const QRect area(0, 0, 1920, 1040); + const QRect wider(1720, 1010, 260, 30); + QCOMPARE(fuzzyDock::docked(wider, area, fuzzyDock::Right | fuzzyDock::Bottom), QPoint(1660, 1010)); + QCOMPARE(fuzzyDock::docked(wider, area, 0), wider.topLeft()); + } + + // панель задач слева: рабочая область начинается не с нуля + void dockedShiftedArea() + { + const QRect area(60, 0, 1860, 1080); + QCOMPARE(fuzzyDock::docked(QRect(0, 300, 200, 30), area, fuzzyDock::Left), QPoint(60, 300)); + } +}; + +QTEST_GUILESS_MAIN(tst_fuzzyClock) +#include "tst_fuzzy_clock.moc"