[QT]Rich Text Editor(2)

👉아래는 QT6버전 프로그램 소스 코드 입니다.
Below is the QT6 version program source code.

👉약간의 기능이 더 추가 되어 있습니다.
There are a few more features added.

1.main.cpp

#include "mainwindow.h"

#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();
    return a.exec();
}
/*
 * fontAwesome : 폰트다운로드 / fontdownload
 * https://fontawesome.com/search?q=pdf&o=r
 *
 *
 *
*/

2.mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QTextCharFormat> // QTextCharFormat 사용을 위해 포함 / Included for use with QTextCharFormat
#include <QFileInfo>       // QFileInfo 사용을 위해 포함 / Included for using QFileInfo
// 드래그 이미지 / drag image
#include <QPoint>          // 마우스 드래그 시작 위치 저장을 위해 추가 / Added to save the mouse drag start position

QT_BEGIN_NAMESPACE
namespace Ui {
class MainWindow;
}
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

    // QObject의 이벤트 필터 함수를 오버라이드합니다. / Override QObject's event filter function.
    bool eventFilter(QObject *obj, QEvent *event) override;

private slots:

    // == 파일 관련 슬롯 / File-related slots ==
    bool saveFile(const QString &path);
    void save(); // Save
    void saveAs(); // Save As
    void actionNew();  // 새 파일 / new file
    void actionOpen(); // 파일 열기 / open
    void actionExit(); // 종료 / Exit

    // == 편집 / Edit ==
    void actionFind();     // 찾기 기능 / find
    void actionReplace();  // 바꾸기 기능 / replace

    // == 뷰 ==
    void actionZoomIn();  // 줌인 / zoom in
    void actionZoomOut(); // 줌아웃 / zoom out
    void actionZoomReset(); // 줌 초기화 / zoom reset

    // == 이미지 / Image ==
    void actionInsertImage(); // 이미지 삽입 / insert image

    // == Export ==
    void actionExportBase64(); // Export As Base64
    void actionExportBase64WithImages(); // Export As Base64 for image
    void actionExportPdf(); //Export As Pdf

    // == Import ==
    void actionImportBase64(); // Import As Base64

    // == 텍스트 기능 / Text function ==
    void actionBold(bool checked);
    void actionItalic(bool checked);
    void actionUnderline(bool checked);
    void actionStrike(bool checked);

    // -- 자동연결설정(이전에 사용하던 방식으로 경고 발생) --
    // -- Automatic connection setup (warning occurs in the previous method) --
    // void on_actionColor_triggered();
    // void on_actionFont_triggered();

    // -- 수동연결 --
    void actionColor(); // 색상 선택은 토글이 아니므로 bool 인자가 없음 / Color selection is not a toggle, so there is no bool argument.
    void actionFont();  // 폰트 선택은 토글이 아니므로 bool 인자가 없음 / Font selection is not a toggle, so there is no bool argument.

    // 현재 커서 위치의 서식 변경을 감지하는 슬롯
    // Slot to detect format changes at the current cursor position
    void updateFormat(const QTextCharFormat &format);


    // == 도움말 기능 / help ==
    void actionHelp();  // 도움말 / help
    void actionAbout(); // 정보 / about


private:
    Ui::MainWindow *ui;
    QString currentFilePath; // 현재 파일을 추적하는 변수 추가 / Add a variable to track the current file

    // == 이미지 리사이즈를 위한 멤버 변수 / Member variables for image resizing ==
    bool m_isResizing = false;
    QPoint m_dragStartPos;
    int m_originalImageWidth = 0;
    int m_originalImageHeight = 0;


    // == 줌인/줌아웃 설정을 위한 멤버 변수 / Member variables for zoom in/zoom out settings ==
    // 폰트 크기 초기화용 변수 추가(변화하는 현재상태 저장)
    // Add a variable to initialize the font size (save the current state as it changes)
    int m_currentFontSize = 10;

    // 기본 폰트 크기를 저장하는 멤버 변수(리셋할때의 값)
    // Member variable that stores the default font size (value when reset)
    int m_defaultPointSize = 10; // 초기 기본값 설정 / Set factory defaults

};

#endif // MAINWINDOW_H

3.mainwindow.cpp

#include "mainwindow.h"
#include "./ui_mainwindow.h"

// 코드 추가 / Add code
#include <QFileDialog>
#include <QFile>
#include <QTextStream>
#include <QMessageBox>
#include <QTextCharFormat>
#include <QColorDialog> // 색상 선택 다이얼로그 / Color selection dialog
#include <QFontDialog>  // 폰트 선택 다이얼로그 / Font selection dialog

#include <QDir>         // QDir 사용을 위해 추가 / Added for QDir use
#include <QFileInfo>    // QFileInfo 사용을 위해 추가 / Added for using QFileInfo
#include <QUrl>         // 이미지 파일 경로를 URL로 변환하기 위해 필요 / Needed to convert image file path to URL
#include <QImage>       // 이미지 로드를 위해 필요 / Required for image loading
#include <QTextDocument> // QTextDocument::addResource 사용을 위해 필요 / Required for using QTextDocument::addResource
#include <QTextImageFormat> // 이미지 삽입 포맷 지정을 위해 필요 / Required to specify image insertion format
#include <QTextCursor>
#include <QInputDialog> // 사용자 입력 다이얼로그를 위해 추가 / Added for user input dialog
#include <QMouseEvent>  // 마우스 이벤트 처리를 위해 추가 / Added for mouse event handling
#include <QtMath>       // qMax, qMin 등을 위해 필요 (여기서는 qMax 사용) / Required for qMax, qMin, etc. (qMax is used here)

// export as base64
#include <QByteArray>
#include <QClipboard>
#include <QApplication>

// export base64 with images
#include <QRegularExpression>
#include <QRegularExpressionMatchIterator>
#include <QBuffer>

