Added improved functionality for running PakTool and added "Save and Repack" button in the World Editor

This commit is contained in:
parax0 2016-03-21 06:55:01 -06:00
parent bb921dc613
commit 2c120e0b16
13 changed files with 641 additions and 42 deletions

271
src/Editor/CPakToolDialog.h Normal file
View File

@ -0,0 +1,271 @@
#ifndef CPAKTOOLDIALOG
#define CPAKTOOLDIALOG
#include <Common/Log.h>
#include <Common/types.h>
#include <Core/Resource/EGame.h>
#include "Editor/UICommon.h"
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QProcess>
#include <QProgressDialog>
class CPakToolDialog : public QProgressDialog
{
Q_OBJECT
public:
enum EResult { eSuccess, eError, eUserCancelled };
private:
enum { eExtract, eRepack, eListDump } mMode;
QProcess *mpPakTool;
QString mPakFilename;
QString mListFilename;
QString mFolderPath;
EGame mGame;
EResult mResult;
QString mPartialString;
bool mSetMax;
bool mUpdating;
// Private Functions
CPakToolDialog(QWidget *pParent = 0)
: QProgressDialog(pParent)
, mpPakTool(nullptr)
, mSetMax(false)
, mUpdating(false)
{
}
virtual QSize sizeHint() const
{
QSize Size = QProgressDialog::sizeHint();
Size.rwidth() *= 3;
return Size;
}
void Run()
{
mpPakTool = new QProcess(this);
QStringList Args;
if (mMode == eExtract)
{
Args << "-x" << mPakFilename;
setLabelText("Extracting...");
}
else if (mMode == eRepack)
{
Args << "-r" << GameString(mGame) << mFolderPath << mPakFilename << mListFilename;
setLabelText("Repacking...");
}
else if (mMode == eListDump)
Args << "-d" << mPakFilename;
setWindowTitle(labelText());
// List dump is really fast, so showing progress dialog isn't necessary for it.
if (mMode != eListDump)
{
connect(mpPakTool, SIGNAL(readyReadStandardOutput()), this, SLOT(ReadStdOut()));
connect(mpPakTool, SIGNAL(finished(int)), this, SLOT(PakToolFinished(int)));
mpPakTool->start("PakTool.exe", Args);
exec();
}
else
{
mpPakTool->start("PakTool.exe", Args);
mpPakTool->waitForFinished(-1);
}
}
EResult Result() const
{
if (wasCanceled()) return eUserCancelled;
else return (EResult) result();
}
private slots:
void ReadStdOut()
{
if (mUpdating) return;
mUpdating = true;
QString StdOut = mpPakTool->readAllStandardOutput();
QStringList Strings = StdOut.split(QRegExp("[\\r\\n]"), QString::SkipEmptyParts);
if (!Strings.isEmpty())
{
Strings.front().prepend(mPartialString);
QCharRef LastChar = StdOut[StdOut.size() - 1];
if (LastChar != '\n' && LastChar != '\r')
{
mPartialString = Strings.back();
if (Strings.size() > 1)
Strings.pop_back();
}
else
mPartialString = "";
const QRegExp kExtracting("^Extracting file ([0-9]{1,6}) of ([0-9]{1,6})");
const QRegExp kRepacking("^Repacking file ([0-9]{1,6}) of ([0-9]{1,6})");
const QRegExp& rkRegExp = (mMode == eExtract ? kExtracting : kRepacking);
int Result = rkRegExp.indexIn(Strings.last());
if (Result != -1)
{
int Cur = rkRegExp.cap(1).toInt();
int Count = rkRegExp.cap(2).toInt();
if (!mSetMax)
{
setMinimum(0);
setMaximum(Count);
mSetMax = true;
}
setValue(Cur);
setLabelText(QString("%1 file %2 of %3...").arg(mMode == eExtract ? "Extracting" : "Repacking").arg(Cur).arg(Count));
setWindowTitle(labelText());
}
update();
}
mUpdating = false;
}
void PakToolFinished(int ExitCode)
{
done(ExitCode == 0 ? eSuccess : eError);
}
public:
static EResult Extract(QString PakFilename, QString *pOutFolder = 0, QWidget *pParent = 0)
{
Log::Write("Extracting pak " + TO_TSTRING(PakFilename));
CPakToolDialog Dialog(pParent);
Dialog.mMode = eExtract;
Dialog.mPakFilename = PakFilename;
Dialog.Run();
EResult Result = Dialog.Result();
if (pOutFolder)
{
QFileInfo Pak(PakFilename);
*pOutFolder = Pak.absolutePath() + '/' + Pak.baseName() + "-pak/";
}
Log::Write("Result: " + TO_TSTRING(ResultString(Result)));
return Result;
}
static EResult Repack(EGame Game, QString TargetPak, QString ListFile, QString FolderPath, QString *pOutPak = 0, QWidget *pParent = 0)
{
Log::Write("Repacking folder " + TO_TSTRING(FolderPath) + " into pak " + TO_TSTRING(TargetPak));
CPakToolDialog Dialog(pParent);
Dialog.mMode = eRepack;
Dialog.mPakFilename = TargetPak;
Dialog.mListFilename = ListFile;
Dialog.mFolderPath = FolderPath;
Dialog.mGame = Game;
Dialog.Run();
EResult Result = Dialog.Result();
if (pOutPak)
*pOutPak = TargetPak;
Log::Write("Result: " + TO_TSTRING(ResultString(Result)));
return Result;
}
static EResult DumpList(QString PakFilename, QString *pOutTxt = 0, QWidget *pParent = 0)
{
Log::Write("Dumping file list for pak " + TO_TSTRING(PakFilename));
CPakToolDialog Dialog(pParent);
Dialog.mMode = eListDump;
Dialog.mPakFilename = PakFilename;
Dialog.Run();
EResult Result = Dialog.Result();
if (pOutTxt)
{
QFileInfo Pak(PakFilename);
*pOutTxt = Pak.absolutePath() + '/' + Pak.baseName() + "-pak.txt";
}
Log::Write("Result: " + TO_TSTRING(ResultString(Result)));
return Result;
}
static QString TargetPakForFolder(QString Folder)
{
QDir Dir(Folder);
QString PakName = Dir.dirName();
if (PakName.endsWith("-pak")) PakName.chop(4);
Dir.cdUp();
QString DirName = Dir.absolutePath();
QString PakPath = DirName + '/' + PakName + ".pak";
if (QFile::exists(PakPath))
return PakPath;
else
return "";
}
static QString TargetListForFolder(QString Folder)
{
QDir Dir(Folder);
QString TxtName = Dir.dirName();
Dir.cdUp();
QString DirName = Dir.absolutePath();
QString ListPath = DirName + '/' + TxtName + ".txt";
if (QFile::exists(ListPath))
return ListPath;
else
return "";
}
static QString GameString(EGame Game)
{
switch (Game)
{
case ePrimeDemo: return "mp1demo";
case ePrime: return "mp1";
case eEchoesDemo: return "mp2demo";
case eEchoes: return "mp2";
case eCorruptionProto: return "mp3proto";
case eCorruption: return "mp3";
case eReturns: return "dkcr";
default: return "INVALID";
}
}
static QString ResultString(EResult Result)
{
switch (Result)
{
case eSuccess: return "Success";
case eError: return "Error";
case eUserCancelled: return "User Cancelled";
default: return "";
}
}
};
#endif // CPAKTOOLDIALOG

