Pith - wedge_linux
wedge_linux/NoteWindow.cpp [21.8 kb]
Modified: 06:16:07 91 026 (18 Jun 026)
6 Days Ago
#include "NoteWindow.h"
#include "SettingsDialog.h"
#include "WedgeSecurity.h"
#include <QSizeGrip>
#include <QPalette>
#include <QDateTime>
#include <QResizeEvent>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QScrollBar>
#include <QRegularExpression>
#include <QTextCursor>
#include <QKeyEvent>
#include <QTextBlock>
#include <QCoreApplication>

NoteWindow::NoteWindow(QWidget *parent) : QWidget(parent) {
    setWindowFlags(Qt::FramelessWindowHint);
    setWindowTitle("wedge");

    networkManager = new NetworkManager(this);
    connect(networkManager, &NetworkManager::contentReceived, this, &NoteWindow::handleIncomingContent);

    setupUi();
    loadData();
    installEventFilter(this);
    
    resize(currentData.width, currentData.height);
    move(currentData.x, currentData.y);
    
    updateAppearance();
    isInitialized = true;
}

void NoteWindow::setupUi() {
    auto *layout = new QVBoxLayout(this);
    layout->setContentsMargins(8, 8, 0, 8); 
    layout->setSpacing(5);

    auto *toolbar = new QHBoxLayout();
    toolbar->setContentsMargins(0, 0, 8, 0); 
    
    QString baseStyle = "QPushButton { border: none; font-size: 18px; background: transparent; color: rgba(0,0,0,0.5); } ";

    topBtn = new QPushButton("\u25B3");
    topBtn->setStyleSheet(baseStyle + "QPushButton { font-weight: bold; padding-bottom: 4px; }");

    settingsBtn = new QPushButton("\u2630");
    settingsBtn->setStyleSheet(baseStyle);

    toolbar->addStretch();
    toolbar->addWidget(topBtn);
    toolbar->addSpacing(10);
    toolbar->addWidget(settingsBtn);
    layout->addLayout(toolbar);

    editor = new QTextEdit();
    editor->setFrameStyle(QFrame::NoFrame);
    editor->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
    editor->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    editor->setLineWrapMode(QTextEdit::WidgetWidth);

    editor->installEventFilter(this);
    highlighter = new WedgeHighlighter(editor->document(), currentData.focusColor, currentData.highlightColor);

    editor->verticalScrollBar()->setStyleSheet(
        "QScrollBar:vertical { border: none; background: transparent; width: 8px; }"
        "QScrollBar::handle:vertical { background: rgba(0, 0, 0, 0.4); min-height: 20px; border-radius: 4px; }"
        "QScrollBar::add-line, QScrollBar::sub-line { height: 0px; }"
    );

    layout->addWidget(editor);

    auto *grip = new QSizeGrip(this);
    layout->addWidget(grip, 0, Qt::AlignBottom | Qt::AlignRight);

    connect(topBtn, &QPushButton::clicked, [this]() {
        if (editor && editor->verticalScrollBar()) {
            editor->verticalScrollBar()->setValue(0);
        }
    });

    connect(settingsBtn, &QPushButton::clicked, this, &NoteWindow::openSettings);
    
    connect(editor, &QTextEdit::textChanged, [this]() {
        currentData.content = editor->toPlainText();
        currentData.lastModified = QDateTime::currentMSecsSinceEpoch();
        ConfigManager::saveNote(currentData);
    });
}

bool NoteWindow::eventFilter(QObject *obj, QEvent *event) {
    if (event->type() == QEvent::WindowActivate) {
        QMetaObject::invokeMethod(this, [this]() { refreshDates(); }, Qt::QueuedConnection);
    }
    
    if (obj == editor && event->type() == QEvent::KeyPress) {
        QKeyEvent *keyEvent = static_cast<QKeyEvent*>(event);
        int key = keyEvent->key();
        if (key == Qt::Key_Return || key == Qt::Key_Enter || key == Qt::Key_Space) {
            if (handleCalculation(key)) {
                return true;
            }
        }
    }
    return QWidget::eventFilter(obj, event);
}