// pdf
#include <QPrinter>
#include <QFileDialog>
#include <QMessageBox>
#include <QPdfWriter>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    // 메인 윈도우의 중앙 영역에 textEdit 위젯을 배치
    // Place the textEdit widget in the center area of ​​the main window
    setCentralWidget(ui->textEdit);

    // == 파일 기능 / File functon ==
    connect(ui->actionSave, &QAction::triggered, this, &MainWindow::save);  // save
    connect(ui->actionSave_As, &QAction::triggered, this, &MainWindow::saveAs); // saveas
    connect(ui->actionExit, &QAction::triggered, this, &MainWindow::actionExit); // exit
    connect(ui->actionNew, &QAction::triggered, this, &MainWindow::actionNew); // new
    connect(ui->actionOpen, &QAction::triggered, this, &MainWindow::actionOpen);// open
    connect(ui->actionExport_As_Base64, &QAction::triggered, this, &MainWindow::actionExportBase64); //save as base64
    connect(ui->actionImport_Base64, &QAction::triggered, this, &MainWindow::actionImportBase64); //Import base64
    connect(ui->actionExport_Base64_With_Images, &QAction::triggered, this, &MainWindow::actionExportBase64WithImages); //Import base64
    connect(ui->actionExport_As_Pdf, &QAction::triggered, this, &MainWindow::actionExportPdf); //Export As Pdf


    // == 편집 기능 구현 / Implementing editing functions ==
    connect(ui->actionCopy, &QAction::triggered, ui->textEdit, &QTextEdit::copy); //copy
    connect(ui->actionCut, &QAction::triggered, ui->textEdit, &QTextEdit::cut); // cut
    connect(ui->actionPaste, &QAction::triggered, ui->textEdit, &QTextEdit::paste); // past
    connect(ui->actionSelect_All, &QAction::triggered, ui->textEdit, &QTextEdit::selectAll); // select all
    connect(ui->actionUndo, &QAction::triggered, ui->textEdit, &QTextEdit::undo); // undo
    connect(ui->actionRedo, &QAction::triggered, ui->textEdit, &QTextEdit::redo); // redo

    // == 뷰 / View ==
    connect(ui->actionZoom_in, &QAction::triggered, this, &MainWindow::actionZoomIn);
    connect(ui->actionZoom_out, &QAction::triggered, this, &MainWindow::actionZoomOut);
    connect(ui->actionZoom_reset, &QAction::triggered, this, &MainWindow::actionZoomReset);

    // 사용자 정의 슬롯 / custom slots
    connect(ui->actionFind, &QAction::triggered, this, &MainWindow::actionFind); // find
    connect(ui->actionReplace, &QAction::triggered, this, &MainWindow::actionReplace); // replace




    // == 텍스트 / Text ==

    // 볼드체 / Bold
    // -- actionBold를 굵게/얇게 상태를 유지하는 '토글' 버튼으로 설정
    // -- Set actionBold to a 'toggle' button that maintains the bold/thin state.
    ui->actionBold->setCheckable(true);

    // -- 툴바 액션 클릭 시 서식을 적용하도록 연결
    // -- Connect to apply formatting when clicking a toolbar action
    connect(ui->actionBold, &QAction::triggered, this, &MainWindow::actionBold);

    // 이탤릭체 / Italic
    ui->actionItalic->setCheckable(true);
    connect(ui->actionItalic, &QAction::triggered, this, &MainWindow::actionItalic);

    // 밑줄 / Underline
    ui->actionUnderline->setCheckable(true);
    connect(ui->actionUnderline, &QAction::triggered, this, &MainWindow::actionUnderline);

    // 취소선 / strikethrough
    ui->actionStrike->setCheckable(true);
    connect(ui->actionStrike, &QAction::triggered, this, &MainWindow::actionStrike);

    // 글꼴 색상 / font color
    connect(ui->actionColor, &QAction::triggered, this, &MainWindow::actionColor);

    // 글꼴 선택 / Select font
    connect(ui->actionFont, &QAction::triggered, this, &MainWindow::actionFont);

    // 이미지 이미지 드래그 리사이즈 /  Image Image Drag Resize
    ui->textEdit->viewport()->installEventFilter(this);




    // == 이미지 삽입 / Insert image ==
    connect(ui->actionInsert_image, &QAction::triggered, this, &MainWindow::actionInsertImage);

    // -- 텍스트 커서의 서식이 바뀔 때마다 툴바 상태를 업데이트하도록 연결
    // -- Connect to update the toolbar state whenever the text cursor's format changes.
    connect(ui->textEdit, &QTextEdit::currentCharFormatChanged,
            this, &MainWindow::updateFormat);


    // == 도움말 기능 / Help ==
    // Help
    connect(ui->actionHelp, &QAction::triggered, this, &MainWindow::actionHelp);

    // About
    connect(ui->actionAbout, &QAction::triggered, this, &MainWindow::actionAbout);
}


// == 슬롯 / Slot ==

// Export As PDF
void MainWindow::actionExportPdf()
{
    // 저장할 PDF 파일 경로 선택 / Select the path to save the PDF file
    QString filePath = QFileDialog::getSaveFileName(this,
                                                    tr("PDF로 내보내기/Export pdf"),
                                                    QDir::homePath() + "/export.pdf",
                                                    tr("PDF 파일/PDF file (*.pdf)"));

    if (filePath.isEmpty())
        return;

    // QPrinter 객체를 PDF 모드로 설정
    // Set the QPrinter object to PDF mode
    QPrinter printer(QPrinter::HighResolution);
    printer.setOutputFormat(QPrinter::PdfFormat);
    printer.setOutputFileName(filePath);

    // 페이지 여백 및 용지 설정
    // Page margins and paper settings
    printer.setPageMargins(QMarginsF(15, 15, 15, 15));
    printer.setPageSize(QPageSize(QPageSize::A4));

    // QTextEdit의 문서를 프린터로 출력
    ui->textEdit->document()->print(&printer);

    QMessageBox::information(this, tr("완료/comolete"), tr("PDF로 성공적으로 내보냈습니다:\n%1").arg(filePath));
}