View File

@ -1,6 +1,7 @@
#include "CStartWindow.h"
#include "ui_CStartWindow.h"
#include "CErrorLogDialog.h"
#include "CPakToolDialog.h"
#include "UICommon.h"
#include "Editor/ModelEditor/CModelEditorWindow.h"
@ -43,8 +44,15 @@ void CStartWindow::on_actionOpen_MLVL_triggered()
if (mpWorldEditor->close())
{
gResCache.SetFolder(TString(WorldFile.toStdString()).GetFileDirectory());
TString Dir = TO_TSTRING(WorldFile).GetFileDirectory();
gResCache.SetFolder(Dir);
mpWorld = gResCache.GetResource(WorldFile.toStdString());
QString QStrDir = TO_QSTRING(Dir);
mpWorldEditor->SetWorldDir(QStrDir);
mpWorldEditor->SetPakFileList(CPakToolDialog::TargetListForFolder(QStrDir));
mpWorldEditor->SetPakTarget(CPakToolDialog::TargetPakForFolder(QStrDir));
FillWorldUI();
}
}
@ -212,44 +220,15 @@ void CStartWindow::on_actionExtract_PAK_triggered()
QString Pak = QFileDialog::getOpenFileName(this, "Select pak", "", "Package (*.pak)");
if (!Pak.isEmpty())
ExtractPackage(Pak, true);
}
void CStartWindow::ExtractPackage(const QString& rkPath, bool PopupOnComplete)
{
// Not the ideal way to be handling this but it's the easiest and it'll be a good holdover until later
QProcess PakTool(this);
// Dump assets
QStringList PakToolArgs;
PakToolArgs << "-x" << rkPath;
QDialog Dialog;
QLayout *pLayout = new QVBoxLayout(&Dialog);
pLayout->addWidget(new QLabel("Extracting...", &Dialog));
Dialog.setWindowFlags(Qt::Window | Qt::WindowTitleHint);
connect(&PakTool, SIGNAL(finished(int)), &Dialog, SLOT(done(int)));
PakTool.start("PakTool.exe", PakToolArgs);
Dialog.exec();
int Result = PakTool.exitCode();
// Dump list
if (Result == 0)
{
PakToolArgs.clear();
PakToolArgs << "-d" << rkPath;
PakTool.start("PakTool.exe", PakToolArgs);
PakTool.waitForFinished();
Result = PakTool.exitCode();
}
CPakToolDialog::EResult Result = CPakToolDialog::Extract(Pak);
// Results
if (PopupOnComplete)
{
if (Result == 0)
if (Result == CPakToolDialog::eSuccess)
Result = CPakToolDialog::DumpList(Pak);
if (Result == CPakToolDialog::eSuccess)
QMessageBox::information(this, "Success", "Extracted pak successfully!");
else
QMessageBox::warning(this, "Error", "Couldn't extract pak");
else if (Result == CPakToolDialog::eError)
QMessageBox::warning(this, "Error", "Unable to extract pak.");
}
}

