Milestone 5: deliver embedded RDP sessions and lifecycle hardening
This commit is contained in:
@@ -32,11 +32,14 @@ ProfileDialog::ProfileDialog(QWidget* parent)
|
||||
m_hostInput(new QLineEdit(this)),
|
||||
m_portInput(new QSpinBox(this)),
|
||||
m_usernameInput(new QLineEdit(this)),
|
||||
m_domainInput(new QLineEdit(this)),
|
||||
m_protocolInput(new QComboBox(this)),
|
||||
m_authModeInput(new QComboBox(this)),
|
||||
m_privateKeyPathInput(new QLineEdit(this)),
|
||||
m_browsePrivateKeyButton(new QPushButton(QStringLiteral("Browse"), this)),
|
||||
m_knownHostsPolicyInput(new QComboBox(this))
|
||||
m_knownHostsPolicyInput(new QComboBox(this)),
|
||||
m_rdpSecurityModeInput(new QComboBox(this)),
|
||||
m_rdpPerformanceProfileInput(new QComboBox(this))
|
||||
{
|
||||
resize(520, 340);
|
||||
|
||||
@@ -48,11 +51,18 @@ ProfileDialog::ProfileDialog(QWidget* parent)
|
||||
m_portInput->setRange(1, 65535);
|
||||
m_portInput->setValue(22);
|
||||
m_usernameInput->setPlaceholderText(QStringLiteral("deploy"));
|
||||
m_domainInput->setPlaceholderText(QStringLiteral("CONTOSO"));
|
||||
|
||||
m_protocolInput->addItems({QStringLiteral("SSH"), QStringLiteral("RDP"), QStringLiteral("VNC")});
|
||||
m_authModeInput->addItems({QStringLiteral("Password"), QStringLiteral("Private Key")});
|
||||
m_knownHostsPolicyInput->addItems(
|
||||
{QStringLiteral("Ask"), QStringLiteral("Strict"), QStringLiteral("Accept New"), QStringLiteral("Ignore")});
|
||||
m_rdpSecurityModeInput->addItems(
|
||||
{QStringLiteral("Negotiate"), QStringLiteral("NLA"), QStringLiteral("TLS"), QStringLiteral("RDP")});
|
||||
m_rdpPerformanceProfileInput->addItems({QStringLiteral("Balanced"),
|
||||
QStringLiteral("Best Quality"),
|
||||
QStringLiteral("Best Performance"),
|
||||
QStringLiteral("Auto Detect")});
|
||||
|
||||
m_privateKeyPathInput->setPlaceholderText(QStringLiteral("/home/user/.ssh/id_ed25519"));
|
||||
|
||||
@@ -80,6 +90,10 @@ ProfileDialog::ProfileDialog(QWidget* parent)
|
||||
this,
|
||||
[this](const QString& protocol) {
|
||||
m_portInput->setValue(standardPortForProtocol(protocol));
|
||||
if (protocol != QStringLiteral("SSH")) {
|
||||
const QSignalBlocker blocker(m_authModeInput);
|
||||
m_authModeInput->setCurrentText(QStringLiteral("Password"));
|
||||
}
|
||||
refreshAuthFields();
|
||||
});
|
||||
|
||||
@@ -92,10 +106,13 @@ ProfileDialog::ProfileDialog(QWidget* parent)
|
||||
form->addRow(QStringLiteral("Host"), m_hostInput);
|
||||
form->addRow(QStringLiteral("Port"), m_portInput);
|
||||
form->addRow(QStringLiteral("Username"), m_usernameInput);
|
||||
form->addRow(QStringLiteral("Domain"), m_domainInput);
|
||||
form->addRow(QStringLiteral("Protocol"), m_protocolInput);
|
||||
form->addRow(QStringLiteral("Auth Mode"), m_authModeInput);
|
||||
form->addRow(QStringLiteral("Private Key"), privateKeyRow);
|
||||
form->addRow(QStringLiteral("Known Hosts"), m_knownHostsPolicyInput);
|
||||
form->addRow(QStringLiteral("RDP Security"), m_rdpSecurityModeInput);
|
||||
form->addRow(QStringLiteral("RDP Performance"), m_rdpPerformanceProfileInput);
|
||||
|
||||
auto* note = new QLabel(
|
||||
QStringLiteral("Passwords are requested at connect time and are not stored."),
|
||||
@@ -124,6 +141,7 @@ void ProfileDialog::setProfile(const Profile& profile)
|
||||
m_hostInput->setText(profile.host);
|
||||
m_portInput->setValue(profile.port > 0 ? profile.port : 22);
|
||||
m_usernameInput->setText(profile.username);
|
||||
m_domainInput->setText(profile.domain);
|
||||
m_privateKeyPathInput->setText(profile.privateKeyPath);
|
||||
|
||||
const int protocolIndex = m_protocolInput->findText(profile.protocol);
|
||||
@@ -138,6 +156,12 @@ void ProfileDialog::setProfile(const Profile& profile)
|
||||
|
||||
const int knownHostsIndex = m_knownHostsPolicyInput->findText(profile.knownHostsPolicy);
|
||||
m_knownHostsPolicyInput->setCurrentIndex(knownHostsIndex >= 0 ? knownHostsIndex : 0);
|
||||
const int securityModeIndex = m_rdpSecurityModeInput->findText(profile.rdpSecurityMode);
|
||||
m_rdpSecurityModeInput->setCurrentIndex(securityModeIndex >= 0 ? securityModeIndex : 0);
|
||||
const int performanceProfileIndex =
|
||||
m_rdpPerformanceProfileInput->findText(profile.rdpPerformanceProfile);
|
||||
m_rdpPerformanceProfileInput->setCurrentIndex(performanceProfileIndex >= 0 ? performanceProfileIndex
|
||||
: 0);
|
||||
|
||||
refreshAuthFields();
|
||||
}
|
||||
@@ -150,10 +174,13 @@ Profile ProfileDialog::profile() const
|
||||
profile.host = m_hostInput->text().trimmed();
|
||||
profile.port = m_portInput->value();
|
||||
profile.username = m_usernameInput->text().trimmed();
|
||||
profile.domain = m_domainInput->text().trimmed();
|
||||
profile.protocol = m_protocolInput->currentText();
|
||||
profile.authMode = m_authModeInput->currentText();
|
||||
profile.privateKeyPath = m_privateKeyPathInput->text().trimmed();
|
||||
profile.knownHostsPolicy = m_knownHostsPolicyInput->currentText();
|
||||
profile.rdpSecurityMode = m_rdpSecurityModeInput->currentText();
|
||||
profile.rdpPerformanceProfile = m_rdpPerformanceProfileInput->currentText();
|
||||
return profile;
|
||||
}
|
||||
|
||||
@@ -173,11 +200,12 @@ void ProfileDialog::accept()
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_protocolInput->currentText() == QStringLiteral("SSH")
|
||||
const QString protocol = m_protocolInput->currentText();
|
||||
if ((protocol == QStringLiteral("SSH") || protocol == QStringLiteral("RDP"))
|
||||
&& m_usernameInput->text().trimmed().isEmpty()) {
|
||||
QMessageBox::warning(this,
|
||||
QStringLiteral("Validation Error"),
|
||||
QStringLiteral("Username is required for SSH profiles."));
|
||||
QStringLiteral("Username is required for %1 profiles.").arg(protocol));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -187,10 +215,14 @@ void ProfileDialog::accept()
|
||||
void ProfileDialog::refreshAuthFields()
|
||||
{
|
||||
const bool isSsh = m_protocolInput->currentText() == QStringLiteral("SSH");
|
||||
const bool isRdp = m_protocolInput->currentText() == QStringLiteral("RDP");
|
||||
const bool isPrivateKey = m_authModeInput->currentText() == QStringLiteral("Private Key");
|
||||
|
||||
m_authModeInput->setEnabled(isSsh);
|
||||
m_privateKeyPathInput->setEnabled(isSsh && isPrivateKey);
|
||||
m_browsePrivateKeyButton->setEnabled(isSsh && isPrivateKey);
|
||||
m_knownHostsPolicyInput->setEnabled(isSsh);
|
||||
m_domainInput->setEnabled(isRdp);
|
||||
m_rdpSecurityModeInput->setEnabled(isRdp);
|
||||
m_rdpPerformanceProfileInput->setEnabled(isRdp);
|
||||
}
|
||||
|
||||
@@ -29,11 +29,14 @@ private:
|
||||
QLineEdit* m_hostInput;
|
||||
QSpinBox* m_portInput;
|
||||
QLineEdit* m_usernameInput;
|
||||
QLineEdit* m_domainInput;
|
||||
QComboBox* m_protocolInput;
|
||||
QComboBox* m_authModeInput;
|
||||
QLineEdit* m_privateKeyPathInput;
|
||||
QPushButton* m_browsePrivateKeyButton;
|
||||
QComboBox* m_knownHostsPolicyInput;
|
||||
QComboBox* m_rdpSecurityModeInput;
|
||||
QComboBox* m_rdpPerformanceProfileInput;
|
||||
|
||||
void refreshAuthFields();
|
||||
};
|
||||
|
||||
@@ -22,18 +22,51 @@ QString buildDatabasePath()
|
||||
return dataDir.filePath(QStringLiteral("orbithub_profiles.sqlite"));
|
||||
}
|
||||
|
||||
QString normalizedRdpSecurityMode(const QString& value)
|
||||
{
|
||||
const QString mode = value.trimmed();
|
||||
if (mode.compare(QStringLiteral("NLA"), Qt::CaseInsensitive) == 0) {
|
||||
return QStringLiteral("NLA");
|
||||
}
|
||||
if (mode.compare(QStringLiteral("TLS"), Qt::CaseInsensitive) == 0) {
|
||||
return QStringLiteral("TLS");
|
||||
}
|
||||
if (mode.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0) {
|
||||
return QStringLiteral("RDP");
|
||||
}
|
||||
return QStringLiteral("Negotiate");
|
||||
}
|
||||
|
||||
QString normalizedRdpPerformanceProfile(const QString& value)
|
||||
{
|
||||
const QString profile = value.trimmed();
|
||||
if (profile.compare(QStringLiteral("Best Quality"), Qt::CaseInsensitive) == 0) {
|
||||
return QStringLiteral("Best Quality");
|
||||
}
|
||||
if (profile.compare(QStringLiteral("Best Performance"), Qt::CaseInsensitive) == 0) {
|
||||
return QStringLiteral("Best Performance");
|
||||
}
|
||||
if (profile.compare(QStringLiteral("Auto Detect"), Qt::CaseInsensitive) == 0) {
|
||||
return QStringLiteral("Auto Detect");
|
||||
}
|
||||
return QStringLiteral("Balanced");
|
||||
}
|
||||
|
||||
void bindProfileFields(QSqlQuery& query, const Profile& profile)
|
||||
{
|
||||
query.addBindValue(profile.name.trimmed());
|
||||
query.addBindValue(profile.host.trimmed());
|
||||
query.addBindValue(profile.port);
|
||||
query.addBindValue(profile.username.trimmed());
|
||||
query.addBindValue(profile.domain.trimmed());
|
||||
query.addBindValue(profile.protocol.trimmed());
|
||||
query.addBindValue(profile.authMode.trimmed());
|
||||
query.addBindValue(profile.privateKeyPath.trimmed());
|
||||
query.addBindValue(profile.knownHostsPolicy.trimmed().isEmpty()
|
||||
? QStringLiteral("Ask")
|
||||
: profile.knownHostsPolicy.trimmed());
|
||||
query.addBindValue(normalizedRdpSecurityMode(profile.rdpSecurityMode));
|
||||
query.addBindValue(normalizedRdpPerformanceProfile(profile.rdpPerformanceProfile));
|
||||
}
|
||||
|
||||
Profile profileFromQuery(const QSqlQuery& query)
|
||||
@@ -44,13 +77,16 @@ Profile profileFromQuery(const QSqlQuery& query)
|
||||
profile.host = query.value(2).toString();
|
||||
profile.port = query.value(3).toInt();
|
||||
profile.username = query.value(4).toString();
|
||||
profile.protocol = query.value(5).toString();
|
||||
profile.authMode = query.value(6).toString();
|
||||
profile.privateKeyPath = query.value(7).toString();
|
||||
profile.knownHostsPolicy = query.value(8).toString();
|
||||
profile.domain = query.value(5).toString();
|
||||
profile.protocol = query.value(6).toString();
|
||||
profile.authMode = query.value(7).toString();
|
||||
profile.privateKeyPath = query.value(8).toString();
|
||||
profile.knownHostsPolicy = query.value(9).toString();
|
||||
if (profile.knownHostsPolicy.isEmpty()) {
|
||||
profile.knownHostsPolicy = QStringLiteral("Ask");
|
||||
}
|
||||
profile.rdpSecurityMode = normalizedRdpSecurityMode(query.value(10).toString());
|
||||
profile.rdpPerformanceProfile = normalizedRdpPerformanceProfile(query.value(11).toString());
|
||||
return profile;
|
||||
}
|
||||
|
||||
@@ -102,12 +138,12 @@ std::vector<Profile> ProfileRepository::listProfiles(const QString& searchQuery)
|
||||
QSqlQuery query(QSqlDatabase::database(m_connectionName));
|
||||
if (searchQuery.trimmed().isEmpty()) {
|
||||
query.prepare(QStringLiteral(
|
||||
"SELECT id, name, host, port, username, protocol, auth_mode, private_key_path, known_hosts_policy "
|
||||
"SELECT id, name, host, port, username, domain, protocol, auth_mode, private_key_path, known_hosts_policy, rdp_security_mode, rdp_performance_profile "
|
||||
"FROM profiles "
|
||||
"ORDER BY lower(name) ASC, id ASC"));
|
||||
} else {
|
||||
query.prepare(QStringLiteral(
|
||||
"SELECT id, name, host, port, username, protocol, auth_mode, private_key_path, known_hosts_policy "
|
||||
"SELECT id, name, host, port, username, domain, protocol, auth_mode, private_key_path, known_hosts_policy, rdp_security_mode, rdp_performance_profile "
|
||||
"FROM profiles "
|
||||
"WHERE lower(name) LIKE lower(?) OR lower(host) LIKE lower(?) "
|
||||
"ORDER BY lower(name) ASC, id ASC"));
|
||||
@@ -138,7 +174,7 @@ std::optional<Profile> ProfileRepository::getProfile(qint64 id) const
|
||||
|
||||
QSqlQuery query(QSqlDatabase::database(m_connectionName));
|
||||
query.prepare(QStringLiteral(
|
||||
"SELECT id, name, host, port, username, protocol, auth_mode, private_key_path, known_hosts_policy "
|
||||
"SELECT id, name, host, port, username, domain, protocol, auth_mode, private_key_path, known_hosts_policy, rdp_security_mode, rdp_performance_profile "
|
||||
"FROM profiles WHERE id = ?"));
|
||||
query.addBindValue(id);
|
||||
|
||||
@@ -169,8 +205,8 @@ std::optional<Profile> ProfileRepository::createProfile(const Profile& profile)
|
||||
|
||||
QSqlQuery query(QSqlDatabase::database(m_connectionName));
|
||||
query.prepare(QStringLiteral(
|
||||
"INSERT INTO profiles(name, host, port, username, protocol, auth_mode, private_key_path, known_hosts_policy) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)"));
|
||||
"INSERT INTO profiles(name, host, port, username, domain, protocol, auth_mode, private_key_path, known_hosts_policy, rdp_security_mode, rdp_performance_profile) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"));
|
||||
bindProfileFields(query, profile);
|
||||
|
||||
if (!query.exec()) {
|
||||
@@ -199,7 +235,7 @@ bool ProfileRepository::updateProfile(const Profile& profile) const
|
||||
QSqlQuery query(QSqlDatabase::database(m_connectionName));
|
||||
query.prepare(QStringLiteral(
|
||||
"UPDATE profiles "
|
||||
"SET name = ?, host = ?, port = ?, username = ?, protocol = ?, auth_mode = ?, private_key_path = ?, known_hosts_policy = ? "
|
||||
"SET name = ?, host = ?, port = ?, username = ?, domain = ?, protocol = ?, auth_mode = ?, private_key_path = ?, known_hosts_policy = ?, rdp_security_mode = ?, rdp_performance_profile = ? "
|
||||
"WHERE id = ?"));
|
||||
bindProfileFields(query, profile);
|
||||
query.addBindValue(profile.id);
|
||||
@@ -250,10 +286,13 @@ bool ProfileRepository::initializeDatabase()
|
||||
"host TEXT NOT NULL DEFAULT '',"
|
||||
"port INTEGER NOT NULL DEFAULT 22,"
|
||||
"username TEXT NOT NULL DEFAULT '',"
|
||||
"domain TEXT NOT NULL DEFAULT '',"
|
||||
"protocol TEXT NOT NULL DEFAULT 'SSH',"
|
||||
"auth_mode TEXT NOT NULL DEFAULT 'Password',"
|
||||
"private_key_path TEXT NOT NULL DEFAULT '',"
|
||||
"known_hosts_policy TEXT NOT NULL DEFAULT 'Ask'"
|
||||
"known_hosts_policy TEXT NOT NULL DEFAULT 'Ask',"
|
||||
"rdp_security_mode TEXT NOT NULL DEFAULT 'Negotiate',"
|
||||
"rdp_performance_profile TEXT NOT NULL DEFAULT 'Balanced'"
|
||||
")"));
|
||||
|
||||
if (!created) {
|
||||
@@ -296,10 +335,13 @@ bool ProfileRepository::ensureProfileSchema() const
|
||||
{QStringLiteral("host"), QStringLiteral("ALTER TABLE profiles ADD COLUMN host TEXT NOT NULL DEFAULT ''")},
|
||||
{QStringLiteral("port"), QStringLiteral("ALTER TABLE profiles ADD COLUMN port INTEGER NOT NULL DEFAULT 22")},
|
||||
{QStringLiteral("username"), QStringLiteral("ALTER TABLE profiles ADD COLUMN username TEXT NOT NULL DEFAULT ''")},
|
||||
{QStringLiteral("domain"), QStringLiteral("ALTER TABLE profiles ADD COLUMN domain TEXT NOT NULL DEFAULT ''")},
|
||||
{QStringLiteral("protocol"), QStringLiteral("ALTER TABLE profiles ADD COLUMN protocol TEXT NOT NULL DEFAULT 'SSH'")},
|
||||
{QStringLiteral("auth_mode"), QStringLiteral("ALTER TABLE profiles ADD COLUMN auth_mode TEXT NOT NULL DEFAULT 'Password'")},
|
||||
{QStringLiteral("private_key_path"), QStringLiteral("ALTER TABLE profiles ADD COLUMN private_key_path TEXT NOT NULL DEFAULT ''")},
|
||||
{QStringLiteral("known_hosts_policy"), QStringLiteral("ALTER TABLE profiles ADD COLUMN known_hosts_policy TEXT NOT NULL DEFAULT 'Ask'")}};
|
||||
{QStringLiteral("known_hosts_policy"), QStringLiteral("ALTER TABLE profiles ADD COLUMN known_hosts_policy TEXT NOT NULL DEFAULT 'Ask'")},
|
||||
{QStringLiteral("rdp_security_mode"), QStringLiteral("ALTER TABLE profiles ADD COLUMN rdp_security_mode TEXT NOT NULL DEFAULT 'Negotiate'")},
|
||||
{QStringLiteral("rdp_performance_profile"), QStringLiteral("ALTER TABLE profiles ADD COLUMN rdp_performance_profile TEXT NOT NULL DEFAULT 'Balanced'")}};
|
||||
|
||||
for (const ColumnDef& column : required) {
|
||||
if (columns.contains(column.name)) {
|
||||
|
||||
@@ -14,10 +14,13 @@ struct Profile
|
||||
QString host;
|
||||
int port = 22;
|
||||
QString username;
|
||||
QString domain;
|
||||
QString protocol = QStringLiteral("SSH");
|
||||
QString authMode = QStringLiteral("Password");
|
||||
QString privateKeyPath;
|
||||
QString knownHostsPolicy = QStringLiteral("Ask");
|
||||
QString rdpSecurityMode = QStringLiteral("Negotiate");
|
||||
QString rdpPerformanceProfile = QStringLiteral("Balanced");
|
||||
};
|
||||
|
||||
class ProfileRepository
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
#include "profile_repository.h"
|
||||
#include "session_window.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <QAbstractItemView>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
@@ -119,13 +117,29 @@ void ProfilesWindow::loadProfiles(const QString& query)
|
||||
for (const Profile& profile : profiles) {
|
||||
auto* item = new QListWidgetItem(formatProfileListItem(profile), m_profilesList);
|
||||
item->setData(Qt::UserRole, QVariant::fromValue(profile.id));
|
||||
item->setToolTip(QStringLiteral("%1://%2@%3:%4\nAuth: %5\nKnown Hosts: %6")
|
||||
.arg(profile.protocol,
|
||||
profile.username.isEmpty() ? QStringLiteral("<none>") : profile.username,
|
||||
profile.host,
|
||||
QString::number(profile.port),
|
||||
profile.authMode,
|
||||
profile.knownHostsPolicy));
|
||||
const QString identity = [&profile]() {
|
||||
if (profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0
|
||||
&& !profile.domain.trimmed().isEmpty()) {
|
||||
return QStringLiteral("%1\\%2").arg(profile.domain.trimmed(),
|
||||
profile.username.trimmed().isEmpty()
|
||||
? QStringLiteral("<none>")
|
||||
: profile.username.trimmed());
|
||||
}
|
||||
return profile.username.isEmpty() ? QStringLiteral("<none>") : profile.username;
|
||||
}();
|
||||
QString tooltip = QStringLiteral("%1://%2@%3:%4\nAuth: %5")
|
||||
.arg(profile.protocol,
|
||||
identity,
|
||||
profile.host,
|
||||
QString::number(profile.port),
|
||||
profile.authMode);
|
||||
if (profile.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive) == 0) {
|
||||
tooltip += QStringLiteral("\nKnown Hosts: %1").arg(profile.knownHostsPolicy);
|
||||
} else if (profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0) {
|
||||
tooltip += QStringLiteral("\nRDP Security: %1\nRDP Performance: %2")
|
||||
.arg(profile.rdpSecurityMode, profile.rdpPerformanceProfile);
|
||||
}
|
||||
item->setToolTip(tooltip);
|
||||
m_profileCache.insert_or_assign(profile.id, profile);
|
||||
}
|
||||
}
|
||||
@@ -264,22 +278,16 @@ void ProfilesWindow::openSessionForItem(QListWidgetItem* item)
|
||||
return;
|
||||
}
|
||||
|
||||
auto* session = new SessionWindow(profile.value());
|
||||
session->setAttribute(Qt::WA_DeleteOnClose);
|
||||
if (m_sessionWindow.isNull()) {
|
||||
m_sessionWindow = new SessionWindow(profile.value());
|
||||
m_sessionWindow->setAttribute(Qt::WA_DeleteOnClose);
|
||||
connect(m_sessionWindow, &QObject::destroyed, this, [this]() { m_sessionWindow = nullptr; });
|
||||
} else {
|
||||
m_sessionWindow->openProfile(profile.value());
|
||||
}
|
||||
|
||||
m_sessionWindows.emplace_back(session);
|
||||
connect(session,
|
||||
&QObject::destroyed,
|
||||
this,
|
||||
[this](QObject* object) {
|
||||
m_sessionWindows.erase(
|
||||
std::remove_if(m_sessionWindows.begin(),
|
||||
m_sessionWindows.end(),
|
||||
[object](const QPointer<SessionWindow>& candidate) {
|
||||
return candidate.isNull() || candidate.data() == object;
|
||||
}),
|
||||
m_sessionWindows.end());
|
||||
});
|
||||
|
||||
session->show();
|
||||
m_sessionWindow->setWindowState(m_sessionWindow->windowState() & ~Qt::WindowMinimized);
|
||||
m_sessionWindow->show();
|
||||
m_sessionWindow->raise();
|
||||
m_sessionWindow->activateWindow();
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
#include <optional>
|
||||
#include <QPointer>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
class QListWidget;
|
||||
class QListWidgetItem;
|
||||
@@ -33,7 +32,7 @@ private:
|
||||
QPushButton* m_newButton;
|
||||
QPushButton* m_editButton;
|
||||
QPushButton* m_deleteButton;
|
||||
std::vector<QPointer<SessionWindow>> m_sessionWindows;
|
||||
QPointer<SessionWindow> m_sessionWindow;
|
||||
std::unique_ptr<ProfileRepository> m_repository;
|
||||
std::unordered_map<qint64, Profile> m_profileCache;
|
||||
|
||||
|
||||
207
src/rdp_display_widget.cpp
Normal file
207
src/rdp_display_widget.cpp
Normal file
@@ -0,0 +1,207 @@
|
||||
#include "rdp_display_widget.h"
|
||||
|
||||
#include <QKeyEvent>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QResizeEvent>
|
||||
#include <QTimer>
|
||||
#include <QWheelEvent>
|
||||
#include <QtGlobal>
|
||||
|
||||
namespace {
|
||||
QSize sanitizeSize(const QSize& size)
|
||||
{
|
||||
return QSize(qMax(1, size.width()), qMax(1, size.height()));
|
||||
}
|
||||
}
|
||||
|
||||
RdpDisplayWidget::RdpDisplayWidget(QWidget* parent)
|
||||
: QWidget(parent), m_remoteSize(1280, 720)
|
||||
{
|
||||
setFocusPolicy(Qt::StrongFocus);
|
||||
setMouseTracking(true);
|
||||
setAutoFillBackground(false);
|
||||
setMinimumSize(320, 200);
|
||||
|
||||
QTimer::singleShot(0, this, [this]() {
|
||||
const QSize size = sanitizeSize(this->size());
|
||||
emit viewportSizeChanged(size.width(), size.height());
|
||||
});
|
||||
}
|
||||
|
||||
void RdpDisplayWidget::setFrame(const QImage& frame)
|
||||
{
|
||||
if (frame.isNull()) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_frame = frame;
|
||||
m_remoteSize = sanitizeSize(frame.size());
|
||||
update();
|
||||
}
|
||||
|
||||
void RdpDisplayWidget::setRemoteDesktopSize(int width, int height)
|
||||
{
|
||||
if (width < 1 || height < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QSize nextSize(width, height);
|
||||
if (m_remoteSize == nextSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_remoteSize = nextSize;
|
||||
update();
|
||||
}
|
||||
|
||||
void RdpDisplayWidget::clearFrame()
|
||||
{
|
||||
m_frame = QImage();
|
||||
update();
|
||||
}
|
||||
|
||||
void RdpDisplayWidget::paintEvent(QPaintEvent* event)
|
||||
{
|
||||
Q_UNUSED(event);
|
||||
|
||||
QPainter painter(this);
|
||||
painter.fillRect(rect(), QColor(QStringLiteral("#101214")));
|
||||
|
||||
const QRectF target = renderRect();
|
||||
if (!m_frame.isNull()) {
|
||||
painter.drawImage(target, m_frame);
|
||||
} else {
|
||||
painter.setPen(QColor(QStringLiteral("#b0bec5")));
|
||||
painter.drawText(rect(),
|
||||
Qt::AlignCenter,
|
||||
QStringLiteral("Waiting for remote desktop frame..."));
|
||||
}
|
||||
}
|
||||
|
||||
void RdpDisplayWidget::resizeEvent(QResizeEvent* event)
|
||||
{
|
||||
QWidget::resizeEvent(event);
|
||||
const QSize size = sanitizeSize(event->size());
|
||||
emit viewportSizeChanged(size.width(), size.height());
|
||||
}
|
||||
|
||||
void RdpDisplayWidget::keyPressEvent(QKeyEvent* event)
|
||||
{
|
||||
if (event == nullptr || event->isAutoRepeat()) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit keyInput(event->key(),
|
||||
event->nativeScanCode(),
|
||||
event->text(),
|
||||
true,
|
||||
static_cast<int>(event->modifiers()));
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void RdpDisplayWidget::keyReleaseEvent(QKeyEvent* event)
|
||||
{
|
||||
if (event == nullptr || event->isAutoRepeat()) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit keyInput(event->key(),
|
||||
event->nativeScanCode(),
|
||||
event->text(),
|
||||
false,
|
||||
static_cast<int>(event->modifiers()));
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void RdpDisplayWidget::mousePressEvent(QMouseEvent* event)
|
||||
{
|
||||
if (event == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
setFocus(Qt::MouseFocusReason);
|
||||
const QPoint mapped = mapToRemote(event->position());
|
||||
emit mouseButtonInput(mapped.x(), mapped.y(), static_cast<int>(event->button()), true);
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void RdpDisplayWidget::mouseReleaseEvent(QMouseEvent* event)
|
||||
{
|
||||
if (event == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QPoint mapped = mapToRemote(event->position());
|
||||
emit mouseButtonInput(mapped.x(), mapped.y(), static_cast<int>(event->button()), false);
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void RdpDisplayWidget::mouseMoveEvent(QMouseEvent* event)
|
||||
{
|
||||
if (event == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QPoint mapped = mapToRemote(event->position());
|
||||
emit mouseMoveInput(mapped.x(), mapped.y());
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void RdpDisplayWidget::wheelEvent(QWheelEvent* event)
|
||||
{
|
||||
if (event == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QPoint mapped = mapToRemote(event->position());
|
||||
const QPoint angle = event->angleDelta();
|
||||
emit mouseWheelInput(mapped.x(), mapped.y(), angle.x(), angle.y());
|
||||
event->accept();
|
||||
}
|
||||
|
||||
QRectF RdpDisplayWidget::renderRect() const
|
||||
{
|
||||
const QSize remote = effectiveRemoteSize();
|
||||
const QRectF area = rect();
|
||||
if (area.isEmpty()) {
|
||||
return QRectF();
|
||||
}
|
||||
|
||||
const qreal scale = qMin(area.width() / remote.width(), area.height() / remote.height());
|
||||
const qreal drawWidth = remote.width() * scale;
|
||||
const qreal drawHeight = remote.height() * scale;
|
||||
const qreal x = area.x() + ((area.width() - drawWidth) * 0.5);
|
||||
const qreal y = area.y() + ((area.height() - drawHeight) * 0.5);
|
||||
return QRectF(x, y, drawWidth, drawHeight);
|
||||
}
|
||||
|
||||
QPoint RdpDisplayWidget::mapToRemote(const QPointF& pos) const
|
||||
{
|
||||
const QSize remote = effectiveRemoteSize();
|
||||
const QRectF target = renderRect();
|
||||
if (target.isEmpty()) {
|
||||
return QPoint(0, 0);
|
||||
}
|
||||
|
||||
const qreal clampedX = qBound(target.left(), pos.x(), target.right());
|
||||
const qreal clampedY = qBound(target.top(), pos.y(), target.bottom());
|
||||
|
||||
const qreal normalizedX = (clampedX - target.left()) / qMax(1.0, target.width());
|
||||
const qreal normalizedY = (clampedY - target.top()) / qMax(1.0, target.height());
|
||||
|
||||
const int remoteX = qBound(0, static_cast<int>(normalizedX * remote.width()), remote.width() - 1);
|
||||
const int remoteY = qBound(0, static_cast<int>(normalizedY * remote.height()), remote.height() - 1);
|
||||
return QPoint(remoteX, remoteY);
|
||||
}
|
||||
|
||||
QSize RdpDisplayWidget::effectiveRemoteSize() const
|
||||
{
|
||||
if (m_remoteSize.width() > 0 && m_remoteSize.height() > 0) {
|
||||
return m_remoteSize;
|
||||
}
|
||||
if (!m_frame.isNull()) {
|
||||
return sanitizeSize(m_frame.size());
|
||||
}
|
||||
return QSize(1280, 720);
|
||||
}
|
||||
50
src/rdp_display_widget.h
Normal file
50
src/rdp_display_widget.h
Normal file
@@ -0,0 +1,50 @@
|
||||
#ifndef ORBITHUB_RDP_DISPLAY_WIDGET_H
|
||||
#define ORBITHUB_RDP_DISPLAY_WIDGET_H
|
||||
|
||||
#include <QImage>
|
||||
#include <QWidget>
|
||||
|
||||
class QKeyEvent;
|
||||
class QMouseEvent;
|
||||
class QPaintEvent;
|
||||
class QResizeEvent;
|
||||
class QWheelEvent;
|
||||
|
||||
class RdpDisplayWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit RdpDisplayWidget(QWidget* parent = nullptr);
|
||||
|
||||
void setFrame(const QImage& frame);
|
||||
void setRemoteDesktopSize(int width, int height);
|
||||
void clearFrame();
|
||||
|
||||
signals:
|
||||
void keyInput(int key, quint32 nativeScanCode, const QString& text, bool pressed, int modifiers);
|
||||
void mouseMoveInput(int x, int y);
|
||||
void mouseButtonInput(int x, int y, int button, bool pressed);
|
||||
void mouseWheelInput(int x, int y, int deltaX, int deltaY);
|
||||
void viewportSizeChanged(int width, int height);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent* event) override;
|
||||
void resizeEvent(QResizeEvent* event) override;
|
||||
void keyPressEvent(QKeyEvent* event) override;
|
||||
void keyReleaseEvent(QKeyEvent* event) override;
|
||||
void mousePressEvent(QMouseEvent* event) override;
|
||||
void mouseReleaseEvent(QMouseEvent* event) override;
|
||||
void mouseMoveEvent(QMouseEvent* event) override;
|
||||
void wheelEvent(QWheelEvent* event) override;
|
||||
|
||||
private:
|
||||
QImage m_frame;
|
||||
QSize m_remoteSize;
|
||||
|
||||
QRectF renderRect() const;
|
||||
QPoint mapToRemote(const QPointF& pos) const;
|
||||
QSize effectiveRemoteSize() const;
|
||||
};
|
||||
|
||||
#endif
|
||||
1809
src/rdp_session_backend.cpp
Normal file
1809
src/rdp_session_backend.cpp
Normal file
File diff suppressed because it is too large
Load Diff
108
src/rdp_session_backend.h
Normal file
108
src/rdp_session_backend.h
Normal file
@@ -0,0 +1,108 @@
|
||||
#ifndef ORBITHUB_RDP_SESSION_BACKEND_H
|
||||
#define ORBITHUB_RDP_SESSION_BACKEND_H
|
||||
|
||||
#include "session_backend.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
struct rdp_freerdp;
|
||||
|
||||
class RdpSessionBackend : public SessionBackend
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit RdpSessionBackend(const Profile& profile, QObject* parent = nullptr);
|
||||
~RdpSessionBackend() override;
|
||||
|
||||
public slots:
|
||||
void connectSession(const SessionConnectOptions& options) override;
|
||||
void disconnectSession() override;
|
||||
void reconnectSession(const SessionConnectOptions& options) override;
|
||||
void sendInput(const QString& input) override;
|
||||
void confirmHostKey(bool trustHost) override;
|
||||
void updateTerminalSize(int columns, int rows) override;
|
||||
void sendKeyEvent(int key,
|
||||
quint32 nativeScanCode,
|
||||
const QString& text,
|
||||
bool pressed,
|
||||
int modifiers) override;
|
||||
void sendMouseMoveEvent(int x, int y) override;
|
||||
void sendMouseButtonEvent(int x, int y, int button, bool pressed) override;
|
||||
void sendMouseWheelEvent(int x, int y, int deltaX, int deltaY) override;
|
||||
|
||||
private:
|
||||
enum class InputEventType {
|
||||
Key,
|
||||
MouseMove,
|
||||
MouseButton,
|
||||
MouseWheel,
|
||||
Resize,
|
||||
};
|
||||
|
||||
struct InputEvent {
|
||||
InputEventType type = InputEventType::MouseMove;
|
||||
int key = 0;
|
||||
quint32 nativeScanCode = 0;
|
||||
QString text;
|
||||
bool pressed = false;
|
||||
int modifiers = 0;
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
int button = 0;
|
||||
int deltaX = 0;
|
||||
int deltaY = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
};
|
||||
|
||||
SessionState m_state;
|
||||
SessionConnectOptions m_activeOptions;
|
||||
std::atomic_bool m_userInitiatedDisconnect;
|
||||
|
||||
std::atomic_int m_requestedDesktopWidth;
|
||||
std::atomic_int m_requestedDesktopHeight;
|
||||
|
||||
std::thread m_worker;
|
||||
std::atomic_bool m_workerRunning;
|
||||
std::atomic_bool m_stopRequested;
|
||||
|
||||
std::mutex m_instanceMutex;
|
||||
rdp_freerdp* m_instance;
|
||||
|
||||
std::mutex m_inputMutex;
|
||||
std::deque<InputEvent> m_inputEvents;
|
||||
|
||||
std::mutex m_displayControlMutex;
|
||||
void* m_displayControlContext;
|
||||
bool m_displayControlReady;
|
||||
bool m_resizeFailureLogged;
|
||||
int m_lastResizeWidth;
|
||||
int m_lastResizeHeight;
|
||||
|
||||
void setState(SessionState state, const QString& message);
|
||||
bool validateProfile(QString& message) const;
|
||||
void startWorker();
|
||||
void stopWorker(bool userInitiated);
|
||||
void workerMain();
|
||||
void enqueueInputEvent(const InputEvent& event);
|
||||
void processInputEvents(rdp_freerdp* instance);
|
||||
bool sendDisplayResize(rdp_freerdp* instance, int width, int height);
|
||||
public:
|
||||
void onChannelConnectedEvent(const char* name, void* channelInterface);
|
||||
void onChannelDisconnectedEvent(const char* name, void* channelInterface);
|
||||
void onDisplayControlCaps(uint32_t maxNumMonitors,
|
||||
uint32_t maxMonitorAreaFactorA,
|
||||
uint32_t maxMonitorAreaFactorB);
|
||||
private:
|
||||
void emitStateAsync(SessionState state, const QString& message);
|
||||
void emitConnectionFailureAsync(const QString& displayMessage, const QString& rawMessage);
|
||||
int sanitizeDesktopWidth(int width) const;
|
||||
int sanitizeDesktopHeight(int height) const;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -3,8 +3,10 @@
|
||||
|
||||
#include "profile_repository.h"
|
||||
|
||||
#include <QImage>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QtGlobal>
|
||||
|
||||
class SessionConnectOptions
|
||||
{
|
||||
@@ -44,6 +46,37 @@ public slots:
|
||||
virtual void sendInput(const QString& input) = 0;
|
||||
virtual void confirmHostKey(bool trustHost) = 0;
|
||||
virtual void updateTerminalSize(int columns, int rows) = 0;
|
||||
virtual void sendKeyEvent(int key,
|
||||
quint32 nativeScanCode,
|
||||
const QString& text,
|
||||
bool pressed,
|
||||
int modifiers)
|
||||
{
|
||||
Q_UNUSED(key);
|
||||
Q_UNUSED(nativeScanCode);
|
||||
Q_UNUSED(text);
|
||||
Q_UNUSED(pressed);
|
||||
Q_UNUSED(modifiers);
|
||||
}
|
||||
virtual void sendMouseMoveEvent(int x, int y)
|
||||
{
|
||||
Q_UNUSED(x);
|
||||
Q_UNUSED(y);
|
||||
}
|
||||
virtual void sendMouseButtonEvent(int x, int y, int button, bool pressed)
|
||||
{
|
||||
Q_UNUSED(x);
|
||||
Q_UNUSED(y);
|
||||
Q_UNUSED(button);
|
||||
Q_UNUSED(pressed);
|
||||
}
|
||||
virtual void sendMouseWheelEvent(int x, int y, int deltaX, int deltaY)
|
||||
{
|
||||
Q_UNUSED(x);
|
||||
Q_UNUSED(y);
|
||||
Q_UNUSED(deltaX);
|
||||
Q_UNUSED(deltaY);
|
||||
}
|
||||
|
||||
signals:
|
||||
void stateChanged(SessionState state, const QString& message);
|
||||
@@ -51,6 +84,8 @@ signals:
|
||||
void connectionError(const QString& displayMessage, const QString& rawMessage);
|
||||
void outputReceived(const QString& text);
|
||||
void hostKeyConfirmationRequested(const QString& prompt);
|
||||
void frameUpdated(const QImage& frame);
|
||||
void remoteDesktopSizeChanged(int width, int height);
|
||||
|
||||
private:
|
||||
Profile m_profile;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "session_backend_factory.h"
|
||||
|
||||
#include "rdp_session_backend.h"
|
||||
#include "session_backend.h"
|
||||
#include "ssh_session_backend.h"
|
||||
#include "unsupported_session_backend.h"
|
||||
@@ -9,6 +10,9 @@ std::unique_ptr<SessionBackend> createSessionBackend(const Profile& profile)
|
||||
if (profile.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive) == 0) {
|
||||
return std::make_unique<SshSessionBackend>(profile);
|
||||
}
|
||||
if (profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0) {
|
||||
return std::make_unique<RdpSessionBackend>(profile);
|
||||
}
|
||||
|
||||
return std::make_unique<UnsupportedSessionBackend>(profile);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "session_tab.h"
|
||||
|
||||
#include "rdp_display_widget.h"
|
||||
#include "session_backend_factory.h"
|
||||
#include "terminal_view.h"
|
||||
|
||||
@@ -62,6 +63,7 @@ SessionTab::SessionTab(const Profile& profile, QWidget* parent)
|
||||
m_state(SessionState::Disconnected),
|
||||
m_terminalThemeName(QStringLiteral("Dark")),
|
||||
m_sshTerminal(nullptr),
|
||||
m_rdpDisplay(nullptr),
|
||||
m_terminalOutput(nullptr),
|
||||
m_eventLog(nullptr),
|
||||
m_toggleEventsButton(nullptr),
|
||||
@@ -148,6 +150,26 @@ SessionTab::SessionTab(const Profile& profile, QWidget* parent)
|
||||
m_backend,
|
||||
&SessionBackend::updateTerminalSize,
|
||||
Qt::QueuedConnection);
|
||||
connect(this,
|
||||
&SessionTab::requestKeyEvent,
|
||||
m_backend,
|
||||
&SessionBackend::sendKeyEvent,
|
||||
Qt::QueuedConnection);
|
||||
connect(this,
|
||||
&SessionTab::requestMouseMoveEvent,
|
||||
m_backend,
|
||||
&SessionBackend::sendMouseMoveEvent,
|
||||
Qt::QueuedConnection);
|
||||
connect(this,
|
||||
&SessionTab::requestMouseButtonEvent,
|
||||
m_backend,
|
||||
&SessionBackend::sendMouseButtonEvent,
|
||||
Qt::QueuedConnection);
|
||||
connect(this,
|
||||
&SessionTab::requestMouseWheelEvent,
|
||||
m_backend,
|
||||
&SessionBackend::sendMouseWheelEvent,
|
||||
Qt::QueuedConnection);
|
||||
|
||||
connect(m_backend,
|
||||
&SessionBackend::stateChanged,
|
||||
@@ -174,6 +196,24 @@ SessionTab::SessionTab(const Profile& profile, QWidget* parent)
|
||||
this,
|
||||
&SessionTab::onBackendHostKeyConfirmationRequested,
|
||||
Qt::QueuedConnection);
|
||||
connect(m_backend,
|
||||
&SessionBackend::frameUpdated,
|
||||
this,
|
||||
[this](const QImage& frame) {
|
||||
if (m_rdpDisplay != nullptr) {
|
||||
m_rdpDisplay->setFrame(frame);
|
||||
}
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
connect(m_backend,
|
||||
&SessionBackend::remoteDesktopSizeChanged,
|
||||
this,
|
||||
[this](int width, int height) {
|
||||
if (m_rdpDisplay != nullptr) {
|
||||
m_rdpDisplay->setRemoteDesktopSize(width, height);
|
||||
}
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
|
||||
m_backendThread->start();
|
||||
}
|
||||
@@ -284,6 +324,12 @@ void SessionTab::clearTerminal()
|
||||
emit requestInput(QStringLiteral("\x0c"));
|
||||
}
|
||||
m_terminalOutput->setFocus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_rdpDisplay != nullptr) {
|
||||
m_rdpDisplay->clearFrame();
|
||||
m_rdpDisplay->setFocus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,6 +354,16 @@ QString SessionTab::terminalThemeName() const
|
||||
return m_terminalThemeName;
|
||||
}
|
||||
|
||||
bool SessionTab::supportsThemeSelection() const
|
||||
{
|
||||
return m_useKodoTermForSsh || m_terminalOutput != nullptr;
|
||||
}
|
||||
|
||||
bool SessionTab::supportsClearAction() const
|
||||
{
|
||||
return m_useKodoTermForSsh || m_terminalOutput != nullptr;
|
||||
}
|
||||
|
||||
void SessionTab::onBackendStateChanged(SessionState state, const QString& message)
|
||||
{
|
||||
setState(state, message);
|
||||
@@ -322,6 +378,9 @@ void SessionTab::onBackendConnectionError(const QString& displayMessage, const Q
|
||||
{
|
||||
m_lastError = rawMessage.isEmpty() ? displayMessage : rawMessage;
|
||||
appendEvent(QStringLiteral("Error: %1").arg(displayMessage));
|
||||
if (!rawMessage.trimmed().isEmpty() && rawMessage.trimmed() != displayMessage.trimmed()) {
|
||||
appendEvent(QStringLiteral("Raw Error: %1").arg(rawMessage.trimmed()));
|
||||
}
|
||||
}
|
||||
|
||||
void SessionTab::onBackendOutputReceived(const QString& text)
|
||||
@@ -363,12 +422,21 @@ void SessionTab::setupUi()
|
||||
config.maxScrollback = 12000;
|
||||
m_sshTerminal->setConfig(config);
|
||||
rootLayout->addWidget(m_sshTerminal, 1);
|
||||
} else if (m_profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0) {
|
||||
m_rdpDisplay = new RdpDisplayWidget(this);
|
||||
rootLayout->addWidget(m_rdpDisplay, 1);
|
||||
} else {
|
||||
m_terminalOutput = new TerminalView(this);
|
||||
m_terminalOutput->setFont(defaultTerminalFont());
|
||||
m_terminalOutput->setMinimumHeight(260);
|
||||
m_terminalOutput->setPlaceholderText(
|
||||
QStringLiteral("Session is connecting. Type directly here once connected."));
|
||||
m_terminalOutput->setReadOnly(true);
|
||||
if (m_profile.protocol.compare(QStringLiteral("VNC"), Qt::CaseInsensitive) == 0) {
|
||||
m_terminalOutput->setPlaceholderText(
|
||||
QStringLiteral("Embedded VNC session output appears here when the backend is available."));
|
||||
} else {
|
||||
m_terminalOutput->setPlaceholderText(
|
||||
QStringLiteral("Session output appears here."));
|
||||
}
|
||||
rootLayout->addWidget(m_terminalOutput, 1);
|
||||
}
|
||||
|
||||
@@ -415,6 +483,33 @@ void SessionTab::setupUi()
|
||||
&TerminalView::terminalSizeChanged,
|
||||
this,
|
||||
[this](int columns, int rows) { emit requestTerminalSize(columns, rows); });
|
||||
} else if (m_rdpDisplay != nullptr) {
|
||||
connect(m_rdpDisplay,
|
||||
&RdpDisplayWidget::viewportSizeChanged,
|
||||
this,
|
||||
[this](int width, int height) { emit requestTerminalSize(width, height); });
|
||||
connect(m_rdpDisplay,
|
||||
&RdpDisplayWidget::keyInput,
|
||||
this,
|
||||
[this](int key, quint32 nativeScanCode, const QString& text, bool pressed, int modifiers) {
|
||||
emit requestKeyEvent(key, nativeScanCode, text, pressed, modifiers);
|
||||
});
|
||||
connect(m_rdpDisplay,
|
||||
&RdpDisplayWidget::mouseMoveInput,
|
||||
this,
|
||||
[this](int x, int y) { emit requestMouseMoveEvent(x, y); });
|
||||
connect(m_rdpDisplay,
|
||||
&RdpDisplayWidget::mouseButtonInput,
|
||||
this,
|
||||
[this](int x, int y, int button, bool pressed) {
|
||||
emit requestMouseButtonEvent(x, y, button, pressed);
|
||||
});
|
||||
connect(m_rdpDisplay,
|
||||
&RdpDisplayWidget::mouseWheelInput,
|
||||
this,
|
||||
[this](int x, int y, int deltaX, int deltaY) {
|
||||
emit requestMouseWheelEvent(x, y, deltaX, deltaY);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,7 +518,41 @@ std::optional<SessionConnectOptions> SessionTab::buildConnectOptions()
|
||||
SessionConnectOptions options;
|
||||
options.knownHostsPolicy = m_profile.knownHostsPolicy;
|
||||
|
||||
if (m_profile.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive) != 0) {
|
||||
const bool isSsh = m_profile.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive) == 0;
|
||||
const bool isRdp = m_profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0;
|
||||
|
||||
if (!isSsh && !isRdp) {
|
||||
return options;
|
||||
}
|
||||
|
||||
if (isRdp) {
|
||||
if (m_profile.authMode.compare(QStringLiteral("Password"), Qt::CaseInsensitive) != 0) {
|
||||
return options;
|
||||
}
|
||||
|
||||
bool accepted = false;
|
||||
const QString password = QInputDialog::getText(
|
||||
this,
|
||||
QStringLiteral("RDP Password"),
|
||||
QStringLiteral("Password for %1:")
|
||||
.arg(m_profile.username.trimmed().isEmpty()
|
||||
? m_profile.host
|
||||
: QStringLiteral("%1@%2").arg(m_profile.username, m_profile.host)),
|
||||
QLineEdit::Password,
|
||||
QString(),
|
||||
&accepted);
|
||||
if (!accepted) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (password.isEmpty()) {
|
||||
QMessageBox::warning(this,
|
||||
QStringLiteral("Connect"),
|
||||
QStringLiteral("Password is required for password authentication."));
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
options.password = password;
|
||||
return options;
|
||||
}
|
||||
|
||||
@@ -481,28 +610,26 @@ std::optional<SessionConnectOptions> SessionTab::buildConnectOptions()
|
||||
|
||||
bool SessionTab::validateProfileForConnect()
|
||||
{
|
||||
if (m_profile.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive) != 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (m_profile.host.trimmed().isEmpty()) {
|
||||
QMessageBox::warning(this,
|
||||
QStringLiteral("Connect"),
|
||||
QStringLiteral("SSH host is required."));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_profile.username.trimmed().isEmpty()) {
|
||||
QMessageBox::warning(this,
|
||||
QStringLiteral("Connect"),
|
||||
QStringLiteral("SSH username is required."));
|
||||
QStringLiteral("%1 host is required.").arg(m_profile.protocol));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_profile.port < 1 || m_profile.port > 65535) {
|
||||
QMessageBox::warning(this,
|
||||
QStringLiteral("Connect"),
|
||||
QStringLiteral("SSH port must be between 1 and 65535."));
|
||||
QStringLiteral("Port must be between 1 and 65535."));
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((m_profile.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive) == 0
|
||||
|| m_profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0)
|
||||
&& m_profile.username.trimmed().isEmpty()) {
|
||||
QMessageBox::warning(this,
|
||||
QStringLiteral("Connect"),
|
||||
QStringLiteral("%1 username is required.").arg(m_profile.protocol));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -554,6 +681,14 @@ void SessionTab::refreshActionButtons()
|
||||
if (m_terminalOutput != nullptr) {
|
||||
m_terminalOutput->setEnabled(isConnected);
|
||||
m_terminalOutput->setFocus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_rdpDisplay != nullptr) {
|
||||
m_rdpDisplay->setEnabled(isConnected);
|
||||
if (isConnected) {
|
||||
m_rdpDisplay->setFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "session_backend.h"
|
||||
|
||||
#include <QWidget>
|
||||
#include <QtGlobal>
|
||||
|
||||
#include <optional>
|
||||
|
||||
@@ -12,6 +13,7 @@ class QPlainTextEdit;
|
||||
class QThread;
|
||||
class SessionBackend;
|
||||
class TerminalView;
|
||||
class RdpDisplayWidget;
|
||||
class QToolButton;
|
||||
class KodoTerm;
|
||||
|
||||
@@ -30,6 +32,8 @@ public:
|
||||
void clearTerminal();
|
||||
void setTerminalThemeName(const QString& themeName);
|
||||
QString terminalThemeName() const;
|
||||
bool supportsThemeSelection() const;
|
||||
bool supportsClearAction() const;
|
||||
|
||||
signals:
|
||||
void tabTitleChanged(const QString& title);
|
||||
@@ -40,6 +44,14 @@ signals:
|
||||
void requestInput(const QString& input);
|
||||
void requestHostKeyConfirmation(bool trustHost);
|
||||
void requestTerminalSize(int columns, int rows);
|
||||
void requestKeyEvent(int key,
|
||||
quint32 nativeScanCode,
|
||||
const QString& text,
|
||||
bool pressed,
|
||||
int modifiers);
|
||||
void requestMouseMoveEvent(int x, int y);
|
||||
void requestMouseButtonEvent(int x, int y, int button, bool pressed);
|
||||
void requestMouseWheelEvent(int x, int y, int deltaX, int deltaY);
|
||||
|
||||
private slots:
|
||||
void onBackendStateChanged(SessionState state, const QString& message);
|
||||
@@ -59,6 +71,7 @@ private:
|
||||
QString m_terminalThemeName;
|
||||
|
||||
KodoTerm* m_sshTerminal;
|
||||
RdpDisplayWidget* m_rdpDisplay;
|
||||
TerminalView* m_terminalOutput;
|
||||
QPlainTextEdit* m_eventLog;
|
||||
QToolButton* m_toggleEventsButton;
|
||||
|
||||
@@ -72,24 +72,32 @@ SessionWindow::SessionWindow(const Profile& profile, QWidget* parent)
|
||||
QMenu menu(this);
|
||||
QAction* disconnectAction = menu.addAction(QStringLiteral("Disconnect"));
|
||||
QAction* reconnectAction = menu.addAction(QStringLiteral("Reconnect"));
|
||||
menu.addSeparator();
|
||||
QMenu* themeMenu = menu.addMenu(QStringLiteral("Theme"));
|
||||
QList<QAction*> themeActions;
|
||||
const QString currentTheme = tab->terminalThemeName();
|
||||
for (const QString& themeName : terminalThemeNames()) {
|
||||
QAction* themeAction = themeMenu->addAction(themeName);
|
||||
themeAction->setCheckable(true);
|
||||
themeAction->setChecked(
|
||||
themeName.compare(currentTheme, Qt::CaseInsensitive) == 0);
|
||||
themeActions.append(themeAction);
|
||||
|
||||
if (tab->supportsThemeSelection()) {
|
||||
menu.addSeparator();
|
||||
QMenu* themeMenu = menu.addMenu(QStringLiteral("Theme"));
|
||||
const QString currentTheme = tab->terminalThemeName();
|
||||
for (const QString& themeName : terminalThemeNames()) {
|
||||
QAction* themeAction = themeMenu->addAction(themeName);
|
||||
themeAction->setCheckable(true);
|
||||
themeAction->setChecked(
|
||||
themeName.compare(currentTheme, Qt::CaseInsensitive) == 0);
|
||||
themeActions.append(themeAction);
|
||||
}
|
||||
}
|
||||
QAction* clearAction = menu.addAction(QStringLiteral("Clear"));
|
||||
|
||||
QAction* clearAction = nullptr;
|
||||
if (tab->supportsClearAction()) {
|
||||
clearAction = menu.addAction(QStringLiteral("Clear"));
|
||||
}
|
||||
|
||||
QAction* chosen = menu.exec(m_tabs->tabBar()->mapToGlobal(pos));
|
||||
if (chosen == disconnectAction) {
|
||||
tab->disconnectSession();
|
||||
} else if (chosen == reconnectAction) {
|
||||
tab->reconnectSession();
|
||||
} else if (chosen == clearAction) {
|
||||
} else if (clearAction != nullptr && chosen == clearAction) {
|
||||
tab->clearTerminal();
|
||||
} else {
|
||||
for (QAction* themeAction : themeActions) {
|
||||
@@ -105,11 +113,19 @@ SessionWindow::SessionWindow(const Profile& profile, QWidget* parent)
|
||||
addSessionTab(profile);
|
||||
}
|
||||
|
||||
void SessionWindow::openProfile(const Profile& profile)
|
||||
{
|
||||
addSessionTab(profile);
|
||||
}
|
||||
|
||||
void SessionWindow::addSessionTab(const Profile& profile)
|
||||
{
|
||||
auto* tab = new SessionTab(profile, this);
|
||||
const int index = m_tabs->addTab(tab, tab->tabTitle());
|
||||
m_tabs->setCurrentIndex(index);
|
||||
if (m_tabs->count() > 1) {
|
||||
setWindowTitle(QStringLiteral("OrbitHub Sessions"));
|
||||
}
|
||||
m_tabs->tabBar()->setTabTextColor(
|
||||
index, tabColorForState(SessionState::Disconnected, m_tabs->palette()));
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ class SessionWindow : public QMainWindow
|
||||
|
||||
public:
|
||||
explicit SessionWindow(const Profile& profile, QWidget* parent = nullptr);
|
||||
void openProfile(const Profile& profile);
|
||||
|
||||
private:
|
||||
QTabWidget* m_tabs;
|
||||
|
||||
Reference in New Issue
Block a user