// Export base64 with images
void MainWindow::actionExportBase64WithImages()
{
    QTextDocument *doc = ui->textEdit->document();
    QString html = doc->toHtml();

    // 이미지 경로를 Base64 데이터 URI로 변환
    // Convert image path to Base64 data URI
    static const QRegularExpression imgRegex(R"###(<img[^>]*src="([^"]+)"[^>]*>)###");
    QRegularExpressionMatchIterator it = imgRegex.globalMatch(html);

    while (it.hasNext()) {
        QRegularExpressionMatch match = it.next();
        QString imgTag = match.captured(0);
        QString imgPath = match.captured(1);

        if (imgPath.startsWith("data:image"))
            continue;

        QImage image(QUrl(imgPath).toLocalFile());
        if (image.isNull())
            continue;

        QByteArray ba;
        QBuffer buffer(&ba);
        buffer.open(QIODevice::WriteOnly);
        image.save(&buffer, "PNG");

        QString base64Data = QString("data:image/png;base64,%1")
                                 .arg(QString::fromLatin1(ba.toBase64()));

        QString newTag = imgTag;
        newTag.replace(imgPath, base64Data);
        html.replace(imgTag, newTag);
    }


    // 최종 HTML을 Base64로 인코딩
    // Encode the final HTML to Base64
    QByteArray finalBase64 = html.toUtf8().toBase64();

    // 파일 저장 대화 상자 / Save file dialog box
    QString filePath = QFileDialog::getSaveFileName(this, tr("Export as Base64 HTML"),
                                                    QDir::homePath() + "/export_with_images.txt",
                                                    tr("텍스트 파일/text file (*.txt);;모든 파일 (*)"));
    if (filePath.isEmpty())
        return;

    // 파일 저장 / Save file
    QFile file(filePath);
    if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
        QTextStream out(&file);
        out << finalBase64;
        file.close();
        QMessageBox::information(this, tr("완료/Complete"), tr("이미지를 포함한 Base64 HTML로 내보냈습니다."));
    }
}


// base64 Import
void MainWindow::actionImportBase64()
{
    // 파일 열기 대화 상자 / Open file dialog box
    QString filePath = QFileDialog::getOpenFileName(
        this, tr("Base64 파일 열기"),
        QDir::homePath(),
        tr("텍스트 파일 (*.txt);;모든 파일 (*)"));

    if (filePath.isEmpty())
        return;

    // 파일 읽기 / read file
    QFile file(filePath);
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
        return;

    QByteArray base64Data = file.readAll();
    // Base64 디코딩 / Base64 Encoding
    QByteArray decoded = QByteArray::fromBase64(base64Data);
    QString html = QString::fromUtf8(decoded);


    // 문서를 완전히 새로 생성하여 기존 상태 리셋 / Reset the previous state by creating a completely new document
    QTextDocument *newDoc = new QTextDocument(this);
    ui->textEdit->setDocument(newDoc);

    // HTML 로드 / HTML Load
    ui->textEdit->setHtml(html);

    QMessageBox::information(this, tr("완료/Complete"), tr("Base64 문서를 불러왔습니다."));
}


// base 64로 저장하기 / Save as base 64
void MainWindow::actionExportBase64()
{
    // QTextEdit의 내용을 HTML로 가져옴 / Get the contents of QTextEdit as HTML
    QString htmlContent = ui->textEdit->toHtml();

    // HTML 문자열을 Base64로 인코딩 / Encode HTML string to Base64
    QByteArray base64Data = htmlContent.toUtf8().toBase64();

    // 사용자에게 저장 또는 클립보드 복사 선택지 제공 / Give users the option to save or copy to clipboard
    QStringList options = {tr("파일로 저장"), tr("클립보드에 복사"), tr("취소")};
    bool ok;
    QString choice = QInputDialog::getItem(this, tr("Base64 내보내기"),
                                           tr("작업 선택:"), options, 0, false, &ok);
    if (!ok || choice == tr("취소"))
        return;

    if (choice == tr("파일로 저장")) {
        QString filePath = QFileDialog::getSaveFileName(this,
                                                        tr("Base64 파일로 내보내기"),
                                                        QDir::homePath() + "/export_base64.txt",
                                                        tr("텍스트 파일 (*.txt);;모든 파일 (*)"));

        if (!filePath.isEmpty()) {
            QFile file(filePath);
            if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
                QTextStream out(&file);
                out << base64Data;
                file.close();
                QMessageBox::information(this, tr("완료/Complete"), tr("Base64 데이터가 파일로 저장되었습니다."));
            } else {
                QMessageBox::critical(this, tr("오류/Error"), tr("파일을 저장할 수 없습니다:\n") + file.errorString());
            }
        }
    } else if (choice == tr("클립보드에 복사/Copy to clipboard")) {
        QClipboard *clipboard = QApplication::clipboard();
        clipboard->setText(QString::fromUtf8(base64Data));
        QMessageBox::information(this, tr("완료/Complete"), tr("Base64 데이터가 클립보드에 복사되었습니다."));
    }
}