QString NoteWindow::getOtcDateResult(int day, QString mon) {
    static QMap<QString, int> months = {
        {"jan", 1}, {"feb", 2}, {"mar", 3}, {"apr", 4}, {"may", 5}, {"jun", 6},
        {"jul", 7}, {"aug", 8}, {"sep", 9}, {"oct", 10}, {"nov", 11}, {"dec", 12}
    };
    int m = months.value(mon.toLower(), 0);
    if (m == 0) return "";
    QDate now = QDate::currentDate();
    QDate target(now.year(), m, day);
    if (target < now) target = target.addYears(1);
    QDate otcStart(target.year(), 3, 20);
    if (target < otcStart) otcStart = otcStart.addYears(-1);
    return QString("(%1)=%2").arg(otcStart.daysTo(target) + 1).arg(now.daysTo(target));
}

void WedgeHighlighter::highlightBlock(const QString &text) {

    // next time high light
    
    int state = previousBlockState();
    if (state == -1) {
        state = 0;
    }

    QDateTime now = QDateTime::currentDateTime();
    int dayOfMonth = now.date().day();
    QString dayOfWeek = now.toString("ddd");
    QString month = now.toString("MMM");
    
    QDate currentDate = now.date();
    QDate otcStart(currentDate.year(), 3, 20);
    if (currentDate < otcStart) {
        otcStart = otcStart.addYears(-1);
    }
    qint64 otcDay = otcStart.daysTo(currentDate) + 1;

    QString headerPattern = QString("(?i)^%1\\s+%2\\s+0?%3\\s+%4.*")
                            .arg(otcDay)
                            .arg(QRegularExpression::escape(dayOfWeek))
                            .arg(dayOfMonth)
                            .arg(QRegularExpression::escape(month));
    
    QRegularExpression headerRegex(headerPattern);
    QString trimmedText = text.trimmed();

    if (headerRegex.match(text).hasMatch()) {
        state = 1; 
    } else if (trimmedText.isEmpty()) {
        state = 0; 
    } else if (state == 1 || state == 2) {
        static QRegularExpression timeRegex("^\\s*(\\d{2}):(\\d{2})");
        QRegularExpressionMatch timeMatch = timeRegex.match(text);

        if (timeMatch.hasMatch()) {
            int h = timeMatch.captured(1).toInt();
            int m = timeMatch.captured(2).toInt();
            int currentMinutes = now.time().hour() * 60 + now.time().minute();

            if ((h * 60 + m) > currentMinutes) {
                QTextCharFormat highlightFormat;
                highlightFormat.setBackground(m_highlightColor);
                setFormat(0, text.length(), highlightFormat);
                
                state = 3; 
            } else {
                state = 2; 
            }
        }
    } else if (state == 3) {
        state = 3;
    }

    // bold regex

    static QRegularExpression boldRegex("^.*!$");
    if (boldRegex.match(text).hasMatch()) {
        QTextCharFormat format;
        format.setFontWeight(QFont::Bold);
        format.setForeground(m_focusColor);
        setFormat(0, text.length(), format);
    }
    
    // italic regex

    static QRegularExpression italicRegex("^.*\\?$");
    if (italicRegex.match(text).hasMatch()) {
        QTextCharFormat format;
        format.setFontItalic(true);
        setFormat(0, text.length(), format);
    }

    // under line regex

    static QRegularExpression underlineRegex("^\\s*([^\\s].*\\*)$");
    QRegularExpressionMatch uMatch = underlineRegex.match(text);
    if (uMatch.hasMatch()) {
        QTextCharFormat uFormat;
        uFormat.setFontUnderline(true);
        setFormat(uMatch.capturedStart(1), uMatch.capturedLength(1), uFormat);
    }

    // strike thru regex

    static QRegularExpression strikethruRegex("^\\s*([^\\s].*~)$");
    QRegularExpressionMatch sMatch = strikethruRegex.match(text);
    if (sMatch.hasMatch()) {
        QTextCharFormat sFormat;
        sFormat.setFontStrikeOut(true);
        setFormat(sMatch.capturedStart(1), sMatch.capturedLength(1), sFormat);
    }

    setCurrentBlockState(state);
}

