mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-07-23 03:31:00 -04:00
refactor: Restructure project into sources/tests layout and dissolve PluginLLMCore
This commit is contained in:
95
sources/widgets/AddCustomInstructionDialog.cpp
Normal file
95
sources/widgets/AddCustomInstructionDialog.cpp
Normal file
@@ -0,0 +1,95 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "AddCustomInstructionDialog.hpp"
|
||||
#include "plugin/QodeAssisttr.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFormLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
AddCustomInstructionDialog::AddCustomInstructionDialog(QWidget *parent)
|
||||
: QDialog(parent)
|
||||
{
|
||||
setWindowTitle(Tr::tr("Add Custom Instruction"));
|
||||
setupUi();
|
||||
resize(500, 400);
|
||||
}
|
||||
|
||||
AddCustomInstructionDialog::AddCustomInstructionDialog(const CustomInstruction &instruction, QWidget *parent)
|
||||
: QDialog(parent)
|
||||
, m_instruction(instruction)
|
||||
{
|
||||
setWindowTitle(Tr::tr("Edit Custom Instruction"));
|
||||
setupUi();
|
||||
m_nameEdit->setText(instruction.name);
|
||||
m_bodyEdit->setPlainText(instruction.body);
|
||||
m_defaultCheckBox->setChecked(instruction.isDefault);
|
||||
resize(500, 400);
|
||||
}
|
||||
|
||||
void AddCustomInstructionDialog::setupUi()
|
||||
{
|
||||
QVBoxLayout *mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(10, 10, 10, 10);
|
||||
mainLayout->setSpacing(10);
|
||||
|
||||
QFormLayout *formLayout = new QFormLayout();
|
||||
m_nameEdit = new QLineEdit(this);
|
||||
m_nameEdit->setPlaceholderText(Tr::tr("Enter instruction name..."));
|
||||
formLayout->addRow(Tr::tr("Name:"), m_nameEdit);
|
||||
|
||||
mainLayout->addLayout(formLayout);
|
||||
|
||||
QLabel *bodyLabel = new QLabel(Tr::tr("Instruction Body:"), this);
|
||||
mainLayout->addWidget(bodyLabel);
|
||||
|
||||
m_bodyEdit = new QPlainTextEdit(this);
|
||||
m_bodyEdit->setPlaceholderText(
|
||||
Tr::tr("Enter the refactoring instruction that will be sent to the LLM..."));
|
||||
mainLayout->addWidget(m_bodyEdit);
|
||||
|
||||
m_defaultCheckBox = new QCheckBox(Tr::tr("Set as default instruction"), this);
|
||||
m_defaultCheckBox->setToolTip(
|
||||
Tr::tr("This instruction will be automatically selected when opening Quick Refactor dialog"));
|
||||
mainLayout->addWidget(m_defaultCheckBox);
|
||||
|
||||
QDialogButtonBox *buttonBox
|
||||
= new QDialogButtonBox(QDialogButtonBox::Save | QDialogButtonBox::Cancel, this);
|
||||
|
||||
connect(buttonBox, &QDialogButtonBox::accepted, this, [this]() {
|
||||
if (m_nameEdit->text().trimmed().isEmpty()) {
|
||||
QMessageBox::warning(this, Tr::tr("Invalid Input"), Tr::tr("Instruction name cannot be empty."));
|
||||
return;
|
||||
}
|
||||
if (m_bodyEdit->toPlainText().trimmed().isEmpty()) {
|
||||
QMessageBox::warning(this, Tr::tr("Invalid Input"), Tr::tr("Instruction body cannot be empty."));
|
||||
return;
|
||||
}
|
||||
accept();
|
||||
});
|
||||
|
||||
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
|
||||
mainLayout->addWidget(buttonBox);
|
||||
}
|
||||
|
||||
CustomInstruction AddCustomInstructionDialog::getInstruction() const
|
||||
{
|
||||
CustomInstruction instruction = m_instruction;
|
||||
instruction.name = m_nameEdit->text().trimmed();
|
||||
instruction.body = m_bodyEdit->toPlainText().trimmed();
|
||||
instruction.isDefault = m_defaultCheckBox->isChecked();
|
||||
return instruction;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
||||
39
sources/widgets/AddCustomInstructionDialog.hpp
Normal file
39
sources/widgets/AddCustomInstructionDialog.hpp
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
#include <QString>
|
||||
|
||||
#include "CustomInstructionsManager.hpp"
|
||||
|
||||
class QLineEdit;
|
||||
class QPlainTextEdit;
|
||||
class QCheckBox;
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class AddCustomInstructionDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit AddCustomInstructionDialog(QWidget *parent = nullptr);
|
||||
explicit AddCustomInstructionDialog(const CustomInstruction &instruction, QWidget *parent = nullptr);
|
||||
~AddCustomInstructionDialog() override = default;
|
||||
|
||||
CustomInstruction getInstruction() const;
|
||||
|
||||
private:
|
||||
void setupUi();
|
||||
|
||||
QLineEdit *m_nameEdit;
|
||||
QPlainTextEdit *m_bodyEdit;
|
||||
QCheckBox *m_defaultCheckBox;
|
||||
CustomInstruction m_instruction;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
||||
85
sources/widgets/CompletionErrorHandler.cpp
Normal file
85
sources/widgets/CompletionErrorHandler.cpp
Normal file
@@ -0,0 +1,85 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "CompletionErrorHandler.hpp"
|
||||
|
||||
#include <texteditor/texteditor.h>
|
||||
#include <utils/tooltip/tooltip.h>
|
||||
|
||||
#include "ErrorWidget.hpp"
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
void CompletionErrorHandler::showError(
|
||||
TextEditor::TextEditorWidget *widget,
|
||||
const QString &errorMessage,
|
||||
int autoHideMs)
|
||||
{
|
||||
m_widget = widget;
|
||||
m_errorMessage = errorMessage;
|
||||
|
||||
if (m_widget) {
|
||||
const QRect cursorRect = m_widget->cursorRect(m_widget->textCursor());
|
||||
m_errorPosition = m_widget->viewport()->mapToGlobal(cursorRect.topLeft())
|
||||
- Utils::ToolTip::offsetFromPosition();
|
||||
|
||||
identifyMatch(m_widget, m_widget->textCursor().position(), [this, autoHideMs](auto priority) {
|
||||
if (priority != Priority_None) {
|
||||
if (m_errorWidget) {
|
||||
m_errorWidget->deleteLater();
|
||||
}
|
||||
|
||||
m_errorWidget = new ErrorWidget(m_errorMessage, m_widget);
|
||||
|
||||
const QRect cursorRect = m_widget->cursorRect(m_widget->textCursor());
|
||||
QPoint globalPos = m_widget->viewport()->mapToGlobal(cursorRect.topLeft());
|
||||
QPoint localPos = m_widget->mapFromGlobal(globalPos);
|
||||
localPos.ry() -= m_errorWidget->height() + 5;
|
||||
|
||||
if (localPos.y() < 0) {
|
||||
localPos.ry() = cursorRect.bottom() + 5;
|
||||
}
|
||||
|
||||
m_errorWidget->move(localPos);
|
||||
m_errorWidget->show();
|
||||
m_errorWidget->raise();
|
||||
|
||||
QObject::connect(m_errorWidget, &ErrorWidget::dismissed, m_errorWidget, [this]() {
|
||||
hideError();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void CompletionErrorHandler::hideError()
|
||||
{
|
||||
if (m_errorWidget) {
|
||||
m_errorWidget->deleteLater();
|
||||
m_errorWidget = nullptr;
|
||||
}
|
||||
Utils::ToolTip::hideImmediately();
|
||||
m_errorMessage.clear();
|
||||
}
|
||||
|
||||
void CompletionErrorHandler::identifyMatch(
|
||||
TextEditor::TextEditorWidget *editorWidget, int pos, ReportPriority report)
|
||||
{
|
||||
if (!editorWidget || m_errorMessage.isEmpty()) {
|
||||
report(Priority_None);
|
||||
return;
|
||||
}
|
||||
|
||||
report(Priority_Tooltip);
|
||||
}
|
||||
|
||||
void CompletionErrorHandler::operateTooltip(
|
||||
TextEditor::TextEditorWidget *editorWidget, const QPoint &point)
|
||||
{
|
||||
Q_UNUSED(editorWidget)
|
||||
Q_UNUSED(point)
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
||||
39
sources/widgets/CompletionErrorHandler.hpp
Normal file
39
sources/widgets/CompletionErrorHandler.hpp
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <texteditor/basehoverhandler.h>
|
||||
#include <QPointer>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class ErrorWidget;
|
||||
|
||||
class CompletionErrorHandler : public TextEditor::BaseHoverHandler
|
||||
{
|
||||
public:
|
||||
void showError(
|
||||
TextEditor::TextEditorWidget *widget,
|
||||
const QString &errorMessage,
|
||||
int autoHideMs = 5000);
|
||||
|
||||
void hideError();
|
||||
|
||||
bool isErrorVisible() const { return !m_errorWidget.isNull(); }
|
||||
|
||||
protected:
|
||||
void identifyMatch(
|
||||
TextEditor::TextEditorWidget *editorWidget, int pos, ReportPriority report) override;
|
||||
void operateTooltip(TextEditor::TextEditorWidget *editorWidget, const QPoint &point) override;
|
||||
|
||||
private:
|
||||
QPointer<TextEditor::TextEditorWidget> m_widget;
|
||||
QPointer<ErrorWidget> m_errorWidget;
|
||||
QString m_errorMessage;
|
||||
QPoint m_errorPosition;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
||||
57
sources/widgets/CompletionHintHandler.cpp
Normal file
57
sources/widgets/CompletionHintHandler.cpp
Normal file
@@ -0,0 +1,57 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "CompletionHintHandler.hpp"
|
||||
#include "CompletionHintWidget.hpp"
|
||||
|
||||
#include <texteditor/texteditor.h>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
CompletionHintHandler::CompletionHintHandler() = default;
|
||||
|
||||
CompletionHintHandler::~CompletionHintHandler()
|
||||
{
|
||||
hideHint();
|
||||
}
|
||||
|
||||
void CompletionHintHandler::showHint(TextEditor::TextEditorWidget *widget, QPoint position, int fontSize)
|
||||
{
|
||||
if (!widget) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_hintWidget) {
|
||||
m_hintWidget = new CompletionHintWidget(widget, fontSize);
|
||||
}
|
||||
|
||||
m_hintWidget->move(position);
|
||||
m_hintWidget->show();
|
||||
m_hintWidget->raise();
|
||||
}
|
||||
|
||||
void CompletionHintHandler::hideHint()
|
||||
{
|
||||
if (m_hintWidget) {
|
||||
m_hintWidget->deleteLater();
|
||||
m_hintWidget = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool CompletionHintHandler::isHintVisible() const
|
||||
{
|
||||
return !m_hintWidget.isNull() && m_hintWidget->isVisible();
|
||||
}
|
||||
|
||||
void CompletionHintHandler::updateHintPosition(TextEditor::TextEditorWidget *widget, QPoint position)
|
||||
{
|
||||
if (!widget || !m_hintWidget) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_hintWidget->move(position);
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
||||
34
sources/widgets/CompletionHintHandler.hpp
Normal file
34
sources/widgets/CompletionHintHandler.hpp
Normal file
@@ -0,0 +1,34 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QPointer>
|
||||
#include <functional>
|
||||
|
||||
namespace TextEditor {
|
||||
class TextEditorWidget;
|
||||
}
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class CompletionHintWidget;
|
||||
|
||||
class CompletionHintHandler
|
||||
{
|
||||
public:
|
||||
CompletionHintHandler();
|
||||
~CompletionHintHandler();
|
||||
|
||||
void showHint(TextEditor::TextEditorWidget *widget, QPoint position, int fontSize);
|
||||
void hideHint();
|
||||
bool isHintVisible() const;
|
||||
void updateHintPosition(TextEditor::TextEditorWidget *widget, QPoint position);
|
||||
|
||||
private:
|
||||
QPointer<CompletionHintWidget> m_hintWidget;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
||||
68
sources/widgets/CompletionHintWidget.cpp
Normal file
68
sources/widgets/CompletionHintWidget.cpp
Normal file
@@ -0,0 +1,68 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "CompletionHintWidget.hpp"
|
||||
|
||||
#include <QPainter>
|
||||
|
||||
#include <utils/theme/theme.h>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
CompletionHintWidget::CompletionHintWidget(QWidget *parent, int fontSize)
|
||||
: QWidget(parent)
|
||||
, m_isHovered(false)
|
||||
{
|
||||
m_accentColor = Utils::creatorTheme()->color(Utils::Theme::TextColorNormal);
|
||||
|
||||
setMouseTracking(true);
|
||||
setFocusPolicy(Qt::NoFocus);
|
||||
setAttribute(Qt::WA_TranslucentBackground);
|
||||
|
||||
int triangleSize = qMax(6, fontSize / 2);
|
||||
setFixedSize(triangleSize, triangleSize);
|
||||
}
|
||||
|
||||
void CompletionHintWidget::paintEvent(QPaintEvent *event)
|
||||
{
|
||||
Q_UNUSED(event);
|
||||
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
QColor triangleColor = m_accentColor;
|
||||
triangleColor.setAlpha(m_isHovered ? 255 : 200);
|
||||
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(triangleColor);
|
||||
|
||||
QPolygonF triangle;
|
||||
int w = width();
|
||||
int h = height();
|
||||
|
||||
triangle << QPointF(0, 0)
|
||||
<< QPointF(0, h)
|
||||
<< QPointF(w, h / 2.0);
|
||||
|
||||
painter.drawPolygon(triangle);
|
||||
}
|
||||
|
||||
void CompletionHintWidget::enterEvent(QEnterEvent *event)
|
||||
{
|
||||
Q_UNUSED(event);
|
||||
m_isHovered = true;
|
||||
setCursor(Qt::PointingHandCursor);
|
||||
update();
|
||||
}
|
||||
|
||||
void CompletionHintWidget::leaveEvent(QEvent *event)
|
||||
{
|
||||
Q_UNUSED(event);
|
||||
m_isHovered = false;
|
||||
setCursor(Qt::ArrowCursor);
|
||||
update();
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
||||
29
sources/widgets/CompletionHintWidget.hpp
Normal file
29
sources/widgets/CompletionHintWidget.hpp
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class CompletionHintWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit CompletionHintWidget(QWidget *parent = nullptr, int fontSize = 12);
|
||||
~CompletionHintWidget() override = default;
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void enterEvent(QEnterEvent *event) override;
|
||||
void leaveEvent(QEvent *event) override;
|
||||
|
||||
private:
|
||||
QColor m_accentColor;
|
||||
bool m_isHovered;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
||||
96
sources/widgets/CompletionProgressHandler.cpp
Normal file
96
sources/widgets/CompletionProgressHandler.cpp
Normal file
@@ -0,0 +1,96 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "CompletionProgressHandler.hpp"
|
||||
|
||||
#include <QGraphicsBlurEffect>
|
||||
#include <QGraphicsPixmapItem>
|
||||
#include <QGraphicsScene>
|
||||
#include <QLabel>
|
||||
#include <QPainter>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include <texteditor/texteditor.h>
|
||||
#include <utils/progressindicator.h>
|
||||
#include <utils/theme/theme.h>
|
||||
#include <utils/tooltip/tooltip.h>
|
||||
|
||||
#include "ProgressWidget.hpp"
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
void CompletionProgressHandler::showProgress(TextEditor::TextEditorWidget *widget)
|
||||
{
|
||||
m_widget = widget;
|
||||
|
||||
if (m_widget) {
|
||||
const QRect cursorRect = m_widget->cursorRect(m_widget->textCursor());
|
||||
m_iconPosition = m_widget->viewport()->mapToGlobal(cursorRect.topLeft())
|
||||
- Utils::ToolTip::offsetFromPosition();
|
||||
|
||||
identifyMatch(m_widget, m_widget->textCursor().position(), [this](auto priority) {
|
||||
if (priority != Priority_None)
|
||||
operateTooltip(m_widget, m_iconPosition);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void CompletionProgressHandler::hideProgress()
|
||||
{
|
||||
if (m_progressWidget) {
|
||||
m_progressWidget->deleteLater();
|
||||
m_progressWidget = nullptr;
|
||||
}
|
||||
Utils::ToolTip::hideImmediately();
|
||||
}
|
||||
|
||||
void CompletionProgressHandler::setCancelCallback(std::function<void()> callback)
|
||||
{
|
||||
m_cancelCallback = callback;
|
||||
}
|
||||
|
||||
void CompletionProgressHandler::identifyMatch(
|
||||
TextEditor::TextEditorWidget *editorWidget, int pos, ReportPriority report)
|
||||
{
|
||||
if (!editorWidget) {
|
||||
report(Priority_None);
|
||||
return;
|
||||
}
|
||||
|
||||
report(Priority_Tooltip);
|
||||
}
|
||||
|
||||
void CompletionProgressHandler::operateTooltip(
|
||||
TextEditor::TextEditorWidget *editorWidget, const QPoint &point)
|
||||
{
|
||||
if (!editorWidget)
|
||||
return;
|
||||
|
||||
if (m_progressWidget) {
|
||||
delete m_progressWidget;
|
||||
}
|
||||
|
||||
m_progressWidget = new ProgressWidget(editorWidget);
|
||||
|
||||
if (m_cancelCallback) {
|
||||
m_progressWidget->setCancelCallback(m_cancelCallback);
|
||||
}
|
||||
|
||||
const QRect cursorRect = editorWidget->cursorRect(editorWidget->textCursor());
|
||||
QPoint globalPos = editorWidget->viewport()->mapToGlobal(cursorRect.topLeft());
|
||||
QPoint localPos = editorWidget->mapFromGlobal(globalPos);
|
||||
|
||||
localPos.rx() += 5;
|
||||
localPos.ry() -= m_progressWidget->height() + 5;
|
||||
|
||||
if (localPos.y() < 0) {
|
||||
localPos.ry() = cursorRect.bottom() + 5;
|
||||
}
|
||||
|
||||
m_progressWidget->move(localPos);
|
||||
m_progressWidget->show();
|
||||
m_progressWidget->raise();
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
35
sources/widgets/CompletionProgressHandler.hpp
Normal file
35
sources/widgets/CompletionProgressHandler.hpp
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <texteditor/basehoverhandler.h>
|
||||
#include <QPointer>
|
||||
#include <functional>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class ProgressWidget;
|
||||
|
||||
class CompletionProgressHandler : public TextEditor::BaseHoverHandler
|
||||
{
|
||||
public:
|
||||
void showProgress(TextEditor::TextEditorWidget *widget);
|
||||
void hideProgress();
|
||||
void setCancelCallback(std::function<void()> callback);
|
||||
bool isProgressVisible() const { return !m_progressWidget.isNull(); }
|
||||
|
||||
protected:
|
||||
void identifyMatch(
|
||||
TextEditor::TextEditorWidget *editorWidget, int pos, ReportPriority report) override;
|
||||
void operateTooltip(TextEditor::TextEditorWidget *editorWidget, const QPoint &point) override;
|
||||
|
||||
private:
|
||||
QPointer<TextEditor::TextEditorWidget> m_widget;
|
||||
QPointer<ProgressWidget> m_progressWidget;
|
||||
QPoint m_iconPosition;
|
||||
std::function<void()> m_cancelCallback;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
110
sources/widgets/ContextExtractor.hpp
Normal file
110
sources/widgets/ContextExtractor.hpp
Normal file
@@ -0,0 +1,110 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QTextBlock>
|
||||
#include <QTextDocument>
|
||||
|
||||
#include <texteditor/texteditor.h>
|
||||
#include <utils/textutils.h>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class ContextExtractor
|
||||
{
|
||||
public:
|
||||
static QString extractBefore(TextEditor::TextEditorWidget *editor,
|
||||
const Utils::Text::Range &range,
|
||||
int lineCount)
|
||||
{
|
||||
if (!editor || lineCount <= 0) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
QTextDocument *doc = editor->document();
|
||||
int startLine = range.begin.line;
|
||||
int contextStartLine = qMax(1, startLine - lineCount);
|
||||
|
||||
QStringList contextLines;
|
||||
for (int line = contextStartLine; line < startLine; ++line) {
|
||||
QTextBlock block = doc->findBlockByNumber(line - 1);
|
||||
if (block.isValid()) {
|
||||
contextLines.append(block.text());
|
||||
}
|
||||
}
|
||||
|
||||
return contextLines.join('\n');
|
||||
}
|
||||
|
||||
static QString extractAfter(TextEditor::TextEditorWidget *editor,
|
||||
const Utils::Text::Range &range,
|
||||
int lineCount)
|
||||
{
|
||||
if (!editor || lineCount <= 0) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
QTextDocument *doc = editor->document();
|
||||
int endLine = range.end.line;
|
||||
int totalLines = doc->blockCount();
|
||||
int contextEndLine = qMin(totalLines, endLine + lineCount);
|
||||
|
||||
QStringList contextLines;
|
||||
for (int line = endLine + 1; line <= contextEndLine; ++line) {
|
||||
QTextBlock block = doc->findBlockByNumber(line - 1);
|
||||
if (block.isValid()) {
|
||||
contextLines.append(block.text());
|
||||
}
|
||||
}
|
||||
|
||||
return contextLines.join('\n');
|
||||
}
|
||||
|
||||
static QString extractLineContext(QTextDocument *doc, int position, bool before)
|
||||
{
|
||||
QTextCursor cursor(doc);
|
||||
cursor.setPosition(position);
|
||||
|
||||
if (before) {
|
||||
int posInBlock = cursor.positionInBlock();
|
||||
cursor.movePosition(QTextCursor::StartOfBlock);
|
||||
cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, posInBlock);
|
||||
return cursor.selectedText();
|
||||
} else {
|
||||
cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
||||
return cursor.selectedText();
|
||||
}
|
||||
}
|
||||
|
||||
static QStringList extractSurroundingLines(QTextDocument *doc, int position, int linesBefore, int linesAfter)
|
||||
{
|
||||
QTextCursor cursor(doc);
|
||||
cursor.setPosition(position);
|
||||
QTextBlock currentBlock = cursor.block();
|
||||
|
||||
QStringList result;
|
||||
|
||||
QTextBlock blockBefore = currentBlock.previous();
|
||||
QStringList beforeLines;
|
||||
for (int i = 0; i < linesBefore && blockBefore.isValid(); ++i) {
|
||||
beforeLines.prepend(blockBefore.text());
|
||||
blockBefore = blockBefore.previous();
|
||||
}
|
||||
result.append(beforeLines);
|
||||
|
||||
QTextBlock blockAfter = currentBlock.next();
|
||||
for (int i = 0; i < linesAfter && blockAfter.isValid(); ++i) {
|
||||
result.append(blockAfter.text());
|
||||
blockAfter = blockAfter.next();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
||||
239
sources/widgets/CustomInstructionsManager.cpp
Normal file
239
sources/widgets/CustomInstructionsManager.cpp
Normal file
@@ -0,0 +1,239 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "CustomInstructionsManager.hpp"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QUuid>
|
||||
|
||||
#include <coreplugin/icore.h>
|
||||
|
||||
#include <logger/Logger.hpp>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
CustomInstructionsManager::CustomInstructionsManager(QObject *parent)
|
||||
: QObject(parent)
|
||||
{}
|
||||
|
||||
CustomInstructionsManager &CustomInstructionsManager::instance()
|
||||
{
|
||||
static CustomInstructionsManager instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
QString CustomInstructionsManager::getInstructionsDirectory() const
|
||||
{
|
||||
QString path = QString("%1/qodeassist/quick_refactor/instructions")
|
||||
.arg(Core::ICore::userResourcePath().toFSPathString());
|
||||
return path;
|
||||
}
|
||||
|
||||
bool CustomInstructionsManager::ensureDirectoryExists() const
|
||||
{
|
||||
QDir dir(getInstructionsDirectory());
|
||||
if (!dir.exists()) {
|
||||
return dir.mkpath(".");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CustomInstructionsManager::loadInstructions()
|
||||
{
|
||||
m_instructions.clear();
|
||||
|
||||
if (!ensureDirectoryExists()) {
|
||||
LOG_MESSAGE("Failed to create instructions directory");
|
||||
return false;
|
||||
}
|
||||
|
||||
QDir dir(getInstructionsDirectory());
|
||||
QStringList filters;
|
||||
filters << "*.json";
|
||||
QFileInfoList files = dir.entryInfoList(filters, QDir::Files);
|
||||
|
||||
for (const QFileInfo &fileInfo : files) {
|
||||
QFile file(fileInfo.absoluteFilePath());
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
LOG_MESSAGE(QString("Failed to open instruction file: %1").arg(fileInfo.fileName()));
|
||||
continue;
|
||||
}
|
||||
|
||||
QJsonParseError error;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error);
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
LOG_MESSAGE(
|
||||
QString("Failed to parse instruction file %1: %2")
|
||||
.arg(fileInfo.fileName(), error.errorString()));
|
||||
continue;
|
||||
}
|
||||
|
||||
QJsonObject obj = doc.object();
|
||||
CustomInstruction instruction;
|
||||
instruction.id = obj["id"].toString();
|
||||
instruction.name = obj["name"].toString();
|
||||
instruction.body = obj["body"].toString();
|
||||
instruction.isDefault = obj["default"].toBool(false);
|
||||
|
||||
if (instruction.id.isEmpty() || instruction.name.isEmpty()) {
|
||||
LOG_MESSAGE(QString("Invalid instruction in file: %1").arg(fileInfo.fileName()));
|
||||
continue;
|
||||
}
|
||||
|
||||
m_instructions.append(instruction);
|
||||
}
|
||||
|
||||
LOG_MESSAGE(QString("Loaded %1 custom instructions").arg(m_instructions.size()));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CustomInstructionsManager::saveInstruction(const CustomInstruction &instruction)
|
||||
{
|
||||
if (!ensureDirectoryExists()) {
|
||||
LOG_MESSAGE("Failed to create instructions directory");
|
||||
return false;
|
||||
}
|
||||
|
||||
CustomInstruction newInstruction = instruction;
|
||||
QString oldFileName;
|
||||
|
||||
if (newInstruction.id.isEmpty()) {
|
||||
newInstruction.id = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
} else {
|
||||
// Check if instruction with this ID already exists and get old file name
|
||||
for (int i = 0; i < m_instructions.size(); ++i) {
|
||||
if (m_instructions[i].id == newInstruction.id) {
|
||||
// Build old filename to delete it if name changed
|
||||
QString oldName = m_instructions[i].name;
|
||||
oldName.replace(' ', '_');
|
||||
oldFileName = QString("%1/%2_%3.json")
|
||||
.arg(getInstructionsDirectory(), oldName, newInstruction.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If this instruction is marked as default, remove default flag from all others
|
||||
if (newInstruction.isDefault) {
|
||||
for (int i = 0; i < m_instructions.size(); ++i) {
|
||||
if (m_instructions[i].id != newInstruction.id && m_instructions[i].isDefault) {
|
||||
m_instructions[i].isDefault = false;
|
||||
|
||||
// Update the file for this instruction
|
||||
QString sanitizedName = m_instructions[i].name;
|
||||
sanitizedName.replace(' ', '_');
|
||||
QString otherFileName = QString("%1/%2_%3.json")
|
||||
.arg(getInstructionsDirectory(), sanitizedName, m_instructions[i].id);
|
||||
|
||||
QJsonObject otherObj;
|
||||
otherObj["id"] = m_instructions[i].id;
|
||||
otherObj["name"] = m_instructions[i].name;
|
||||
otherObj["body"] = m_instructions[i].body;
|
||||
otherObj["default"] = false;
|
||||
otherObj["version"] = "0.1";
|
||||
|
||||
QFile otherFile(otherFileName);
|
||||
if (otherFile.open(QIODevice::WriteOnly)) {
|
||||
otherFile.write(QJsonDocument(otherObj).toJson(QJsonDocument::Indented));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int existingIndex = -1;
|
||||
for (int i = 0; i < m_instructions.size(); ++i) {
|
||||
if (m_instructions[i].id == newInstruction.id) {
|
||||
existingIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
QJsonObject obj;
|
||||
obj["id"] = newInstruction.id;
|
||||
obj["name"] = newInstruction.name;
|
||||
obj["body"] = newInstruction.body;
|
||||
obj["default"] = newInstruction.isDefault;
|
||||
obj["version"] = "0.1";
|
||||
|
||||
QJsonDocument doc(obj);
|
||||
|
||||
QString sanitizedName = newInstruction.name;
|
||||
sanitizedName.replace(' ', '_');
|
||||
QString fileName = QString("%1/%2_%3.json")
|
||||
.arg(getInstructionsDirectory(), sanitizedName, newInstruction.id);
|
||||
|
||||
if (!oldFileName.isEmpty() && oldFileName != fileName) {
|
||||
QFile::remove(oldFileName);
|
||||
}
|
||||
|
||||
QFile file(fileName);
|
||||
if (!file.open(QIODevice::WriteOnly)) {
|
||||
LOG_MESSAGE(QString("Failed to save instruction to file: %1").arg(fileName));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (file.write(doc.toJson(QJsonDocument::Indented)) == -1) {
|
||||
LOG_MESSAGE(QString("Failed to write instruction data: %1").arg(file.errorString()));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
m_instructions[existingIndex] = newInstruction;
|
||||
} else {
|
||||
m_instructions.append(newInstruction);
|
||||
}
|
||||
|
||||
emit instructionsChanged();
|
||||
LOG_MESSAGE(QString("Saved custom instruction: %1").arg(newInstruction.name));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CustomInstructionsManager::deleteInstruction(const QString &id)
|
||||
{
|
||||
int index = -1;
|
||||
for (int i = 0; i < m_instructions.size(); ++i) {
|
||||
if (m_instructions[i].id == id) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (index < 0) {
|
||||
LOG_MESSAGE(QString("Instruction not found: %1").arg(id));
|
||||
return false;
|
||||
}
|
||||
|
||||
QString sanitizedName = m_instructions[index].name;
|
||||
sanitizedName.replace(' ', '_');
|
||||
QString fileName = QString("%1/%2_%3.json")
|
||||
.arg(getInstructionsDirectory(), sanitizedName, id);
|
||||
|
||||
QFile file(fileName);
|
||||
if (!file.remove()) {
|
||||
LOG_MESSAGE(QString("Failed to delete instruction file: %1").arg(fileName));
|
||||
return false;
|
||||
}
|
||||
|
||||
m_instructions.removeAt(index);
|
||||
emit instructionsChanged();
|
||||
LOG_MESSAGE(QString("Deleted custom instruction with id: %1").arg(id));
|
||||
return true;
|
||||
}
|
||||
|
||||
CustomInstruction CustomInstructionsManager::getInstructionById(const QString &id) const
|
||||
{
|
||||
for (const CustomInstruction &instruction : m_instructions) {
|
||||
if (instruction.id == id) {
|
||||
return instruction;
|
||||
}
|
||||
}
|
||||
return CustomInstruction();
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
||||
49
sources/widgets/CustomInstructionsManager.hpp
Normal file
49
sources/widgets/CustomInstructionsManager.hpp
Normal file
@@ -0,0 +1,49 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
struct CustomInstruction
|
||||
{
|
||||
QString id;
|
||||
QString name;
|
||||
QString body;
|
||||
bool isDefault = false;
|
||||
};
|
||||
|
||||
class CustomInstructionsManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
static CustomInstructionsManager &instance();
|
||||
|
||||
bool loadInstructions();
|
||||
bool saveInstruction(const CustomInstruction &instruction);
|
||||
bool deleteInstruction(const QString &id);
|
||||
|
||||
QVector<CustomInstruction> instructions() const { return m_instructions; }
|
||||
CustomInstruction getInstructionById(const QString &id) const;
|
||||
|
||||
signals:
|
||||
void instructionsChanged();
|
||||
|
||||
private:
|
||||
explicit CustomInstructionsManager(QObject *parent = nullptr);
|
||||
~CustomInstructionsManager() override = default;
|
||||
|
||||
QString getInstructionsDirectory() const;
|
||||
bool ensureDirectoryExists() const;
|
||||
|
||||
QVector<CustomInstruction> m_instructions;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
||||
56
sources/widgets/DiffStatistics.hpp
Normal file
56
sources/widgets/DiffStatistics.hpp
Normal file
@@ -0,0 +1,56 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QCoreApplication>
|
||||
|
||||
#include <utils/differ.h>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class DiffStatistics
|
||||
{
|
||||
Q_DECLARE_TR_FUNCTIONS(DiffStatistics)
|
||||
|
||||
public:
|
||||
DiffStatistics() = default;
|
||||
|
||||
void calculate(const QList<Utils::Diff> &diffList)
|
||||
{
|
||||
m_linesAdded = 0;
|
||||
m_linesRemoved = 0;
|
||||
|
||||
for (const auto &diff : diffList) {
|
||||
if (diff.command == Utils::Diff::Insert) {
|
||||
m_linesAdded += diff.text.count('\n') + (diff.text.isEmpty() ? 0 : 1);
|
||||
} else if (diff.command == Utils::Diff::Delete) {
|
||||
m_linesRemoved += diff.text.count('\n') + (diff.text.isEmpty() ? 0 : 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int linesAdded() const { return m_linesAdded; }
|
||||
int linesRemoved() const { return m_linesRemoved; }
|
||||
|
||||
QString formatSummary() const
|
||||
{
|
||||
if (m_linesAdded > 0 && m_linesRemoved > 0) {
|
||||
return tr("+%1 lines, -%2 lines").arg(m_linesAdded).arg(m_linesRemoved);
|
||||
} else if (m_linesAdded > 0) {
|
||||
return tr("+%1 lines").arg(m_linesAdded);
|
||||
} else if (m_linesRemoved > 0) {
|
||||
return tr("-%1 lines").arg(m_linesRemoved);
|
||||
}
|
||||
return tr("No changes");
|
||||
}
|
||||
|
||||
private:
|
||||
int m_linesAdded{0};
|
||||
int m_linesRemoved{0};
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
||||
125
sources/widgets/EditorChatButton.cpp
Normal file
125
sources/widgets/EditorChatButton.cpp
Normal file
@@ -0,0 +1,125 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "EditorChatButton.hpp"
|
||||
|
||||
#include <utils/theme/theme.h>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
EditorChatButton::EditorChatButton(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
m_textColor = Utils::creatorTheme()->color(Utils::Theme::TextColorNormal);
|
||||
m_backgroundColor = Utils::creatorTheme()->color(Utils::Theme::BackgroundColorNormal);
|
||||
|
||||
m_logoPixmap = QPixmap(":/resources/images/qoderassist-icon.png");
|
||||
|
||||
if (!m_logoPixmap.isNull()) {
|
||||
QImage image = m_logoPixmap.toImage();
|
||||
image = image.convertToFormat(QImage::Format_ARGB32);
|
||||
|
||||
for (int y = 0; y < image.height(); ++y) {
|
||||
for (int x = 0; x < image.width(); ++x) {
|
||||
QColor pixelColor = QColor::fromRgba(image.pixel(x, y));
|
||||
|
||||
int brightness = (pixelColor.red() + pixelColor.green() + pixelColor.blue()) / 3;
|
||||
|
||||
if (brightness > 200) {
|
||||
pixelColor.setAlpha(0);
|
||||
image.setPixelColor(x, y, pixelColor);
|
||||
} else if (pixelColor.alpha() > 0) {
|
||||
int alpha = pixelColor.alpha();
|
||||
pixelColor = m_textColor;
|
||||
pixelColor.setAlpha(alpha);
|
||||
image.setPixelColor(x, y, pixelColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_logoPixmap = QPixmap::fromImage(image);
|
||||
m_logoPixmap = m_logoPixmap.scaled(24, 24, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
}
|
||||
|
||||
setFixedSize(40, 40);
|
||||
setCursor(Qt::PointingHandCursor);
|
||||
setToolTip(tr("Open QodeAssist Chat"));
|
||||
}
|
||||
|
||||
EditorChatButton::~EditorChatButton() = default;
|
||||
|
||||
void EditorChatButton::paintEvent(QPaintEvent *)
|
||||
{
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
QColor bgColor = m_backgroundColor;
|
||||
if (m_isPressed) {
|
||||
bgColor = bgColor.darker(120);
|
||||
} else if (m_isHovered) {
|
||||
bgColor = bgColor.lighter(110);
|
||||
}
|
||||
painter.fillRect(rect(), bgColor);
|
||||
|
||||
QRect buttonRect = rect().adjusted(4, 4, -4, -4);
|
||||
painter.setPen(Qt::NoPen);
|
||||
QColor buttonBgColor
|
||||
= m_isPressed ? Utils::creatorTheme()->color(Utils::Theme::BackgroundColorHover).darker(110)
|
||||
: Utils::creatorTheme()->color(Utils::Theme::BackgroundColorHover);
|
||||
|
||||
if (m_isHovered) {
|
||||
buttonBgColor = buttonBgColor.lighter(110);
|
||||
}
|
||||
painter.setBrush(buttonBgColor);
|
||||
painter.drawEllipse(buttonRect);
|
||||
|
||||
if (!m_logoPixmap.isNull()) {
|
||||
QRect logoRect(
|
||||
(width() - m_logoPixmap.width()) / 2,
|
||||
(height() - m_logoPixmap.height()) / 2,
|
||||
m_logoPixmap.width(),
|
||||
m_logoPixmap.height());
|
||||
painter.drawPixmap(logoRect, m_logoPixmap);
|
||||
}
|
||||
}
|
||||
|
||||
void EditorChatButton::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->button() == Qt::LeftButton) {
|
||||
m_isPressed = true;
|
||||
update();
|
||||
}
|
||||
QWidget::mousePressEvent(event);
|
||||
}
|
||||
|
||||
void EditorChatButton::mouseReleaseEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->button() == Qt::LeftButton && m_isPressed) {
|
||||
m_isPressed = false;
|
||||
update();
|
||||
if (rect().contains(event->pos())) {
|
||||
emit clicked();
|
||||
}
|
||||
}
|
||||
QWidget::mouseReleaseEvent(event);
|
||||
}
|
||||
|
||||
void EditorChatButton::enterEvent(QEnterEvent *event)
|
||||
{
|
||||
m_isHovered = true;
|
||||
update();
|
||||
QWidget::enterEvent(event);
|
||||
}
|
||||
|
||||
void EditorChatButton::leaveEvent(QEvent *event)
|
||||
{
|
||||
m_isHovered = false;
|
||||
m_isPressed = false;
|
||||
update();
|
||||
QWidget::leaveEvent(event);
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
38
sources/widgets/EditorChatButton.hpp
Normal file
38
sources/widgets/EditorChatButton.hpp
Normal file
@@ -0,0 +1,38 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QPushButton>
|
||||
#include <QTimer>
|
||||
#include <QWidget>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class EditorChatButton : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit EditorChatButton(QWidget *parent = nullptr);
|
||||
~EditorChatButton() override;
|
||||
|
||||
signals:
|
||||
void clicked();
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
void mouseReleaseEvent(QMouseEvent *event) override;
|
||||
void enterEvent(QEnterEvent *event) override;
|
||||
void leaveEvent(QEvent *event) override;
|
||||
|
||||
private:
|
||||
QPixmap m_logoPixmap;
|
||||
QColor m_textColor;
|
||||
QColor m_backgroundColor;
|
||||
bool m_isPressed = false;
|
||||
bool m_isHovered = false;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
74
sources/widgets/EditorChatButtonHandler.cpp
Normal file
74
sources/widgets/EditorChatButtonHandler.cpp
Normal file
@@ -0,0 +1,74 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "EditorChatButtonHandler.hpp"
|
||||
#include "EditorChatButton.hpp"
|
||||
|
||||
#include <texteditor/texteditor.h>
|
||||
#include <utils/tooltip/tooltip.h>
|
||||
#include <QPoint>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
EditorChatButtonHandler::~EditorChatButtonHandler()
|
||||
{
|
||||
delete m_chatButton;
|
||||
}
|
||||
|
||||
void EditorChatButtonHandler::showButton(TextEditor::TextEditorWidget *widget)
|
||||
{
|
||||
if (!widget)
|
||||
return;
|
||||
|
||||
m_widget = widget;
|
||||
|
||||
identifyMatch(widget, widget->textCursor().position(), [this](auto priority) {
|
||||
if (priority != Priority_None && m_widget) {
|
||||
const QTextCursor cursor = m_widget->textCursor();
|
||||
const QRect selectionRect = m_widget->cursorRect(cursor);
|
||||
m_cursorPosition = m_widget->viewport()->mapToGlobal(selectionRect.topLeft())
|
||||
- Utils::ToolTip::offsetFromPosition();
|
||||
operateTooltip(m_widget, m_cursorPosition);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void EditorChatButtonHandler::hideButton()
|
||||
{
|
||||
Utils::ToolTip::hide();
|
||||
}
|
||||
|
||||
void EditorChatButtonHandler::identifyMatch(
|
||||
TextEditor::TextEditorWidget *editorWidget, int pos, ReportPriority report)
|
||||
{
|
||||
if (!editorWidget) {
|
||||
report(Priority_None);
|
||||
return;
|
||||
}
|
||||
|
||||
report(Priority_Tooltip);
|
||||
}
|
||||
|
||||
void EditorChatButtonHandler::operateTooltip(
|
||||
TextEditor::TextEditorWidget *editorWidget, const QPoint &point)
|
||||
{
|
||||
if (!editorWidget)
|
||||
return;
|
||||
|
||||
if (!Utils::ToolTip::isVisible()) {
|
||||
m_chatButton = new EditorChatButton(editorWidget);
|
||||
m_buttonHeight = m_chatButton->height();
|
||||
|
||||
QPoint showPoint = point;
|
||||
showPoint.ry() -= m_buttonHeight;
|
||||
|
||||
Utils::ToolTip::show(showPoint, m_chatButton, editorWidget);
|
||||
} else {
|
||||
QPoint showPoint = point;
|
||||
showPoint.ry() -= m_buttonHeight;
|
||||
Utils::ToolTip::move(showPoint);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
37
sources/widgets/EditorChatButtonHandler.hpp
Normal file
37
sources/widgets/EditorChatButtonHandler.hpp
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "widgets/EditorChatButton.hpp"
|
||||
#include <texteditor/basehoverhandler.h>
|
||||
#include <QPointer>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class EditorChatButtonHandler : public TextEditor::BaseHoverHandler
|
||||
{
|
||||
public:
|
||||
explicit EditorChatButtonHandler() = default;
|
||||
~EditorChatButtonHandler() override;
|
||||
|
||||
void showButton(TextEditor::TextEditorWidget *widget);
|
||||
void hideButton();
|
||||
|
||||
signals:
|
||||
void chatButtonClicked(TextEditor::TextEditorWidget *widget);
|
||||
|
||||
protected:
|
||||
void identifyMatch(
|
||||
TextEditor::TextEditorWidget *editorWidget, int pos, ReportPriority report) override;
|
||||
void operateTooltip(TextEditor::TextEditorWidget *editorWidget, const QPoint &point) override;
|
||||
|
||||
private:
|
||||
QPointer<TextEditor::TextEditorWidget> m_widget;
|
||||
QPoint m_cursorPosition;
|
||||
EditorChatButton *m_chatButton = nullptr;
|
||||
int m_buttonHeight = 0;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
198
sources/widgets/ErrorWidget.cpp
Normal file
198
sources/widgets/ErrorWidget.cpp
Normal file
@@ -0,0 +1,198 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "ErrorWidget.hpp"
|
||||
|
||||
#include <QFontMetrics>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainterPath>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
ErrorWidget::ErrorWidget(const QString &errorMessage, QWidget *parent, int autoHideMs)
|
||||
: QWidget(parent)
|
||||
, m_errorMessage(errorMessage)
|
||||
, m_autoHideTimer(nullptr)
|
||||
, m_isHovered(false)
|
||||
{
|
||||
setupColors();
|
||||
setupIcon();
|
||||
|
||||
QFont errorFont = font();
|
||||
errorFont.setPointSize(qMax(8, errorFont.pointSize() - 2));
|
||||
setFont(errorFont);
|
||||
|
||||
setFixedSize(calculateSize());
|
||||
setAttribute(Qt::WA_TranslucentBackground);
|
||||
setMouseTracking(true);
|
||||
|
||||
if (autoHideMs > 0) {
|
||||
m_autoHideTimer = new QTimer(this);
|
||||
m_autoHideTimer->setSingleShot(true);
|
||||
connect(m_autoHideTimer, &QTimer::timeout, this, [this]() {
|
||||
if (!m_isHovered) {
|
||||
emit dismissed();
|
||||
deleteLater();
|
||||
}
|
||||
});
|
||||
m_autoHideTimer->start(autoHideMs);
|
||||
}
|
||||
}
|
||||
|
||||
ErrorWidget::~ErrorWidget()
|
||||
{
|
||||
if (m_autoHideTimer) {
|
||||
m_autoHideTimer->stop();
|
||||
}
|
||||
}
|
||||
|
||||
void ErrorWidget::setErrorMessage(const QString &message)
|
||||
{
|
||||
m_errorMessage = message;
|
||||
QFont smallFont = font();
|
||||
smallFont.setPointSize(qMax(8, smallFont.pointSize() - 2));
|
||||
setFont(smallFont);
|
||||
setFixedSize(calculateSize());
|
||||
update();
|
||||
}
|
||||
|
||||
void ErrorWidget::setupColors()
|
||||
{
|
||||
m_textColor = Utils::creatorTheme()->color(Utils::Theme::TextColorNormal);
|
||||
m_backgroundColor = Utils::creatorTheme()->color(Utils::Theme::BackgroundColorNormal);
|
||||
m_errorColor = Utils::creatorTheme()->color(Utils::Theme::TextColorError);
|
||||
}
|
||||
|
||||
void ErrorWidget::setupIcon()
|
||||
{
|
||||
// Create a smaller error icon (exclamation mark in a circle)
|
||||
QPixmap pixmap(18, 18);
|
||||
pixmap.fill(Qt::transparent);
|
||||
|
||||
QPainter painter(&pixmap);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
// Draw circle
|
||||
painter.setPen(QPen(m_errorColor, 1.5));
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.drawEllipse(1, 1, 16, 16);
|
||||
|
||||
// Draw exclamation mark
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(m_errorColor);
|
||||
|
||||
// Vertical bar of exclamation
|
||||
painter.drawRect(8, 4, 2, 8);
|
||||
|
||||
// Dot of exclamation
|
||||
painter.drawRect(8, 13, 2, 2);
|
||||
|
||||
m_errorIcon = pixmap;
|
||||
}
|
||||
|
||||
QSize ErrorWidget::calculateSize() const
|
||||
{
|
||||
QFontMetrics fm(font());
|
||||
|
||||
// Maximum width for the text area
|
||||
const int maxTextWidth = 350;
|
||||
const int iconWidth = 18;
|
||||
const int padding = 8;
|
||||
const int margin = 12;
|
||||
|
||||
// Calculate text area with word wrapping
|
||||
QRect textRect = fm.boundingRect(
|
||||
0, 0, maxTextWidth, 1000,
|
||||
Qt::AlignLeft | Qt::TextWordWrap,
|
||||
m_errorMessage
|
||||
);
|
||||
|
||||
// Total width: margin + icon + padding + text + margin
|
||||
int totalWidth = margin + iconWidth + padding + textRect.width() + margin;
|
||||
|
||||
// Total height: larger of icon or text, plus vertical margins
|
||||
int contentHeight = qMax(iconWidth, textRect.height());
|
||||
int totalHeight = contentHeight + margin * 2;
|
||||
|
||||
return QSize(totalWidth, totalHeight);
|
||||
}
|
||||
|
||||
void ErrorWidget::paintEvent(QPaintEvent *)
|
||||
{
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
painter.setRenderHint(QPainter::TextAntialiasing);
|
||||
|
||||
// Draw background with border
|
||||
QColor bgColor = m_backgroundColor;
|
||||
if (m_isHovered) {
|
||||
bgColor = bgColor.lighter(110);
|
||||
}
|
||||
|
||||
QPainterPath path;
|
||||
path.addRoundedRect(rect().adjusted(1, 1, -1, -1), 4, 4);
|
||||
|
||||
painter.fillPath(path, bgColor);
|
||||
painter.setPen(QPen(m_errorColor.lighter(150), 1));
|
||||
painter.drawPath(path);
|
||||
|
||||
const int iconSize = 18;
|
||||
const int padding = 8;
|
||||
const int margin = 12;
|
||||
|
||||
// Draw error icon
|
||||
if (!m_errorIcon.isNull()) {
|
||||
QRect iconRect(margin, margin, iconSize, iconSize);
|
||||
painter.drawPixmap(iconRect, m_errorIcon);
|
||||
}
|
||||
|
||||
// Draw error message with word wrap
|
||||
painter.setPen(m_textColor);
|
||||
QRect textRect = rect().adjusted(
|
||||
margin + iconSize + padding, // left
|
||||
margin, // top
|
||||
-margin, // right
|
||||
-margin // bottom
|
||||
);
|
||||
|
||||
painter.drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter | Qt::TextWordWrap, m_errorMessage);
|
||||
}
|
||||
|
||||
void ErrorWidget::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->button() == Qt::LeftButton) {
|
||||
emit dismissed();
|
||||
deleteLater();
|
||||
}
|
||||
QWidget::mousePressEvent(event);
|
||||
}
|
||||
|
||||
void ErrorWidget::enterEvent(QEnterEvent *event)
|
||||
{
|
||||
m_isHovered = true;
|
||||
update();
|
||||
|
||||
// Stop auto-hide timer while hovering
|
||||
if (m_autoHideTimer && m_autoHideTimer->isActive()) {
|
||||
m_autoHideTimer->stop();
|
||||
}
|
||||
|
||||
QWidget::enterEvent(event);
|
||||
}
|
||||
|
||||
void ErrorWidget::leaveEvent(QEvent *event)
|
||||
{
|
||||
m_isHovered = false;
|
||||
update();
|
||||
|
||||
// Restart auto-hide timer when leaving
|
||||
if (m_autoHideTimer) {
|
||||
m_autoHideTimer->start(2000); // Give 2 more seconds after leaving
|
||||
}
|
||||
|
||||
QWidget::leaveEvent(event);
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
||||
50
sources/widgets/ErrorWidget.hpp
Normal file
50
sources/widgets/ErrorWidget.hpp
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QPainter>
|
||||
#include <QTimer>
|
||||
#include <QWidget>
|
||||
|
||||
#include <utils/theme/theme.h>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class ErrorWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ErrorWidget(const QString &errorMessage, QWidget *parent = nullptr, int autoHideMs = 5000);
|
||||
~ErrorWidget();
|
||||
|
||||
void setErrorMessage(const QString &message);
|
||||
|
||||
QString errorMessage() const { return m_errorMessage; }
|
||||
|
||||
signals:
|
||||
void dismissed();
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *) override;
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
void enterEvent(QEnterEvent *event) override;
|
||||
void leaveEvent(QEvent *event) override;
|
||||
|
||||
private:
|
||||
QString m_errorMessage;
|
||||
QTimer *m_autoHideTimer;
|
||||
QColor m_textColor;
|
||||
QColor m_backgroundColor;
|
||||
QColor m_errorColor;
|
||||
QPixmap m_errorIcon;
|
||||
bool m_isHovered;
|
||||
|
||||
void setupColors();
|
||||
void setupIcon();
|
||||
QSize calculateSize() const;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
||||
156
sources/widgets/ProgressWidget.cpp
Normal file
156
sources/widgets/ProgressWidget.cpp
Normal file
@@ -0,0 +1,156 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "ProgressWidget.hpp"
|
||||
|
||||
#include <QMouseEvent>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
ProgressWidget::ProgressWidget(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, m_isHovered(false)
|
||||
{
|
||||
m_dotPosition = 0;
|
||||
m_timer.setInterval(300);
|
||||
connect(&m_timer, &QTimer::timeout, this, [this]() {
|
||||
m_dotPosition = (m_dotPosition + 1) % 4;
|
||||
update();
|
||||
});
|
||||
m_timer.start();
|
||||
|
||||
m_textColor = Utils::creatorTheme()->color(Utils::Theme::TextColorNormal);
|
||||
m_backgroundColor = Utils::creatorTheme()->color(Utils::Theme::BackgroundColorNormal);
|
||||
|
||||
m_logoPixmap = QPixmap(":/resources/images/qoderassist-icon.png");
|
||||
|
||||
if (!m_logoPixmap.isNull()) {
|
||||
QImage image = m_logoPixmap.toImage();
|
||||
image = image.convertToFormat(QImage::Format_ARGB32);
|
||||
|
||||
for (int y = 0; y < image.height(); ++y) {
|
||||
for (int x = 0; x < image.width(); ++x) {
|
||||
QColor pixelColor = QColor::fromRgba(image.pixel(x, y));
|
||||
|
||||
int brightness = (pixelColor.red() + pixelColor.green() + pixelColor.blue()) / 3;
|
||||
|
||||
if (brightness > 200) {
|
||||
pixelColor.setAlpha(0);
|
||||
image.setPixelColor(x, y, pixelColor);
|
||||
} else if (pixelColor.alpha() > 0) {
|
||||
int alpha = pixelColor.alpha();
|
||||
pixelColor = m_textColor;
|
||||
pixelColor.setAlpha(alpha);
|
||||
image.setPixelColor(x, y, pixelColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_logoPixmap = QPixmap::fromImage(image);
|
||||
m_logoPixmap = m_logoPixmap.scaled(24, 24, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
}
|
||||
|
||||
setFixedSize(40, 40);
|
||||
setMouseTracking(true);
|
||||
}
|
||||
|
||||
ProgressWidget::~ProgressWidget()
|
||||
{
|
||||
m_timer.stop();
|
||||
}
|
||||
|
||||
void ProgressWidget::setCancelCallback(std::function<void()> callback)
|
||||
{
|
||||
m_cancelCallback = callback;
|
||||
}
|
||||
|
||||
void ProgressWidget::paintEvent(QPaintEvent *)
|
||||
{
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
painter.fillRect(rect(), m_backgroundColor);
|
||||
|
||||
if (!m_isHovered) {
|
||||
if (!m_logoPixmap.isNull()) {
|
||||
QRect logoRect(
|
||||
(width() - m_logoPixmap.width()) / 2,
|
||||
5,
|
||||
m_logoPixmap.width(),
|
||||
m_logoPixmap.height());
|
||||
painter.drawPixmap(logoRect, m_logoPixmap);
|
||||
}
|
||||
|
||||
int dotSpacing = 6;
|
||||
int dotSize = 4;
|
||||
int totalDotWidth = 3 * dotSize + 2 * dotSpacing;
|
||||
int startX = (width() - totalDotWidth) / 2;
|
||||
int dotY = height() - 8;
|
||||
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
QColor dotColor = m_textColor;
|
||||
|
||||
if (m_dotPosition == 0) {
|
||||
dotColor.setAlpha(128);
|
||||
} else {
|
||||
if (i == m_dotPosition - 1) {
|
||||
dotColor.setAlpha(255);
|
||||
} else {
|
||||
dotColor.setAlpha(80);
|
||||
}
|
||||
}
|
||||
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(dotColor);
|
||||
|
||||
int x = startX + i * (dotSize + dotSpacing);
|
||||
painter.drawEllipse(x, dotY, dotSize, dotSize);
|
||||
}
|
||||
}
|
||||
|
||||
if (m_isHovered) {
|
||||
int closeSize = 14;
|
||||
int centerX = width() / 2;
|
||||
int centerY = height() / 2;
|
||||
|
||||
QPen closePen(m_textColor, 2);
|
||||
closePen.setCapStyle(Qt::RoundCap);
|
||||
painter.setPen(closePen);
|
||||
|
||||
int offset = closeSize / 2;
|
||||
painter.drawLine(centerX - offset, centerY - offset, centerX + offset, centerY + offset);
|
||||
painter.drawLine(centerX + offset, centerY - offset, centerX - offset, centerY + offset);
|
||||
}
|
||||
}
|
||||
|
||||
void ProgressWidget::enterEvent(QEnterEvent *event)
|
||||
{
|
||||
Q_UNUSED(event);
|
||||
m_isHovered = true;
|
||||
setCursor(Qt::PointingHandCursor);
|
||||
update();
|
||||
}
|
||||
|
||||
void ProgressWidget::leaveEvent(QEvent *event)
|
||||
{
|
||||
Q_UNUSED(event);
|
||||
m_isHovered = false;
|
||||
setCursor(Qt::ArrowCursor);
|
||||
update();
|
||||
}
|
||||
|
||||
void ProgressWidget::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->button() == Qt::LeftButton && m_isHovered) {
|
||||
event->accept();
|
||||
auto callback = m_cancelCallback;
|
||||
emit cancelRequested();
|
||||
if (callback)
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
QWidget::mousePressEvent(event);
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
45
sources/widgets/ProgressWidget.hpp
Normal file
45
sources/widgets/ProgressWidget.hpp
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QPainter>
|
||||
#include <QTimer>
|
||||
#include <QWidget>
|
||||
#include <functional>
|
||||
|
||||
#include <utils/progressindicator.h>
|
||||
#include <utils/theme/theme.h>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class ProgressWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
ProgressWidget(QWidget *parent = nullptr);
|
||||
~ProgressWidget();
|
||||
|
||||
void setCancelCallback(std::function<void()> callback);
|
||||
|
||||
signals:
|
||||
void cancelRequested();
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *) override;
|
||||
void enterEvent(QEnterEvent *event) override;
|
||||
void leaveEvent(QEvent *event) override;
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
|
||||
private:
|
||||
QTimer m_timer;
|
||||
int m_dotPosition;
|
||||
QColor m_textColor;
|
||||
QColor m_backgroundColor;
|
||||
QPixmap m_logoPixmap;
|
||||
bool m_isHovered;
|
||||
std::function<void()> m_cancelCallback;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
650
sources/widgets/QuickRefactorDialog.cpp
Normal file
650
sources/widgets/QuickRefactorDialog.cpp
Normal file
@@ -0,0 +1,650 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "QuickRefactorDialog.hpp"
|
||||
#include "AddCustomInstructionDialog.hpp"
|
||||
#include "CustomInstructionsManager.hpp"
|
||||
#include "plugin/QodeAssisttr.h"
|
||||
|
||||
#include "settings/ConfigurationManager.hpp"
|
||||
#include "settings/GeneralSettings.hpp"
|
||||
#include "settings/QuickRefactorSettings.hpp"
|
||||
#include "settings/SettingsConstants.hpp"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QComboBox>
|
||||
#include <QCompleter>
|
||||
#include <QDesktopServices>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDir>
|
||||
#include <QFontMetrics>
|
||||
#include <QFrame>
|
||||
#include <QHBoxLayout>
|
||||
#include <QIcon>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
#include <QPainter>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QScreen>
|
||||
#include <QStringListModel>
|
||||
#include <QSvgRenderer>
|
||||
#include <QTimer>
|
||||
#include <QToolButton>
|
||||
#include <QUrl>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include <coreplugin/icore.h>
|
||||
|
||||
#include <utils/theme/theme.h>
|
||||
#include <utils/utilsicons.h>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
static QIcon createThemedIcon(const QString &svgPath, const QColor &color)
|
||||
{
|
||||
QSvgRenderer renderer(svgPath);
|
||||
if (!renderer.isValid()) {
|
||||
return QIcon();
|
||||
}
|
||||
|
||||
QSize iconSize(16, 16);
|
||||
QPixmap pixmap(iconSize);
|
||||
pixmap.fill(Qt::transparent);
|
||||
|
||||
QPainter painter(&pixmap);
|
||||
renderer.render(&painter);
|
||||
painter.end();
|
||||
|
||||
QImage image = pixmap.toImage().convertToFormat(QImage::Format_ARGB32);
|
||||
|
||||
uchar *bits = image.bits();
|
||||
const int bytesPerPixel = 4;
|
||||
const int totalBytes = image.width() * image.height() * bytesPerPixel;
|
||||
|
||||
const int newR = color.red();
|
||||
const int newG = color.green();
|
||||
const int newB = color.blue();
|
||||
|
||||
for (int i = 0; i < totalBytes; i += bytesPerPixel) {
|
||||
int alpha = bits[i + 3];
|
||||
if (alpha > 0) {
|
||||
bits[i] = newB;
|
||||
bits[i + 1] = newG;
|
||||
bits[i + 2] = newR;
|
||||
}
|
||||
}
|
||||
|
||||
return QIcon(QPixmap::fromImage(image));
|
||||
}
|
||||
|
||||
QuickRefactorDialog::QuickRefactorDialog(QWidget *parent, const QString &lastInstructions)
|
||||
: QDialog(parent)
|
||||
, m_lastInstructions(lastInstructions)
|
||||
{
|
||||
setWindowTitle(Tr::tr("Quick Refactor"));
|
||||
setupUi();
|
||||
|
||||
if (!m_lastInstructions.isEmpty()) {
|
||||
m_instructionEdit->setPlainText(m_lastInstructions);
|
||||
m_instructionEdit->selectAll();
|
||||
}
|
||||
|
||||
QTimer::singleShot(0, this, &QuickRefactorDialog::updateDialogSize);
|
||||
m_instructionEdit->installEventFilter(this);
|
||||
m_commandsComboBox->installEventFilter(this);
|
||||
updateDialogSize();
|
||||
|
||||
m_instructionEdit->setFocus();
|
||||
}
|
||||
|
||||
void QuickRefactorDialog::setupUi()
|
||||
{
|
||||
QVBoxLayout *mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(10, 10, 10, 10);
|
||||
mainLayout->setSpacing(8);
|
||||
|
||||
QHBoxLayout *actionsLayout = new QHBoxLayout();
|
||||
actionsLayout->setSpacing(4);
|
||||
createActionButtons();
|
||||
actionsLayout->addWidget(m_repeatButton);
|
||||
actionsLayout->addWidget(m_improveButton);
|
||||
actionsLayout->addWidget(m_alternativeButton);
|
||||
actionsLayout->addStretch();
|
||||
|
||||
m_configComboBox = new QComboBox(this);
|
||||
m_configComboBox->setMinimumWidth(200);
|
||||
m_configComboBox->setToolTip(Tr::tr("Switch AI configuration"));
|
||||
actionsLayout->addWidget(m_configComboBox);
|
||||
|
||||
Utils::Theme *theme = Utils::creatorTheme();
|
||||
QColor iconColor = theme ? theme->color(Utils::Theme::TextColorNormal) : QColor(Qt::white);
|
||||
|
||||
m_toolsIconOn = createThemedIcon(":/qt/qml/ChatView/icons/tools-icon-on.svg", iconColor);
|
||||
m_toolsIconOff = createThemedIcon(":/qt/qml/ChatView/icons/tools-icon-off.svg", iconColor);
|
||||
|
||||
m_toolsButton = new QToolButton(this);
|
||||
m_toolsButton->setCheckable(true);
|
||||
m_toolsButton->setChecked(Settings::quickRefactorSettings().useTools());
|
||||
m_toolsButton->setIcon(m_toolsButton->isChecked() ? m_toolsIconOn : m_toolsIconOff);
|
||||
m_toolsButton->setToolTip(Tr::tr("Enable/Disable AI Tools"));
|
||||
m_toolsButton->setIconSize(QSize(16, 16));
|
||||
actionsLayout->addWidget(m_toolsButton);
|
||||
|
||||
connect(m_toolsButton, &QToolButton::toggled, this, [this](bool checked) {
|
||||
m_toolsButton->setIcon(checked ? m_toolsIconOn : m_toolsIconOff);
|
||||
Settings::quickRefactorSettings().useTools.setValue(checked);
|
||||
Settings::quickRefactorSettings().writeSettings();
|
||||
});
|
||||
|
||||
m_thinkingIconOn = createThemedIcon(":/qt/qml/ChatView/icons/thinking-icon-on.svg", iconColor);
|
||||
m_thinkingIconOff = createThemedIcon(":/qt/qml/ChatView/icons/thinking-icon-off.svg", iconColor);
|
||||
|
||||
m_thinkingButton = new QToolButton(this);
|
||||
m_thinkingButton->setCheckable(true);
|
||||
m_thinkingButton->setChecked(Settings::quickRefactorSettings().useThinking());
|
||||
m_thinkingButton->setIcon(m_thinkingButton->isChecked() ? m_thinkingIconOn : m_thinkingIconOff);
|
||||
m_thinkingButton->setToolTip(Tr::tr("Enable/Disable Thinking Mode"));
|
||||
m_thinkingButton->setIconSize(QSize(16, 16));
|
||||
actionsLayout->addWidget(m_thinkingButton);
|
||||
|
||||
connect(m_thinkingButton, &QToolButton::toggled, this, [this](bool checked) {
|
||||
m_thinkingButton->setIcon(checked ? m_thinkingIconOn : m_thinkingIconOff);
|
||||
Settings::quickRefactorSettings().useThinking.setValue(checked);
|
||||
Settings::quickRefactorSettings().writeSettings();
|
||||
});
|
||||
|
||||
m_settingsButton = new QToolButton(this);
|
||||
m_settingsButton->setIcon(Utils::Icons::SETTINGS_TOOLBAR.icon());
|
||||
m_settingsButton->setToolTip(Tr::tr("Open Quick Refactor Settings"));
|
||||
m_settingsButton->setIconSize(QSize(16, 16));
|
||||
actionsLayout->addWidget(m_settingsButton);
|
||||
|
||||
connect(m_settingsButton, &QToolButton::clicked, this, &QuickRefactorDialog::onOpenSettings);
|
||||
|
||||
mainLayout->addLayout(actionsLayout);
|
||||
|
||||
QLabel *instructionLabel = new QLabel(Tr::tr("Your Current Instruction:"), this);
|
||||
mainLayout->addWidget(instructionLabel);
|
||||
|
||||
m_instructionEdit = new QPlainTextEdit(this);
|
||||
m_instructionEdit->setMinimumHeight(80);
|
||||
m_instructionEdit->setPlaceholderText(Tr::tr("Type or edit your instruction..."));
|
||||
mainLayout->addWidget(m_instructionEdit);
|
||||
|
||||
QHBoxLayout *savedInstructionsLayout = new QHBoxLayout();
|
||||
savedInstructionsLayout->setSpacing(4);
|
||||
|
||||
QLabel *savedLabel = new QLabel(Tr::tr("Or Load saved:"), this);
|
||||
savedInstructionsLayout->addWidget(savedLabel);
|
||||
|
||||
m_commandsComboBox = new QComboBox(this);
|
||||
m_commandsComboBox->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
|
||||
m_commandsComboBox->setEditable(true);
|
||||
m_commandsComboBox->setInsertPolicy(QComboBox::NoInsert);
|
||||
m_commandsComboBox->lineEdit()->setPlaceholderText(Tr::tr("Search saved instructions..."));
|
||||
|
||||
QCompleter *completer = new QCompleter(this);
|
||||
completer->setCompletionMode(QCompleter::PopupCompletion);
|
||||
completer->setCaseSensitivity(Qt::CaseInsensitive);
|
||||
completer->setFilterMode(Qt::MatchContains);
|
||||
m_commandsComboBox->setCompleter(completer);
|
||||
|
||||
savedInstructionsLayout->addWidget(m_commandsComboBox);
|
||||
|
||||
m_addCommandButton = new QToolButton(this);
|
||||
m_addCommandButton->setText("+");
|
||||
m_addCommandButton->setToolTip(Tr::tr("Add Custom Instruction"));
|
||||
m_addCommandButton->setFocusPolicy(Qt::NoFocus);
|
||||
savedInstructionsLayout->addWidget(m_addCommandButton);
|
||||
|
||||
m_editCommandButton = new QToolButton(this);
|
||||
m_editCommandButton->setText("✎");
|
||||
m_editCommandButton->setToolTip(Tr::tr("Edit Custom Instruction"));
|
||||
m_editCommandButton->setFocusPolicy(Qt::NoFocus);
|
||||
savedInstructionsLayout->addWidget(m_editCommandButton);
|
||||
|
||||
m_deleteCommandButton = new QToolButton(this);
|
||||
m_deleteCommandButton->setText("−");
|
||||
m_deleteCommandButton->setToolTip(Tr::tr("Delete Custom Instruction"));
|
||||
m_deleteCommandButton->setFocusPolicy(Qt::NoFocus);
|
||||
savedInstructionsLayout->addWidget(m_deleteCommandButton);
|
||||
|
||||
m_openFolderButton = new QToolButton(this);
|
||||
m_openFolderButton->setText("📁");
|
||||
m_openFolderButton->setToolTip(Tr::tr("Open Instructions Folder"));
|
||||
m_openFolderButton->setFocusPolicy(Qt::NoFocus);
|
||||
savedInstructionsLayout->addWidget(m_openFolderButton);
|
||||
|
||||
mainLayout->addLayout(savedInstructionsLayout);
|
||||
|
||||
connect(
|
||||
m_instructionEdit,
|
||||
&QPlainTextEdit::textChanged,
|
||||
this,
|
||||
&QuickRefactorDialog::updateDialogSize);
|
||||
connect(
|
||||
m_commandsComboBox,
|
||||
QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
this,
|
||||
&QuickRefactorDialog::onCommandSelected);
|
||||
connect(m_addCommandButton, &QToolButton::clicked, this, &QuickRefactorDialog::onAddCustomCommand);
|
||||
connect(
|
||||
m_editCommandButton, &QToolButton::clicked, this, &QuickRefactorDialog::onEditCustomCommand);
|
||||
connect(
|
||||
m_deleteCommandButton,
|
||||
&QToolButton::clicked,
|
||||
this,
|
||||
&QuickRefactorDialog::onDeleteCustomCommand);
|
||||
connect(
|
||||
m_openFolderButton,
|
||||
&QToolButton::clicked,
|
||||
this,
|
||||
&QuickRefactorDialog::onOpenInstructionsFolder);
|
||||
|
||||
loadCustomCommands();
|
||||
loadAvailableConfigurations();
|
||||
|
||||
connect(
|
||||
m_configComboBox,
|
||||
QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
this,
|
||||
&QuickRefactorDialog::onConfigurationChanged);
|
||||
|
||||
QDialogButtonBox *buttonBox
|
||||
= new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||
connect(buttonBox, &QDialogButtonBox::accepted, this, &QuickRefactorDialog::validateAndAccept);
|
||||
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
mainLayout->addWidget(buttonBox);
|
||||
|
||||
QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok);
|
||||
QPushButton *cancelButton = buttonBox->button(QDialogButtonBox::Cancel);
|
||||
|
||||
if (okButton) {
|
||||
okButton->installEventFilter(this);
|
||||
}
|
||||
if (cancelButton) {
|
||||
cancelButton->installEventFilter(this);
|
||||
}
|
||||
|
||||
setTabOrder(m_instructionEdit, m_commandsComboBox);
|
||||
setTabOrder(m_commandsComboBox, okButton);
|
||||
setTabOrder(okButton, cancelButton);
|
||||
}
|
||||
|
||||
void QuickRefactorDialog::createActionButtons()
|
||||
{
|
||||
Utils::Icon REPEAT_ICON(
|
||||
{{":/resources/images/repeat-last-instruct-icon.png", Utils::Theme::IconsBaseColor}});
|
||||
Utils::Icon IMPROVE_ICON(
|
||||
{{":/resources/images/improve-current-code-icon.png", Utils::Theme::IconsBaseColor}});
|
||||
Utils::Icon ALTER_ICON(
|
||||
{{":/resources/images/suggest-new-icon.png", Utils::Theme::IconsBaseColor}});
|
||||
m_repeatButton = new QToolButton(this);
|
||||
m_repeatButton->setIcon(REPEAT_ICON.icon());
|
||||
m_repeatButton->setToolTip(Tr::tr("Repeat Last Instructions"));
|
||||
m_repeatButton->setEnabled(!m_lastInstructions.isEmpty());
|
||||
connect(m_repeatButton, &QToolButton::clicked, this, &QuickRefactorDialog::useLastInstructions);
|
||||
|
||||
m_improveButton = new QToolButton(this);
|
||||
m_improveButton->setIcon(IMPROVE_ICON.icon());
|
||||
m_improveButton->setToolTip(Tr::tr("Improve Current Code"));
|
||||
connect(
|
||||
m_improveButton, &QToolButton::clicked, this, &QuickRefactorDialog::useImproveCodeTemplate);
|
||||
|
||||
m_alternativeButton = new QToolButton(this);
|
||||
m_alternativeButton->setIcon(ALTER_ICON.icon());
|
||||
m_alternativeButton->setToolTip(Tr::tr("Suggest Alternative Solution"));
|
||||
connect(
|
||||
m_alternativeButton,
|
||||
&QToolButton::clicked,
|
||||
this,
|
||||
&QuickRefactorDialog::useAlternativeSolutionTemplate);
|
||||
}
|
||||
|
||||
QString QuickRefactorDialog::instructions() const
|
||||
{
|
||||
return m_instructionEdit->toPlainText().trimmed();
|
||||
}
|
||||
|
||||
void QuickRefactorDialog::setInstructions(const QString &instructions)
|
||||
{
|
||||
m_instructionEdit->setPlainText(instructions);
|
||||
}
|
||||
|
||||
QuickRefactorDialog::Action QuickRefactorDialog::selectedAction() const
|
||||
{
|
||||
return m_selectedAction;
|
||||
}
|
||||
|
||||
void QuickRefactorDialog::keyPressEvent(QKeyEvent *event)
|
||||
{
|
||||
QDialog::keyPressEvent(event);
|
||||
}
|
||||
|
||||
bool QuickRefactorDialog::eventFilter(QObject *watched, QEvent *event)
|
||||
{
|
||||
if (event->type() == QEvent::KeyPress) {
|
||||
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
|
||||
|
||||
if (watched == m_instructionEdit) {
|
||||
if (keyEvent->key() == Qt::Key_Tab) {
|
||||
m_commandsComboBox->setFocus();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (watched == m_commandsComboBox || watched == m_commandsComboBox->lineEdit()) {
|
||||
if (keyEvent->key() == Qt::Key_Tab) {
|
||||
QPushButton *okButton = findChild<QPushButton *>();
|
||||
if (okButton && okButton->text() == "OK") {
|
||||
okButton->setFocus();
|
||||
} else {
|
||||
focusNextChild();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return QDialog::eventFilter(watched, event);
|
||||
}
|
||||
|
||||
void QuickRefactorDialog::useLastInstructions()
|
||||
{
|
||||
if (!m_lastInstructions.isEmpty()) {
|
||||
m_commandsComboBox->setCurrentIndex(0);
|
||||
m_instructionEdit->setPlainText(m_lastInstructions);
|
||||
m_selectedAction = Action::RepeatLast;
|
||||
}
|
||||
accept();
|
||||
}
|
||||
|
||||
void QuickRefactorDialog::useImproveCodeTemplate()
|
||||
{
|
||||
m_commandsComboBox->setCurrentIndex(0);
|
||||
m_instructionEdit->setPlainText(
|
||||
Tr::tr(
|
||||
"Improve the selected code by enhancing readability, efficiency, and maintainability. "
|
||||
"Follow best practices for C++/Qt and fix any potential issues."));
|
||||
m_selectedAction = Action::ImproveCode;
|
||||
accept();
|
||||
}
|
||||
|
||||
void QuickRefactorDialog::useAlternativeSolutionTemplate()
|
||||
{
|
||||
m_commandsComboBox->setCurrentIndex(0);
|
||||
m_instructionEdit->setPlainText(
|
||||
Tr::tr(
|
||||
"Suggest an alternative implementation approach for the selected code. "
|
||||
"Provide a different solution that might be cleaner, more efficient, "
|
||||
"or uses different Qt/C++ patterns or idioms."));
|
||||
m_selectedAction = Action::AlternativeSolution;
|
||||
accept();
|
||||
}
|
||||
|
||||
void QuickRefactorDialog::updateDialogSize()
|
||||
{
|
||||
QString text = m_instructionEdit->toPlainText();
|
||||
|
||||
QFontMetrics fm(m_instructionEdit->font());
|
||||
|
||||
QStringList lines = text.split('\n');
|
||||
int lineCount = qMax(lines.size(), 3);
|
||||
|
||||
m_instructionEdit->setMaximumHeight(QWIDGETSIZE_MAX);
|
||||
|
||||
int lineHeight = fm.height() + 2;
|
||||
int textEditHeight = qMin(qMax(lineCount, 3) * lineHeight, 15 * lineHeight);
|
||||
m_instructionEdit->setMinimumHeight(textEditHeight);
|
||||
|
||||
int maxWidth = 500;
|
||||
for (const QString &line : lines) {
|
||||
int lineWidth = fm.horizontalAdvance(line) + 30;
|
||||
maxWidth = qMax(maxWidth, qMin(lineWidth, 800));
|
||||
}
|
||||
|
||||
QScreen *screen = QApplication::primaryScreen();
|
||||
QRect screenGeometry = screen->availableGeometry();
|
||||
|
||||
int newWidth = qMin(maxWidth + 40, screenGeometry.width() * 3 / 4);
|
||||
int newHeight = qMin(m_instructionEdit->minimumHeight() + 200, screenGeometry.height() * 3 / 4);
|
||||
|
||||
resize(newWidth, newHeight);
|
||||
}
|
||||
|
||||
void QuickRefactorDialog::loadCustomCommands()
|
||||
{
|
||||
m_commandsComboBox->clear();
|
||||
m_commandsComboBox->addItem("", QString());
|
||||
|
||||
auto &manager = CustomInstructionsManager::instance();
|
||||
const QVector<CustomInstruction> &instructions = manager.instructions();
|
||||
|
||||
QStringList instructionNames;
|
||||
|
||||
for (const CustomInstruction &instruction : instructions) {
|
||||
m_commandsComboBox->addItem(instruction.name, instruction.id);
|
||||
instructionNames.append(instruction.name);
|
||||
}
|
||||
|
||||
if (m_commandsComboBox->completer()) {
|
||||
QStringListModel *model = new QStringListModel(instructionNames, this);
|
||||
m_commandsComboBox->completer()->setModel(model);
|
||||
}
|
||||
|
||||
bool hasInstructions = !instructions.isEmpty();
|
||||
m_editCommandButton->setEnabled(hasInstructions);
|
||||
m_deleteCommandButton->setEnabled(hasInstructions);
|
||||
}
|
||||
|
||||
CustomInstruction QuickRefactorDialog::findCurrentInstruction() const
|
||||
{
|
||||
QString currentText = m_commandsComboBox->currentText().trimmed();
|
||||
if (currentText.isEmpty()) {
|
||||
return CustomInstruction();
|
||||
}
|
||||
|
||||
auto &manager = CustomInstructionsManager::instance();
|
||||
const QVector<CustomInstruction> &instructions = manager.instructions();
|
||||
|
||||
for (const CustomInstruction &instruction : instructions) {
|
||||
if (instruction.name == currentText) {
|
||||
return instruction;
|
||||
}
|
||||
}
|
||||
|
||||
int currentIndex = m_commandsComboBox->currentIndex();
|
||||
if (currentIndex > 0) {
|
||||
QString instructionId = m_commandsComboBox->itemData(currentIndex).toString();
|
||||
if (!instructionId.isEmpty()) {
|
||||
return manager.getInstructionById(instructionId);
|
||||
}
|
||||
}
|
||||
|
||||
return CustomInstruction();
|
||||
}
|
||||
|
||||
void QuickRefactorDialog::onCommandSelected(int index)
|
||||
{
|
||||
if (index <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
CustomInstruction instruction = findCurrentInstruction();
|
||||
if (!instruction.id.isEmpty()) {
|
||||
m_instructionEdit->setPlainText(instruction.body);
|
||||
}
|
||||
}
|
||||
|
||||
void QuickRefactorDialog::onAddCustomCommand()
|
||||
{
|
||||
AddCustomInstructionDialog dialog(this);
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
CustomInstruction instruction = dialog.getInstruction();
|
||||
auto &manager = CustomInstructionsManager::instance();
|
||||
|
||||
if (manager.saveInstruction(instruction)) {
|
||||
loadCustomCommands();
|
||||
m_commandsComboBox->setCurrentText(instruction.name);
|
||||
} else {
|
||||
QMessageBox::warning(
|
||||
this,
|
||||
Tr::tr("Error"),
|
||||
Tr::tr("Failed to save custom instruction. Check logs for details."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void QuickRefactorDialog::onEditCustomCommand()
|
||||
{
|
||||
CustomInstruction instruction = findCurrentInstruction();
|
||||
|
||||
if (instruction.id.isEmpty()) {
|
||||
QMessageBox::information(
|
||||
this,
|
||||
Tr::tr("No Instruction Selected"),
|
||||
Tr::tr("Please select an instruction to edit."));
|
||||
return;
|
||||
}
|
||||
|
||||
AddCustomInstructionDialog dialog(instruction, this);
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
CustomInstruction updatedInstruction = dialog.getInstruction();
|
||||
auto &manager = CustomInstructionsManager::instance();
|
||||
|
||||
if (manager.saveInstruction(updatedInstruction)) {
|
||||
loadCustomCommands();
|
||||
m_commandsComboBox->setCurrentText(updatedInstruction.name);
|
||||
} else {
|
||||
QMessageBox::warning(
|
||||
this,
|
||||
Tr::tr("Error"),
|
||||
Tr::tr("Failed to update custom instruction. Check logs for details."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void QuickRefactorDialog::onDeleteCustomCommand()
|
||||
{
|
||||
CustomInstruction instruction = findCurrentInstruction();
|
||||
|
||||
if (instruction.id.isEmpty()) {
|
||||
QMessageBox::information(
|
||||
this,
|
||||
Tr::tr("No Instruction Selected"),
|
||||
Tr::tr("Please select an instruction to delete."));
|
||||
return;
|
||||
}
|
||||
|
||||
QMessageBox::StandardButton reply = QMessageBox::question(
|
||||
this,
|
||||
Tr::tr("Confirm Deletion"),
|
||||
Tr::tr("Are you sure you want to delete the instruction '%1'?").arg(instruction.name),
|
||||
QMessageBox::Yes | QMessageBox::No);
|
||||
|
||||
if (reply == QMessageBox::Yes) {
|
||||
auto &manager = CustomInstructionsManager::instance();
|
||||
if (manager.deleteInstruction(instruction.id)) {
|
||||
loadCustomCommands();
|
||||
m_commandsComboBox->setCurrentIndex(0);
|
||||
m_commandsComboBox->clearEditText();
|
||||
} else {
|
||||
QMessageBox::warning(
|
||||
this,
|
||||
Tr::tr("Error"),
|
||||
Tr::tr("Failed to delete custom instruction. Check logs for details."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void QuickRefactorDialog::onOpenInstructionsFolder()
|
||||
{
|
||||
QString path = QString("%1/qodeassist/quick_refactor/instructions")
|
||||
.arg(Core::ICore::userResourcePath().toFSPathString());
|
||||
|
||||
QDir dir(path);
|
||||
if (!dir.exists()) {
|
||||
dir.mkpath(".");
|
||||
}
|
||||
|
||||
QUrl url = QUrl::fromLocalFile(dir.absolutePath());
|
||||
QDesktopServices::openUrl(url);
|
||||
}
|
||||
|
||||
void QuickRefactorDialog::onOpenSettings()
|
||||
{
|
||||
Settings::showSettings(Constants::QODE_ASSIST_QUICK_REFACTOR_SETTINGS_PAGE_ID);
|
||||
}
|
||||
|
||||
QString QuickRefactorDialog::selectedConfiguration() const
|
||||
{
|
||||
return m_selectedConfiguration;
|
||||
}
|
||||
|
||||
void QuickRefactorDialog::loadAvailableConfigurations()
|
||||
{
|
||||
auto &manager = Settings::ConfigurationManager::instance();
|
||||
manager.loadConfigurations(Settings::ConfigurationType::QuickRefactor);
|
||||
|
||||
QVector<Settings::AIConfiguration> configs = manager.configurations(
|
||||
Settings::ConfigurationType::QuickRefactor);
|
||||
|
||||
m_configComboBox->clear();
|
||||
m_configComboBox->addItem(Tr::tr("Current"), QString());
|
||||
|
||||
for (const Settings::AIConfiguration &config : configs) {
|
||||
m_configComboBox->addItem(config.name, config.id);
|
||||
}
|
||||
|
||||
auto &settings = Settings::generalSettings();
|
||||
QString currentProvider = settings.qrProvider.value();
|
||||
QString currentModel = settings.qrModel.value();
|
||||
QString currentConfigText = QString("%1/%2").arg(currentProvider, currentModel);
|
||||
m_configComboBox->setItemText(0, Tr::tr("Current (%1)").arg(currentConfigText));
|
||||
}
|
||||
|
||||
void QuickRefactorDialog::onConfigurationChanged(int index)
|
||||
{
|
||||
if (index == 0) {
|
||||
m_selectedConfiguration.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
QString configId = m_configComboBox->itemData(index).toString();
|
||||
m_selectedConfiguration = m_configComboBox->itemText(index);
|
||||
|
||||
auto &manager = Settings::ConfigurationManager::instance();
|
||||
Settings::AIConfiguration config
|
||||
= manager.getConfigurationById(configId, Settings::ConfigurationType::QuickRefactor);
|
||||
|
||||
if (!config.id.isEmpty()) {
|
||||
auto &settings = Settings::generalSettings();
|
||||
|
||||
settings.qrProvider.setValue(config.provider);
|
||||
settings.qrModel.setValue(config.model);
|
||||
settings.qrTemplate.setValue(config.templateName);
|
||||
settings.qrUrl.setValue(config.url);
|
||||
settings.qrCustomEndpoint.setValue(config.customEndpoint);
|
||||
|
||||
settings.writeSettings();
|
||||
}
|
||||
}
|
||||
|
||||
void QuickRefactorDialog::validateAndAccept()
|
||||
{
|
||||
QString instruction = m_instructionEdit->toPlainText().trimmed();
|
||||
|
||||
if (instruction.isEmpty()) {
|
||||
QMessageBox::warning(
|
||||
this,
|
||||
Tr::tr("No Instruction"),
|
||||
Tr::tr("Please type an instruction or select a saved one."));
|
||||
m_instructionEdit->setFocus();
|
||||
return;
|
||||
}
|
||||
|
||||
accept();
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
86
sources/widgets/QuickRefactorDialog.hpp
Normal file
86
sources/widgets/QuickRefactorDialog.hpp
Normal file
@@ -0,0 +1,86 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
#include <QString>
|
||||
#include "CustomInstructionsManager.hpp"
|
||||
|
||||
class QPlainTextEdit;
|
||||
class QToolButton;
|
||||
class QLabel;
|
||||
class QComboBox;
|
||||
class QLineEdit;
|
||||
class QFrame;
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class QuickRefactorDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum class Action { Custom, RepeatLast, ImproveCode, AlternativeSolution };
|
||||
|
||||
explicit QuickRefactorDialog(
|
||||
QWidget *parent = nullptr, const QString &lastInstructions = QString());
|
||||
~QuickRefactorDialog() override = default;
|
||||
|
||||
QString instructions() const;
|
||||
void setInstructions(const QString &instructions);
|
||||
|
||||
Action selectedAction() const;
|
||||
|
||||
QString selectedConfiguration() const;
|
||||
|
||||
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||
void keyPressEvent(QKeyEvent *event) override;
|
||||
|
||||
private slots:
|
||||
void useLastInstructions();
|
||||
void useImproveCodeTemplate();
|
||||
void useAlternativeSolutionTemplate();
|
||||
void updateDialogSize();
|
||||
void onCommandSelected(int index);
|
||||
void onAddCustomCommand();
|
||||
void onEditCustomCommand();
|
||||
void onDeleteCustomCommand();
|
||||
void onOpenInstructionsFolder();
|
||||
void onOpenSettings();
|
||||
void loadCustomCommands();
|
||||
void loadAvailableConfigurations();
|
||||
void onConfigurationChanged(int index);
|
||||
void validateAndAccept();
|
||||
|
||||
private:
|
||||
void setupUi();
|
||||
void createActionButtons();
|
||||
CustomInstruction findCurrentInstruction() const;
|
||||
|
||||
QPlainTextEdit *m_instructionEdit;
|
||||
QToolButton *m_repeatButton;
|
||||
QToolButton *m_improveButton;
|
||||
QToolButton *m_alternativeButton;
|
||||
QToolButton *m_addCommandButton;
|
||||
QToolButton *m_editCommandButton;
|
||||
QToolButton *m_deleteCommandButton;
|
||||
QToolButton *m_openFolderButton;
|
||||
QToolButton *m_settingsButton;
|
||||
QToolButton *m_toolsButton;
|
||||
QToolButton *m_thinkingButton;
|
||||
QComboBox *m_commandsComboBox;
|
||||
QComboBox *m_configComboBox;
|
||||
|
||||
Action m_selectedAction = Action::Custom;
|
||||
QString m_lastInstructions;
|
||||
QString m_selectedConfiguration;
|
||||
|
||||
QIcon m_toolsIconOn;
|
||||
QIcon m_toolsIconOff;
|
||||
QIcon m_thinkingIconOn;
|
||||
QIcon m_thinkingIconOff;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
747
sources/widgets/RefactorWidget.cpp
Normal file
747
sources/widgets/RefactorWidget.cpp
Normal file
@@ -0,0 +1,747 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "RefactorWidget.hpp"
|
||||
#include "DiffStatistics.hpp"
|
||||
|
||||
#include <texteditor/textdocument.h>
|
||||
#include <texteditor/syntaxhighlighter.h>
|
||||
|
||||
#include <QCloseEvent>
|
||||
#include <QEnterEvent>
|
||||
#include <QEvent>
|
||||
#include <QGuiApplication>
|
||||
#include <QHBoxLayout>
|
||||
#include <QKeyEvent>
|
||||
#include <QLabel>
|
||||
#include <QPainter>
|
||||
#include <QRegion>
|
||||
#include <QScreen>
|
||||
#include <QScrollBar>
|
||||
#include <QSharedPointer>
|
||||
#include <QSplitter>
|
||||
#include <QTextBlock>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include <coreplugin/icore.h>
|
||||
#include <utils/differ.h>
|
||||
#include <utils/theme/theme.h>
|
||||
|
||||
#include "settings/QuickRefactorSettings.hpp"
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
CustomSplitterHandle::CustomSplitterHandle(Qt::Orientation orientation, QSplitter *parent)
|
||||
: QSplitterHandle(orientation, parent)
|
||||
{
|
||||
if (orientation == Qt::Horizontal) {
|
||||
setCursor(Qt::SplitHCursor);
|
||||
} else {
|
||||
setCursor(Qt::SplitVCursor);
|
||||
}
|
||||
|
||||
setMouseTracking(true);
|
||||
}
|
||||
|
||||
void CustomSplitterHandle::paintEvent(QPaintEvent *)
|
||||
{
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
QColor bgColor = Utils::creatorColor(Utils::Theme::BackgroundColorHover);
|
||||
bgColor.setAlpha(m_hovered ? 150 : 50);
|
||||
painter.fillRect(rect(), bgColor);
|
||||
|
||||
QColor lineColor = Utils::creatorColor(Utils::Theme::SplitterColor);
|
||||
lineColor.setAlpha(m_hovered ? 255 : 180);
|
||||
|
||||
const int lineWidth = m_hovered ? 3 : 2;
|
||||
const int margin = 10;
|
||||
painter.setPen(QPen(lineColor, lineWidth));
|
||||
|
||||
if (orientation() == Qt::Horizontal) {
|
||||
int x = width() / 2;
|
||||
painter.drawLine(x, margin, x, height() - margin);
|
||||
|
||||
painter.setBrush(lineColor);
|
||||
int centerY = height() / 2;
|
||||
const int dotSize = m_hovered ? 3 : 2;
|
||||
const int dotSpacing = 8;
|
||||
for (int i = -2; i <= 2; ++i) {
|
||||
painter.drawEllipse(QPoint(x, centerY + i * dotSpacing), dotSize, dotSize);
|
||||
}
|
||||
} else {
|
||||
int y = height() / 2;
|
||||
painter.drawLine(margin, y, width() - margin, y);
|
||||
}
|
||||
}
|
||||
|
||||
void CustomSplitterHandle::enterEvent(QEnterEvent *event)
|
||||
{
|
||||
m_hovered = true;
|
||||
update();
|
||||
QSplitterHandle::enterEvent(event);
|
||||
}
|
||||
|
||||
void CustomSplitterHandle::leaveEvent(QEvent *event)
|
||||
{
|
||||
m_hovered = false;
|
||||
update();
|
||||
QSplitterHandle::leaveEvent(event);
|
||||
}
|
||||
|
||||
CustomSplitter::CustomSplitter(Qt::Orientation orientation, QWidget *parent)
|
||||
: QSplitter(orientation, parent)
|
||||
{
|
||||
}
|
||||
|
||||
QSplitterHandle *CustomSplitter::createHandle()
|
||||
{
|
||||
return new CustomSplitterHandle(orientation(), this);
|
||||
}
|
||||
|
||||
RefactorWidget::RefactorWidget(TextEditor::TextEditorWidget *sourceEditor, QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, m_sourceEditor(sourceEditor)
|
||||
, m_leftEditor(nullptr)
|
||||
, m_rightEditor(nullptr)
|
||||
, m_leftContainer(nullptr)
|
||||
, m_splitter(nullptr)
|
||||
, m_statsLabel(nullptr)
|
||||
, m_applyButton(nullptr)
|
||||
, m_declineButton(nullptr)
|
||||
, m_editorWidth(800)
|
||||
, m_syncingScroll(false)
|
||||
, m_isClosing(false)
|
||||
, m_linesAdded(0)
|
||||
, m_linesRemoved(0)
|
||||
{
|
||||
setupUi();
|
||||
applyEditorSettings();
|
||||
setWindowFlags(Qt::Popup | Qt::FramelessWindowHint);
|
||||
setAttribute(Qt::WA_DeleteOnClose);
|
||||
setFocusPolicy(Qt::StrongFocus);
|
||||
}
|
||||
|
||||
RefactorWidget::~RefactorWidget()
|
||||
{
|
||||
}
|
||||
|
||||
void RefactorWidget::setupUi()
|
||||
{
|
||||
auto *mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(6, 6, 6, 6);
|
||||
mainLayout->setSpacing(4);
|
||||
|
||||
m_statsLabel = new QLabel(this);
|
||||
m_statsLabel->setAlignment(Qt::AlignLeft);
|
||||
mainLayout->addWidget(m_statsLabel, 0);
|
||||
|
||||
m_leftDocument = QSharedPointer<TextEditor::TextDocument>::create();
|
||||
m_rightDocument = QSharedPointer<TextEditor::TextDocument>::create();
|
||||
|
||||
Qt::Orientation initialOrientation = Settings::quickRefactorSettings().widgetOrientation.value() == 1
|
||||
? Qt::Vertical : Qt::Horizontal;
|
||||
|
||||
m_splitter = new CustomSplitter(initialOrientation, this);
|
||||
m_splitter->setChildrenCollapsible(false);
|
||||
m_splitter->setHandleWidth(12);
|
||||
m_splitter->setStyleSheet("QSplitter::handle { background-color: transparent; }");
|
||||
|
||||
m_leftEditor = new TextEditor::TextEditorWidget();
|
||||
m_leftEditor->setTextDocument(m_leftDocument);
|
||||
m_leftEditor->setReadOnly(true);
|
||||
m_leftEditor->setFrameStyle(QFrame::StyledPanel);
|
||||
m_leftEditor->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
m_leftEditor->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
||||
m_leftEditor->setMinimumWidth(150);
|
||||
m_leftEditor->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||
m_leftEditor->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
|
||||
|
||||
m_rightEditor = new TextEditor::TextEditorWidget();
|
||||
m_rightEditor->setTextDocument(m_rightDocument);
|
||||
m_rightEditor->setReadOnly(false);
|
||||
m_rightEditor->setFrameStyle(QFrame::StyledPanel);
|
||||
m_rightEditor->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
||||
m_rightEditor->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
||||
m_rightEditor->setMinimumWidth(150);
|
||||
m_rightEditor->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||
|
||||
m_leftContainer = new QWidget();
|
||||
m_leftContainer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||
auto *leftLayout = new QVBoxLayout(m_leftContainer);
|
||||
leftLayout->setSpacing(2);
|
||||
leftLayout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
auto *originalLabel = new QLabel(tr("◄ Original"), m_leftContainer);
|
||||
leftLayout->addWidget(originalLabel, 0);
|
||||
leftLayout->addWidget(m_leftEditor, 1);
|
||||
|
||||
auto *rightContainer = new QWidget();
|
||||
rightContainer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||
auto *rightLayout = new QVBoxLayout(rightContainer);
|
||||
rightLayout->setSpacing(2);
|
||||
rightLayout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
auto *refactoredLabel = new QLabel(tr("Refactored ►"), rightContainer);
|
||||
rightLayout->addWidget(refactoredLabel, 0);
|
||||
rightLayout->addWidget(m_rightEditor, 1);
|
||||
|
||||
m_splitter->addWidget(m_leftContainer);
|
||||
m_splitter->addWidget(rightContainer);
|
||||
|
||||
m_splitter->setStretchFactor(0, 1);
|
||||
m_splitter->setStretchFactor(1, 1);
|
||||
|
||||
mainLayout->addWidget(m_splitter, 1);
|
||||
|
||||
connect(m_leftEditor->verticalScrollBar(), &QScrollBar::valueChanged,
|
||||
this, &RefactorWidget::syncLeftScroll);
|
||||
connect(m_rightEditor->verticalScrollBar(), &QScrollBar::valueChanged,
|
||||
this, &RefactorWidget::syncRightScroll);
|
||||
connect(m_leftEditor->horizontalScrollBar(), &QScrollBar::valueChanged,
|
||||
this, &RefactorWidget::syncLeftHorizontalScroll);
|
||||
connect(m_rightEditor->horizontalScrollBar(), &QScrollBar::valueChanged,
|
||||
this, &RefactorWidget::syncRightHorizontalScroll);
|
||||
|
||||
connect(m_rightDocument->document(), &QTextDocument::contentsChanged,
|
||||
this, &RefactorWidget::onRightEditorTextChanged);
|
||||
|
||||
auto *buttonLayout = new QHBoxLayout();
|
||||
buttonLayout->setContentsMargins(0, 2, 0, 0);
|
||||
buttonLayout->setSpacing(6);
|
||||
|
||||
#ifdef Q_OS_MACOS
|
||||
m_applyButton = new QPushButton(tr("✓ Apply (⌘+Enter)"), this);
|
||||
#else
|
||||
m_applyButton = new QPushButton(tr("✓ Apply (Ctrl+Enter)"), this);
|
||||
#endif
|
||||
m_applyButton->setFocusPolicy(Qt::NoFocus);
|
||||
m_applyButton->setCursor(Qt::PointingHandCursor);
|
||||
m_applyButton->setMaximumHeight(24);
|
||||
|
||||
m_declineButton = new QPushButton(tr("✗ Decline (Esc)"), this);
|
||||
m_declineButton->setFocusPolicy(Qt::NoFocus);
|
||||
m_declineButton->setCursor(Qt::PointingHandCursor);
|
||||
m_declineButton->setMaximumHeight(24);
|
||||
|
||||
buttonLayout->addStretch();
|
||||
buttonLayout->addWidget(m_applyButton);
|
||||
buttonLayout->addWidget(m_declineButton);
|
||||
|
||||
mainLayout->addLayout(buttonLayout, 0);
|
||||
|
||||
connect(m_applyButton, &QPushButton::clicked, this, &RefactorWidget::applyRefactoring);
|
||||
connect(m_declineButton, &QPushButton::clicked, this, &RefactorWidget::declineRefactoring);
|
||||
}
|
||||
|
||||
void RefactorWidget::setDiffContent(const QString &originalText, const QString &refactoredText)
|
||||
{
|
||||
setDiffContent(originalText, refactoredText, QString(), QString());
|
||||
}
|
||||
|
||||
void RefactorWidget::setDiffContent(const QString &originalText, const QString &refactoredText,
|
||||
const QString &contextBefore, const QString &contextAfter)
|
||||
{
|
||||
m_originalText = originalText;
|
||||
m_refactoredText = refactoredText;
|
||||
m_contextBefore = contextBefore;
|
||||
m_contextAfter = contextAfter;
|
||||
|
||||
m_leftContainer->setVisible(true);
|
||||
|
||||
QString leftFullText;
|
||||
QString rightFullText;
|
||||
|
||||
if (!contextBefore.isEmpty()) {
|
||||
leftFullText = contextBefore + "\n";
|
||||
rightFullText = contextBefore + "\n";
|
||||
}
|
||||
|
||||
leftFullText += originalText;
|
||||
rightFullText += refactoredText;
|
||||
|
||||
if (!contextAfter.isEmpty()) {
|
||||
leftFullText += "\n" + contextAfter;
|
||||
rightFullText += "\n" + contextAfter;
|
||||
}
|
||||
|
||||
m_leftDocument->setPlainText(leftFullText);
|
||||
m_rightDocument->setPlainText(rightFullText);
|
||||
|
||||
applySyntaxHighlighting();
|
||||
|
||||
if (!contextBefore.isEmpty() || !contextAfter.isEmpty()) {
|
||||
dimContextLines(contextBefore, contextAfter);
|
||||
}
|
||||
|
||||
Utils::Differ differ;
|
||||
m_cachedDiffList = differ.diff(m_originalText, m_refactoredText);
|
||||
|
||||
highlightDifferences();
|
||||
addLineMarkers();
|
||||
|
||||
calculateStats();
|
||||
updateStatsLabel();
|
||||
|
||||
updateSizeToContent();
|
||||
}
|
||||
|
||||
void RefactorWidget::highlightDifferences()
|
||||
{
|
||||
if (m_cachedDiffList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QList<Utils::Diff> leftDiffs;
|
||||
QList<Utils::Diff> rightDiffs;
|
||||
Utils::Differ::splitDiffList(m_cachedDiffList, &leftDiffs, &rightDiffs);
|
||||
|
||||
int contextBeforeOffset = m_contextBefore.isEmpty() ? 0 : (m_contextBefore.length() + 1);
|
||||
|
||||
QColor normalTextColor = Utils::creatorColor(Utils::Theme::TextColorNormal);
|
||||
|
||||
QTextCursor leftCursor(m_leftDocument->document());
|
||||
QTextCharFormat removedFormat;
|
||||
QColor removedBg = Utils::creatorColor(Utils::Theme::TextColorError);
|
||||
removedBg.setAlpha(30);
|
||||
removedFormat.setBackground(removedBg);
|
||||
removedFormat.setForeground(normalTextColor);
|
||||
|
||||
int leftPos = 0;
|
||||
for (const auto &diff : leftDiffs) {
|
||||
if (diff.command == Utils::Diff::Delete) {
|
||||
leftCursor.setPosition(contextBeforeOffset + leftPos);
|
||||
leftCursor.setPosition(contextBeforeOffset + leftPos + diff.text.length(), QTextCursor::KeepAnchor);
|
||||
leftCursor.setCharFormat(removedFormat);
|
||||
}
|
||||
if (diff.command != Utils::Diff::Insert) {
|
||||
leftPos += diff.text.length();
|
||||
}
|
||||
}
|
||||
|
||||
QTextCursor rightCursor(m_rightDocument->document());
|
||||
QTextCharFormat addedFormat;
|
||||
QColor addedBg = Utils::creatorColor(Utils::Theme::IconsRunColor);
|
||||
addedBg.setAlpha(60);
|
||||
addedFormat.setBackground(addedBg);
|
||||
addedFormat.setForeground(normalTextColor);
|
||||
|
||||
int rightPos = 0;
|
||||
for (const auto &diff : rightDiffs) {
|
||||
if (diff.command == Utils::Diff::Insert) {
|
||||
rightCursor.setPosition(contextBeforeOffset + rightPos);
|
||||
rightCursor.setPosition(contextBeforeOffset + rightPos + diff.text.length(), QTextCursor::KeepAnchor);
|
||||
rightCursor.setCharFormat(addedFormat);
|
||||
}
|
||||
if (diff.command != Utils::Diff::Delete) {
|
||||
rightPos += diff.text.length();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RefactorWidget::dimContextLines(const QString &contextBefore, const QString &contextAfter)
|
||||
{
|
||||
QTextCharFormat dimFormat;
|
||||
dimFormat.setForeground(Utils::creatorColor(Utils::Theme::TextColorDisabled));
|
||||
|
||||
auto dimLines = [&](QTextDocument *doc, int lineCount, bool fromStart) {
|
||||
QTextCursor cursor(doc);
|
||||
if (!fromStart) {
|
||||
cursor.movePosition(QTextCursor::End);
|
||||
}
|
||||
|
||||
for (int i = 0; i < lineCount; ++i) {
|
||||
cursor.movePosition(QTextCursor::StartOfBlock);
|
||||
cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
||||
cursor.setCharFormat(dimFormat);
|
||||
|
||||
if (fromStart) {
|
||||
if (!cursor.block().isValid() || !cursor.movePosition(QTextCursor::NextBlock)) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (!cursor.movePosition(QTextCursor::PreviousBlock)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!contextBefore.isEmpty()) {
|
||||
int lines = contextBefore.count('\n') + (contextBefore.endsWith('\n') ? 0 : 1);
|
||||
dimLines(m_leftDocument->document(), lines, true);
|
||||
dimLines(m_rightDocument->document(), lines, true);
|
||||
}
|
||||
|
||||
if (!contextAfter.isEmpty()) {
|
||||
int lines = contextAfter.count('\n') + (contextAfter.endsWith('\n') ? 0 : 1);
|
||||
dimLines(m_leftDocument->document(), lines, false);
|
||||
dimLines(m_rightDocument->document(), lines, false);
|
||||
}
|
||||
}
|
||||
|
||||
QString RefactorWidget::getRefactoredText() const
|
||||
{
|
||||
return m_applyText;
|
||||
}
|
||||
|
||||
void RefactorWidget::setRange(const Utils::Text::Range &range)
|
||||
{
|
||||
m_range = range;
|
||||
}
|
||||
|
||||
void RefactorWidget::setEditorWidth(int width)
|
||||
{
|
||||
m_editorWidth = width;
|
||||
updateSizeToContent();
|
||||
}
|
||||
|
||||
void RefactorWidget::setApplyCallback(std::function<void(const QString &)> callback)
|
||||
{
|
||||
m_applyCallback = callback;
|
||||
}
|
||||
|
||||
void RefactorWidget::setDeclineCallback(std::function<void()> callback)
|
||||
{
|
||||
m_declineCallback = callback;
|
||||
}
|
||||
|
||||
void RefactorWidget::applyRefactoring()
|
||||
{
|
||||
if (m_isClosing) return;
|
||||
m_isClosing = true;
|
||||
|
||||
if (m_applyCallback) {
|
||||
m_applyCallback(m_applyText);
|
||||
}
|
||||
emit applied();
|
||||
close();
|
||||
}
|
||||
|
||||
void RefactorWidget::declineRefactoring()
|
||||
{
|
||||
if (m_isClosing) return;
|
||||
m_isClosing = true;
|
||||
|
||||
if (m_declineCallback) {
|
||||
m_declineCallback();
|
||||
}
|
||||
emit declined();
|
||||
close();
|
||||
}
|
||||
|
||||
void RefactorWidget::paintEvent(QPaintEvent *)
|
||||
{
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
const QColor bgColor = Utils::creatorColor(Utils::Theme::BackgroundColorNormal);
|
||||
const QColor borderColor = Utils::creatorColor(Utils::Theme::SplitterColor);
|
||||
|
||||
painter.fillRect(rect(), bgColor);
|
||||
painter.setPen(QPen(borderColor, 2));
|
||||
painter.drawRoundedRect(rect().adjusted(2, 2, -2, -2), 6, 6);
|
||||
}
|
||||
|
||||
bool RefactorWidget::event(QEvent *event)
|
||||
{
|
||||
if (event->type() == QEvent::ShortcutOverride) {
|
||||
auto *keyEvent = static_cast<QKeyEvent *>(event);
|
||||
|
||||
if (((keyEvent->key() == Qt::Key_Enter || keyEvent->key() == Qt::Key_Return) &&
|
||||
keyEvent->modifiers() == Qt::ControlModifier) ||
|
||||
keyEvent->key() == Qt::Key_Escape) {
|
||||
event->accept();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (event->type() == QEvent::KeyPress) {
|
||||
auto *keyEvent = static_cast<QKeyEvent *>(event);
|
||||
|
||||
if ((keyEvent->key() == Qt::Key_Enter || keyEvent->key() == Qt::Key_Return) &&
|
||||
keyEvent->modifiers() == Qt::ControlModifier) {
|
||||
applyRefactoring();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (keyEvent->key() == Qt::Key_Escape) {
|
||||
declineRefactoring();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return QWidget::event(event);
|
||||
}
|
||||
|
||||
void RefactorWidget::syncLeftScroll(int value)
|
||||
{
|
||||
if (m_syncingScroll) return;
|
||||
m_syncingScroll = true;
|
||||
m_rightEditor->verticalScrollBar()->setValue(value);
|
||||
m_syncingScroll = false;
|
||||
}
|
||||
|
||||
void RefactorWidget::syncRightScroll(int value)
|
||||
{
|
||||
if (m_syncingScroll) return;
|
||||
m_syncingScroll = true;
|
||||
m_leftEditor->verticalScrollBar()->setValue(value);
|
||||
m_syncingScroll = false;
|
||||
}
|
||||
|
||||
void RefactorWidget::syncLeftHorizontalScroll(int value)
|
||||
{
|
||||
if (m_syncingScroll) return;
|
||||
m_syncingScroll = true;
|
||||
m_rightEditor->horizontalScrollBar()->setValue(value);
|
||||
m_syncingScroll = false;
|
||||
}
|
||||
|
||||
void RefactorWidget::syncRightHorizontalScroll(int value)
|
||||
{
|
||||
if (m_syncingScroll) return;
|
||||
m_syncingScroll = true;
|
||||
m_leftEditor->horizontalScrollBar()->setValue(value);
|
||||
m_syncingScroll = false;
|
||||
}
|
||||
|
||||
void RefactorWidget::onRightEditorTextChanged()
|
||||
{
|
||||
QString fullText = m_rightDocument->plainText();
|
||||
int startPos = m_contextBefore.isEmpty() ? 0 : m_contextBefore.length() + 1;
|
||||
int endPos = m_contextAfter.isEmpty() ? fullText.length() : fullText.length() - m_contextAfter.length() - 1;
|
||||
m_applyText = fullText.mid(startPos, endPos - startPos);
|
||||
}
|
||||
|
||||
void RefactorWidget::closeEvent(QCloseEvent *event)
|
||||
{
|
||||
if (!m_isClosing) {
|
||||
declineRefactoring();
|
||||
}
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void RefactorWidget::calculateStats()
|
||||
{
|
||||
DiffStatistics stats;
|
||||
stats.calculate(m_cachedDiffList);
|
||||
m_linesAdded = stats.linesAdded();
|
||||
m_linesRemoved = stats.linesRemoved();
|
||||
}
|
||||
|
||||
void RefactorWidget::updateStatsLabel()
|
||||
{
|
||||
DiffStatistics stats;
|
||||
stats.calculate(m_cachedDiffList);
|
||||
m_statsLabel->setText("📊 " + stats.formatSummary());
|
||||
}
|
||||
|
||||
void RefactorWidget::applySyntaxHighlighting()
|
||||
{
|
||||
if (!m_sourceEditor) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto *sourceDoc = m_sourceEditor->textDocument();
|
||||
if (!sourceDoc || !sourceDoc->syntaxHighlighter()) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_leftDocument->setMimeType(sourceDoc->mimeType());
|
||||
m_rightDocument->setMimeType(sourceDoc->mimeType());
|
||||
}
|
||||
|
||||
void RefactorWidget::addLineMarkers()
|
||||
{
|
||||
if (m_cachedDiffList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QList<Utils::Diff> leftDiffs;
|
||||
QList<Utils::Diff> rightDiffs;
|
||||
Utils::Differ::splitDiffList(m_cachedDiffList, &leftDiffs, &rightDiffs);
|
||||
|
||||
int contextBeforeOffset = m_contextBefore.isEmpty() ? 0 : (m_contextBefore.length() + 1);
|
||||
|
||||
QColor removedMarker = Utils::creatorColor(Utils::Theme::TextColorError);
|
||||
QColor addedMarker = Utils::creatorColor(Utils::Theme::IconsRunColor);
|
||||
|
||||
QTextCursor leftCursor(m_leftDocument->document());
|
||||
int leftPos = 0;
|
||||
|
||||
for (const auto &diff : leftDiffs) {
|
||||
if (diff.command == Utils::Diff::Delete) {
|
||||
leftCursor.setPosition(contextBeforeOffset + leftPos);
|
||||
leftCursor.movePosition(QTextCursor::StartOfBlock);
|
||||
leftCursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
||||
|
||||
QTextBlockFormat blockFormat;
|
||||
blockFormat.setBackground(QBrush(removedMarker.lighter(185)));
|
||||
blockFormat.setLeftMargin(4);
|
||||
blockFormat.setProperty(QTextFormat::FullWidthSelection, true);
|
||||
|
||||
leftCursor.setBlockFormat(blockFormat);
|
||||
}
|
||||
if (diff.command != Utils::Diff::Insert) {
|
||||
leftPos += diff.text.length();
|
||||
}
|
||||
}
|
||||
|
||||
QTextCursor rightCursor(m_rightDocument->document());
|
||||
int rightPos = 0;
|
||||
|
||||
for (const auto &diff : rightDiffs) {
|
||||
if (diff.command == Utils::Diff::Insert) {
|
||||
rightCursor.setPosition(contextBeforeOffset + rightPos);
|
||||
rightCursor.movePosition(QTextCursor::StartOfBlock);
|
||||
rightCursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
||||
|
||||
QTextBlockFormat blockFormat;
|
||||
blockFormat.setBackground(QBrush(addedMarker.lighter(195)));
|
||||
blockFormat.setLeftMargin(4);
|
||||
blockFormat.setProperty(QTextFormat::FullWidthSelection, true);
|
||||
|
||||
rightCursor.setBlockFormat(blockFormat);
|
||||
}
|
||||
if (diff.command != Utils::Diff::Delete) {
|
||||
rightPos += diff.text.length();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RefactorWidget::updateSizeToContent()
|
||||
{
|
||||
QFontMetrics fm(m_rightEditor->font());
|
||||
int lineHeight = fm.height();
|
||||
|
||||
int leftMaxWidth = 0;
|
||||
int leftLineCount = 0;
|
||||
QTextBlock leftBlock = m_leftDocument->document()->begin();
|
||||
while (leftBlock.isValid()) {
|
||||
int lineWidth = fm.horizontalAdvance(leftBlock.text());
|
||||
leftMaxWidth = qMax(leftMaxWidth, lineWidth);
|
||||
leftLineCount++;
|
||||
leftBlock = leftBlock.next();
|
||||
}
|
||||
|
||||
int rightMaxWidth = 0;
|
||||
int rightLineCount = 0;
|
||||
QTextBlock rightBlock = m_rightDocument->document()->begin();
|
||||
while (rightBlock.isValid()) {
|
||||
int lineWidth = fm.horizontalAdvance(rightBlock.text());
|
||||
rightMaxWidth = qMax(rightMaxWidth, lineWidth);
|
||||
rightLineCount++;
|
||||
rightBlock = rightBlock.next();
|
||||
}
|
||||
|
||||
int maxLineWidth = qMax(leftMaxWidth, rightMaxWidth);
|
||||
int maxLineCount = qMax(leftLineCount, rightLineCount);
|
||||
|
||||
const int editorPadding = 60;
|
||||
const int scrollBarWidth = 20;
|
||||
const int statsLabelHeight = 30;
|
||||
const int buttonLayoutHeight = 35;
|
||||
const int layoutMargins = 20;
|
||||
const int labelHeight = 20;
|
||||
|
||||
int singleEditorWidth = maxLineWidth + editorPadding + scrollBarWidth;
|
||||
int contentHeight = maxLineCount * lineHeight + statsLabelHeight + buttonLayoutHeight + layoutMargins + labelHeight;
|
||||
|
||||
bool horizontal = m_splitter->orientation() == Qt::Horizontal;
|
||||
|
||||
int contentWidth;
|
||||
if (horizontal) {
|
||||
contentWidth = singleEditorWidth * 2 + m_splitter->handleWidth() + 20;
|
||||
} else {
|
||||
contentWidth = singleEditorWidth + 20;
|
||||
}
|
||||
|
||||
QScreen *screen = window()->screen();
|
||||
if (!screen) {
|
||||
screen = QGuiApplication::primaryScreen();
|
||||
}
|
||||
int screenWidth = screen ? screen->availableGeometry().width() : 1920;
|
||||
int screenHeight = screen ? screen->availableGeometry().height() : 1080;
|
||||
|
||||
const int minWidth = Settings::quickRefactorSettings().widgetMinWidth();
|
||||
const int maxWidth = qMin(Settings::quickRefactorSettings().widgetMaxWidth(),
|
||||
qMin(m_editorWidth - 40, screenWidth - 100));
|
||||
const int minHeight = Settings::quickRefactorSettings().widgetMinHeight();
|
||||
const int maxHeight = qMin(Settings::quickRefactorSettings().widgetMaxHeight(), screenHeight - 100);
|
||||
|
||||
int targetWidth = qBound(minWidth, contentWidth, maxWidth);
|
||||
int targetHeight = qBound(minHeight, contentHeight, maxHeight);
|
||||
|
||||
setFixedSize(targetWidth, targetHeight);
|
||||
|
||||
updateGeometry();
|
||||
}
|
||||
|
||||
void RefactorWidget::applyEditorSettings()
|
||||
{
|
||||
if (!m_sourceEditor || !m_leftEditor || !m_rightEditor) {
|
||||
return;
|
||||
}
|
||||
|
||||
QFont editorFont = m_sourceEditor->font();
|
||||
m_leftEditor->setFont(editorFont);
|
||||
m_rightEditor->setFont(editorFont);
|
||||
|
||||
QString labelStyle = QString("color: %1; padding: 2px 4px;")
|
||||
.arg(Utils::creatorColor(Utils::Theme::TextColorDisabled).name());
|
||||
|
||||
for (auto *label : findChildren<QLabel *>()) {
|
||||
if (label != m_statsLabel) {
|
||||
QFont labelFont = label->font();
|
||||
labelFont.setPointSize(qMax(8, editorFont.pointSize() - 2));
|
||||
label->setFont(labelFont);
|
||||
label->setStyleSheet(labelStyle);
|
||||
}
|
||||
}
|
||||
|
||||
QFont statsFont = m_statsLabel->font();
|
||||
statsFont.setBold(true);
|
||||
statsFont.setPointSize(qMax(9, editorFont.pointSize() - 1));
|
||||
m_statsLabel->setFont(statsFont);
|
||||
|
||||
m_statsLabel->setStyleSheet(QString(
|
||||
"color: %1; padding: 4px 6px; background-color: %2; border-radius: 3px;")
|
||||
.arg(Utils::creatorColor(Utils::Theme::TextColorNormal).name())
|
||||
.arg(Utils::creatorColor(Utils::Theme::BackgroundColorHover).name()));
|
||||
|
||||
updateButtonStyles();
|
||||
}
|
||||
|
||||
void RefactorWidget::updateButtonStyles()
|
||||
{
|
||||
if (!m_applyButton || !m_declineButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
int baseFontSize = m_sourceEditor ? qMax(9, m_sourceEditor->font().pointSize() - 2) : 10;
|
||||
|
||||
auto createStyle = [&](const QColor &color, bool bold) {
|
||||
return QString(
|
||||
"QPushButton {"
|
||||
" background-color: %1; color: %2; border: 1px solid %3;"
|
||||
" border-radius: 3px; padding: 2px 8px; font-size: %4pt;%5"
|
||||
"}"
|
||||
"QPushButton:hover { background-color: %6; border: 1px solid %2; }"
|
||||
"QPushButton:pressed { background-color: %7; }")
|
||||
.arg(Utils::creatorColor(Utils::Theme::BackgroundColorNormal).name())
|
||||
.arg(color.name())
|
||||
.arg(Utils::creatorColor(Utils::Theme::SplitterColor).name())
|
||||
.arg(baseFontSize)
|
||||
.arg(bold ? QLatin1StringView(" font-weight: bold;") : QLatin1StringView(""))
|
||||
.arg(Utils::creatorColor(Utils::Theme::BackgroundColorHover).name())
|
||||
.arg(Utils::creatorColor(Utils::Theme::BackgroundColorSelected).name());
|
||||
};
|
||||
|
||||
m_applyButton->setStyleSheet(createStyle(Utils::creatorColor(Utils::Theme::TextColorNormal), true));
|
||||
m_declineButton->setStyleSheet(createStyle(Utils::creatorColor(Utils::Theme::TextColorError), false));
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
126
sources/widgets/RefactorWidget.hpp
Normal file
126
sources/widgets/RefactorWidget.hpp
Normal file
@@ -0,0 +1,126 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
#include <QPushButton>
|
||||
#include <QLabel>
|
||||
#include <QVBoxLayout>
|
||||
#include <QKeyEvent>
|
||||
#include <QEnterEvent>
|
||||
#include <QSharedPointer>
|
||||
#include <QSplitter>
|
||||
#include <QSplitterHandle>
|
||||
#include <functional>
|
||||
|
||||
#include <texteditor/texteditor.h>
|
||||
#include <utils/textutils.h>
|
||||
#include <utils/differ.h>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class CustomSplitterHandle : public QSplitterHandle
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit CustomSplitterHandle(Qt::Orientation orientation, QSplitter *parent);
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void enterEvent(QEnterEvent *event) override;
|
||||
void leaveEvent(QEvent *event) override;
|
||||
private:
|
||||
bool m_hovered = false;
|
||||
};
|
||||
|
||||
class CustomSplitter : public QSplitter
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit CustomSplitter(Qt::Orientation orientation, QWidget *parent = nullptr);
|
||||
protected:
|
||||
QSplitterHandle *createHandle() override;
|
||||
};
|
||||
|
||||
class RefactorWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit RefactorWidget(TextEditor::TextEditorWidget *sourceEditor, QWidget *parent = nullptr);
|
||||
~RefactorWidget() override;
|
||||
|
||||
void setDiffContent(const QString &originalText, const QString &refactoredText);
|
||||
void setDiffContent(const QString &originalText, const QString &refactoredText,
|
||||
const QString &contextBefore, const QString &contextAfter);
|
||||
|
||||
void setApplyText(const QString &text) { m_applyText = text; }
|
||||
void setRange(const Utils::Text::Range &range);
|
||||
void setEditorWidth(int width);
|
||||
|
||||
QString getRefactoredText() const;
|
||||
|
||||
void setApplyCallback(std::function<void(const QString &)> callback);
|
||||
void setDeclineCallback(std::function<void()> callback);
|
||||
|
||||
signals:
|
||||
void applied();
|
||||
void declined();
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
bool event(QEvent *event) override;
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
|
||||
private slots:
|
||||
void syncLeftScroll(int value);
|
||||
void syncRightScroll(int value);
|
||||
void syncLeftHorizontalScroll(int value);
|
||||
void syncRightHorizontalScroll(int value);
|
||||
void onRightEditorTextChanged();
|
||||
|
||||
private:
|
||||
TextEditor::TextEditorWidget *m_sourceEditor;
|
||||
TextEditor::TextEditorWidget *m_leftEditor;
|
||||
TextEditor::TextEditorWidget *m_rightEditor;
|
||||
QSharedPointer<TextEditor::TextDocument> m_leftDocument;
|
||||
QSharedPointer<TextEditor::TextDocument> m_rightDocument;
|
||||
QWidget *m_leftContainer;
|
||||
QSplitter *m_splitter;
|
||||
QLabel *m_statsLabel;
|
||||
QPushButton *m_applyButton;
|
||||
QPushButton *m_declineButton;
|
||||
|
||||
QString m_originalText;
|
||||
QString m_refactoredText;
|
||||
QString m_applyText;
|
||||
QString m_contextBefore;
|
||||
QString m_contextAfter;
|
||||
Utils::Text::Range m_range;
|
||||
int m_editorWidth;
|
||||
bool m_syncingScroll;
|
||||
bool m_isClosing;
|
||||
int m_linesAdded;
|
||||
int m_linesRemoved;
|
||||
|
||||
QList<Utils::Diff> m_cachedDiffList;
|
||||
|
||||
std::function<void(const QString &)> m_applyCallback;
|
||||
std::function<void()> m_declineCallback;
|
||||
|
||||
void setupUi();
|
||||
void applyRefactoring();
|
||||
void declineRefactoring();
|
||||
void updateSizeToContent();
|
||||
void highlightDifferences();
|
||||
void dimContextLines(const QString &contextBefore, const QString &contextAfter);
|
||||
void calculateStats();
|
||||
void updateStatsLabel();
|
||||
void applySyntaxHighlighting();
|
||||
void addLineMarkers();
|
||||
void applyEditorSettings();
|
||||
void updateButtonStyles();
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
148
sources/widgets/RefactorWidgetHandler.cpp
Normal file
148
sources/widgets/RefactorWidgetHandler.cpp
Normal file
@@ -0,0 +1,148 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "RefactorWidgetHandler.hpp"
|
||||
#include "RefactorWidget.hpp"
|
||||
#include "ContextExtractor.hpp"
|
||||
|
||||
#include <QScrollBar>
|
||||
#include <QTextBlock>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
RefactorWidgetHandler::RefactorWidgetHandler(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
RefactorWidgetHandler::~RefactorWidgetHandler()
|
||||
{
|
||||
hideRefactorWidget();
|
||||
}
|
||||
|
||||
void RefactorWidgetHandler::showRefactorWidget(
|
||||
TextEditor::TextEditorWidget *editor,
|
||||
const QString &originalText,
|
||||
const QString &refactoredText,
|
||||
const Utils::Text::Range &range)
|
||||
{
|
||||
QString contextBefore = ContextExtractor::extractBefore(editor, range, 3);
|
||||
QString contextAfter = ContextExtractor::extractAfter(editor, range, 3);
|
||||
showRefactorWidget(editor, originalText, refactoredText, range, contextBefore, contextAfter);
|
||||
}
|
||||
|
||||
void RefactorWidgetHandler::showRefactorWidget(
|
||||
TextEditor::TextEditorWidget *editor,
|
||||
const QString &originalText,
|
||||
const QString &refactoredText,
|
||||
const Utils::Text::Range &range,
|
||||
const QString &contextBefore,
|
||||
const QString &contextAfter)
|
||||
{
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
hideRefactorWidget();
|
||||
|
||||
m_editor = editor;
|
||||
m_refactorWidget = new RefactorWidget(editor);
|
||||
|
||||
m_refactorWidget->setDiffContent(originalText, refactoredText, contextBefore, contextAfter);
|
||||
m_refactorWidget->setApplyText(refactoredText);
|
||||
m_refactorWidget->setRange(range);
|
||||
m_refactorWidget->setEditorWidth(getEditorWidth());
|
||||
|
||||
if (m_applyCallback) {
|
||||
m_refactorWidget->setApplyCallback(m_applyCallback);
|
||||
}
|
||||
|
||||
if (m_declineCallback) {
|
||||
m_refactorWidget->setDeclineCallback(m_declineCallback);
|
||||
}
|
||||
|
||||
updateWidgetPosition();
|
||||
m_refactorWidget->show();
|
||||
m_refactorWidget->raise();
|
||||
}
|
||||
|
||||
void RefactorWidgetHandler::hideRefactorWidget()
|
||||
{
|
||||
if (!m_refactorWidget.isNull()) {
|
||||
m_refactorWidget->close();
|
||||
m_refactorWidget = nullptr;
|
||||
}
|
||||
m_editor = nullptr;
|
||||
}
|
||||
|
||||
void RefactorWidgetHandler::setApplyCallback(std::function<void(const QString &)> callback)
|
||||
{
|
||||
m_applyCallback = callback;
|
||||
}
|
||||
|
||||
void RefactorWidgetHandler::setDeclineCallback(std::function<void()> callback)
|
||||
{
|
||||
m_declineCallback = callback;
|
||||
}
|
||||
|
||||
void RefactorWidgetHandler::setTextToApply(const QString &text)
|
||||
{
|
||||
if (!m_refactorWidget.isNull()) {
|
||||
m_refactorWidget->setApplyText(text);
|
||||
}
|
||||
}
|
||||
|
||||
void RefactorWidgetHandler::updateWidgetPosition()
|
||||
{
|
||||
if (m_refactorWidget.isNull() || m_editor.isNull()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QPoint position = calculateWidgetPosition();
|
||||
m_refactorWidget->move(position);
|
||||
}
|
||||
|
||||
QPoint RefactorWidgetHandler::calculateWidgetPosition()
|
||||
{
|
||||
if (m_editor.isNull()) {
|
||||
return QPoint(0, 0);
|
||||
}
|
||||
|
||||
QTextCursor cursor = m_editor->textCursor();
|
||||
QRect cursorRect = m_editor->cursorRect(cursor);
|
||||
QPoint globalPos = m_editor->mapToGlobal(cursorRect.bottomLeft());
|
||||
globalPos.setY(globalPos.y() + 10);
|
||||
|
||||
if (m_refactorWidget) {
|
||||
QRect widgetRect(globalPos, m_refactorWidget->size());
|
||||
QRect screenRect = m_editor->screen()->availableGeometry();
|
||||
|
||||
if (widgetRect.right() > screenRect.right()) {
|
||||
globalPos.setX(screenRect.right() - m_refactorWidget->width() - 10);
|
||||
}
|
||||
|
||||
if (widgetRect.bottom() > screenRect.bottom()) {
|
||||
globalPos.setY(m_editor->mapToGlobal(cursorRect.topLeft()).y()
|
||||
- m_refactorWidget->height() - 10);
|
||||
}
|
||||
|
||||
if (globalPos.x() < screenRect.left()) {
|
||||
globalPos.setX(screenRect.left() + 10);
|
||||
}
|
||||
|
||||
if (globalPos.y() < screenRect.top()) {
|
||||
globalPos.setY(screenRect.top() + 10);
|
||||
}
|
||||
}
|
||||
|
||||
return globalPos;
|
||||
}
|
||||
|
||||
int RefactorWidgetHandler::getEditorWidth() const
|
||||
{
|
||||
return m_editor.isNull() ? 800 : m_editor->viewport()->width();
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
||||
60
sources/widgets/RefactorWidgetHandler.hpp
Normal file
60
sources/widgets/RefactorWidgetHandler.hpp
Normal file
@@ -0,0 +1,60 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QPointer>
|
||||
#include <functional>
|
||||
|
||||
#include <texteditor/texteditor.h>
|
||||
#include <utils/textutils.h>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class RefactorWidget;
|
||||
|
||||
class RefactorWidgetHandler : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit RefactorWidgetHandler(QObject *parent = nullptr);
|
||||
~RefactorWidgetHandler() override;
|
||||
|
||||
void showRefactorWidget(
|
||||
TextEditor::TextEditorWidget *editor,
|
||||
const QString &originalText,
|
||||
const QString &refactoredText,
|
||||
const Utils::Text::Range &range);
|
||||
|
||||
void showRefactorWidget(
|
||||
TextEditor::TextEditorWidget *editor,
|
||||
const QString &originalText,
|
||||
const QString &refactoredText,
|
||||
const Utils::Text::Range &range,
|
||||
const QString &contextBefore,
|
||||
const QString &contextAfter);
|
||||
|
||||
void hideRefactorWidget();
|
||||
|
||||
bool isWidgetVisible() const { return !m_refactorWidget.isNull(); }
|
||||
|
||||
void setApplyCallback(std::function<void(const QString &)> callback);
|
||||
void setDeclineCallback(std::function<void()> callback);
|
||||
void setTextToApply(const QString &text);
|
||||
|
||||
private:
|
||||
QPointer<TextEditor::TextEditorWidget> m_editor;
|
||||
QPointer<RefactorWidget> m_refactorWidget;
|
||||
|
||||
std::function<void(const QString &)> m_applyCallback;
|
||||
std::function<void()> m_declineCallback;
|
||||
|
||||
void updateWidgetPosition();
|
||||
QPoint calculateWidgetPosition();
|
||||
int getEditorWidth() const;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
||||
Reference in New Issue
Block a user