mirror of https://github.com/PCSX2/pcsx2.git
Debugger: Implement subsequent/next scan for Mem Search (#10260)
Co-authored-by: Ty <AmFobes@gmail.com>
This commit is contained in:
parent
9a6e5458c9
commit
90e9b60287
|
@ -32,6 +32,7 @@
|
||||||
#include <QtWidgets/QMessageBox>
|
#include <QtWidgets/QMessageBox>
|
||||||
#include <QtConcurrent/QtConcurrent>
|
#include <QtConcurrent/QtConcurrent>
|
||||||
#include <QtCore/QFutureWatcher>
|
#include <QtCore/QFutureWatcher>
|
||||||
|
#include <QtWidgets/QScrollBar>
|
||||||
|
|
||||||
#include "demangler/demangler.h"
|
#include "demangler/demangler.h"
|
||||||
|
|
||||||
|
@ -101,7 +102,9 @@ CpuWidget::CpuWidget(QWidget* parent, DebugInterface& cpu)
|
||||||
connect(m_ui.txtFuncSearch, &QLineEdit::textChanged, [this] { updateFunctionList(); });
|
connect(m_ui.txtFuncSearch, &QLineEdit::textChanged, [this] { updateFunctionList(); });
|
||||||
|
|
||||||
connect(m_ui.btnSearch, &QPushButton::clicked, this, &CpuWidget::onSearchButtonClicked);
|
connect(m_ui.btnSearch, &QPushButton::clicked, this, &CpuWidget::onSearchButtonClicked);
|
||||||
connect(m_ui.listSearchResults, &QListWidget::itemDoubleClicked, [this](QListWidgetItem* item) { m_ui.memoryviewWidget->gotoAddress(item->data(256).toUInt()); });
|
connect(m_ui.btnFilterSearch, &QPushButton::clicked, this, &CpuWidget::onSearchButtonClicked);
|
||||||
|
connect(m_ui.listSearchResults, &QListWidget::itemDoubleClicked, [this](QListWidgetItem* item) { m_ui.memoryviewWidget->gotoAddress(item->text().toUInt(nullptr, 16)); });
|
||||||
|
connect(m_ui.listSearchResults->verticalScrollBar(), &QScrollBar::valueChanged, this, &CpuWidget::onSearchResultsListScroll);
|
||||||
connect(m_ui.cmbSearchType, &QComboBox::currentIndexChanged, [this](int i) {
|
connect(m_ui.cmbSearchType, &QComboBox::currentIndexChanged, [this](int i) {
|
||||||
if (i < 4)
|
if (i < 4)
|
||||||
m_ui.chkSearchHex->setEnabled(true);
|
m_ui.chkSearchHex->setEnabled(true);
|
||||||
|
@ -122,6 +125,11 @@ CpuWidget::CpuWidget(QWidget* parent, DebugInterface& cpu)
|
||||||
m_ui.listFunctions->setVisible(false);
|
m_ui.listFunctions->setVisible(false);
|
||||||
}
|
}
|
||||||
this->repaint();
|
this->repaint();
|
||||||
|
|
||||||
|
// Ensures we don't retrigger the load results function unintentionally
|
||||||
|
m_resultsLoadTimer.setInterval(100);
|
||||||
|
m_resultsLoadTimer.setSingleShot(true);
|
||||||
|
connect(&m_resultsLoadTimer, &QTimer::timeout, this, &CpuWidget::loadSearchResults);
|
||||||
}
|
}
|
||||||
|
|
||||||
CpuWidget::~CpuWidget() = default;
|
CpuWidget::~CpuWidget() = default;
|
||||||
|
@ -816,114 +824,145 @@ void CpuWidget::onStackListDoubleClick(const QModelIndex& index)
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
static std::vector<u32> searchWorker(DebugInterface* cpu, u32 start, u32 end, T value)
|
static bool checkAddressValueMatches(DebugInterface* cpu, u32 addr, T value)
|
||||||
{
|
{
|
||||||
std::vector<u32> hitAddresses;
|
T val = 0;
|
||||||
for (u32 addr = start; addr < end; addr += sizeof(T))
|
switch (sizeof(T))
|
||||||
{
|
{
|
||||||
T val = 0;
|
case sizeof(u8):
|
||||||
switch (sizeof(T))
|
val = cpu->read8(addr);
|
||||||
|
break;
|
||||||
|
case sizeof(u16):
|
||||||
|
val = cpu->read16(addr);
|
||||||
|
break;
|
||||||
|
case sizeof(u32):
|
||||||
{
|
{
|
||||||
case sizeof(u8):
|
if (std::is_same_v<T, float>)
|
||||||
val = cpu->read8(addr);
|
|
||||||
break;
|
|
||||||
case sizeof(u16):
|
|
||||||
val = cpu->read16(addr);
|
|
||||||
break;
|
|
||||||
case sizeof(u32):
|
|
||||||
{
|
{
|
||||||
if (std::is_same_v<T, float>)
|
const float fTop = value + 0.00001f;
|
||||||
{
|
const float fBottom = value - 0.00001f;
|
||||||
const float fTop = value + 0.00001f;
|
const float memValue = std::bit_cast<float, u32>(cpu->read32(addr));
|
||||||
const float fBottom = value - 0.00001f;
|
return (fBottom < memValue && memValue < fTop);
|
||||||
const float memValue = std::bit_cast<float, u32>(cpu->read32(addr));
|
|
||||||
if (fBottom < memValue && memValue < fTop)
|
|
||||||
{
|
|
||||||
hitAddresses.emplace_back(addr);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
val = cpu->read32(addr);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case sizeof(u64):
|
|
||||||
{
|
|
||||||
if (std::is_same_v<T, double>)
|
|
||||||
{
|
|
||||||
const double dTop = value + 0.00001f;
|
|
||||||
const double dBottom = value - 0.00001f;
|
|
||||||
const double memValue = std::bit_cast<double, u64>(cpu->read64(addr));
|
|
||||||
if (dBottom < memValue && memValue < dTop)
|
|
||||||
{
|
|
||||||
hitAddresses.emplace_back(addr);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
val = cpu->read64(addr);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
val = cpu->read32(addr);
|
||||||
Console.Error("Debugger: Unknown type when doing memory search!");
|
break;
|
||||||
return hitAddresses;
|
}
|
||||||
break;
|
case sizeof(u64):
|
||||||
|
{
|
||||||
|
if (std::is_same_v<T, double>)
|
||||||
|
{
|
||||||
|
const double dTop = value + 0.00001f;
|
||||||
|
const double dBottom = value - 0.00001f;
|
||||||
|
const double memValue = std::bit_cast<double, u64>(cpu->read64(addr));
|
||||||
|
return (dBottom < memValue && memValue < dTop);
|
||||||
|
}
|
||||||
|
|
||||||
|
val = cpu->read64(addr);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (val == value)
|
default:
|
||||||
|
Console.Error("Debugger: Unknown type when doing memory search!");
|
||||||
|
return false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return val == value;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
static std::vector<u32> searchWorker(DebugInterface* cpu, std::vector<u32> searchAddresses, u32 start, u32 end, T value)
|
||||||
|
{
|
||||||
|
std::vector<u32> hitAddresses;
|
||||||
|
const bool isSearchingRange = searchAddresses.size() <= 0;
|
||||||
|
if (isSearchingRange)
|
||||||
|
{
|
||||||
|
for (u32 addr = start; addr < end; addr += sizeof(T))
|
||||||
{
|
{
|
||||||
hitAddresses.push_back(addr);
|
if (checkAddressValueMatches(cpu, addr, value))
|
||||||
|
{
|
||||||
|
hitAddresses.push_back(addr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (const u32 addr : searchAddresses)
|
||||||
|
{
|
||||||
|
if (checkAddressValueMatches(cpu, addr, value))
|
||||||
|
{
|
||||||
|
hitAddresses.push_back(addr);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return hitAddresses;
|
return hitAddresses;
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::vector<u32> searchWorkerByteArray(DebugInterface* cpu, u32 start, u32 end, QByteArray value)
|
static bool compareByteArrayAtAddress(DebugInterface* cpu, u32 addr, QByteArray value)
|
||||||
{
|
{
|
||||||
std::vector<u32> hitAddresses;
|
for (qsizetype i = 0; i < value.length(); i++)
|
||||||
for (u32 addr = start; addr < end; addr += 1)
|
|
||||||
{
|
{
|
||||||
bool hit = true;
|
if (static_cast<char>(cpu->read8(addr + i)) != value[i])
|
||||||
for (qsizetype i = 0; i < value.length(); i++)
|
|
||||||
{
|
{
|
||||||
if (static_cast<char>(cpu->read8(addr + i)) != value[i])
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<u32> searchWorkerByteArray(DebugInterface* cpu, std::vector<u32> searchAddresses, u32 start, u32 end, QByteArray value)
|
||||||
|
{
|
||||||
|
|
||||||
|
std::vector<u32> hitAddresses;
|
||||||
|
const bool isSearchingRange = searchAddresses.size() <= 0;
|
||||||
|
if (isSearchingRange)
|
||||||
|
{
|
||||||
|
for (u32 addr = start; addr < end; addr += 1)
|
||||||
|
{
|
||||||
|
if (compareByteArrayAtAddress(cpu, addr, value))
|
||||||
{
|
{
|
||||||
hit = false;
|
hitAddresses.emplace_back(addr);
|
||||||
break;
|
addr += value.length() - 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (hit)
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (u32 addr : searchAddresses)
|
||||||
{
|
{
|
||||||
hitAddresses.emplace_back(addr);
|
if (compareByteArrayAtAddress(cpu, addr, value))
|
||||||
addr += value.length() - 1;
|
{
|
||||||
|
hitAddresses.emplace_back(addr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return hitAddresses;
|
return hitAddresses;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<u32> startWorker(DebugInterface* cpu, int type, u32 start, u32 end, QString value, int base)
|
std::vector<u32> startWorker(DebugInterface* cpu, int type, std::vector<u32> searchAddresses, u32 start, u32 end, QString value, int base)
|
||||||
{
|
{
|
||||||
|
|
||||||
const bool isSigned = value.startsWith("-");
|
const bool isSigned = value.startsWith("-");
|
||||||
switch (type)
|
switch (type)
|
||||||
{
|
{
|
||||||
case 0:
|
case 0:
|
||||||
return isSigned ? searchWorker<s8>(cpu, start, end, value.toShort(nullptr, base)) : searchWorker<u8>(cpu, start, end, value.toUShort(nullptr, base));
|
return isSigned ? searchWorker<s8>(cpu, searchAddresses, start, end, value.toShort(nullptr, base)) : searchWorker<u8>(cpu, searchAddresses, start, end, value.toUShort(nullptr, base));
|
||||||
case 1:
|
case 1:
|
||||||
return isSigned ? searchWorker<s16>(cpu, start, end, value.toShort(nullptr, base)) : searchWorker<u16>(cpu, start, end, value.toUShort(nullptr, base));
|
return isSigned ? searchWorker<s16>(cpu, searchAddresses, start, end, value.toShort(nullptr, base)) : searchWorker<u16>(cpu, searchAddresses, start, end, value.toUShort(nullptr, base));
|
||||||
case 2:
|
case 2:
|
||||||
return isSigned ? searchWorker<s32>(cpu, start, end, value.toInt(nullptr, base)) : searchWorker<u32>(cpu, start, end, value.toUInt(nullptr, base));
|
return isSigned ? searchWorker<s32>(cpu, searchAddresses, start, end, value.toInt(nullptr, base)) : searchWorker<u32>(cpu, searchAddresses, start, end, value.toUInt(nullptr, base));
|
||||||
case 3:
|
case 3:
|
||||||
return isSigned ? searchWorker<s64>(cpu, start, end, value.toLong(nullptr, base)) : searchWorker<s64>(cpu, start, end, value.toULongLong(nullptr, base));
|
return isSigned ? searchWorker<s64>(cpu, searchAddresses, start, end, value.toLong(nullptr, base)) : searchWorker<s64>(cpu, searchAddresses, start, end, value.toULongLong(nullptr, base));
|
||||||
case 4:
|
case 4:
|
||||||
return searchWorker<float>(cpu, start, end, value.toFloat());
|
return searchWorker<float>(cpu, searchAddresses, start, end, value.toFloat());
|
||||||
case 5:
|
case 5:
|
||||||
return searchWorker<double>(cpu, start, end, value.toDouble());
|
return searchWorker<double>(cpu, searchAddresses, start, end, value.toDouble());
|
||||||
case 6:
|
case 6:
|
||||||
return searchWorkerByteArray(cpu, start, end, value.toUtf8());
|
return searchWorkerByteArray(cpu, searchAddresses, start, end, value.toUtf8());
|
||||||
case 7:
|
case 7:
|
||||||
return searchWorkerByteArray(cpu, start, end, QByteArray::fromHex(value.toUtf8()));
|
return searchWorkerByteArray(cpu, searchAddresses, start, end, QByteArray::fromHex(value.toUtf8()));
|
||||||
default:
|
default:
|
||||||
Console.Error("Debugger: Unknown type when doing memory search!");
|
Console.Error("Debugger: Unknown type when doing memory search!");
|
||||||
break;
|
break;
|
||||||
|
@ -1024,16 +1063,53 @@ void CpuWidget::onSearchButtonClicked()
|
||||||
m_ui.listSearchResults->clear();
|
m_ui.listSearchResults->clear();
|
||||||
const auto& results = workerWatcher->future().result();
|
const auto& results = workerWatcher->future().result();
|
||||||
|
|
||||||
for (const auto& address : results)
|
m_searchResults = results;
|
||||||
{
|
loadSearchResults();
|
||||||
QListWidgetItem* item = new QListWidgetItem(QtUtils::FilledQStringFromValue(address, 16));
|
m_ui.btnFilterSearch->setDisabled(m_ui.listSearchResults->count() == 0);
|
||||||
item->setData(256, address);
|
|
||||||
m_ui.listSearchResults->addItem(item);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
m_ui.btnSearch->setDisabled(true);
|
m_ui.btnSearch->setDisabled(true);
|
||||||
|
QPushButton* senderButton = qobject_cast<QPushButton*>(sender());
|
||||||
|
bool isFilterSearch = senderButton == m_ui.btnFilterSearch;
|
||||||
|
std::vector<u32> addresses;
|
||||||
|
if (isFilterSearch)
|
||||||
|
{
|
||||||
|
addresses = m_searchResults;
|
||||||
|
}
|
||||||
QFuture<std::vector<u32>> workerFuture =
|
QFuture<std::vector<u32>> workerFuture =
|
||||||
QtConcurrent::run(startWorker, &m_cpu, searchType, searchStart, searchEnd, searchValue, searchHex ? 16 : 10);
|
QtConcurrent::run(startWorker, &m_cpu, searchType, addresses, searchStart, searchEnd, searchValue, searchHex ? 16 : 10);
|
||||||
workerWatcher->setFuture(workerFuture);
|
workerWatcher->setFuture(workerFuture);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CpuWidget::onSearchResultsListScroll(u32 value)
|
||||||
|
{
|
||||||
|
bool hasResultsToLoad = static_cast<size_t>(m_ui.listSearchResults->count()) < m_searchResults.size();
|
||||||
|
bool scrolledSufficiently = value > (m_ui.listSearchResults->verticalScrollBar()->maximum() * 0.95);
|
||||||
|
|
||||||
|
if (!m_resultsLoadTimer.isActive() && hasResultsToLoad && scrolledSufficiently)
|
||||||
|
{
|
||||||
|
// Load results once timer ends, allowing us to debounce repeated requests and only do one load.
|
||||||
|
m_resultsLoadTimer.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CpuWidget::loadSearchResults() {
|
||||||
|
const u32 numLoaded = m_ui.listSearchResults->count();
|
||||||
|
const u32 amountLeftToLoad = m_searchResults.size() - numLoaded;
|
||||||
|
if (amountLeftToLoad < 1)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const bool isFirstLoad = numLoaded == 0;
|
||||||
|
const u32 maxLoadAmount = isFirstLoad ? m_initialResultsLoadLimit : m_numResultsAddedPerLoad;
|
||||||
|
const u32 numToLoad = amountLeftToLoad > maxLoadAmount ? maxLoadAmount : amountLeftToLoad;
|
||||||
|
|
||||||
|
for (u32 i = 0; i < numToLoad; i++)
|
||||||
|
{
|
||||||
|
u32 address = m_searchResults.at(numLoaded + i);
|
||||||
|
QListWidgetItem* item = new QListWidgetItem(QtUtils::FilledQStringFromValue(address, 16));
|
||||||
|
item->setData(256, address);
|
||||||
|
m_ui.listSearchResults->addItem(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
#include <QtWidgets/QWidget>
|
#include <QtWidgets/QWidget>
|
||||||
#include <QtWidgets/QTableWidget>
|
#include <QtWidgets/QTableWidget>
|
||||||
#include <QtCore/QSortFilterProxyModel>
|
#include <QtCore/QSortFilterProxyModel>
|
||||||
|
#include <QtCore/QTimer>
|
||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
@ -94,9 +95,12 @@ public slots:
|
||||||
};
|
};
|
||||||
|
|
||||||
void onSearchButtonClicked();
|
void onSearchButtonClicked();
|
||||||
|
void onSearchResultsListScroll(u32 value);
|
||||||
|
void loadSearchResults();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::vector<QTableWidget*> m_registerTableViews;
|
std::vector<QTableWidget*> m_registerTableViews;
|
||||||
|
std::vector<u32> m_searchResults;
|
||||||
|
|
||||||
QMenu* m_stacklistContextMenu = 0;
|
QMenu* m_stacklistContextMenu = 0;
|
||||||
QMenu* m_funclistContextMenu = 0;
|
QMenu* m_funclistContextMenu = 0;
|
||||||
|
@ -110,7 +114,10 @@ private:
|
||||||
ThreadModel m_threadModel;
|
ThreadModel m_threadModel;
|
||||||
QSortFilterProxyModel m_threadProxyModel;
|
QSortFilterProxyModel m_threadProxyModel;
|
||||||
StackModel m_stackModel;
|
StackModel m_stackModel;
|
||||||
|
QTimer m_resultsLoadTimer;
|
||||||
|
|
||||||
bool m_demangleFunctions = true;
|
bool m_demangleFunctions = true;
|
||||||
bool m_moduleView = true;
|
bool m_moduleView = true;
|
||||||
|
u32 m_initialResultsLoadLimit = 20000;
|
||||||
|
u32 m_numResultsAddedPerLoad = 10000;
|
||||||
};
|
};
|
||||||
|
|
|
@ -183,14 +183,14 @@
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||||
<item>
|
<item>
|
||||||
<layout class="QGridLayout" name="gridLayout_2">
|
<layout class="QGridLayout" name="gridLayout_2">
|
||||||
<item row="5" column="0">
|
<item row="6" column="0">
|
||||||
<widget class="QLabel" name="label_5">
|
<widget class="QLabel" name="label_5">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>End</string>
|
<string>End</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="4" column="1" colspan="3">
|
<item row="5" column="1" colspan="3">
|
||||||
<widget class="QLineEdit" name="txtSearchStart">
|
<widget class="QLineEdit" name="txtSearchStart">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string notr="true">0x00</string>
|
<string notr="true">0x00</string>
|
||||||
|
@ -207,28 +207,28 @@
|
||||||
<item row="0" column="1">
|
<item row="0" column="1">
|
||||||
<widget class="QLineEdit" name="txtSearchValue"/>
|
<widget class="QLineEdit" name="txtSearchValue"/>
|
||||||
</item>
|
</item>
|
||||||
<item row="4" column="0">
|
<item row="5" column="0">
|
||||||
<widget class="QLabel" name="label_4">
|
<widget class="QLabel" name="label_4">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Start</string>
|
<string>Start</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="5" column="1" colspan="3">
|
<item row="6" column="1" colspan="3">
|
||||||
<widget class="QLineEdit" name="txtSearchEnd">
|
<widget class="QLineEdit" name="txtSearchEnd">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string notr="true">0x2000000</string>
|
<string notr="true">0x2000000</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="0">
|
<item row="2" column="0">
|
||||||
<widget class="QLabel" name="label_2">
|
<widget class="QLabel" name="label_2">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Type</string>
|
<string>Type</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="1">
|
<item row="2" column="1">
|
||||||
<widget class="QComboBox" name="cmbSearchType">
|
<widget class="QComboBox" name="cmbSearchType">
|
||||||
<item>
|
<item>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
@ -272,14 +272,14 @@
|
||||||
</item>
|
</item>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="2">
|
<item row="2" column="2">
|
||||||
<widget class="QLabel" name="label_3">
|
<widget class="QLabel" name="label_3">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Hex</string>
|
<string>Hex</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="3">
|
<item row="2" column="3">
|
||||||
<widget class="QCheckBox" name="chkSearchHex">
|
<widget class="QCheckBox" name="chkSearchHex">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string/>
|
<string/>
|
||||||
|
@ -296,6 +296,16 @@
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="1" column="2" colspan="2">
|
||||||
|
<widget class="QPushButton" name="btnFilterSearch">
|
||||||
|
<property name="enabled">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Filter Search</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
|
|
Loading…
Reference in New Issue