View File

@ -44,7 +44,6 @@ private slots:
private:
void FillWorldUI();
void FillAreaUI();
void ExtractPackage(const QString& rkPath, bool PopupOnComplete);
};
#endif // PWESTARTWINDOW_H

View File

@ -157,7 +157,9 @@ HEADERS += \
Undo/ObjReferences.h \
Undo/CCloneSelectionCommand.h \
CNodeCopyMimeData.h \
Undo/CPasteNodesCommand.h
Undo/CPasteNodesCommand.h \
CPakToolDialog.h \
WorldEditor/CRepackInfoDialog.h
# Source Files
SOURCES += \
@ -216,7 +218,8 @@ SOURCES += \
Undo/CDeleteSelectionCommand.cpp \
Undo/CCreateInstanceCommand.cpp \
Undo/CCloneSelectionCommand.cpp \
Undo/CPasteNodesCommand.cpp
Undo/CPasteNodesCommand.cpp \
WorldEditor/CRepackInfoDialog.cpp
# UI Files
FORMS += \
@ -235,4 +238,5 @@ FORMS += \
WorldEditor/CPoiMapEditDialog.ui \
WorldEditor/CTemplateEditDialog.ui \
WorldEditor/CLinkDialog.ui \
WorldEditor/CSelectInstanceDialog.ui
WorldEditor/CSelectInstanceDialog.ui \
WorldEditor/CRepackInfoDialog.ui

View File

