From 795497d7eda5da83a8a175d0a439c4a63acce127 Mon Sep 17 00:00:00 2001 From: Roman Vladimirov Date: Sat, 11 Nov 2023 22:29:20 +0300 Subject: [PATCH] > Few fixes for focus handling + Added supporting downloading files --- src/ListModels/outputformatslistmodel.cpp | 2 + src/ViewModels/backendviewmodel.cpp | 7 ++- src/ViewModels/backendviewmodel.h | 1 + src/ViewModels/httpperformerviewmodel.cpp | 25 +++++---- src/ViewModels/httpperformerviewmodel.h | 2 +- src/ViewModels/httprequestresultviewmodel.cpp | 37 ++++++++++++-- src/ViewModels/httprequestresultviewmodel.h | 9 +++- src/Views/AboutWindow.qml | 2 +- src/Views/GlobalVariablesWindow.qml | 4 ++ src/Views/HttpRequestEditor.qml | 15 +++--- src/Views/HttpResultViewer.qml | 51 +++++++++++++------ src/Views/OpenApiExportWindow.qml | 4 ++ src/globalconstants.cpp | 1 + src/globalconstants.h | 1 + src/main.qml | 13 +++++ 15 files changed, 137 insertions(+), 37 deletions(-) diff --git a/src/ListModels/outputformatslistmodel.cpp b/src/ListModels/outputformatslistmodel.cpp index b537f04..4012d21 100644 --- a/src/ListModels/outputformatslistmodel.cpp +++ b/src/ListModels/outputformatslistmodel.cpp @@ -24,12 +24,14 @@ OutputFormatsListModel::OutputFormatsListModel(QObject *parent) m_outputFormats.append(OutputFormatXml); m_outputFormats.append(OutputFormatHtml); m_outputFormats.append(OutputFormatImage); + m_outputFormats.append(OutputNeedDownloaded); m_outputFormatTitles.insert(OutputFormatAuto, "Auto"); m_outputFormatTitles.insert(OutputFormatJson, "JSON"); m_outputFormatTitles.insert(OutputFormatXml, "XML document"); m_outputFormatTitles.insert(OutputFormatImage, "Image"); m_outputFormatTitles.insert(OutputFormatHtml, "HTML"); + m_outputFormatTitles.insert(OutputNeedDownloaded, "Attachment"); } int OutputFormatsListModel::rowCount(const QModelIndex &parent) const diff --git a/src/ViewModels/backendviewmodel.cpp b/src/ViewModels/backendviewmodel.cpp index 2f89496..5a4cda5 100644 --- a/src/ViewModels/backendviewmodel.cpp +++ b/src/ViewModels/backendviewmodel.cpp @@ -40,6 +40,7 @@ void BackendViewModel::addNewRequest() auto request = model->requestModel(); request->setTextAdvisor(m_textAdviser); + request->setSelectedItem(0); // select first empty field for new request m_requests->addItem(model); } @@ -67,7 +68,6 @@ bool BackendViewModel::shortcutHandler(const QString &shortcut) noexcept } else if (command == m_performQueriesMultipleCommand) { m_requestPerformer->performAllRequest(); } else if (command == m_performQueryCommand) { - m_requestPerformer->performAllRequest(); auto request = m_requests->getSelectedRequest(); m_requestPerformer->performOneRequest(request); } else if (command == m_cancelQueryCommand) { @@ -259,6 +259,11 @@ void BackendViewModel::importFromOpenApi(int index) noexcept m_requests->selectItem(createdIndex); } +void BackendViewModel::saveDownloadedFile(const QString &fileName) noexcept +{ + m_requests->selectedItem()->resultModel()->saveBodyToFile(removeProtocol(fileName)); +} + void BackendViewModel::deleteCurrentRequest() noexcept { if (m_requests->singleRequest()) addNewRequest(); diff --git a/src/ViewModels/backendviewmodel.h b/src/ViewModels/backendviewmodel.h index 87b3644..a31ec35 100644 --- a/src/ViewModels/backendviewmodel.h +++ b/src/ViewModels/backendviewmodel.h @@ -119,6 +119,7 @@ class BackendViewModel : public QObject Q_INVOKABLE void generateImage(const QString& filePath) noexcept; Q_INVOKABLE void generateImageToClipboard() noexcept; Q_INVOKABLE void importFromOpenApi(int index) noexcept; + Q_INVOKABLE void saveDownloadedFile(const QString& fileName) noexcept; void deleteCurrentRequest() noexcept; diff --git a/src/ViewModels/httpperformerviewmodel.cpp b/src/ViewModels/httpperformerviewmodel.cpp index 292edbd..a624484 100644 --- a/src/ViewModels/httpperformerviewmodel.cpp +++ b/src/ViewModels/httpperformerviewmodel.cpp @@ -76,11 +76,18 @@ void HttpPerformerViewModel::cancelRequest() void HttpPerformerViewModel::performOneRequest(HttpRequestModel *request) { - if (m_runningRequests->contains(request->requestId().toString())) return; + if (m_runningRequests.contains(request->requestId().toString())) return; if (performSingleRequest(request)) { - addToCounter(1); - emit countRequestsChanged(); + if (m_countFinishedRequests < m_countRequests) { + addToCounter(1); + emit countRequestsChanged(); + } else { + m_countFinishedRequests = 0; + m_countRequests = 0; + emit countFinishedRequestsChanged(); + emit countRequestsChanged(); + } } } @@ -249,7 +256,7 @@ void HttpPerformerViewModel::startTrackRequest(QNetworkReply *reply, const QUuid reply->setProperty("id", id.toString()); - m_runningRequests->insert(id.toString(), resultModel); + m_runningRequests.insert(id.toString(), resultModel); } void HttpPerformerViewModel::adjustOptions(QStringList options, QNetworkRequest &request) @@ -280,7 +287,7 @@ bool HttpPerformerViewModel::performSingleRequest(HttpRequestModel *modelRequest auto id = modelRequest->requestId(); //if request already performing don't need make something - if (m_runningRequests->contains(id.toString())) return false; + if (m_runningRequests.contains(id.toString())) return false; auto resultModel = modelRequest->resultModel(); auto requestModel = modelRequest->requestModel(); @@ -378,7 +385,7 @@ bool HttpPerformerViewModel::performSingleRequest(HttpRequestModel *modelRequest void HttpPerformerViewModel::addToCounter(int number) noexcept { - if (m_countRequests == m_countFinishedRequests) { + if (m_countRequests > 0 && m_countRequests == m_countFinishedRequests) { m_countRequests = 0; m_countFinishedRequests = 0; } @@ -421,13 +428,13 @@ void HttpPerformerViewModel::runPostScript(const QString &script, QObject* prope void HttpPerformerViewModel::requestFinished(QNetworkReply *reply) { auto id = reply->property("id").toString(); - if (!m_runningRequests->contains(id)) return; + if (!m_runningRequests.contains(id)) return; - auto result = m_runningRequests->value(id); + auto result = m_runningRequests.value(id); if (result == nullptr) return; result->untrackRequestTime(); - m_runningRequests->remove(id); + m_runningRequests.remove(id); reduceFromCounter(); result->setNetworkError(""); diff --git a/src/ViewModels/httpperformerviewmodel.h b/src/ViewModels/httpperformerviewmodel.h index cc56831..85c5322 100644 --- a/src/ViewModels/httpperformerviewmodel.h +++ b/src/ViewModels/httpperformerviewmodel.h @@ -40,7 +40,7 @@ class HttpPerformerViewModel : public QObject private: QScopedPointer m_networkManager { new QNetworkAccessManager() }; QScopedPointer> m_rawHeaders { new QMap() }; - QScopedPointer> m_runningRequests { new QMap() }; + QMap m_runningRequests { QMap() }; QSharedPointer> m_requests { nullptr }; int m_countRequests { 0 }; int m_countFinishedRequests { 0 }; diff --git a/src/ViewModels/httprequestresultviewmodel.cpp b/src/ViewModels/httprequestresultviewmodel.cpp index c498a8b..660ba3a 100644 --- a/src/ViewModels/httprequestresultviewmodel.cpp +++ b/src/ViewModels/httprequestresultviewmodel.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include "httprequestresultviewmodel.h" HttpRequestResultViewModel::HttpRequestResultViewModel(QObject *parent) @@ -53,6 +54,7 @@ void HttpRequestResultViewModel::setBody(const QByteArray &body) noexcept m_bodyModel->setBody("", ""); m_isFormatting = false; m_showImage = false; + m_showDownloadFile = false; emit isFormattingChanged(); return; } @@ -66,6 +68,7 @@ void HttpRequestResultViewModel::setBody(const QByteArray &body) noexcept m_isFormatting = !outputFormat.isEmpty(); m_showImage = outputFormat == OutputFormatImage; + m_showDownloadFile = outputFormat == OutputNeedDownloaded; m_bodyModel->setBody(body, outputFormat); @@ -78,6 +81,7 @@ void HttpRequestResultViewModel::setBody(const QByteArray &body) noexcept emit responseSizeChanged(); emit isFormattingChanged(); emit showImageChanged(); + emit showDownloadFileChanged(); emit actualFormatChanged(); } @@ -88,11 +92,13 @@ void HttpRequestResultViewModel::reformatting() noexcept m_isFormatting = !outputFormat.isEmpty(); m_showImage = outputFormat == OutputFormatImage; + m_showDownloadFile = outputFormat == OutputNeedDownloaded; m_bodyModel->reformatBody(outputFormat); emit isFormattingChanged(); emit showImageChanged(); + emit showDownloadFileChanged(); } QString HttpRequestResultViewModel::responseTime() const noexcept @@ -263,6 +269,17 @@ void HttpRequestResultViewModel::copyBodyToClipboard() } } +void HttpRequestResultViewModel::saveBodyToFile(const QString &fileName) +{ + QFile file(fileName); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) return; + + auto content = m_bodyModel->getFullBody(); + file.write(content.toUtf8()); + + file.close(); +} + void HttpRequestResultViewModel::reformatBody() { reformatting(); @@ -284,14 +301,19 @@ QString HttpRequestResultViewModel::getReadableSize(uint64_t size) const noexcep return result; } -QString HttpRequestResultViewModel::getFormatFromContentType() const noexcept +QString HttpRequestResultViewModel::getFormatFromContentType() noexcept { QString contentTypeHeader = ""; + QString contentDisposition = ""; foreach (auto header, m_headers) { if (header.contains("content-type:", Qt::CaseInsensitive)) { contentTypeHeader = header.toLower(); break; } + if (header.contains("content-disposition:", Qt::CaseInsensitive)) { + contentDisposition = header.toLower(); + break; + } } if (contentTypeHeader.isEmpty()) return ""; @@ -310,6 +332,15 @@ QString HttpRequestResultViewModel::getFormatFromContentType() const noexcept return OutputFormatImage; } + if (!contentDisposition.isEmpty()) { + m_defaultDownloadFile = ""; + auto value = contentDisposition.toLower().replace("content-disposition: ", ""); + if (!value.contains("attachment", Qt::CaseInsensitive)) return ""; + auto fileNameIndex = value.indexOf("filename=", Qt::CaseInsensitive); + if (fileNameIndex > -1) m_defaultDownloadFile = value.mid(fileNameIndex + 10); + return OutputNeedDownloaded; + } + return ""; } @@ -346,7 +377,7 @@ QStringList HttpRequestResultViewModel::getHeaderLines() .replace("", "") .replace("\n", "") .replace("\r", ""); - if (clearedHeader.count() > lineCount) { + if (clearedHeader.size() > lineCount) { int position = 0; QString content; bool isFull = false; @@ -356,7 +387,7 @@ QStringList HttpRequestResultViewModel::getHeaderLines() if (content.isEmpty()) break; lines.append(content); position += lineCount; - isFull = content.count() == lineCount; + isFull = content.size() == lineCount; } while (isFull); } else { lines.append(clearedHeader); diff --git a/src/ViewModels/httprequestresultviewmodel.h b/src/ViewModels/httprequestresultviewmodel.h index 6e8e0c0..cc2d258 100644 --- a/src/ViewModels/httprequestresultviewmodel.h +++ b/src/ViewModels/httprequestresultviewmodel.h @@ -41,6 +41,7 @@ class HttpRequestResultViewModel : public QObject Q_PROPERTY(bool isFormatting READ isFormatting NOTIFY isFormattingChanged) Q_PROPERTY(bool showImage READ showImage NOTIFY showImageChanged) Q_PROPERTY(bool hasError READ hasError NOTIFY hasErrorChanged) + Q_PROPERTY(bool showDownloadFile READ showDownloadFile NOTIFY showDownloadFileChanged) private: int m_statusCode { 0 }; @@ -60,6 +61,8 @@ class HttpRequestResultViewModel : public QObject QString m_actualFormat { "" }; bool m_customErrorResult { false }; QString m_postScript { "" }; + bool m_showDownloadFile { false }; + QString m_defaultDownloadFile { "" }; public: explicit HttpRequestResultViewModel(QObject *parent = nullptr); @@ -107,6 +110,8 @@ class HttpRequestResultViewModel : public QObject bool hasError() const noexcept { return m_customErrorResult || !m_networkError.isEmpty(); } + bool showDownloadFile() const noexcept { return m_showDownloadFile; } + void setCustomErrorResult(bool hasErrors, const QString& errorMessage); uint64_t originResponseSize() const noexcept { return m_responseSize; } @@ -117,11 +122,12 @@ class HttpRequestResultViewModel : public QObject Q_INVOKABLE void copyHeadersToClipboard(); Q_INVOKABLE void copyBodyToClipboard(); + Q_INVOKABLE void saveBodyToFile(const QString& fileName); Q_INVOKABLE void reformatBody(); private: QString getReadableSize(uint64_t size) const noexcept; - QString getFormatFromContentType() const noexcept; + QString getFormatFromContentType() noexcept; void setBoldTextToPainter(QPainter& painter) noexcept; void setNormalTextToPainter(QPainter& painter) noexcept; void paintText(QPainter& painter, const QImage& image, int& currentLine, int& lineHeight, const QString& text, bool bold) noexcept; @@ -144,6 +150,7 @@ class HttpRequestResultViewModel : public QObject void actualFormatChanged(); void errorSavingGeneratedFile(); void hasErrorChanged(); + void showDownloadFileChanged(); }; diff --git a/src/Views/AboutWindow.qml b/src/Views/AboutWindow.qml index 2fa9a59..20d21a8 100644 --- a/src/Views/AboutWindow.qml +++ b/src/Views/AboutWindow.qml @@ -75,7 +75,7 @@ ApplicationWindow { anchors.top: opensourceInfo.bottom anchors.leftMargin: 20 anchors.topMargin: 12 - text: "Used Qt version 6.2.3
If you want to get sources of Qt please email this address." + text: "Used Qt
If you want to get sources of Qt please email this address." font.pointSize: 10 onLinkActivated: function (link) { Qt.openUrlExternally(link); diff --git a/src/Views/GlobalVariablesWindow.qml b/src/Views/GlobalVariablesWindow.qml index d17e236..41919b9 100644 --- a/src/Views/GlobalVariablesWindow.qml +++ b/src/Views/GlobalVariablesWindow.qml @@ -92,4 +92,8 @@ ApplicationWindow { if (handled) globalEventHandler.setHandledLastSession(); } } + + onActiveFocusItemChanged: { + if (!root.activeFocusItem) globalEventHandler.clear(); + } } diff --git a/src/Views/HttpRequestEditor.qml b/src/Views/HttpRequestEditor.qml index bbd5b35..8551b59 100644 --- a/src/Views/HttpRequestEditor.qml +++ b/src/Views/HttpRequestEditor.qml @@ -23,6 +23,7 @@ Item { ListView { id: listView + focus: true anchors.fill: parent clip: true boundsBehavior: ListView.StopAtBounds @@ -39,9 +40,8 @@ Item { onIsNeedFocusedChanged: { if (isNeedFocused) { + listView.forceActiveFocus(); textArea.forceActiveFocus(); - } else { - textArea.focus = false; } } @@ -65,14 +65,17 @@ Item { onPressed: { if (listView.model.selectedItem !== currentIndex) listView.model.selectedItem = currentIndex; } + onActiveFocusChanged: { + if (backend.tabs.currentTab !== 'Request') return; //dirty hack but I don't know how to resolve it + + if (isNeedFocused && !textArea.activeFocus) { + textArea.forceActiveFocus(); + } + } } } ScrollBar.vertical: ScrollBar { active: true } } - - Component.onCompleted: { - viewModel.selectedItem = 0; //WORKAROUND: fix loosing focus after start application - } } diff --git a/src/Views/HttpResultViewer.qml b/src/Views/HttpResultViewer.qml index 9066668..adadc46 100644 --- a/src/Views/HttpResultViewer.qml +++ b/src/Views/HttpResultViewer.qml @@ -199,18 +199,9 @@ Item { viewModel.bodyModel.searchText(text); backend.refreshFindedIndex(); } - Keys.onPressed: (event) => { - const isControl = event.modifiers & Qt.ControlModifier; - const isShift = event.modifiers & Qt.ShiftModifier; - const isAlt = event.modifiers & Qt.AltModifier; - if (isControl && event.key === Qt.Key_Z) { // disable shotcut Ctrl-Z because it can make undo - event.accepted = true; - return; - } - if (isAlt) { - event.accepted = true; - return; - } + onPressed: { + root.focus = true; + searchTextField.forceActiveFocus(); } } @@ -252,7 +243,7 @@ Item { Menu { id: outputFormatMenu - y: -20 + y: -60 modal: true focus: true closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent @@ -315,9 +306,9 @@ Item { Loader { id: listComponentLoader - visible: !viewModel.showImage + visible: !viewModel.showImage && !viewModel.showDownloadFile anchors.fill: parent - sourceComponent: !viewModel.showImage ? listComponent : null + sourceComponent: !viewModel.showImage && !viewModel.showDownloadFile ? listComponent : null } Component { @@ -395,6 +386,36 @@ Item { } } } + + Loader { + visible: viewModel.showDownloadFile + anchors.fill: parent + sourceComponent: viewModel.showDownloadFile ? downloadComponent : null + } + + Component { + id: downloadComponent + + Item { + anchors.fill: parent + anchors.topMargin: -20 + + Text { + anchors.centerIn: parent + textFormat: Text.RichText + font.pointSize: 11 + text: "Save file as" + onLinkActivated: { + saveDownloadFileDialog.open(); + } + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.NoButton + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + } + } + } + } } } } diff --git a/src/Views/OpenApiExportWindow.qml b/src/Views/OpenApiExportWindow.qml index 786c1f7..107f2ab 100644 --- a/src/Views/OpenApiExportWindow.qml +++ b/src/Views/OpenApiExportWindow.qml @@ -560,4 +560,8 @@ ApplicationWindow { if (handled) globalEventHandler.setHandledLastSession(); } } + + onActiveFocusItemChanged: { + if (!root.activeFocusItem) globalEventHandler.clear(); + } } diff --git a/src/globalconstants.cpp b/src/globalconstants.cpp index c188a59..2d8d890 100644 --- a/src/globalconstants.cpp +++ b/src/globalconstants.cpp @@ -41,5 +41,6 @@ QString OutputFormatJson = "json"; QString OutputFormatHtml = "html"; QString OutputFormatXml = "xml"; QString OutputFormatImage = "image"; +QString OutputNeedDownloaded = "downloadable"; bool IsPortable = false; diff --git a/src/globalconstants.h b/src/globalconstants.h index 6326091..0fe16cb 100644 --- a/src/globalconstants.h +++ b/src/globalconstants.h @@ -41,5 +41,6 @@ extern QString OutputFormatJson; extern QString OutputFormatHtml; extern QString OutputFormatXml; extern QString OutputFormatImage; +extern QString OutputNeedDownloaded; extern bool IsPortable; diff --git a/src/main.qml b/src/main.qml index 8e89843..727de9a 100644 --- a/src/main.qml +++ b/src/main.qml @@ -26,6 +26,7 @@ ApplicationWindow { HttpRequestEditor { id: requestEditor + focus: true visible: backend.tabs.currentTab === 'Request' viewModel: backend.requests.selectedItem.requestModel } @@ -213,6 +214,15 @@ ApplicationWindow { } } + FileDialog { + id: saveDownloadFileDialog + fileMode: FileDialog.SaveFile + nameFilters: ["Any files (*.*)"] + onAccepted: { + backend.saveDownloadedFile(saveDownloadFileDialog.selectedFile); + } + } + GlobalEventHandlerModel { id: globalEventHandler onKeysChanged: function (shortcut) { @@ -223,4 +233,7 @@ ApplicationWindow { } } + onActiveFocusItemChanged: { + if (!window.activeFocusItem) globalEventHandler.clear(); + } }