Working colors and columns

This commit is contained in:
wargio 2024-01-02 18:13:03 +08:00
parent 5e2d58ea11
commit d974c24daa
12 changed files with 558 additions and 37 deletions

View File

@ -158,6 +158,7 @@ set(SOURCES
tools/basefind/BaseFindResultsDialog.cpp tools/basefind/BaseFindResultsDialog.cpp
tools/bindiff/DiffLoadDialog.cpp tools/bindiff/DiffLoadDialog.cpp
tools/bindiff/DiffWaitDialog.cpp tools/bindiff/DiffWaitDialog.cpp
tools/bindiff/DiffWindow.cpp
) )
set(HEADER_FILES set(HEADER_FILES
core/Cutter.h core/Cutter.h
@ -325,6 +326,7 @@ set(HEADER_FILES
tools/basefind/BaseFindResultsDialog.h tools/basefind/BaseFindResultsDialog.h
tools/bindiff/DiffLoadDialog.h tools/bindiff/DiffLoadDialog.h
tools/bindiff/DiffWaitDialog.h tools/bindiff/DiffWaitDialog.h
tools/bindiff/DiffWindow.h
) )
set(UI_FILES set(UI_FILES
dialogs/AboutDialog.ui dialogs/AboutDialog.ui
@ -401,6 +403,7 @@ set(UI_FILES
tools/basefind/BaseFindResultsDialog.ui tools/basefind/BaseFindResultsDialog.ui
tools/bindiff/DiffLoadDialog.ui tools/bindiff/DiffLoadDialog.ui
tools/bindiff/DiffWaitDialog.ui tools/bindiff/DiffWaitDialog.ui
tools/bindiff/DiffWindow.ui
) )
set(QRC_FILES set(QRC_FILES
resources.qrc resources.qrc

View File

@ -82,10 +82,6 @@ const QHash<QString, QHash<ColorFlags, QColor>> Configuration::cutterOptionColor
{ { DarkFlag, QColor(0x4c, 0xaf, 0x50) }, { LightFlag, QColor(0x00, 0xc8, 0x53) } } }, { { DarkFlag, QColor(0x4c, 0xaf, 0x50) }, { LightFlag, QColor(0x00, 0xc8, 0x53) } } },
{ "gui.match.partial", { "gui.match.partial",
{ { DarkFlag, QColor(0xff, 0xc1, 0x07) }, { LightFlag, QColor(0xff, 0xa0, 0x00) } } }, { { DarkFlag, QColor(0xff, 0xc1, 0x07) }, { LightFlag, QColor(0xff, 0xa0, 0x00) } } },
{ "gui.match.none",
{ { DarkFlag, QColor(0xf4, 0x43, 0x36) }, { LightFlag, QColor(0xd3, 0x2f, 0x2f) } } },
{ "gui.match.new",
{ { DarkFlag, QColor(0x21, 0x96, 0xf3) }, { LightFlag, QColor(0x19, 0x76, 0xd2) } } },
{ "lineHighlight", { "lineHighlight",
{ { DarkFlag, QColor(0x15, 0x1d, 0x1d, 0x96) }, { { DarkFlag, QColor(0x15, 0x1d, 0x1d, 0x96) },
{ LightFlag, QColor(0xd2, 0xd2, 0xff, 0x96) } } }, { LightFlag, QColor(0xd2, 0xd2, 0xff, 0x96) } } },

View File

@ -100,27 +100,27 @@ QList<BinDiffMatchDescription> BinDiff::matches()
desc.simtype = RZ_ANALYSIS_SIMILARITY_TYPE_STR(pair->similarity); desc.simtype = RZ_ANALYSIS_SIMILARITY_TYPE_STR(pair->similarity);
desc.similarity = pair->similarity; desc.similarity = pair->similarity;
pairs << desc; pairs.push_back(desc);
} }
return pairs; return pairs;
} }
QList<FunctionDescription> BinDiff::mismatch(bool fileA) QList<FunctionDescription> BinDiff::mismatch(bool originalFile)
{ {
QList<FunctionDescription> list; QList<FunctionDescription> list;
RzAnalysisFunction *func = nullptr;
RzList *unmatch = fileA ? result->unmatch_a : result->unmatch_b;
RzListIter *it = nullptr;
if (!result) { if (!result) {
return list; return list;
} }
RzAnalysisFunction *func = nullptr;
RzList *unmatch = originalFile ? result->unmatch_a : result->unmatch_b;
RzListIter *it = nullptr;
CutterRzListForeach (unmatch, it, RzAnalysisFunction, func) { CutterRzListForeach (unmatch, it, RzAnalysisFunction, func) {
FunctionDescription desc; FunctionDescription desc;
setFunctionDescription(&desc, func); setFunctionDescription(&desc, func);
list << desc; list.push_back(desc);
} }
return list; return list;

View File

@ -22,7 +22,7 @@ public:
void setAnalysisLevel(int aLevel); void setAnalysisLevel(int aLevel);
QList<BinDiffMatchDescription> matches(); QList<BinDiffMatchDescription> matches();
QList<FunctionDescription> mismatch(bool fileA); QList<FunctionDescription> mismatch(bool originalFile);
public slots: public slots:
void cancel(); void cancel();

View File

@ -4564,6 +4564,32 @@ bool CutterCore::isWriteModeEnabled()
return false; return false;
} }
RzList *get_functions(RzAnalysis *analysis)
{
RzList *functions = rz_analysis_get_fcns(analysis);
if (!functions) {
return nullptr;
}
RzList *list = rz_list_newf(nullptr);
if (!list) {
return list;
}
RzAnalysisFunction *func = nullptr;
RzListIter *it = nullptr;
CutterRzListForeach (functions, it, RzAnalysisFunction, func) {
QString name = func->name;
if (name.startsWith("sym.imp.") || name.startsWith("loc.imp.") || name.startsWith("imp.")) {
continue;
}
rz_list_add_sorted(list, func, analysis->columnSort);
}
return list;
}
RzAnalysisMatchResult *CutterCore::diffNewFile(const QString &filePath, int level, RzAnalysisMatchResult *CutterCore::diffNewFile(const QString &filePath, int level,
RzAnalysisMatchThreadInfoCb callback, void *user) RzAnalysisMatchThreadInfoCb callback, void *user)
{ {
@ -4621,21 +4647,18 @@ RzAnalysisMatchResult *CutterCore::diffNewFile(const QString &filePath, int leve
goto fail; goto fail;
} }
fcns_a = rz_list_clone(rz_analysis_get_fcns(core_a->analysis)); fcns_a = get_functions(core_a->analysis);
if (rz_list_empty(fcns_a)) { if (rz_list_empty(fcns_a)) {
qWarning() << "no functions found in the current opened file"; qWarning() << "no functions found in the current opened file";
goto fail; goto fail;
} }
fcns_b = rz_list_clone(rz_analysis_get_fcns(core_b->analysis)); fcns_b = get_functions(core_b->analysis);
if (rz_list_empty(fcns_b)) { if (rz_list_empty(fcns_b)) {
qWarning() << "no functions found in " << filePath; qWarning() << "no functions found in " << filePath;
goto fail; goto fail;
} }
rz_list_sort(fcns_a, core_a->analysis->columnSort);
rz_list_sort(fcns_b, core_b->analysis->columnSort);
opts.analysis_a = core_a->analysis; opts.analysis_a = core_a->analysis;
opts.analysis_b = core_b->analysis; opts.analysis_b = core_b->analysis;
opts.callback = callback; opts.callback = callback;

View File

@ -20,7 +20,7 @@
</sizepolicy> </sizepolicy>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
<string notr="true">Select which file to compare.</string> <string notr="false">Select which file to compare.</string>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_2"> <layout class="QVBoxLayout" name="verticalLayout_2">
<property name="sizeConstraint"> <property name="sizeConstraint">

View File

@ -6,6 +6,8 @@
#include <core/Cutter.h> #include <core/Cutter.h>
#include <rz_util.h> #include <rz_util.h>
#include "DiffWindow.h"
DiffWaitDialog::DiffWaitDialog(QWidget *parent) DiffWaitDialog::DiffWaitDialog(QWidget *parent)
: QDialog(parent), timer(parent), bDiff(new BinDiff()), ui(new Ui::DiffWaitDialog) : QDialog(parent), timer(parent), bDiff(new BinDiff()), ui(new Ui::DiffWaitDialog)
{ {
@ -29,27 +31,18 @@ DiffWaitDialog::DiffWaitDialog(QWidget *parent)
DiffWaitDialog::~DiffWaitDialog() DiffWaitDialog::~DiffWaitDialog()
{ {
if (bDiff->isRunning()) { if (bDiff && bDiff->isRunning()) {
bDiff->cancel(); bDiff->cancel();
bDiff->wait(); bDiff->wait();
delete bDiff;
} }
} }
QList<BinDiffMatchDescription> DiffWaitDialog::matches()
{
return bDiff->matches();
}
QList<FunctionDescription> DiffWaitDialog::mismatch(bool fileA)
{
return bDiff->mismatch(fileA);
}
void DiffWaitDialog::show(QString original, QString modified, int level) void DiffWaitDialog::show(QString original, QString modified, int level)
{ {
connect(this, &DiffWaitDialog::cancelJob, bDiff.get(), &BinDiff::cancel); connect(this, &DiffWaitDialog::cancelJob, bDiff, &BinDiff::cancel);
connect(bDiff.get(), &BinDiff::progress, this, &DiffWaitDialog::onProgress); connect(bDiff, &BinDiff::progress, this, &DiffWaitDialog::onProgress);
connect(bDiff.get(), &BinDiff::complete, this, &DiffWaitDialog::onCompletion); connect(bDiff, &BinDiff::complete, this, &DiffWaitDialog::onCompletion);
connect(&timer, &QTimer::timeout, this, &DiffWaitDialog::updateElapsedTime); connect(&timer, &QTimer::timeout, this, &DiffWaitDialog::updateElapsedTime);
ui->lineEditOriginal->setText(original); ui->lineEditOriginal->setText(original);
@ -86,6 +79,12 @@ void DiffWaitDialog::onProgress(BinDiffStatusDescription status)
void DiffWaitDialog::onCompletion() void DiffWaitDialog::onCompletion()
{ {
timer.stop(); timer.stop();
auto results = new DiffWindow(bDiff, parentWidget());
bDiff = nullptr;
results->showMaximized();
close();
} }
void DiffWaitDialog::updateElapsedTime() void DiffWaitDialog::updateElapsedTime()

View File

@ -24,9 +24,6 @@ public:
void show(QString original, QString modified, int level); void show(QString original, QString modified, int level);
QList<BinDiffMatchDescription> matches();
QList<FunctionDescription> mismatch(bool fileA);
public slots: public slots:
void onProgress(BinDiffStatusDescription status); void onProgress(BinDiffStatusDescription status);
void onCompletion(); void onCompletion();
@ -41,7 +38,7 @@ private slots:
private: private:
QElapsedTimer eTimer; QElapsedTimer eTimer;
QTimer timer; QTimer timer;
std::unique_ptr<BinDiff> bDiff; BinDiff *bDiff;
std::unique_ptr<Ui::DiffWaitDialog> ui; std::unique_ptr<Ui::DiffWaitDialog> ui;
}; };

View File

@ -20,7 +20,7 @@
</sizepolicy> </sizepolicy>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
<string notr="true">Performing binary diffing</string> <string notr="false">Performing binary diffing</string>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_2"> <layout class="QVBoxLayout" name="verticalLayout_2">
<property name="sizeConstraint"> <property name="sizeConstraint">

View File

@ -0,0 +1,239 @@
#include "DiffWindow.h"
#include "ui_DiffWindow.h"
#include <core/Cutter.h>
#include "common/Configuration.h"
#include "CutterConfig.h"
#include <rz_th.h>
#include <QFileDialog>
#include <QMessageBox>
DiffMatchModel::DiffMatchModel(QList<BinDiffMatchDescription> *list, QColor cPerf, QColor cPart,
QObject *parent)
: QAbstractListModel(parent), list(list), perfect(cPerf), partial(cPart)
{
}
int DiffMatchModel::rowCount(const QModelIndex &) const
{
return list->count();
}
int DiffMatchModel::columnCount(const QModelIndex &) const
{
return DiffMatchModel::ColumnCount;
}
QVariant DiffMatchModel::data(const QModelIndex &index, int role) const
{
if (index.row() >= list->count())
return QVariant();
const BinDiffMatchDescription &entry = list->at(index.row());
switch (role) {
case Qt::DisplayRole:
switch (index.column()) {
case NameOrig:
return entry.original.name;
case SizeOrig:
return QString::asprintf("%llu (%#llx)", entry.original.linearSize,
entry.original.linearSize);
case AddressOrig:
return RzAddressString(entry.original.offset);
case Similarity:
return QString::asprintf("%.2f (%.2f %%)", entry.similarity, entry.similarity * 100.0);
case AddressMod:
return RzAddressString(entry.modified.offset);
case SizeMod:
return QString::asprintf("%llu (%#llx)", entry.modified.linearSize,
entry.modified.linearSize);
case NameMod:
return entry.modified.name;
default:
return QVariant();
}
case Qt::ToolTipRole: {
return entry.simtype;
}
case Qt::BackgroundRole: {
return gradientByRatio(entry.similarity);
}
default:
return QVariant();
}
}
QVariant DiffMatchModel::headerData(int section, Qt::Orientation, int role) const
{
switch (role) {
case Qt::DisplayRole:
switch (section) {
case NameOrig:
return tr("Name (A)");
case SizeOrig:
return tr("Size (A)");
case AddressOrig:
return tr("Address (A)");
case Similarity:
return tr("Similarity");
case AddressMod:
return tr("Address (B)");
case SizeMod:
return tr("Size (B)");
case NameMod:
return tr("Name (B)");
default:
return QVariant();
}
default:
return QVariant();
}
}
QColor DiffMatchModel::gradientByRatio(const double ratio) const
{
float red = partial.redF() + (ratio * (perfect.redF() - partial.redF()));
float green = partial.greenF() + (ratio * (perfect.greenF() - partial.greenF()));
float blue = partial.blueF() + (ratio * (perfect.blueF() - partial.blueF()));
return QColor::fromRgbF(red, green, blue);
}
DiffMismatchModel::DiffMismatchModel(QList<FunctionDescription> *list, QObject *parent)
: QAbstractListModel(parent), list(list)
{
}
int DiffMismatchModel::rowCount(const QModelIndex &) const
{
return list->count();
}
int DiffMismatchModel::columnCount(const QModelIndex &) const
{
return DiffMismatchModel::ColumnCount;
}
QVariant DiffMismatchModel::data(const QModelIndex &index, int role) const
{
if (index.row() >= list->count())
return QVariant();
const FunctionDescription &entry = list->at(index.row());
switch (role) {
case Qt::DisplayRole:
switch (index.column()) {
case FuncName:
return entry.name;
case FuncAddress:
return RzAddressString(entry.offset);
case FuncLinearSize:
return QString::asprintf("%llu (%#llx)", entry.linearSize, entry.linearSize);
case FuncNargs:
return QString::asprintf("%llu", entry.nargs);
case FuncNlocals:
return QString::asprintf("%llu", entry.nlocals);
case FuncNbbs:
return QString::asprintf("%llu", entry.nbbs);
case FuncCalltype:
return entry.calltype;
case FuncEdges:
return QString::asprintf("%llu", entry.edges);
case FuncStackframe:
return QString::asprintf("%llu", entry.stackframe);
default:
return QVariant();
}
case Qt::ToolTipRole: {
return entry.name;
}
default:
return QVariant();
}
}
QVariant DiffMismatchModel::headerData(int section, Qt::Orientation, int role) const
{
switch (role) {
case Qt::DisplayRole:
switch (section) {
case FuncName:
return tr("Name");
case FuncAddress:
return tr("Address");
case FuncLinearSize:
return tr("Linear Size");
case FuncNargs:
return tr("Num Args");
case FuncNlocals:
return tr("Num Locals");
case FuncNbbs:
return tr("Basic Blocks");
case FuncCalltype:
return tr("Call Type");
case FuncEdges:
return tr("Edges");
case FuncStackframe:
return tr("Stackframe");
default:
return QVariant();
}
default:
return QVariant();
}
}
DiffWindow::DiffWindow(BinDiff *bd, QWidget *parent)
: QDialog(parent), ui(new Ui::DiffWindow), bDiff(bd)
{
ui->setupUi(this);
ui->comboBoxShowInfo->addItem(tr("Summary"));
ui->comboBoxShowInfo->addItem(tr("AAAA"));
ui->comboBoxShowInfo->addItem(tr("BBBB"));
listMatch = bDiff->matches();
listDel = bDiff->mismatch(true);
listAdd = bDiff->mismatch(false);
QColor perfect = Config()->getColor("gui.match.perfect");
QColor partial = Config()->getColor("gui.match.partial");
modelMatch = new DiffMatchModel(&listMatch, perfect, partial, this);
modelDel = new DiffMismatchModel(&listDel, this);
modelAdd = new DiffMismatchModel(&listAdd, this);
// Matches Table
ui->tableViewMatch->setModel(modelMatch);
ui->tableViewMatch->sortByColumn(DiffMatchModel::Similarity, Qt::AscendingOrder);
ui->tableViewMatch->verticalHeader()->hide();
ui->tableViewMatch->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
ui->tableViewMatch->setContextMenuPolicy(Qt::CustomContextMenu);
// Deletion Table
ui->tableViewRem->setModel(modelDel);
ui->tableViewRem->sortByColumn(DiffMismatchModel::FuncName, Qt::AscendingOrder);
ui->tableViewRem->verticalHeader()->hide();
ui->tableViewRem->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
ui->tableViewRem->setContextMenuPolicy(Qt::CustomContextMenu);
// Addition Table
ui->tableViewAdd->setModel(modelAdd);
ui->tableViewAdd->sortByColumn(DiffMismatchModel::FuncName, Qt::AscendingOrder);
ui->tableViewAdd->verticalHeader()->hide();
ui->tableViewAdd->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
ui->tableViewAdd->setContextMenuPolicy(Qt::CustomContextMenu);
}
DiffWindow::~DiffWindow() {}
void DiffWindow::actionExport_as_JSON() {}
void DiffWindow::actionExport_as_Markdown() {}

View File

@ -0,0 +1,108 @@
#ifndef DIFF_WINDOW_H
#define DIFF_WINDOW_H
#include <QDialog>
#include <QListWidgetItem>
#include <memory>
#include <core/Cutter.h>
class DiffWindow;
namespace Ui {
class DiffWindow;
}
class DiffMatchModel : public QAbstractListModel
{
Q_OBJECT
friend DiffWindow;
public:
enum Column {
NameOrig = 0,
SizeOrig,
AddressOrig,
Similarity,
AddressMod,
SizeMod,
NameMod,
ColumnCount
};
DiffMatchModel(QList<BinDiffMatchDescription> *list, QColor cPerf, QColor cPart,
QObject *parent = nullptr);
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const;
int rowCount(const QModelIndex &parent = QModelIndex()) const;
int columnCount(const QModelIndex &parent = QModelIndex()) const;
QColor gradientByRatio(const double ratio) const;
private:
QList<BinDiffMatchDescription> *list;
QColor perfect, partial;
};
class DiffMismatchModel : public QAbstractListModel
{
Q_OBJECT
friend DiffWindow;
public:
enum Column {
FuncName = 0,
FuncAddress,
FuncLinearSize,
FuncNargs,
FuncNlocals,
FuncNbbs,
FuncCalltype,
FuncEdges,
FuncStackframe,
ColumnCount
};
DiffMismatchModel(QList<FunctionDescription> *list, QObject *parent = nullptr);
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const;
int rowCount(const QModelIndex &parent = QModelIndex()) const;
int columnCount(const QModelIndex &parent = QModelIndex()) const;
private:
QList<FunctionDescription> *list;
};
class DiffWindow : public QDialog
{
Q_OBJECT
public:
explicit DiffWindow(BinDiff *bd, QWidget *parent = nullptr);
~DiffWindow();
private slots:
void actionExport_as_JSON();
void actionExport_as_Markdown();
private:
std::unique_ptr<Ui::DiffWindow> ui;
std::unique_ptr<BinDiff> bDiff;
DiffMatchModel *modelMatch;
DiffMismatchModel *modelDel;
DiffMismatchModel *modelAdd;
QList<BinDiffMatchDescription> listMatch;
QList<FunctionDescription> listDel;
QList<FunctionDescription> listAdd;
};
#endif // DIFF_WINDOW_H

View File

@ -0,0 +1,156 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DiffWindow</class>
<widget class="QDialog" name="DiffWindow">
<property name="windowModality">
<enum>Qt::NonModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>992</width>
<height>641</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Diff Between Binaries</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum>
</property>
<item>
<widget class="QMenuBar" name="menuBar">
<property name="defaultUp">
<bool>false</bool>
</property>
<property name="nativeMenuBar">
<bool>true</bool>
</property>
<widget class="QMenu" name="menuExport">
<property name="title">
<string>Export as ...</string>
</property>
<addaction name="actionExport_as_JSON"/>
<addaction name="actionExport_as_Markdown"/>
</widget>
<addaction name="menuExport"/>
</widget>
</item>
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tabMatchFunc">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="contextMenuPolicy">
<enum>Qt::DefaultContextMenu</enum>
</property>
<attribute name="title">
<string>Match Funcs</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QTableView" name="tableViewMatch"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="tabMismatchRem">
<attribute name="title">
<string>Removed Funcs</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayoutMismatchRem">
<item>
<widget class="QTableView" name="tableViewRem"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="tabMismatchAdd">
<attribute name="title">
<string>Added Funcs</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayoutMismatchAdd">
<item>
<widget class="QTableView" name="tableViewAdd"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="tabInfo">
<attribute name="title">
<string>Info Diff</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayoutInfo">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<item>
<widget class="QLabel" name="label">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>527</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Show...</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBoxShowInfo">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QTableView" name="tableViewInfo"/>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout"/>
</item>
</layout>
<action name="actionExport_as_JSON">
<property name="text">
<string>Export as JSON</string>
</property>
</action>
<action name="actionExport_as_Markdown">
<property name="text">
<string>Export as Markdown</string>
</property>
</action>
</widget>
<resources/>
<connections/>
</ui>