// HTML저장 / Save html
bool MainWindow::saveFile(const QString &path)
{
    QFile file(path);
    // QTextEdit의 내용을 가져옵니다.
    //QString content = ui->textEdit->toPlainText(); // 일반 텍스트 저장 예시 / Plain text storage example
    // 만약 리치 텍스트(HTML)로 저장하려면:
     QString content = ui->textEdit->toHtml();

    if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
        QTextStream out(&file);
        out << content;
        file.close();
        currentFilePath = path; // 저장 성공 시 경로 업데이트 / Update path when save is successful
        setWindowTitle(QFileInfo(path).fileName() + " - Editor"); // 창 제목 변경 / Change window title
        return true;
    } else {
        QMessageBox::critical(this, tr("오류/Error"), tr("파일을 저장할 수 없습니다:\n") + file.errorString());
        return false;
    }
}


// 다른 이름으로 저장하기 / Save As
void MainWindow::saveAs()
{
    // QFileDialog를 사용하여 사용자에게 파일 경로를 묻습니다.
    // Use QFileDialog to ask the user for a file path.
    QString filePath = QFileDialog::getSaveFileName(this,
                                                    tr("문서 저장/Save Document"),
                                                    currentFilePath.isEmpty() ? QDir::homePath() : currentFilePath,
                                                    tr("텍스트 파일 (*.txt);;HTML 파일 (*.html);;모든 파일 (*)"));

    if (!filePath.isEmpty()) {
        saveFile(filePath);
    }
}

// 저장하기 / Save
void MainWindow::save()
{
    if (currentFilePath.isEmpty()) {
        // 경로가 없으면 'Save As'를 호출합니다. / If there is no path, call 'Save As'.
        saveAs();
    } else {
        // 경로가 있으면 기존 파일에 덮어씁니다. / If the path exists, it will overwrite the existing file.
        saveFile(currentFilePath);
    }
}

// 종료하기 슬롯 / Exit slot
void MainWindow::actionExit()
{
    close(); // 메인 윈도우를 닫고 애플리케이션을 종료합니다. / Close the main window and exit the application.
}

// 새 파일 생성 슬롯 / New file creation slot
void MainWindow::actionNew()
{
    ui->textEdit->clear();
    currentFilePath.clear();
    setWindowTitle(tr("Untitled - Editor"));
}

// 파일 열기 슬롯 / file open slot
void MainWindow::actionOpen()
{
    QString filePath = QFileDialog::getOpenFileName(this,
                                                    tr("문서 열기/Open"),
                                                    QDir::homePath(),
                                                    tr("리치 텍스트 파일 (*.html *.htm);;텍스트 파일 (*.txt);;이미지 파일 (*.png *.jpg *.jpeg *.gif);;모든 파일 (*)"));
    if (!filePath.isEmpty()) {
        QFile file(filePath);
        if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
            QString content = file.readAll();

            if (filePath.endsWith(".html", Qt::CaseInsensitive) || filePath.endsWith(".htm", Qt::CaseInsensitive)) {
                ui->textEdit->setHtml(content); // HTML 형식으로 로드 / Load in HTML format
            }
            // 이미지 파일을 바로 열면 텍스트 편집기에 삽입 / Open the image file directly and insert it into a text editor
            else if (filePath.endsWith(".png", Qt::CaseInsensitive) ||
                     filePath.endsWith(".jpg", Qt::CaseInsensitive) ||
                     filePath.endsWith(".jpeg", Qt::CaseInsensitive) ||
                     filePath.endsWith(".gif", Qt::CaseInsensitive)) {

                ui->textEdit->clear();
                QTextDocument *doc = ui->textEdit->document();
                QUrl imageUrl = QUrl::fromLocalFile(filePath);
                QImage image(filePath);

                doc->addResource(QTextDocument::ImageResource, imageUrl, image);

                QTextImageFormat imageFormat;
                imageFormat.setWidth(image.width());
                imageFormat.setHeight(image.height());
                imageFormat.setName(imageUrl.toString());

                QTextCursor cursor = ui->textEdit->textCursor();
                cursor.insertImage(imageFormat);

            } else {
                ui->textEdit->setText(content); // 일반 텍스트로 로드 / Load as plain text
            }

            file.close();
            currentFilePath = filePath;
            setWindowTitle(QFileInfo(filePath).fileName() + tr(" - Editor"));
        } else {
            QMessageBox::critical(this, tr("오류/Error"), tr("파일을 열 수 없습니다:\n") + file.errorString());
        }
    }
}


// == 편집 기능 슬롯 / Edit function slot ==

// '찾기' 기능 / 'Find' function
void MainWindow::actionFind()
{
    bool ok;
    // 사용자에게 찾을 텍스트를 입력받습니다. / Ask the user to enter the text they want to find.
    QString text = QInputDialog::getText(this, tr("텍스트 찾기"),
                                         tr("찾을 단어를 입력하세요:"), QLineEdit::Normal,
                                         "", &ok);
    if (ok && !text.isEmpty()) {
        QTextDocument::FindFlags flags = QTextDocument::FindFlags();

        // QTextEdit::find()를 사용하여 다음 일치 항목을 찾고 선택합니다.
        bool found = ui->textEdit->find(text, flags);

        if (!found) {
            QMessageBox::information(this, tr("찾기 결과"), tr("'%1'을(를) 찾을 수 없습니다.").arg(text));
        }
    }
}