QString NoteWindow::refreshAlarms(const QString &content) {

    // alarm count down

    QDateTime now = QDateTime::currentDateTime();
    int currentMinutes = now.time().hour() * 60 + now.time().minute();

    QDate currentDate = now.date();
    QDate otcStart(currentDate.year(), 3, 20);
    if (currentDate < otcStart) otcStart = otcStart.addYears(-1);
    qint64 todayOtcDay = otcStart.daysTo(currentDate) + 1;

    QStringList lines = content.split('\n');
    qint64 currentHeaderOtcDay = todayOtcDay;
    bool hasHeader = false;

    QRegularExpression headerRegex("^(\\d+)\\s+([a-zA-Z]{3})\\s+(\\d{1,2})\\s+([a-zA-Z]{3})", QRegularExpression::CaseInsensitiveOption);
    QRegularExpression alarmRegex("^(\\d{2}):(\\d{2})\\s+(.*?)\\s*\\((-\\d+(?:\\.\\d+)?|0)?\\)$");

    QStringList updatedLines;
    for (const QString &line : lines) {
        QString trimmed = line.trimmed();
        auto headerMatch = headerRegex.match(trimmed);
        if (headerMatch.hasMatch()) {
            currentHeaderOtcDay = headerMatch.captured(1).toLongLong();
            hasHeader = true;
            updatedLines.append(line);
            continue;
        }

        auto alarmMatch = alarmRegex.match(trimmed);
        if (alarmMatch.hasMatch()) {
            int h = alarmMatch.captured(1).toInt();
            int m = alarmMatch.captured(2).toInt();
            QString desc = alarmMatch.captured(3);

            int alarmMinutes = h * 60 + m;
            qint64 headerDay = hasHeader ? currentHeaderOtcDay : todayOtcDay;

            QString valueInside;
            if (headerDay < todayOtcDay) {
                valueInside = "0";
            } else if (headerDay == todayOtcDay) {
                int diffMinutes = alarmMinutes - currentMinutes;
                if (diffMinutes <= 0) {
                    valueInside = "0";
                } else {
                    double hours = qMax(0.1, diffMinutes / 60.0);
                    valueInside = QString("-%1").arg(hours, 0, 'f', 1);
                }
            } else {
                qint64 diffDays = headerDay - todayOtcDay;
                int diffMinutes = (diffDays * 24 * 60) + alarmMinutes - currentMinutes;
                double hours = qMax(0.1, diffMinutes / 60.0);
                valueInside = QString("-%1").arg(hours, 0, 'f', 1);
            }

            int leadingSpacesCount = 0;
            while (leadingSpacesCount < line.length() && line[leadingSpacesCount].isSpace()) {
                leadingSpacesCount++;
            }
            QString leadingSpaces = line.left(leadingSpacesCount);

            updatedLines.append(QString("%1%2:%3  %4(%5)")
                .arg(leadingSpaces)
                .arg(h, 2, 10, QChar('0'))
                .arg(m, 2, 10, QChar('0'))
                .arg(desc)
                .arg(valueInside));
        } else {
            updatedLines.append(line);
        }
    }
    return updatedLines.join('\n');
}

