diff --git a/VpkCompare.pro b/VpkCompare.pro new file mode 100644 index 0000000..173d8b3 --- /dev/null +++ b/VpkCompare.pro @@ -0,0 +1,33 @@ +#------------------------------------------------- +# +# Project created by QtCreator 2014-07-26T00:35:15 +# +#------------------------------------------------- + +QT += core gui + +greaterThan(QT_MAJOR_VERSION, 4): QT += widgets + +TARGET = VpkCompare +TEMPLATE = app + + +SOURCES += \ + vpkcompare.cpp \ + main.cpp \ + dropenabledlistwidget.cpp \ + ignoredfilesdialog.cpp \ + addignoredfiledialog.cpp + +HEADERS += \ + vpkcompare.h \ + dropenabledlistwidget.h \ + ignoredfilesdialog.h \ + addignoredfiledialog.h + +FORMS += \ + vpkcompare.ui \ + ignoredfilesdialog.ui \ + addignoredfiledialog.ui + +RESOURCES += diff --git a/addignoredfiledialog.cpp b/addignoredfiledialog.cpp new file mode 100644 index 0000000..265f522 --- /dev/null +++ b/addignoredfiledialog.cpp @@ -0,0 +1,19 @@ +#include "addignoredfiledialog.h" +#include "ui_addignoredfiledialog.h" + +AddIgnoredFileDialog::AddIgnoredFileDialog(QWidget *parent) : + QDialog(parent), + ui(new Ui::AddIgnoredFileDialog) +{ + ui->setupUi(this); +} + +AddIgnoredFileDialog::~AddIgnoredFileDialog() +{ + delete ui; +} + +QString AddIgnoredFileDialog::filename() const +{ + return ui->filename->text(); +} diff --git a/addignoredfiledialog.h b/addignoredfiledialog.h new file mode 100644 index 0000000..b4eecb1 --- /dev/null +++ b/addignoredfiledialog.h @@ -0,0 +1,24 @@ +#ifndef ADDIGNOREDFILEDIALOG_H +#define ADDIGNOREDFILEDIALOG_H + +#include + +namespace Ui { +class AddIgnoredFileDialog; +} + +class AddIgnoredFileDialog : public QDialog +{ + Q_OBJECT + +public: + explicit AddIgnoredFileDialog(QWidget *parent = 0); + ~AddIgnoredFileDialog(); + + QString filename() const; + +private: + Ui::AddIgnoredFileDialog *ui; +}; + +#endif // ADDIGNOREDFILEDIALOG_H diff --git a/addignoredfiledialog.ui b/addignoredfiledialog.ui new file mode 100644 index 0000000..3c86f48 --- /dev/null +++ b/addignoredfiledialog.ui @@ -0,0 +1,80 @@ + + + AddIgnoredFileDialog + + + + 0 + 0 + 266 + 73 + + + + Enter the Name of the File to Ignore + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + + buttonBox + accepted() + AddIgnoredFileDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + AddIgnoredFileDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/dropenabledlistwidget.cpp b/dropenabledlistwidget.cpp new file mode 100644 index 0000000..387b938 --- /dev/null +++ b/dropenabledlistwidget.cpp @@ -0,0 +1,73 @@ +#include "dropenabledlistwidget.h" + +#include + +#include +#include +#include + +#include + +DropEnabledListWidget::DropEnabledListWidget(QWidget *parent) : + QListWidget(parent) +{ + setAcceptDrops(true); +} + +void DropEnabledListWidget::dragEnterEvent(QDragEnterEvent *event) +{ + QStringList formats = event->mimeData()->formats(); + for (int i = 0; i < formats.size(); ++i) + { + // std::cout << formats[i].toLatin1().data() << std::endl; + }//*/ + if (event->mimeData()->hasFormat("application/x-qt-windows-mime;value=\"FileNameW\"")) + { + event->acceptProposedAction(); + //std::cout << "accepted x-qt-windows-mime proposed action" << std::endl; + } + else if (event->mimeData()->hasUrls()) + { + event->acceptProposedAction(); + //std::cout << "accepted proposed action" << std::endl; + } +} + +void DropEnabledListWidget::dragMoveEvent(QDragMoveEvent * event) +{ + if (event->mimeData()->hasFormat("application/x-qt-windows-mime;value=\"FileNameW\"")) + { + event->acceptProposedAction(); + //std::cout << "accepted proposed action" << std::endl; + } + else if (event->mimeData()->hasUrls()) + { + event->acceptProposedAction(); + //std::cout << "accepted proposed action" << std::endl; + } +} + +void DropEnabledListWidget::dropEvent(QDropEvent *event) +{ + //textBrowser->setPlainText(event->mimeData()->text()); + //mimeTypeCombo->clear(); + //mimeTypeCombo->addItems(event->mimeData()->formats()); + + QList urls = event->mimeData()->urls(); + for (int i = 0; i < urls.size(); ++i) + { + if (urls[i].isLocalFile()) + { + QListWidgetItem *item = new QListWidgetItem(QDir::toNativeSeparators(urls[i].toLocalFile())); + item->setFlags(item->flags() | Qt::ItemIsEditable); + this->addItem(item); + } + //std::cout << urls[i].fileName().toLatin1().data() << std::endl; + } + + //std::cout << .data() << std::endl; + //this->addItem(QString::fromWCharArray((wchar_t*)event->mimeData()->data("application/x-qt-windows-mime;value=\"FileNameW\"").data())); + + event->acceptProposedAction(); +} + diff --git a/dropenabledlistwidget.h b/dropenabledlistwidget.h new file mode 100644 index 0000000..c1bbb5b --- /dev/null +++ b/dropenabledlistwidget.h @@ -0,0 +1,24 @@ +#ifndef DROPENABLEDLISTWIDGET_H +#define DROPENABLEDLISTWIDGET_H + +#include + +class DropEnabledListWidget : public QListWidget +{ + Q_OBJECT +public: + explicit DropEnabledListWidget(QWidget *parent = 0); + + + +signals: + +public slots: + +protected: + virtual void dragEnterEvent(QDragEnterEvent *event); + void dragMoveEvent(QDragMoveEvent * event); + virtual void dropEvent(QDropEvent *event); +}; + +#endif // DROPENABLEDLISTWIDGET_H diff --git a/ignoredfilesdialog.cpp b/ignoredfilesdialog.cpp new file mode 100644 index 0000000..00ab431 --- /dev/null +++ b/ignoredfilesdialog.cpp @@ -0,0 +1,53 @@ +#include "ignoredfilesdialog.h" +#include "ui_ignoredfilesdialog.h" +#include "addignoredfiledialog.h" + +IgnoredFilesDialog::IgnoredFilesDialog(QWidget *parent, QStringList ignore_list) : + QDialog(parent), + ui(new Ui::IgnoredFilesDialog) +{ + ui->setupUi(this); + for (int i = 0; i < ignore_list.size(); ++i) + { + QListWidgetItem *item = new QListWidgetItem(ignore_list[i]); + item->setFlags(item->flags() | Qt::ItemIsEditable); + ui->ignore_list->addItem(item); + } + //ui->ignore_list->insertItems(0, ignore_list); +} + +IgnoredFilesDialog::~IgnoredFilesDialog() +{ + delete ui; +} + +QStringList IgnoredFilesDialog::ignoreList() const +{ + QStringList result; + for (int i = 0; i < ui->ignore_list->count(); ++i) + { + result.append(ui->ignore_list->item(i)->text()); + } + return result; +} + +void IgnoredFilesDialog::on_add_clicked() +{ + AddIgnoredFileDialog dialog(this); + //connect(dialog_, SIGNAL(accepted()), this, SLOT(on_addignoredfiledialog_accepted())); + if (QDialog::Accepted == dialog.exec()) + { + if (!dialog.filename().isEmpty()) + { + QListWidgetItem *item = new QListWidgetItem(dialog.filename()); + item->setFlags(item->flags() | Qt::ItemIsEditable); + ui->ignore_list->addItem(item); + //ui->ignore_list->addItem(dialog.filename()); + } + } +} + +void IgnoredFilesDialog::on_remove_clicked() +{ + delete ui->ignore_list->takeItem(ui->ignore_list->currentRow()); +} diff --git a/ignoredfilesdialog.h b/ignoredfilesdialog.h new file mode 100644 index 0000000..b8141c2 --- /dev/null +++ b/ignoredfilesdialog.h @@ -0,0 +1,31 @@ +#ifndef IGNOREDFILESDIALOG_H +#define IGNOREDFILESDIALOG_H + +#include + +class AddIgnoredFileDialog; + +namespace Ui { +class IgnoredFilesDialog; +} + +class IgnoredFilesDialog : public QDialog +{ + Q_OBJECT + +public: + explicit IgnoredFilesDialog(QWidget *parent, QStringList ignore_list); + ~IgnoredFilesDialog(); + + QStringList ignoreList() const; + +private slots: + void on_add_clicked(); + + void on_remove_clicked(); + +private: + Ui::IgnoredFilesDialog *ui; +}; + +#endif // IGNOREDFILESDIALOG_H diff --git a/ignoredfilesdialog.ui b/ignoredfilesdialog.ui new file mode 100644 index 0000000..5dddb70 --- /dev/null +++ b/ignoredfilesdialog.ui @@ -0,0 +1,121 @@ + + + IgnoredFilesDialog + + + + 0 + 0 + 413 + 151 + + + + List of Files to Ignore when Displaying Conflicted Files + + + + + + <html><head/><body><p>Drop files here. Use double click to edit items</p></body></html> + + + + + + + + + + 31 + 16777215 + + + + - + + + + + + + + 31 + 16777215 + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + DropEnabledListWidget + QListWidget +
dropenabledlistwidget.h
+
+
+ + + + buttonBox + accepted() + IgnoredFilesDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + IgnoredFilesDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + +
diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..96577a5 --- /dev/null +++ b/main.cpp @@ -0,0 +1,10 @@ +#include "vpkcompare.h" +#include + +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + VpkCompare w; + w.show(); + return a.exec(); +} diff --git a/vpkcompare.cpp b/vpkcompare.cpp new file mode 100644 index 0000000..31aa18c --- /dev/null +++ b/vpkcompare.cpp @@ -0,0 +1,374 @@ +#include "vpkcompare.h" + +#include + +#include +#include +#include +#include +#include +#include + +#include "ignoredfilesdialog.h" + +VpkCompare::VpkCompare(QWidget *parent, Qt::WindowFlags flags) + : QMainWindow(parent, flags), + vpkexe_process_(NULL), + vpkfile_number_(0) +{ + ui.setupUi(this); + + vpkexe_process_ = new QProcess(this); + bool b = connect(vpkexe_process_, + SIGNAL(finished(int, QProcess::ExitStatus)), + this, + SLOT(onVpkExeProcessFinished(int, QProcess::ExitStatus))); + Q_ASSERT(b); + + QSettings settings("settings.ini", QSettings::IniFormat, this); + QString vpkexe = "E:\\Games\\Steam\\SteamApps\\common\\nuclear dawn\\bin" + "\\vpk.exe"; + ui.vpkexe_path->setText(settings.value("VPKEXE_PATH", vpkexe).toString()); + this->resize(settings.value("SIZE", + QSize(this->width(), this->height())).toSize()); +} + +VpkCompare::~VpkCompare() +{ + QSettings settings("settings.ini", QSettings::IniFormat, this); + settings.setValue("VPKEXE_PATH", ui.vpkexe_path->text()); + settings.setValue("SIZE", this->size()); +} + +void VpkCompare::on_browse_vpkexe_clicked() +{ + QString default_dir_path;; + QFileInfo fi(ui.vpkexe_path->text()); + if (fi.exists()) + { + default_dir_path = fi.absolutePath(); + } + else + { + default_dir_path = QDir::homePath(); + } + + QFileDialog d; + QString path = d.getOpenFileName(this, tr("Select vpk.exe file"), + default_dir_path, + tr("Executable (vpk.exe);;All files (*.*)")); + + if (!path.isEmpty()) + ui.vpkexe_path->setText(QDir::toNativeSeparators(path)); +} + +void VpkCompare::on_addVpkButton_clicked() +{ + QString default_dir_path = QDir::homePath(); + + QFileDialog d; + QStringList files = d.getOpenFileNames(this, tr("Select one or more vpk " + "files"), default_dir_path, tr("VPK files (*.vpk);;All files (*.*)")); + + for (int i = 0; i < files.size(); ++i) + { + if (!files[i].isEmpty() && QFile::exists(files[i])) + ui.vpkListWidget->addItem(QDir::toNativeSeparators(files[i])); + } +} + +void VpkCompare::on_removeVpkButton_clicked() +{ + delete ui.vpkListWidget->takeItem(ui.vpkListWidget->currentRow()); +} + +void VpkCompare::on_compare_clicked() +{ + if (ui.vpkListWidget->count() < 2) + { + QMessageBox::warning(this, tr("Not enough VPK files added"), + tr("Please add 2 or more VPK files for " + "comparison. Aborting ")); + return; + } + + QStringList check_list; + check_list << ui.vpkexe_path->text(); + for (int i = 0; i < ui.vpkListWidget->count(); ++i) + { + check_list << ui.vpkListWidget->item(i)->text(); + } + + if (!filesExist(check_list)) + { + return; + } + + runVpkExe(); +} + +void VpkCompare::on_ignore_clicked() +{ + IgnoredFilesDialog dlg(this, ignore_list_); + if (QDialog::Accepted == dlg.exec()) + { + ignore_list_ = dlg.ignoreList(); + if (ignore_list_.empty()) + { + QFont font = ui.ignore->font(); + font.setBold(false); + ui.ignore->setFont(font); + } + else + { + QFont font = ui.ignore->font(); + font.setBold(true); + ui.ignore->setFont(font); + } + } +} + +void VpkCompare::onVpkExeProcessFinished(int exitCode, + QProcess::ExitStatus exitStatus) +{ + Q_ASSERT(vpkexe_process_); + QByteArray standard_output = vpkexe_process_->readAllStandardOutput(); + QByteArray error_output = vpkexe_process_->readAllStandardError(); + + QString status = exitStatus == QProcess::NormalExit ? "Normal" : "Crashed"; + + log(tr("VPK.exe finished with status: %1, exit code: %2").arg(status).arg(exitCode)); + //log(tr("VPK.exe standard output: ")); + //log(standard_output); + if (error_output.size() > 0) + { + log(tr("VPK.exe error output: ")); + log(error_output); + } + + // parse output + vpk_files_info_.push_back(VpkFileInfo()); + VpkFileInfo &fi = vpk_files_info_[vpk_files_info_.size() - 1]; + fi.vpkfilepath = ui.vpkListWidget->item(vpkfile_number_ - 1)->text(); + + QTextStream vpk_output_stream(standard_output, QIODevice::ReadOnly); + while (!vpk_output_stream.atEnd()) + { + QString line = vpk_output_stream.readLine(); + QString::SectionFlag flag = QString::SectionSkipEmpty; + fi.pathes.push_back(line.section(' ', 0, 0, flag)); + fi.crcs.push_back(line.section(' ', 1, 1, flag).remove(0, 6)); + } + + // run next vpk.exe or comparison if all vpk files were parsed + if (vpkfile_number_ < ui.vpkListWidget->count()) + runVpkExe(); + else + { + for (int i = 0; i < vpk_files_info_.size() - 1; ++i) + { + for (int k = i + 1; k < vpk_files_info_.size(); ++k) + { + compare(vpk_files_info_[i], vpk_files_info_[k]); + } + } + + // perform cleanup + vpk_files_info_.clear(); + vpkfile_number_ = 0; + } +} + + +/* +void VpkCompare::on_embed_clicked() +{ + QStringList check_list; + check_list << ui.vpkexe_path->text() << ui.bspfile_path->text() << ui.data_folder_path->text(); + if (!filesExist(check_list)) + { + return; + } + + QFile bspzip_filelist_file(QDir::homePath() + "/bspzip_filelist.txt"); + log(tr("Opening file for writing: %1").arg( bspzip_filelist_file.fileName() )); + if (!bspzip_filelist_file.open(QIODevice::WriteOnly | QIODevice::Text)) + { + log(tr("Failed to open file for writing: %1").arg(bspzip_filelist_file.fileName())); + return; + } + + QDir data_dir( ui.data_folder_path->text(), "", QDir::Name | QDir::IgnoreCase, QDir::AllDirs | QDir::Files| QDir::NoDotAndDotDot ); + getDataFolderFiles(data_dir, &data_folder_files_); + + if (data_folder_files_.isEmpty()) + { + log(tr("No files found for embedding in directory: %1 Aborting...").arg(data_dir.absolutePath())); + return; + } + + log("Adding file list into filelist.txt..."); + + QTextStream out(&bspzip_filelist_file); + QString absolute_file_path; + Q_FOREACH(absolute_file_path, data_folder_files_) + { + absolute_file_path = QDir::toNativeSeparators(absolute_file_path); + QString relative_file_path = absolute_file_path.mid(ui.data_folder_path->text().size() + 1); + + log(relative_file_path); + log(absolute_file_path); + + // no quotes here as BSPZIP doesn't handle them + out << relative_file_path << "\n"; + out << absolute_file_path << "\n"; + } + data_folder_files_.clear(); // must be cleared! or we get bugs + + bspzip_filelist_file.close(); + log(tr("File bspzip_filelist.txt was successfully created")); + + QString bsp_file_path = QDir::toNativeSeparators(ui.bspfile_path->text()); + QString filelist_path = QDir::toNativeSeparators(bspzip_filelist_file.fileName()); + QStringList process_arguments; + process_arguments << "-addorupdatelist"; + process_arguments << bsp_file_path; + process_arguments << filelist_path; + process_arguments << bsp_file_path; + +// if (bspzip_process_) +// { +// delete bspzip_process_; +// } + + log(tr("Starting BSPZIP.EXE with following arguments")); + Q_FOREACH(QString argument, process_arguments) + { + log(argument); + } + + bspzip_process_->start(ui.vpkexe_path->text(), process_arguments); +} + +void VpkCompare::on_extract_clicked() +{ + QString bsp_file_path = QDir::toNativeSeparators(ui.bspfile_path->text()); + QString data_folder = QDir::toNativeSeparators(ui.data_folder_path->text()); + + QStringList check_list; + check_list << ui.vpkexe_path->text() << bsp_file_path << data_folder; + if (!filesExist(check_list)) + { + return; + } + + QStringList process_arguments; + process_arguments << "-extractfiles"; + process_arguments << bsp_file_path; + process_arguments << data_folder; + +// if (bspzip_process_) +// { +// delete bspzip_process_; +// } + +// bspzip_process_ = new QProcess(this); +// bool b = connect(bspzip_process_, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(onBspZipProcessFinished(int, QProcess::ExitStatus))); +// Q_ASSERT(b); + + bspzip_process_->start(ui.vpkexe_path->text(), process_arguments); +} +//*/ + +void VpkCompare::log(const QString &logstr) +{ + ui.console->appendPlainText(logstr); + //std::cout << logstr.toLatin1().constData() << std::endl; +} + + +bool VpkCompare::filesExist(const QStringList &files) +{ + Q_FOREACH(QString file_path, files) + { + QFileInfo fi(file_path); + if (!fi.exists()) + { + QMessageBox::warning(this, tr("File Not Found"), tr("The file doesn't exist: %1. Aborting ").arg(file_path)); + return false; + } + } + return true; +} + +void VpkCompare::runVpkExe() +{ + if (vpkfile_number_ >= ui.vpkListWidget->count()) + return; + + QStringList process_arguments; + process_arguments << "L"; + process_arguments << ui.vpkListWidget->item(vpkfile_number_)->text(); + ++vpkfile_number_; + + vpkexe_process_->start(ui.vpkexe_path->text(), process_arguments); +} + +void VpkCompare::compare(const VpkFileInfo &a, const VpkFileInfo &b) +{ + bool first_time = true; + bool check_crc = ui.crcCheckBox->isChecked(); + int conflict_counter = 0; + int same_crc_counter = 0; + int ignored_counter = 0; + + for (int i = 0; i < a.pathes.size(); ++i) + { + for (int k = 0; k < b.pathes.size(); ++k) + { + if (a.pathes[i] == b.pathes[k]) + { + if (check_crc) + { + // skip files with same crc + if (a.crcs[i] == b.crcs[k]) + { + ++same_crc_counter; + continue; + } + } + + // check ignores + if (ignore_list_.size() > 0) + { + bool must_be_ignored = false; + for (int q = 0; q < ignore_list_.size(); ++q) + { + QRegExp exp(ignore_list_.at(q), Qt::CaseInsensitive, QRegExp::Wildcard); + if (exp.exactMatch(a.pathes[i])) + { + ++ignored_counter; + must_be_ignored = true; + break; + } + } + if (must_be_ignored) + continue; + } + + if (first_time) + { + log(tr("Collisions between:\n %1 \n %2").arg(a.vpkfilepath).arg(b.vpkfilepath)); + first_time = false; + } + log(a.pathes[i]); + ++conflict_counter; + } + } + } + if (conflict_counter) + log(tr("%1 files conflicted, %2 with same crc, %3 " + "ignored").arg(conflict_counter).arg(same_crc_counter). + arg(ignored_counter)); +} + diff --git a/vpkcompare.h b/vpkcompare.h new file mode 100644 index 0000000..5e72929 --- /dev/null +++ b/vpkcompare.h @@ -0,0 +1,54 @@ +#ifndef BSPZIPGUI_H +#define BSPZIPGUI_H + +#include +#include +#include +#include + +#include "ui_vpkcompare.h" + +struct VpkFileInfo +{ + QVector pathes; // file path + QVector crcs; // file crc + QString vpkfilepath; // full path to vpk file +}; + +class VpkCompare : public QMainWindow +{ + Q_OBJECT + +public: + VpkCompare(QWidget *parent = 0, Qt::WindowFlags flags = 0); + ~VpkCompare(); + +public slots: + void on_browse_vpkexe_clicked(); + void on_addVpkButton_clicked(); + void on_removeVpkButton_clicked(); + void on_compare_clicked(); + void on_ignore_clicked(); + +// void on_embed_clicked(); +// void on_extract_clicked(); + + void onVpkExeProcessFinished(int exitCode, QProcess::ExitStatus exitStatus ); + + +private: + void log(const QString &logstr); +// void getDataFolderFiles(const QDir &dir, QStringList *file_list); + bool filesExist(const QStringList &files); + void runVpkExe(); + void compare(const VpkFileInfo &a, const VpkFileInfo &b); +// QStringList data_folder_files_; + QProcess *vpkexe_process_; + + Ui::VpkCompareClass ui; + int vpkfile_number_; + QVector vpk_files_info_; + QStringList ignore_list_; +}; + +#endif // BSPZIPGUI_H diff --git a/vpkcompare.ui b/vpkcompare.ui new file mode 100644 index 0000000..99f69e3 --- /dev/null +++ b/vpkcompare.ui @@ -0,0 +1,171 @@ + + + VpkCompareClass + + + + 0 + 0 + 518 + 412 + + + + VpkCompare 1.0 + + + + + + + + + <html><head/><body><p>Path to the vpk.exe file which will be used to examine the contents of VPK file</p></body></html> + + + + + + + Browse... + + + + + + + vpk.exe path + + + + + + + + + + 0 + 0 + + + + + 16777215 + 100 + + + + <html><head/><body><p>Drag and drop VPK files here or use + button</p></body></html> + + + QAbstractItemView::DropOnly + + + + + + + + + + 31 + 16777215 + + + + - + + + + + + + + 31 + 16777215 + + + + + + + + + + + + <html><head/><body><p>Check this to only show files which have different CRC checksum</p></body></html> + + + Check CRC + + + + + + + Add names of files which will be ignored and not diplayed as conflicted + + + Ignore... + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + <html><head/><body><p>Start the comparison process</p></body></html> + + + Compare + + + + + + + + + <html><head/><body><p>Output window</p></body></html> + + + + + + + + + + + + 0 + 0 + 518 + 21 + + + + + + + + DropEnabledListWidget + QListWidget +
dropenabledlistwidget.h
+
+
+ + +