diff --git a/CMakeLists.txt b/CMakeLists.txt index 2fc126f..ac476f8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,7 +10,7 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) if(MSVC) - add_compile_options(/Zc:__cplusplus) + add_compile_options(/permissive- /Zc:__cplusplus) endif() find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core Widgets LinguistTools) @@ -19,21 +19,27 @@ find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Widgets LinguistTool set(TS_FILES src/QDiffX_en_150.ts) set(PROJECT_SOURCES - main.cpp + main.cpp + src/QDiffWidget.cpp + src/QDiffTextBrowser.cpp + src/QLineNumberArea.cpp +) +set(PROJECT_HEADERS + src/QDiffWidget.h + src/QDiffTextBrowser.h + src/QLineNumberArea.h ) set(QDIFFX_CORE_SOURCES - src/QDiffWidget.cpp - src/DMP/diff_match_patch.cpp - src/DTLAlgorithm.cpp - src/DMPAlgorithm.cpp - src/QAlgorithmRegistry.cpp - src/QAlgorithmManager.cpp - src/QAlgorithmException.cpp + src/DMP/diff_match_patch.cpp + src/DTLAlgorithm.cpp + src/DMPAlgorithm.cpp + src/QAlgorithmRegistry.cpp + src/QAlgorithmManager.cpp + src/QAlgorithmException.cpp ) set(QDIFFX_CORE_HEADERS - src/QDiffWidget.h src/DMP/diff_match_patch.h src/DTLAlgorithm.h src/DMPAlgorithm.h @@ -62,36 +68,37 @@ if(${QT_VERSION_MAJOR} GREATER_EQUAL 6) qt_add_executable(QDiffX MANUAL_FINALIZATION ${PROJECT_SOURCES} + ${PROJECT_HEADERS} ${TS_FILES} ) # Define target properties for Android with Qt 6 as: # set_property(TARGET QDiffX APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR # ${CMAKE_CURRENT_SOURCE_DIR}/android) # For more information, see https://doc.qt.io/qt-6/qt-add-executable.html#target-creation - target_link_libraries(QDiffX PRIVATE Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Widgets QDiffXCore) - target_include_directories(QDiffX PRIVATE ${CMAKE_SOURCE_DIR}/src) - qt_create_translation(QM_FILES ${CMAKE_SOURCE_DIR} ${TS_FILES}) +target_link_libraries(QDiffX PRIVATE Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Widgets QDiffXCore) +target_include_directories(QDiffX PRIVATE ${CMAKE_SOURCE_DIR}/src) +qt_create_translation(QM_FILES ${CMAKE_SOURCE_DIR} ${TS_FILES}) else() if(ANDROID) add_library(QDiffX SHARED ${PROJECT_SOURCES} ) -# Define properties for Android with Qt 5 after find_package() calls as: -# set(ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android") - else() - add_executable(QDiffX - ${PROJECT_SOURCES} - ) - endif() + # Define properties for Android with Qt 5 after find_package() calls as: + # set(ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android") +else() + add_executable(QDiffX + ${PROJECT_SOURCES} + ) +endif() - qt5_create_translation(QM_FILES ${CMAKE_SOURCE_DIR} ${TS_FILES}) +qt5_create_translation(QM_FILES ${CMAKE_SOURCE_DIR} ${TS_FILES}) endif() # Qt for iOS sets MACOSX_BUNDLE_GUI_IDENTIFIER automatically since Qt 6.1. # If you are developing for iOS or macOS you should consider setting an # explicit, fixed bundle identifier manually though. if(${QT_VERSION} VERSION_LESS 6.1.0) - set(BUNDLE_ID_OPTION MACOSX_BUNDLE_GUI_IDENTIFIER com.example.QDiffX) + set(BUNDLE_ID_OPTION MACOSX_BUNDLE_GUI_IDENTIFIER com.example.QDiffX) endif() set_target_properties(QDiffX PROPERTIES ${BUNDLE_ID_OPTION} diff --git a/main.cpp b/main.cpp index c09a468..1922159 100644 --- a/main.cpp +++ b/main.cpp @@ -18,6 +18,8 @@ int main(int argc, char *argv[]) } } QDiffX::QDiffWidget w; + w.setContent("sdnk\nlsdk\nf\n", "aknf\nakf\nlkfn\n"); + w.setGeometry(0,0,400,800); w.show(); return a.exec(); } diff --git a/src/DMP/diff_match_patch.h b/src/DMP/diff_match_patch.h index abcd069..58fbbef 100644 --- a/src/DMP/diff_match_patch.h +++ b/src/DMP/diff_match_patch.h @@ -255,7 +255,7 @@ class diff_match_patch { * encoded text2 and the List of unique strings. The zeroth element * of the List of unique strings is intentionally blank. */ - protected: + public: QList diff_linesToChars(const QString &text1, const QString &text2); // return elems 0 and 1 are QString, elem 2 is QStringList /** @@ -276,7 +276,7 @@ class diff_match_patch { * @param diffs LinkedList of Diff objects. * @param lineArray List of unique strings. */ - private: + public: void diff_charsToLines(QList &diffs, const QStringList &lineArray); /** diff --git a/src/DMPAlgorithm.cpp b/src/DMPAlgorithm.cpp index f22577e..1885937 100644 --- a/src/DMPAlgorithm.cpp +++ b/src/DMPAlgorithm.cpp @@ -1,4 +1,9 @@ #include "DMPAlgorithm.h" +#include +#include +#include +#include +#include namespace QDiffX{ @@ -36,24 +41,20 @@ QDiffX::QDiffResult QDiffX::DMPAlgorithm::calculateDiff(const QString &leftFile, dmpChanges = diffLineByLine(leftFile, rightFile); break; - case DiffMode::CharByChar: - dmpChanges = diffCharByChar(leftFile, rightFile); + case DiffMode::WordByWord: + dmpChanges = diffWordByWord(leftFile, rightFile); break; case DiffMode::Auto: default: - dmpChanges = m_dmp.diff_main(leftFile, rightFile); - m_dmp.diff_cleanupSemantic(dmpChanges); - m_dmp.diff_cleanupEfficiency(dmpChanges); + dmpChanges = diffLineByLine(leftFile, rightFile); + break; } QList changes = convertDiffList(dmpChanges); - // Calculate line numbers for the changes - calculateLineNumbers(changes, leftFile, rightFile); - result.setChanges(changes); result.setSuccess(true); @@ -61,7 +62,7 @@ QDiffX::QDiffResult QDiffX::DMPAlgorithm::calculateDiff(const QString &leftFile, QMap metadata; metadata["algorithm"] = "DMP"; metadata["mode"] = (mode == DiffMode::LineByLine) ? "line" : - (mode == DiffMode::CharByChar) ? "char" : "auto"; + (mode == DiffMode::WordByWord) ? "word" : "auto"; metadata["total_changes"] = changes.size(); result.setMetaData(metadata); @@ -72,28 +73,62 @@ QDiffX::QDiffResult QDiffX::DMPAlgorithm::calculateDiff(const QString &leftFile, return result; } -QList DMPAlgorithm::diffCharByChar(const QString &leftFile, const QString &rightFile) + + +QList DMPAlgorithm::diffWordByWord(const QString &leftFile, const QString &rightFile) { - // checkLines=false for pure character-by-character comparison - QList diffs = m_dmp.diff_main(leftFile, rightFile, false); + QString modifiedLeft = leftFile; + QString modifiedRight = rightFile; + + + modifiedLeft.replace("\n", "§NEWLINE§"); + modifiedRight.replace("\n", "§NEWLINE§"); + + // Replace spaces with newlines temporarily + modifiedLeft.replace(" ", "\n"); + modifiedRight.replace(" ", "\n"); + + QList result = m_dmp.diff_linesToChars(modifiedLeft, modifiedRight); + + QString chars1 = result[0].toString(); + QString chars2 = result[1].toString(); + QStringList lineArray = result[2].toStringList(); + qDebug()<< result[0].toString() << "\n" << result[1].toString() << "\n"<< result[2].toStringList(); ; + + QList diffs = m_dmp.diff_main(chars1, chars2); + m_dmp.diff_charsToLines(diffs, lineArray); + + + for (Diff &diff : diffs) { + diff.text.replace("\n", " "); + diff.text.replace("§NEWLINE§", "\n"); + qDebug() << diff.toString() << "\n"; + } - // Step 2: Apply cleanup for better results - m_dmp.diff_cleanupSemantic(diffs); - m_dmp.diff_cleanupEfficiency(diffs); return diffs; } + + QList DMPAlgorithm::diffLineByLine(const QString &leftFile, const QString &rightFile) { - // Use DMP's integrated line mode by setting checklines=true - QList diffs = m_dmp.diff_main(leftFile, rightFile, true); - // Apply cleanup for better results - m_dmp.diff_cleanupSemantic(diffs); - m_dmp.diff_cleanupEfficiency(diffs); + QList lineResult = m_dmp.diff_linesToChars(leftFile, rightFile); - return diffs; + // Extract the elements: + QString chars1 = lineResult[0].toString(); + QString chars2 = lineResult[1].toString(); + QStringList lineArray = lineResult[2].toStringList(); // line mapping + + // Diff the encoded strings + QList lineDiffs = m_dmp.diff_main(chars1, chars2); + + // Convert back to lines + m_dmp.diff_charsToLines(lineDiffs, lineArray); + + + return lineDiffs; } AlgorithmCapabilities DMPAlgorithm::getCapabilities() const @@ -102,9 +137,9 @@ AlgorithmCapabilities DMPAlgorithm::getCapabilities() const caps.supportsLargeFiles = false; caps.supportsUnicode = true; caps.supportsBinary = false; - caps.supportsLineByLine = true; // Has diff_lineMode() - caps.supportsCharByChar = true; // Default character-level precision - caps.supportsWordByWord = false; + caps.supportsLineByLine = true; + caps.supportsCharByChar = false; + caps.supportsWordByWord = true; caps.maxRecommendedSize = 1024 * 1024; caps.description = "Reimplemented Google diff-match-patch,deprecated Qt4 components Replaced and updated to modern C++, optimized performance"; @@ -182,12 +217,13 @@ QList DMPAlgorithm::convertDiffList(const QList &dmpDiffs) con { QList changes; int position =0; + int line = 1; for(auto &diff : dmpDiffs){ DiffChange change; change.operation = convertOperation(diff.operation) ; change.text = diff.text ; - change.lineNumber = -1; // will be calculated later using calculateLineNumbers() + change.lineNumber = line; // will be calculated later using calculateLineNumbers() change.position = position; changes.append(change); @@ -195,6 +231,7 @@ QList DMPAlgorithm::convertDiffList(const QList &dmpDiffs) con if (diff.operation != DELETE) { position += diff.text.length(); } + line++; } return changes; } diff --git a/src/DMPAlgorithm.h b/src/DMPAlgorithm.h index 4dc259d..74bab15 100644 --- a/src/DMPAlgorithm.h +++ b/src/DMPAlgorithm.h @@ -18,7 +18,7 @@ class DMPAlgorithm : public QDiffAlgorithm // QDiffAlgorithm interface Implementation QDiffResult calculateDiff(const QString &leftFile, const QString &rightFile, DiffMode = DiffMode::Auto) override; - QList diffCharByChar(const QString &leftFile, const QString &rightFile); + QList diffWordByWord(const QString &leftFile, const QString &rightFile); QList diffLineByLine(const QString &leftFile, const QString &rightFile); QString getName() const override { return "Diff-Match-Patch-GoogleAlgorithme-Modernized"; } @@ -37,10 +37,12 @@ class DMPAlgorithm : public QDiffAlgorithm bool isRecommendedFor(const QString &leftText, const QString &rightText) const override; double calculateSimilarity(const QList &changes, const QString &leftText, const QString &rightText) const; + private: DiffOperation convertOperation(Operation dmpOp) const; QList convertDiffList(const QList& dmpDiffs) const; void calculateLineNumbers(QList &changes,const QString &leftFile,const QString rightFile) const; + private: diff --git a/src/DTLAlgorithm.cpp b/src/DTLAlgorithm.cpp index 1782f52..733fae6 100644 --- a/src/DTLAlgorithm.cpp +++ b/src/DTLAlgorithm.cpp @@ -33,29 +33,15 @@ QDiffResult DTLAlgorithm::calculateDiff(const QString &leftFile, const QString & changes = diffLineByLine(leftFile, rightFile); break; - case DiffMode::CharByChar: - changes = diffCharByChar(leftFile, rightFile); - break; + case DiffMode::Auto: default: { - // Auto mode: choose based on file size and content - int totalSize = leftFile.length() + rightFile.length(); - QVariant threshold = getConfiguration().value(CONFIG_LARGE_FILE_THRESHOLD, 1024 * 1024); - - if (totalSize > threshold.toInt()) { - // Large files: use line-by-line for better performance - changes = diffLineByLine(leftFile, rightFile); - } else { - // Small files: use character-by-character for precision - changes = diffCharByChar(leftFile, rightFile); - } + changes = diffLineByLine(leftFile, rightFile); break; } } - // Calculate line numbers for the changes - calculateLineNumbers(changes, leftFile, rightFile); result.setChanges(changes); result.setSuccess(true); @@ -64,8 +50,7 @@ QDiffResult DTLAlgorithm::calculateDiff(const QString &leftFile, const QString & QMap metadata; metadata["algorithm"] = "DTL"; metadata["algorithm_name"] = getName(); - metadata["mode"] = (mode == DiffMode::LineByLine) ? "line" : - (mode == DiffMode::CharByChar) ? "char" : "auto"; + metadata["mode"] = (mode == DiffMode::LineByLine) ? "line" : "auto"; metadata["total_changes"] = changes.size(); result.setMetaData(metadata); @@ -95,19 +80,7 @@ QList DTLAlgorithm::diffLineByLine(const QString &leftFile, const QS return convertDTLSequence(dtlDiff); } -QList DTLAlgorithm::diffCharByChar(const QString &leftFile, const QString &rightFile) -{ - // Convert strings to character vectors for DTL - std::vector leftChars(leftFile.begin(), leftFile.end()); - std::vector rightChars(rightFile.begin(), rightFile.end()); - // Create DTL diff object and calculate differences - dtl::Diff dtlDiff(leftChars, rightChars); - dtlDiff.compose(); - - // Convert DTL result to QDiffX format - return convertDTLSequenceChar(dtlDiff); -} // ----------------------- Algorithm Info ------------------------- @@ -119,7 +92,7 @@ AlgorithmCapabilities DTLAlgorithm::getCapabilities() const caps.supportsUnicode = true; caps.supportsBinary = false; // Text-based algorithm caps.supportsLineByLine = true; // Primary strength - caps.supportsCharByChar = true; // Also supported + caps.supportsCharByChar = false; // Also supported caps.supportsWordByWord = false; // Not implemented caps.maxRecommendedSize = 10 * 1024 * 1024; // 10MB caps.description = "High-performance DTL (Diff Template Library) algorithm optimized for large files and line-based comparisons"; @@ -183,12 +156,13 @@ QList DTLAlgorithm::convertDTLSequence(const dtl::Diff &dtl QList changes; auto ses = dtlDiff.getSes(); int position = 0; + int line = 1; for (const auto &edit : ses.getSequence()) { DiffChange change; change.operation = convertDTLOperation(edit.second.type); change.text = edit.first; - change.lineNumber = -1; // Will be calculated later + change.lineNumber = line; // Will be calculated later change.position = position; changes.append(change); @@ -197,34 +171,13 @@ QList DTLAlgorithm::convertDTLSequence(const dtl::Diff &dtl if (edit.second.type != dtl::SES_DELETE) { position += edit.first.length(); } + line++; } return changes; } -QList DTLAlgorithm::convertDTLSequenceChar(const dtl::Diff &dtlDiff) const -{ - QList changes; - auto ses = dtlDiff.getSes(); - int position = 0; - - for (const auto &edit : ses.getSequence()) { - DiffChange change; - change.operation = convertDTLOperation(edit.second.type); - change.text = QString(edit.first); // Convert QChar to QString - change.lineNumber = -1; // Will be calculated later - change.position = position; - - changes.append(change); - // Update position (don't advance for deletions) - if (edit.second.type != dtl::SES_DELETE) { - position += change.text.length(); - } - } - - return changes; -} DiffOperation DTLAlgorithm::convertDTLOperation(dtl::edit_t dtlOp) const { @@ -243,17 +196,14 @@ DiffOperation DTLAlgorithm::convertDTLOperation(dtl::edit_t dtlOp) const void DTLAlgorithm::calculateLineNumbers(QList &changes, const QString &leftFile, const QString &rightFile) const { int leftLine = 1, rightLine = 1; + int line = 1; int leftPos = 0, rightPos = 0; for (auto &change : changes) { switch (change.operation) { case DiffOperation::Equal:{ - change.lineNumber = leftLine; - int lineJumps = change.text.count('\n'); - leftLine += lineJumps; - rightLine += lineJumps; - leftPos += change.text.length(); - rightPos += change.text.length(); + change.lineNumber = line; + line++ ; break;} case DiffOperation::Delete: diff --git a/src/DTLAlgorithm.h b/src/DTLAlgorithm.h index 580eb15..aac1f58 100644 --- a/src/DTLAlgorithm.h +++ b/src/DTLAlgorithm.h @@ -19,7 +19,6 @@ class DTLAlgorithm : public QDiffAlgorithm // diff methods QList diffLineByLine(const QString &leftFile, const QString &rightFile); - QList diffCharByChar(const QString &leftFile, const QString &rightFile); QString getName() const override { return "DTL-Diff-Template-Library-Algorithm"; } QString getDescription() const override { @@ -41,7 +40,6 @@ class DTLAlgorithm : public QDiffAlgorithm private: // DTL conversion helpers QList convertDTLSequence(const dtl::Diff &dtlDiff) const; - QList convertDTLSequenceChar(const dtl::Diff &dtlDiff) const; DiffOperation convertDTLOperation(dtl::edit_t dtlOp) const; void calculateLineNumbers(QList &changes, const QString &leftFile, const QString &rightFile) const; diff --git a/src/QAlgorithmManager.h b/src/QAlgorithmManager.h index 9ef9afc..9f83d16 100644 --- a/src/QAlgorithmManager.h +++ b/src/QAlgorithmManager.h @@ -3,12 +3,7 @@ #include "QAlgorithmRegistry.h" #include "QAlgorithmManagerError.h" #include -/* - TODO: --calculateDiffSync --calculateDiffWithAlgorithm --calculateDiff -*/ + namespace QDiffX { diff --git a/src/QDiffTextBrowser.cpp b/src/QDiffTextBrowser.cpp new file mode 100644 index 0000000..9dd3e4b --- /dev/null +++ b/src/QDiffTextBrowser.cpp @@ -0,0 +1,343 @@ +#include "QDiffTextBrowser.h" +#include +#include +#include +#include +#include +#include +#include + + +namespace QDiffX{ + +QDiffTextBrowser::QDiffTextBrowser(QWidget* parent) { + m_lineNumberArea = new QLineNumberArea(this); + + setLineWrapMode(QTextEdit::NoWrap); + setReadOnly(true); + + connect(this->verticalScrollBar(), &QScrollBar::valueChanged, + m_lineNumberArea, QOverload<>::of(&QWidget::update)); + connect(this->horizontalScrollBar(), &QScrollBar::valueChanged, + m_lineNumberArea, QOverload<>::of(&QWidget::update)); + connect(this->document(), &QTextDocument::blockCountChanged, + this, [this]() { m_lineNumberArea->update(); }); + +} + +QDiffTextBrowser::~QDiffTextBrowser() +{ + if(m_lineNumberArea) + delete m_lineNumberArea; +} + + + +int QDiffTextBrowser::lineNumberAreaWidth() const +{ + int lineCount = this->document()->blockCount(); + int lineDigitCount = std::to_string(lineCount).length() ; + int charWidth = fontMetrics().horizontalAdvance(QLatin1Char('9')); + + int padding = this->width() * LINE_NUMBER_AREA_PADDING_RATIO; + + return padding + charWidth * lineDigitCount; +} + +void QDiffTextBrowser::setDiffResult(const QDiffResult &result) +{ + m_diffResult = result; + m_lineOperations.clear(); + + if (!result.success()) { + setPlainText(tr("Error: %1").arg(result.errorMessage())); + return; + } + + QMap lineContent; + int maxLineNumber = -1; + + int offset = 0; + for (auto &change : result.changes()) { + if(!change.text.endsWith("\n")) + change.text += '\n'; + + if (change.lineNumber >= 0) { + lineContent[change.lineNumber] = change.text; + for(int i = 0 ; i < change.text.count("\n") ; i++){ + m_lineOperations[change.lineNumber + i + offset] = change.operation; + } + maxLineNumber = qMax(maxLineNumber, change.lineNumber + offset); + } + offset += change.text.count("\n") - 1; + } + + // Build the document content + QString content; + QStringList lines; + + // Create enough lines to accommodate the highest line number + for (int i = 1; i <= maxLineNumber; ++i) { + if (lineContent.contains(i)) { + lines.append(lineContent[i]); + } else { + lines.append(QString()); // Empty line + m_lineOperations[i + offset] = DiffOperation::Equal; + } + } + + for (const auto &change : result.changes()) { + if (change.lineNumber < 0) { + lines.append(change.text); + m_lineOperations[lines.size() - 1] = change.operation; + } + } + + content = lines.join(' '); + setPlainText(content); + + applyBlockSpacing(); + applyDiffHighlighting(); + + update(); +} + +void QDiffTextBrowser::applyDiffHighlighting() { + QTextCursor cursor(document()); + cursor.beginEditBlock(); + + QTextBlock block = document()->firstBlock(); + int blockNumber = 1; + + while (block.isValid()) { + if (m_lineOperations.contains(blockNumber)) { + QTextCharFormat format = getFormatForOperation(m_lineOperations[blockNumber]); + + cursor.setPosition(block.position()); + cursor.setPosition(block.position() + block.length() - 1, QTextCursor::KeepAnchor); + cursor.mergeCharFormat(format); + } + + block = block.next(); + blockNumber++; + } + + cursor.endEditBlock(); +} + +void QDiffTextBrowser::applyBlockSpacing() +{ + QTextCursor cursor(document()); + cursor.beginEditBlock(); + + QTextBlock block = document()->firstBlock(); + while (block.isValid()) { + QTextBlockFormat blockFormat; + blockFormat.setTopMargin(TEXT_TOP_BOTTOM_MARGIN); + blockFormat.setBottomMargin(TEXT_TOP_BOTTOM_MARGIN); + + QTextCursor blockCursor(block); + blockCursor.setBlockFormat(blockFormat); + + block = block.next(); + } + + cursor.endEditBlock(); +} + +QColor QDiffTextBrowser::getBackgroundColorForOperation(DiffOperation operation) const { + switch (operation) { + case DiffOperation::Insert: + return QColor(INSERT_BG_COLOR); + case DiffOperation::Delete: + return QColor(DELETE_BG_COLOR); + case DiffOperation::Replace: + return QColor(REPLACE_BG_COLOR); + case DiffOperation::Equal: + default: + return QColor(); + } +} + +QTextCharFormat QDiffTextBrowser::getFormatForOperation(DiffOperation operation) const { + QTextCharFormat format; + + switch (operation) { + case DiffOperation::Insert: + format.setForeground(QColor(0x155724)); // Dark green text + break; + case DiffOperation::Delete: + format.setForeground(QColor(0x721C24)); // Dark red text + break; + case DiffOperation::Replace: + format.setForeground(QColor(0x856404)); // Dark yellow text + break; + case DiffOperation::Equal: + default: + break; + } + + return format; +} + +void QDiffTextBrowser::resizeEvent(QResizeEvent *event) +{ + QTextBrowser::resizeEvent(event); + QRect cr = contentsRect(); + m_lineNumberArea->setGeometry(QRect(cr.left(), cr.top() + 1, lineNumberAreaWidth(), cr.height()-2)); + + QTextFrame *rootFrame = document()->rootFrame(); + QTextFrameFormat format = rootFrame->frameFormat(); + format.setLeftMargin(m_lineNumberArea->width() + TEXT_LEFT_MARGIN); + rootFrame->setFrameFormat(format); + adjustFontSize(); + + m_lineNumberArea->update(); +} + +void QDiffTextBrowser::scrollContentsBy(int dx, int dy) +{ + QTextBrowser::scrollContentsBy(dx, dy); + m_lineNumberArea->update(); +} + +void QDiffTextBrowser::paintEvent(QPaintEvent *event) +{ + + QPainter painter(viewport()); + + QTextBlock block = firstVisibleBlock(); + int blockNumber = block.blockNumber()+1; + + QPointF scrollOffset( + this->horizontalScrollBar()->value(), + this->verticalScrollBar()->value() + ); + + while (block.isValid()) { + QRectF blockRect = document()->documentLayout()->blockBoundingRect(block); + QPointF visualPos = blockRect.topLeft() - scrollOffset; + + // Calculate the visual rectangle for the full width + QRectF visualRect = QRectF( + 0, // Start from left edge of viewport + visualPos.y(), + viewport()->width() , // Full width of the viewport + blockRect.height() + TEXT_TOP_BOTTOM_MARGIN + ); + + // Only paint if the block is visible + if (visualRect.intersects(event->rect()) && visualRect.bottom() >= 0) { + if (m_lineOperations.contains(blockNumber )) { + QColor backgroundColor = getBackgroundColorForOperation(m_lineOperations[blockNumber]); + if (backgroundColor.isValid()) { + painter.fillRect(visualRect, backgroundColor); + } + } + } + + block = block.next(); + blockNumber++; + + + if (visualPos.y() > event->rect().bottom()) { + break; + } + } + + + QTextBrowser::paintEvent(event); +} + +void QDiffTextBrowser::paintLineNumberArea(QPaintEvent *event) +{ + QPainter painter(m_lineNumberArea); + painter.fillRect(event->rect(), QColor(LINE_NUMBER_BG_COLOR)); + + painter.setPen(QColor(LINE_NUMBER_BORDER_COLOR)); + painter.drawLine(m_lineNumberArea->width() - 1, event->rect().top(), + m_lineNumberArea->width() - 1, event->rect().bottom()); + + QTextBlock block = firstVisibleBlock(); + int blockNumber = block.blockNumber(); + + QPointF scrollOffset( + horizontalScrollBar()->value(), + verticalScrollBar()->value() + ); + + while (block.isValid()) { + QRectF blockRect = document()->documentLayout()->blockBoundingRect(block); + + QPointF visualPos = blockRect.topLeft() - scrollOffset; + + if (visualPos.y() > event->rect().bottom()) { + break; + } + + if (block.isVisible() && (visualPos.y() + blockRect.height()) >= event->rect().top()) { + QString number = QString::number(blockNumber + 1); + + QRectF drawRect( + 0, + visualPos.y(), + m_lineNumberArea->width() - m_lineNumberArea->width() * LINE_NUMBER_TEXT_WIDTH_RATIO, + blockRect.height() + ); + + painter.setPen(QColor(LINE_NUMBER_TEXT_COLOR)); + painter.drawText(drawRect, Qt::AlignRight | Qt::AlignVCenter, number); + } + + block = block.next(); + ++blockNumber; + } + +} + +void QDiffTextBrowser::adjustFontSize() +{ + int baseSize = BASE_FONT_SIZE; + int scaledSize = baseSize * height() / FONT_SCALE_DIVISOR; + scaledSize = qBound(MIN_FONT_SIZE, scaledSize, MAX_FONT_SIZE); + + QFont font = this->font(); + font.setPointSize(scaledSize); + setFont(font); +} + +QTextBlock QDiffTextBrowser::firstVisibleBlock() +{ + QTextBlock block = document()->firstBlock() ; + QPointF scrollOffset( + this->horizontalScrollBar()->value(), + this->verticalScrollBar()->value() + ); + + while(block.isValid()){ + QRectF blockRect = this->document()->documentLayout()->blockBoundingRect(block); + QPointF visualPos = blockRect.topLeft() - scrollOffset; + + if (visualPos.y() + blockRect.height() >= 0) { + break; + } + + block = block.next(); + } + return block; +} + +qreal QDiffTextBrowser::blockTop(const QTextBlock &block) +{ + QRectF blockRect = document()->documentLayout()->blockBoundingRect(block); + return blockRect.top() - verticalScrollBar()->value(); +} + +qreal QDiffTextBrowser::blockBottom(const QTextBlock &block) +{ + QRectF blockRect = document()->documentLayout()->blockBoundingRect(block); + return blockRect.bottom() - verticalScrollBar()->value(); +} + + +}//namespace QDiffX diff --git a/src/QDiffTextBrowser.h b/src/QDiffTextBrowser.h new file mode 100644 index 0000000..d6f2390 --- /dev/null +++ b/src/QDiffTextBrowser.h @@ -0,0 +1,72 @@ +#pragma once + +#include +#include +#include "QLineNumberArea.h" +#include "QDiffAlgorithm.h" + +namespace QDiffX{ + +class QLineNumberArea; + +class QDiffTextBrowser : public QTextBrowser +{ + Q_OBJECT +public: + explicit QDiffTextBrowser(QWidget* parent = nullptr); + ~QDiffTextBrowser(); + + int lineNumberAreaWidth() const; + void setDiffResult(const QDiffResult& result); + + void paintLineNumberArea(QPaintEvent* event); + void applyDiffHighlighting(); + void applyBlockSpacing(); + + + + QTextCharFormat getFormatForOperation(DiffOperation operation) const; + QColor getBackgroundColorForOperation(DiffOperation operation) const; + + // Line Number Area + static constexpr double LINE_NUMBER_AREA_PADDING_RATIO = 0.07; + static constexpr double LINE_NUMBER_TEXT_WIDTH_RATIO = 0.3; + + //Text + static constexpr int MIN_FONT_SIZE = 14; + static constexpr int MAX_FONT_SIZE = 36; + static constexpr int BASE_FONT_SIZE = 18; + static constexpr int FONT_SCALE_DIVISOR = 400; + static constexpr int TEXT_LEFT_MARGIN = 25; + static constexpr int TEXT_TOP_BOTTOM_MARGIN = 8; + + //Colors + static constexpr uint32_t LINE_NUMBER_BG_COLOR = 0xFFFEFC; + static constexpr uint32_t LINE_NUMBER_BORDER_COLOR = 0xDDDDDD; + static constexpr uint32_t LINE_NUMBER_TEXT_COLOR = 0x999999; + static constexpr uint32_t INSERT_BG_COLOR = 0xD4EDDA; + static constexpr uint32_t DELETE_BG_COLOR = 0xF8D7DA; + static constexpr uint32_t REPLACE_BG_COLOR = 0xFFF3CD; + static constexpr uint32_t INSERT_TEXT_COLOR = 0x155724; + static constexpr uint32_t DELETE_TEXT_COLOR = 0x721C24; + static constexpr uint32_t REPLACE_TEXT_COLOR = 0x856404; + + +protected: + void resizeEvent(QResizeEvent* event) override; + void scrollContentsBy(int dx, int dy) override; + void paintEvent(QPaintEvent* event) override; + +private: + void adjustFontSize(); + //Helpers + QTextBlock firstVisibleBlock(); + qreal blockTop(const QTextBlock& block); + qreal blockBottom(const QTextBlock& block); +private: + QLineNumberArea* m_lineNumberArea; + QDiffResult m_diffResult; + QMap m_lineOperations; +}; + +}// namespace QDiffX diff --git a/src/QDiffWidget.cpp b/src/QDiffWidget.cpp index 6893474..0d5a775 100644 --- a/src/QDiffWidget.cpp +++ b/src/QDiffWidget.cpp @@ -24,16 +24,18 @@ void QDiffWidget::setupUI() { QVBoxLayout *mainLayout = new QVBoxLayout(this); - QHBoxLayout *headerLayout = new QHBoxLayout(this); - QLabel *leftLabel = new QLabel(m_leftLabel, this); - QLabel *rightLabel = new QLabel(m_rightLabel, this); + QHBoxLayout *headerLayout = new QHBoxLayout(); + QLabel *leftLabel = new QLabel(m_leftLabel); + QLabel *rightLabel = new QLabel(m_rightLabel); headerLayout->addWidget(leftLabel); headerLayout->addWidget(rightLabel); mainLayout->addLayout(headerLayout); - m_splitter = new QSplitter(Qt::Horizontal, this); - m_leftTextBrowser = new QTextBrowser(m_splitter); - m_rightTextBrowser = new QTextBrowser(m_splitter); + m_splitter = new QSplitter(Qt::Horizontal); + m_leftTextBrowser = new QDiffX::QDiffTextBrowser(); + m_rightTextBrowser = new QDiffX::QDiffTextBrowser(); + m_splitter->addWidget(m_leftTextBrowser); + m_splitter->addWidget(m_rightTextBrowser); mainLayout->addWidget(m_splitter); } diff --git a/src/QDiffWidget.h b/src/QDiffWidget.h index 9ab6d4e..e049612 100644 --- a/src/QDiffWidget.h +++ b/src/QDiffWidget.h @@ -2,6 +2,7 @@ #include #include +#include #include namespace QDiffX { @@ -71,8 +72,8 @@ class QDiffWidget : public QWidget private: QSplitter *m_splitter; - QTextBrowser *m_leftTextBrowser; - QTextBrowser *m_rightTextBrowser; + QDiffTextBrowser *m_leftTextBrowser; + QDiffTextBrowser *m_rightTextBrowser; QString m_leftContent; QString m_rightContent; diff --git a/src/QLineNumberArea.cpp b/src/QLineNumberArea.cpp new file mode 100644 index 0000000..fd876bc --- /dev/null +++ b/src/QLineNumberArea.cpp @@ -0,0 +1,21 @@ +#include "QLineNumberArea.h" +#include "QDiffTextBrowser.h" +#include +#include + +namespace QDiffX{ + + +QLineNumberArea::QLineNumberArea(QDiffTextBrowser *editor) + : QWidget(editor) , m_editor(editor) +{} + +QSize QLineNumberArea::sizeHint() const { + return QSize(m_editor->lineNumberAreaWidth(), 0); +} + +void QLineNumberArea::paintEvent(QPaintEvent* event) { + m_editor->paintLineNumberArea(event); +} + +}//namespace QDiffX diff --git a/src/QLineNumberArea.h b/src/QLineNumberArea.h new file mode 100644 index 0000000..248fd06 --- /dev/null +++ b/src/QLineNumberArea.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include "QDiffTextBrowser.h" + +namespace QDiffX{ + +class QDiffTextBrowser; + +class QLineNumberArea : public QWidget +{ + Q_OBJECT +public: + explicit QLineNumberArea(QDiffTextBrowser* editor); + QSize sizeHint() const override; +protected: + void paintEvent(QPaintEvent* event) override; +private: + QDiffTextBrowser* m_editor; +}; + + +}//namespace QDiffX