void NoteWindow::refreshDates() {

    // days counter

    if (!editor) return;
    QString content = editor->toPlainText();
    QString newContent = content;
    
    static QRegularExpression scanRegex("(?<=\\s|^)(\\d{1,2})([a-zA-Z]{3})\\(\\d*\\)=\\d*");
    QRegularExpressionMatchIterator i = scanRegex.globalMatch(content);
    int offset = 0;
    while (i.hasNext()) {
        QRegularExpressionMatch match = i.next();
        QString res = getOtcDateResult(match.captured(1).toInt(), match.captured(2));
        if (!res.isEmpty()) {
            QString replacement = match.captured(1) + match.captured(2) + res;
            newContent.replace(match.capturedStart() + offset, match.capturedLength(), replacement);
            offset += (replacement.length() - match.capturedLength());
        }
    }

    newContent = refreshAlarms(newContent);

    if (newContent != content) {
        editor->blockSignals(true);
        
        QTextCursor userCursor = editor->textCursor();
        int oldPos = userCursor.position();

        QTextCursor writeCursor(editor->document());
        writeCursor.beginEditBlock();
        writeCursor.select(QTextCursor::Document);
        writeCursor.insertText(newContent);
        writeCursor.endEditBlock();
        
        userCursor.setPosition(qMin(oldPos, newContent.length()));
        editor->setTextCursor(userCursor);
        
        editor->blockSignals(false);
        
        currentData.content = newContent;
        ConfigManager::saveNote(currentData);
    }
}

bool NoteWindow::handleCalculation(int key) {

    // in-line calculations

    QTextCursor cursor = editor->textCursor();
    QString lineText = cursor.block().text().left(cursor.positionInBlock());

    static QRegularExpression mathRegex("(?<=\\s|^)((\\d+(\\.\\d+)?%?[\\+\\-\\*\\/])+\\d+(\\.\\d+)?%?)=$");
    static QRegularExpression dateRegex("(?<=\\s|^)(\\d{1,2})([a-zA-Z]{3})\\(\\)=$");
    static QRegularExpression listRegex("^(\\s*)(\\d+)\\.\\s");
    static QRegularExpression hrRegex("(?<=\\s|^)hr\\((\\d+)\\)$");
    static QRegularExpression dashRegex("(?<=\\s|^)dash\\((\\d+)\\)$");

    auto mMatch = mathRegex.match(lineText);
    if (mMatch.hasMatch()) {
        QString expression = mMatch.captured(1);

        static QRegularExpression percentRegex("([\\d\\.]+)\\s*([\\+\\-\\*\\/])\\s*([\\d\\.]+)%");
        
        while (expression.contains('%')) {
            QRegularExpressionMatch pMatch = percentRegex.match(expression);
            if (!pMatch.hasMatch()) break;

            QString base = pMatch.captured(1);
            QString op = pMatch.captured(2);
            QString percentVal = pMatch.captured(3);

            QString replacement = QString("%1 %2 (%1 * (%3 / 100))").arg(base, op, percentVal);
            expression.replace(pMatch.capturedStart(), pMatch.capturedLength(), replacement);
        }

        QJSValue result = mathEngine.evaluate(expression);
        if (!result.isError()) {
            QString resStr = QString::number(result.toNumber(), 'f', 2);
            if (resStr.contains('.')) {
                while (resStr.endsWith('0')) resStr.chop(1);
                if (resStr.endsWith('.')) resStr.chop(1);
            }
            editor->blockSignals(true); 
            cursor.insertText(resStr); 
            editor->blockSignals(false);
            currentData.content = editor->toPlainText(); 
            ConfigManager::saveNote(currentData);
        }
        return (key == Qt::Key_Space);
    }

    auto dMatch = dateRegex.match(lineText);
    if (dMatch.hasMatch()) {
        QString res = getOtcDateResult(dMatch.captured(1).toInt(), dMatch.captured(2));
        if (!res.isEmpty()) {
            editor->blockSignals(true); cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, 3);
            cursor.insertText(res); editor->blockSignals(false);
            currentData.content = editor->toPlainText(); ConfigManager::saveNote(currentData);
        }
        return (key == Qt::Key_Space);
    }

    auto hMatch = hrRegex.match(lineText);
    if (hMatch.hasMatch()) {
        int count = hMatch.captured(1).toInt();
        if (count > 0) {
            QString hrline = QString("\u2014").repeated(qMin(count, 100));
            editor->blockSignals(true);
            cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, hMatch.capturedLength());
            cursor.insertText(hrline);
            editor->blockSignals(false);
            currentData.content = editor->toPlainText(); ConfigManager::saveNote(currentData);
        }
        return (key == Qt::Key_Space);
    }

    auto emMatch = dashRegex.match(lineText);
    if (emMatch.hasMatch()) {
        int counts = emMatch.captured(1).toInt();
        if (counts > 0) {
            QString dashes = QString("\u2014 ").repeated(qMin(counts, 100));
            editor->blockSignals(true);
            cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, emMatch.capturedLength());
            cursor.insertText(dashes);
            editor->blockSignals(false);
            currentData.content = editor->toPlainText(); ConfigManager::saveNote(currentData);
        }
        return (key == Qt::Key_Space);
    }

    if (key != Qt::Key_Space) {
        auto lMatch = listRegex.match(lineText);
        if (lMatch.hasMatch()) {
            QString ws = lMatch.captured(1);
            int num = lMatch.captured(2).toInt();
            QString prefix = "\n" + ws + QString::number(num + 1) + ". ";
            editor->blockSignals(true);
            cursor.beginEditBlock();
            cursor.insertText(prefix);
            QTextBlock nextBlock = cursor.block().next();
            int nextExpected = num + 1;
            int i = 0;
            while (nextBlock.isValid()) {
                auto m = listRegex.match(nextBlock.text());
                if (m.hasMatch() && m.captured(1) == ws && m.captured(2).toInt() == nextExpected) {
                    int foundNum = m.captured(2).toInt();
                    QTextCursor nextCursor(nextBlock);
                    nextCursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, m.capturedLength());
                    nextCursor.insertText(m.captured(1) + QString::number(foundNum + 1) + ". ");
                    nextExpected++;
                } else if (i > 0) break;
                nextBlock = nextBlock.next(); i++;
            }
            cursor.endEditBlock(); editor->blockSignals(false);
            currentData.content = editor->toPlainText(); ConfigManager::saveNote(currentData);
            return true;
        }
        static QRegularExpression indentRegex("^(\\s+)");
        auto iMatch = indentRegex.match(lineText);
        if (iMatch.hasMatch()) {
            QString indent = "\n" + iMatch.captured(1);
            editor->blockSignals(true);
            cursor.insertText(indent);
            editor->blockSignals(false);
            currentData.content = editor->toPlainText(); ConfigManager::saveNote(currentData);
            return true;
        }
    }
    return false;
}

