Pith - wedge_linux
wedge_linux/WedgeNote.cpp [22.8 kb]
Modified: 22:03:58 118 026 (15 Jul 026)
10 Days Ago
#include "WedgeCommands.h"
#include "WedgeData.h"
#include "WedgeHeader.h"
#include "WedgeNote.h"
#include "WedgeSecurity.h"
#include "WedgeSettings.h"
#include <algorithm>
#include <QContextMenuEvent>
#include <QCoreApplication>
#include <QDateTime>
#include <QDialog>
#include <QGraphicsOpacityEffect>
#include <QHBoxLayout>
#include <QKeyEvent>
#include <QLabel>
#include <QMenu>
#include <QPalette>
#include <QPushButton>
#include <QRegularExpression>
#include <QResizeEvent>
#include <QScrollBar>
#include <QSizeGrip>
#include <QTextBlock>
#include <QTextCursor>
#include <QTimer>
#include <QVBoxLayout>

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

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

    auto *backupTimer = new QTimer(this);
    connect(backupTimer, &QTimer::timeout, this, []() { ConfigManager::backupNote(); });
    ConfigManager::backupNote();
    backupTimer->start(3600000); // 1 hour

    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 = QString("QPushButton { border: none; font-size: 18px; background: transparent; color: %1; } ")
                            .arg(currentData.textColor.name());

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

    dataBtn = new QPushButton("\u2688");
    dataBtn->setStyleSheet(baseStyle + "QPushButton { padding-bottom: 1px; }");

    settingsBtn = new QPushButton("\u25FC");
    settingsBtn->setStyleSheet(baseStyle + "QPushButton { padding-bottom: 5px; }");

    for (QPushButton *b : {topBtn, dataBtn, settingsBtn}) {
        auto *fx = new QGraphicsOpacityEffect(b);
        fx->setOpacity(0.5);
        b->setGraphicsEffect(fx);
    }

    toolbar->addStretch();
    toolbar->addWidget(topBtn);
    toolbar->addSpacing(10);
    toolbar->addWidget(dataBtn);
    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);
    editor->viewport()->installEventFilter(this);
    highlighter = new WedgeHighlighter(editor->document(), currentData.focusColor, currentData.highlightColor, currentData.textColor, currentData.headerFormat);

    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(dataBtn, &QPushButton::clicked, this, &NoteWindow::openDataWindow);
    
    connect(editor, &QTextEdit::textChanged, [this]() {
        QString newContent = editor->toPlainText();
        bool contentChanged = (newContent != currentData.content);
        currentData.content = newContent;
        if (isInitialized && contentChanged) {
            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->viewport() && event->type() == QEvent::ContextMenu) {
        auto *ce = static_cast<QContextMenuEvent*>(event);
        QMenu *menu = editor->createStandardContextMenu();
        menu->addSeparator();
        QAction *dayAct = menu->addAction("Add Next Header");
        connect(dayAct, &QAction::triggered, this, &NoteWindow::insertNextDayHeader);
        QAction *sortAct = menu->addAction("Sort (A-Z)");
        sortAct->setEnabled(editor->textCursor().hasSelection());
        connect(sortAct, &QAction::triggered, this, &NoteWindow::sortSelection);
        menu->exec(ce->globalPos());
        delete menu;
        return true;
    }
    
    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::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 = WedgeHeader::matchRegex(currentData.headerFormat);
    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("otc").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 = WedgeCommands::otcDateResult(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) return;

    int prefix = 0;
    int minLen = qMin(content.length(), newContent.length());
    while (prefix < minLen && content.at(prefix) == newContent.at(prefix))
        ++prefix;

    int suffix = 0;
    while (suffix < (minLen - prefix) &&
           content.at(content.length() - 1 - suffix) ==
           newContent.at(newContent.length() - 1 - suffix))
        ++suffix;

    const int oldChangedEnd = content.length() - suffix;
    const int newChangedEnd = newContent.length() - suffix;
    const int delta = newContent.length() - content.length();

    editor->blockSignals(true);

    QScrollBar *vbar = editor->verticalScrollBar();
    int scrollValue = vbar ? vbar->value() : 0;

    QTextCursor userCursor = editor->textCursor();
    int oldPos = userCursor.position();
    int newPos;
    if (oldPos <= prefix)
        newPos = oldPos;
    else if (oldPos >= oldChangedEnd)
        newPos = oldPos + delta;
    else
        newPos = qMin(oldPos, newChangedEnd);

    QTextCursor writeCursor(editor->document());
    writeCursor.beginEditBlock();
    writeCursor.setPosition(prefix);
    writeCursor.setPosition(oldChangedEnd, QTextCursor::KeepAnchor);
    writeCursor.insertText(newContent.mid(prefix, newChangedEnd - prefix));
    writeCursor.endEditBlock();

    userCursor.setPosition(qBound(0, newPos, newContent.length()));
    editor->setTextCursor(userCursor);

    if (vbar) vbar->setValue(scrollValue);

    editor->blockSignals(false);

    currentData.content = newContent;
    ConfigManager::saveNote(currentData);
}

void NoteWindow::insertNextDayHeader() {

    // next day header insert

    if (!editor) return;

    const QString content = editor->toPlainText();
    const QString fmt = currentData.headerFormat;
    QRegularExpression hRe = WedgeHeader::matchRegex(fmt);

    struct H { int start; qint64 otc; };
    QList<H> headers;
    QRegularExpressionMatchIterator it = hRe.globalMatch(content);
    while (it.hasNext()) {
        QRegularExpressionMatch m = it.next();
        headers.append({ static_cast<int>(m.capturedStart()), m.captured("otc").toLongLong() });
    }

    QString newContent;

    if (headers.isEmpty()) {
        newContent = WedgeHeader::render(fmt, QDate::currentDate()) + "\n\n" + content;
    } else {
        qint64 maxOtc = headers.first().otc;
        for (const H &h : headers) maxOtc = qMax(maxOtc, h.otc);

        const qint64 target = maxOtc + 1;
        const QString header = WedgeHeader::render(fmt, WedgeHeader::dateFromOtcDay(target));
        const bool descending = (headers.size() >= 2) && (headers.first().otc >= headers.last().otc);

        int insertAt = -1;
        for (const H &h : headers) {
            if (descending ? (h.otc < target) : (h.otc > target)) { insertAt = h.start; break; }
        }

        if (insertAt >= 0) {
            newContent = content.left(insertAt) + header + "\n\n" + content.mid(insertAt);
        } else {
            int i = headers.last().start;
            while (i < content.length() && content[i] != '\n') ++i;
            if (i < content.length()) ++i;
            while (i < content.length()) {
                int end = i;
                while (end < content.length() && content[end] != '\n') ++end;
                const QString line = content.mid(i, end - i);
                if (line.trimmed().isEmpty() || hRe.match(line).hasMatch()) break;
                i = end;
                if (i < content.length()) ++i;
            }
            QString prefix = content.left(i);
            QString suffix = content.mid(i);
            while (!suffix.isEmpty() && suffix.at(0).isSpace()) suffix.remove(0, 1);
            const QString spacing = prefix.endsWith("\n\n") ? "" : (prefix.endsWith("\n") ? "\n" : "\n\n");
            newContent = prefix + spacing + header + "\n\n" + suffix;
        }
    }

    editor->blockSignals(true);
    editor->setPlainText(newContent);
    editor->blockSignals(false);

    currentData.content = newContent;
    currentData.lastModified = QDateTime::currentMSecsSinceEpoch();
    ConfigManager::saveNote(currentData);
    // highlighter->rehighlight();
}

void NoteWindow::sortSelection() {
    if (!editor) return;
    QTextCursor cursor = editor->textCursor();
    if (!cursor.hasSelection()) return;

    const QString txt = editor->toPlainText();
    const int selMin = cursor.selectionStart();
    const int selMax = cursor.selectionEnd();

    int start = txt.lastIndexOf('\n', selMin - 1);
    start = (start == -1) ? 0 : start + 1;
    int end = txt.indexOf('\n', selMax);
    if (end == -1) end = txt.length();

    const QStringList lines = txt.mid(start, end - start).split('\n');

    auto indentLen = [](const QString &s) {
        int n = 0;
        while (n < s.length() && s[n].isSpace()) ++n;
        return n;
    };

    int baseIndent = 0;
    for (const QString &l : lines) {
        if (!l.trimmed().isEmpty()) { baseIndent = indentLen(l); break; }
    }

    QList<QStringList> groups;
    for (const QString &line : lines) {
        if (groups.isEmpty() ||
            (!line.trimmed().isEmpty() && indentLen(line) <= baseIndent)) {
            groups.append(QStringList{line});
        } else {
            groups.last().append(line);
        }
    }

    std::sort(groups.begin(), groups.end(),
              [](const QStringList &a, const QStringList &b) {
                  return QString::compare(a.first(), b.first(),
                                          Qt::CaseInsensitive) < 0;
              });

    QStringList flat;
    for (const QStringList &g : groups) flat += g;
    const QString sorted = flat.join('\n');

    editor->blockSignals(true);
    QTextCursor edit(editor->document());
    edit.beginEditBlock();
    edit.setPosition(start);
    edit.setPosition(end, QTextCursor::KeepAnchor);
    edit.insertText(sorted);
    edit.endEditBlock();

    QTextCursor sel = editor->textCursor();
    sel.setPosition(start);
    sel.setPosition(start + sorted.length(), QTextCursor::KeepAnchor);
    editor->setTextCursor(sel);
    editor->blockSignals(false);

    currentData.content = editor->toPlainText();
    currentData.lastModified = QDateTime::currentMSecsSinceEpoch();
    ConfigManager::saveNote(currentData);
}

bool NoteWindow::handleCalculation(int key) {
    WedgeCommands::Result r = WedgeCommands::apply(editor, mathEngine, key);
    if (r.changed) {
        currentData.content = editor->toPlainText();
        ConfigManager::saveNote(currentData);
    }
    return r.consumed;
}

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, currentData.textColor);
    highlighter->setHeaderFormat(currentData.headerFormat);

    QString btnBase = QString("QPushButton { border: none; font-size: 18px; background: transparent; color: %1; } ")
                          .arg(currentData.textColor.name());
    topBtn->setStyleSheet(btnBase + "QPushButton { padding-bottom: 4px; }");
    dataBtn->setStyleSheet(btnBase + "QPushButton { padding-bottom: 1px; }");
    settingsBtn->setStyleSheet(btnBase + "QPushButton { padding-bottom: 5px; }");
    
    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::openDataWindow() {
    DataWindow 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, &DataWindow::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, &DataWindow::syncRequested, this, [this]() {
        if (!networkManager->isConnected()) return;

        if (currentData.lastModified < networkManager->peerLastModified()) {
            QDialog dlg(this);
            dlg.setWindowTitle("");

            QVBoxLayout *v = new QVBoxLayout(&dlg);

            QLabel *msg = new QLabel("Local data is older than destination.", &dlg);
            msg->setAlignment(Qt::AlignCenter);
            v->addWidget(msg, 0, Qt::AlignCenter);

            v->addSpacing(7);

            QHBoxLayout *btnRow = new QHBoxLayout();
            QPushButton *cancelBtn = new QPushButton("Cancel", &dlg);
            QPushButton *approveBtn = new QPushButton("Approve", &dlg);
            btnRow->addStretch();
            btnRow->addWidget(cancelBtn);
            btnRow->addWidget(approveBtn);
            btnRow->addStretch();
            v->addLayout(btnRow);

            connect(approveBtn, &QPushButton::clicked, &dlg, &QDialog::accept);
            connect(cancelBtn, &QPushButton::clicked, &dlg, &QDialog::reject);

            if (dlg.exec() != QDialog::Accepted) return;
        }

        networkManager->sendNoteContent(editor->toPlainText(), currentData.passKey, currentData.lastModified);
        currentData.lastSynced = QDateTime::currentMSecsSinceEpoch();
        ConfigManager::saveNote(currentData);
    });

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

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

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

    if (dialog.exec() == QDialog::Accepted) {
        NoteData updated = dialog.getUpdatedData();
        currentData.passKey = updated.passKey;
        currentData.ipAddress = updated.ipAddress;
        currentData.networkRole = updated.networkRole;
        currentData.networkRoute = updated.networkRoute;
        currentData.headerFormat = updated.headerFormat;
        highlighter->setHeaderFormat(currentData.headerFormat);
        ConfigManager::saveNote(currentData);
    }
}

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

    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->blockSignals(true);
    editor->setPlainText(content);
    editor->blockSignals(false);
    currentData.content = content;
    currentData.lastModified = QDateTime::currentMSecsSinceEpoch();
    currentData.lastSynced = QDateTime::currentMSecsSinceEpoch();
    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
Wedge - Android 126.026
Wedge - Linux 124.026
Shim - Android 117.026
Miter - 114.026
Kerf - Android 112.026
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)