Internal
Public Access
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df99c78998 | ||
|
|
dffca3afef | ||
|
|
4f8fa3272b | ||
|
|
96c8403f3b | ||
|
|
3e621219f1 | ||
|
|
e0969041a7 | ||
|
|
c4a62e8fb6 | ||
|
|
9a22597d4e | ||
|
|
e90e9b5abf | ||
|
|
92d8b62820 | ||
|
|
7ee930693e |
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
cmake_minimum_required(VERSION 3.21)
|
cmake_minimum_required(VERSION 3.21)
|
||||||
|
|
||||||
project(OrbitHub VERSION 2026.9.8.3 LANGUAGES CXX)
|
project(OrbitHub VERSION 2026.9.15 LANGUAGES CXX)
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 17)
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
@@ -134,6 +134,7 @@ add_executable(orbithub WIN32 MACOSX_BUNDLE ${ORBITHUB_SOURCES})
|
|||||||
|
|
||||||
target_link_libraries(orbithub PRIVATE Qt6::Widgets Qt6::Sql)
|
target_link_libraries(orbithub PRIVATE Qt6::Widgets Qt6::Sql)
|
||||||
target_link_libraries(orbithub PRIVATE KodoTerm::KodoTerm)
|
target_link_libraries(orbithub PRIVATE KodoTerm::KodoTerm)
|
||||||
|
target_compile_definitions(orbithub PRIVATE ORBITHUB_VERSION_STRING="${PROJECT_VERSION}")
|
||||||
if(TARGET freerdp AND TARGET winpr)
|
if(TARGET freerdp AND TARGET winpr)
|
||||||
target_compile_definitions(orbithub PRIVATE ORBITHUB_HAS_FREERDP)
|
target_compile_definitions(orbithub PRIVATE ORBITHUB_HAS_FREERDP)
|
||||||
target_include_directories(orbithub PRIVATE
|
target_include_directories(orbithub PRIVATE
|
||||||
|
|||||||
@@ -16,14 +16,14 @@ OrbitHub is in active development.
|
|||||||
- Milestones completed: M0-M5, and M7-M9
|
- Milestones completed: M0-M5, and M7-M9
|
||||||
- Current milestone: Milestone 10 (v1.0 Stabilization)
|
- Current milestone: Milestone 10 (v1.0 Stabilization)
|
||||||
- Deferred milestone: Milestone 6 (VNC Fully Working)
|
- Deferred milestone: Milestone 6 (VNC Fully Working)
|
||||||
- Latest checkpoint tag: `v2026.9.8.3`
|
- Latest checkpoint tag: `v2026.9.15`
|
||||||
- VNC implementation milestone (M6) is currently deferred
|
- VNC implementation milestone (M6) is currently deferred
|
||||||
|
|
||||||
Progress and milestone details:
|
Progress and milestone details:
|
||||||
- [docs/PROGRESS.md](docs/PROGRESS.md)
|
- [docs/PROGRESS.md](docs/PROGRESS.md)
|
||||||
|
|
||||||
Latest release (installers for Windows, Linux, and macOS):
|
Latest release (installers for Windows, Linux, and macOS):
|
||||||
- [v2026.9.8.3](https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.8.3)
|
- [v2026.9.15](https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.15)
|
||||||
|
|
||||||
User Guide:
|
User Guide:
|
||||||
- [docs/USER_GUIDE.md](docs/USER_GUIDE.md) (also available as a PDF attached to each release, and in-app via `Help -> User Guide`)
|
- [docs/USER_GUIDE.md](docs/USER_GUIDE.md) (also available as a PDF attached to each release, and in-app via `Help -> User Guide`)
|
||||||
|
|||||||
@@ -200,6 +200,9 @@ Git:
|
|||||||
- Release: [v2026.9.8](https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.8) (`v2026.9.8` tag, installers for Windows/Linux/macOS)
|
- Release: [v2026.9.8](https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.8) (`v2026.9.8` tag, installers for Windows/Linux/macOS)
|
||||||
- Release: [v2026.9.8.2](https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.8.2) — same-day patch fixing RDP TLS certificate verification (was fully disabled) and preparing Flatpak packaging for Flathub submission
|
- Release: [v2026.9.8.2](https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.8.2) — same-day patch fixing RDP TLS certificate verification (was fully disabled) and preparing Flatpak packaging for Flathub submission
|
||||||
- Release: [v2026.9.8.3](https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.8.3) — same-day patch adding an in-app User Guide and standalone User Guide PDF
|
- Release: [v2026.9.8.3](https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.8.3) — same-day patch adding an in-app User Guide and standalone User Guide PDF
|
||||||
|
- Release: [v2026.9.14](https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.14) — fixes distorted RDP text on HiDPI monitors and reduces RDP resize-related display glitches
|
||||||
|
- Release: [v2026.9.14.2](https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.14.2) — same-day patch fixing RDP display corruption (missing/misplaced taskbar) after resizing the session window (the client never resized its own display buffer for channel-driven RDP resizes)
|
||||||
|
- Release: [v2026.9.15](https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.15) — adds Import/Export for profile lists (File menu)
|
||||||
|
|
||||||
## Milestone 10 - v1.0 Stabilization
|
## Milestone 10 - v1.0 Stabilization
|
||||||
|
|
||||||
|
|||||||
@@ -19,5 +19,5 @@ modules:
|
|||||||
sources:
|
sources:
|
||||||
- type: git
|
- type: git
|
||||||
url: https://git.darksingularity.org/DarkSingularity/orbithub.git
|
url: https://git.darksingularity.org/DarkSingularity/orbithub.git
|
||||||
tag: v2026.9.8.2
|
tag: v2026.9.15
|
||||||
commit: 4c649f727fb794f4b5afdc3186e545e20ce2f138
|
commit: dffca3afef80b5a3ca4832e0b7f748775cb90328
|
||||||
|
|||||||
@@ -34,6 +34,24 @@
|
|||||||
</screenshot>
|
</screenshot>
|
||||||
</screenshots>
|
</screenshots>
|
||||||
<releases>
|
<releases>
|
||||||
|
<release version="2026.9.15" date="2026-09-15">
|
||||||
|
<url>https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.15</url>
|
||||||
|
<description>
|
||||||
|
<p>Adds Import/Export for profile lists (File menu).</p>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="2026.9.14.2" date="2026-09-14">
|
||||||
|
<url>https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.14.2</url>
|
||||||
|
<description>
|
||||||
|
<p>Fixes RDP display corruption (missing/misplaced taskbar, stale composited content) after resizing the session window.</p>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="2026.9.14" date="2026-09-14">
|
||||||
|
<url>https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.14</url>
|
||||||
|
<description>
|
||||||
|
<p>Fixes distorted RDP text on HiDPI monitors, and reduces RDP resize-related display glitches.</p>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
<release version="2026.9.8.3" date="2026-09-08">
|
<release version="2026.9.8.3" date="2026-09-08">
|
||||||
<url>https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.8.3</url>
|
<url>https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.8.3</url>
|
||||||
<description>
|
<description>
|
||||||
|
|||||||
+27
-2
@@ -8,6 +8,31 @@
|
|||||||
#include <QTextBrowser>
|
#include <QTextBrowser>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// palette(mid) is meant for 3D bevel/shadow decoration, not text — it has
|
||||||
|
// poor contrast against the window background in dark themes (barely
|
||||||
|
// legible). Blend the widget's actual text and window colors instead, so
|
||||||
|
// the result is reliably readable — de-emphasized relative to full-strength
|
||||||
|
// text, but never low-contrast — in either a light or dark theme.
|
||||||
|
QColor mutedTextColor(const QWidget* widget)
|
||||||
|
{
|
||||||
|
const QPalette pal = widget->palette();
|
||||||
|
const QColor text = pal.color(QPalette::WindowText);
|
||||||
|
const QColor background = pal.color(QPalette::Window);
|
||||||
|
constexpr qreal kTextWeight = 0.65;
|
||||||
|
|
||||||
|
auto blend = [kTextWeight](int textChannel, int backgroundChannel) {
|
||||||
|
return qBound(0,
|
||||||
|
qRound(textChannel * kTextWeight + backgroundChannel * (1.0 - kTextWeight)),
|
||||||
|
255);
|
||||||
|
};
|
||||||
|
|
||||||
|
return QColor(blend(text.red(), background.red()),
|
||||||
|
blend(text.green(), background.green()),
|
||||||
|
blend(text.blue(), background.blue()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
AboutDialog::AboutDialog(QWidget* parent) : QDialog(parent)
|
AboutDialog::AboutDialog(QWidget* parent) : QDialog(parent)
|
||||||
{
|
{
|
||||||
setWindowTitle(QStringLiteral("About OrbitHub"));
|
setWindowTitle(QStringLiteral("About OrbitHub"));
|
||||||
@@ -34,7 +59,7 @@ AboutDialog::AboutDialog(QWidget* parent) : QDialog(parent)
|
|||||||
QStringLiteral("Unified remote session manager for SSH, RDP, and VNC workflows."),
|
QStringLiteral("Unified remote session manager for SSH, RDP, and VNC workflows."),
|
||||||
this);
|
this);
|
||||||
subtitle->setWordWrap(true);
|
subtitle->setWordWrap(true);
|
||||||
subtitle->setStyleSheet(QStringLiteral("color: palette(mid);"));
|
subtitle->setStyleSheet(QStringLiteral("color: %1;").arg(mutedTextColor(this).name()));
|
||||||
|
|
||||||
const QString version = QCoreApplication::applicationVersion().trimmed().isEmpty()
|
const QString version = QCoreApplication::applicationVersion().trimmed().isEmpty()
|
||||||
? QStringLiteral("Development build")
|
? QStringLiteral("Development build")
|
||||||
@@ -42,7 +67,7 @@ AboutDialog::AboutDialog(QWidget* parent) : QDialog(parent)
|
|||||||
auto* buildLine = new QLabel(
|
auto* buildLine = new QLabel(
|
||||||
QStringLiteral("Version: %1 | Qt runtime linked dynamically").arg(version),
|
QStringLiteral("Version: %1 | Qt runtime linked dynamically").arg(version),
|
||||||
this);
|
this);
|
||||||
buildLine->setStyleSheet(QStringLiteral("color: palette(mid);"));
|
buildLine->setStyleSheet(QStringLiteral("color: %1;").arg(mutedTextColor(this).name()));
|
||||||
|
|
||||||
titleColumn->addWidget(title);
|
titleColumn->addWidget(title);
|
||||||
titleColumn->addWidget(subtitle);
|
titleColumn->addWidget(subtitle);
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ int main(int argc, char* argv[])
|
|||||||
QApplication app(argc, argv);
|
QApplication app(argc, argv);
|
||||||
app.setOrganizationName(QStringLiteral("FireBugIT"));
|
app.setOrganizationName(QStringLiteral("FireBugIT"));
|
||||||
app.setApplicationName(QStringLiteral("OrbitHub"));
|
app.setApplicationName(QStringLiteral("OrbitHub"));
|
||||||
|
#ifdef ORBITHUB_VERSION_STRING
|
||||||
|
app.setApplicationVersion(QStringLiteral(ORBITHUB_VERSION_STRING));
|
||||||
|
#endif
|
||||||
app.setWindowIcon(createOrbitHubAppIcon());
|
app.setWindowIcon(createOrbitHubAppIcon());
|
||||||
|
|
||||||
SessionWindow window;
|
SessionWindow window;
|
||||||
|
|||||||
@@ -7,8 +7,13 @@
|
|||||||
#include <QAction>
|
#include <QAction>
|
||||||
#include <QAbstractItemView>
|
#include <QAbstractItemView>
|
||||||
#include <QComboBox>
|
#include <QComboBox>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFileDialog>
|
||||||
#include <QHeaderView>
|
#include <QHeaderView>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QLineEdit>
|
#include <QLineEdit>
|
||||||
#include <QInputDialog>
|
#include <QInputDialog>
|
||||||
@@ -85,6 +90,53 @@ bool profileHasTag(const Profile& profile, const QString& requestedTag)
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constexpr int kProfileExportFormatVersion = 1;
|
||||||
|
|
||||||
|
QJsonObject profileToJson(const Profile& profile)
|
||||||
|
{
|
||||||
|
QJsonObject object;
|
||||||
|
object.insert(QStringLiteral("name"), profile.name);
|
||||||
|
object.insert(QStringLiteral("host"), profile.host);
|
||||||
|
object.insert(QStringLiteral("port"), profile.port);
|
||||||
|
object.insert(QStringLiteral("username"), profile.username);
|
||||||
|
object.insert(QStringLiteral("domain"), profile.domain);
|
||||||
|
object.insert(QStringLiteral("folderPath"), profile.folderPath);
|
||||||
|
object.insert(QStringLiteral("protocol"), profile.protocol);
|
||||||
|
object.insert(QStringLiteral("authMode"), profile.authMode);
|
||||||
|
object.insert(QStringLiteral("privateKeyPath"), profile.privateKeyPath);
|
||||||
|
object.insert(QStringLiteral("knownHostsPolicy"), profile.knownHostsPolicy);
|
||||||
|
object.insert(QStringLiteral("rdpSecurityMode"), profile.rdpSecurityMode);
|
||||||
|
object.insert(QStringLiteral("rdpPerformanceProfile"), profile.rdpPerformanceProfile);
|
||||||
|
object.insert(QStringLiteral("tags"), profile.tags);
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deliberately excludes id (import always creates new rows -- an imported
|
||||||
|
// profile's id has no meaning in the destination database) and any
|
||||||
|
// credential material (none is ever persisted on Profile in the first
|
||||||
|
// place, see ProfileRepository).
|
||||||
|
Profile profileFromJson(const QJsonObject& object)
|
||||||
|
{
|
||||||
|
Profile profile;
|
||||||
|
profile.name = object.value(QStringLiteral("name")).toString();
|
||||||
|
profile.host = object.value(QStringLiteral("host")).toString();
|
||||||
|
profile.port = object.value(QStringLiteral("port")).toInt(22);
|
||||||
|
profile.username = object.value(QStringLiteral("username")).toString();
|
||||||
|
profile.domain = object.value(QStringLiteral("domain")).toString();
|
||||||
|
profile.folderPath = object.value(QStringLiteral("folderPath")).toString();
|
||||||
|
profile.protocol = object.value(QStringLiteral("protocol")).toString(QStringLiteral("SSH"));
|
||||||
|
profile.authMode = object.value(QStringLiteral("authMode")).toString(QStringLiteral("Password"));
|
||||||
|
profile.privateKeyPath = object.value(QStringLiteral("privateKeyPath")).toString();
|
||||||
|
profile.knownHostsPolicy =
|
||||||
|
object.value(QStringLiteral("knownHostsPolicy")).toString(QStringLiteral("Ask"));
|
||||||
|
profile.rdpSecurityMode =
|
||||||
|
object.value(QStringLiteral("rdpSecurityMode")).toString(QStringLiteral("Negotiate"));
|
||||||
|
profile.rdpPerformanceProfile =
|
||||||
|
object.value(QStringLiteral("rdpPerformanceProfile")).toString(QStringLiteral("Balanced"));
|
||||||
|
profile.tags = object.value(QStringLiteral("tags")).toString();
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ProfilesWindow::ProfilesWindow(QWidget* parent)
|
ProfilesWindow::ProfilesWindow(QWidget* parent)
|
||||||
@@ -930,3 +982,128 @@ void ProfilesWindow::createFolderInCurrentContext()
|
|||||||
const QString folderPath = folderPathForItem(m_profilesTree->currentItem());
|
const QString folderPath = folderPathForItem(m_profilesTree->currentItem());
|
||||||
createFolderInContext(folderPath);
|
createFolderInContext(folderPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ProfilesWindow::exportProfiles()
|
||||||
|
{
|
||||||
|
const QString fileName = QFileDialog::getSaveFileName(
|
||||||
|
this, QStringLiteral("Export Profiles"), QStringLiteral("orbithub-profiles.json"),
|
||||||
|
QStringLiteral("JSON Files (*.json)"));
|
||||||
|
if (fileName.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonArray folders;
|
||||||
|
for (const QString& folderPath : m_repository->listFolders()) {
|
||||||
|
folders.append(folderPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonArray profiles;
|
||||||
|
for (const Profile& profile : m_repository->listProfiles()) {
|
||||||
|
profiles.append(profileToJson(profile));
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject root;
|
||||||
|
root.insert(QStringLiteral("orbithubProfileExport"), kProfileExportFormatVersion);
|
||||||
|
root.insert(QStringLiteral("folders"), folders);
|
||||||
|
root.insert(QStringLiteral("profiles"), profiles);
|
||||||
|
|
||||||
|
QFile file(fileName);
|
||||||
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
||||||
|
QMessageBox::warning(this,
|
||||||
|
QStringLiteral("Export Profiles"),
|
||||||
|
QStringLiteral("Failed to write %1: %2")
|
||||||
|
.arg(fileName, file.errorString()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
file.write(QJsonDocument(root).toJson(QJsonDocument::Indented));
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
QMessageBox::information(this,
|
||||||
|
QStringLiteral("Export Profiles"),
|
||||||
|
QStringLiteral("Exported %1 profile(s) to %2.")
|
||||||
|
.arg(profiles.size())
|
||||||
|
.arg(fileName));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProfilesWindow::importProfiles()
|
||||||
|
{
|
||||||
|
const QString fileName = QFileDialog::getOpenFileName(
|
||||||
|
this, QStringLiteral("Import Profiles"), QString(), QStringLiteral("JSON Files (*.json)"));
|
||||||
|
if (fileName.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QFile file(fileName);
|
||||||
|
if (!file.open(QIODevice::ReadOnly)) {
|
||||||
|
QMessageBox::warning(this,
|
||||||
|
QStringLiteral("Import Profiles"),
|
||||||
|
QStringLiteral("Failed to read %1: %2")
|
||||||
|
.arg(fileName, file.errorString()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonParseError parseError{};
|
||||||
|
const QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &parseError);
|
||||||
|
file.close();
|
||||||
|
if (parseError.error != QJsonParseError::NoError || !doc.isObject()) {
|
||||||
|
QMessageBox::warning(this,
|
||||||
|
QStringLiteral("Import Profiles"),
|
||||||
|
QStringLiteral("%1 is not a valid OrbitHub profile export: %2")
|
||||||
|
.arg(fileName, parseError.errorString()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QJsonObject root = doc.object();
|
||||||
|
if (!root.value(QStringLiteral("profiles")).isArray()) {
|
||||||
|
QMessageBox::warning(this,
|
||||||
|
QStringLiteral("Import Profiles"),
|
||||||
|
QStringLiteral("%1 does not contain a profile list.").arg(fileName));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const QJsonValue& folderValue : root.value(QStringLiteral("folders")).toArray()) {
|
||||||
|
const QString folderPath = folderValue.toString();
|
||||||
|
if (!folderPath.isEmpty()) {
|
||||||
|
m_repository->createFolder(folderPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const QJsonArray profilesArray = root.value(QStringLiteral("profiles")).toArray();
|
||||||
|
int imported = 0;
|
||||||
|
QStringList failures;
|
||||||
|
for (const QJsonValue& profileValue : profilesArray) {
|
||||||
|
if (!profileValue.isObject()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const Profile profile = profileFromJson(profileValue.toObject());
|
||||||
|
if (profile.name.isEmpty() || profile.host.isEmpty()) {
|
||||||
|
failures.push_back(QStringLiteral("(unnamed profile): missing name or host"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// No need to createFolder(profile.folderPath) here: the tree view
|
||||||
|
// already synthesizes folder nodes from a profile's own folderPath
|
||||||
|
// (see ProfilesWindow::loadProfiles). Explicit profile_folders rows
|
||||||
|
// are only for folders with no profiles in them, and those are
|
||||||
|
// already recreated above from the export's top-level "folders"
|
||||||
|
// list -- doing it again per-profile would just add spurious
|
||||||
|
// entries not present in the original export.
|
||||||
|
if (m_repository->createProfile(profile).has_value()) {
|
||||||
|
++imported;
|
||||||
|
} else {
|
||||||
|
failures.push_back(QStringLiteral("%1: %2").arg(profile.name, m_repository->lastError()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadProfiles();
|
||||||
|
|
||||||
|
QString summary = QStringLiteral("Imported %1 of %2 profile(s) from %3.")
|
||||||
|
.arg(imported)
|
||||||
|
.arg(profilesArray.size())
|
||||||
|
.arg(fileName);
|
||||||
|
if (!failures.isEmpty()) {
|
||||||
|
summary += QStringLiteral("\n\nFailed:\n%1").arg(failures.join(QChar::fromLatin1('\n')));
|
||||||
|
QMessageBox::warning(this, QStringLiteral("Import Profiles"), summary);
|
||||||
|
} else {
|
||||||
|
QMessageBox::information(this, QStringLiteral("Import Profiles"), summary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ public:
|
|||||||
|
|
||||||
void createProfileInCurrentContext();
|
void createProfileInCurrentContext();
|
||||||
void createFolderInCurrentContext();
|
void createFolderInCurrentContext();
|
||||||
|
void exportProfiles();
|
||||||
|
void importProfiles();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void connectRequested(const Profile& profile);
|
void connectRequested(const Profile& profile);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "rdp_display_widget.h"
|
#include "rdp_display_widget.h"
|
||||||
|
|
||||||
#include <QCursor>
|
#include <QCursor>
|
||||||
|
#include <QEvent>
|
||||||
#include <QKeyEvent>
|
#include <QKeyEvent>
|
||||||
#include <QMouseEvent>
|
#include <QMouseEvent>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
@@ -15,20 +16,38 @@ QSize sanitizeSize(const QSize& size)
|
|||||||
{
|
{
|
||||||
return QSize(qMax(1, size.width()), qMax(1, size.height()));
|
return QSize(qMax(1, size.width()), qMax(1, size.height()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
qreal sanitizeDevicePixelRatio(qreal ratio)
|
||||||
|
{
|
||||||
|
if (!(ratio > 0.0)) {
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
return qBound(1.0, ratio, 4.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Windows' virtual-display driver can visibly glitch (stale composited
|
||||||
|
// content left on screen) when asked to change resolution repeatedly in
|
||||||
|
// quick succession, which naturally happens as the window's layout settles
|
||||||
|
// right after creation/connect. Coalescing bursts of resize events into one
|
||||||
|
// request avoids triggering that.
|
||||||
|
constexpr int kResizeDebounceMs = 150;
|
||||||
}
|
}
|
||||||
|
|
||||||
RdpDisplayWidget::RdpDisplayWidget(QWidget* parent)
|
RdpDisplayWidget::RdpDisplayWidget(QWidget* parent)
|
||||||
: QWidget(parent), m_remoteSize(1280, 720), m_cursorMode(CursorMode::Default)
|
: QWidget(parent),
|
||||||
|
m_remoteSize(1280, 720),
|
||||||
|
m_cursorMode(CursorMode::Default),
|
||||||
|
m_resizeDebounceTimer(new QTimer(this))
|
||||||
{
|
{
|
||||||
setFocusPolicy(Qt::StrongFocus);
|
setFocusPolicy(Qt::StrongFocus);
|
||||||
setMouseTracking(true);
|
setMouseTracking(true);
|
||||||
setAutoFillBackground(false);
|
setAutoFillBackground(false);
|
||||||
setMinimumSize(320, 200);
|
setMinimumSize(320, 200);
|
||||||
|
|
||||||
QTimer::singleShot(0, this, [this]() {
|
m_resizeDebounceTimer->setSingleShot(true);
|
||||||
const QSize size = sanitizeSize(this->size());
|
connect(m_resizeDebounceTimer, &QTimer::timeout, this, &RdpDisplayWidget::emitViewportGeometry);
|
||||||
emit viewportSizeChanged(size.width(), size.height());
|
|
||||||
});
|
scheduleViewportGeometryEmit();
|
||||||
}
|
}
|
||||||
|
|
||||||
void RdpDisplayWidget::setFrame(const QImage& frame)
|
void RdpDisplayWidget::setFrame(const QImage& frame)
|
||||||
@@ -54,6 +73,13 @@ void RdpDisplayWidget::setRemoteDesktopSize(int width, int height)
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_remoteSize = nextSize;
|
m_remoteSize = nextSize;
|
||||||
|
// The next actual frame (via setFrame) arrives asynchronously and isn't
|
||||||
|
// guaranteed to be sized to match yet. Drawing the old frame stretched
|
||||||
|
// to a renderRect() computed from the new m_remoteSize would scale it
|
||||||
|
// by the wrong factor for the transition window, producing visibly
|
||||||
|
// distorted/duplicated-looking content. Clear it and show the existing
|
||||||
|
// "waiting for frame" placeholder until a correctly-sized frame lands.
|
||||||
|
m_frame = QImage();
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,11 +166,39 @@ void RdpDisplayWidget::paintEvent(QPaintEvent* event)
|
|||||||
void RdpDisplayWidget::resizeEvent(QResizeEvent* event)
|
void RdpDisplayWidget::resizeEvent(QResizeEvent* event)
|
||||||
{
|
{
|
||||||
QWidget::resizeEvent(event);
|
QWidget::resizeEvent(event);
|
||||||
const QSize size = sanitizeSize(event->size());
|
scheduleViewportGeometryEmit();
|
||||||
emit viewportSizeChanged(size.width(), size.height());
|
|
||||||
applyCursor();
|
applyCursor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool RdpDisplayWidget::event(QEvent* event)
|
||||||
|
{
|
||||||
|
// Fires when this widget's effective screen changes (e.g. dragged to a
|
||||||
|
// different monitor), which is what changes devicePixelRatio(). Newer
|
||||||
|
// Qt versions add a more specific QEvent::DevicePixelRatioChange, but
|
||||||
|
// this project's Qt 6.2 floor doesn't have it.
|
||||||
|
if (event->type() == QEvent::ScreenChangeInternal) {
|
||||||
|
scheduleViewportGeometryEmit();
|
||||||
|
}
|
||||||
|
return QWidget::event(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RdpDisplayWidget::scheduleViewportGeometryEmit()
|
||||||
|
{
|
||||||
|
// Restarting an already-running single-shot timer resets its countdown,
|
||||||
|
// so a burst of resize events collapses into one emission after things
|
||||||
|
// settle, rather than one request per event.
|
||||||
|
m_resizeDebounceTimer->start(kResizeDebounceMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RdpDisplayWidget::emitViewportGeometry()
|
||||||
|
{
|
||||||
|
const QSize logicalSize = sanitizeSize(this->size());
|
||||||
|
const qreal ratio = sanitizeDevicePixelRatio(this->devicePixelRatioF());
|
||||||
|
const QSize physicalSize(qRound(logicalSize.width() * ratio), qRound(logicalSize.height() * ratio));
|
||||||
|
emit viewportSizeChanged(physicalSize.width(), physicalSize.height());
|
||||||
|
emit displayScaleChanged(ratio);
|
||||||
|
}
|
||||||
|
|
||||||
void RdpDisplayWidget::keyPressEvent(QKeyEvent* event)
|
void RdpDisplayWidget::keyPressEvent(QKeyEvent* event)
|
||||||
{
|
{
|
||||||
if (event == nullptr) {
|
if (event == nullptr) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ class QKeyEvent;
|
|||||||
class QMouseEvent;
|
class QMouseEvent;
|
||||||
class QPaintEvent;
|
class QPaintEvent;
|
||||||
class QResizeEvent;
|
class QResizeEvent;
|
||||||
|
class QTimer;
|
||||||
class QWheelEvent;
|
class QWheelEvent;
|
||||||
|
|
||||||
class RdpDisplayWidget : public QWidget
|
class RdpDisplayWidget : public QWidget
|
||||||
@@ -30,10 +31,12 @@ signals:
|
|||||||
void mouseButtonInput(int x, int y, int button, bool pressed);
|
void mouseButtonInput(int x, int y, int button, bool pressed);
|
||||||
void mouseWheelInput(int x, int y, int deltaX, int deltaY);
|
void mouseWheelInput(int x, int y, int deltaX, int deltaY);
|
||||||
void viewportSizeChanged(int width, int height);
|
void viewportSizeChanged(int width, int height);
|
||||||
|
void displayScaleChanged(qreal devicePixelRatio);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void paintEvent(QPaintEvent* event) override;
|
void paintEvent(QPaintEvent* event) override;
|
||||||
void resizeEvent(QResizeEvent* event) override;
|
void resizeEvent(QResizeEvent* event) override;
|
||||||
|
bool event(QEvent* event) override;
|
||||||
void keyPressEvent(QKeyEvent* event) override;
|
void keyPressEvent(QKeyEvent* event) override;
|
||||||
void keyReleaseEvent(QKeyEvent* event) override;
|
void keyReleaseEvent(QKeyEvent* event) override;
|
||||||
void mousePressEvent(QMouseEvent* event) override;
|
void mousePressEvent(QMouseEvent* event) override;
|
||||||
@@ -54,11 +57,14 @@ private:
|
|||||||
QImage m_cursorImage;
|
QImage m_cursorImage;
|
||||||
QPoint m_cursorHotspot;
|
QPoint m_cursorHotspot;
|
||||||
CursorMode m_cursorMode;
|
CursorMode m_cursorMode;
|
||||||
|
QTimer* m_resizeDebounceTimer;
|
||||||
|
|
||||||
QRectF renderRect() const;
|
QRectF renderRect() const;
|
||||||
QPoint mapToRemote(const QPointF& pos) const;
|
QPoint mapToRemote(const QPointF& pos) const;
|
||||||
QSize effectiveRemoteSize() const;
|
QSize effectiveRemoteSize() const;
|
||||||
void applyCursor();
|
void applyCursor();
|
||||||
|
void emitViewportGeometry();
|
||||||
|
void scheduleViewportGeometryEmit();
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
+113
-5
@@ -104,6 +104,22 @@ QString normalizedRdpSecurityMode(const QString& value)
|
|||||||
return QStringLiteral("Negotiate");
|
return QStringLiteral("Negotiate");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MS-RDPEDISP restricts DesktopScaleFactor/DeviceScaleFactor to exactly
|
||||||
|
// these three values; FreeRDP's own reference client enforces the same
|
||||||
|
// set (client/common/cmdline.c, parse_scale_options). Anything else is
|
||||||
|
// silently ignored by the server, so map the real, continuous
|
||||||
|
// devicePixelRatio down to the nearest one.
|
||||||
|
UINT32 nearestFreeRdpScaleValue(qreal ratio)
|
||||||
|
{
|
||||||
|
if (ratio <= 1.2) {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
if (ratio <= 1.6) {
|
||||||
|
return 140;
|
||||||
|
}
|
||||||
|
return 180;
|
||||||
|
}
|
||||||
|
|
||||||
QString normalizedRdpPerformanceProfile(const QString& value)
|
QString normalizedRdpPerformanceProfile(const QString& value)
|
||||||
{
|
{
|
||||||
const QString profile = value.trimmed();
|
const QString profile = value.trimmed();
|
||||||
@@ -1304,6 +1320,7 @@ RdpSessionBackend::RdpSessionBackend(const Profile& profile, QObject* parent)
|
|||||||
m_userInitiatedDisconnect(false),
|
m_userInitiatedDisconnect(false),
|
||||||
m_requestedDesktopWidth(kDefaultDesktopWidth),
|
m_requestedDesktopWidth(kDefaultDesktopWidth),
|
||||||
m_requestedDesktopHeight(kDefaultDesktopHeight),
|
m_requestedDesktopHeight(kDefaultDesktopHeight),
|
||||||
|
m_devicePixelRatio(1.0),
|
||||||
m_workerRunning(false),
|
m_workerRunning(false),
|
||||||
m_stopRequested(false),
|
m_stopRequested(false),
|
||||||
m_instance(nullptr),
|
m_instance(nullptr),
|
||||||
@@ -1312,6 +1329,7 @@ RdpSessionBackend::RdpSessionBackend(const Profile& profile, QObject* parent)
|
|||||||
m_resizeFailureLogged(false),
|
m_resizeFailureLogged(false),
|
||||||
m_lastResizeWidth(0),
|
m_lastResizeWidth(0),
|
||||||
m_lastResizeHeight(0),
|
m_lastResizeHeight(0),
|
||||||
|
m_lastResizeScale(0),
|
||||||
m_cliprdrContext(nullptr)
|
m_cliprdrContext(nullptr)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -1403,6 +1421,28 @@ void RdpSessionBackend::updateTerminalSize(int columns, int rows)
|
|||||||
enqueueInputEvent(event);
|
enqueueInputEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RdpSessionBackend::updateDisplayScale(qreal devicePixelRatio)
|
||||||
|
{
|
||||||
|
const qreal clamped = qBound(1.0, devicePixelRatio, 4.0);
|
||||||
|
m_devicePixelRatio.store(clamped);
|
||||||
|
|
||||||
|
if (!m_workerRunning.load()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reuses the resize input-event path so the worker thread (which owns
|
||||||
|
// m_instance) picks this up safely; processInputEvents' dedup check
|
||||||
|
// also compares the quantized scale value, so this correctly triggers
|
||||||
|
// a fresh SendMonitorLayout even when width/height haven't changed
|
||||||
|
// (e.g. the window moved to a different-DPI monitor at the same
|
||||||
|
// logical size).
|
||||||
|
InputEvent event;
|
||||||
|
event.type = InputEventType::Resize;
|
||||||
|
event.width = sanitizeDesktopWidth(m_requestedDesktopWidth.load());
|
||||||
|
event.height = sanitizeDesktopHeight(m_requestedDesktopHeight.load());
|
||||||
|
enqueueInputEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
void RdpSessionBackend::sendKeyEvent(int key,
|
void RdpSessionBackend::sendKeyEvent(int key,
|
||||||
quint32 nativeScanCode,
|
quint32 nativeScanCode,
|
||||||
const QString& text,
|
const QString& text,
|
||||||
@@ -1637,9 +1677,29 @@ void RdpSessionBackend::workerMain()
|
|||||||
const QString performanceProfile = normalizedRdpPerformanceProfile(p.rdpPerformanceProfile);
|
const QString performanceProfile = normalizedRdpPerformanceProfile(p.rdpPerformanceProfile);
|
||||||
freerdp_settings_set_bool(settings, FreeRDP_SupportDisplayControl, TRUE);
|
freerdp_settings_set_bool(settings, FreeRDP_SupportDisplayControl, TRUE);
|
||||||
freerdp_settings_set_bool(settings, FreeRDP_DynamicResolutionUpdate, TRUE);
|
freerdp_settings_set_bool(settings, FreeRDP_DynamicResolutionUpdate, TRUE);
|
||||||
|
// Lets us actively request a full-screen repaint after a resize (see
|
||||||
|
// sendDisplayResize) — some RDP hosts (particularly VMs using a
|
||||||
|
// synthetic/virtual display driver) occasionally fail to fully redraw
|
||||||
|
// their own desktop after a resolution change; requesting a refresh
|
||||||
|
// forces them to resend everything rather than leaving stale content.
|
||||||
|
freerdp_settings_set_bool(settings, FreeRDP_RefreshRect, TRUE);
|
||||||
freerdp_settings_set_uint32(settings, FreeRDP_DesktopWidth, static_cast<UINT32>(desktopWidth));
|
freerdp_settings_set_uint32(settings, FreeRDP_DesktopWidth, static_cast<UINT32>(desktopWidth));
|
||||||
freerdp_settings_set_uint32(settings, FreeRDP_DesktopHeight, static_cast<UINT32>(desktopHeight));
|
freerdp_settings_set_uint32(settings, FreeRDP_DesktopHeight, static_cast<UINT32>(desktopHeight));
|
||||||
freerdp_settings_set_uint32(settings, FreeRDP_ColorDepth, 32);
|
freerdp_settings_set_uint32(settings, FreeRDP_ColorDepth, 32);
|
||||||
|
// Tell the remote session the real display scale so it renders its own
|
||||||
|
// UI/ClearType text natively at that size, instead of assuming 96 DPI
|
||||||
|
// and having the result stretched (and ClearType's subpixel hinting
|
||||||
|
// distorted) by client-side scaling. The override flags are required —
|
||||||
|
// without them FreeRDP ignores the scale factor values entirely.
|
||||||
|
{
|
||||||
|
const UINT32 scaleValue = nearestFreeRdpScaleValue(m_devicePixelRatio.load());
|
||||||
|
freerdp_settings_set_uint32(settings, FreeRDP_DesktopScaleFactor, scaleValue);
|
||||||
|
freerdp_settings_set_uint32(settings, FreeRDP_DeviceScaleFactor, scaleValue);
|
||||||
|
freerdp_settings_set_uint64(settings,
|
||||||
|
FreeRDP_MonitorOverrideFlags,
|
||||||
|
FREERDP_MONITOR_OVERRIDE_DESKTOP_SCALE
|
||||||
|
| FREERDP_MONITOR_OVERRIDE_DEVICE_SCALE);
|
||||||
|
}
|
||||||
freerdp_settings_set_bool(settings, FreeRDP_AuthenticationOnly, FALSE);
|
freerdp_settings_set_bool(settings, FreeRDP_AuthenticationOnly, FALSE);
|
||||||
freerdp_settings_set_bool(settings, FreeRDP_AutoLogonEnabled, TRUE);
|
freerdp_settings_set_bool(settings, FreeRDP_AutoLogonEnabled, TRUE);
|
||||||
if (!applyRdpSecurityMode(settings, securityMode)) {
|
if (!applyRdpSecurityMode(settings, securityMode)) {
|
||||||
@@ -1831,9 +1891,14 @@ bool RdpSessionBackend::sendDisplayResize(rdp_freerdp* instance, int width, int
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const qreal ratio = m_devicePixelRatio.load();
|
||||||
|
const UINT32 scaleValue = nearestFreeRdpScaleValue(ratio);
|
||||||
|
|
||||||
rdpSettings* settings = instance->context->settings;
|
rdpSettings* settings = instance->context->settings;
|
||||||
freerdp_settings_set_uint32(settings, FreeRDP_DesktopWidth, static_cast<UINT32>(width));
|
freerdp_settings_set_uint32(settings, FreeRDP_DesktopWidth, static_cast<UINT32>(width));
|
||||||
freerdp_settings_set_uint32(settings, FreeRDP_DesktopHeight, static_cast<UINT32>(height));
|
freerdp_settings_set_uint32(settings, FreeRDP_DesktopHeight, static_cast<UINT32>(height));
|
||||||
|
freerdp_settings_set_uint32(settings, FreeRDP_DesktopScaleFactor, scaleValue);
|
||||||
|
freerdp_settings_set_uint32(settings, FreeRDP_DeviceScaleFactor, scaleValue);
|
||||||
|
|
||||||
DispClientContext* dispContext = nullptr;
|
DispClientContext* dispContext = nullptr;
|
||||||
bool displayControlReady = false;
|
bool displayControlReady = false;
|
||||||
@@ -1851,17 +1916,58 @@ bool RdpSessionBackend::sendDisplayResize(rdp_freerdp* instance, int width, int
|
|||||||
layout.Width = static_cast<UINT32>(width);
|
layout.Width = static_cast<UINT32>(width);
|
||||||
layout.Height = static_cast<UINT32>(height);
|
layout.Height = static_cast<UINT32>(height);
|
||||||
layout.Orientation = ORIENTATION_LANDSCAPE;
|
layout.Orientation = ORIENTATION_LANDSCAPE;
|
||||||
layout.DesktopScaleFactor = freerdp_settings_get_uint32(settings, FreeRDP_DesktopScaleFactor);
|
layout.DesktopScaleFactor = scaleValue;
|
||||||
layout.DeviceScaleFactor = freerdp_settings_get_uint32(settings, FreeRDP_DeviceScaleFactor);
|
layout.DeviceScaleFactor = scaleValue;
|
||||||
|
// Physical size in mm must reflect the real DPI (96 * ratio), not
|
||||||
|
// the plain baseline, so it stays consistent with the scale factor
|
||||||
|
// above rather than implying a standard-DPI display of this size.
|
||||||
|
const double effectiveDpi = kDefaultDpi * ratio;
|
||||||
layout.PhysicalWidth = static_cast<UINT32>(
|
layout.PhysicalWidth = static_cast<UINT32>(
|
||||||
std::lround((static_cast<double>(width) / kDefaultDpi) * kMillimetersPerInch));
|
std::lround((static_cast<double>(width) / effectiveDpi) * kMillimetersPerInch));
|
||||||
layout.PhysicalHeight = static_cast<UINT32>(
|
layout.PhysicalHeight = static_cast<UINT32>(
|
||||||
std::lround((static_cast<double>(height) / kDefaultDpi) * kMillimetersPerInch));
|
std::lround((static_cast<double>(height) / effectiveDpi) * kMillimetersPerInch));
|
||||||
|
|
||||||
const UINT rc = dispContext->SendMonitorLayout(dispContext, 1, &layout);
|
const UINT rc = dispContext->SendMonitorLayout(dispContext, 1, &layout);
|
||||||
if (rc == CHANNEL_RC_OK) {
|
if (rc == CHANNEL_RC_OK) {
|
||||||
m_lastResizeWidth = width;
|
m_lastResizeWidth = width;
|
||||||
m_lastResizeHeight = height;
|
m_lastResizeHeight = height;
|
||||||
|
m_lastResizeScale = static_cast<int>(scaleValue);
|
||||||
|
|
||||||
|
// The Display Control channel (MS-RDPEDISP) has no server
|
||||||
|
// acknowledgment PDU, and real hosts apply the new resolution
|
||||||
|
// without a Deactivation-Reactivation sequence — so
|
||||||
|
// update->DesktopResize (which only fires for that sequence, or
|
||||||
|
// for the GFX/Progressive pipeline we don't use) never runs for
|
||||||
|
// a channel-driven resize. Without a matching gdi_resize() call,
|
||||||
|
// gdi->width/height stay at the old size, and FreeRDP's own
|
||||||
|
// surface-bits handling (intersect_rect in gdi.c) then silently
|
||||||
|
// *drops* any update reaching outside those stale bounds —
|
||||||
|
// which is what produced the missing/misplaced taskbar and
|
||||||
|
// stale composited-looking content: this call is the fix, not
|
||||||
|
// just a best-effort nudge.
|
||||||
|
if (instance->context->gdi != nullptr
|
||||||
|
&& gdi_resize(instance->context->gdi, static_cast<UINT32>(width),
|
||||||
|
static_cast<UINT32>(height))) {
|
||||||
|
emit remoteDesktopSizeChanged(width, height);
|
||||||
|
} else if (instance->context->gdi != nullptr) {
|
||||||
|
emit eventLogged(QStringLiteral(
|
||||||
|
"RDP warning: local resize to %1x%2 failed; display may show stale content "
|
||||||
|
"until the next full repaint.")
|
||||||
|
.arg(width)
|
||||||
|
.arg(height));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Best-effort nudge: some hosts don't fully repaint their own
|
||||||
|
// desktop after a resolution change (observed: taskbar missing
|
||||||
|
// until something else forces a redraw). Explicitly asking for
|
||||||
|
// the whole new area to be resent costs little and helps
|
||||||
|
// recover from that when it happens.
|
||||||
|
if (instance->context->update != nullptr
|
||||||
|
&& instance->context->update->RefreshRect != nullptr) {
|
||||||
|
RECTANGLE_16 fullArea = {0, 0, static_cast<UINT16>(qMin(width, 65535)),
|
||||||
|
static_cast<UINT16>(qMin(height, 65535))};
|
||||||
|
instance->context->update->RefreshRect(instance->context, 1, &fullArea);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2035,7 +2141,9 @@ void RdpSessionBackend::processInputEvents(rdp_freerdp* instance)
|
|||||||
if (hasResize) {
|
if (hasResize) {
|
||||||
const int width = sanitizeDesktopWidth(resizeWidth);
|
const int width = sanitizeDesktopWidth(resizeWidth);
|
||||||
const int height = sanitizeDesktopHeight(resizeHeight);
|
const int height = sanitizeDesktopHeight(resizeHeight);
|
||||||
if (width != m_lastResizeWidth || height != m_lastResizeHeight) {
|
const int scaleValue = static_cast<int>(nearestFreeRdpScaleValue(m_devicePixelRatio.load()));
|
||||||
|
if (width != m_lastResizeWidth || height != m_lastResizeHeight
|
||||||
|
|| scaleValue != m_lastResizeScale) {
|
||||||
if (sendDisplayResize(instance, width, height)) {
|
if (sendDisplayResize(instance, width, height)) {
|
||||||
if (m_resizeFailureLogged) {
|
if (m_resizeFailureLogged) {
|
||||||
emit eventLogged(QStringLiteral("Dynamic RDP resize recovered."));
|
emit eventLogged(QStringLiteral("Dynamic RDP resize recovered."));
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ public slots:
|
|||||||
void sendInput(const QString& input) override;
|
void sendInput(const QString& input) override;
|
||||||
void confirmHostKey(bool trustHost) override;
|
void confirmHostKey(bool trustHost) override;
|
||||||
void updateTerminalSize(int columns, int rows) override;
|
void updateTerminalSize(int columns, int rows) override;
|
||||||
|
void updateDisplayScale(qreal devicePixelRatio) override;
|
||||||
void sendKeyEvent(int key,
|
void sendKeyEvent(int key,
|
||||||
quint32 nativeScanCode,
|
quint32 nativeScanCode,
|
||||||
const QString& text,
|
const QString& text,
|
||||||
@@ -68,6 +69,7 @@ private:
|
|||||||
|
|
||||||
std::atomic_int m_requestedDesktopWidth;
|
std::atomic_int m_requestedDesktopWidth;
|
||||||
std::atomic_int m_requestedDesktopHeight;
|
std::atomic_int m_requestedDesktopHeight;
|
||||||
|
std::atomic<qreal> m_devicePixelRatio;
|
||||||
|
|
||||||
std::thread m_worker;
|
std::thread m_worker;
|
||||||
std::atomic_bool m_workerRunning;
|
std::atomic_bool m_workerRunning;
|
||||||
@@ -85,6 +87,7 @@ private:
|
|||||||
bool m_resizeFailureLogged;
|
bool m_resizeFailureLogged;
|
||||||
int m_lastResizeWidth;
|
int m_lastResizeWidth;
|
||||||
int m_lastResizeHeight;
|
int m_lastResizeHeight;
|
||||||
|
int m_lastResizeScale;
|
||||||
|
|
||||||
std::mutex m_cliprdrMutex;
|
std::mutex m_cliprdrMutex;
|
||||||
void* m_cliprdrContext;
|
void* m_cliprdrContext;
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ public slots:
|
|||||||
virtual void sendInput(const QString& input) = 0;
|
virtual void sendInput(const QString& input) = 0;
|
||||||
virtual void confirmHostKey(bool trustHost) = 0;
|
virtual void confirmHostKey(bool trustHost) = 0;
|
||||||
virtual void updateTerminalSize(int columns, int rows) = 0;
|
virtual void updateTerminalSize(int columns, int rows) = 0;
|
||||||
|
virtual void updateDisplayScale(qreal devicePixelRatio)
|
||||||
|
{
|
||||||
|
Q_UNUSED(devicePixelRatio);
|
||||||
|
}
|
||||||
virtual void setClipboardText(const QString& text)
|
virtual void setClipboardText(const QString& text)
|
||||||
{
|
{
|
||||||
Q_UNUSED(text);
|
Q_UNUSED(text);
|
||||||
|
|||||||
@@ -176,6 +176,11 @@ SessionTab::SessionTab(const Profile& profile,
|
|||||||
m_backend,
|
m_backend,
|
||||||
&SessionBackend::updateTerminalSize,
|
&SessionBackend::updateTerminalSize,
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
|
connect(this,
|
||||||
|
&SessionTab::requestDisplayScale,
|
||||||
|
m_backend,
|
||||||
|
&SessionBackend::updateDisplayScale,
|
||||||
|
Qt::QueuedConnection);
|
||||||
connect(this,
|
connect(this,
|
||||||
&SessionTab::requestKeyEvent,
|
&SessionTab::requestKeyEvent,
|
||||||
m_backend,
|
m_backend,
|
||||||
@@ -860,6 +865,10 @@ void SessionTab::setupUi()
|
|||||||
&RdpDisplayWidget::viewportSizeChanged,
|
&RdpDisplayWidget::viewportSizeChanged,
|
||||||
this,
|
this,
|
||||||
[this](int width, int height) { emit requestTerminalSize(width, height); });
|
[this](int width, int height) { emit requestTerminalSize(width, height); });
|
||||||
|
connect(m_rdpDisplay,
|
||||||
|
&RdpDisplayWidget::displayScaleChanged,
|
||||||
|
this,
|
||||||
|
[this](qreal ratio) { emit requestDisplayScale(ratio); });
|
||||||
connect(m_rdpDisplay,
|
connect(m_rdpDisplay,
|
||||||
&RdpDisplayWidget::keyInput,
|
&RdpDisplayWidget::keyInput,
|
||||||
this,
|
this,
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ signals:
|
|||||||
void requestInput(const QString& input);
|
void requestInput(const QString& input);
|
||||||
void requestHostKeyConfirmation(bool trustHost);
|
void requestHostKeyConfirmation(bool trustHost);
|
||||||
void requestTerminalSize(int columns, int rows);
|
void requestTerminalSize(int columns, int rows);
|
||||||
|
void requestDisplayScale(qreal devicePixelRatio);
|
||||||
void requestKeyEvent(int key,
|
void requestKeyEvent(int key,
|
||||||
quint32 nativeScanCode,
|
quint32 nativeScanCode,
|
||||||
const QString& text,
|
const QString& text,
|
||||||
|
|||||||
@@ -170,6 +170,9 @@ SessionWindow::SessionWindow(QWidget* parent)
|
|||||||
QAction* newProfileAction = fileMenu->addAction(QStringLiteral("New Profile"));
|
QAction* newProfileAction = fileMenu->addAction(QStringLiteral("New Profile"));
|
||||||
QAction* newFolderAction = fileMenu->addAction(QStringLiteral("New Folder"));
|
QAction* newFolderAction = fileMenu->addAction(QStringLiteral("New Folder"));
|
||||||
fileMenu->addSeparator();
|
fileMenu->addSeparator();
|
||||||
|
QAction* importProfilesAction = fileMenu->addAction(QStringLiteral("Import Profiles..."));
|
||||||
|
QAction* exportProfilesAction = fileMenu->addAction(QStringLiteral("Export Profiles..."));
|
||||||
|
fileMenu->addSeparator();
|
||||||
QAction* quitAction = fileMenu->addAction(QStringLiteral("Quit"));
|
QAction* quitAction = fileMenu->addAction(QStringLiteral("Quit"));
|
||||||
|
|
||||||
connect(newProfileAction,
|
connect(newProfileAction,
|
||||||
@@ -180,6 +183,14 @@ SessionWindow::SessionWindow(QWidget* parent)
|
|||||||
&QAction::triggered,
|
&QAction::triggered,
|
||||||
this,
|
this,
|
||||||
[this]() { m_profilesWidget->createFolderInCurrentContext(); });
|
[this]() { m_profilesWidget->createFolderInCurrentContext(); });
|
||||||
|
connect(importProfilesAction,
|
||||||
|
&QAction::triggered,
|
||||||
|
this,
|
||||||
|
[this]() { m_profilesWidget->importProfiles(); });
|
||||||
|
connect(exportProfilesAction,
|
||||||
|
&QAction::triggered,
|
||||||
|
this,
|
||||||
|
[this]() { m_profilesWidget->exportProfiles(); });
|
||||||
connect(quitAction, &QAction::triggered, this, []() { qApp->quit(); });
|
connect(quitAction, &QAction::triggered, this, []() { qApp->quit(); });
|
||||||
|
|
||||||
QMenu* helpMenu = menuBar()->addMenu(QStringLiteral("Help"));
|
QMenu* helpMenu = menuBar()->addMenu(QStringLiteral("Help"));
|
||||||
|
|||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
3.23.1-dev0
|
||||||
Reference in New Issue
Block a user