Internal
Public Access
Milestone 8 UX: folder tree workflows, about dialog, and app icon polish
This commit is contained in:
+225
-7
@@ -7,6 +7,7 @@
|
||||
#include <KodoTerm/KodoTerm.hpp>
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QFile>
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QFont>
|
||||
@@ -17,10 +18,14 @@
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QApplication>
|
||||
#include <QClipboard>
|
||||
#include <QComboBox>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QThread>
|
||||
#include <QTimer>
|
||||
#include <QToolButton>
|
||||
#include <QTextStream>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include <memory>
|
||||
@@ -53,7 +58,9 @@ TerminalTheme themeForName(const QString& themeName)
|
||||
}
|
||||
}
|
||||
|
||||
SessionTab::SessionTab(const Profile& profile, QWidget* parent)
|
||||
SessionTab::SessionTab(const Profile& profile,
|
||||
const SessionUiPreferences& preferences,
|
||||
QWidget* parent)
|
||||
: QWidget(parent),
|
||||
m_profile(profile),
|
||||
m_backendThread(nullptr),
|
||||
@@ -61,13 +68,21 @@ SessionTab::SessionTab(const Profile& profile, QWidget* parent)
|
||||
m_useKodoTermForSsh(profile.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive)
|
||||
== 0),
|
||||
m_state(SessionState::Disconnected),
|
||||
m_terminalThemeName(QStringLiteral("Dark")),
|
||||
m_terminalThemeName(preferences.terminalThemeName.trimmed().isEmpty()
|
||||
? QStringLiteral("Dark")
|
||||
: preferences.terminalThemeName.trimmed()),
|
||||
m_sshTerminal(nullptr),
|
||||
m_rdpDisplay(nullptr),
|
||||
m_terminalOutput(nullptr),
|
||||
m_eventLog(nullptr),
|
||||
m_toggleEventsButton(nullptr),
|
||||
m_eventsPanel(nullptr)
|
||||
m_eventFilterInput(nullptr),
|
||||
m_eventSeverityFilterInput(nullptr),
|
||||
m_clearEventsButton(nullptr),
|
||||
m_exportEventsButton(nullptr),
|
||||
m_eventsPanel(nullptr),
|
||||
m_eventSeverityFilter(EventSeverity::Info),
|
||||
m_eventsPanelExpanded(preferences.eventsPanelExpanded)
|
||||
{
|
||||
qRegisterMetaType<SessionConnectOptions>("SessionConnectOptions");
|
||||
qRegisterMetaType<SessionState>("SessionState");
|
||||
@@ -347,6 +362,7 @@ void SessionTab::setTerminalThemeName(const QString& themeName)
|
||||
m_terminalThemeName = normalized;
|
||||
applyTerminalTheme(m_terminalThemeName);
|
||||
appendEvent(QStringLiteral("Terminal theme set to %1.").arg(m_terminalThemeName));
|
||||
emit terminalThemeChanged(m_terminalThemeName);
|
||||
}
|
||||
|
||||
QString SessionTab::terminalThemeName() const
|
||||
@@ -364,6 +380,111 @@ bool SessionTab::supportsClearAction() const
|
||||
return m_useKodoTermForSsh || m_terminalOutput != nullptr;
|
||||
}
|
||||
|
||||
bool SessionTab::isEventsPanelExpanded() const
|
||||
{
|
||||
return m_eventsPanelExpanded;
|
||||
}
|
||||
|
||||
void SessionTab::setEventsPanelExpanded(bool expanded)
|
||||
{
|
||||
if (m_eventsPanel == nullptr || m_toggleEventsButton == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_eventsPanelExpanded == expanded && m_eventsPanel->isVisible() == expanded) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_eventsPanelExpanded = expanded;
|
||||
setPanelExpanded(m_toggleEventsButton, m_eventsPanel, QStringLiteral("Events"), expanded);
|
||||
emit eventsPanelVisibilityChanged(m_eventsPanelExpanded);
|
||||
}
|
||||
|
||||
void SessionTab::clearEvents()
|
||||
{
|
||||
m_eventEntries.clear();
|
||||
if (m_eventLog != nullptr) {
|
||||
m_eventLog->clear();
|
||||
}
|
||||
}
|
||||
|
||||
void SessionTab::copyEvents() const
|
||||
{
|
||||
QStringList visibleLines;
|
||||
for (const EventEntry& entry : m_eventEntries) {
|
||||
const bool matchesText = m_eventFilter.isEmpty()
|
||||
|| entry.line.contains(m_eventFilter, Qt::CaseInsensitive);
|
||||
bool matchesSeverity = true;
|
||||
if (m_eventSeverityFilter == EventSeverity::Warning) {
|
||||
matchesSeverity = entry.severity == EventSeverity::Warning;
|
||||
} else if (m_eventSeverityFilter == EventSeverity::Error) {
|
||||
matchesSeverity = entry.severity == EventSeverity::Error;
|
||||
} else if (m_eventSeverityFilter == EventSeverity::Info) {
|
||||
matchesSeverity = true;
|
||||
}
|
||||
|
||||
if (matchesText && matchesSeverity) {
|
||||
visibleLines.push_back(entry.line);
|
||||
}
|
||||
}
|
||||
|
||||
if (!visibleLines.isEmpty()) {
|
||||
QApplication::clipboard()->setText(visibleLines.join(QChar::fromLatin1('\n')));
|
||||
}
|
||||
}
|
||||
|
||||
void SessionTab::exportEventsToFile()
|
||||
{
|
||||
QStringList visibleLines;
|
||||
for (const EventEntry& entry : m_eventEntries) {
|
||||
const bool matchesText = m_eventFilter.isEmpty()
|
||||
|| entry.line.contains(m_eventFilter, Qt::CaseInsensitive);
|
||||
bool matchesSeverity = true;
|
||||
if (m_eventSeverityFilter == EventSeverity::Warning) {
|
||||
matchesSeverity = entry.severity == EventSeverity::Warning;
|
||||
} else if (m_eventSeverityFilter == EventSeverity::Error) {
|
||||
matchesSeverity = entry.severity == EventSeverity::Error;
|
||||
} else if (m_eventSeverityFilter == EventSeverity::Info) {
|
||||
matchesSeverity = true;
|
||||
}
|
||||
if (matchesText && matchesSeverity) {
|
||||
visibleLines.push_back(entry.line);
|
||||
}
|
||||
}
|
||||
|
||||
if (visibleLines.isEmpty()) {
|
||||
QMessageBox::information(this,
|
||||
QStringLiteral("Export Events"),
|
||||
QStringLiteral("No events match the current filters."));
|
||||
return;
|
||||
}
|
||||
|
||||
const QString defaultName =
|
||||
QStringLiteral("orbithub-events-%1.log")
|
||||
.arg(QDateTime::currentDateTime().toString(QStringLiteral("yyyyMMdd-HHmmss")));
|
||||
const QString targetPath = QFileDialog::getSaveFileName(this,
|
||||
QStringLiteral("Export Session Events"),
|
||||
defaultName,
|
||||
QStringLiteral("Log Files (*.log);;Text Files (*.txt);;All Files (*)"));
|
||||
if (targetPath.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QFile file(targetPath);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
QMessageBox::warning(this,
|
||||
QStringLiteral("Export Events"),
|
||||
QStringLiteral("Failed to write file: %1").arg(targetPath));
|
||||
return;
|
||||
}
|
||||
|
||||
QTextStream stream(&file);
|
||||
for (const QString& line : visibleLines) {
|
||||
stream << line << '\n';
|
||||
}
|
||||
file.close();
|
||||
}
|
||||
|
||||
void SessionTab::onBackendStateChanged(SessionState state, const QString& message)
|
||||
{
|
||||
setState(state, message);
|
||||
@@ -445,7 +566,21 @@ void SessionTab::setupUi()
|
||||
auto* eventsHeader = new QHBoxLayout();
|
||||
m_toggleEventsButton = new QToolButton(this);
|
||||
m_toggleEventsButton->setCheckable(true);
|
||||
m_eventFilterInput = new QLineEdit(this);
|
||||
m_eventFilterInput->setPlaceholderText(QStringLiteral("Filter events..."));
|
||||
m_eventSeverityFilterInput = new QComboBox(this);
|
||||
m_eventSeverityFilterInput->addItem(QStringLiteral("All"));
|
||||
m_eventSeverityFilterInput->addItem(QStringLiteral("Warnings"));
|
||||
m_eventSeverityFilterInput->addItem(QStringLiteral("Errors"));
|
||||
m_clearEventsButton = new QToolButton(this);
|
||||
m_clearEventsButton->setText(QStringLiteral("Clear Events"));
|
||||
m_exportEventsButton = new QToolButton(this);
|
||||
m_exportEventsButton->setText(QStringLiteral("Export Events"));
|
||||
eventsHeader->addWidget(m_toggleEventsButton);
|
||||
eventsHeader->addWidget(m_eventFilterInput, 1);
|
||||
eventsHeader->addWidget(m_eventSeverityFilterInput);
|
||||
eventsHeader->addWidget(m_exportEventsButton);
|
||||
eventsHeader->addWidget(m_clearEventsButton);
|
||||
eventsHeader->addStretch();
|
||||
|
||||
m_eventsPanel = new QWidget(this);
|
||||
@@ -464,15 +599,43 @@ void SessionTab::setupUi()
|
||||
rootLayout->addLayout(eventsHeader);
|
||||
rootLayout->addWidget(m_eventsPanel);
|
||||
|
||||
setPanelExpanded(m_toggleEventsButton, m_eventsPanel, QStringLiteral("Events"), false);
|
||||
setPanelExpanded(
|
||||
m_toggleEventsButton, m_eventsPanel, QStringLiteral("Events"), m_eventsPanelExpanded);
|
||||
|
||||
connect(m_toggleEventsButton,
|
||||
&QToolButton::toggled,
|
||||
this,
|
||||
[this](bool expanded) {
|
||||
setPanelExpanded(
|
||||
m_toggleEventsButton, m_eventsPanel, QStringLiteral("Events"), expanded);
|
||||
setEventsPanelExpanded(expanded);
|
||||
});
|
||||
connect(m_eventFilterInput,
|
||||
&QLineEdit::textChanged,
|
||||
this,
|
||||
[this](const QString& text) {
|
||||
m_eventFilter = text.trimmed();
|
||||
refreshEventLogView();
|
||||
});
|
||||
connect(m_eventSeverityFilterInput,
|
||||
&QComboBox::currentTextChanged,
|
||||
this,
|
||||
[this](const QString& selected) {
|
||||
if (selected.compare(QStringLiteral("Errors"), Qt::CaseInsensitive) == 0) {
|
||||
m_eventSeverityFilter = EventSeverity::Error;
|
||||
} else if (selected.compare(QStringLiteral("Warnings"), Qt::CaseInsensitive) == 0) {
|
||||
m_eventSeverityFilter = EventSeverity::Warning;
|
||||
} else {
|
||||
m_eventSeverityFilter = EventSeverity::Info;
|
||||
}
|
||||
refreshEventLogView();
|
||||
});
|
||||
connect(m_exportEventsButton,
|
||||
&QToolButton::clicked,
|
||||
this,
|
||||
[this]() { exportEventsToFile(); });
|
||||
connect(m_clearEventsButton,
|
||||
&QToolButton::clicked,
|
||||
this,
|
||||
[this]() { clearEvents(); });
|
||||
|
||||
if (m_terminalOutput != nullptr) {
|
||||
connect(m_terminalOutput,
|
||||
@@ -639,7 +802,14 @@ bool SessionTab::validateProfileForConnect()
|
||||
void SessionTab::appendEvent(const QString& message)
|
||||
{
|
||||
const QString timestamp = QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd HH:mm:ss"));
|
||||
m_eventLog->appendPlainText(QStringLiteral("[%1] %2").arg(timestamp, message));
|
||||
m_eventEntries.push_back(
|
||||
EventEntry{QStringLiteral("[%1] %2").arg(timestamp, message),
|
||||
classifyEventSeverity(message)});
|
||||
constexpr int kMaxEventLines = 5000;
|
||||
while (m_eventEntries.size() > static_cast<size_t>(kMaxEventLines)) {
|
||||
m_eventEntries.erase(m_eventEntries.begin());
|
||||
}
|
||||
refreshEventLogView();
|
||||
}
|
||||
|
||||
void SessionTab::setState(SessionState state, const QString& message)
|
||||
@@ -803,3 +973,51 @@ void SessionTab::applyTerminalTheme(const QString& themeName)
|
||||
m_terminalOutput->setThemeName(themeName);
|
||||
}
|
||||
}
|
||||
|
||||
void SessionTab::refreshEventLogView()
|
||||
{
|
||||
if (m_eventLog == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
QStringList visibleLines;
|
||||
visibleLines.reserve(static_cast<int>(m_eventEntries.size()));
|
||||
|
||||
for (const EventEntry& entry : m_eventEntries) {
|
||||
if (!m_eventFilter.isEmpty() && !entry.line.contains(m_eventFilter, Qt::CaseInsensitive)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (m_eventSeverityFilter == EventSeverity::Warning
|
||||
&& entry.severity != EventSeverity::Warning) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (m_eventSeverityFilter == EventSeverity::Error
|
||||
&& entry.severity != EventSeverity::Error) {
|
||||
continue;
|
||||
}
|
||||
|
||||
visibleLines.push_back(entry.line);
|
||||
}
|
||||
|
||||
m_eventLog->setPlainText(visibleLines.join(QChar::fromLatin1('\n')));
|
||||
m_eventLog->moveCursor(QTextCursor::End);
|
||||
}
|
||||
|
||||
SessionTab::EventSeverity SessionTab::classifyEventSeverity(const QString& message)
|
||||
{
|
||||
const QString normalized = message.trimmed().toLower();
|
||||
if (normalized.startsWith(QStringLiteral("error:"))
|
||||
|| normalized.contains(QStringLiteral("failed"))
|
||||
|| normalized.contains(QStringLiteral("permission denied"))) {
|
||||
return EventSeverity::Error;
|
||||
}
|
||||
|
||||
if (normalized.startsWith(QStringLiteral("warning:"))
|
||||
|| normalized.contains(QStringLiteral("warning"))) {
|
||||
return EventSeverity::Warning;
|
||||
}
|
||||
|
||||
return EventSeverity::Info;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user