@ -43,5 +43,6 @@
<file>icons/Edit_16px.png</file>
<file>icons/Edit_24px.png</file>
<file>icons/Edit_32px.png</file>
<file>icons/SaveAndRepack_32px.png</file>
</qresource>
</RCC>

View File

@ -0,0 +1,114 @@
#include "CRepackInfoDialog.h"
#include "ui_CRepackInfoDialog.h"
#include "Editor/CPakToolDialog.h"
#include <QDir>
#include <QFile>
#include <QFileDialog>
#include <QPushButton>
CRepackInfoDialog::CRepackInfoDialog(QString TargetFolder, QString ListFile, QString OutputPak, QWidget *pParent /*= 0*/)
: QDialog(pParent)
, ui(new Ui::CRepackInfoDialog)
{
ui->setupUi(this);
ui->FolderLineEdit->setText(TargetFolder);
ui->FileListLineEdit->setText(ListFile);
ui->OutputPakLineEdit->setText(OutputPak);
UpdateStatus();
connect(ui->FolderToolButton, SIGNAL(clicked()), this, SLOT(BrowseFolderClicked()));
connect(ui->FileListToolButton, SIGNAL(clicked()), this, SLOT(BrowseListClicked()));
connect(ui->OutputPakToolButton, SIGNAL(clicked()), this, SLOT(BrowseOutPakClicked()));
connect(ui->FolderLineEdit, SIGNAL(textChanged(QString)), this, SLOT(UpdateStatus()));
connect(ui->FileListLineEdit, SIGNAL(textChanged(QString)), this, SLOT(UpdateStatus()));
connect(ui->OutputPakLineEdit, SIGNAL(textChanged(QString)), this, SLOT(UpdateStatus()));
}
CRepackInfoDialog::~CRepackInfoDialog()
{
delete ui;
}
bool CRepackInfoDialog::TargetFolderValid() const
{
return QDir(ui->FolderLineEdit->text()).exists();
}
bool CRepackInfoDialog::ListFileValid() const
{
return QFile::exists(ui->FileListLineEdit->text());
}
bool CRepackInfoDialog::OutputPakValid() const
{
return QFile::exists(ui->OutputPakLineEdit->text());
}
QString CRepackInfoDialog::TargetFolder() const
{
return ui->FolderLineEdit->text();
}
QString CRepackInfoDialog::ListFile() const
{
return ui->FileListLineEdit->text();
}
QString CRepackInfoDialog::OutputPak() const
{
return ui->OutputPakLineEdit->text();
}
// ************ PUBLIC SLOTS ************
void CRepackInfoDialog::BrowseFolderClicked()
{
QString Folder = QFileDialog::getExistingDirectory(this, "Choose directory");
if (!Folder.isEmpty())
{
ui->FolderLineEdit->setText(Folder);
ui->FolderLineEdit->setFocus();
}
}
void CRepackInfoDialog::BrowseListClicked()
{
QString List = QFileDialog::getOpenFileName(this, "Open list file", "", "All supported files (*.txt *.pak);;Text file (*.txt);;Pak file (*.pak)");
if (!List.isEmpty())
{
if (List.endsWith(".txt"))
ui->FileListLineEdit->setText(List);
else if (List.endsWith(".pak"))
{
QString Txt;
CPakToolDialog::DumpList(List, &Txt);
ui->FileListLineEdit->setText(Txt);
}
ui->FileListLineEdit->setFocus();
}
}
void CRepackInfoDialog::BrowseOutPakClicked()
{
QString Pak = QFileDialog::getSaveFileName(this, "Save pak", "", "Pak File (*.pak)");
if (!Pak.isEmpty())
{
ui->OutputPakLineEdit->setText(Pak);
ui->OutputPakLineEdit->setFocus();
}
}
void CRepackInfoDialog::UpdateStatus()
{
static const QString skInvalidStylesheet = "border: 1px solid red";
ui->FolderLineEdit->setStyleSheet(TargetFolderValid() ? "" : skInvalidStylesheet);
ui->FileListLineEdit->setStyleSheet(ListFileValid() ? "" : skInvalidStylesheet);
ui->OutputPakLineEdit->setStyleSheet(OutputPakValid() ? "" : skInvalidStylesheet);
ui->ButtonBox->button(QDialogButtonBox::Ok)->setEnabled(TargetFolderValid() && ListFileValid() && OutputPakValid());
}