// '바꾸기' 기능 / replace function
void MainWindow::actionReplace()
{
    //  찾을 텍스트 입력 / Enter text to find
    bool ok;
    QString findText = QInputDialog::getText(this, tr("텍스트 바꾸기"),
                                             tr("찾을 단어를 입력하세요:"), QLineEdit::Normal,
                                             "", &ok);
    if (!ok || findText.isEmpty()) return;

    // 바꿀 텍스트 입력 / Enter text to find
    QString replaceText = QInputDialog::getText(this, tr("텍스트 바꾸기"),
                                                tr("바꿀 단어를 입력하세요:"), QLineEdit::Normal,
                                                "", &ok);
    if (!ok) return;

    QTextCursor cursor = ui->textEdit->textCursor();
    QTextDocument::FindFlags flags = QTextDocument::FindFlags();
    int count = 0;

    // 텍스트를 문서 시작부터 다시 찾기 시작하도록 커서를 문서 시작으로 이동
    // Move the cursor to the beginning of the document to start searching text again from the beginning of the document
    cursor.movePosition(QTextCursor::Start);
    ui->textEdit->setTextCursor(cursor);

    // 문서 끝까지 반복하며 찾기 및 바꾸기
    // Find and replace until the end of the document
    while (ui->textEdit->find(findText, flags)) {
        // find()가 성공하면 텍스트는 이미 선택되어 있습니다.
        // If find() succeeds, the text is already selected.
        QTextCursor currentSelection = ui->textEdit->textCursor();

        // 선택된 텍스트를 replaceText로 대체
        // Replace the selected text with replaceText
        currentSelection.insertText(replaceText);
        count++;

        // 다음 검색을 위해 커서를 삽입된 텍스트 뒤로 이동 (find가 자동으로 처리함)
        // Move the cursor after the inserted text for the next search (find handles this automatically)
    }

    QMessageBox::information(this, tr("바꾸기 완료"), tr("총 %1개의 항목을 변경했습니다.").arg(count));
}

// ==  뷰 기능 슬롯 / View function slot ==

//----------------------------------------------------------------
// -- 내장함수 기능을 사용하면 저장이나 내보내기 한 파일을 적용되지 않음--
// -- Using built-in functions does not apply to files saved or exported--
//----------------------------------------------------------------
// 줌인(내장함수) / Zoom in (built-in function)
// void MainWindow::actionZoomIn()
// {
//     // ui->textEdit은 QMainWindow에 포함된 QTextEdit 위젯의 objectName이라고 가정합니다.
//     if (ui->textEdit) {
//         qDebug() << "Zoom In triggered";
//         // 기본 줌 단계를 사용하여 확대 / Zoom in using the default zoom level
//         ui->textEdit->zoomIn(1);
//     }
// }

// 줌아웃(내장함수) / Zoom out (built-in function)
// void MainWindow::actionZoomOut()
// {
//     if (ui->textEdit) {
//         qDebug() << "Zoom Out triggered";
//         // 기본 줌 단계를 사용하여 축소 / Zoom out using the default zoom level
//         ui->textEdit->zoomOut(1);
//     }
// }
//-----------------------------------------------------------------------


// 줌인 커스텀 / Zoom in custom
void MainWindow::actionZoomIn()
{
    // ui->textEdit이 유효한지 확인합니다. / Check if ui->textEdit is valid.
    if (!ui->textEdit) {
        return;
    }

    // 현재 문서의 기본 폰트(또는 커서 위치의 폰트) 정보를 가져옵니다.
    // Get information about the default font of the current document (or the font at the cursor position).

    // 문서 전체의 기본 폰트 크기를 사용할지, 현재 커서의 폰트 크기를 사용할지 결정합니다.
    // Determines whether to use the default font size for the entire document or the font size of the current cursor.

    // 줌 기능은 보통 문서 전체의 기본 폰트 크기를 변경합니다.
    // The zoom feature usually changes the default font size for the entire document.
    QFont font = ui->textEdit->document()->defaultFont();
    int currentSize = font.pointSize();

    // 폰트 크기가 포인트 단위로 설정되지 않았을 경우 (예: 픽셀) 또는 0일 경우,
    // 적절한 기본값(예: 12)을 사용합니다.
    // If the font size is not set in points (e.g., pixels) or is 0,
    // use an appropriate default value (e.g., 12).
    if (currentSize <= 0) {
        currentSize = 12;
    }

    // 줌 단계 설정 및 최대 크기 제한 (Max Zoom)
    // Set zoom level and limit maximum size (Max Zoom)
    const int ZOOM_STEP = 2; // 확대 단위를 2pt로 설정 / Set the zoom unit to 2pt
    const int MAX_FONT_SIZE = 48; // 최대 폰트 크기를 48pt로 제한 / Limit maximum font size to 48pt

    if (currentSize < MAX_FONT_SIZE) {
        int newSize = qMin(currentSize + ZOOM_STEP, MAX_FONT_SIZE);

        // 새 폰트 크기 적용 / Apply new font size
        font.setPointSize(newSize);
        ui->textEdit->document()->setDefaultFont(font);

        // 텍스트 전체에 새 폰트 크기 적용 (기존 HTML 로드 시 인라인 스타일이 적용되어 있다면 필요)
        // Apply new font size to all text (needed if inline styles were applied when loading existing HTML)
        QTextCursor cursor(ui->textEdit->document());
        cursor.select(QTextCursor::Document);
        QTextCharFormat fmt;
        fmt.setFontPointSize(newSize);

        // 포맷을 병합하여 폰트 크기만 변경하고 다른 스타일은 유지합니다.
        // Merge formats to change only the font size and keep other styles.
        cursor.mergeCharFormat(fmt);
        cursor.clearSelection();

        qDebug() << "Zoom In to size:" << newSize;

        // (옵션) 줌 상태 표시 업데이트
        // (Optional) Update the zoom status display
        // updateZoomStatus(newSize);
    } else {
        qDebug() << "Maximum zoom level reached.";
    }
}



