Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions src/EditorManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
*/

#include <QApplication>
#include <QTimer>

#include "ApplicationSettings.h"

Expand Down Expand Up @@ -46,10 +47,41 @@ const int MARK_HIDELINESUNDERLINE = 21;
EditorManager::EditorManager(ApplicationSettings *settings, QObject *parent)
: QObject(parent), settings(settings)
{
fileWatcher = new QFileSystemWatcher(this);

connect(fileWatcher, &QFileSystemWatcher::fileChanged, this, [=](const QString &path) {
// Debounce: a single external write commonly fires this signal 2+
// times in a row. Only arm one timer per path; further signals
// received before it fires are simply dropped.
if (pendingFileChanges.contains(path)) {
return;
}

pendingFileChanges.insert(path);

QTimer::singleShot(250, this, [=]() {
pendingFileChanges.remove(path);
processExternalFileChange(path);
});
});

connect(this, &EditorManager::editorCreated, this, [=](ScintillaNext *editor) {
connect(editor, &ScintillaNext::closed, this, [=]() {
emit editorClosed(editor);
});

connect(editor, &ScintillaNext::closed, this, [=]() {
unwatchEditorFile(editor);
});

// Covers "Save As" and renaming a "New" buffer into an actual file,
// in addition to a plain rename - fileInfo is already up to date
// by the time this signal fires.
connect(editor, &ScintillaNext::renamed, this, [=]() {
watchEditorFile(editor);
});

watchEditorFile(editor);
});

connect(settings, &ApplicationSettings::showWrapSymbolChanged, this, [=](bool b) {
Expand Down Expand Up @@ -364,6 +396,51 @@ QList<QPointer<ScintillaNext> > EditorManager::getEditors()
return editors;
}

void EditorManager::processExternalFileChange(const QString &path)
{
for (ScintillaNext *editor : qAsConst(editors)) {
if (editor && editor->isFile() && editor->getFilePath() == path) {
// Some editors/tools save by replacing the file (rename-over-write),
// which makes the OS-level watch silently drop the path. Re-arm it
// whenever we can, so subsequent external edits keep being detected.
if (QFileInfo::exists(path) && !fileWatcher->files().contains(path)) {
fileWatcher->addPath(path);
}

emit editorFileChangedOnDisk(editor);
}
}
}

void EditorManager::watchEditorFile(ScintillaNext *editor)
{
const QString oldPath = watchedPaths.value(editor);
const QString newPath = editor->isFile() ? editor->getFilePath() : QString();

if (oldPath == newPath) {
return;
}

if (!oldPath.isEmpty()) {
fileWatcher->removePath(oldPath);
watchedPaths.remove(editor);
}

if (!newPath.isEmpty()) {
fileWatcher->addPath(newPath);
watchedPaths.insert(editor, newPath);
}
}

void EditorManager::unwatchEditorFile(ScintillaNext *editor)
{
const QString path = watchedPaths.take(editor);

if (!path.isEmpty()) {
fileWatcher->removePath(path);
}
}

int EditorManager::detectEOLMode(ScintillaNext *editor) const
{
qInfo(Q_FUNC_INFO);
Expand Down
27 changes: 27 additions & 0 deletions src/EditorManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@

#include <QObject>
#include <QPointer>
#include <QFileSystemWatcher>
#include <QHash>
#include <QSet>


class ApplicationSettings;
Expand All @@ -45,14 +48,38 @@ class EditorManager : public QObject
void editorCreated(ScintillaNext *editor);
void editorClosed(ScintillaNext *editor);

// Emitted asynchronously whenever the OS reports that an open file was
// touched outside the application (write, permissions, delete, etc).
// Does NOT get emitted for the application's own saves, since the
// in-memory timestamp is refreshed synchronously before this signal
// could ever be observed.
void editorFileChangedOnDisk(ScintillaNext *editor);

private:
void setupEditor(ScintillaNext *editor);
void purgeOldEditorPointers();
QList<QPointer<ScintillaNext>> getEditors();
int detectEOLMode(ScintillaNext *editor) const;

void watchEditorFile(ScintillaNext *editor);
void unwatchEditorFile(ScintillaNext *editor);
void processExternalFileChange(const QString &path);

QList<QPointer<ScintillaNext>> editors;
ApplicationSettings *settings;

QFileSystemWatcher *fileWatcher;

// Tracks the path currently registered with fileWatcher for each editor,
// so a rename/"Save As" can drop the old path instead of leaking an
// orphaned watch on it.
QHash<ScintillaNext *, QString> watchedPaths;

// A single external write commonly triggers 2+ fileChanged signals in a
// row (this is a well-known QFileSystemWatcher/inotify quirk, not a bug
// in this class). Paths in this set already have a debounce timer
// pending, so further signals for them are ignored until it fires.
QSet<QString> pendingFileChanges;
};

#endif // EDITORMANAGER_H
42 changes: 41 additions & 1 deletion src/dialogs/MainWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,27 @@
connect(dockedEditor, &DockedEditor::contextMenuRequestedForEditor, this, &MainWindow::tabBarRightClicked);
connect(dockedEditor, &DockedEditor::titleBarDoubleClicked, this, &MainWindow::newFile);

// React immediately (instead of only on tab-switch/window-focus) when an
// open file is changed by another program - but only if NotepadNext is
// actually the foreground window and the affected editor is the one
// currently visible, matching Notepad++'s behavior. Otherwise, do
// nothing here: the existing activateEditor() (tab switch) and
// focusIn() (window regains focus) code paths already re-check the
// file's timestamp and will surface the dialog at the right time.
connect(app->getEditorManager(), &EditorManager::editorFileChangedOnDisk, this, [=, this](ScintillaNext *editor) {
if (!isActiveWindow()) {
return;
}

if (editor != currentEditor()) {
return;
}

if (checkFileForModification(editor)) {
updateGui(currentEditor());
}
});

// Set up the menus
connect(ui->actionNew, &QAction::triggered, this, &MainWindow::newFile);
connect(ui->actionOpen, &QAction::triggered, this, &MainWindow::openFileDialog);
Expand Down Expand Up @@ -739,7 +760,7 @@
showEditorZoomLevelIndicator();
});
connect(ui->actionZoomOut, &QAction::triggered, this, [this]() {
// Scintilla can zoom out to "-10" but on a screen with fractional scaling it throws alot of Qt warnings

Check failure on line 763 in src/dialogs/MainWindow.cpp

View workflow job for this annotation

GitHub Actions / Check for spelling errors

alot ==> a lot, allot
if (zoomLevel == -9) return;

for (ScintillaNext *editor : editors()) {
Expand Down Expand Up @@ -1952,8 +1973,15 @@
else if (state == ScintillaNext::Modified) {
qInfo("ScintillaNext::Modified");
const QString filePath = editor->getFilePath();
auto reply = QMessageBox::question(this, tr("Reload File"), tr("<b>%1</b> has been modified by another program. Do you want to reload it?").arg(filePath));
QString message = tr("<b>%1</b> has been modified by another program. Do you want to reload it?").arg(filePath);

if (!editor->isSavedToDisk()) {
message += tr("<br><br><b>Warning:</b> you have unsaved changes in this tab. "
"Reloading will discard them permanently.");
}

auto reply = QMessageBox::question(this, tr("Reload File"), message);

if (reply == QMessageBox::Yes) {
editor->reload();
}
Expand Down Expand Up @@ -2103,6 +2131,18 @@
}
}

void MainWindow::changeEvent(QEvent *event)
{
QMainWindow::changeEvent(event);

if (event->type() == QEvent::ActivationChange) {
qInfo() << "ActivationChange reçu, isActiveWindow() =" << isActiveWindow();
if (isActiveWindow()) {
focusIn();
}
}
}

void MainWindow::addEditor(ScintillaNext *editor)
{
qInfo(Q_FUNC_INFO);
Expand Down
1 change: 1 addition & 0 deletions src/dialogs/MainWindow.h
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ public slots:
void closeEvent(QCloseEvent *event) override;
void dragEnterEvent(QDragEnterEvent *event) override;
void dropEvent(QDropEvent *event) override;
void changeEvent(QEvent *event) override;

private slots:
void tabBarRightClicked(ScintillaNext *editor);
Expand Down
Loading