View File

@ -0,0 +1,34 @@
#ifndef CREPACKINFODIALOG_H
#define CREPACKINFODIALOG_H
#include <QDialog>
namespace Ui {
class CRepackInfoDialog;
}
class CRepackInfoDialog : public QDialog
{
Q_OBJECT
Ui::CRepackInfoDialog *ui;
public:
explicit CRepackInfoDialog(QString TargetFolder, QString ListFile, QString OutputPak, QWidget *pParent = 0);
~CRepackInfoDialog();
bool TargetFolderValid() const;
bool ListFileValid() const;
bool OutputPakValid() const;
QString TargetFolder() const;
QString ListFile() const;
QString OutputPak() const;
public slots:
void BrowseFolderClicked();
void BrowseListClicked();
void BrowseOutPakClicked();
void UpdateStatus();
};
#endif // CREPACKINFODIALOG_H

View File

@ -0,0 +1,127 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CRepackInfoDialog</class>
<widget class="QDialog" name="CRepackInfoDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>125</height>
</rect>
</property>
<property name="windowTitle">
<string>Set Pak Info</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="FolderLabel">
<property name="text">
<string>Folder</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="FolderLineEdit"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="FileListLabel">
<property name="text">
<string>File List</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="FileListLineEdit"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="OutputPakLabel">
<property name="text">
<string>Output Pak</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="OutputPakLineEdit"/>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QToolButton" name="FolderToolButton">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="FileListToolButton">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="OutputPakToolButton">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="ButtonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>ButtonBox</sender>
<signal>accepted()</signal>
<receiver>CRepackInfoDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>ButtonBox</sender>
<signal>rejected()</signal>
<receiver>CRepackInfoDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -17,7 +17,7 @@
<item>
<widget class="QTabWidget" name="TabWidget">
<property name="currentIndex">
<number>1</number>
<number>0</number>
</property>
<widget class="QWidget" name="LayersTab">
<attribute name="title">

View File