// 줌아웃 커스텀 / Zoom out coustom
void MainWindow::actionZoomOut()
{
    // ui->textEdit이 유효한지 확인합니다.
    // Check if ui->textEdit is valid.
    if (!ui->textEdit) {
        return;
    }

    // 현재 문서의 기본 폰트 정보를 가져옵니다.
    // Get the default font information for the current document.
    QFont font = ui->textEdit->document()->defaultFont();
    int currentSize = font.pointSize();

    // 폰트 크기가 포인트 단위로 설정되지 않았거나 0일 경우, 적절한 기본값(예: 12)을 사용합니다.
    // If the font size is not set in points or is 0, a reasonable default (e.g. 12) is used.
    if (currentSize <= 0) {
        currentSize = 12;
    }

    // 줌 단계 설정 및 최소 크기 제한 (Min Zoom)
    // Set zoom level and minimum size limit (Min Zoom)
    const int ZOOM_STEP = 2; // 축소 단위를 2pt로 설정 (줌 인과 동일하게 유지) / Set the zoom unit to 2pt (keep it the same as zoom in)
    const int MIN_FONT_SIZE = 8; // 최소 폰트 크기를 8pt로 제한 / Limit minimum font size to 8pt

    if (currentSize > MIN_FONT_SIZE) {

        // 현재 크기에서 줌 단계만큼 빼고, 최소 크기(MIN_FONT_SIZE)보다 작아지지 않도록 제한합니다.
        // Subtract the zoom level from the current size, and limit it to no smaller than the minimum size (MIN_FONT_SIZE).
        int newSize = qMax(currentSize - ZOOM_STEP, MIN_FONT_SIZE);

        // 새 폰트 크기 적용 / Apply new font size
        font.setPointSize(newSize);
        ui->textEdit->document()->setDefaultFont(font);

        // 텍스트 전체에 새 폰트 크기 적용 (줌 아웃이 뷰포트뿐만 아니라 실제 폰트 크기에 반영되도록)
        // Apply the new font size to the entire text (so that zooming out reflects the actual font size, not just the viewport)
        QTextCursor cursor(ui->textEdit->document());
        cursor.select(QTextCursor::Document);
        QTextCharFormat fmt;
        fmt.setFontPointSize(newSize);

        // 포맷을 병합하여 폰트 크기만 변경하고 다른 스타일은 유지합니다. / Merge formats to change only the font size and keep other styles.
        cursor.mergeCharFormat(fmt);
        cursor.clearSelection();

        qDebug() << "Zoom Out to size:" << newSize;

        // 줌 상태 표시 업데이트 / Updated zoom status display
        // updateZoomStatus(newSize);
    } else {
        qDebug() << "Minimum zoom level reached.";
    }
}

// 줌 초기화 / Reset zoom
void MainWindow::actionZoomReset()
{
    // ui->textEdit이 유효한지 확인합니다.
    // Check if ui->textEdit is valid.
    if (!ui->textEdit) {
        return;
    }

    // 기본 폰트 크기를 가져옵니다. / Get the default font size.
    const int newSize = m_defaultPointSize;

    // 문서의 기본 폰트를 설정합니다. / Sets the default font for the document.
    QFont font = ui->textEdit->document()->defaultFont();
    font.setPointSize(newSize);
    ui->textEdit->document()->setDefaultFont(font);

    // 텍스트 전체에 서식 일괄 적용 (인라인 스타일을 덮어쓰기 위함)
    // Apply formatting to all text (to overwrite inline styles)
    QTextCursor cursor(ui->textEdit->document());
    cursor.select(QTextCursor::Document);
    QTextCharFormat fmt;
    fmt.setFontPointSize(newSize);

    // 포맷을 병합하여 폰트 크기만 기본값으로 변경합니다.
    // Merge formats to only change the font size to default.
    cursor.mergeCharFormat(fmt);
    cursor.clearSelection();

    qDebug() << "Zoom Reset to default size:" << newSize;

    // 줌 상태 표시 업데이트 / Updated zoom status display
    // updateZoomStatus(newSize);
}


// ==  텍스트 기능 구현 슬롯 / Text function implementation slot ==

// 굵게 버튼이 클릭될 때 호출되는 슬롯 / A slot called when the bold button is clicked
// 볼드체 적용 슬롯 구현 / Implementing a bold font slot
void MainWindow::actionBold(bool checked)
{
    // 새로운 문자 서식 객체를 생성 / Create a new character format object
    QTextCharFormat format;

    // QAction의 체크 상태(눌림 상태)에 따라 굵게/보통을 설정
    // Set bold/normal depending on the checked state (pressed state) of QAction

    // QAction::triggered(bool checked) 시그널의 'checked' 값을 사용합니다.
    // Use the 'checked' value of the QAction::triggered(bool checked) signal.
    format.setFontWeight(checked ? QFont::Bold : QFont::Normal);

    // 서식을 텍스트 편집기에 병합 (선택 영역 또는 커서 위치의 단어에 적용)
    // Merge formatting into text editor (applies to selection or word at cursor position)

    // 이 코드는 QTextEdit::mergeCurrentCharFormat()을 사용합니다.
    // This code uses QTextEdit::mergeCurrentCharFormat().
    ui->textEdit->mergeCurrentCharFormat(format);

    // QAction의 상태는 연결된 QToolButton에 의해 자동으로 토글됩니다.
    // The state of a QAction is automatically toggled by the connected QToolButton.
}



// 이탤릭 버튼이 클릭될 때 호출되는 슬롯 / A slot called when the italic button is clicked.
void MainWindow::actionItalic(bool checked)
{
    QTextCharFormat format;

    // checked 상태에 따라 이탤릭 속성 설정 / Set italic property based on checked state
    format.setFontItalic(checked);
    ui->textEdit->mergeCurrentCharFormat(format);
}

// 밑줄 버튼이 클릭될 때 호출되는 슬롯
// Slot called when the underline button is clicked
void MainWindow::actionUnderline(bool checked)
{
    QTextCharFormat format;

    // checked 상태에 따라 밑줄 속성 설정
    // Set the underline property according to the checked state
    format.setFontUnderline(checked);
    ui->textEdit->mergeCurrentCharFormat(format);
}

