Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cb6617222d | |||
| 43e8e332fb | |||
| 5b81ef9c12 | |||
| 250783f63b |
13
BareCode.desktop
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Version=1.0
|
||||||
|
Type=Application
|
||||||
|
Name=BareCode
|
||||||
|
GenericName=Code Editor
|
||||||
|
Comment=Modularer Code-Editor für HTML, PHP, CSS und mehr
|
||||||
|
Exec=BareCode %F
|
||||||
|
Icon=barecode
|
||||||
|
Terminal=false
|
||||||
|
Categories=Development;TextEditor;
|
||||||
|
MimeType=text/plain;text/html;text/css;text/x-php;text/x-csrc;text/x-chdr;text/x-c++src;text/x-c++hdr;
|
||||||
|
Keywords=editor;code;html;php;css;c++;
|
||||||
|
StartupWMClass=BareCode
|
||||||
1
BareCode.rc
Normal file
@@ -0,0 +1 @@
|
|||||||
|
IDI_ICON1 ICON "resources/BareCode.ico"
|
||||||
@@ -29,6 +29,7 @@ find_package(Qt6 REQUIRED COMPONENTS
|
|||||||
Core
|
Core
|
||||||
Gui
|
Gui
|
||||||
Widgets
|
Widgets
|
||||||
|
Concurrent
|
||||||
)
|
)
|
||||||
|
|
||||||
qt_standard_project_setup()
|
qt_standard_project_setup()
|
||||||
@@ -43,10 +44,18 @@ add_subdirectory(src)
|
|||||||
qt_add_resources(BARECODE_RESOURCES resources/resources.qrc)
|
qt_add_resources(BARECODE_RESOURCES resources/resources.qrc)
|
||||||
|
|
||||||
# Main executable
|
# Main executable
|
||||||
qt_add_executable(BareCode
|
if(WIN32)
|
||||||
main.cpp
|
qt_add_executable(BareCode
|
||||||
${BARECODE_RESOURCES}
|
main.cpp
|
||||||
)
|
BareCode.rc
|
||||||
|
${BARECODE_RESOURCES}
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
qt_add_executable(BareCode
|
||||||
|
main.cpp
|
||||||
|
${BARECODE_RESOURCES}
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
target_link_libraries(BareCode PRIVATE
|
target_link_libraries(BareCode PRIVATE
|
||||||
BareCode_Core
|
BareCode_Core
|
||||||
@@ -67,3 +76,18 @@ include(GNUInstallDirs)
|
|||||||
install(TARGETS BareCode
|
install(TARGETS BareCode
|
||||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if(UNIX AND NOT APPLE)
|
||||||
|
# Icons in den Standard-Hicolor-Theme-Pfad installieren
|
||||||
|
foreach(SIZE 16 32 48 64 128 256 512)
|
||||||
|
install(FILES resources/icon_${SIZE}.png
|
||||||
|
DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/${SIZE}x${SIZE}/apps
|
||||||
|
RENAME barecode.png
|
||||||
|
)
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
# .desktop Datei
|
||||||
|
install(FILES BareCode.desktop
|
||||||
|
DESTINATION ${CMAKE_INSTALL_DATADIR}/applications
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|||||||
12
main.cpp
@@ -1,4 +1,5 @@
|
|||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
|
#include <QIcon>
|
||||||
#include "core/MainWindow.h"
|
#include "core/MainWindow.h"
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
@@ -9,6 +10,17 @@ int main(int argc, char *argv[])
|
|||||||
app.setApplicationVersion("1.0.0");
|
app.setApplicationVersion("1.0.0");
|
||||||
app.setOrganizationName("BareCode");
|
app.setOrganizationName("BareCode");
|
||||||
|
|
||||||
|
// Icon in allen verfügbaren Größen setzen
|
||||||
|
QIcon appIcon;
|
||||||
|
appIcon.addFile(":/icon_16.png", QSize(16, 16));
|
||||||
|
appIcon.addFile(":/icon_32.png", QSize(32, 32));
|
||||||
|
appIcon.addFile(":/icon_48.png", QSize(48, 48));
|
||||||
|
appIcon.addFile(":/icon_64.png", QSize(64, 64));
|
||||||
|
appIcon.addFile(":/icon_128.png", QSize(128, 128));
|
||||||
|
appIcon.addFile(":/icon_256.png", QSize(256, 256));
|
||||||
|
appIcon.addFile(":/icon_512.png", QSize(512, 512));
|
||||||
|
app.setWindowIcon(appIcon);
|
||||||
|
|
||||||
MainWindow window;
|
MainWindow window;
|
||||||
window.show();
|
window.show();
|
||||||
|
|
||||||
|
|||||||
BIN
resources/BareCode.ico
Normal file
|
After Width: | Height: | Size: 846 B |
BIN
resources/icon_128.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
resources/icon_16.png
Normal file
|
After Width: | Height: | Size: 824 B |
BIN
resources/icon_24.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
resources/icon_256.png
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
resources/icon_32.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
resources/icon_48.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
resources/icon_512.png
Normal file
|
After Width: | Height: | Size: 303 KiB |
BIN
resources/icon_64.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
@@ -1,5 +1,11 @@
|
|||||||
<RCC>
|
<RCC>
|
||||||
<qresource prefix="/">
|
<qresource prefix="/">
|
||||||
<!-- Icons and other assets can be added here -->
|
<file>icon_512.png</file>
|
||||||
|
<file>icon_256.png</file>
|
||||||
|
<file>icon_128.png</file>
|
||||||
|
<file>icon_64.png</file>
|
||||||
|
<file>icon_48.png</file>
|
||||||
|
<file>icon_32.png</file>
|
||||||
|
<file>icon_16.png</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
|
|||||||
@@ -35,6 +35,12 @@ MainWindow::MainWindow(QWidget *parent)
|
|||||||
{
|
{
|
||||||
m_projectManager->openProject(lastPath);
|
m_projectManager->openProject(lastPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Letzte Session wiederherstellen (geöffnete Dateien + aktiver Tab)
|
||||||
|
m_editor->restoreSession(
|
||||||
|
m_settings->lastOpenFiles(),
|
||||||
|
m_settings->lastActiveFile()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
MainWindow::~MainWindow() = default;
|
MainWindow::~MainWindow() = default;
|
||||||
@@ -110,6 +116,9 @@ void MainWindow::setupMenuBar()
|
|||||||
m_actSearch = mBearbeiten->addAction(tr("&Suchen / Ersetzen…"), this, &MainWindow::onShowSearch);
|
m_actSearch = mBearbeiten->addAction(tr("&Suchen / Ersetzen…"), this, &MainWindow::onShowSearch);
|
||||||
m_actSearch->setShortcut(QKeySequence::Find);
|
m_actSearch->setShortcut(QKeySequence::Find);
|
||||||
|
|
||||||
|
m_actFileSearch = mBearbeiten->addAction(tr("In &Dateien suchen…"), this, &MainWindow::onShowFileSearch);
|
||||||
|
m_actFileSearch->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_F);
|
||||||
|
|
||||||
// ---- Ansicht ----
|
// ---- Ansicht ----
|
||||||
QMenu *mAnsicht = menuBar()->addMenu(tr("&Ansicht"));
|
QMenu *mAnsicht = menuBar()->addMenu(tr("&Ansicht"));
|
||||||
|
|
||||||
@@ -272,10 +281,16 @@ void MainWindow::onAbout()
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Slots – Projekt
|
// Slots – Projekt
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
void MainWindow::onShowFileSearch()
|
||||||
|
{
|
||||||
|
m_editor->showFileSearchPanel();
|
||||||
|
}
|
||||||
|
|
||||||
void MainWindow::onProjectOpened(const QString &path)
|
void MainWindow::onProjectOpened(const QString &path)
|
||||||
{
|
{
|
||||||
setWindowTitle(QString("BareCode – %1").arg(path));
|
setWindowTitle(QString("BareCode – %1").arg(path));
|
||||||
m_fileTree->setRootPath(path);
|
m_fileTree->setRootPath(path);
|
||||||
|
m_editor->setSearchRoot(path);
|
||||||
statusBar()->showMessage(tr("Projekt geöffnet: %1").arg(path), 4000);
|
statusBar()->showMessage(tr("Projekt geöffnet: %1").arg(path), 4000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,6 +298,7 @@ void MainWindow::onProjectClosed()
|
|||||||
{
|
{
|
||||||
setWindowTitle("BareCode");
|
setWindowTitle("BareCode");
|
||||||
m_fileTree->clearRoot();
|
m_fileTree->clearRoot();
|
||||||
|
m_editor->setSearchRoot(QString());
|
||||||
statusBar()->showMessage(tr("Projekt geschlossen"), 3000);
|
statusBar()->showMessage(tr("Projekt geschlossen"), 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,6 +310,10 @@ void MainWindow::saveWindowState()
|
|||||||
QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode");
|
QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode");
|
||||||
s.setValue("window/geometry", saveGeometry());
|
s.setValue("window/geometry", saveGeometry());
|
||||||
s.setValue("window/state", saveState());
|
s.setValue("window/state", saveState());
|
||||||
|
|
||||||
|
// Session speichern
|
||||||
|
m_settings->setLastOpenFiles(m_editor->openFilePaths());
|
||||||
|
m_settings->setLastActiveFile(m_editor->activeFilePath());
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::restoreWindowState()
|
void MainWindow::restoreWindowState()
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ private slots:
|
|||||||
void onUndo();
|
void onUndo();
|
||||||
void onRedo();
|
void onRedo();
|
||||||
void onShowSearch();
|
void onShowSearch();
|
||||||
|
void onShowFileSearch();
|
||||||
// Ansicht
|
// Ansicht
|
||||||
void onToggleDarkMode(bool checked);
|
void onToggleDarkMode(bool checked);
|
||||||
// Hilfe
|
// Hilfe
|
||||||
@@ -80,6 +81,7 @@ private:
|
|||||||
QAction *m_actUndo = nullptr;
|
QAction *m_actUndo = nullptr;
|
||||||
QAction *m_actRedo = nullptr;
|
QAction *m_actRedo = nullptr;
|
||||||
QAction *m_actSearch = nullptr;
|
QAction *m_actSearch = nullptr;
|
||||||
|
QAction *m_actFileSearch = nullptr;
|
||||||
QAction *m_actDarkMode = nullptr;
|
QAction *m_actDarkMode = nullptr;
|
||||||
QAction *m_actAbout = nullptr;
|
QAction *m_actAbout = nullptr;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -88,3 +88,26 @@ void Settings::setLastProjectPath(const QString &path)
|
|||||||
{
|
{
|
||||||
m_settings.setValue("project/lastPath", path);
|
m_settings.setValue("project/lastPath", path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Session – geöffnete Dateien
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
QStringList Settings::lastOpenFiles() const
|
||||||
|
{
|
||||||
|
return m_settings.value("session/openFiles", QStringList()).toStringList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Settings::setLastOpenFiles(const QStringList &files)
|
||||||
|
{
|
||||||
|
m_settings.setValue("session/openFiles", files);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Settings::lastActiveFile() const
|
||||||
|
{
|
||||||
|
return m_settings.value("session/activeFile", QString()).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Settings::setLastActiveFile(const QString &file)
|
||||||
|
{
|
||||||
|
m_settings.setValue("session/activeFile", file);
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,6 +29,13 @@ public:
|
|||||||
int fileTreeWidth() const;
|
int fileTreeWidth() const;
|
||||||
void setFileTreeWidth(int width);
|
void setFileTreeWidth(int width);
|
||||||
|
|
||||||
|
// Zuletzt geöffnete Dateien (Session-Wiederherstellung)
|
||||||
|
QStringList lastOpenFiles() const;
|
||||||
|
void setLastOpenFiles(const QStringList &files);
|
||||||
|
|
||||||
|
QString lastActiveFile() const;
|
||||||
|
void setLastActiveFile(const QString &file);
|
||||||
|
|
||||||
// Recent
|
// Recent
|
||||||
QString lastProjectPath() const;
|
QString lastProjectPath() const;
|
||||||
void setLastProjectPath(const QString &path);
|
void setLastProjectPath(const QString &path);
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ set(EDITOR_SOURCES
|
|||||||
EditorTab.h
|
EditorTab.h
|
||||||
SearchPanel.cpp
|
SearchPanel.cpp
|
||||||
SearchPanel.h
|
SearchPanel.h
|
||||||
|
FileSearchPanel.cpp
|
||||||
|
FileSearchPanel.h
|
||||||
)
|
)
|
||||||
|
|
||||||
add_library(BareCode_Editor STATIC ${EDITOR_SOURCES})
|
add_library(BareCode_Editor STATIC ${EDITOR_SOURCES})
|
||||||
@@ -17,6 +19,7 @@ target_link_libraries(BareCode_Editor PUBLIC
|
|||||||
Qt6::Core
|
Qt6::Core
|
||||||
Qt6::Gui
|
Qt6::Gui
|
||||||
Qt6::Widgets
|
Qt6::Widgets
|
||||||
|
Qt6::Concurrent
|
||||||
BareCode_Highlighter
|
BareCode_Highlighter
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -194,6 +194,126 @@ void CodeEditor::updateLineNumberArea(const QRect &rect, int dy)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CodeEditor::paintEvent(QPaintEvent *event)
|
||||||
|
{
|
||||||
|
// Zuerst den normalen Editor-Inhalt zeichnen
|
||||||
|
QPlainTextEdit::paintEvent(event);
|
||||||
|
|
||||||
|
// Danach die Einrück-Führungslinien darüber legen
|
||||||
|
const int tabSize = m_settings->tabSize();
|
||||||
|
if (tabSize <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPainter painter(viewport());
|
||||||
|
|
||||||
|
// Farbe: subtil, passt zu Hell- und Dunkeltheme
|
||||||
|
QColor guideColor = palette().color(QPalette::Text);
|
||||||
|
guideColor.setAlpha(30);
|
||||||
|
painter.setPen(QPen(guideColor, 1, Qt::SolidLine));
|
||||||
|
|
||||||
|
const QFontMetrics fm(font());
|
||||||
|
const int spaceWidth = fm.horizontalAdvance(' ');
|
||||||
|
const int tabPixels = tabSize * spaceWidth;
|
||||||
|
|
||||||
|
if (tabPixels <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// X-Startposition des Textes direkt aus dem Layout des ersten Blocks holen.
|
||||||
|
// Das ist der einzige zuverlässige Weg — Qt berücksichtigt intern Margins,
|
||||||
|
// Gutter und Frame-Abstände die sich nicht sauber manuell nachrechnen lassen.
|
||||||
|
int textOriginX = 0;
|
||||||
|
{
|
||||||
|
QTextBlock firstBlock = firstVisibleBlock();
|
||||||
|
if (!firstBlock.isValid())
|
||||||
|
{
|
||||||
|
firstBlock = document()->begin();
|
||||||
|
}
|
||||||
|
if (firstBlock.isValid())
|
||||||
|
{
|
||||||
|
const QRectF blockRect = blockBoundingGeometry(firstBlock)
|
||||||
|
.translated(contentOffset());
|
||||||
|
// Position des ersten Zeichens im Layout
|
||||||
|
const QTextLayout *layout = firstBlock.layout();
|
||||||
|
if (layout && layout->lineCount() > 0)
|
||||||
|
{
|
||||||
|
textOriginX = static_cast<int>(blockRect.left()
|
||||||
|
+ layout->lineAt(0).position().x());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
textOriginX = static_cast<int>(blockRect.left());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Horizontalen Scroll-Offset berücksichtigen
|
||||||
|
const int scrollX = horizontalScrollBar()->value();
|
||||||
|
|
||||||
|
// Sichtbaren Zeilenbereich bestimmen
|
||||||
|
QTextBlock block = firstVisibleBlock();
|
||||||
|
const int bottom = event->rect().bottom();
|
||||||
|
|
||||||
|
while (block.isValid())
|
||||||
|
{
|
||||||
|
const QRectF blockRect = blockBoundingGeometry(block).translated(contentOffset());
|
||||||
|
|
||||||
|
if (blockRect.top() > bottom)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (block.isVisible())
|
||||||
|
{
|
||||||
|
const QString text = block.text();
|
||||||
|
|
||||||
|
// Einrückungstiefe der Zeile zählen (Leerzeichen / Tabs)
|
||||||
|
int indentSpaces = 0;
|
||||||
|
for (const QChar &ch : text)
|
||||||
|
{
|
||||||
|
if (ch == ' ')
|
||||||
|
{
|
||||||
|
++indentSpaces;
|
||||||
|
}
|
||||||
|
else if (ch == '\t')
|
||||||
|
{
|
||||||
|
// Tab auffüllen auf nächsten Tab-Stop
|
||||||
|
indentSpaces = ((indentSpaces / tabSize) + 1) * tabSize;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const int indentStops = indentSpaces / tabSize;
|
||||||
|
|
||||||
|
// Für jeden Einrückungslevel eine vertikale Linie zeichnen
|
||||||
|
for (int stop = 1; stop <= indentStops; ++stop)
|
||||||
|
{
|
||||||
|
const int xPixel = textOriginX + stop * tabPixels - scrollX;
|
||||||
|
|
||||||
|
// Nur im sichtbaren Bereich zeichnen
|
||||||
|
if (xPixel < lineNumberAreaWidth() || xPixel > viewport()->width())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int y1 = static_cast<int>(blockRect.top());
|
||||||
|
const int y2 = static_cast<int>(blockRect.bottom());
|
||||||
|
|
||||||
|
painter.drawLine(xPixel, y1, xPixel, y2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
block = block.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void CodeEditor::resizeEvent(QResizeEvent *event)
|
void CodeEditor::resizeEvent(QResizeEvent *event)
|
||||||
{
|
{
|
||||||
QPlainTextEdit::resizeEvent(event);
|
QPlainTextEdit::resizeEvent(event);
|
||||||
@@ -269,19 +389,77 @@ void CodeEditor::highlightCurrentLine()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Key handling – auto-indent + Tab → spaces
|
// Key handling – Tab/Shift+Tab, Smart-Backspace, Auto-Indent
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
void CodeEditor::keyPressEvent(QKeyEvent *event)
|
void CodeEditor::keyPressEvent(QKeyEvent *event)
|
||||||
{
|
{
|
||||||
// Tab key: insert spaces instead of a real tab character
|
const int tabSize = m_settings->tabSize();
|
||||||
if (event->key() == Qt::Key_Tab && m_settings->useSpacesForTabs())
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Shift+Tab: Einrückung zurückziehen
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
if (event->key() == Qt::Key_Backtab ||
|
||||||
|
(event->key() == Qt::Key_Tab && event->modifiers() & Qt::ShiftModifier))
|
||||||
|
{
|
||||||
|
QTextCursor cursor = textCursor();
|
||||||
|
|
||||||
|
QTextBlock startBlock = document()->findBlock(cursor.selectionStart());
|
||||||
|
QTextBlock endBlock = document()->findBlock(cursor.selectionEnd());
|
||||||
|
|
||||||
|
cursor.beginEditBlock();
|
||||||
|
for (QTextBlock b = startBlock; b != endBlock.next(); b = b.next())
|
||||||
|
{
|
||||||
|
const QString lineText = b.text();
|
||||||
|
int toRemove = 0;
|
||||||
|
|
||||||
|
if (m_settings->useSpacesForTabs())
|
||||||
|
{
|
||||||
|
// Bis zu tabSize führende Leerzeichen entfernen
|
||||||
|
for (int i = 0; i < tabSize && i < lineText.length(); ++i)
|
||||||
|
{
|
||||||
|
if (lineText[i] == ' ')
|
||||||
|
{
|
||||||
|
++toRemove;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Einen führenden Tab entfernen
|
||||||
|
if (!lineText.isEmpty() && lineText[0] == '\t')
|
||||||
|
{
|
||||||
|
toRemove = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toRemove > 0)
|
||||||
|
{
|
||||||
|
QTextCursor lineCursor(b);
|
||||||
|
lineCursor.movePosition(QTextCursor::StartOfBlock);
|
||||||
|
lineCursor.movePosition(QTextCursor::Right,
|
||||||
|
QTextCursor::KeepAnchor,
|
||||||
|
toRemove);
|
||||||
|
lineCursor.removeSelectedText();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cursor.endEditBlock();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Tab: Einrücken (Leerzeichen oder echter Tab)
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
if (event->key() == Qt::Key_Tab)
|
||||||
{
|
{
|
||||||
const int tabSize = m_settings->tabSize();
|
|
||||||
QTextCursor cursor = textCursor();
|
QTextCursor cursor = textCursor();
|
||||||
|
|
||||||
if (cursor.hasSelection())
|
if (cursor.hasSelection())
|
||||||
{
|
{
|
||||||
// Indent selected lines
|
// Mehrere Zeilen einrücken
|
||||||
QTextBlock startBlock = document()->findBlock(cursor.selectionStart());
|
QTextBlock startBlock = document()->findBlock(cursor.selectionStart());
|
||||||
QTextBlock endBlock = document()->findBlock(cursor.selectionEnd());
|
QTextBlock endBlock = document()->findBlock(cursor.selectionEnd());
|
||||||
|
|
||||||
@@ -289,27 +467,76 @@ void CodeEditor::keyPressEvent(QKeyEvent *event)
|
|||||||
for (QTextBlock b = startBlock; b != endBlock.next(); b = b.next())
|
for (QTextBlock b = startBlock; b != endBlock.next(); b = b.next())
|
||||||
{
|
{
|
||||||
QTextCursor lineCursor(b);
|
QTextCursor lineCursor(b);
|
||||||
lineCursor.insertText(QString(tabSize, ' '));
|
lineCursor.movePosition(QTextCursor::StartOfBlock);
|
||||||
|
if (m_settings->useSpacesForTabs())
|
||||||
|
{
|
||||||
|
lineCursor.insertText(QString(tabSize, ' '));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
lineCursor.insertText("\t");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
cursor.endEditBlock();
|
cursor.endEditBlock();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Calculate spaces needed to reach next tab stop
|
if (m_settings->useSpacesForTabs())
|
||||||
const int col = cursor.columnNumber();
|
{
|
||||||
const int spacesNeeded = tabSize - (col % tabSize);
|
// Zum nächsten Tab-Stop auffüllen
|
||||||
cursor.insertText(QString(spacesNeeded, ' '));
|
const int col = cursor.columnNumber();
|
||||||
|
const int spacesNeeded = tabSize - (col % tabSize);
|
||||||
|
cursor.insertText(QString(spacesNeeded, ' '));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
cursor.insertText("\t");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enter / Return: auto-indent
|
// -----------------------------------------------------------------------
|
||||||
|
// Smart Backspace: springt zur vorherigen Einrückungsstufe
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
if (event->key() == Qt::Key_Backspace
|
||||||
|
&& !textCursor().hasSelection()
|
||||||
|
&& m_settings->useSpacesForTabs())
|
||||||
|
{
|
||||||
|
QTextCursor cursor = textCursor();
|
||||||
|
const int col = cursor.columnNumber();
|
||||||
|
|
||||||
|
if (col > 0)
|
||||||
|
{
|
||||||
|
// Prüfen ob links vom Cursor nur Leerzeichen bis Zeilenbeginn stehen
|
||||||
|
const QString lineText = cursor.block().text();
|
||||||
|
const QString leftOfCursor = lineText.left(col);
|
||||||
|
const bool onlySpaces = leftOfCursor.trimmed().isEmpty();
|
||||||
|
|
||||||
|
if (onlySpaces && col > 0)
|
||||||
|
{
|
||||||
|
// Zur vorherigen Tab-Stop-Position springen
|
||||||
|
const int targetCol = ((col - 1) / tabSize) * tabSize;
|
||||||
|
const int toDelete = col - targetCol;
|
||||||
|
|
||||||
|
cursor.movePosition(QTextCursor::Left,
|
||||||
|
QTextCursor::KeepAnchor,
|
||||||
|
toDelete);
|
||||||
|
cursor.removeSelectedText();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Enter / Return: Auto-Indent
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter)
|
if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter)
|
||||||
{
|
{
|
||||||
QTextCursor cursor = textCursor();
|
QTextCursor cursor = textCursor();
|
||||||
const QString currentLine = cursor.block().text();
|
const QString currentLine = cursor.block().text();
|
||||||
|
|
||||||
// Count leading whitespace
|
// Führende Leerzeichen der aktuellen Zeile zählen
|
||||||
int leadingSpaces = 0;
|
int leadingSpaces = 0;
|
||||||
for (const QChar &ch : currentLine)
|
for (const QChar &ch : currentLine)
|
||||||
{
|
{
|
||||||
@@ -319,7 +546,7 @@ void CodeEditor::keyPressEvent(QKeyEvent *event)
|
|||||||
}
|
}
|
||||||
else if (ch == '\t')
|
else if (ch == '\t')
|
||||||
{
|
{
|
||||||
leadingSpaces += m_settings->tabSize();
|
leadingSpaces += tabSize;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -327,15 +554,13 @@ void CodeEditor::keyPressEvent(QKeyEvent *event)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Let the base class insert the newline first
|
|
||||||
QPlainTextEdit::keyPressEvent(event);
|
QPlainTextEdit::keyPressEvent(event);
|
||||||
|
|
||||||
// Then re-indent
|
|
||||||
if (leadingSpaces > 0)
|
if (leadingSpaces > 0)
|
||||||
{
|
{
|
||||||
const QString indent = m_settings->useSpacesForTabs()
|
const QString indent = m_settings->useSpacesForTabs()
|
||||||
? QString(leadingSpaces, ' ')
|
? QString(leadingSpaces, ' ')
|
||||||
: QString(leadingSpaces / m_settings->tabSize(), '\t');
|
: QString(leadingSpaces / tabSize, '\t');
|
||||||
textCursor().insertText(indent);
|
textCursor().insertText(indent);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -343,3 +568,4 @@ void CodeEditor::keyPressEvent(QKeyEvent *event)
|
|||||||
|
|
||||||
QPlainTextEdit::keyPressEvent(event);
|
QPlainTextEdit::keyPressEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ signals:
|
|||||||
protected:
|
protected:
|
||||||
void resizeEvent(QResizeEvent *event) override;
|
void resizeEvent(QResizeEvent *event) override;
|
||||||
void keyPressEvent(QKeyEvent *event) override;
|
void keyPressEvent(QKeyEvent *event) override;
|
||||||
|
void paintEvent(QPaintEvent *event) override;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void updateLineNumberAreaWidth(int newBlockCount);
|
void updateLineNumberAreaWidth(int newBlockCount);
|
||||||
|
|||||||
@@ -2,8 +2,12 @@
|
|||||||
#include "EditorTab.h"
|
#include "EditorTab.h"
|
||||||
#include "CodeEditor.h"
|
#include "CodeEditor.h"
|
||||||
#include "SearchPanel.h"
|
#include "SearchPanel.h"
|
||||||
|
#include "FileSearchPanel.h"
|
||||||
|
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QTextDocument>
|
||||||
|
#include <QTextBlock>
|
||||||
|
|
||||||
EditorPanel::EditorPanel(Settings *settings, QWidget *parent)
|
EditorPanel::EditorPanel(Settings *settings, QWidget *parent)
|
||||||
: QWidget(parent)
|
: QWidget(parent)
|
||||||
@@ -27,15 +31,20 @@ void EditorPanel::setupUi()
|
|||||||
m_tabWidget->setDocumentMode(true);
|
m_tabWidget->setDocumentMode(true);
|
||||||
|
|
||||||
m_searchPanel = new SearchPanel(this);
|
m_searchPanel = new SearchPanel(this);
|
||||||
|
m_fileSearch = new FileSearchPanel(this);
|
||||||
|
|
||||||
m_layout->addWidget(m_tabWidget, 1);
|
m_layout->addWidget(m_tabWidget, 1);
|
||||||
m_layout->addWidget(m_searchPanel, 0);
|
m_layout->addWidget(m_searchPanel, 0);
|
||||||
|
m_layout->addWidget(m_fileSearch, 0);
|
||||||
|
|
||||||
connect(m_tabWidget, &QTabWidget::tabCloseRequested,
|
connect(m_tabWidget, &QTabWidget::tabCloseRequested,
|
||||||
this, &EditorPanel::onTabCloseRequested);
|
this, &EditorPanel::onTabCloseRequested);
|
||||||
|
|
||||||
connect(m_tabWidget, &QTabWidget::currentChanged,
|
connect(m_tabWidget, &QTabWidget::currentChanged,
|
||||||
this, &EditorPanel::onCurrentTabChanged);
|
this, &EditorPanel::onCurrentTabChanged);
|
||||||
|
|
||||||
|
connect(m_fileSearch, &FileSearchPanel::fileLineRequested,
|
||||||
|
this, &EditorPanel::goToLine);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -70,7 +79,20 @@ void EditorPanel::openFile(const QString &filePath)
|
|||||||
m_tabWidget->setTabToolTip(index, filePath);
|
m_tabWidget->setTabToolTip(index, filePath);
|
||||||
m_openTabs.insert(filePath, tab);
|
m_openTabs.insert(filePath, tab);
|
||||||
|
|
||||||
// Tab-Titel nach "Speichern unter" aktualisieren
|
// Änderungsindikator im Tab-Titel (● = ungespeichert)
|
||||||
|
connect(tab->editor()->document(), &QTextDocument::modificationChanged,
|
||||||
|
this, [this, tab](bool modified)
|
||||||
|
{
|
||||||
|
const int idx = m_tabWidget->indexOf(tab);
|
||||||
|
if (idx == -1)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const QString name = QFileInfo(tab->filePath()).fileName();
|
||||||
|
m_tabWidget->setTabText(idx, modified ? "● " + name : name);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tab-Titel nach "Speichern unter" aktualisieren (neuer Dateiname, kein Punkt)
|
||||||
connect(tab->editor(), &CodeEditor::fileSaved, this, [this, tab](const QString &savedPath)
|
connect(tab->editor(), &CodeEditor::fileSaved, this, [this, tab](const QString &savedPath)
|
||||||
{
|
{
|
||||||
const int idx = m_tabWidget->indexOf(tab);
|
const int idx = m_tabWidget->indexOf(tab);
|
||||||
@@ -113,9 +135,88 @@ void EditorPanel::saveAllFiles()
|
|||||||
|
|
||||||
void EditorPanel::showSearchPanel()
|
void EditorPanel::showSearchPanel()
|
||||||
{
|
{
|
||||||
|
m_fileSearch->hide();
|
||||||
m_searchPanel->activate();
|
m_searchPanel->activate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void EditorPanel::showFileSearchPanel()
|
||||||
|
{
|
||||||
|
m_searchPanel->hide();
|
||||||
|
m_fileSearch->activate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EditorPanel::setSearchRoot(const QString &path)
|
||||||
|
{
|
||||||
|
m_fileSearch->setSearchRoot(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EditorPanel::goToLine(const QString &filePath, int line)
|
||||||
|
{
|
||||||
|
// Datei öffnen falls noch nicht geöffnet
|
||||||
|
openFile(filePath);
|
||||||
|
|
||||||
|
EditorTab *tab = m_openTabs.value(filePath, nullptr);
|
||||||
|
if (!tab)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_tabWidget->setCurrentWidget(tab);
|
||||||
|
|
||||||
|
// Zur gewünschten Zeile springen
|
||||||
|
CodeEditor *editor = tab->editor();
|
||||||
|
QTextBlock block = editor->document()->findBlockByLineNumber(line - 1);
|
||||||
|
if (block.isValid())
|
||||||
|
{
|
||||||
|
QTextCursor cursor(block);
|
||||||
|
cursor.movePosition(QTextCursor::StartOfBlock);
|
||||||
|
editor->setTextCursor(cursor);
|
||||||
|
editor->centerCursor();
|
||||||
|
editor->setFocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList EditorPanel::openFilePaths() const
|
||||||
|
{
|
||||||
|
QStringList paths;
|
||||||
|
for (int i = 0; i < m_tabWidget->count(); ++i)
|
||||||
|
{
|
||||||
|
EditorTab *tab = qobject_cast<EditorTab *>(m_tabWidget->widget(i));
|
||||||
|
if (tab)
|
||||||
|
{
|
||||||
|
paths.append(tab->filePath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString EditorPanel::activeFilePath() const
|
||||||
|
{
|
||||||
|
EditorTab *tab = currentTab();
|
||||||
|
return tab ? tab->filePath() : QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EditorPanel::restoreSession(const QStringList &files, const QString &activeFile)
|
||||||
|
{
|
||||||
|
for (const QString &path : files)
|
||||||
|
{
|
||||||
|
if (QFile::exists(path))
|
||||||
|
{
|
||||||
|
openFile(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aktiven Tab wiederherstellen
|
||||||
|
if (!activeFile.isEmpty())
|
||||||
|
{
|
||||||
|
const int idx = findTabForFile(activeFile);
|
||||||
|
if (idx != -1)
|
||||||
|
{
|
||||||
|
m_tabWidget->setCurrentIndex(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void EditorPanel::undo()
|
void EditorPanel::undo()
|
||||||
{
|
{
|
||||||
if (EditorTab *tab = currentTab())
|
if (EditorTab *tab = currentTab())
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
class EditorTab;
|
class EditorTab;
|
||||||
class Settings;
|
class Settings;
|
||||||
class SearchPanel;
|
class SearchPanel;
|
||||||
|
class FileSearchPanel;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// EditorPanel – Rechtes Panel: Tab-Leiste + Editoren + Such/Ersetzen-Panel.
|
// EditorPanel – Rechtes Panel: Tab-Leiste + Editoren + Such/Ersetzen-Panel.
|
||||||
@@ -25,7 +26,15 @@ public slots:
|
|||||||
void saveCurrentFile();
|
void saveCurrentFile();
|
||||||
void saveCurrentFileAs();
|
void saveCurrentFileAs();
|
||||||
void saveAllFiles();
|
void saveAllFiles();
|
||||||
|
void setSearchRoot(const QString &path);
|
||||||
void showSearchPanel();
|
void showSearchPanel();
|
||||||
|
void showFileSearchPanel();
|
||||||
|
void goToLine(const QString &filePath, int line);
|
||||||
|
|
||||||
|
// Session
|
||||||
|
QStringList openFilePaths() const;
|
||||||
|
QString activeFilePath() const;
|
||||||
|
void restoreSession(const QStringList &files, const QString &activeFile);
|
||||||
void undo();
|
void undo();
|
||||||
void redo();
|
void redo();
|
||||||
|
|
||||||
@@ -41,10 +50,11 @@ private:
|
|||||||
int findTabForFile(const QString &filePath) const;
|
int findTabForFile(const QString &filePath) const;
|
||||||
EditorTab *currentTab() const;
|
EditorTab *currentTab() const;
|
||||||
|
|
||||||
Settings *m_settings = nullptr;
|
Settings *m_settings = nullptr;
|
||||||
QVBoxLayout *m_layout = nullptr;
|
QVBoxLayout *m_layout = nullptr;
|
||||||
QTabWidget *m_tabWidget = nullptr;
|
QTabWidget *m_tabWidget = nullptr;
|
||||||
SearchPanel *m_searchPanel = nullptr;
|
SearchPanel *m_searchPanel = nullptr;
|
||||||
|
FileSearchPanel *m_fileSearch = nullptr;
|
||||||
|
|
||||||
QHash<QString, EditorTab *> m_openTabs;
|
QHash<QString, EditorTab *> m_openTabs;
|
||||||
};
|
};
|
||||||
|
|||||||
330
src/editor/FileSearchPanel.cpp
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
#include "FileSearchPanel.h"
|
||||||
|
|
||||||
|
#include <QDir>
|
||||||
|
#include <QDirIterator>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QTextStream>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QKeyEvent>
|
||||||
|
#include <QtConcurrent/QtConcurrent>
|
||||||
|
#include <QFuture>
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Konstruktor
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
FileSearchPanel::FileSearchPanel(QWidget *parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
{
|
||||||
|
setupUi();
|
||||||
|
hide();
|
||||||
|
|
||||||
|
m_watcher = new QFutureWatcher<QList<Match>>(this);
|
||||||
|
connect(m_watcher, &QFutureWatcher<QList<Match>>::finished,
|
||||||
|
this, &FileSearchPanel::onSearchFinished);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// UI
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
void FileSearchPanel::setupUi()
|
||||||
|
{
|
||||||
|
QVBoxLayout *root = new QVBoxLayout(this);
|
||||||
|
root->setContentsMargins(6, 4, 6, 4);
|
||||||
|
root->setSpacing(4);
|
||||||
|
|
||||||
|
// ---- Zeile 1: Suchbegriff ----
|
||||||
|
QHBoxLayout *row1 = new QHBoxLayout();
|
||||||
|
row1->addWidget(new QLabel(tr("Suchen in Dateien:"), this));
|
||||||
|
|
||||||
|
m_searchEdit = new QLineEdit(this);
|
||||||
|
m_searchEdit->setPlaceholderText(tr("Suchbegriff…"));
|
||||||
|
m_searchEdit->setClearButtonEnabled(true);
|
||||||
|
row1->addWidget(m_searchEdit, 1);
|
||||||
|
|
||||||
|
m_btnSearch = new QPushButton(tr("Suchen"), this);
|
||||||
|
m_btnSearch->setDefault(true);
|
||||||
|
row1->addWidget(m_btnSearch);
|
||||||
|
|
||||||
|
m_btnClose = new QPushButton(tr("✕"), this);
|
||||||
|
m_btnClose->setFixedWidth(24);
|
||||||
|
m_btnClose->setFlat(true);
|
||||||
|
m_btnClose->setToolTip(tr("Schließen"));
|
||||||
|
row1->addWidget(m_btnClose);
|
||||||
|
|
||||||
|
root->addLayout(row1);
|
||||||
|
|
||||||
|
// ---- Zeile 2: Optionen + Filter ----
|
||||||
|
QHBoxLayout *row2 = new QHBoxLayout();
|
||||||
|
m_chkCase = new QCheckBox(tr("Groß-/Kleinschreibung"), this);
|
||||||
|
m_chkWord = new QCheckBox(tr("Ganzes Wort"), this);
|
||||||
|
m_chkRegex = new QCheckBox(tr("Regex"), this);
|
||||||
|
|
||||||
|
row2->addWidget(m_chkCase);
|
||||||
|
row2->addWidget(m_chkWord);
|
||||||
|
row2->addWidget(m_chkRegex);
|
||||||
|
row2->addSpacing(12);
|
||||||
|
|
||||||
|
row2->addWidget(new QLabel(tr("Dateitypen:"), this));
|
||||||
|
m_filterEdit = new QLineEdit(this);
|
||||||
|
m_filterEdit->setText("*.html *.php *.css *.js *.c *.cpp *.h");
|
||||||
|
m_filterEdit->setFixedWidth(220);
|
||||||
|
m_filterEdit->setToolTip(tr("Leerzeichen-getrennte Muster, z.B.: *.php *.html"));
|
||||||
|
row2->addWidget(m_filterEdit);
|
||||||
|
row2->addStretch();
|
||||||
|
|
||||||
|
root->addLayout(row2);
|
||||||
|
|
||||||
|
// ---- Fortschritt + Status ----
|
||||||
|
m_progress = new QProgressBar(this);
|
||||||
|
m_progress->setRange(0, 0); // Unbestimmter Modus
|
||||||
|
m_progress->setFixedHeight(4);
|
||||||
|
m_progress->hide();
|
||||||
|
root->addWidget(m_progress);
|
||||||
|
|
||||||
|
m_statusLabel = new QLabel(this);
|
||||||
|
m_statusLabel->setStyleSheet("color: palette(mid);");
|
||||||
|
root->addWidget(m_statusLabel);
|
||||||
|
|
||||||
|
// ---- Ergebnisliste ----
|
||||||
|
m_results = new QTreeWidget(this);
|
||||||
|
m_results->setHeaderHidden(true);
|
||||||
|
m_results->setRootIsDecorated(true);
|
||||||
|
m_results->setIndentation(16);
|
||||||
|
m_results->setUniformRowHeights(true);
|
||||||
|
m_results->setAlternatingRowColors(true);
|
||||||
|
root->addWidget(m_results, 1);
|
||||||
|
|
||||||
|
// ---- Verbindungen ----
|
||||||
|
connect(m_btnSearch, &QPushButton::clicked, this, &FileSearchPanel::onSearch);
|
||||||
|
connect(m_searchEdit, &QLineEdit::returnPressed, this, &FileSearchPanel::onSearch);
|
||||||
|
connect(m_btnClose, &QPushButton::clicked, this, [this]()
|
||||||
|
{
|
||||||
|
hide();
|
||||||
|
});
|
||||||
|
connect(m_results, &QTreeWidget::itemActivated,
|
||||||
|
this, &FileSearchPanel::onResultActivated);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Öffentliche Schnittstelle
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
void FileSearchPanel::setSearchRoot(const QString &path)
|
||||||
|
{
|
||||||
|
m_searchRoot = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileSearchPanel::activate()
|
||||||
|
{
|
||||||
|
show();
|
||||||
|
m_searchEdit->setFocus();
|
||||||
|
m_searchEdit->selectAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Suche starten
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
void FileSearchPanel::onSearch()
|
||||||
|
{
|
||||||
|
const QString needle = m_searchEdit->text().trimmed();
|
||||||
|
if (needle.isEmpty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_searchRoot.isEmpty())
|
||||||
|
{
|
||||||
|
m_statusLabel->setText(tr("Kein Projektverzeichnis geöffnet."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Laufende Suche abbrechen
|
||||||
|
if (m_watcher->isRunning())
|
||||||
|
{
|
||||||
|
m_watcher->cancel();
|
||||||
|
m_watcher->waitForFinished();
|
||||||
|
}
|
||||||
|
|
||||||
|
m_results->clear();
|
||||||
|
m_statusLabel->setText(tr("Suche läuft…"));
|
||||||
|
m_progress->show();
|
||||||
|
m_btnSearch->setEnabled(false);
|
||||||
|
|
||||||
|
const QString root = m_searchRoot;
|
||||||
|
const bool cs = m_chkCase->isChecked();
|
||||||
|
const bool word = m_chkWord->isChecked();
|
||||||
|
const bool regex = m_chkRegex->isChecked();
|
||||||
|
const QStringList extensions = m_filterEdit->text().simplified().split(' ',
|
||||||
|
Qt::SkipEmptyParts);
|
||||||
|
|
||||||
|
QFuture<QList<Match>> future = QtConcurrent::run(
|
||||||
|
[this, root, needle, cs, word, regex, extensions]()
|
||||||
|
{
|
||||||
|
return searchInFiles(root, needle, cs, word, regex, extensions);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
m_watcher->setFuture(future);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Suchergebnisse anzeigen
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
void FileSearchPanel::onSearchFinished()
|
||||||
|
{
|
||||||
|
m_progress->hide();
|
||||||
|
m_btnSearch->setEnabled(true);
|
||||||
|
|
||||||
|
if (m_watcher->isCanceled())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QList<Match> matches = m_watcher->result();
|
||||||
|
|
||||||
|
// Ergebnisse gruppiert nach Datei aufbauen
|
||||||
|
QString currentFile;
|
||||||
|
QTreeWidgetItem *fileItem = nullptr;
|
||||||
|
int fileCount = 0;
|
||||||
|
int matchCount = 0;
|
||||||
|
|
||||||
|
for (const Match &m : matches)
|
||||||
|
{
|
||||||
|
if (m.filePath != currentFile)
|
||||||
|
{
|
||||||
|
currentFile = m.filePath;
|
||||||
|
++fileCount;
|
||||||
|
|
||||||
|
fileItem = new QTreeWidgetItem(m_results);
|
||||||
|
fileItem->setText(0, QFileInfo(m.filePath).fileName());
|
||||||
|
fileItem->setToolTip(0, m.filePath);
|
||||||
|
fileItem->setData(0, Qt::UserRole, m.filePath);
|
||||||
|
fileItem->setData(0, Qt::UserRole + 1, -1);
|
||||||
|
|
||||||
|
QFont boldFont = fileItem->font(0);
|
||||||
|
boldFont.setBold(true);
|
||||||
|
fileItem->setFont(0, boldFont);
|
||||||
|
fileItem->setExpanded(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
QTreeWidgetItem *lineItem = new QTreeWidgetItem(fileItem);
|
||||||
|
lineItem->setText(0, QString(" Zeile %1: %2")
|
||||||
|
.arg(m.line)
|
||||||
|
.arg(m.content.trimmed().left(120)));
|
||||||
|
lineItem->setToolTip(0, m.content.trimmed());
|
||||||
|
lineItem->setData(0, Qt::UserRole, m.filePath);
|
||||||
|
lineItem->setData(0, Qt::UserRole + 1, m.line);
|
||||||
|
|
||||||
|
++matchCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Datei-Titelzeilen um Trefferanzahl ergänzen
|
||||||
|
for (int i = 0; i < m_results->topLevelItemCount(); ++i)
|
||||||
|
{
|
||||||
|
QTreeWidgetItem *item = m_results->topLevelItem(i);
|
||||||
|
const int count = item->childCount();
|
||||||
|
item->setText(0, QString("%1 (%2 Treffer)")
|
||||||
|
.arg(QFileInfo(item->data(0, Qt::UserRole).toString()).fileName())
|
||||||
|
.arg(count));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchCount == 0)
|
||||||
|
{
|
||||||
|
m_statusLabel->setText(tr("Keine Treffer gefunden."));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_statusLabel->setText(tr("%1 Treffer in %2 Datei(en).")
|
||||||
|
.arg(matchCount)
|
||||||
|
.arg(fileCount));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Klick auf Treffer → Datei + Zeile öffnen
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
void FileSearchPanel::onResultActivated(QTreeWidgetItem *item, int /*column*/)
|
||||||
|
{
|
||||||
|
const QString path = item->data(0, Qt::UserRole).toString();
|
||||||
|
const int line = item->data(0, Qt::UserRole + 1).toInt();
|
||||||
|
|
||||||
|
if (path.isEmpty() || line < 0)
|
||||||
|
{
|
||||||
|
// Datei-Titelzeile: nur auf-/zuklappen
|
||||||
|
item->setExpanded(!item->isExpanded());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit fileLineRequested(path, line);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Eigentliche Suchroutine (läuft in Thread-Pool)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
QList<FileSearchPanel::Match> FileSearchPanel::searchInFiles(
|
||||||
|
const QString &root,
|
||||||
|
const QString &needle,
|
||||||
|
bool caseSensitive,
|
||||||
|
bool wholeWord,
|
||||||
|
bool useRegex,
|
||||||
|
const QStringList &extensions) const
|
||||||
|
{
|
||||||
|
QList<Match> results;
|
||||||
|
|
||||||
|
// Regulären Ausdruck vorbereiten
|
||||||
|
QString pattern = useRegex ? needle : QRegularExpression::escape(needle);
|
||||||
|
if (wholeWord)
|
||||||
|
{
|
||||||
|
pattern = "\\b" + pattern + "\\b";
|
||||||
|
}
|
||||||
|
|
||||||
|
QRegularExpression re(pattern,
|
||||||
|
caseSensitive
|
||||||
|
? QRegularExpression::NoPatternOption
|
||||||
|
: QRegularExpression::CaseInsensitiveOption);
|
||||||
|
|
||||||
|
if (!re.isValid())
|
||||||
|
{
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verzeichnis rekursiv durchsuchen
|
||||||
|
QDirIterator it(root,
|
||||||
|
extensions.isEmpty()
|
||||||
|
? QStringList("*")
|
||||||
|
: extensions,
|
||||||
|
QDir::Files,
|
||||||
|
QDirIterator::Subdirectories);
|
||||||
|
|
||||||
|
while (it.hasNext())
|
||||||
|
{
|
||||||
|
if (m_watcher->isCanceled())
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString filePath = it.next();
|
||||||
|
|
||||||
|
QFile file(filePath);
|
||||||
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTextStream stream(&file);
|
||||||
|
stream.setEncoding(QStringConverter::Utf8);
|
||||||
|
|
||||||
|
int lineNumber = 0;
|
||||||
|
while (!stream.atEnd())
|
||||||
|
{
|
||||||
|
++lineNumber;
|
||||||
|
const QString line = stream.readLine();
|
||||||
|
|
||||||
|
if (re.match(line).hasMatch())
|
||||||
|
{
|
||||||
|
results.append({ filePath, lineNumber, line });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
77
src/editor/FileSearchPanel.h
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QWidget>
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QCheckBox>
|
||||||
|
#include <QTreeWidget>
|
||||||
|
#include <QTreeWidgetItem>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QProgressBar>
|
||||||
|
#include <QFutureWatcher>
|
||||||
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// FileSearchPanel – Suche in allen Dateien eines Verzeichnisses.
|
||||||
|
//
|
||||||
|
// Ergebnisse werden als aufklappbare Liste angezeigt:
|
||||||
|
// Dateiname (N Treffer)
|
||||||
|
// └ Zeile 12: <Zeileninhalt>
|
||||||
|
// └ Zeile 34: <Zeileninhalt>
|
||||||
|
//
|
||||||
|
// Klick auf einen Treffer öffnet die Datei im Editor und springt zur Zeile.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
class FileSearchPanel : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit FileSearchPanel(QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
void setSearchRoot(const QString &path);
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void activate();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void fileLineRequested(const QString &filePath, int line);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onSearch();
|
||||||
|
void onResultActivated(QTreeWidgetItem *item, int column);
|
||||||
|
void onSearchFinished();
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Match
|
||||||
|
{
|
||||||
|
QString filePath;
|
||||||
|
int line;
|
||||||
|
QString content;
|
||||||
|
};
|
||||||
|
|
||||||
|
void setupUi();
|
||||||
|
QList<Match> searchInFiles(const QString &root,
|
||||||
|
const QString &needle,
|
||||||
|
bool caseSensitive,
|
||||||
|
bool wholeWord,
|
||||||
|
bool useRegex,
|
||||||
|
const QStringList &extensions) const;
|
||||||
|
|
||||||
|
QString m_searchRoot;
|
||||||
|
|
||||||
|
QLineEdit *m_searchEdit = nullptr;
|
||||||
|
QCheckBox *m_chkCase = nullptr;
|
||||||
|
QCheckBox *m_chkWord = nullptr;
|
||||||
|
QCheckBox *m_chkRegex = nullptr;
|
||||||
|
QLineEdit *m_filterEdit = nullptr; // Dateiendungen-Filter
|
||||||
|
QPushButton *m_btnSearch = nullptr;
|
||||||
|
QPushButton *m_btnClose = nullptr;
|
||||||
|
QLabel *m_statusLabel = nullptr;
|
||||||
|
QTreeWidget *m_results = nullptr;
|
||||||
|
QProgressBar *m_progress = nullptr;
|
||||||
|
|
||||||
|
QFutureWatcher<QList<Match>> *m_watcher = nullptr;
|
||||||
|
};
|
||||||
@@ -373,6 +373,62 @@ void PhpHighlighter::highlightPhpRange(const QString &text, int start, int lengt
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// /* */ Block-Kommentare innerhalb des PHP-Bereichs mehrzeilig behandeln.
|
||||||
|
// Block-Zustand 3 = wir sind mitten in einem /* ... */ PHP-Kommentar.
|
||||||
|
static const QRegularExpression blockOpen(R"(/\*)");
|
||||||
|
static const QRegularExpression blockClose(R"(\*/)");
|
||||||
|
|
||||||
|
int searchFrom = 0;
|
||||||
|
|
||||||
|
// Waren wir bereits in einem Block-Kommentar?
|
||||||
|
if (previousBlockState() == 3)
|
||||||
|
{
|
||||||
|
QRegularExpressionMatch closeMatch = blockClose.match(phpText, 0);
|
||||||
|
if (closeMatch.hasMatch())
|
||||||
|
{
|
||||||
|
const int end = static_cast<int>(closeMatch.capturedStart())
|
||||||
|
+ static_cast<int>(closeMatch.capturedLength());
|
||||||
|
setFormat(start, end, m_phpCommentFormat);
|
||||||
|
searchFrom = end;
|
||||||
|
// Block-Kommentar geschlossen — Zustand wird weiter unten gesetzt
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Gesamter Bereich ist noch Kommentar
|
||||||
|
setFormat(start, length, m_phpCommentFormat);
|
||||||
|
setCurrentBlockState(3);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Neue /* ... */ Kommentare innerhalb dieses PHP-Bereichs suchen
|
||||||
|
while (searchFrom < phpText.length())
|
||||||
|
{
|
||||||
|
QRegularExpressionMatch openMatch = blockOpen.match(phpText, searchFrom);
|
||||||
|
if (!openMatch.hasMatch())
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int openPos = static_cast<int>(openMatch.capturedStart());
|
||||||
|
QRegularExpressionMatch closeMatch = blockClose.match(phpText, openPos + 2);
|
||||||
|
|
||||||
|
if (closeMatch.hasMatch())
|
||||||
|
{
|
||||||
|
const int closeEnd = static_cast<int>(closeMatch.capturedStart())
|
||||||
|
+ static_cast<int>(closeMatch.capturedLength());
|
||||||
|
setFormat(start + openPos, closeEnd - openPos, m_phpCommentFormat);
|
||||||
|
searchFrom = closeEnd;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Kein schließendes */ gefunden — geht über Zeilenende
|
||||||
|
setFormat(start + openPos, length - openPos, m_phpCommentFormat);
|
||||||
|
setCurrentBlockState(3);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PhpHighlighter::highlightBlock(const QString &text)
|
void PhpHighlighter::highlightBlock(const QString &text)
|
||||||
@@ -380,8 +436,10 @@ void PhpHighlighter::highlightBlock(const QString &text)
|
|||||||
// Zuerst HTML-Basis-Regeln auf den gesamten Text anwenden
|
// Zuerst HTML-Basis-Regeln auf den gesamten Text anwenden
|
||||||
SyntaxHighlighter::highlightBlock(text);
|
SyntaxHighlighter::highlightBlock(text);
|
||||||
|
|
||||||
// Dann PHP-Blöcke <?php ... ?> und <?= ... ?> finden und überschreiben
|
// Block-Zustände:
|
||||||
// Block-Zustände: 0 = HTML, 2 = innerhalb PHP-Block
|
// 0 = HTML-Modus
|
||||||
|
// 2 = innerhalb PHP-Block (kein /* */ Kommentar)
|
||||||
|
// 3 = innerhalb PHP /* */ Block-Kommentar
|
||||||
setCurrentBlockState(0);
|
setCurrentBlockState(0);
|
||||||
|
|
||||||
static const QRegularExpression phpOpen(R"(<\?(?:php|=)?\s?)",
|
static const QRegularExpression phpOpen(R"(<\?(?:php|=)?\s?)",
|
||||||
@@ -390,9 +448,9 @@ void PhpHighlighter::highlightBlock(const QString &text)
|
|||||||
|
|
||||||
int pos = 0;
|
int pos = 0;
|
||||||
|
|
||||||
if (previousBlockState() == 2)
|
if (previousBlockState() == 2 || previousBlockState() == 3)
|
||||||
{
|
{
|
||||||
// Wir befinden uns bereits in einem PHP-Block
|
// Wir befinden uns bereits in einem PHP-Block (ggf. in einem Kommentar)
|
||||||
QRegularExpressionMatch closeMatch = phpClose.match(text, 0);
|
QRegularExpressionMatch closeMatch = phpClose.match(text, 0);
|
||||||
if (closeMatch.hasMatch())
|
if (closeMatch.hasMatch())
|
||||||
{
|
{
|
||||||
@@ -408,7 +466,12 @@ void PhpHighlighter::highlightBlock(const QString &text)
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
highlightPhpRange(text, 0, text.length());
|
highlightPhpRange(text, 0, text.length());
|
||||||
setCurrentBlockState(2);
|
// highlightPhpRange setzt den Zustand auf 3 falls nötig,
|
||||||
|
// sonst behalten wir 2 (offener PHP-Block ohne Kommentar)
|
||||||
|
if (currentBlockState() != 3)
|
||||||
|
{
|
||||||
|
setCurrentBlockState(2);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -424,7 +487,6 @@ void PhpHighlighter::highlightBlock(const QString &text)
|
|||||||
const int openStart = static_cast<int>(openMatch.capturedStart());
|
const int openStart = static_cast<int>(openMatch.capturedStart());
|
||||||
const int openEnd = openStart + static_cast<int>(openMatch.capturedLength());
|
const int openEnd = openStart + static_cast<int>(openMatch.capturedLength());
|
||||||
|
|
||||||
// <?php-Tag selbst einfärben
|
|
||||||
setFormat(openStart, static_cast<int>(openMatch.capturedLength()), m_phpTagFormat);
|
setFormat(openStart, static_cast<int>(openMatch.capturedLength()), m_phpTagFormat);
|
||||||
|
|
||||||
QRegularExpressionMatch closeMatch = phpClose.match(text, openEnd);
|
QRegularExpressionMatch closeMatch = phpClose.match(text, openEnd);
|
||||||
@@ -441,9 +503,11 @@ void PhpHighlighter::highlightBlock(const QString &text)
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// PHP-Block geht über Zeilenende hinaus
|
|
||||||
highlightPhpRange(text, openEnd, text.length() - openEnd);
|
highlightPhpRange(text, openEnd, text.length() - openEnd);
|
||||||
setCurrentBlockState(2);
|
if (currentBlockState() != 3)
|
||||||
|
{
|
||||||
|
setCurrentBlockState(2);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||