#include "WedgeCommands.h"
#include "WedgeCrypto.h"
#include "WedgeHeader.h"
#include "WedgeNote.h"
#include "WedgeSecurity.h"
#include "WedgeSettings.h"
#include "WedgeSync.h"
#include <algorithm>
#include <QContextMenuEvent>
#include <QCoreApplication>
#include <QDateTime>
#include <QDesktopServices>
#include <QDialog>
#include <QGraphicsOpacityEffect>
#include <QGuiApplication>
#include <QHBoxLayout>
#include <QIcon>
#include <QKeyEvent>
#include <QKeySequence>
#include <QLabel>
#include <QMenu>
#include <QPainter>
#include <QPalette>
#include <QPixmap>
#include <QProcess>
#include <QPushButton>
#include <QRegularExpression>
#include <QResizeEvent>
#include <QScrollBar>
#include <QSizeGrip>
#include <QSvgRenderer>
#include <QTextBlock>
#include <QTextCursor>
#include <QTextDocument>
#include <QTimer>
#include <QUrl>
#include <QVBoxLayout>
static constexpr int kIconPx = 18;
static QIcon tintedIcon(const QString &path, const QColor &color) {
QSvgRenderer renderer(path);
if (!renderer.isValid()) return QIcon();
const qreal dpr = qGuiApp ? qGuiApp->devicePixelRatio() : 1.0;
QPixmap pm(QSize(kIconPx, kIconPx) * dpr);
pm.setDevicePixelRatio(dpr);
pm.fill(Qt::transparent);
QPainter p(&pm);
p.setRenderHint(QPainter::Antialiasing, true);
renderer.render(&p, QRectF(0, 0, kIconPx, kIconPx));
p.setCompositionMode(QPainter::CompositionMode_SourceIn);
p.fillRect(QRectF(0, 0, kIconPx, kIconPx), color);
p.end();
return QIcon(pm);
}
static void blockSpan(const QTextCursor &c, QTextBlock &first, QTextBlock &last) {
const QTextDocument *doc = c.document();
first = doc->findBlock(c.selectionStart());
last = doc->findBlock(c.selectionEnd());
if (c.hasSelection() && last != first && c.selectionEnd() == last.position())
last = last.previous();
}
// re-number list
static void relist(const QTextBlock &b) {
static const QRegularExpression re("^(\\s*)(\\d+)\\.\\s");
const QRegularExpressionMatch head = re.match(b.text());
if (!head.hasMatch()) return;
const QString ws = head.captured(1);
auto item = [&ws](const QTextBlock &x) {
const QRegularExpressionMatch m = re.match(x.text());
return (m.hasMatch() && m.captured(1) == ws) ? m : QRegularExpressionMatch();
};
QTextBlock a = b;
while (item(a.previous()).hasMatch()) a = a.previous();
int base = item(a).captured(2).toInt();
QTextBlock z = a;
for (QTextBlock x = a.next(); item(x).hasMatch(); x = x.next()) {
base = qMin(base, item(x).captured(2).toInt());
z = x;
}
int n = base;
for (QTextBlock x = a;; x = x.next()) {
const QRegularExpressionMatch m = item(x);
const QString num = QString::number(n++);
if (m.captured(2) != num) {
QTextCursor c(x);
c.setPosition(x.position() + m.capturedStart(2));
c.setPosition(x.position() + m.capturedEnd(2), QTextCursor::KeepAnchor);
c.insertText(num);
}
if (x == z) break;
}
}
static QString urlAt(const QTextCursor &c) {
static const QRegularExpression urlRegex(
"(?:https?://|www\\.)[^\\s<>\"']*[^\\s<>\"'.,;:!?*~)\\]]");
const int pos = c.positionInBlock();
QRegularExpressionMatchIterator it = urlRegex.globalMatch(c.block().text());
while (it.hasNext()) {
const QRegularExpressionMatch m = it.next();
if (pos >= m.capturedStart() && pos <= m.capturedEnd())
return m.captured();
}
return QString();
}
static bool headerDateOf(const QString &line, const QString &fmt,
const QRegularExpression &loose, QDate &out) {
const QRegularExpressionMatch m = loose.match(line);
if (!m.hasMatch()) return false;
bool ok = false;
const qint64 otc = m.captured("otc").toLongLong(&ok);
if (!ok || otc < 1 || otc > 366) return false;
const QDate base = WedgeHeader::otcStartFor(QDate::currentDate());
for (int dy : {0, -1, 1}) {
const QDate d = base.addYears(dy).addDays(otc - 1);
if (WedgeHeader::dateRegex(fmt, d).match(line).hasMatch()) {
out = d;
return true;
}
}
return false;
}
NoteWindow::NoteWindow(QWidget *parent) : QWidget(parent) {
setWindowFlags(Qt::FramelessWindowHint);
setWindowTitle("wedge");
networkManager = new NetworkManager(this);
connect(networkManager, &NetworkManager::contentReceived, this, &NoteWindow::handleIncomingContent);
networkManager->startListener();
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);
topBtn = new QPushButton();
cryptoBtn = new QPushButton();
syncBtn = new QPushButton();
settingsBtn = new QPushButton();
closeBtn = new QPushButton();
for (QPushButton *b : {topBtn, cryptoBtn, syncBtn, settingsBtn, closeBtn}) {
b->setFlat(true);
b->setStyleSheet("QPushButton { border: none; background: transparent; }");
b->setIconSize(QSize(kIconPx, kIconPx));
b->setFixedSize(kIconPx, kIconPx);
b->setCursor(Qt::PointingHandCursor);
auto *fx = new QGraphicsOpacityEffect(b);
fx->setOpacity(0.5);
b->setGraphicsEffect(fx);
}
applyToolbarIcons();
toolbar->addStretch();
toolbar->addWidget(topBtn);
toolbar->addSpacing(10);
toolbar->addWidget(syncBtn);
toolbar->addSpacing(10);
toolbar->addWidget(cryptoBtn);
toolbar->addSpacing(10);
toolbar->addWidget(settingsBtn);
toolbar->addSpacing(10);
toolbar->addWidget(closeBtn);
layout->addLayout(toolbar);
editor = new QTextEdit();
editor->setFrameStyle(QFrame::NoFrame);
editor->setAcceptRichText(false);
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, [this]() {
if (editor && editor->verticalScrollBar()) {
editor->verticalScrollBar()->setValue(0);
}
});
connect(cryptoBtn, &QPushButton::clicked, this, &NoteWindow::toggleEncryption);
connect(settingsBtn, &QPushButton::clicked, this, &NoteWindow::openSettings);
connect(syncBtn, &QPushButton::clicked, this, &NoteWindow::openSyncWindow);
connect(closeBtn, &QPushButton::clicked, this, &NoteWindow::closeApplication);
connect(editor, &QTextEdit::textChanged, [this]() {
QString newContent = editor->toPlainText();
bool contentChanged = (newContent != currentData.content);
currentData.content = newContent;
if (isInitialized && contentChanged) {
currentData.lastModified = QDateTime::currentMSecsSinceEpoch();
networkManager->noteActivity();
}
ConfigManager::saveNote(currentData);
updateCryptoState();
});
connect(networkManager, &NetworkManager::autoSyncDue, this, &NoteWindow::autoMergeTick);
}
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();
const QColor ic = menu->palette().color(QPalette::Text);
const QString url = urlAt(editor->cursorForPosition(ce->pos()));
menu->addSeparator();
QAction *urlAct = menu->addAction(tintedIcon(":/icons/browser.svg", ic), "Open in Browser");
urlAct->setEnabled(!url.isEmpty());
connect(urlAct, &QAction::triggered, this, [url]() {
QDesktopServices::openUrl(QUrl::fromUserInput(url));
});
QAction *dayAct = menu->addAction(tintedIcon(":/icons/header.svg", ic), "Add Next Header");
connect(dayAct, &QAction::triggered, this, &NoteWindow::insertNextDayHeader);
QAction *sortAct = menu->addAction(tintedIcon(":/icons/sort.svg", ic), "Sort (A-Z)");
sortAct->setEnabled(editor->textCursor().hasSelection());
connect(sortAct, &QAction::triggered, this, &NoteWindow::sortSelection);
menu->addSeparator();
QTextBlock spanFirst, spanLast;
blockSpan(editor->textCursor(), spanFirst, spanLast);
QAction *upAct = menu->addAction(tintedIcon(":/icons/top.svg", ic), "Move Up");
upAct->setShortcut(QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_Up));
upAct->setEnabled(spanFirst.previous().isValid());
connect(upAct, &QAction::triggered, this, [this]() { moveLines(true); });
QAction *downAct = menu->addAction(tintedIcon(":/icons/down.svg", ic), "Move Down");
downAct->setShortcut(QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_Down));
downAct->setEnabled(spanLast.next().isValid());
connect(downAct, &QAction::triggered, this, [this]() { moveLines(false); });
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();
const Qt::KeyboardModifiers mods = keyEvent->modifiers()
& (Qt::ControlModifier | Qt::ShiftModifier | Qt::AltModifier | Qt::MetaModifier);
if (mods == (Qt::ControlModifier | Qt::ShiftModifier)
&& (key == Qt::Key_Up || key == Qt::Key_Down)) {
moveLines(key == Qt::Key_Up);
return true;
}
if (key == Qt::Key_Return || key == Qt::Key_Enter || key == Qt::Key_Space) {
if (handleCalculation(key)) {
return true;
}
}
}
return QWidget::eventFilter(obj, event);
}
static void notifyAlarm(const QString &time, const QString &text) {
const QString body = QString("Wedge: %1 %2").arg(time, text.trimmed());
QProcess::startDetached("notify-send", QStringList() << "Wedge" << body);
}
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)?\\)|\\[(-\\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);
bool isAlarm = trimmed.endsWith(')');
QString prevValue = isAlarm ? alarmMatch.captured(4) : alarmMatch.captured(5);
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);
}
if (isAlarm && valueInside == "0" && !prevValue.isEmpty() && prevValue != "0") {
notifyAlarm(QString("%1:%2")
.arg(h, 2, 10, QChar('0'))
.arg(m, 2, 10, QChar('0')),
desc);
}
int leadingSpacesCount = 0;
while (leadingSpacesCount < line.length() && line[leadingSpacesCount].isSpace()) {
leadingSpacesCount++;
}
QString leadingSpaces = line.left(leadingSpacesCount);
updatedLines.append(QString("%1%2:%3 %4%5%6%7")
.arg(leadingSpaces)
.arg(h, 2, 10, QChar('0'))
.arg(m, 2, 10, QChar('0'))
.arg(desc)
.arg(QString(isAlarm ? "(" : "["))
.arg(valueInside)
.arg(QString(isAlarm ? ")" : "]")));
} 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{2})?\\((\\d*)\\)=(-?\\d*)");
QRegularExpressionMatchIterator i = scanRegex.globalMatch(content);
int offset = 0;
while (i.hasNext()) {
QRegularExpressionMatch match = i.next();
const QString prev = match.captured(5);
if (prev == "0") continue;
const int year = match.captured(3).isEmpty() ? -1 : 2000 + match.captured(3).toInt();
QString res = WedgeCommands::otcDateResult(match.captured(1).toInt(), match.captured(2), year);
if (!res.isEmpty()) {
if (!prev.isEmpty() && res.section('=', 1).toLongLong() > prev.toLongLong())
res = res.section('=', 0, 0) + "=0";
QString replacement = match.captured(1) + match.captured(2) + match.captured(3) + 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 fmt = currentData.headerFormat;
if (fmt.isEmpty()) return;
const QRegularExpression loose = WedgeHeader::matchRegex(fmt);
QTextDocument *doc = editor->document();
QTextBlock lastHeader;
QDate lastDate;
for (QTextBlock b = doc->begin(); b.isValid(); b = b.next()) {
QDate d;
if (headerDateOf(b.text(), fmt, loose, d)) {
lastHeader = b;
lastDate = d;
}
}
QString header;
QString insert;
int at = 0;
int lead = 0;
if (!lastHeader.isValid()) {
header = WedgeHeader::render(fmt, QDate::currentDate());
insert = header + "\n\n";
} else {
header = WedgeHeader::render(fmt, lastDate.addDays(1));
QTextBlock blank = lastHeader.next();
while (blank.isValid() && !blank.text().trimmed().isEmpty())
blank = blank.next();
if (blank.isValid()) {
const QTextBlock after = blank.next();
const bool gap = !after.isValid() || !after.text().trimmed().isEmpty();
at = blank.position() + blank.text().length();
lead = 1;
insert = "\n" + header + (gap ? "\n" : "");
} else {
const QTextBlock tail = doc->lastBlock();
at = tail.position() + tail.text().length();
lead = 2;
insert = "\n\n" + header + "\n";
}
}
QTextCursor edit(doc);
edit.beginEditBlock();
edit.setPosition(at);
edit.insertText(insert);
edit.endEditBlock();
QTextCursor caret = editor->textCursor();
caret.setPosition(qBound(0, at + lead + static_cast<int>(header.length()), doc->characterCount() - 1));
editor->setTextCursor(caret);
editor->ensureCursorVisible();
}
void NoteWindow::moveLines(bool up) {
if (!editor) return;
QTextCursor cursor = editor->textCursor();
QTextBlock first, last;
blockSpan(cursor, first, last);
const QTextBlock neighbor = up ? first.previous() : last.next();
if (!neighbor.isValid()) return;
QStringList moved;
for (QTextBlock b = first; b.isValid(); b = b.next()) {
moved << b.text();
if (b == last) break;
}
const QString block = moved.join('\n');
const QString other = neighbor.text();
const QTextBlock tail = up ? last : neighbor;
const int start = up ? neighbor.position() : first.position();
const int end = tail.position() + tail.text().length();
const int shift = (other.length() + 1) * (up ? -1 : 1);
const int maxPos = editor->document()->characterCount() - 1;
const int anchor = qBound(0, cursor.anchor() + shift, maxPos);
const int pos = qBound(0, cursor.position() + shift, maxPos);
QTextCursor edit(editor->document());
edit.beginEditBlock();
edit.setPosition(start);
edit.setPosition(end, QTextCursor::KeepAnchor);
edit.insertText(up ? block + "\n" + other : other + "\n" + block);
cursor.setPosition(anchor);
cursor.setPosition(pos, QTextCursor::KeepAnchor);
QTextBlock mb = editor->document()->findBlock(up ? start : start + other.length() + 1);
for (int k = 0; k < moved.size() && mb.isValid(); ++k, mb = mb.next())
relist(mb);
edit.endEditBlock();
editor->setTextCursor(cursor);
editor->ensureCursorVisible();
}
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, 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);
applyToolbarIcons();
updateCryptoState();
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::applyToolbarIcons() {
const QColor c = currentData.textColor;
topBtn->setIcon(tintedIcon(":/icons/top.svg", c));
cryptoBtn->setIcon(tintedIcon(":/icons/encrypt.svg", c));
syncBtn->setIcon(tintedIcon(":/icons/sync.svg", c));
settingsBtn->setIcon(tintedIcon(":/icons/settings.svg", c));
closeBtn->setIcon(tintedIcon(":/icons/close.svg", c));
}
void NoteWindow::updateCryptoState() {
const bool active = editor && !editor->toPlainText().isEmpty();
cryptoBtn->setEnabled(active);
if (auto *fx = qobject_cast<QGraphicsOpacityEffect *>(cryptoBtn->graphicsEffect()))
fx->setOpacity(active ? 0.5 : 0.15);
}
void NoteWindow::closeApplication() {
ConfigManager::saveNote(currentData);
QCoreApplication::quit();
}
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::openSyncWindow() {
SyncWindow dialog(currentData, this);
connect(&dialog, &SyncWindow::dataChanged, this, [this](const NoteData &d) {
currentData.passKey = d.passKey;
currentData.ipAddress = d.ipAddress;
currentData.networkRole = d.networkRole;
currentData.networkRoute = d.networkRoute;
currentData.autoMerge = d.autoMerge;
networkManager->setAutoMerge(d.autoMerge);
ConfigManager::saveNote(currentData);
});
connect(networkManager, &NetworkManager::statusChanged, dialog.netStatusValLabel, &QLabel::setText);
connect(&dialog, &SyncWindow::syncRequested, this, [this]() {
networkManager->requestSync(static_cast<NetworkManager::Route>(currentData.networkRoute));
});
connect(networkManager, &NetworkManager::linkReady, &dialog, [this]() {
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;
}
if (!archiveFirst()) return;
networkManager->sendNoteContent(editor->toPlainText(), currentData.passKey, currentData.lastModified);
currentData.lastSynced = QDateTime::currentMSecsSinceEpoch();
ConfigManager::saveNote(currentData);
});
if (networkManager->isConnected()) {
dialog.netStatusValLabel->setText("Connected");
}
dialog.exec();
}
void NoteWindow::setEditorContent(const QString &text) {
editor->blockSignals(true);
editor->setPlainText(text);
editor->blockSignals(false);
currentData.content = text;
currentData.lastModified = QDateTime::currentMSecsSinceEpoch();
ConfigManager::saveNote(currentData);
updateCryptoState();
}
void NoteWindow::toggleEncryption() {
const bool locked = WedgeSecurity::isEncrypted(editor->toPlainText());
CryptoWindow dialog(locked ? CryptoWindow::Decrypt : CryptoWindow::Encrypt, this);
connect(&dialog, &CryptoWindow::phraseSubmitted, this, [this, &dialog, locked](const QString &phrase) {
const QString out = locked
? WedgeSecurity::decrypt(editor->toPlainText(), phrase)
: WedgeSecurity::encrypt(editor->toPlainText(), phrase);
if (out.isEmpty()) return;
setEditorContent(out);
dialog.accept();
});
dialog.exec();
}
void NoteWindow::openSettings() {
SettingsDialog dialog(currentData, this);
connect(&dialog, &SettingsDialog::settingsChanged, this, [this](const NoteData &d) {
currentData.autoStart = d.autoStart;
currentData.bgColor = d.bgColor;
currentData.focusColor = d.focusColor;
currentData.fontFamily = d.fontFamily;
currentData.fontSize = d.fontSize;
currentData.headerFormat = d.headerFormat;
currentData.hideTaskbar = d.hideTaskbar;
currentData.highlightColor = d.highlightColor;
currentData.textColor = d.textColor;
updateAppearance();
ConfigManager::saveNote(currentData);
});
dialog.exec();
}
void NoteWindow::mousePressEvent(QMouseEvent *event) {
if (event->button() == Qt::LeftButton) {
dragPosition = event->globalPosition().toPoint() - frameGeometry().topLeft();
event->accept();
}
}
void NoteWindow::handleIncomingContent(const QString &content) {
if (!archiveFirst()) return;
editor->blockSignals(true);
editor->setPlainText(content);
editor->blockSignals(false);
currentData.content = content;
currentData.lastModified = networkManager->peerLastModified();
currentData.lastSynced = QDateTime::currentMSecsSinceEpoch();
ConfigManager::saveNote(currentData);
updateCryptoState();
}
void NoteWindow::autoMergeTick() {
if (currentData.lastModified <= networkManager->peerLastModified()) return;
if (!archiveFirst()) return;
networkManager->sendNoteContent(editor->toPlainText(), currentData.passKey,
currentData.lastModified);
currentData.lastSynced = QDateTime::currentMSecsSinceEpoch();
ConfigManager::saveNote(currentData);
}
bool NoteWindow::archiveFirst() {
return !currentData.autoMerge || ConfigManager::backupNote();
}
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();
}
}