void NoteWindow::updateAppearance() {
    Qt::WindowFlags flags = Qt::FramelessWindowHint;
    if (currentData.hideTaskbar) {
        flags |= Qt::Tool;
    } else {
        flags |= Qt::Window;
    }
    
    if (windowFlags() != flags) {
        setWindowFlags(flags);
        setAttribute(Qt::WA_QuitOnClose, true);
        show(); 
    }

    QPalette pal = palette();
    pal.setColor(QPalette::Window, currentData.bgColor);
    setPalette(pal);
    setAutoFillBackground(true);

    if (editor) {
        QPalette editorPal = editor->palette();
        editorPal.setColor(QPalette::Text, currentData.textColor);
        editorPal.setColor(QPalette::Base, currentData.bgColor);
        editor->setPalette(editorPal);
        editor->setAutoFillBackground(true);
    }

    highlighter->setColors(currentData.focusColor, currentData.highlightColor);
    
    editor->setStyleSheet(QString(
        "QTextEdit { background-color: %1; color: %2; font-family: '%3'; font-size: %4pt; border: none; }"
    ).arg(currentData.bgColor.name())
     .arg(currentData.textColor.name())
     .arg(currentData.fontFamily)
     .arg(currentData.fontSize));
}

void NoteWindow::closeEvent(QCloseEvent *event) {
    QCoreApplication::quit();
    event->accept();
}