// 취소선 버튼이 클릭될 때 호출되는 슬롯
// Slot called when the cancel button is clicked
void MainWindow::actionStrike(bool checked)
{
    QTextCharFormat format;

    // checked 상태에 따라 취소선 속성 설정
    // Set strikethrough property based on checked status
    format.setFontStrikeOut(checked);
    ui->textEdit->mergeCurrentCharFormat(format);
}

// 글꼴 색상 버튼이 클릭될 때 호출되는 슬롯 (수동연결 사용시)
// Slot called when the font color button is clicked (when using manual linking)
void MainWindow::actionColor()
{
    // 현재 텍스트 색상을 기본값으로 사용 / Use current text color as default
    QColor initialColor = ui->textEdit->currentCharFormat().foreground().color();
    QColor color = QColorDialog::getColor(initialColor, this, tr("글꼴 색상 선택"));

    if (color.isValid()) {
        QTextCharFormat format;
        // 선택된 색상을 전경색(글꼴 색상)으로 설정 / Set the selected color as the foreground color (font color)
        format.setForeground(QBrush(color));
        ui->textEdit->mergeCurrentCharFormat(format);
    }
}

// 글꼴 선택 버튼이 클릭될 때 호출되는 슬롯 (수동연결 사용시)
// Slot called when the font selection button is clicked (when using manual linking)
void MainWindow::actionFont()
{
    // 현재 폰트를 기본값으로 사용 / Use the current font as default
    QFont initialFont = ui->textEdit->currentCharFormat().font();
    bool ok;
    QFont font = QFontDialog::getFont(&ok, initialFont, this, tr("글꼴 선택"));

    if (ok) {
        QTextCharFormat format;
        // 선택된 폰트를 적용 / Apply selected font
        format.setFont(font);
        ui->textEdit->mergeCurrentCharFormat(format);
    }
}

// == 자동연결은 경고 발생함 ==
// void MainWindow::on_actionColor_triggered()
// {
//     // 현재 텍스트 색상을 기본값으로 사용
//     QColor initialColor = ui->textEdit->currentCharFormat().foreground().color();
//     QColor color = QColorDialog::getColor(initialColor, this, tr("글꼴 색상 선택"));

//     if (color.isValid()) {
//         QTextCharFormat format;
//         // 선택된 색상을 전경색(글꼴 색상)으로 설정
//         format.setForeground(QBrush(color));
//         ui->textEdit->mergeCurrentCharFormat(format);
//     }
// }

// void MainWindow::on_actionFont_triggered()
// {
//     // 현재 폰트를 기본값으로 사용
//     QFont initialFont = ui->textEdit->currentCharFormat().font();
//     bool ok;
//     QFont font = QFontDialog::getFont(&ok, initialFont, this, tr("글꼴 선택"));

//     if (ok) {
//         QTextCharFormat format;
//         // 선택된 폰트를 적용
//         format.setFont(font);
//         ui->textEdit->mergeCurrentCharFormat(format);
//     }
// }


// == 이미지 삽입 슬롯 / Image insertion slot  ==
void MainWindow::actionInsertImage()
{
    QString imagePath = QFileDialog::getOpenFileName(this,
                                                     tr("이미지 삽입"),
                                                     QDir::homePath(),
                                                     tr("이미지 파일 (*.png *.jpg *.jpeg *.gif *.bmp)"));

    if (!imagePath.isEmpty()) {
        QImage image(imagePath);

        if (image.isNull()) {
            QMessageBox::critical(this, tr("오류"), tr("이미지 파일을 로드할 수 없습니다."));
            return;
        }

        QTextDocument *doc = ui->textEdit->document();
        QUrl imageUrl = QUrl::fromLocalFile(imagePath);

        doc->addResource(QTextDocument::ImageResource, imageUrl, image);

        QTextImageFormat imageFormat;
        // 기본 삽입 크기를 원본 크기로 설정 / Set default insert size to original size
        imageFormat.setWidth(image.width());
        imageFormat.setHeight(image.height());
        imageFormat.setName(imageUrl.toString());

        QTextCursor cursor = ui->textEdit->textCursor();
        cursor.insertImage(imageFormat);
    }
}

// == 이벤트 필터 구현 (이미지 드래그 리사이즈) / Implementing an event filter (image drag resize) ==
bool MainWindow::eventFilter(QObject *obj, QEvent *event)
{
    if (obj == ui->textEdit->viewport()) {
        QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event);
        QTextCursor cursor = ui->textEdit->textCursor();
        QTextCharFormat format = cursor.charFormat();

        // 마우스 누름 이벤트 (Resize 시작 감지) / Mouse press event (detect resize start)
        if (event->type() == QEvent::MouseButtonPress && mouseEvent->button() == Qt::LeftButton) {
            if (format.isImageFormat()) {
                // 이미지가 선택된 위치에서 클릭이 발생한 경우 / If a click occurs at the location where the image is selected
                m_isResizing = true;
                m_dragStartPos = mouseEvent->pos();

                // 현재 이미지 포맷에서 기존 크기를 가져옵니다. / Get the original size from the current image format.
                QTextImageFormat imageFormat = format.toImageFormat();
                m_originalImageWidth = imageFormat.width();
                m_originalImageHeight = imageFormat.height();

                // 리사이즈 중에는 마우스 이벤트를 QTextEdit으로 전달하지 않습니다.
                // Mouse events are not passed to QTextEdit during resizing.
                return true;
            }
        }

        // 마우스 이동 이벤트 (Resize 진행) / Mouse move event (Resize in progress)
        else if (event->type() == QEvent::MouseMove) {
            if (m_isResizing && mouseEvent->buttons() & Qt::LeftButton) {

                // X축 이동 거리를 기반으로 크기를 조절합니다. / Scales based on the distance moved along the X-axis.
                int deltaX = mouseEvent->pos().x() - m_dragStartPos.x();

                // 새로운 너비 계산 (최소 50픽셀 제한) / New width calculation (minimum 50 pixels limit)
                int newWidth = qMax(50, m_originalImageWidth + deltaX);

                // 종횡비 유지하여 새로운 높이 계산 / Calculate new height while maintaining aspect ratio
                // 소수점 연산을 위해 double로 캐스팅 / Cast to double for decimal operations
                int newHeight = (int)((double)m_originalImageHeight * ((double)newWidth / m_originalImageWidth));

                // 현재 커서 위치에 있는 이미지 포맷을 업데이트합니다.
                // Updates the image format at the current cursor position.
                QTextCursor currentCursor = ui->textEdit->textCursor();
                if (currentCursor.charFormat().isImageFormat()) {
                    QTextImageFormat newImageFormat = currentCursor.charFormat().toImageFormat();

                    newImageFormat.setWidth(newWidth);
                    newImageFormat.setHeight(newHeight);

                    // 서식 적용 / Apply Formatting
                    ui->textEdit->mergeCurrentCharFormat(newImageFormat);
                }

                return true; // 이벤트를 소비하여 텍스트 선택 등 기본 동작 방지 / Consuming events to prevent default actions such as text selection
            }
        }

        // 마우스 놓기 이벤트 (Resize 종료) / Mouse release event (Resize ends)
        else if (event->type() == QEvent::MouseButtonRelease) {
            if (m_isResizing && mouseEvent->button() == Qt::LeftButton) {
                m_isResizing = false;
                // 리사이즈 종료 / End of resize
                return true;
            }
        }
    }

    // 필터링하지 않은 모든 이벤트를 대상 객체로 전달합니다.
    // Forward all unfiltered events to the target object.
    return QMainWindow::eventFilter(obj, event);
}


