#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();
}
}