void NoteWindow::resizeEvent(QResizeEvent *event) {
    if (isInitialized) {
        currentData.width = event->size().width();
        currentData.height = event->size().height();
        ConfigManager::saveNote(currentData);
    }
    QWidget::resizeEvent(event);
}

void NoteWindow::loadData() {
    currentData = ConfigManager::loadNote();
    if (editor) {
        editor->blockSignals(true);
        editor->setPlainText(currentData.content);
        editor->blockSignals(false);
        refreshDates();
    }
}

void NoteWindow::openSettings() {
    SettingsDialog dialog(currentData, this);

    connect(networkManager, &NetworkManager::statusChanged, dialog.netStatusValLabel, &QLabel::setText);

    connect(networkManager, &NetworkManager::statusChanged, &dialog, [&dialog](const QString &status, bool isConnected) {
        dialog.connectBtn->setText(isConnected ? "Disconnect" : "Connect");
    });

    connect(&dialog, &SettingsDialog::connectRequested, this, [this](int routeIndex, int roleIndex) {
        if (networkManager->isConnected()) {
            networkManager->disconnectFromPeer();
        } else {
            NetworkManager::Route route = static_cast<NetworkManager::Route>(routeIndex);
            NetworkManager::Role role = static_cast<NetworkManager::Role>(roleIndex);
            networkManager->startConnection(route, role);
        }
    });

    connect(&dialog, &SettingsDialog::syncRequested, this, [this]() {
        if (networkManager->isConnected()) {
            networkManager->sendNoteContent(editor->toPlainText(), currentData.passKey, currentData.lastModified);
        }
    });

    if (networkManager->isConnected()) {
        dialog.netStatusValLabel->setText("Connected");
        dialog.connectBtn->setText("Disconnect");
    }

    connect(&dialog, &SettingsDialog::encryptRequested, this, [this](const QString &phrase) {
        QString plain = editor->toPlainText();
        QString cipher = WedgeSecurity::encrypt(plain, phrase);
        if (!cipher.isEmpty()) {
            editor->setPlainText(cipher);
            currentData.content = cipher;
            ConfigManager::saveNote(currentData);
        }
    });

    connect(&dialog, &SettingsDialog::decryptRequested, this, [this](const QString &phrase) {
        QString cipher = editor->toPlainText();
        if (WedgeSecurity::isEncrypted(cipher)) {
            QString plain = WedgeSecurity::decrypt(cipher, phrase);
            if (!plain.isEmpty()) {
                editor->setPlainText(plain);
                currentData.content = plain;
                ConfigManager::saveNote(currentData);
            }
        }
    });

    if (dialog.exec() == QDialog::Accepted) {
        currentData = dialog.getUpdatedData();
        updateAppearance();
        ConfigManager::saveNote(currentData);
    }
}

void NoteWindow::mousePressEvent(QMouseEvent *event) {
    if (event->button() == Qt::LeftButton) {
        dragPosition = event->globalPosition().toPoint() - frameGeometry().topLeft();
        event->accept();
    }
}

void NoteWindow::handleIncomingContent(const QString &content) {
    editor->setPlainText(content);
    currentData.content = content;
    ConfigManager::saveNote(currentData);
}

void NoteWindow::mouseMoveEvent(QMouseEvent *event) {
    if (event->buttons() & Qt::LeftButton) {
        QPoint newPos = event->globalPosition().toPoint() - dragPosition;
        move(newPos);
        if (isInitialized) {
            currentData.x = newPos.x();
            currentData.y = newPos.y();
            ConfigManager::saveNote(currentData);
        }
        event->accept();
    }
}
Updates
OTC Applet - Linux 93.026.1
Wedge - Linux 90.026.1
Wedge - Android 90.026.1
Shim - Android 86.026.1
Kerf - Android 86.026.4
Dev
TVShow (227) 'CSA'
TVShow (228) 'APT'
TVProgram (83) 'BXT'
Miter Update(s)
Peen (Messaging)

Menu
Calendar
Project Tin (024/029)
Miter
RSS Feed
User Avatar
@vgmlr
=SUM(parts)