// 커서 위치의 문자 서식이 바뀔 때 호출되는 슬롯
// Slot called when the character format at the cursor position changes

// 툴바 상태 업데이트 슬롯 구현 / Implementing a Toolbar State Update Slot
void MainWindow::updateFormat(const QTextCharFormat &format)
{
    // 현재 서식에서 폰트의 굵기를 확인 / Check the font weight in the current format
    bool isBold = format.font().bold();

    // -- actionBold의 체크 상태를 폰트의 굵기 상태에 맞게 업데이트 / Update the check state of actionBold to match the bold state of the font.
    // -- 이렇게 해야 사용자가 굵은 글씨로 커서를 옮기면 버튼이 눌린 상태로 보입니다. / This way, when the user moves the cursor over the bold text, the button appears pressed.
    ui->actionBold->setChecked(isBold);

    // 다른 서식(Italic, Underline)에 대해서도 이 함수 내에서 동일하게 처리할 수 있습니다.
    // You can handle other formats (Italic, Underline) in the same way within this function.
    // ui->actionItalic->setChecked(format.font().italic());


    // 이탤릭체 상태 업데이트 / Italic status updates
    bool isItalic = format.font().italic();
    ui->actionItalic->setChecked(isItalic);

    // 밑줄 상태 업데이트 / Underline status update
    bool isUnderline = format.font().underline();
    ui->actionUnderline->setChecked(isUnderline);

    // 취소선 상태 업데이트 / Strikethrough status update
    bool isStrike = format.font().strikeOut();
    ui->actionStrike->setChecked(isStrike);
}

// == 도움말 & Help 기능 / Help & Help function ==
void MainWindow::actionHelp()
{
    QMessageBox::information(this,
                             tr("도움말/Help"),
                             tr("<h3>간단한 텍스트 편집기 사용법/How to use a simple text editor</h3>"
                                "<p><strong>파일 메뉴:</strong> 새 파일, 열기, 저장, PDF/Base64 내보내기를 지원합니다.</p>"
                                "<p><strong>File Menu:</strong> Supports New File, Open, Save, and PDF/Base64 Export.</p>"
                                "<p><strong>편집 메뉴:</strong> 찾기 및 바꾸기 기능을 사용할 수 있습니다.</p>"
                                "<p><strong>Edit menu:</strong> You can use the Find and Replace feature.</p>"
                                "<p><strong>뷰 메뉴:</strong> 텍스트 줌인/줌아웃/초기화를 할 수 있습니다.</p>"
                                "<p><strong>View Menu:</strong> You can zoom in/out/reset the text.</p>"
                                "<p><strong>서식 지정:</strong> 텍스트를 굵게, 기울임꼴, 밑줄, 취소선으로 설정하고, 색상 및 폰트를 변경할 수 있습니다.</p>"
                                "<p><strong>Formatting:</strong> You can make text bold, italic, underline, strikethrough, and change the color and font.</p>"
                                "<p><strong>이미지:</strong> 로컬 이미지를 삽입하고 마우스로 크기를 조절할 수 있습니다.</p>"
                                "<p><strong>Image:</strong> Insert a local image and resize it with your mouse.</p>"
                                ),
                             QMessageBox::Ok);
}

// about slot
void MainWindow::actionAbout()
{
    QMessageBox::about(this,
                       tr("Rich Text Editor 정보/Rich Text Editor information"),
                       tr("<h2>Rich Text Editor v1.0 (Qt)</h2>"
                          "<p>Qt 프레임워크를 기반으로 개발된 간단한 텍스트 편집기입니다.</p>"
                          "<p>A Rich text editor developed based on the Qt framework.</p>"
                          "<p>주요 기능: 파일 입출력, 텍스트 서식 지정, 이미지 삽입 및 크기 조절.</p>"
                          "<p>Key features: file input/output, text formatting, image insertion and resizing.</p>"
                          "<p>Copyright &copy; 2025</p>"

                          ));
}


// 메모리해제 / "<p>Key features: file input/output, text formatting, image insertion and resizing.</p>"
MainWindow::~MainWindow()
{
    delete ui;
}


Leave a Reply