@ -2,12 +2,14 @@
#include "ui_CWorldEditor.h"
#include "CConfirmUnlinkDialog.h"
#include "CLayerEditor.h"
#include "CRepackInfoDialog.h"
#include "CTemplateMimeData.h"
#include "WModifyTab.h"
#include "WInstancesTab.h"
#include "Editor/CBasicViewport.h"
#include "Editor/CNodeCopyMimeData.h"
#include "Editor/CPakToolDialog.h"
#include "Editor/CSelectionIterator.h"
#include "Editor/UICommon.h"
#include "Editor/PropertyEdit/CPropertyView.h"
@ -114,6 +116,7 @@ CWorldEditor::CWorldEditor(QWidget *parent)
connect(&mUndoStack, SIGNAL(indexChanged(int)), this, SLOT(OnUndoStackIndexChanged()));
connect(ui->ActionSave, SIGNAL(triggered()), this, SLOT(Save()));
connect(ui->ActionSaveAndRepack, SIGNAL(triggered()), this, SLOT(SaveAndRepack()));
ui->CreateTabEditorProperties->SyncToEditor(this);
ui->ModifyTabEditorProperties->SyncToEditor(this);
@ -347,6 +350,37 @@ bool CWorldEditor::Save()
}
}
bool CWorldEditor::SaveAndRepack()
{
if (!Save()) return false;
if (!CanRepack())
{
CRepackInfoDialog Dialog(mWorldDir, mPakFileList, mPakTarget, this);
Dialog.exec();
if (Dialog.result() == QDialog::Accepted)
{
SetWorldDir(Dialog.TargetFolder());
SetPakFileList(Dialog.ListFile());
SetPakTarget(Dialog.OutputPak());
if (!CanRepack()) return false;
}
else return false;
}
QString PakOut;
CPakToolDialog::EResult Result = CPakToolDialog::Repack(CurrentGame(), mPakTarget, mPakFileList, mWorldDir, &PakOut);
if (Result == CPakToolDialog::eError)
QMessageBox::warning(this, "Error", "Failed to repack!");
else if (Result == CPakToolDialog::eSuccess)
QMessageBox::information(this, "Success", "Successfully saved pak: " + PakOut);
return (Result == CPakToolDialog::eSuccess);
}
void CWorldEditor::OnLinksModified(const QList<CScriptObject*>& rkInstances)
{
foreach (CScriptObject *pInstance, rkInstances)

View File

@ -19,6 +19,8 @@
#include <Core/SRayIntersection.h>
#include <QComboBox>
#include <QDir>
#include <QFile>
#include <QList>
#include <QMainWindow>
#include <QTimer>
@ -44,6 +46,10 @@ class CWorldEditor : public INodeEditor
CScriptObject *mpNewLinkSender;
CScriptObject *mpNewLinkReceiver;
QString mWorldDir;
QString mPakFileList;
QString mPakTarget;
public:
explicit CWorldEditor(QWidget *parent = 0);
~CWorldEditor();
@ -57,6 +63,12 @@ public:
inline CLinkDialog* LinkDialog() const { return mpLinkDialog; }
CSceneViewport* Viewport() const;
inline void SetWorldDir(QString WorldDir) { mWorldDir = (QDir(WorldDir).exists() ? WorldDir : ""); }
inline void SetPakFileList(QString FileList) { mPakFileList = (QFile::exists(FileList) ? FileList : ""); }
inline void SetPakTarget(QString PakTarget) { mPakTarget = (QFile::exists(PakTarget) ? PakTarget : ""); }
inline bool CanRepack() const { return !mWorldDir.isEmpty() && !mPakFileList.isEmpty() && !mPakTarget.isEmpty(); }
public slots:
virtual void NotifyNodeAboutToBeDeleted(CSceneNode *pNode);
@ -64,6 +76,7 @@ public slots:
void Copy();
void Paste();
bool Save();
bool SaveAndRepack();
void OnLinksModified(const QList<CScriptObject*>& rkInstances);
void OnPropertyModified(IProperty *pProp);
void SetSelectionActive(bool Active);

View File

@ -393,6 +393,7 @@
<bool>false</bool>
</attribute>
<addaction name="ActionSave"/>
<addaction name="ActionSaveAndRepack"/>
<addaction name="separator"/>
<addaction name="ActionLink"/>
<addaction name="ActionUnlink"/>
@ -412,6 +413,7 @@
<string>File</string>
</property>
<addaction name="ActionSave"/>
<addaction name="ActionSaveAndRepack"/>
</widget>
<widget class="QMenu" name="menuEdit">
<property name="title">
@ -509,6 +511,9 @@
<property name="toolTip">
<string>Link</string>
</property>
<property name="shortcut">
<string>Alt+C</string>
</property>
</action>
<action name="ActionUnlink">
<property name="icon">
@ -521,6 +526,9 @@
<property name="toolTip">
<string>Unlink</string>
</property>
<property name="shortcut">
<string>Alt+D</string>
</property>
</action>
<action name="ActionDrawWorld">
<property name="checkable">
@ -792,6 +800,21 @@
<string>Paste</string>
</property>
</action>
<action name="ActionSaveAndRepack">
<property name="icon">
<iconset resource="../Icons.qrc">
<normaloff>:/icons/SaveAndRepack_32px.png</normaloff>:/icons/SaveAndRepack_32px.png</iconset>
</property>
<property name="text">
<string>Save and Repack</string>
</property>
<property name="toolTip">
<string>Save and Repack</string>
</property>
<property name="shortcut">
<string>Ctrl+Shift+S</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB