Error handling for Yggdrasil accounts
This commit is contained in:
parent
c32aaf244d
commit
d40b6d8eb3
@ -45,8 +45,8 @@ set(CORE_SOURCES
|
||||
ResourceDownloadTask.h
|
||||
ResourceDownloadTask.cpp
|
||||
|
||||
CreateAuthlibInjectorAccount.cpp
|
||||
CreateAuthlibInjectorAccount.h
|
||||
GetAuthlibInjectorApiLocation.cpp
|
||||
GetAuthlibInjectorApiLocation.h
|
||||
|
||||
# Use tracking separate from memory management
|
||||
Usable.h
|
||||
|
@ -16,7 +16,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "CreateAuthlibInjectorAccount.h"
|
||||
#include "GetAuthlibInjectorApiLocation.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QJsonDocument>
|
||||
@ -28,40 +28,40 @@
|
||||
|
||||
#include "Application.h"
|
||||
|
||||
CreateAuthlibInjectorAccount::CreateAuthlibInjectorAccount(QUrl url, MinecraftAccountPtr account, QString username)
|
||||
GetAuthlibInjectorApiLocation::GetAuthlibInjectorApiLocation(QUrl url, MinecraftAccountPtr account, QString username)
|
||||
: NetRequest(), m_account(account), m_username(username)
|
||||
{
|
||||
m_url = url;
|
||||
m_sink.reset(new Sink(*this));
|
||||
}
|
||||
|
||||
QNetworkReply* CreateAuthlibInjectorAccount::getReply(QNetworkRequest& request)
|
||||
QNetworkReply* GetAuthlibInjectorApiLocation::getReply(QNetworkRequest& request)
|
||||
{
|
||||
setStatus(tr("Getting authlib-injector server details"));
|
||||
return m_network->get(request);
|
||||
}
|
||||
|
||||
CreateAuthlibInjectorAccount::Ptr CreateAuthlibInjectorAccount::make(QUrl url, MinecraftAccountPtr account, QString username)
|
||||
GetAuthlibInjectorApiLocation::Ptr GetAuthlibInjectorApiLocation::make(QUrl url, MinecraftAccountPtr account, QString username)
|
||||
{
|
||||
return CreateAuthlibInjectorAccount::Ptr(new CreateAuthlibInjectorAccount(url, account, username));
|
||||
return GetAuthlibInjectorApiLocation::Ptr(new GetAuthlibInjectorApiLocation(url, account, username));
|
||||
}
|
||||
|
||||
auto CreateAuthlibInjectorAccount::Sink::init(QNetworkRequest& request) -> Task::State
|
||||
auto GetAuthlibInjectorApiLocation::Sink::init(QNetworkRequest& request) -> Task::State
|
||||
{
|
||||
return Task::State::Running;
|
||||
}
|
||||
|
||||
auto CreateAuthlibInjectorAccount::Sink::write(QByteArray& data) -> Task::State
|
||||
auto GetAuthlibInjectorApiLocation::Sink::write(QByteArray& data) -> Task::State
|
||||
{
|
||||
return Task::State::Running;
|
||||
}
|
||||
|
||||
auto CreateAuthlibInjectorAccount::Sink::abort() -> Task::State
|
||||
auto GetAuthlibInjectorApiLocation::Sink::abort() -> Task::State
|
||||
{
|
||||
return Task::State::Failed;
|
||||
}
|
||||
|
||||
auto CreateAuthlibInjectorAccount::Sink::finalize(QNetworkReply& reply) -> Task::State
|
||||
auto GetAuthlibInjectorApiLocation::Sink::finalize(QNetworkReply& reply) -> Task::State
|
||||
{
|
||||
QVariant header = reply.rawHeader("X-Authlib-Injector-API-Location");
|
||||
QUrl url = m_outer.m_url;
|
||||
@ -76,7 +76,7 @@ auto CreateAuthlibInjectorAccount::Sink::finalize(QNetworkReply& reply) -> Task:
|
||||
return Task::State::Succeeded;
|
||||
}
|
||||
|
||||
MinecraftAccountPtr CreateAuthlibInjectorAccount::getAccount()
|
||||
MinecraftAccountPtr GetAuthlibInjectorApiLocation::getAccount()
|
||||
{
|
||||
return m_account;
|
||||
}
|
@ -21,20 +21,20 @@
|
||||
#include "minecraft/auth/MinecraftAccount.h"
|
||||
#include "net/NetRequest.h"
|
||||
|
||||
class CreateAuthlibInjectorAccount : public Net::NetRequest {
|
||||
class GetAuthlibInjectorApiLocation : public Net::NetRequest {
|
||||
Q_OBJECT
|
||||
public:
|
||||
using Ptr = shared_qobject_ptr<CreateAuthlibInjectorAccount>;
|
||||
CreateAuthlibInjectorAccount(QUrl url, MinecraftAccountPtr account, QString username);
|
||||
virtual ~CreateAuthlibInjectorAccount() = default;
|
||||
using Ptr = shared_qobject_ptr<GetAuthlibInjectorApiLocation>;
|
||||
GetAuthlibInjectorApiLocation(QUrl url, MinecraftAccountPtr account, QString username);
|
||||
virtual ~GetAuthlibInjectorApiLocation() = default;
|
||||
|
||||
static CreateAuthlibInjectorAccount::Ptr make(QUrl url, MinecraftAccountPtr account, QString username);
|
||||
static GetAuthlibInjectorApiLocation::Ptr make(QUrl url, MinecraftAccountPtr account, QString username);
|
||||
|
||||
MinecraftAccountPtr getAccount();
|
||||
|
||||
class Sink : public Net::Sink {
|
||||
public:
|
||||
Sink(CreateAuthlibInjectorAccount& outer) : m_outer(outer) {}
|
||||
Sink(GetAuthlibInjectorApiLocation& outer) : m_outer(outer) {}
|
||||
virtual ~Sink() = default;
|
||||
|
||||
public:
|
||||
@ -45,7 +45,7 @@ class CreateAuthlibInjectorAccount : public Net::NetRequest {
|
||||
auto hasLocalData() -> bool override { return false; }
|
||||
|
||||
private:
|
||||
CreateAuthlibInjectorAccount& m_outer;
|
||||
GetAuthlibInjectorApiLocation& m_outer;
|
||||
};
|
||||
|
||||
protected slots:
|
@ -131,7 +131,7 @@ bool AuthFlow::changeState(AccountTaskState newState, QString reason)
|
||||
return false;
|
||||
}
|
||||
case AccountTaskState::STATE_FAILED_HARD: {
|
||||
setStatus(tr("Failed to authenticate. The session has expired."));
|
||||
setStatus(tr("Failed to authenticate."));
|
||||
m_data->errorString = reason;
|
||||
m_data->accountState = AccountState::Expired;
|
||||
emitFailed(reason);
|
||||
|
@ -7,7 +7,7 @@ YggdrasilStep::YggdrasilStep(AccountData* data, std::optional<QString> password)
|
||||
|
||||
QString YggdrasilStep::describe()
|
||||
{
|
||||
return tr("Logging in with Mojang account.");
|
||||
return tr("Logging in with Yggdrasil.");
|
||||
}
|
||||
|
||||
void YggdrasilStep::perform()
|
||||
@ -64,6 +64,7 @@ void YggdrasilStep::login(QString password)
|
||||
|
||||
m_task.reset(new NetJob("YggdrasilStep", APPLICATION->network()));
|
||||
m_task->setAskRetry(false);
|
||||
m_task->setAutoRetryLimit(0);
|
||||
m_task->addNetAction(m_request);
|
||||
|
||||
connect(m_task.get(), &Task::finished, this, &YggdrasilStep::onRequestDone);
|
||||
@ -102,6 +103,7 @@ void YggdrasilStep::refresh()
|
||||
|
||||
m_task.reset(new NetJob("YggdrasilStep", APPLICATION->network()));
|
||||
m_task->setAskRetry(false);
|
||||
m_task->setAutoRetryLimit(0);
|
||||
m_task->addNetAction(m_request);
|
||||
|
||||
connect(m_task.get(), &Task::finished, this, &YggdrasilStep::onRequestDone);
|
||||
@ -111,10 +113,90 @@ void YggdrasilStep::refresh()
|
||||
|
||||
void YggdrasilStep::onRequestDone()
|
||||
{
|
||||
// TODO handle errors
|
||||
qDebug() << "Yggdrasil request done";
|
||||
switch (m_request->error()) {
|
||||
case QNetworkReply::NoError:
|
||||
break;
|
||||
case QNetworkReply::AuthenticationRequiredError:
|
||||
// These cases will be handled as usual
|
||||
break;
|
||||
case QNetworkReply::TimeoutError:
|
||||
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Authentication operation timed out."));
|
||||
return;
|
||||
case QNetworkReply::OperationCanceledError:
|
||||
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Authentication operation cancelled."));
|
||||
return;
|
||||
case QNetworkReply::SslHandshakeFailedError:
|
||||
emit finished(AccountTaskState::STATE_FAILED_SOFT,
|
||||
tr("<b>SSL Handshake failed.</b><br/>There might be a few causes for it:<br/>"
|
||||
"<ul>"
|
||||
"<li>You use Windows and need to update your root certificates, please install any outstanding updates.</li>"
|
||||
"<li>Some device on your network is interfering with SSL traffic. In that case, "
|
||||
"you have bigger worries than Minecraft not starting.</li>"
|
||||
"<li>Possibly something else. Check the log file for details</li>"
|
||||
"</ul>"));
|
||||
return;
|
||||
// used for invalid credentials and similar errors. Fall through.
|
||||
case QNetworkReply::ContentAccessDenied:
|
||||
case QNetworkReply::ContentOperationNotPermittedError:
|
||||
break;
|
||||
case QNetworkReply::ContentGoneError: {
|
||||
emit finished(AccountTaskState::STATE_FAILED_GONE,
|
||||
tr("The Mojang account no longer exists. It may have been migrated to a Microsoft account."));
|
||||
return;
|
||||
}
|
||||
default:
|
||||
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Authentication operation failed due to a network error: %1 (%2)")
|
||||
.arg(m_request->errorString())
|
||||
.arg(m_request->error()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to parse the response regardless of the response code.
|
||||
// Sometimes the auth server will give more information and an error code.
|
||||
// Check the response code.
|
||||
QJsonParseError jsonError;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(*m_response, &jsonError);
|
||||
// Check the response code.
|
||||
int responseCode = m_request->replyStatusCode();
|
||||
|
||||
if (responseCode == 200) {
|
||||
// If the response code was 200, then there shouldn't be an error. Make sure
|
||||
// anyways.
|
||||
// Also, sometimes an empty reply indicates success. If there was no data received,
|
||||
// pass an empty json object to the processResponse function.
|
||||
if (jsonError.error == QJsonParseError::NoError || m_response->size() == 0) {
|
||||
processResponse(m_response->size() > 0 ? doc.object() : QJsonObject());
|
||||
return;
|
||||
} else {
|
||||
emit finished(AccountTaskState::STATE_FAILED_SOFT,
|
||||
tr("Failed to parse authentication server response JSON response: %1 at offset %2.")
|
||||
.arg(jsonError.errorString())
|
||||
.arg(jsonError.offset));
|
||||
qCritical() << *m_response;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If the response code was not 200, then Yggdrasil may have given us information
|
||||
// about the error.
|
||||
// If we can parse the response, then get information from it. Otherwise just say
|
||||
// there was an unknown error.
|
||||
if (jsonError.error == QJsonParseError::NoError) {
|
||||
// We were able to parse the server's response. Woo!
|
||||
// Call processError. If a subclass has overridden it then they'll handle their
|
||||
// stuff there.
|
||||
qDebug() << "The request failed, but the server gave us an error message. Processing error.";
|
||||
processError(doc.object());
|
||||
} else {
|
||||
// The server didn't say anything regarding the error. Give the user an unknown
|
||||
// error.
|
||||
qDebug() << "The request failed and the server gave no error message. Unknown error.";
|
||||
emit finished(
|
||||
AccountTaskState::STATE_FAILED_SOFT,
|
||||
tr("An unknown error occurred when trying to communicate with the authentication server: %1").arg(m_request->errorString()));
|
||||
}
|
||||
|
||||
YggdrasilStep::processResponse(doc.object());
|
||||
}
|
||||
|
||||
@ -122,9 +204,7 @@ void YggdrasilStep::processResponse(QJsonObject responseData)
|
||||
{
|
||||
// Read the response data. We need to get the client token, access token, and the selected
|
||||
// profile.
|
||||
qDebug() << "Processing authentication response.";
|
||||
|
||||
// qDebug() << responseData;
|
||||
// If we already have a client token, make sure the one the server gave us matches our
|
||||
// existing one.
|
||||
QString clientToken = responseData.value("clientToken").toString("");
|
||||
@ -192,15 +272,15 @@ void YggdrasilStep::processResponse(QJsonObject responseData)
|
||||
|
||||
void YggdrasilStep::processError(QJsonObject responseData)
|
||||
{
|
||||
/*QJsonValue errorVal = responseData.value("error");*/
|
||||
/*QJsonValue errorMessageValue = responseData.value("errorMessage");*/
|
||||
/*QJsonValue causeVal = responseData.value("cause");*/
|
||||
/**/
|
||||
/*if (errorVal.isString() && errorMessageValue.isString()) {*/
|
||||
/* m_error = std::shared_ptr<Error>(new Error{ errorVal.toString(""), errorMessageValue.toString(""), causeVal.toString("") });*/
|
||||
/* changeState(AccountTaskState::STATE_FAILED_HARD, m_error->m_errorMessageVerbose);*/
|
||||
/*} else {*/
|
||||
/* // Error is not in standard format. Don't set m_error and return unknown error.*/
|
||||
/* changeState(AccountTaskState::STATE_FAILED_HARD, tr("An unknown Yggdrasil error occurred."));*/
|
||||
/*}*/
|
||||
QJsonValue errorVal = responseData.value("error");
|
||||
QJsonValue errorMessageValue = responseData.value("errorMessage");
|
||||
QJsonValue causeVal = responseData.value("cause");
|
||||
|
||||
if (errorVal.isString() && errorMessageValue.isString()) {
|
||||
/*m_error = std::shared_ptr<Error>(new Error{ errorVal.toString(""), errorMessageValue.toString(""), causeVal.toString("") });*/
|
||||
emit finished(AccountTaskState::STATE_FAILED_HARD, errorMessageValue.toString(""));
|
||||
} else {
|
||||
// Error is not in standard format. Don't set m_error and return unknown error.
|
||||
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("An unknown Yggdrasil error occurred."));
|
||||
}
|
||||
}
|
||||
|
@ -66,8 +66,8 @@ auto NetJob::addNetAction(Net::NetRequest::Ptr action) -> bool
|
||||
|
||||
void NetJob::executeNextSubTask()
|
||||
{
|
||||
// We're finished, check for failures and retry if we can (up to 3 times)
|
||||
if (isRunning() && m_queue.isEmpty() && m_doing.isEmpty() && !m_failed.isEmpty() && m_try < 3) {
|
||||
// We're finished, check for failures and retry if we can (up to m_auto_retry_limit times)
|
||||
if (isRunning() && m_queue.isEmpty() && m_doing.isEmpty() && !m_failed.isEmpty() && m_try < m_auto_retry_limit) {
|
||||
m_try += 1;
|
||||
while (!m_failed.isEmpty()) {
|
||||
auto task = m_failed.take(*m_failed.keyBegin());
|
||||
@ -188,3 +188,8 @@ void NetJob::setAskRetry(bool askRetry)
|
||||
{
|
||||
m_ask_retry = askRetry;
|
||||
}
|
||||
|
||||
void NetJob::setAutoRetryLimit(int autoRetryLimit)
|
||||
{
|
||||
m_auto_retry_limit = autoRetryLimit;
|
||||
}
|
||||
|
@ -63,6 +63,7 @@ class NetJob : public ConcurrentTask {
|
||||
auto getFailedActions() -> QList<Net::NetRequest*>;
|
||||
auto getFailedFiles() -> QList<QString>;
|
||||
void setAskRetry(bool askRetry);
|
||||
void setAutoRetryLimit(int autoRetryLimit);
|
||||
|
||||
public slots:
|
||||
// Qt can't handle auto at the start for some reason?
|
||||
@ -81,5 +82,6 @@ class NetJob : public ConcurrentTask {
|
||||
|
||||
int m_try = 1;
|
||||
bool m_ask_retry = true;
|
||||
int m_auto_retry_limit = 3;
|
||||
int m_manual_try = 0;
|
||||
};
|
||||
|
@ -20,14 +20,17 @@
|
||||
#include "ui/dialogs/CustomMessageBox.h"
|
||||
#include "ui_AuthlibInjectorLoginDialog.h"
|
||||
|
||||
#include "CreateAuthlibInjectorAccount.h"
|
||||
#include "Application.h"
|
||||
#include "GetAuthlibInjectorApiLocation.h"
|
||||
|
||||
#include <QtWidgets/QPushButton>
|
||||
|
||||
AuthlibInjectorLoginDialog::AuthlibInjectorLoginDialog(QWidget* parent) : QDialog(parent), ui(new Ui::AuthlibInjectorLoginDialog)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
ui->userTextBox->setFocus();
|
||||
ui->loadingLabel->setVisible(false);
|
||||
ui->errorMessage->setVisible(false);
|
||||
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
|
||||
setAcceptDrops(true);
|
||||
|
||||
@ -88,6 +91,7 @@ void AuthlibInjectorLoginDialog::dropEvent(QDropEvent* event)
|
||||
// Stage 1: User interaction
|
||||
void AuthlibInjectorLoginDialog::accept()
|
||||
{
|
||||
ui->errorMessage->setVisible(false);
|
||||
auto fixedAuthlibInjectorUrl = AuthlibInjectorLoginDialog::fixUrl(ui->authlibInjectorTextBox->text());
|
||||
|
||||
auto response = CustomMessageBox::selectable(this, QObject::tr("Confirm account creation"),
|
||||
@ -108,15 +112,16 @@ void AuthlibInjectorLoginDialog::accept()
|
||||
// Get the authlib-injector API root
|
||||
auto netJob = NetJob::Ptr(new NetJob("Get authlib-injector API root", APPLICATION->network()));
|
||||
netJob->setAskRetry(false);
|
||||
netJob->setAutoRetryLimit(0);
|
||||
|
||||
auto username = ui->userTextBox->text();
|
||||
m_createAuthlibInjectorAccountTask = CreateAuthlibInjectorAccount::make(fixedAuthlibInjectorUrl, m_account, username);
|
||||
netJob->addNetAction(m_createAuthlibInjectorAccountTask);
|
||||
m_apiLocationRequest = GetAuthlibInjectorApiLocation::make(fixedAuthlibInjectorUrl, m_account, username);
|
||||
netJob->addNetAction(m_apiLocationRequest);
|
||||
|
||||
m_createAuthlibInjectorAccountNetJob.reset(netJob);
|
||||
connect(netJob.get(), &NetJob::succeeded, this, &AuthlibInjectorLoginDialog::onUrlTaskSucceeded);
|
||||
connect(netJob.get(), &NetJob::failed, this, &AuthlibInjectorLoginDialog::onTaskFailed);
|
||||
m_createAuthlibInjectorAccountNetJob->start();
|
||||
m_apiLocationTask.reset(netJob);
|
||||
connect(netJob.get(), &NetJob::succeeded, this, &AuthlibInjectorLoginDialog::onApiLocationTaskSucceeded);
|
||||
connect(netJob.get(), &NetJob::failed, this, &AuthlibInjectorLoginDialog::onApiLocationTaskFailed);
|
||||
m_apiLocationTask->start();
|
||||
}
|
||||
|
||||
void AuthlibInjectorLoginDialog::setUserInputsEnabled(bool enable)
|
||||
@ -144,6 +149,11 @@ void AuthlibInjectorLoginDialog::on_authlibInjectorTextBox_textEdited(const QStr
|
||||
->setEnabled(!newText.isEmpty() && !ui->passTextBox->text().isEmpty() && !ui->authlibInjectorTextBox->text().isEmpty());
|
||||
}
|
||||
|
||||
void AuthlibInjectorLoginDialog::onApiLocationTaskFailed(const QString& reason)
|
||||
{
|
||||
onTaskFailed(m_apiLocationRequest->errorString());
|
||||
}
|
||||
|
||||
void AuthlibInjectorLoginDialog::onTaskFailed(const QString& reason)
|
||||
{
|
||||
// Set message
|
||||
@ -156,16 +166,17 @@ void AuthlibInjectorLoginDialog::onTaskFailed(const QString& reason)
|
||||
processed += "<br />";
|
||||
}
|
||||
}
|
||||
ui->label->setText(processed);
|
||||
ui->errorMessage->setText(processed);
|
||||
ui->errorMessage->setVisible(true);
|
||||
|
||||
// Re-enable user-interaction
|
||||
setUserInputsEnabled(true);
|
||||
ui->loadingLabel->setVisible(false);
|
||||
}
|
||||
|
||||
void AuthlibInjectorLoginDialog::onUrlTaskSucceeded()
|
||||
void AuthlibInjectorLoginDialog::onApiLocationTaskSucceeded()
|
||||
{
|
||||
m_account = m_createAuthlibInjectorAccountTask->getAccount();
|
||||
m_account = m_apiLocationRequest->getAccount();
|
||||
m_loginTask = m_account->login(false, ui->passTextBox->text());
|
||||
connect(m_loginTask.get(), &Task::failed, this, &AuthlibInjectorLoginDialog::onTaskFailed);
|
||||
connect(m_loginTask.get(), &Task::succeeded, this, &AuthlibInjectorLoginDialog::onTaskSucceeded);
|
||||
@ -180,7 +191,7 @@ void AuthlibInjectorLoginDialog::onTaskSucceeded()
|
||||
|
||||
void AuthlibInjectorLoginDialog::onTaskStatus(const QString& status)
|
||||
{
|
||||
ui->label->setText(status);
|
||||
ui->errorMessage->setText(status);
|
||||
}
|
||||
|
||||
// Public interface
|
||||
|
@ -22,8 +22,7 @@
|
||||
#include <QtWidgets/QDialog>
|
||||
|
||||
#include <net/NetJob.h>
|
||||
#include "Application.h"
|
||||
#include "CreateAuthlibInjectorAccount.h"
|
||||
#include "GetAuthlibInjectorApiLocation.h"
|
||||
#include "minecraft/auth/MinecraftAccount.h"
|
||||
#include "tasks/Task.h"
|
||||
|
||||
@ -47,8 +46,9 @@ class AuthlibInjectorLoginDialog : public QDialog {
|
||||
protected slots:
|
||||
void accept();
|
||||
|
||||
void onApiLocationTaskSucceeded();
|
||||
void onApiLocationTaskFailed(const QString&);
|
||||
void onTaskFailed(const QString& reason);
|
||||
void onUrlTaskSucceeded();
|
||||
void onTaskSucceeded();
|
||||
void onTaskStatus(const QString& status);
|
||||
|
||||
@ -65,6 +65,6 @@ class AuthlibInjectorLoginDialog : public QDialog {
|
||||
Ui::AuthlibInjectorLoginDialog* ui;
|
||||
MinecraftAccountPtr m_account;
|
||||
Task::Ptr m_loginTask;
|
||||
Task::Ptr m_createAuthlibInjectorAccountNetJob;
|
||||
CreateAuthlibInjectorAccount::Ptr m_createAuthlibInjectorAccountTask;
|
||||
Task::Ptr m_apiLocationTask;
|
||||
GetAuthlibInjectorApiLocation::Ptr m_apiLocationRequest;
|
||||
};
|
||||
|
@ -80,6 +80,22 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="errorMessage">
|
||||
<property name="text">
|
||||
<string notr="true">Error message label placeholder.</string>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::RichText</enum>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
|
||||
</property>
|
||||
<property name="openExternalLinks">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
|
Loading…
Reference in New Issue
Block a user