├── .gitignore ├── CMakeLists.txt ├── README.md ├── clientinfo.cpp ├── clientinfo.h ├── config.h.in ├── createprogress.cpp ├── createprogress.h ├── createprogress.ui ├── doc └── screenshot.png ├── main.cpp ├── mainwindow.cpp ├── mainwindow.h ├── mainwindow.ui ├── optionsdialog.cpp ├── optionsdialog.h ├── optionsdialog.ui ├── outpreviewlistitem.cpp ├── outpreviewlistitem.h ├── par2calc.cpp ├── par2calc.h ├── par2outinfo.cpp ├── par2outinfo.h ├── parparclient.cpp ├── parparclient.h ├── parpargui_en_GB.ts ├── progressdialog.cpp ├── progressdialog.h ├── res.qrc ├── res ├── icon.ico └── parpargui.rc ├── settings.cpp ├── settings.h ├── sizeedit.cpp ├── sizeedit.h ├── slicecountspinbox.cpp ├── slicecountspinbox.h ├── sourcefile.cpp ├── sourcefile.h ├── sourcefileframe.cpp ├── sourcefileframe.h ├── sourcefilelistitem.cpp ├── sourcefilelistitem.h ├── util.cpp └── util.h /.gitignore: -------------------------------------------------------------------------------- 1 | # This file is used to ignore files which are generated 2 | # ---------------------------------------------------------------------------- 3 | 4 | *~ 5 | *.autosave 6 | *.a 7 | *.core 8 | *.moc 9 | *.o 10 | *.obj 11 | *.orig 12 | *.rej 13 | *.so 14 | *.so.* 15 | *_pch.h.cpp 16 | *_resource.rc 17 | *.qm 18 | .#* 19 | *.*# 20 | core 21 | !core/ 22 | tags 23 | .DS_Store 24 | .directory 25 | *.debug 26 | Makefile* 27 | *.prl 28 | *.app 29 | moc_*.cpp 30 | ui_*.h 31 | qrc_*.cpp 32 | Thumbs.db 33 | *.res 34 | /.qmake.cache 35 | /.qmake.stash 36 | 37 | # qtcreator generated files 38 | *.pro.user* 39 | CMakeLists.txt.user* 40 | 41 | # xemacs temporary files 42 | *.flc 43 | 44 | # Vim temporary files 45 | .*.swp 46 | 47 | # Visual Studio generated files 48 | *.ib_pdb_index 49 | *.idb 50 | *.ilk 51 | *.pdb 52 | *.sln 53 | *.suo 54 | *.vcproj 55 | *vcproj.*.*.user 56 | *.ncb 57 | *.sdf 58 | *.opensdf 59 | *.vcxproj 60 | *vcxproj.* 61 | 62 | # MinGW generated files 63 | *.Debug 64 | *.Release 65 | 66 | # Python byte code 67 | *.pyc 68 | 69 | # Binaries 70 | # -------- 71 | *.dll 72 | *.exe 73 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | 3 | project(parpargui VERSION 0.4 LANGUAGES CXX) 4 | 5 | set(CMAKE_INCLUDE_CURRENT_DIR ON) 6 | 7 | set(CMAKE_AUTOUIC ON) 8 | set(CMAKE_AUTOMOC ON) 9 | set(CMAKE_AUTORCC ON) 10 | 11 | set(CMAKE_CXX_STANDARD 11) 12 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 13 | 14 | if(CMAKE_BUILD_TYPE STREQUAL "Release") 15 | # our Release builds are statically linked 16 | list(PREPEND CMAKE_FIND_LIBRARY_SUFFIXES .a .lib) 17 | #set(CMAKE_EXE_LINKER_FLAGS "-static") 18 | endif() 19 | configure_file("config.h.in" "config.h") 20 | 21 | find_package(QT NAMES Qt6 Qt5 COMPONENTS Widgets REQUIRED ) 22 | find_package(QT NAMES Qt6 Qt5 OPTIONAL_COMPONENTS LinguistTools) 23 | find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Widgets REQUIRED) 24 | find_package(Qt${QT_VERSION_MAJOR} OPTIONAL_COMPONENTS LinguistTools) 25 | 26 | set(TS_FILES parpargui_en_GB.ts) 27 | 28 | set(PROJECT_SOURCES 29 | main.cpp 30 | mainwindow.cpp 31 | mainwindow.h 32 | mainwindow.ui 33 | optionsdialog.h optionsdialog.cpp optionsdialog.ui 34 | sizeedit.h sizeedit.cpp 35 | settings.h settings.cpp 36 | util.cpp util.h 37 | par2calc.h par2calc.cpp 38 | createprogress.h createprogress.cpp createprogress.ui 39 | progressdialog.h progressdialog.cpp 40 | slicecountspinbox.h slicecountspinbox.cpp 41 | par2outinfo.h par2outinfo.cpp 42 | outpreviewlistitem.h outpreviewlistitem.cpp 43 | sourcefilelistitem.h sourcefilelistitem.cpp 44 | sourcefile.h sourcefile.cpp 45 | sourcefileframe.h sourcefileframe.cpp 46 | clientinfo.h clientinfo.cpp 47 | parparclient.h parparclient.cpp 48 | res.qrc 49 | ${TS_FILES} 50 | ) 51 | 52 | set(APP_ICON_RESOURCE_WINDOWS "${CMAKE_CURRENT_SOURCE_DIR}/res/parpargui.rc") 53 | 54 | if(${QT_VERSION_MAJOR} GREATER_EQUAL 6) 55 | qt_add_executable(parpargui 56 | MANUAL_FINALIZATION 57 | ${PROJECT_SOURCES} 58 | ${APP_ICON_RESOURCE_WINDOWS} 59 | ) 60 | # Define target properties for Android with Qt 6 as: 61 | # set_property(TARGET parpargui APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR 62 | # ${CMAKE_CURRENT_SOURCE_DIR}/android) 63 | # For more information, see https://doc.qt.io/qt-6/qt-add-executable.html#target-creation 64 | 65 | if(Qt6LinguistTools_FOUND) 66 | qt_create_translation(QM_FILES ${CMAKE_SOURCE_DIR} ${TS_FILES}) 67 | endif() 68 | else() 69 | if(ANDROID) 70 | add_library(parpargui SHARED 71 | ${PROJECT_SOURCES} 72 | ) 73 | # Define properties for Android with Qt 5 after find_package() calls as: 74 | # set(ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android") 75 | else() 76 | add_executable(parpargui 77 | ${PROJECT_SOURCES} 78 | ${APP_ICON_RESOURCE_WINDOWS} 79 | ) 80 | endif() 81 | 82 | if(Qt5LinguistTools_FOUND) 83 | qt5_create_translation(QM_FILES ${CMAKE_SOURCE_DIR} ${TS_FILES}) 84 | endif() 85 | endif() 86 | 87 | target_link_libraries(parpargui PRIVATE Qt${QT_VERSION_MAJOR}::Widgets) 88 | 89 | set_target_properties(parpargui PROPERTIES 90 | MACOSX_BUNDLE_GUI_IDENTIFIER my.example.com 91 | MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} 92 | MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR} 93 | ) 94 | 95 | # Release desirables 96 | if(CMAKE_BUILD_TYPE STREQUAL "Release") 97 | # TODO: proper LTO with Qt source 98 | set_property(TARGET ${PROJECT_NAME} PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE) 99 | if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") 100 | add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD 101 | COMMAND ${CMAKE_STRIP} ${PROJECT_NAME}${CMAKE_EXECUTABLE_SUFFIX}) 102 | endif() 103 | endif() 104 | set_property(TARGET ${PROJECT_NAME} PROPERTY WIN32_EXECUTABLE true) 105 | 106 | if(QT_VERSION_MAJOR EQUAL 6) 107 | qt_finalize_executable(parpargui) 108 | endif() 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ParPar GUI 2 | ====== 3 | 4 | ParPar GUI is a graphical front-end to the [command-line ParPar application](https://github.com/animetosho/ParPar), which is a high performance, multi-threaded [PAR2](https://en.wikipedia.org/wiki/Parchive) *creation-only* tool (verify/repair unsupported). The GUI exposes most of ParPar’s features, saving users from having to learn the command-line flags for most functionality. 5 | 6 | ![Screenshot](doc/screenshot.png) 7 | 8 | # Download 9 | 10 | See the [Releases page](https://github.com/animetosho/ParParGUI/releases). 11 | 12 | Compiling from Source 13 | ======================= 14 | 15 | ParPar GUI is built using the C++/Qt framework. It should be compatible with both Qt5 and Qt6 versions 16 | As such, you’ll need C++ build tools and the QtWidgets library (or the whole Qt SDK), then compile via CMake. 17 | 18 | The following are sample commands for Debian Linux: 19 | 20 | ```bash 21 | apt-get install build-essential cmake qtbase5-dev 22 | cmake . # in the directory of the source code 23 | make 24 | # result should be a binary './parpargui' 25 | ``` 26 | 27 | Alternatively, you can [install Qt Creator](https://www.qt.io/download-qt-installer), run it and load the *CMakeLists.txt* file as a project, and compile that. 28 | 29 | Once built, you’ll need to ensure ParPar GUI can find ParPar itself. By default, it’ll automatically search the current directory for ParPar binaries, or the *bin/parpar.js* file along with the Node.js interpreter. Alternatively, the correct paths can be configured via the Options dialog. 30 | See [building info](https://github.com/animetosho/ParPar#install-from-source) on ParPar’s side for help on obtaining ParPar. 31 | 32 | License 33 | ======= 34 | 35 | This code is Public Domain or [CC0](https://creativecommons.org/publicdomain/zero/1.0/legalcode) (or equivalent) if PD isn’t recognised. 36 | 37 | See Also 38 | ======== 39 | 40 | [MultiPar](https://github.com/Yutaka-Sawada/MultiPar) for a full featured (create+repair) PAR2 client (Windows only). 41 | -------------------------------------------------------------------------------- /clientinfo.cpp: -------------------------------------------------------------------------------- 1 | #include "clientinfo.h" 2 | #include "settings.h" 3 | #include 4 | #include 5 | 6 | ClientInfo::ClientInfo(QObject* parent) : QObject(parent), parpar(parent) 7 | { 8 | connect(&parpar, &ParParClient::failed, this, &ClientInfo::failed); 9 | // treat failing to start, like a crash 10 | connect(&parpar, &ParParClient::output, this, [this](const QJsonObject& doc) { 11 | _version = doc.value("version").toString(); 12 | _creator = doc.value("creator").toString(); 13 | 14 | emit updated(); 15 | }); 16 | 17 | // default values 18 | _version = "0.0.0"; 19 | _creator = "ParPar v0.0.0 [https://animetosho.org/app/parpar]"; 20 | } 21 | 22 | void ClientInfo::refresh() 23 | { 24 | parpar.run({"--version"}); 25 | } 26 | 27 | -------------------------------------------------------------------------------- /clientinfo.h: -------------------------------------------------------------------------------- 1 | #ifndef CLIENTINFO_H 2 | #define CLIENTINFO_H 3 | 4 | #include "parparclient.h" 5 | 6 | class ClientInfo : public QObject 7 | { 8 | Q_OBJECT 9 | 10 | ClientInfo(QObject* parent = nullptr); 11 | ParParClient parpar; 12 | QString _version; 13 | QString _creator; 14 | 15 | signals: 16 | void updated(); 17 | void failed(const QString& error); 18 | 19 | public: 20 | static ClientInfo& getInstance() 21 | { 22 | static ClientInfo instance; 23 | return instance; 24 | } 25 | 26 | static inline QString& version() 27 | { 28 | return getInstance()._version; 29 | } 30 | static inline QString& creator() 31 | { 32 | return getInstance()._creator; 33 | } 34 | 35 | void refresh(); 36 | }; 37 | 38 | #endif // CLIENTINFO_H 39 | -------------------------------------------------------------------------------- /config.h.in: -------------------------------------------------------------------------------- 1 | #ifndef CONFIG_H_IN 2 | #define CONFIG_H_IN 3 | 4 | #define PROJECT_NAME "@PROJECT_NAME@" 5 | #define PROJECT_VERSION "@PROJECT_VERSION@" 6 | #define PROJECT_VERSION_MAJOR @PROJECT_VERSION_MAJOR@ 7 | #define PROJECT_VERSION_MINOR @PROJECT_VERSION_MINOR@ 8 | #define PROJECT_VERSION_PATCH @PROJECT_VERSION_PATCH@ 9 | #define PROJECT_VERSION_TWEAK @PROJECT_VERSION_TWEAK@ 10 | 11 | #endif // CONFIG_H_IN 12 | -------------------------------------------------------------------------------- /createprogress.cpp: -------------------------------------------------------------------------------- 1 | #include "createprogress.h" 2 | #include "ui_createprogress.h" 3 | #include "settings.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #ifdef Q_OS_WINDOWS 10 | #define WIN32_LEAN_AND_MEAN 11 | #include 12 | // from https://stackoverflow.com/a/11010508/459150 13 | typedef LONG (NTAPI *NtSuspendResumeProcess)(IN HANDLE ProcessHandle); 14 | #elif defined(Q_OS_UNIX) 15 | #include 16 | #include 17 | #endif 18 | 19 | #ifdef Q_OS_WINDOWS 20 | #define WIN_PROGRESS(f, ...) if(pTL) pTL->f(hWnd, __VA_ARGS__) 21 | #else 22 | #define WIN_PROGRESS(f, ...) (void)0 23 | #endif 24 | 25 | void CreateProgress::init() 26 | { 27 | isCancelled = false; 28 | 29 | ui->lblTime->setText(""); 30 | ui->txtMessage->hide(); 31 | this->setFixedHeight(this->layout()->sizeHint().height()); 32 | this->setSizeGripEnabled(false); 33 | 34 | elapsedTimeBase = 0; 35 | stdoutBuffer.clear(); 36 | ui->progressBar->setMaximum(0); 37 | WIN_PROGRESS(SetProgressState, TBPF_INDETERMINATE); 38 | 39 | sysTrayIcon.hide(); 40 | } 41 | 42 | CreateProgress::CreateProgress(QWidget *parent) : 43 | QDialog(parent, Qt::Dialog | Qt::WindowTitleHint | Qt::WindowMinimizeButtonHint | Qt::WindowCloseButtonHint) 44 | , ui(new Ui::CreateProgress) 45 | , sysTrayIcon(this) 46 | { 47 | ui->setupUi(this); 48 | 49 | connect(&parpar, &QProcess::readyReadStandardOutput, this, &CreateProgress::gotStdout); 50 | connect(&parpar, QOverload::of(&QProcess::finished), this, &CreateProgress::finished); 51 | 52 | connect(&timer, &QTimer::timeout, this, &CreateProgress::timerTick); 53 | timer.setInterval(1000); 54 | 55 | #if !defined(Q_OS_WINDOWS) && !defined(Q_OS_UNIX) 56 | ui->btnBackground->hide(); 57 | ui->btnPause->hide(); 58 | #endif 59 | 60 | #ifdef Q_OS_WINDOWS 61 | HRESULT hr = CoCreateInstance(CLSID_TaskbarList, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pTL)); 62 | if (SUCCEEDED(hr)) { 63 | hr = pTL->HrInit(); 64 | if (!SUCCEEDED(hr)) { 65 | pTL->Release(); 66 | pTL = nullptr; 67 | } 68 | } else 69 | pTL = nullptr; 70 | // only the parent window has a taskbar icon, so need to attach it there 71 | // TODO: probably better to try and search for the QMainWindow instance rather than assume the parent 72 | hWnd = reinterpret_cast(reinterpret_cast(this->parent())->winId()); 73 | #endif 74 | 75 | // TODO: get application icon instead 76 | QPixmap p(16, 16); 77 | p.fill(Qt::transparent); 78 | sysTrayIcon.setIcon(QIcon(p)); 79 | sysTrayIcon.setToolTip(tr("ParPar notification icon")); 80 | connect(&sysTrayIcon, &QSystemTrayIcon::messageClicked, this, &CreateProgress::notificationClicked); 81 | connect(&sysTrayIcon, &QSystemTrayIcon::activated, this, [=](QSystemTrayIcon::ActivationReason reason) { 82 | this->notificationClicked(); 83 | }); 84 | 85 | init(); 86 | 87 | const auto& settings = Settings::getInstance(); 88 | ui->btnBackground->setChecked(settings.runBackground()); 89 | ui->btnNotify->setChecked(settings.runNotification()); 90 | 91 | // TODO: consider adding decimal percentage bar: https://www.qtcentre.org/threads/70885-QProgressBar-with-in-decimal?s=bdfd413197898978b8ef6ea933c3e67e&p=307578#post307578 92 | } 93 | 94 | void CreateProgress::run(QStringList args, const QHash& env, const QByteArray& inFiles, const QString& baseOutput_, const QString& outDir_, const QStringList& outFiles_) 95 | { 96 | baseOutput = baseOutput_; 97 | outDir = outDir_; 98 | outFiles = outFiles_; 99 | 100 | auto cmd = Settings::getInstance().parparBin(); 101 | parpar.setProgram(cmd[0]); 102 | if(cmd.length() > 1) 103 | args.prepend(cmd[1]); 104 | 105 | args << "--quiet" << "--json" << "--progress" << "stdout" << "--input-file0" << "-"; 106 | parpar.setArguments(args); 107 | 108 | if(!env.empty()) { 109 | auto procEnv = QProcessEnvironment::systemEnvironment(); 110 | auto it = QHashIterator(env); 111 | while(it.hasNext()) { 112 | it.next(); 113 | procEnv.insert(it.key(), it.value()); 114 | } 115 | parpar.setProcessEnvironment(procEnv); 116 | } 117 | 118 | this->setWindowTitle(tr("ParPar - Creating %2") 119 | .arg(baseOutput)); 120 | 121 | // send stdin when process starts 122 | connect(&parpar, &QProcess::started, this, [=]() { 123 | ui->lblStatus->setText(""); 124 | ui->btnPause->setEnabled(true); 125 | 126 | if(ui->btnBackground->isChecked()) 127 | this->on_btnBackground_clicked(); 128 | 129 | this->parpar.write(inFiles); 130 | this->parpar.closeWriteChannel(); 131 | }); 132 | // TODO: disconnect this signal after start? 133 | // - or see if it's even needed 134 | connect(&parpar, &QProcess::errorOccurred, this, [this](QProcess::ProcessError err) { 135 | if(err == QProcess::FailedToStart) { 136 | // it seems that this is often called too soon - before the window has set up 137 | this->ended(tr("Failed to execute ParPar"), false); 138 | /* 139 | this->isCancelled = true; 140 | this->close(); 141 | QMessageBox::critical(reinterpret_cast(this->parent()), tr("Create PAR2"), tr("ParPar failed to launch")); 142 | */ 143 | } 144 | // for other errors let finished handler handle it 145 | }); 146 | 147 | elapsedTime.start(); 148 | timer.start(); 149 | timerTick(); 150 | parpar.start(); 151 | } 152 | 153 | void CreateProgress::gotStdout() 154 | { 155 | stdoutBuffer.append(parpar.readAllStandardOutput()); 156 | 157 | // find end of JSON message 158 | int p = stdoutBuffer.indexOf("\n}"); 159 | while(p > 0) { 160 | // extract message 161 | const auto doc = QJsonDocument::fromJson(stdoutBuffer.left(p + 2)); 162 | stdoutBuffer = stdoutBuffer.mid(p + 2); 163 | p = stdoutBuffer.indexOf("\n}"); 164 | 165 | if(!doc.isNull() && !doc.isEmpty()) { 166 | const auto msg = doc.object().toVariantHash(); 167 | const auto type = msg.value("type", "").toString(); 168 | if(type == "progress") { 169 | bool ok; 170 | float progress = msg.value("progress_percent", -1).toFloat(&ok); 171 | if(progress >= 0 && ok) { 172 | ui->progressBar->setMaximum(10000); 173 | ui->progressBar->setValue(progress * 100); 174 | WIN_PROGRESS(SetProgressState, TBPF_NORMAL); 175 | WIN_PROGRESS(SetProgressValue, progress, 10000); 176 | 177 | updateTitlePerc(); 178 | } 179 | } 180 | } 181 | } 182 | } 183 | 184 | void CreateProgress::updateTitlePerc() 185 | { 186 | float perc = ui->progressBar->value(); 187 | perc /= 100; 188 | this->setWindowTitle(tr("ParPar - %1% creating %2").arg( 189 | QLocale().toString(perc, 'f', 2), 190 | baseOutput 191 | )); 192 | } 193 | 194 | CreateProgress::~CreateProgress() 195 | { 196 | delete ui; 197 | #ifdef Q_OS_WINDOWS 198 | WIN_PROGRESS(SetProgressState, TBPF_NOPROGRESS); 199 | if(pTL) pTL->Release(); 200 | #endif 201 | } 202 | 203 | void CreateProgress::finished(int exitCode, QProcess::ExitStatus exitStatus) 204 | { 205 | if(isCancelled) return; 206 | if(exitStatus != QProcess::ExitStatus::NormalExit) { 207 | this->ended(tr("ParPar process crashed or failed to start"), true); 208 | } else if(exitCode != 0) { 209 | ended(tr("PAR2 creation failed (exit code: %1)").arg(exitCode), true); 210 | } else { 211 | ended("", true); 212 | } 213 | } 214 | 215 | void CreateProgress::ended(const QString &error, bool showOutput) 216 | { 217 | //gotStdout(); // flush remaining stdout 218 | 219 | bool success = error.isEmpty(); 220 | ui->progressBar->setEnabled(false); 221 | ui->lblTime->setEnabled(false); 222 | ui->btnPause->setEnabled(false); 223 | if(success) { 224 | ui->lblStatus->setText(tr("PAR2 successfully created")); 225 | ui->progressBar->setMaximum(10000); 226 | ui->progressBar->setValue(10000); 227 | this->setWindowTitle(tr("ParPar - Finished creating %1").arg(baseOutput)); 228 | WIN_PROGRESS(SetProgressState, TBPF_NOPROGRESS); 229 | } else { 230 | ui->lblStatus->setText(error); 231 | this->setWindowTitle(tr("ParPar - Failed creating %1").arg(baseOutput)); 232 | WIN_PROGRESS(SetProgressState, TBPF_ERROR); 233 | 234 | ui->buttonBox->setStandardButtons(QDialogButtonBox::Cancel | QDialogButtonBox::Retry); 235 | } 236 | ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Close")); 237 | if(ui->progressBar->maximum() == 0) { 238 | // process never started - stop scrolling progress bar 239 | ui->progressBar->setMaximum(1); 240 | ui->progressBar->setTextVisible(false); 241 | WIN_PROGRESS(SetProgressState, TBPF_NOPROGRESS); 242 | } else { 243 | quint64 time = elapsedTimeBase+elapsedTime.elapsed(); 244 | ui->lblTime->setText(tr("Time taken: %1:%2:%3%4%5") 245 | .arg(time/3600000, 2, 10, QChar('0')) 246 | .arg((time/60000)%60, 2, 10, QChar('0')) 247 | .arg((time/1000)%60, 2, 10, QChar('0')) 248 | .arg(QLocale().decimalPoint()) 249 | .arg(time%1000, 3, 10, QChar('0'))); 250 | } 251 | ui->btnPause->setChecked(false); 252 | 253 | timer.stop(); 254 | 255 | if(isCancelled) return; 256 | 257 | bool hasOutput = false; 258 | if(showOutput) { 259 | auto output = QString::fromUtf8(parpar.readAllStandardError()).trimmed(); 260 | if(!output.isEmpty()) { 261 | hasOutput = true; 262 | ui->txtMessage->setPlainText(output); 263 | ui->txtMessage->show(); 264 | 265 | this->setMinimumHeight(this->minimumHeight() + ui->txtMessage->minimumHeight() + this->layout()->spacing()); 266 | this->setMaximumHeight(INT_MAX); 267 | this->resize(this->width(), this->layout()->sizeHint().height()); 268 | 269 | this->setSizeGripEnabled(true); 270 | 271 | if(success) 272 | ui->lblStatus->setText(tr("PAR2 created with warnings")); 273 | } 274 | } 275 | 276 | if(!success) deleteOutput(); 277 | ui->buttonBox->button(QDialogButtonBox::Cancel)->setFocus(); 278 | 279 | if(ui->btnNotify->isChecked()) { 280 | if(QSystemTrayIcon::supportsMessages()) { 281 | sysTrayIcon.show(); 282 | sysTrayIcon.showMessage(this->windowTitle(), ui->lblStatus->text(), success ? QSystemTrayIcon::Information : QSystemTrayIcon::Warning); 283 | } else { 284 | QApplication::beep(); 285 | } 286 | QApplication::alert(this); 287 | } 288 | 289 | if(!hasOutput && success && Settings::getInstance().runClose()) 290 | this->close(); 291 | } 292 | 293 | void CreateProgress::deleteOutput() 294 | { 295 | QString failed; 296 | QDir dir(outDir); 297 | for(const auto& name : outFiles) { 298 | if(dir.exists(name)) { 299 | if(!dir.remove(name)) 300 | failed += QString("\r\n") + name; 301 | } 302 | } 303 | if(!failed.isEmpty()) { 304 | QMessageBox::critical(this, tr("Delete Output Files"), tr("Failed to delete the following incomplete file(s):%1") 305 | .arg(failed)); 306 | } 307 | } 308 | 309 | void CreateProgress::timerTick() 310 | { 311 | quint64 time = (elapsedTimeBase+elapsedTime.elapsed() + 500) / 1000; 312 | ui->lblTime->setText(tr("Elapsed time: %1:%2:%3") 313 | .arg(time/3600, 2, 10, QChar('0')) 314 | .arg((time/60)%60, 2, 10, QChar('0')) 315 | .arg(time%60, 2, 10, QChar('0'))); 316 | } 317 | 318 | void CreateProgress::on_CreateProgress_rejected() 319 | { 320 | if(parpar.state() == QProcess::Running) { 321 | isCancelled = true; 322 | // TODO: look at using .terminate() for non-Windows 323 | parpar.kill(); 324 | ended(tr("Cancelled"), false); 325 | parpar.waitForFinished(1000); // try to avoid warning of destroying QProcess whilst still active 326 | deleteOutput(); 327 | 328 | if(Settings::getInstance().runClose()) 329 | this->close(); 330 | } 331 | } 332 | 333 | 334 | void CreateProgress::on_btnBackground_clicked() 335 | { 336 | bool isBackground = ui->btnBackground->isChecked(); 337 | 338 | if(parpar.state() == QProcess::Running) { 339 | #ifdef Q_OS_WINDOWS 340 | static DWORD normPrio = 0x7fff; 341 | HANDLE hPP = OpenProcess(PROCESS_SET_INFORMATION | PROCESS_QUERY_LIMITED_INFORMATION, FALSE, parpar.processId()); 342 | if(hPP == NULL) { 343 | ui->btnBackground->setEnabled(false); 344 | ui->btnBackground->setChecked(!isBackground); 345 | return; 346 | } 347 | if(normPrio == 0x7fff) { 348 | normPrio = GetPriorityClass(hPP); 349 | if(!normPrio) { 350 | // failed to retrieve, disable option 351 | ui->btnBackground->setEnabled(false); 352 | ui->btnBackground->setChecked(false); 353 | CloseHandle(hPP); 354 | return; 355 | } 356 | if(normPrio == IDLE_PRIORITY_CLASS) { 357 | // already at low priority 358 | ui->btnBackground->setEnabled(false); 359 | ui->btnBackground->setChecked(true); 360 | CloseHandle(hPP); 361 | return; 362 | } 363 | } 364 | if(!SetPriorityClass(hPP, isBackground ? IDLE_PRIORITY_CLASS : normPrio)) { 365 | ui->btnBackground->setChecked(!isBackground); 366 | } 367 | CloseHandle(hPP); 368 | #elif defined(Q_OS_UNIX) 369 | static int normPrio = 0x7fff; 370 | if(normPrio == 0x7fff) { 371 | // get the normal priority level (assume we start there) 372 | errno = 0; 373 | normPrio = getpriority(PRIO_PROCESS, parpar.processId()); 374 | if(normPrio == -1 && errno) { 375 | // failed to retrieve, disable option 376 | ui->btnBackground->setEnabled(false); 377 | ui->btnBackground->setChecked(false); 378 | return; 379 | } 380 | if(normPrio >= 19) { 381 | // already at low priority 382 | ui->btnBackground->setEnabled(false); 383 | ui->btnBackground->setChecked(true); 384 | return; 385 | } 386 | } 387 | 388 | if(setpriority(PRIO_PROCESS, parpar.processId(), isBackground ? 19 : normPrio)) { 389 | ui->btnBackground->setChecked(!isBackground); 390 | // TODO: it seems like you can't increase priority back to normal on Linux 391 | // TODO: if failed, the settings change is still persisted 392 | } 393 | #endif 394 | } 395 | 396 | Settings::getInstance().setRunBackground(isBackground); 397 | } 398 | 399 | 400 | void CreateProgress::on_btnPause_clicked() 401 | { 402 | bool isPaused = ui->btnPause->isChecked(); 403 | 404 | #ifdef Q_OS_WINDOWS 405 | static NtSuspendResumeProcess pfnNtSuspendProcess = nullptr; 406 | static NtSuspendResumeProcess pfnNtResumeProcess = nullptr; 407 | if(!pfnNtSuspendProcess) { 408 | HMODULE ntdll = GetModuleHandleA("ntdll"); 409 | pfnNtSuspendProcess = (NtSuspendResumeProcess)GetProcAddress(ntdll, "NtSuspendProcess"); 410 | pfnNtResumeProcess = (NtSuspendResumeProcess)GetProcAddress(ntdll, "NtResumeProcess"); 411 | } 412 | if(!pfnNtSuspendProcess || !pfnNtResumeProcess) { 413 | ui->btnPause->setEnabled(false); 414 | ui->btnPause->setChecked(!isPaused); 415 | return; 416 | } 417 | HANDLE hPP = OpenProcess(PROCESS_ALL_ACCESS, FALSE, parpar.processId()); 418 | if(hPP == NULL) { 419 | ui->btnPause->setEnabled(false); 420 | ui->btnPause->setChecked(!isPaused); 421 | return; 422 | } 423 | #endif 424 | 425 | if(isPaused) { 426 | #ifdef Q_OS_WINDOWS 427 | pfnNtSuspendProcess(hPP); 428 | CloseHandle(hPP); 429 | #elif defined(Q_OS_UNIX) 430 | if(kill(parpar.processId(), SIGSTOP)) { 431 | ui->btnPause->setChecked(!isPaused); 432 | return; 433 | } 434 | #endif 435 | 436 | this->setWindowTitle(tr("ParPar - Paused creating %1").arg(baseOutput)); 437 | ui->lblStatus->setText(tr("Paused")); 438 | elapsedTimeBase += elapsedTime.elapsed(); 439 | timer.stop(); 440 | } else { 441 | #ifdef Q_OS_WINDOWS 442 | pfnNtResumeProcess(hPP); 443 | CloseHandle(hPP); 444 | #elif defined(Q_OS_UNIX) 445 | if(kill(parpar.processId(), SIGCONT)) { 446 | ui->btnPause->setChecked(!isPaused); 447 | return; 448 | } 449 | #endif 450 | 451 | updateTitlePerc(); 452 | ui->lblStatus->setText(""); 453 | elapsedTime.start(); 454 | timer.start(); 455 | timerTick(); 456 | } 457 | //ui->progressBar->setDisabled(isPaused); 458 | ui->lblTime->setDisabled(isPaused); 459 | WIN_PROGRESS(SetProgressState, isPaused ? TBPF_PAUSED : TBPF_NORMAL); 460 | } 461 | 462 | // TODO: also hide this when the window is activated 463 | void CreateProgress::notificationClicked() 464 | { 465 | sysTrayIcon.hide(); 466 | this->activateWindow(); 467 | } 468 | 469 | void CreateProgress::rerun() 470 | { 471 | init(); 472 | this->setWindowTitle(tr("ParPar - Creating %2") 473 | .arg(baseOutput)); 474 | 475 | ui->buttonBox->setStandardButtons(QDialogButtonBox::Cancel); 476 | ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); 477 | ui->progressBar->setEnabled(true); 478 | ui->lblTime->setEnabled(true); 479 | ui->progressBar->setTextVisible(true); 480 | 481 | elapsedTime.restart(); 482 | parpar.start(); 483 | timer.start(); 484 | timerTick(); 485 | } 486 | 487 | void CreateProgress::on_buttonBox_accepted() 488 | { 489 | rerun(); 490 | } 491 | 492 | 493 | void CreateProgress::on_btnNotify_clicked() 494 | { 495 | Settings::getInstance().setRunNotification(ui->btnNotify->isChecked()); 496 | } 497 | 498 | -------------------------------------------------------------------------------- /createprogress.h: -------------------------------------------------------------------------------- 1 | #ifndef CREATEPROGRESS_H 2 | #define CREATEPROGRESS_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #ifdef Q_OS_WINDOWS 12 | #include 13 | #endif 14 | 15 | namespace Ui { 16 | class CreateProgress; 17 | } 18 | 19 | class CreateProgress : public QDialog 20 | { 21 | Q_OBJECT 22 | 23 | public: 24 | explicit CreateProgress(QWidget *parent = nullptr); 25 | void run(QStringList args, const QHash& env, const QByteArray& inFiles, const QString& baseOutput_, const QString& outDir_, const QStringList& outFiles_); 26 | ~CreateProgress(); 27 | 28 | private slots: 29 | void timerTick(); 30 | void gotStdout(); 31 | void finished(int exitCode, QProcess::ExitStatus exitStatus); 32 | void notificationClicked(); 33 | 34 | void on_CreateProgress_rejected(); 35 | 36 | void on_btnBackground_clicked(); 37 | 38 | void on_btnPause_clicked(); 39 | 40 | void on_buttonBox_accepted(); 41 | 42 | void on_btnNotify_clicked(); 43 | 44 | private: 45 | Ui::CreateProgress *ui; 46 | QProcess parpar; 47 | QString baseOutput; 48 | QString outDir; 49 | QStringList outFiles; 50 | 51 | QByteArray stdoutBuffer; 52 | bool isCancelled; 53 | 54 | void deleteOutput(); 55 | void ended(const QString& error, bool showOutput); 56 | 57 | QTimer timer; 58 | quint64 elapsedTimeBase; 59 | QElapsedTimer elapsedTime; 60 | 61 | void updateTitlePerc(); 62 | #ifdef Q_OS_WINDOWS 63 | ITaskbarList3* pTL; 64 | HWND hWnd; 65 | #endif 66 | QSystemTrayIcon sysTrayIcon; 67 | 68 | void init(); 69 | void rerun(); 70 | }; 71 | 72 | #endif // CREATEPROGRESS_H 73 | -------------------------------------------------------------------------------- /createprogress.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | CreateProgress 4 | 5 | 6 | Qt::WindowModal 7 | 8 | 9 | 10 | 0 11 | 0 12 | 461 13 | 307 14 | 15 | 16 | 17 | ParPar - Starting... 18 | 19 | 20 | 21 | 22 | 23 | 0 24 | 25 | 26 | 0 27 | 28 | 29 | Qt::AlignCenter 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 0 38 | 0 39 | 40 | 41 | 42 | Elapsed Time Display 43 | 44 | 45 | Qt::AlignCenter 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 0 54 | 0 55 | 56 | 57 | 58 | 59 | true 60 | 61 | 62 | 63 | <html><head/><body><p>Starting...</p></body></html> 64 | 65 | 66 | Qt::AlignCenter 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 0 75 | 40 76 | 77 | 78 | 79 | true 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | Run at a lower priority to reduce impact on system processor 89 | 90 | 91 | &Background 92 | 93 | 94 | true 95 | 96 | 97 | 98 | 99 | 100 | 101 | Emit a notification once the process completes 102 | 103 | 104 | &Notification 105 | 106 | 107 | true 108 | 109 | 110 | 111 | 112 | 113 | 114 | false 115 | 116 | 117 | Suspend/resume process 118 | 119 | 120 | &Pause 121 | 122 | 123 | true 124 | 125 | 126 | 127 | 128 | 129 | 130 | QDialogButtonBox::Cancel 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | buttonBox 142 | rejected() 143 | CreateProgress 144 | reject() 145 | 146 | 147 | 283 148 | 297 149 | 150 | 151 | 20 152 | 20 153 | 154 | 155 | 156 | 157 | 158 | -------------------------------------------------------------------------------- /doc/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/animetosho/ParParGUI/3d7d4995d5e4c38528012a76d513cb6528dc075b/doc/screenshot.png -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #include "mainwindow.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "config.h" 8 | 9 | int main(int argc, char *argv[]) 10 | { 11 | // consistency with Qt6 12 | #if QT_VERSION < 0x060000 13 | QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling, true); 14 | #endif 15 | #if QT_VERSION >= 0x050E00 16 | QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); 17 | #endif 18 | 19 | QCoreApplication::setApplicationName(PROJECT_NAME); 20 | QCoreApplication::setApplicationVersion(PROJECT_VERSION); 21 | 22 | QApplication a(argc, argv); 23 | 24 | QTranslator translator; 25 | const QStringList uiLanguages = QLocale::system().uiLanguages(); 26 | for (const QString &locale : uiLanguages) { 27 | const QString baseName = "parpargui_" + QLocale(locale).name(); 28 | if (translator.load(":/i18n/" + baseName)) { 29 | a.installTranslator(&translator); 30 | break; 31 | } 32 | } 33 | 34 | if(a.arguments().contains( 35 | #ifdef Q_OS_WINDOWS 36 | "/h" 37 | #else 38 | "-h" 39 | #endif 40 | )) { 41 | QMessageBox::information(NULL, a.tr("ParPar Arguments Help"), a.tr("Currently, ParParGUI only handles file name arguments, which are loaded as source files on application launch.")); 42 | return 0; 43 | } 44 | 45 | MainWindow w; 46 | w.show(); 47 | return a.exec(); 48 | } 49 | -------------------------------------------------------------------------------- /mainwindow.cpp: -------------------------------------------------------------------------------- 1 | #include "mainwindow.h" 2 | #include "./ui_mainwindow.h" 3 | #include "./optionsdialog.h" 4 | #include "./createprogress.h" 5 | #include 6 | #include 7 | #include 8 | #include "settings.h" 9 | #include "util.h" 10 | #include "progressdialog.h" 11 | #include "par2calc.h" 12 | #include "sourcefilelistitem.h" 13 | #include "outpreviewlistitem.h" 14 | #include "clientinfo.h" 15 | #include 16 | #include 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | MainWindow::MainWindow(QWidget *parent) 25 | : QMainWindow(parent) 26 | , ui(new Ui::MainWindow) 27 | , dlgOptions(this) 28 | , outPreview(this) 29 | { 30 | ui->setupUi(this); 31 | 32 | ui->tvDest->hide(); 33 | 34 | ui->txtInsliceCount->setMainWindow(this); 35 | 36 | const auto& settings = Settings::getInstance(); 37 | ui->stkSource->setCurrentIndex(settings.uiExpSource() ? 1 : 0); 38 | ui->btnDestPreview->setChecked(settings.uiExpDest()); 39 | // handle things which change in Designer 40 | ui->stkDestSizing->setCurrentIndex(0); 41 | this->resize(600, 500); // set an initial size to expand on 42 | 43 | ui->tvSource->sortByColumn(0, Qt::SortOrder::AscendingOrder); 44 | ui->tvDest->sortByColumn(0, Qt::SortOrder::AscendingOrder); 45 | 46 | ui->txtInsliceSize->blockSignals(true); 47 | ui->txtInsliceSize->setText(settings.allocSliceSize()); 48 | ui->txtInsliceSize->blockSignals(false); 49 | ui->txtInsliceCount->blockSignals(true); 50 | ui->txtInsliceCount->setValue(settings.allocSliceCount()); 51 | ui->txtInsliceCount->blockSignals(false); 52 | switch(settings.allocSliceMode()) { 53 | case SettingsDefaultAllocIn::ALLOC_IN_SIZE: 54 | ui->optInsliceSize->blockSignals(true); 55 | ui->optInsliceSize->setChecked(true); 56 | ui->optInsliceSize->blockSignals(false); 57 | break; 58 | case SettingsDefaultAllocIn::ALLOC_IN_COUNT: 59 | ui->optInsliceCount->blockSignals(true); 60 | ui->optInsliceCount->setChecked(true); 61 | ui->optInsliceCount->blockSignals(false); 62 | break; 63 | case SettingsDefaultAllocIn::ALLOC_IN_RATIO: 64 | // handle later 65 | break; 66 | } 67 | 68 | ui->txtOutsliceRatio->blockSignals(true); 69 | ui->txtOutsliceRatio->setValue(settings.allocRecoveryRatio()); 70 | ui->txtOutsliceRatio->blockSignals(false); 71 | ui->txtOutsliceCount->blockSignals(true); 72 | ui->txtOutsliceCount->setValue(settings.allocRecoveryCount()); 73 | ui->txtOutsliceCount->blockSignals(false); 74 | ui->txtOutsliceSize->blockSignals(true); 75 | ui->txtOutsliceSize->setText(settings.allocRecoverySize()); 76 | ui->txtOutsliceSize->blockSignals(false); 77 | switch(settings.allocRecoveryMode()) { 78 | case SettingsDefaultAllocRec::ALLOC_REC_RATIO: 79 | ui->optOutsliceRatio->blockSignals(true); 80 | ui->optOutsliceRatio->setChecked(true); 81 | ui->optOutsliceRatio->blockSignals(false); 82 | break; 83 | case SettingsDefaultAllocRec::ALLOC_REC_COUNT: 84 | ui->optOutsliceCount->blockSignals(true); 85 | ui->optOutsliceCount->setChecked(true); 86 | ui->optOutsliceCount->blockSignals(false); 87 | break; 88 | case SettingsDefaultAllocRec::ALLOC_REC_SIZE: 89 | ui->optOutsliceSize->blockSignals(true); 90 | ui->optOutsliceSize->setChecked(true); 91 | ui->optOutsliceSize->blockSignals(false); 92 | break; 93 | } 94 | 95 | par2SrcSize = 0; 96 | par2FileCount = 0; 97 | srcBaseChosen = false; 98 | destFileChosen = false; 99 | 100 | 101 | auto& clientInfo = ClientInfo::getInstance(); 102 | connect(&clientInfo, &ClientInfo::failed, this, [=](const QString& error) { 103 | QMessageBox::warning(this, tr("ParPar Execute Failed"), tr("Failed to retrieve information from ParPar client. Please ensure that ParPar is available, executable and/or configured in the Options dialog.\n\nDetail: %1").arg(error)); 104 | }); 105 | connect(&clientInfo, &ClientInfo::updated, this, [=]() { 106 | // mostly because the creator could've changed 107 | this->updateDestPreview(); 108 | }); 109 | 110 | this->optionSliceMultiple = 4; 111 | this->optionSliceLimit = settings.sliceLimit(); 112 | auto updateMultiple = [=](bool binaryChanged) { 113 | const auto& settings = Settings::getInstance(); 114 | quint64 newMultiple = sizeToBytes(settings.sliceMultiple()); 115 | if(!newMultiple) newMultiple = 4; 116 | else if(newMultiple & 3) // invalid multiple - can't be used 117 | newMultiple = 4; 118 | if(this->optionSliceLimit != settings.sliceLimit()) { 119 | this->optionSliceLimit = settings.sliceLimit(); 120 | this->optionSliceMultiple = newMultiple; 121 | this->updateInsliceInfo(); 122 | this->checkSourceFileCount(tr("Change slice limit")); 123 | } 124 | else if(this->optionSliceMultiple != newMultiple) { 125 | this->optionSliceMultiple = newMultiple; 126 | this->updateInsliceInfo(); 127 | } 128 | 129 | if(binaryChanged) { // TODO: maybe update regardless of change (e.g. if user renamed files correct) 130 | // dest preview will be updated by the following, so don't need to double update 131 | ClientInfo::getInstance().refresh(); 132 | } else { 133 | this->updateDestPreview(); // a number of options affect this, so always regen 134 | } 135 | }; 136 | connect(&dlgOptions, &OptionsDialog::settingsUpdated, this, updateMultiple); 137 | updateMultiple(true); 138 | 139 | 140 | // handle application arguments 141 | const auto& argv = QCoreApplication::arguments(); 142 | QStringList loadFiles; 143 | for(int i=1; itvSource->width(); 171 | ui->tvSource->header()->setUpdatesEnabled(false); 172 | ui->tvSource->header()->resizeSection(0, w>400 ? w-250 : 150); 173 | ui->tvSource->header()->resizeSection(1, 150); 174 | ui->tvSource->header()->resizeSection(2, 80); 175 | ui->tvSource->header()->setUpdatesEnabled(true); 176 | 177 | w = ui->tvDest->width(); 178 | ui->tvDest->header()->setUpdatesEnabled(false); 179 | ui->tvDest->header()->resizeSection(0, w>320 ? w-240 : 80); 180 | ui->tvDest->header()->resizeSection(1, 80); 181 | ui->tvDest->header()->resizeSection(2, 70); 182 | ui->tvDest->header()->resizeSection(3, 70); 183 | ui->tvDest->header()->setUpdatesEnabled(true); 184 | } 185 | 186 | void MainWindow::adjustExpansion(bool allowExpand) { 187 | bool inExp = ui->stkSource->currentIndex() == 1; 188 | bool outExp = ui->btnDestPreview->isChecked(); 189 | ui->splitter->setUpdatesEnabled(false); 190 | for(int i=0; isplitter->count(); i++) 191 | ui->splitter->handle(i)->setEnabled(inExp && outExp); 192 | ui->splitter->setUpdatesEnabled(true); 193 | 194 | ui->tvDest->setVisible(outExp); 195 | ui->btnDestPreview->setArrowType(outExp ? Qt::UpArrow : Qt::DownArrow); 196 | ui->stkSource->setUpdatesEnabled(false); 197 | if(inExp) { 198 | ui->stkSource->setMinimumHeight(ui->stkSourceAdv->minimumHeight()); 199 | ui->stkSource->setMaximumHeight(ui->stkSourceAdv->maximumHeight()); 200 | } else { 201 | ui->stkSource->setFixedHeight(ui->stkSourceBasic->layout()->sizeHint().height()); 202 | } 203 | ui->stkSource->setUpdatesEnabled(true); 204 | 205 | ui->stkDestSizing->setFixedHeight(ui->stkDestSizing->currentWidget()->layout()->sizeHint().height()); 206 | ui->stkDestSizing->updateGeometry(); 207 | 208 | auto destOptsMargin = ui->fraDestOpts->layout()->contentsMargins(); 209 | destOptsMargin.setBottom(outExp ? destOptsMargin.top() : 0); 210 | ui->fraDestOpts->layout()->setContentsMargins(destOptsMargin); 211 | 212 | auto destPolicy = ui->grpDest->sizePolicy(); 213 | destPolicy.setVerticalPolicy(outExp ? QSizePolicy::Expanding : QSizePolicy::Maximum); 214 | ui->grpDest->setUpdatesEnabled(false); 215 | ui->grpDest->setSizePolicy(destPolicy); 216 | ui->grpDest->adjustSize(); 217 | ui->grpDest->setUpdatesEnabled(true); 218 | 219 | this->setUpdatesEnabled(false); 220 | if(inExp || outExp) { 221 | this->setMaximumHeight(16777215); 222 | ui->scrollArea->setMaximumHeight(16777215); 223 | 224 | if(allowExpand) { 225 | // TODO: detect screen height and restrict resize? 226 | /* 227 | // the default QTreeWidget size is a bit too high - try restricting it and let the layout compute the appropriate size 228 | ui->tvSource->setFixedHeight(100); 229 | int scrollDiff = ui->scrollAreaContents->layout()->sizeHint().height() - ui->scrollAreaContents->height(); 230 | if(scrollDiff > 0) { 231 | // try to eliminate scrollbar 232 | this->resize(this->width(), this->height() + scrollDiff); 233 | } 234 | ui->tvSource->setMaximumHeight(INT_MAX); 235 | */ 236 | 237 | int wantedMinHeight = this->layout()->sizeHint().height(); 238 | if(inExp) wantedMinHeight += 150; 239 | if(outExp) wantedMinHeight += ui->tvDest->minimumHeight(); 240 | if(this->height() < wantedMinHeight) 241 | this->resize(this->width(), wantedMinHeight); 242 | } 243 | } else { 244 | // for some reason, the layout engine overscales by default, so manually size the major containers 245 | auto scrollMargins = ui->scrollAreaContents->layout()->contentsMargins(); 246 | int scrollHeight = ui->stkSourceBasic->layout()->sizeHint().height() 247 | + ui->fraSlices->sizeHint().height() 248 | + ui->grpDest->sizeHint().height() 249 | + scrollMargins.bottom() + scrollMargins.top() 250 | + ui->scrollAreaContents->layout()->spacing()*2; 251 | //ui->scrollArea->setFixedHeight(scrollHeight); 252 | this->setFixedHeight(scrollHeight + ui->lytBottomButtons->sizeHint().height()); 253 | } 254 | this->setMinimumWidth(575); // for some reason, this can get lost? 255 | this->setUpdatesEnabled(true); 256 | } 257 | 258 | void MainWindow::showEvent(QShowEvent *event) { 259 | QMainWindow::showEvent(event); 260 | 261 | on_cboDestDist_currentIndexChanged(ui->cboDestDist->currentIndex()); 262 | adjustExpansion(true); // TODO: above calls adjustExpansion - need to avoid a double-call 263 | rescale(); 264 | 265 | 266 | /* 267 | // check ParPar executable 268 | bool parparMissing; 269 | auto parparBin = Settings::getInstance().parparBin(&parparMissing); 270 | if(!parparMissing) { 271 | for(const auto& part : parparBin) { 272 | if(!QFile::exists(part)) { 273 | parparMissing = true; 274 | break; 275 | } 276 | } 277 | } 278 | if(parparMissing) { 279 | dlgOptions.open(); 280 | QMessageBox::warning(&dlgOptions, tr("ParPar Executable"), tr("The ParPar executable was not found. Please configure it in this Options dialog.")); 281 | dlgOptions.focusWidget(); // TODO: this still doesn't seem to prioritize focus onto the dialog 282 | } else { 283 | QFileInfo binInfo(parparBin[0]); 284 | if(binInfo.isReadable() && !binInfo.isExecutable()) { 285 | // possibly common problem on Unixes where the executable bit isn't set 286 | if(QMessageBox::Yes == QMessageBox::warning(this, tr("ParPar Executable"), tr("The Node/ParPar executable is not executable. Do you want to try and enable it?"), QMessageBox::Yes | QMessageBox::No)) { 287 | QFile exe(parparBin[0]); 288 | auto ans = QMessageBox::Retry; 289 | while(ans == QMessageBox::Retry) { 290 | if(exe.setPermissions(exe.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup | QFileDevice::ExeOther)) 291 | break; 292 | ans = QMessageBox::critical(this, tr("ParPar Executable"), tr("Failed to set executable permissions to the Node/ParPar executable."), QMessageBox::Retry | QMessageBox::Ignore); 293 | } 294 | } 295 | } 296 | } 297 | */ 298 | } 299 | void MainWindow::resizeEvent(QResizeEvent *event) 300 | { 301 | QMainWindow::resizeEvent(event); 302 | rescale(); 303 | } 304 | 305 | void MainWindow::on_btnAbout_clicked() 306 | { 307 | QMessageBox::information(this, tr("About ParPar GUI"), QString("ParPar GUI v%1\nParPar v%2") 308 | .arg(QCoreApplication::applicationVersion(), 309 | ClientInfo::version()) 310 | ); 311 | } 312 | 313 | 314 | void MainWindow::on_btnOptions_clicked() 315 | { 316 | dlgOptions.open(); 317 | } 318 | 319 | void MainWindow::on_txtInsliceCount_valueChanged(int value) 320 | { 321 | ui->optInsliceCount->blockSignals(true); 322 | ui->optInsliceCount->setChecked(true); 323 | ui->optInsliceCount->blockSignals(false); 324 | par2SliceSize = Par2Calc::sliceSizeFromCount(value, optionSliceMultiple, optionSliceLimit, par2SrcFiles, par2FileCount); 325 | ui->txtInsliceSize->setBytesApprox(par2SliceSize, true); 326 | 327 | updateOutsliceInfo(false); 328 | } 329 | void MainWindow::on_txtInsliceCount_editingFinished() 330 | { 331 | if(!ui->optInsliceCount->isChecked()) return; 332 | 333 | int newValue = ui->txtInsliceCount->value(); 334 | par2SliceSize = Par2Calc::sliceSizeFromCount(newValue, optionSliceMultiple, optionSliceLimit, par2SrcFiles, par2FileCount); 335 | if(newValue != ui->txtInsliceCount->value()) { 336 | ui->txtInsliceCount->blockSignals(true); 337 | ui->txtInsliceCount->setValue(newValue); 338 | ui->txtInsliceCount->blockSignals(false); 339 | } 340 | ui->txtInsliceSize->setBytesApprox(par2SliceSize, true); 341 | 342 | updateOutsliceInfo(); 343 | } 344 | 345 | 346 | void MainWindow::on_btnSourceAdd_clicked() 347 | { 348 | auto files = QFileDialog::getOpenFileNames(this, tr("Add source files"), ui->txtSourcePath->text(), tr("All files (*.*)")); 349 | if(!files.isEmpty()) { 350 | // TODO: select items that were just added 351 | sourceAddFiles(files); 352 | if(ui->txtDestFile->text().isEmpty()) 353 | autoSelectDestFile(); 354 | checkSourceFileCount(); 355 | updateSrcFilesState(); 356 | } 357 | } 358 | 359 | void MainWindow::on_btnSourceAddDir_clicked() 360 | { 361 | auto dir = QFileDialog::getExistingDirectory(this, tr("Add source files from directory"), ui->txtSourcePath->text()); 362 | if(!dir.isEmpty()) { 363 | sourceAddDir(dir); 364 | if(ui->txtDestFile->text().isEmpty()) 365 | autoSelectDestFile(); 366 | checkSourceFileCount(); 367 | updateSrcFilesState(); 368 | } 369 | } 370 | 371 | 372 | static void sourceDelHelper(QTreeWidgetItem* item, SrcFileList& files, quint64& totalSize, int& totalCount, bool isRecur = false) 373 | { 374 | int children = item->childCount(); 375 | while(children--) 376 | sourceDelHelper(item->child(children), files, totalSize, totalCount, true); 377 | 378 | auto key = item->data(0, Qt::UserRole).toString(); 379 | if(!key.isEmpty()) { // key will be empty for directories 380 | if(files[key].size()) { 381 | totalSize -= files[key].size(); 382 | totalCount--; 383 | } 384 | files.remove(key); 385 | } 386 | 387 | auto parent = item->parent(); 388 | delete item; 389 | // remove empty parents 390 | if(isRecur) return; 391 | while(parent && parent->childCount() == 0) { 392 | item = parent; 393 | parent = item->parent(); 394 | delete item; 395 | } 396 | } 397 | void MainWindow::on_btnSourceDel_clicked() 398 | { 399 | auto selections = ui->tvSource->selectedItems(); 400 | ui->tvSource->setUpdatesEnabled(false); 401 | while(!selections.isEmpty()) { 402 | sourceDelHelper(selections.at(0), par2SrcFiles, par2SrcSize, par2FileCount); 403 | selections = ui->tvSource->selectedItems(); 404 | } 405 | ui->tvSource->setUpdatesEnabled(true); 406 | updateSrcFilesState(); 407 | if(par2SrcFiles.isEmpty()) { 408 | // assume we're starting afresh 409 | srcBaseChosen = false; 410 | destFileChosen = false; 411 | } 412 | } 413 | 414 | static void recurseAddDir(const QDir& dir, SrcFileList& dest, ProgressDialog& progress) 415 | { 416 | // TODO: options for symlinks/system/hidden (system probably undesirable on Unix) files 417 | auto list = dir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); 418 | 419 | // since we don't know how deep this is going to get, we'll overallocate the progress and fix it at the end 420 | progress.setMaximum(progress.maximum() + list.size()*2); 421 | 422 | for(auto& info : list) { 423 | if(progress.wasCanceled()) return; 424 | auto key = info.canonicalFilePath(); 425 | if(info.isDir()) { 426 | recurseAddDir(key, dest, progress); 427 | } else { 428 | #ifdef Q_OS_WINDOWS 429 | key = key.toLower(); 430 | #endif 431 | dest.insert(key, info); 432 | } 433 | progress.inc(); 434 | } 435 | progress.setMaximum(progress.maximum() - list.size()); 436 | } 437 | void MainWindow::sourceAddFiles(const QStringList &files) 438 | { 439 | ProgressDialog progress(this, tr("Adding files...")); 440 | progress.setMaximum(files.size()); 441 | 442 | par2SrcFiles.reserve(par2SrcFiles.size() + files.size()); 443 | for(const auto& file : files) { 444 | if(progress.wasCanceled()) break; 445 | 446 | QFileInfo info(file); 447 | if(info.isDir()) { 448 | recurseAddDir(file, par2SrcFiles, progress); 449 | } else { 450 | auto key = info.canonicalFilePath(); 451 | #ifdef Q_OS_WINDOWS 452 | key = key.toLower(); 453 | #endif 454 | par2SrcFiles.insert(key, info); 455 | progress.inc(); 456 | } 457 | } 458 | progress.end(); 459 | 460 | if(!srcBaseChosen) 461 | ui->txtSourcePath->setText(srcFilesCommonPath().replace("/", QDir::separator())); 462 | reloadSourceFiles(); 463 | } 464 | void MainWindow::sourceAddDir(const QString& dir) 465 | { 466 | ProgressDialog progress(this, tr("Adding files...")); 467 | progress.setMaximum(1); // we don't know the maximum, so add it as we go; can't set to 0 as it'd close the window 468 | 469 | bool previouslyEmpty = par2SrcFiles.isEmpty(); 470 | recurseAddDir(dir, par2SrcFiles, progress); 471 | progress.end(); 472 | 473 | QString basePath = dir; 474 | if(!previouslyEmpty) 475 | basePath = srcFilesCommonPath(); 476 | if(!srcBaseChosen) 477 | ui->txtSourcePath->setText(basePath.replace("/", QDir::separator())); 478 | reloadSourceFiles(); 479 | } 480 | 481 | void MainWindow::on_btnSourcePathBrowse_clicked() 482 | { 483 | auto dir = QFileDialog::getExistingDirectory(this, tr("Select base path"), ui->txtSourcePath->text()); 484 | if(!dir.isEmpty()) { 485 | ui->txtSourcePath->setText(dir.replace("/", QDir::separator())); 486 | srcBaseChosen = true; 487 | reloadSourceFiles(); 488 | updateDestPreview(); 489 | } 490 | } 491 | 492 | 493 | void MainWindow::on_btnDestPreview_clicked() 494 | { 495 | bool show = ui->btnDestPreview->isChecked(); 496 | 497 | adjustExpansion(show); 498 | ui->scrollArea->ensureWidgetVisible(ui->tvDest); 499 | Settings::getInstance().setUiExpDest(show); 500 | 501 | updateDestPreview(); 502 | } 503 | 504 | 505 | void MainWindow::on_cboSourcePaths_currentIndexChanged(int index) 506 | { 507 | bool enablePath = (index == 1); 508 | ui->txtSourcePath->setEnabled(enablePath); 509 | ui->btnSourcePathBrowse->setEnabled(enablePath); 510 | 511 | ui->tvSource->setItemsExpandable(index != 0); 512 | ui->tvSource->setRootIsDecorated(index != 0); 513 | reloadSourceFiles(); 514 | updateDestPreview(); 515 | } 516 | 517 | 518 | void MainWindow::on_btnDestFileBrowse_clicked() 519 | { 520 | auto file = QFileDialog::getSaveFileName(this, tr("Select destination recovery base file"), ui->txtDestFile->text(), tr("PAR2 files (*.par2)")); 521 | if(!file.isEmpty()) { 522 | ui->txtDestFile->setText(file.replace("/", QDir::separator())); 523 | destFileChosen = true; 524 | updateSrcFilesState(); 525 | updateDestPreview(); 526 | } 527 | } 528 | 529 | 530 | void MainWindow::on_txtInsliceSize_valueChanged(quint64 size, bool finished) 531 | { 532 | if(finished) { 533 | if(!ui->optInsliceSize->isChecked()) return; 534 | } else { 535 | ui->optInsliceSize->blockSignals(true); 536 | ui->optInsliceSize->setChecked(true); 537 | ui->optInsliceSize->blockSignals(false); 538 | } 539 | 540 | par2SliceSize = size; 541 | int count = Par2Calc::sliceCountFromSize(par2SliceSize, optionSliceMultiple, optionSliceLimit, par2SrcFiles, par2FileCount); 542 | if(finished && size != par2SliceSize) { 543 | ui->txtInsliceSize->setBytes(par2SliceSize); 544 | } 545 | ui->txtInsliceCount->blockSignals(true); 546 | ui->txtInsliceCount->setValue(count); 547 | ui->txtInsliceCount->blockSignals(false); 548 | 549 | updateOutsliceInfo(finished); 550 | } 551 | 552 | void MainWindow::on_txtOutsliceRatio_valueChanged(double arg1) 553 | { 554 | ui->optOutsliceRatio->blockSignals(true); 555 | ui->optOutsliceRatio->setChecked(true); 556 | ui->optOutsliceRatio->blockSignals(false); 557 | 558 | int srcSlices = ui->txtInsliceCount->value(); 559 | int destSlices = ceil(srcSlices * (arg1/100)); 560 | if(destSlices > 65535) destSlices = 65535; 561 | ui->txtOutsliceCount->blockSignals(true); 562 | ui->txtOutsliceCount->setValue(destSlices); 563 | ui->txtOutsliceCount->blockSignals(false); 564 | 565 | ui->txtOutsliceSize->setBytesApprox(par2SliceSize * destSlices, true); 566 | 567 | updateDestInfo(false); 568 | } 569 | void MainWindow::on_txtOutsliceRatio_editingFinished() 570 | { 571 | if(!ui->optOutsliceRatio->isChecked()) return; 572 | 573 | double val = ui->txtOutsliceRatio->value(); 574 | int srcSlices = ui->txtInsliceCount->value(); 575 | int destSlices = ceil(srcSlices * (val/100)); 576 | if(destSlices > 65535) { 577 | val = 65535*100; 578 | val /= srcSlices; 579 | ui->txtOutsliceRatio->blockSignals(true); 580 | ui->txtOutsliceRatio->setValue(val); 581 | ui->txtOutsliceRatio->blockSignals(false); 582 | } else { 583 | ui->txtOutsliceCount->blockSignals(true); 584 | ui->txtOutsliceCount->setValue(destSlices); 585 | ui->txtOutsliceCount->blockSignals(false); 586 | 587 | ui->txtOutsliceSize->setBytesApprox(par2SliceSize * destSlices, true); 588 | updateDestInfo(); 589 | } 590 | } 591 | void MainWindow::txtOutsliceCount_updated(bool editingFinished) 592 | { 593 | int val = ui->txtOutsliceCount->value(); 594 | double perc = val*100; 595 | perc /= ui->txtInsliceCount->value(); 596 | ui->txtOutsliceRatio->blockSignals(true); 597 | ui->txtOutsliceRatio->setValue(perc); 598 | ui->txtOutsliceRatio->blockSignals(false); 599 | ui->txtOutsliceSize->setBytesApprox(par2SliceSize * val, true); 600 | 601 | updateDestInfo(editingFinished); 602 | } 603 | void MainWindow::on_txtOutsliceCount_valueChanged(int arg1) 604 | { 605 | ui->optOutsliceCount->blockSignals(true); 606 | ui->optOutsliceCount->setChecked(true); 607 | ui->optOutsliceCount->blockSignals(false); 608 | txtOutsliceCount_updated(false); 609 | } 610 | void MainWindow::on_txtOutsliceCount_editingFinished() 611 | { 612 | if(!ui->optOutsliceCount->isChecked()) return; 613 | txtOutsliceCount_updated(true); 614 | } 615 | 616 | void MainWindow::on_txtOutsliceSize_valueChanged(quint64 size, bool finished) 617 | { 618 | if(finished) { 619 | if(!ui->optOutsliceSize->isChecked()) return; 620 | } else { 621 | ui->optOutsliceSize->blockSignals(true); 622 | ui->optOutsliceSize->setChecked(true); 623 | ui->optOutsliceSize->blockSignals(false); 624 | } 625 | 626 | int destSlices = (size + par2SliceSize-1) / par2SliceSize; // round up 627 | if(destSlices > 65535) { 628 | destSlices = 65535; 629 | if(finished) { 630 | size = 65535 * par2SliceSize; 631 | ui->txtOutsliceSize->setBytes(size, true); 632 | return; 633 | } 634 | } 635 | ui->txtOutsliceCount->blockSignals(true); 636 | ui->txtOutsliceCount->setValue(destSlices); 637 | ui->txtOutsliceCount->blockSignals(false); 638 | 639 | double perc = destSlices*100; 640 | perc /= ui->txtInsliceCount->value(); 641 | ui->txtOutsliceRatio->blockSignals(true); 642 | ui->txtOutsliceRatio->setValue(perc); 643 | ui->txtOutsliceRatio->blockSignals(false); 644 | 645 | updateDestInfo(finished); 646 | } 647 | 648 | 649 | 650 | void MainWindow::on_cboDestDist_currentIndexChanged(int index) 651 | { 652 | int selection = ui->cboDestDist->currentIndex(); 653 | if(selection == 0 || selection == 2) { 654 | ui->stkDestSizing->setCurrentIndex(0); 655 | adjustExpansion(false); 656 | } else { 657 | ui->stkDestSizing->setCurrentIndex(selection/2 + 1); 658 | adjustExpansion(true); 659 | } 660 | updateDestPreview(); 661 | } 662 | 663 | 664 | void MainWindow::on_btnSourceAdv_clicked() 665 | { 666 | ui->btnSourceAdv->setChecked(false); 667 | ui->stkSource->setCurrentIndex(1); 668 | ui->btnSourceAdv2->focusWidget(); 669 | 670 | adjustExpansion(true); 671 | Settings::getInstance().setUiExpSource(true); 672 | } 673 | 674 | 675 | void MainWindow::on_btnSourceAdv2_clicked() 676 | { 677 | ui->btnSourceAdv2->setChecked(true); 678 | ui->stkSource->setCurrentIndex(0); 679 | ui->btnSourceAdv->focusWidget(); 680 | 681 | adjustExpansion(false); 682 | Settings::getInstance().setUiExpSource(false); 683 | } 684 | 685 | 686 | void MainWindow::on_btnComment_clicked() 687 | { 688 | bool accepted; 689 | auto newComment = QInputDialog::getMultiLineText(this, tr("PAR2 Comment"), tr("Enter a comment for this PAR2"), par2Comment, &accepted); 690 | if(accepted) { 691 | par2Comment = newComment; 692 | #ifdef Q_OS_WINDOWS 693 | if(par2Comment.length() > 5000) { // command limit is 32767 chars, but we'll warn the user if they could exceed the 8191 limit of cmd.exe 694 | QMessageBox::warning(this, tr("Set Comment"), tr("Comment has been set, however long comments may cause issues with Windows' command length limits. Keeping comments short is recommended.")); 695 | } 696 | #endif 697 | updateDestPreview(); 698 | ui->btnComment->setText(par2Comment.isEmpty() ? tr("Set Comme&nt...") : tr("Edit Comme&nt...")); 699 | } 700 | } 701 | 702 | 703 | void MainWindow::on_btnSourceSetFiles_clicked() 704 | { 705 | auto files = QFileDialog::getOpenFileNames(this, tr("Add source files"), "", tr("All files (*.*)")); 706 | if(!files.isEmpty()) { 707 | par2SrcFiles.clear(); 708 | srcBaseChosen = false; 709 | sourceAddFiles(files); 710 | autoSelectDestFile(); 711 | checkSourceFileCount(); 712 | updateSrcFilesState(); 713 | } 714 | 715 | } 716 | 717 | 718 | void MainWindow::on_btnSourceSetDir_clicked() 719 | { 720 | auto dir = QFileDialog::getExistingDirectory(this, tr("Add source files from directory"), ""); 721 | if(!dir.isEmpty()) { 722 | par2SrcFiles.clear(); 723 | srcBaseChosen = false; 724 | sourceAddDir(dir); 725 | autoSelectDestFile(); 726 | checkSourceFileCount(); 727 | updateSrcFilesState(); 728 | } 729 | } 730 | 731 | QString MainWindow::srcFilesCommonPath() const 732 | { 733 | if(par2SrcFiles.isEmpty()) return ""; 734 | auto keys = par2SrcFiles.keys(); 735 | QString common = keys.at(0); 736 | int p = common.lastIndexOf('/'); 737 | if(p < 0) return ""; // invalid 738 | common = common.left(p+1); 739 | for(int i=1; icboSourcePaths->currentIndex(); 761 | auto tv = ui->tvSource; 762 | par2SrcSize = 0; 763 | par2FileCount = 0; 764 | if(par2SrcFiles.isEmpty()) { 765 | tv->clear(); 766 | return; 767 | } 768 | tv->setUpdatesEnabled(false); 769 | tv->clear(); 770 | 771 | ProgressDialog progress(this, tr("Building file list...")); 772 | progress.setMaximum(par2SrcFiles.size() + 1); 773 | progress.setCancelButton(nullptr); 774 | // TODO: also disable window close button 775 | 776 | if(pathOpt == 0) { 777 | // add without pathing 778 | QList items; 779 | auto srcKeys = par2SrcFiles.keys(); 780 | for(const auto& key : srcKeys) { 781 | auto& file = par2SrcFiles[key]; 782 | auto item = SourceFileListItem::create(nullptr, file); 783 | item->setData(0, Qt::UserRole, key); 784 | items.append(item); 785 | 786 | if(file.size()) { 787 | par2SrcSize += file.size(); 788 | par2FileCount++; 789 | } 790 | file.par2name = file.fileName(); 791 | progress.inc(); 792 | } 793 | tv->addTopLevelItems(items); 794 | } else { 795 | // if absolute, add base path as first item 796 | QString basePath = ""; 797 | bool isRelative = (pathOpt == 1); 798 | if(isRelative) { 799 | basePath = ui->txtSourcePath->text(); 800 | basePath = basePath.replace(QDir::separator(), "/"); 801 | if(!basePath.isEmpty()) { 802 | if(!basePath.endsWith('/')) basePath += "/"; 803 | } else // if there's no common path defined, treat as absolute 804 | isRelative = false; 805 | } 806 | // TODO: for absolute paths (and relative as well) support merging single directories 807 | QDir baseDir(basePath); 808 | 809 | QHash dirNodes; 810 | QList topNodes; 811 | auto it = QMutableHashIterator(par2SrcFiles); 812 | while(it.hasNext()) { 813 | it.next(); 814 | auto& file = it.value(); 815 | 816 | auto relPath = isRelative ? baseDir.relativeFilePath(file.canonicalFilePath()) : file.canonicalFilePath(); 817 | #ifdef Q_OS_WINDOWS 818 | auto pathKey = relPath.toLower().split('/'); 819 | #else 820 | auto pathKey = relPath.split('/'); 821 | #endif 822 | if(pathKey.length() > 1) { 823 | // child node - ensure folder nodes are present 824 | QString key = pathKey[0]; 825 | if(!dirNodes.contains(key)) { 826 | // create top level directory 827 | auto dirItem = SourceFileListItem::create(nullptr, relPath.left(key.length())); 828 | topNodes.append(dirItem); 829 | dirNodes.insert(key, dirItem); 830 | } 831 | for(int i=1; iaddChild(dirItem); 840 | } 841 | key = newKey; 842 | } 843 | // insert actual node 844 | auto newItem = SourceFileListItem::create(dirNodes[key], file); 845 | newItem->setData(0, Qt::UserRole, it.key()); 846 | dirNodes[key]->addChild(newItem); 847 | 848 | file.par2name = relPath; 849 | file.par2name.replace("/", QDir::separator()); 850 | } else { 851 | // top level file 852 | auto newItem = SourceFileListItem::create(nullptr, file); 853 | newItem->setData(0, Qt::UserRole, it.key()); 854 | topNodes.append(newItem); 855 | 856 | file.par2name = file.fileName(); 857 | } 858 | 859 | if(file.size()) { 860 | par2SrcSize += file.size(); 861 | par2FileCount++; 862 | } 863 | progress.inc(); 864 | } 865 | tv->addTopLevelItems(topNodes); 866 | // expand top-level folders 867 | for(auto node : topNodes) { 868 | if(node->childCount() > 0) 869 | node->setExpanded(true); 870 | } 871 | } 872 | tv->setUpdatesEnabled(true); 873 | progress.end(); 874 | } 875 | 876 | static QString wrapFile(const QString& val) 877 | { 878 | if(!val.isEmpty() && val.at(0) == '-') 879 | return QString(".") + QDir::separator() + val; 880 | return val; 881 | } 882 | 883 | QStringList MainWindow::getCmdArgs(QHash& env) const 884 | { 885 | QStringList list; 886 | // slice sizing 887 | if(ui->optInsliceSize->isChecked()) 888 | list << "--input-slices" << ui->txtInsliceSize->getSizeString(); 889 | if(ui->optInsliceCount->isChecked()) 890 | list << "--input-slices" << QString::number(ui->txtInsliceCount->value()); 891 | if(ui->optOutsliceRatio->isChecked()) 892 | list << "--recovery-slices" << (QString::number(ui->txtOutsliceRatio->value()) + "%"); 893 | if(ui->optOutsliceCount->isChecked()) 894 | list << "--recovery-slices" << QString::number(ui->txtOutsliceCount->value()); 895 | if(ui->optOutsliceSize->isChecked()) 896 | list << "--recovery-slices" << ui->txtOutsliceSize->getSizeString(); 897 | 898 | // input options 899 | switch(ui->cboSourcePaths->currentIndex()) { 900 | case 0: list << "--filepath-format" << "basename"; break; 901 | case 1: list << "--filepath-format" << "path" << "--filepath-base" << wrapFile(ui->txtSourcePath->text()); break; 902 | case 2: list << "--filepath-format" << "keep"; break; 903 | } 904 | 905 | // output options 906 | list << "--out" << wrapFile(ui->txtDestFile->text()) << "--overwrite"; 907 | if(ui->txtDestOffset->value() > 0) 908 | list << "--recovery-offset" << QString::number(ui->txtDestOffset->value()); 909 | if(ui->cboDestDist->isEnabled()) { 910 | switch(ui->cboDestDist->currentIndex()) { 911 | case 0: list << "--slice-dist" << "equal" << "--noindex"; break; 912 | case 1: list << "--slice-dist" << "uniform"; 913 | if(ui->optDestFiles->isChecked()) 914 | list << "--recovery-files" << QString::number(ui->txtDestFiles->value()); 915 | if(ui->optDestCount->isChecked()) 916 | list << "--slices-per-file" << QString::number(ui->txtDestCount->value()); 917 | if(ui->optDestSize->isChecked()) 918 | list << "--slices-per-file" << ui->txtDestSize->getSizeString(); 919 | break; 920 | case 2: list << "--slice-dist" << "pow2"; break; 921 | case 3: list << "--slice-dist" << "pow2"; 922 | if(ui->optDestMaxLfile->isChecked()) 923 | list << "--slices-per-file" << "1l"; 924 | if(ui->optDestMaxCount->isChecked()) 925 | list << "--slices-per-file" << QString::number(ui->txtDestMaxCount->value()); 926 | if(ui->optDestMaxSize->isChecked()) 927 | list << "--slices-per-file" << ui->txtDestMaxSize->getSizeString(); 928 | break; 929 | } 930 | } 931 | 932 | if(!par2Comment.isEmpty()) { 933 | if(par2Comment.at(0) == '-') 934 | list << QString("--comment=") + par2Comment; 935 | else 936 | list << "--comment" << par2Comment; 937 | } 938 | 939 | // processing options 940 | const auto& settings = Settings::getInstance(); 941 | if(settings.unicode() != AUTO) 942 | list << (settings.unicode() == INCLUDE ? "--unicode" : "--no-unicode"); 943 | if(settings.charset() != "utf8") 944 | list << "--ascii-charset" << settings.charset(); 945 | if(settings.packetRepMin() != 1) 946 | list << "--min-packet-redundancy" << QString::number(settings.packetRepMin()); 947 | if(settings.packetRepMax() != 16 && settings.packetRepMin() < 16) 948 | list << "--max-packet-redundancy" << QString::number(settings.packetRepMax()); 949 | if(settings.stdNaming()) 950 | list << "--std-naming"; 951 | // TODO: should we include the slice size multiple? 952 | 953 | if(settings.outputSync()) 954 | list << "--write-sync"; 955 | 956 | list << "--seq-read-size" << settings.readSize() 957 | << "--read-buffers" << QString::number(settings.readBuffers()) 958 | << "--read-hash-queue" << QString::number(settings.hashQueue()) 959 | << "--min-chunk-size" << settings.minChunk() 960 | << "--chunk-read-threads" << QString::number(settings.chunkReadThreads()) 961 | << "--recovery-buffers" << QString::number(settings.recBuffers()) 962 | << "--md5-batch-size" << QString::number(settings.hashBatch()) 963 | << "--cpu-minchunk" << settings.cpuMinChunk(); 964 | if(settings.hashMethod() != "auto") 965 | list << "--hash-method" << settings.hashMethod(); 966 | if(settings.md5Method() != "auto") 967 | list << "--md5-method" << settings.md5Method(); 968 | if(settings.procBatch() >= 0) 969 | list << "--proc-batch-size" << QString::number(settings.procBatch()); 970 | if(!settings.memLimit().isEmpty()) 971 | list << "--memory" << settings.memLimit(); 972 | if(!settings.gfMethod().isEmpty()) 973 | list << "--method" << settings.gfMethod(); 974 | if(!settings.tileSize().isEmpty()) 975 | list << "--loop-tile-size" << settings.tileSize(); 976 | if(settings.threadNum() >= 0) 977 | list << "--threads" << QString::number(settings.threadNum()); 978 | 979 | const auto openclDevices = settings.openclDevices(); 980 | for(const auto& dev : openclDevices) { 981 | QString devLine("device=%1,process=%2,minchunk=%3,method=%4"); 982 | QString devName(dev.name); 983 | devName.remove(','); 984 | devLine = devLine.arg(devName, QString::number(dev.alloc) + "%", dev.minChunk, dev.gfMethod); 985 | if(!dev.memLimit.isEmpty()) 986 | devLine += QString(",memory=") + dev.memLimit; 987 | if(dev.batch) 988 | devLine += QString(",batch-size=") + QString::number(dev.batch); 989 | if(dev.iters) 990 | devLine += QString(",iter-count=") + QString::number(dev.iters); 991 | if(dev.outputs) 992 | devLine += QString(",grouping=") + QString::number(dev.outputs); 993 | list << "--opencl" << devLine; 994 | } 995 | 996 | if(settings.chunkReadThreads() >= 3) 997 | env.insert("UV_THREADPOOL_SIZE", QString::number(settings.chunkReadThreads()+2)); 998 | 999 | return list; 1000 | } 1001 | QByteArray MainWindow::getCmdFilelist(bool nullSep) const 1002 | { 1003 | QByteArray fileList; 1004 | fileList.reserve(par2SrcFiles.size() * 256); // rough allocation 1005 | auto it = QHashIterator(par2SrcFiles); 1006 | while(it.hasNext()) { 1007 | it.next(); 1008 | auto file = it.value().canonicalFilePath(); 1009 | fileList.append(file.replace("/", QDir::separator()).toUtf8()); // TODO: do we want to support UCS2 in case of badly encoded filenames? does that even work in Qt? 1010 | if(nullSep) 1011 | fileList.append(static_cast(0)); 1012 | else 1013 | fileList.append("\r\n", 2); 1014 | } 1015 | return fileList; 1016 | } 1017 | 1018 | void MainWindow::on_txtSourcePath_editingFinished() 1019 | { 1020 | // TODO: simply losing focus sets this - it should only be set if some change was made 1021 | srcBaseChosen = !ui->txtSourcePath->text().isEmpty(); 1022 | reloadSourceFiles(); 1023 | updateDestPreview(); 1024 | } 1025 | 1026 | 1027 | void MainWindow::on_btnSourceRefresh_clicked() 1028 | { 1029 | ProgressDialog progress(this, tr("Refreshing file info...")); 1030 | progress.setMaximum(par2SrcFiles.size()); 1031 | 1032 | auto keys = par2SrcFiles.keys(); 1033 | bool changed = false; 1034 | for(const auto& key : keys) { 1035 | if(progress.wasCanceled()) break; 1036 | changed = changed || par2SrcFiles[key].refresh(); 1037 | if(!par2SrcFiles[key].exists()) 1038 | par2SrcFiles.remove(key); 1039 | progress.inc(); 1040 | } 1041 | progress.end(); 1042 | if(changed) { 1043 | reloadSourceFiles(); 1044 | updateSrcFilesState(); 1045 | } 1046 | } 1047 | 1048 | 1049 | void MainWindow::on_txtDestFiles_valueChanged(int arg1) 1050 | { 1051 | ui->optDestFiles->blockSignals(true); 1052 | ui->optDestFiles->setChecked(true); 1053 | ui->optDestFiles->blockSignals(false); 1054 | 1055 | int slices = ui->txtOutsliceCount->value(); 1056 | if(arg1 > slices) arg1 = slices; 1057 | int slicesPerFile = (slices + arg1-1) / arg1; 1058 | ui->txtDestCount->blockSignals(true); 1059 | ui->txtDestCount->setValue(slicesPerFile); 1060 | ui->txtDestCount->blockSignals(false); 1061 | // TODO: the following needs to include PAR2 overheads! 1062 | ui->txtDestSize->setBytesApprox(slicesPerFile * par2SliceSize, true); 1063 | 1064 | if(ui->stkDestSizing->currentIndex() == 1) 1065 | updateDestPreview(); 1066 | } 1067 | void MainWindow::on_txtDestCount_valueChanged(int arg1) 1068 | { 1069 | ui->optDestCount->blockSignals(true); 1070 | ui->optDestCount->setChecked(true); 1071 | ui->optDestCount->blockSignals(false); 1072 | 1073 | int slices = ui->txtOutsliceCount->value(); 1074 | if(arg1 > slices) arg1 = slices; 1075 | int files = (slices + arg1-1) / arg1; 1076 | ui->txtDestFiles->blockSignals(true); 1077 | ui->txtDestFiles->setValue(files); 1078 | ui->txtDestFiles->blockSignals(false); 1079 | 1080 | int slicesPerFile = (slices + files-1) / files; 1081 | // TODO: the following needs to include PAR2 overheads! 1082 | ui->txtDestSize->setBytesApprox(slicesPerFile * par2SliceSize, true); 1083 | 1084 | if(ui->stkDestSizing->currentIndex() == 1) 1085 | updateDestPreview(); 1086 | } 1087 | void MainWindow::on_txtDestSize_valueChanged(quint64 size, bool finished) 1088 | { 1089 | if(finished) { 1090 | if(!ui->optDestSize->isChecked()) return; 1091 | } else { 1092 | ui->optDestSize->blockSignals(true); 1093 | ui->optDestSize->setChecked(true); 1094 | ui->optDestSize->blockSignals(false); 1095 | } 1096 | 1097 | // TODO: the following needs to include PAR2 overheads! 1098 | int slices = ui->txtOutsliceCount->value(); 1099 | int slicesPerFile = (size + par2SliceSize/2) / par2SliceSize; // we'll do rounding here to be consistent with the other size option; ParPar allows all ceil/floor/round 1100 | if(slicesPerFile > slices) { 1101 | slicesPerFile = slices; 1102 | if(finished) { 1103 | quint64 newSize = slices * par2SliceSize; 1104 | ui->txtDestSize->setBytes(newSize, true); 1105 | return; 1106 | } 1107 | } 1108 | if(slicesPerFile < 1) { 1109 | slicesPerFile = 1; 1110 | if(finished) { 1111 | ui->txtDestSize->setBytes(par2SliceSize, true); 1112 | return; 1113 | } 1114 | } 1115 | int files = (slices + slicesPerFile-1) / slicesPerFile; 1116 | ui->txtDestFiles->blockSignals(true); 1117 | ui->txtDestFiles->setValue(files); 1118 | ui->txtDestFiles->blockSignals(false); 1119 | 1120 | slicesPerFile = (slices + files-1) / files; 1121 | ui->txtDestCount->blockSignals(true); 1122 | ui->txtDestCount->setValue(slicesPerFile); 1123 | ui->txtDestCount->blockSignals(false); 1124 | 1125 | if(ui->stkDestSizing->currentIndex() == 1) 1126 | updateDestPreview(); 1127 | } 1128 | 1129 | 1130 | void MainWindow::on_txtDestMaxCount_valueChanged(int arg1) 1131 | { 1132 | ui->optDestMaxCount->blockSignals(true); 1133 | ui->optDestMaxCount->setChecked(true); 1134 | ui->optDestMaxCount->blockSignals(false); 1135 | 1136 | if(arg1 > 32768) arg1 = 32768; 1137 | // TODO: need to include PAR2 overheads 1138 | ui->txtDestMaxSize->setBytesApprox(arg1 * par2SliceSize, true); 1139 | 1140 | if(ui->stkDestSizing->currentIndex() == 3) 1141 | updateDestPreview(); 1142 | } 1143 | void MainWindow::on_txtDestMaxSize_valueChanged(quint64 size, bool finished) 1144 | { 1145 | if(finished) { 1146 | if(!ui->optDestMaxSize->isChecked()) return; 1147 | } else { 1148 | ui->optDestMaxSize->blockSignals(true); 1149 | ui->optDestMaxSize->setChecked(true); 1150 | ui->optDestMaxSize->blockSignals(false); 1151 | } 1152 | 1153 | // TODO: need to include PAR2 overheads 1154 | int slicesFile = (size + par2SliceSize/2) / par2SliceSize; 1155 | if(slicesFile > 32768) { 1156 | slicesFile = 32768; 1157 | if(finished) { 1158 | ui->txtDestMaxSize->setBytes(32768 * par2SliceSize, true); 1159 | return; 1160 | } 1161 | } 1162 | ui->txtDestMaxCount->blockSignals(true); 1163 | ui->txtDestMaxCount->setValue(slicesFile); 1164 | ui->txtDestMaxCount->blockSignals(false); 1165 | 1166 | if(ui->stkDestSizing->currentIndex() == 3) 1167 | updateDestPreview(); 1168 | } 1169 | 1170 | 1171 | 1172 | void MainWindow::on_btnCopyCmd_clicked() 1173 | { 1174 | QString cmd = Settings::getInstance().parparBin().join(" "); 1175 | 1176 | QHash env; 1177 | auto args = getCmdArgs(env); 1178 | for(const auto& arg : args) 1179 | cmd += QString(" ") + escapeShellArg(arg); 1180 | 1181 | // input files 1182 | QString fileList; 1183 | auto it = QHashIterator(par2SrcFiles); 1184 | while(it.hasNext()) { 1185 | it.next(); 1186 | auto file = it.value().canonicalFilePath(); 1187 | fileList += QString(" ") + escapeShellArg(file.replace("/", QDir::separator())); 1188 | } 1189 | 1190 | if(!env.empty()) { 1191 | auto it = QHashIterator(env); 1192 | QString envStr = ""; 1193 | while(it.hasNext()) { 1194 | it.next(); 1195 | #ifdef Q_OS_WINDOWS 1196 | envStr += QString("SET %1=%2\r\n").arg(it.key(), it.value()); 1197 | #else 1198 | envStr += it.key() + "=" + escapeShellArg(it.value()) + " "; 1199 | #endif 1200 | } 1201 | cmd = envStr + cmd; 1202 | } 1203 | 1204 | #ifdef Q_OS_WINDOWS 1205 | // handle max limits, from https://stackoverflow.com/questions/3205027/maximum-length-of-command-line-string 1206 | if(cmd.length()+fileList.length() > 8191) { 1207 | if(cmd.length() < 7500) { 1208 | // offer to write out file list 1209 | auto answer = QMessageBox::warning(this, tr("Copy Command"), tr("The generated command would likely exceed Windows' maximum command length. Would you like to write the list of files to a file, and reference that instead?"), QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); 1210 | if(answer == QMessageBox::Cancel) return; 1211 | if(answer == QMessageBox::Yes) { 1212 | QTemporaryFile listFile(QDir::tempPath() + "/parpargui-filelist.txt"); 1213 | listFile.setAutoRemove(false); 1214 | listFile.open(); 1215 | listFile.write(getCmdFilelist(false)); 1216 | listFile.close(); 1217 | QGuiApplication::clipboard()->setText(cmd + " --input-file " + listFile.fileName().replace("/", QDir::separator())); 1218 | QMessageBox::information(this, tr("Copy Command"), tr("The ParPar command has been copied to the clipboard.")); 1219 | 1220 | return; 1221 | } 1222 | } 1223 | 1224 | QGuiApplication::clipboard()->setText(cmd + fileList); 1225 | QMessageBox::warning(this, tr("Copy Command"), tr("The ParPar command has been copied to the clipboard, but may exceed Windows' maximum command length.")); 1226 | return; 1227 | } 1228 | #endif 1229 | 1230 | QGuiApplication::clipboard()->setText(cmd + fileList); 1231 | QMessageBox::information(this, tr("Copy Command"), tr("The ParPar command has been copied to the clipboard.")); 1232 | } 1233 | 1234 | QList MainWindow::getOutputFiles() 1235 | { 1236 | if(par2FileCount == 0) 1237 | return outPreview.getOutputList(0, 0, 0, 0); 1238 | 1239 | int sliceCount = ui->txtOutsliceCount->value(); 1240 | int sliceLimit = 32768; 1241 | int distMode = ui->cboDestDist->currentIndex(); 1242 | if(distMode == 1) 1243 | sliceLimit = ui->txtDestFiles->value(); 1244 | if(distMode == 3) 1245 | sliceLimit = ui->txtDestMaxCount->value(); 1246 | return outPreview.getOutputList(sliceCount, distMode, sliceLimit, ui->txtDestOffset->value()); 1247 | } 1248 | 1249 | void MainWindow::on_btnCreate_clicked() 1250 | { 1251 | if(!checkSourceFileCount(tr("Create PAR2"))) 1252 | return; 1253 | 1254 | // check dupe filenames (should only be possible if path names are discarded, but always double-check anyway) 1255 | QSet srcNames; 1256 | QSet dupeNames; 1257 | QHashIterator it(par2SrcFiles); 1258 | while(it.hasNext()) { 1259 | it.next(); 1260 | const auto& name = it.value().par2name; 1261 | if(srcNames.contains(name)) 1262 | dupeNames.insert(name); 1263 | else 1264 | srcNames.insert(name); 1265 | } 1266 | if(dupeNames.size() > 0) { 1267 | QMessageBox::warning(this, tr("Create PAR2"), tr("%1 duplicate file name(s) were found in the source file list. Please remove all duplicate names before proceeding.") 1268 | .arg(dupeNames.size())); 1269 | return; 1270 | } 1271 | 1272 | 1273 | QFileInfo output(ui->txtDestFile->text()); 1274 | auto outputFiles = getOutputFiles(); 1275 | 1276 | QString outputBaseName = output.fileName(); 1277 | if(outputBaseName.endsWith(".par2", Qt::CaseInsensitive)) 1278 | outputBaseName = outputBaseName.left(outputBaseName.length()-5); 1279 | int sliceCount = ui->txtOutsliceCount->value(); 1280 | int distMode = ui->cboDestDist->currentIndex(); 1281 | 1282 | QStringList outputFilenames; 1283 | outputFilenames.reserve(outputFiles.count()); 1284 | // check if any file would be overwritten 1285 | QString exists; 1286 | bool tooLong = false; 1287 | QDir outputDir = output.dir(); 1288 | for(const auto& outFile : outputFiles) { 1289 | QString name = outputBaseName + Par2OutInfo::fileExt(outFile.count, outFile.offset, sliceCount); 1290 | if(distMode == 0) 1291 | name = output.fileName(); 1292 | if(outputDir.exists(name)) { 1293 | exists += QString("\n") + name; 1294 | } 1295 | outputFilenames.append(name); 1296 | 1297 | // assume Windows allows 255 UCS2 chars, otherwise 255 bytes, in the file name 1298 | #ifdef _WINDOWS 1299 | if(name.length() > 255) 1300 | #else 1301 | if(name.toLocal8Bit().length() > 255) 1302 | #endif 1303 | tooLong = true; 1304 | } 1305 | if(tooLong) { 1306 | QMessageBox::warning(this, tr("Create PAR2"), tr("One or more output file names are too long. Please shorten the output file name.")); 1307 | return; 1308 | } 1309 | if(!exists.isEmpty()) { 1310 | auto answer = QMessageBox::warning(this, tr("Create PAR2"), tr("The following output file(s) already exist. Do you want to overwrite them?%1") 1311 | .arg(exists), QMessageBox::Yes | QMessageBox::No); 1312 | if(answer != QMessageBox::Yes) return; 1313 | } 1314 | 1315 | // ensure directories for output exist 1316 | if(!outputDir.exists(".") && !outputDir.mkpath(".")) { 1317 | QMessageBox::critical(this, tr("Create PAR2"), tr("Failed to create directory for output.")); 1318 | return; 1319 | } 1320 | 1321 | // launch progress window 1322 | QHash env; 1323 | auto args = getCmdArgs(env); 1324 | CreateProgress w(this); 1325 | w.run(args, env, getCmdFilelist(true), output.fileName(), output.absolutePath(), outputFilenames); 1326 | w.exec(); 1327 | } 1328 | 1329 | 1330 | void MainWindow::on_txtDestFile_textEdited(const QString &arg1) 1331 | { 1332 | destFileChosen = !arg1.isEmpty(); 1333 | updateSrcFilesState(); 1334 | } 1335 | 1336 | void MainWindow::autoSelectDestFile() 1337 | { 1338 | if(destFileChosen) return; 1339 | if(par2SrcFiles.isEmpty()) return; 1340 | 1341 | QString target; 1342 | // if single file, use that name 1343 | if(par2SrcFiles.size() == 1) { 1344 | const auto& file = par2SrcFiles.cbegin().value(); 1345 | target = QDir(file.canonicalPath()).absoluteFilePath(Par2OutInfo::nameSafeLen(file.completeBaseName())); 1346 | } else { 1347 | // otherwise, use common path 1348 | QDir dir(ui->txtSourcePath->text()); 1349 | if(dir.dirName().isEmpty()) return; // root directory - no name available 1350 | target = QDir(dir.canonicalPath()).absoluteFilePath(Par2OutInfo::nameSafeLen(dir.dirName())); 1351 | } 1352 | 1353 | target.replace("/", QDir::separator()); 1354 | 1355 | if(QFile::exists(target + ".par2")) { 1356 | for(int i=2; i<10; i++) { 1357 | QString testName = target + " - " + QString::number(i) + ".par2"; 1358 | if(!QFile::exists(testName)) { 1359 | ui->txtDestFile->setText(testName); 1360 | return; 1361 | } 1362 | } 1363 | } 1364 | ui->txtDestFile->setText(target + ".par2"); 1365 | } 1366 | 1367 | bool MainWindow::checkSourceFileCount(const QString& title) 1368 | { 1369 | if(par2FileCount > 32768) { 1370 | QMessageBox::warning(this, 1371 | title.isEmpty() ? tr("Add source files") : title, 1372 | tr("PAR2 supports a maximum of 32768 (non-empty) files per archive. Currently, there are %1 files loaded. Please remove files to bring the count under the limit before proceeding.") 1373 | .arg(par2FileCount)); 1374 | return false; 1375 | } 1376 | if(par2FileCount > optionSliceLimit) { 1377 | QMessageBox::warning(this, 1378 | title.isEmpty() ? tr("Add source files") : title, 1379 | tr("A slice count limit of %1 has been set, which is less than the %2 currently loaded files. Please remove files to bring the count under the limit before proceeding.") 1380 | .arg(optionSliceLimit, par2FileCount)); 1381 | return false; 1382 | } 1383 | return true; 1384 | } 1385 | 1386 | void MainWindow::updateSrcFilesState() 1387 | { 1388 | bool hasFiles = !par2SrcFiles.isEmpty(); 1389 | bool hasSource = par2FileCount > 0; 1390 | bool hasDest = !ui->txtDestFile->text().isEmpty(); 1391 | ui->grpSrcData->setEnabled(hasSource); 1392 | ui->grpRecData->setEnabled(hasSource); 1393 | //ui->btnDestPreview->setEnabled(hasSource); 1394 | ui->btnCopyCmd->setEnabled(hasFiles && hasDest); 1395 | ui->btnCreate->setEnabled(hasFiles && hasDest); 1396 | 1397 | QString infoStr; 1398 | if(!par2SrcFiles.isEmpty()) { 1399 | if(par2SrcFiles.size() > par2FileCount) { 1400 | infoStr = tr("%1 / %2+%3 file(s)").arg( 1401 | friendlySize(par2SrcSize), 1402 | QLocale().toString(par2FileCount), 1403 | QLocale().toString(par2SrcFiles.size()-par2FileCount)); 1404 | } else { 1405 | infoStr = tr("%1 / %2 file(s)").arg( 1406 | friendlySize(par2SrcSize), 1407 | QLocale().toString(par2SrcFiles.size())); 1408 | } 1409 | } 1410 | ui->lblSourceInfo->setText(infoStr); 1411 | ui->lblSourceInfo2->setText(infoStr); 1412 | 1413 | if(hasSource) { 1414 | updateInsliceInfo(); 1415 | // the update will appropriately enable the sizing controls 1416 | } else { 1417 | ui->cboDestDist->setEnabled(hasSource); 1418 | ui->txtDestOffset->setEnabled(hasSource); 1419 | ui->stkDestSizing->setEnabled(hasSource); 1420 | updateDestPreview(); 1421 | } 1422 | } 1423 | 1424 | 1425 | void MainWindow::on_optInsliceSize_toggled(bool checked) 1426 | { 1427 | if(!checked) return; 1428 | 1429 | int value = ui->txtInsliceCount->value(); 1430 | par2SliceSize = Par2Calc::sliceSizeFromCount(value, optionSliceMultiple, optionSliceLimit, par2SrcFiles, par2FileCount); 1431 | ui->txtInsliceSize->setBytes(par2SliceSize, true); 1432 | } 1433 | void MainWindow::on_optInsliceCount_toggled(bool checked) 1434 | { 1435 | if(!checked) return; 1436 | 1437 | on_txtInsliceCount_editingFinished(); 1438 | } 1439 | void MainWindow::updateInsliceInfo() 1440 | { 1441 | if(par2FileCount < 1) return; 1442 | ui->txtInsliceCount->blockSignals(true); 1443 | ui->txtInsliceCount->setMinimum(par2FileCount); 1444 | ui->txtInsliceCount->setMaximum(Par2Calc::maxSliceCount(optionSliceMultiple, optionSliceLimit, par2SrcFiles)); 1445 | ui->txtInsliceCount->blockSignals(false); 1446 | 1447 | if(Settings::getInstance().allocSliceMode() == SettingsDefaultAllocIn::ALLOC_IN_RATIO) { 1448 | double _count = (double)par2SrcSize; 1449 | _count = sqrt(_count * Settings::getInstance().allocSliceRatio() / 100); 1450 | int count = (std::min)((int)round(_count), optionSliceLimit); 1451 | if(count < 1) count = 1; 1452 | ui->txtInsliceCount->setValue(count); 1453 | ui->optInsliceCount->setChecked(true); 1454 | on_txtInsliceCount_editingFinished(); 1455 | return; 1456 | } 1457 | 1458 | if(ui->optInsliceCount->isChecked()) 1459 | on_txtInsliceCount_editingFinished(); 1460 | if(ui->optInsliceSize->isChecked()) 1461 | on_txtInsliceSize_valueChanged(ui->txtInsliceSize->getBytes(), true); 1462 | } 1463 | 1464 | 1465 | 1466 | void MainWindow::on_optOutsliceRatio_toggled(bool checked) 1467 | { 1468 | if(!checked) return; 1469 | 1470 | on_txtOutsliceRatio_editingFinished(); 1471 | } 1472 | void MainWindow::on_optOutsliceCount_toggled(bool checked) 1473 | { 1474 | if(!checked) return; 1475 | 1476 | txtOutsliceCount_updated(true); 1477 | } 1478 | void MainWindow::on_optOutsliceSize_toggled(bool checked) 1479 | { 1480 | if(!checked) return; 1481 | 1482 | on_txtOutsliceSize_valueChanged(ui->txtOutsliceSize->getBytes(), true); 1483 | } 1484 | void MainWindow::updateOutsliceInfo(bool setMax) 1485 | { 1486 | int sliceCount = ui->txtInsliceCount->value(); 1487 | 1488 | // also updates the 'padding' info 1489 | quint64 padding = sliceCount * par2SliceSize - par2SrcSize; 1490 | ui->lblInslicePadding->setText(friendlySize(padding) + " (" + QLocale().toString((double)(padding * 100) / (par2SrcSize + padding), 'f', 2) + "%)"); 1491 | 1492 | double ratioMax = 6553500.0 / sliceCount; 1493 | ui->txtOutsliceRatio->blockSignals(true); 1494 | ui->txtOutsliceRatio->setMaximum(setMax ? ratioMax : 6553500.0); 1495 | ui->txtOutsliceRatio->blockSignals(false); 1496 | if(ui->optOutsliceRatio->isChecked()) { 1497 | if(setMax) 1498 | on_txtOutsliceRatio_editingFinished(); 1499 | else 1500 | on_txtOutsliceRatio_valueChanged(ui->txtOutsliceRatio->value()); 1501 | } 1502 | if(ui->optOutsliceCount->isChecked()) 1503 | on_txtOutsliceCount_valueChanged(ui->txtOutsliceCount->value()); 1504 | if(ui->optOutsliceSize->isChecked()) 1505 | on_txtOutsliceSize_valueChanged(ui->txtOutsliceSize->getBytes(), true); 1506 | } 1507 | 1508 | void MainWindow::on_optDestFiles_toggled(bool checked) 1509 | { 1510 | if(!checked) return; 1511 | 1512 | on_txtDestFiles_valueChanged(ui->txtDestFiles->value()); 1513 | } 1514 | void MainWindow::on_optDestCount_toggled(bool checked) 1515 | { 1516 | if(!checked) return; 1517 | 1518 | on_txtDestCount_valueChanged(ui->txtDestCount->value()); 1519 | } 1520 | void MainWindow::on_optDestSize_toggled(bool checked) 1521 | { 1522 | if(!checked) return; 1523 | 1524 | int slices = ui->txtDestCount->value(); 1525 | // TODO: include PAR2 overhead 1526 | ui->txtDestSize->setBytes(slices * par2SliceSize); 1527 | } 1528 | void MainWindow::on_optDestMaxLfile_toggled(bool checked) 1529 | { 1530 | if(!checked) return; 1531 | 1532 | quint64 maxSize = 0; 1533 | for(const auto& file : qAsConst(par2SrcFiles)) { 1534 | if(file.size() > maxSize) 1535 | maxSize = file.size(); 1536 | } 1537 | int slices = (maxSize + par2SliceSize-1) / par2SliceSize; 1538 | if(slices > 32768) slices = 32768; 1539 | ui->txtDestMaxCount->blockSignals(true); 1540 | ui->txtDestMaxCount->setValue(slices); 1541 | ui->txtDestMaxCount->blockSignals(false); 1542 | 1543 | ui->txtDestMaxSize->setBytesApprox(slices * par2SliceSize, true); 1544 | 1545 | if(ui->stkDestSizing->currentIndex() == 3) 1546 | updateDestPreview(); 1547 | } 1548 | void MainWindow::on_optDestMaxCount_toggled(bool checked) 1549 | { 1550 | if(!checked) return; 1551 | on_txtDestMaxCount_valueChanged(ui->txtDestMaxCount->value()); 1552 | } 1553 | void MainWindow::on_optDestMaxSize_toggled(bool checked) 1554 | { 1555 | if(!checked) return; 1556 | 1557 | int slices = ui->txtDestMaxCount->value(); 1558 | ui->txtDestMaxSize->setBytes(slices * par2SliceSize); 1559 | } 1560 | 1561 | void MainWindow::updateDestInfo(bool setMax) 1562 | { 1563 | int slices = ui->txtOutsliceCount->value(); 1564 | ui->cboDestDist->setEnabled(slices > 0); 1565 | ui->txtDestOffset->setEnabled(slices > 0); 1566 | ui->stkDestSizing->setEnabled(slices > 0); 1567 | ui->txtDestOffset->setMaximum(65535 - (setMax ? slices : 0)); 1568 | if(slices > 0) { 1569 | ui->txtDestFiles->blockSignals(true); 1570 | ui->txtDestFiles->setMaximum(setMax ? slices : 65535); 1571 | ui->txtDestFiles->blockSignals(false); 1572 | ui->txtDestCount->blockSignals(true); 1573 | ui->txtDestCount->setMaximum(setMax ? slices : 65535); 1574 | ui->txtDestCount->blockSignals(false); 1575 | if(ui->optDestFiles->isChecked()) 1576 | on_txtDestFiles_valueChanged(ui->txtDestFiles->value()); 1577 | if(ui->optDestCount->isChecked()) 1578 | on_txtDestCount_valueChanged(ui->txtDestCount->value()); 1579 | if(ui->optDestSize->isChecked()) 1580 | on_txtDestSize_valueChanged(ui->txtDestSize->getBytes(), setMax); 1581 | if(ui->optDestMaxLfile->isChecked()) 1582 | on_optDestMaxLfile_toggled(true); 1583 | if(ui->optDestMaxCount->isChecked()) 1584 | on_txtDestMaxCount_valueChanged(ui->txtDestMaxCount->value()); 1585 | if(ui->optDestMaxSize->isChecked()) 1586 | on_txtDestMaxSize_valueChanged(ui->txtDestMaxSize->getBytes(), setMax); 1587 | 1588 | int sliceDist = ui->stkDestSizing->currentIndex(); 1589 | if(sliceDist == 0 || sliceDist == 2) 1590 | updateDestPreview(); 1591 | } else 1592 | updateDestPreview(); 1593 | } 1594 | 1595 | void MainWindow::updateDestPreview() 1596 | { 1597 | if(!ui->btnDestPreview->isChecked()) return; 1598 | if(par2SrcFiles.isEmpty()) { 1599 | ui->tvDest->clear(); 1600 | return; 1601 | } 1602 | 1603 | // TODO: consider moving this to a thread as updates can be slow 1604 | int sliceCount = ui->txtOutsliceCount->value(); 1605 | int distMode = ui->cboDestDist->currentIndex(); 1606 | const auto files = getOutputFiles(); 1607 | 1608 | QString fileName = QFileInfo(ui->txtDestFile->text()).fileName(); 1609 | QString baseName = fileName; 1610 | if(baseName.endsWith(".par2", Qt::CaseInsensitive)) 1611 | baseName = baseName.left(baseName.length()-5); 1612 | 1613 | QList items; 1614 | items.reserve(files.size()); 1615 | QStringList itemText{"", "", "", ""}; 1616 | if(distMode == 0) 1617 | itemText[0] = fileName; 1618 | else { 1619 | itemText[0] = baseName; 1620 | itemText[0].reserve(baseName.length() + 20); // ".vol12345+12345.par2".length == 20 1621 | } 1622 | 1623 | // input efficiency * par2SliceSize * 100 1624 | double efficiencyCoeff = (double)(par2SrcSize *100) / ui->txtInsliceCount->value(); 1625 | QLocale locale; 1626 | 1627 | quint64 lastSize = 0; 1628 | double lastEfficiency = 0; 1629 | quint64 totalSize = 0; 1630 | for(const auto& file : files) { 1631 | if(distMode != 0) { 1632 | itemText[0].replace(baseName.length(), 20, Par2OutInfo::fileExt(file.count, file.offset, sliceCount)); 1633 | } 1634 | if(lastSize != file.size) { 1635 | // for large number of files (slowest case), most files will have the same size/slices, so reuse values in such case 1636 | itemText[1] = friendlySize(file.size); 1637 | itemText[2] = locale.toString(file.count); 1638 | lastEfficiency = (double)(file.count * efficiencyCoeff) / file.size; 1639 | itemText[3] = locale.toString(lastEfficiency, 'f', 2) + "%"; 1640 | lastSize = file.size; 1641 | } 1642 | totalSize += file.size; 1643 | auto item = new OutPreviewListItem(static_cast(nullptr), itemText); 1644 | item->setTextAlignment(1, Qt::AlignRight); 1645 | item->setTextAlignment(2, Qt::AlignRight); 1646 | item->setTextAlignment(3, Qt::AlignRight); 1647 | // sort keys 1648 | item->setData(0, Qt::UserRole, file.count == 0 && file.offset == 0 ? -1 : file.offset); 1649 | item->setData(1, Qt::UserRole, file.size); 1650 | item->setData(2, Qt::UserRole, file.count); 1651 | item->setData(3, Qt::UserRole, lastEfficiency); 1652 | items.append(item); 1653 | } 1654 | 1655 | // total item 1656 | if(items.count() > 1) { 1657 | itemText[0] = tr("[Total]"); 1658 | itemText[1] = friendlySize(totalSize); 1659 | itemText[2] = locale.toString(sliceCount); 1660 | lastEfficiency = (double)(sliceCount * efficiencyCoeff) / totalSize; 1661 | itemText[3] = locale.toString(lastEfficiency, 'f', 2) + "%"; 1662 | auto item = new OutPreviewListItem(static_cast(nullptr), itemText); 1663 | item->setTextAlignment(1, Qt::AlignRight); 1664 | item->setTextAlignment(2, Qt::AlignRight); 1665 | item->setTextAlignment(3, Qt::AlignRight); 1666 | QFont boldFont; 1667 | boldFont.setBold(true); 1668 | item->setFont(0, boldFont); 1669 | item->setFont(1, boldFont); 1670 | item->setFont(2, boldFont); 1671 | item->setFont(3, boldFont); 1672 | // sort keys 1673 | item->setData(0, Qt::UserRole, 65536); 1674 | item->setData(1, Qt::UserRole, totalSize); 1675 | item->setData(2, Qt::UserRole, sliceCount); 1676 | item->setData(3, Qt::UserRole, lastEfficiency); 1677 | items.append(item); 1678 | } 1679 | 1680 | ui->tvDest->setUpdatesEnabled(false); 1681 | ui->tvDest->clear(); // TODO: clear seems to be very slow 1682 | ui->tvDest->addTopLevelItems(items); 1683 | ui->tvDest->setUpdatesEnabled(true); 1684 | } 1685 | 1686 | 1687 | void MainWindow::on_txtDestOffset_valueChanged(int arg1) 1688 | { 1689 | updateDestPreview(); 1690 | } 1691 | 1692 | 1693 | 1694 | 1695 | void MainWindow::on_stkSource_filesDropped(const QStringList& files) 1696 | { 1697 | bool clearExisting = ui->stkSource->currentIndex() == 0; 1698 | 1699 | if(clearExisting) { 1700 | par2SrcFiles.clear(); 1701 | srcBaseChosen = false; 1702 | } 1703 | sourceAddFiles(files); 1704 | if(clearExisting || ui->txtDestFile->text().isEmpty()) 1705 | autoSelectDestFile(); 1706 | checkSourceFileCount(); 1707 | updateSrcFilesState(); 1708 | } 1709 | 1710 | -------------------------------------------------------------------------------- /mainwindow.h: -------------------------------------------------------------------------------- 1 | #ifndef MAINWINDOW_H 2 | #define MAINWINDOW_H 3 | 4 | #include 5 | #include 6 | #include "optionsdialog.h" 7 | #include "par2outinfo.h" 8 | #include "sourcefile.h" 9 | 10 | QT_BEGIN_NAMESPACE 11 | namespace Ui { class MainWindow; } 12 | QT_END_NAMESPACE 13 | 14 | class MainWindow : public QMainWindow 15 | { 16 | Q_OBJECT 17 | 18 | public: 19 | MainWindow(QWidget *parent = nullptr); 20 | ~MainWindow(); 21 | 22 | protected: 23 | void resizeEvent(QResizeEvent *event); 24 | void showEvent(QShowEvent *event); 25 | 26 | private slots: 27 | void on_btnAbout_clicked(); 28 | 29 | void on_btnOptions_clicked(); 30 | 31 | void on_txtInsliceCount_valueChanged(int arg1); 32 | 33 | void on_btnSourceAdd_clicked(); 34 | 35 | void on_btnSourceAddDir_clicked(); 36 | 37 | void on_btnSourceDel_clicked(); 38 | 39 | void on_btnSourcePathBrowse_clicked(); 40 | 41 | void on_btnDestPreview_clicked(); 42 | 43 | void on_cboSourcePaths_currentIndexChanged(int index); 44 | 45 | void on_btnDestFileBrowse_clicked(); 46 | 47 | void on_txtOutsliceRatio_valueChanged(double arg1); 48 | 49 | void on_txtOutsliceCount_valueChanged(int arg1); 50 | 51 | void on_cboDestDist_currentIndexChanged(int index); 52 | 53 | void on_btnSourceAdv_clicked(); 54 | 55 | void on_btnSourceAdv2_clicked(); 56 | 57 | void on_btnComment_clicked(); 58 | 59 | void on_btnSourceSetFiles_clicked(); 60 | 61 | void on_btnSourceSetDir_clicked(); 62 | 63 | void on_txtSourcePath_editingFinished(); 64 | 65 | void on_btnSourceRefresh_clicked(); 66 | 67 | void on_txtDestFiles_valueChanged(int arg1); 68 | 69 | void on_txtDestCount_valueChanged(int arg1); 70 | 71 | void on_txtDestMaxCount_valueChanged(int arg1); 72 | 73 | void on_btnCopyCmd_clicked(); 74 | 75 | void on_btnCreate_clicked(); 76 | 77 | void on_txtDestFile_textEdited(const QString &arg1); 78 | 79 | void on_txtInsliceCount_editingFinished(); 80 | 81 | void on_optInsliceSize_toggled(bool checked); 82 | 83 | void on_optInsliceCount_toggled(bool checked); 84 | 85 | void on_txtOutsliceRatio_editingFinished(); 86 | 87 | void on_optOutsliceRatio_toggled(bool checked); 88 | 89 | void on_optOutsliceCount_toggled(bool checked); 90 | 91 | void on_optOutsliceSize_toggled(bool checked); 92 | 93 | void on_optDestFiles_toggled(bool checked); 94 | 95 | void on_optDestCount_toggled(bool checked); 96 | 97 | void on_optDestSize_toggled(bool checked); 98 | 99 | void on_optDestMaxLfile_toggled(bool checked); 100 | 101 | void on_optDestMaxCount_toggled(bool checked); 102 | 103 | void on_optDestMaxSize_toggled(bool checked); 104 | 105 | void on_txtDestOffset_valueChanged(int arg1); 106 | 107 | void on_txtOutsliceCount_editingFinished(); 108 | 109 | void on_txtInsliceSize_valueChanged(quint64 size, bool finished); 110 | 111 | void on_txtOutsliceSize_valueChanged(quint64 size, bool finished); 112 | 113 | void on_txtDestSize_valueChanged(quint64 size, bool finished); 114 | 115 | void on_txtDestMaxSize_valueChanged(quint64 size, bool finished); 116 | 117 | void on_stkSource_filesDropped(const QStringList &); 118 | 119 | private: 120 | Ui::MainWindow *ui; 121 | OptionsDialog dlgOptions; 122 | Par2OutInfo outPreview; 123 | 124 | void rescale(); 125 | void adjustExpansion(bool allowExpand); 126 | 127 | void sourceAddFiles(const QStringList& files); 128 | void sourceAddDir(const QString& dir); 129 | 130 | public: // grant access for SliceCountSpinBox / Par2OutInfo 131 | quint64 optionSliceMultiple; 132 | int optionSliceLimit; 133 | SrcFileList par2SrcFiles; 134 | quint64 par2SrcSize; // cached value 135 | int par2FileCount; // cached value 136 | quint64 par2SliceSize; // cached value - only used for recovery slice calc 137 | QString par2Comment; 138 | private: 139 | QString srcFilesCommonPath() const; 140 | void reloadSourceFiles(); 141 | 142 | QStringList getCmdArgs(QHash& env) const; 143 | QByteArray getCmdFilelist(bool nullSep) const; 144 | QList getOutputFiles(); 145 | 146 | bool srcBaseChosen; 147 | bool destFileChosen; 148 | void autoSelectDestFile(); 149 | bool checkSourceFileCount(const QString& title = QString()); 150 | void updateSrcFilesState(); 151 | 152 | void txtOutsliceCount_updated(bool editingFinished); 153 | 154 | void updateInsliceInfo(); 155 | void updateOutsliceInfo(bool setMax = true); 156 | void updateDestInfo(bool setMax = true); 157 | void updateDestPreview(); 158 | }; 159 | #endif // MAINWINDOW_H 160 | -------------------------------------------------------------------------------- /optionsdialog.cpp: -------------------------------------------------------------------------------- 1 | #include "optionsdialog.h" 2 | #include "ui_optionsdialog.h" 3 | #include 4 | #include 5 | #include 6 | #include "settings.h" 7 | #include 8 | #include 9 | #include "parparclient.h" 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include "util.h" 15 | 16 | OptionsDialog::OptionsDialog(QWidget *parent) : 17 | QDialog(parent), 18 | ui(new Ui::OptionsDialog) 19 | { 20 | ui->setupUi(this); 21 | 22 | auto arch = QSysInfo::currentCpuArchitecture(); 23 | if(arch == "arm" || arch == "arm64") { 24 | ui->cboProcKernel->addItems({"shuffle-neon", "clmul-neon"}); 25 | ui->cboHashKernel->addItems({"simd", "crc", "simd-crc"}); 26 | ui->cboMD5Kernel->addItems({"neon"}); 27 | } 28 | if(arch == "arm64") { 29 | ui->cboProcKernel->addItems({"clmul-sha3", "shuffle128-sve", "shuffle128-sve2", "shuffle2x128-sve2", "shuffle512-sve2", "clmul-sve2"}); 30 | ui->cboMD5Kernel->addItems({"sve2"}); 31 | } 32 | if(arch == "i386" || arch == "x86_64") { 33 | ui->cboProcKernel->addItems({"xor-sse", "xorjit-sse"}); 34 | if(arch == "x86_64") 35 | ui->cboProcKernel->addItems({"xorjit-avx2", "xorjit-avx512"}); 36 | ui->cboProcKernel->addItems({"shuffle-sse", "shuffle-avx", "shuffle-avx2", "shuffle-avx512", "shuffle-vbmi", "shuffle2x-avx2", "shuffle2x-avx512", "affine-sse", "affine-avx2", "affine-avx512", "affine2x-sse", "affine2x-avx2", "affine2x-avx512"}); 37 | ui->cboHashKernel->addItems({"simd", "crc", "simd-crc", "bmi", "avx512"}); 38 | ui->cboMD5Kernel->addItems({"sse", "avx2", "xop", "avx10", "avx512f", "avx512vl"}); 39 | } 40 | if(arch == "riscv32" || arch == "riscv64") { 41 | ui->cboProcKernel->addItems({"shuffle128-rvv", "clmul-rvv"}); 42 | } 43 | 44 | devicesListed = false; 45 | ui->tabWidget->setCurrentIndex(0); 46 | loadSettings(); 47 | } 48 | 49 | OptionsDialog::~OptionsDialog() 50 | { 51 | delete ui; 52 | } 53 | 54 | void OptionsDialog::loadSettings() 55 | { 56 | auto& settings = Settings::getInstance(); 57 | bool chk; 58 | 59 | bool isSystemExecutable; 60 | auto ppBin = settings.parparBin(&isSystemExecutable); 61 | if(ppBin.length() > 1) { 62 | ui->chkPathNode->setChecked(true); 63 | ui->txtPathNode->setText(ppBin[0]); 64 | ui->txtPathParPar->setText(ppBin[1]); 65 | } else if(!isSystemExecutable) { 66 | ui->txtPathParPar->setText(ppBin[0]); 67 | } 68 | ui->cboUnicode->setCurrentIndex(settings.unicode()); 69 | ui->cboCharset->setCurrentText(settings.charset()); 70 | ui->txtPacketMin->setValue(settings.packetRepMin()); 71 | ui->txtPacketMax->setValue(settings.packetRepMax()); 72 | ui->chkStdNaming->setChecked(settings.stdNaming()); 73 | 74 | chk = !settings.sliceMultiple().isEmpty(); 75 | ui->chkSliceMultiple->setChecked(chk); 76 | ui->txtSliceMultiple->setEnabled(chk); 77 | ui->txtSliceMultiple->setText(chk ? settings.sliceMultiple() : "750K"); 78 | ui->txtSliceLimit->setValue(settings.sliceLimit()); 79 | 80 | ui->cboAllocInMode->setCurrentIndex(settings.allocSliceMode()); 81 | on_cboAllocInMode_currentIndexChanged(ui->cboAllocInMode->currentIndex()); 82 | ui->txtAllocInSize->setText(settings.allocSliceSize()); 83 | ui->txtAllocInCount->setValue(settings.allocSliceCount()); 84 | ui->txtAllocInRatio->setValue(settings.allocSliceRatio()); 85 | ui->cboAllocRecMode->setCurrentIndex(settings.allocRecoveryMode()); 86 | on_cboAllocRecMode_currentIndexChanged(ui->cboAllocRecMode->currentIndex()); 87 | ui->txtAllocRecRatio->setValue(settings.allocRecoveryRatio()); 88 | ui->txtAllocRecCount->setValue(settings.allocRecoveryCount()); 89 | ui->txtAllocRecSize->setText(settings.allocRecoverySize()); 90 | 91 | ui->chkRunClose->setChecked(settings.runClose()); 92 | 93 | 94 | ui->txtReadSize->setText(settings.readSize()); 95 | ui->txtReadBufs->setValue(settings.readBuffers()); 96 | ui->txtHashQueue->setMaximum(settings.readBuffers()); 97 | ui->txtHashQueue->setValue(settings.hashQueue()); 98 | ui->cboHashKernel->setCurrentText(settings.hashMethod()); 99 | ui->txtMinChunk->setText(settings.minChunk()); 100 | ui->txtChunkReadThreads->setMaximum(settings.readBuffers()); 101 | ui->txtChunkReadThreads->setValue(settings.chunkReadThreads()); 102 | ui->txtRecBufs->setValue(settings.recBuffers()); 103 | ui->txtRecHashBatch->setMaximum(settings.recBuffers()); 104 | ui->txtRecHashBatch->setValue(settings.hashBatch()); 105 | ui->cboMD5Kernel->setCurrentText(settings.md5Method()); 106 | ui->chkOutputSync->setChecked(settings.outputSync()); 107 | 108 | chk = settings.procBatch() >= 0; 109 | ui->chkProcBatch->setChecked(chk); 110 | ui->txtProcBatch->setEnabled(chk); 111 | ui->txtProcBatch->setValue(chk ? settings.procBatch() : 12); 112 | chk = !settings.memLimit().isEmpty(); 113 | ui->chkMemLimit->setChecked(chk); 114 | ui->txtMemLimit->setEnabled(chk); 115 | ui->txtMemLimit->setText(chk ? settings.memLimit() : "256M"); 116 | chk = !settings.gfMethod().isEmpty(); 117 | ui->chkProcKernel->setChecked(chk); 118 | ui->cboProcKernel->setEnabled(chk); 119 | if(chk) ui->cboProcKernel->setCurrentText(settings.gfMethod()); 120 | chk = !settings.tileSize().isEmpty(); 121 | ui->chkTileSize->setChecked(chk); 122 | ui->txtTileSize->setEnabled(chk); 123 | ui->txtTileSize->setText(chk ? settings.tileSize() : "32K"); 124 | chk = settings.threadNum() >= 0; 125 | ui->chkThreads->setChecked(chk); 126 | ui->txtThreads->setEnabled(chk); 127 | ui->txtThreads->setValue(chk ? settings.threadNum() : QThread::idealThreadCount()); 128 | ui->txtCpuMinChunk->setText(settings.cpuMinChunk()); 129 | 130 | // TODO: load defaults for processing options (e.g. GF kernel) 131 | 132 | checkIOPreset(); 133 | 134 | openclSettings.clear(); 135 | for(const auto& dev : settings.openclDevices()) { 136 | QString key = dev.name.toLower(); 137 | if(openclSettings.contains(key)) { 138 | int idx = 1; 139 | QString idxStr("::%1"); 140 | while(openclSettings.contains(key + idxStr.arg(idx))) 141 | idx++; 142 | key += idxStr.arg(idx); 143 | } 144 | openclSettings.insert(key, dev); 145 | } 146 | 147 | ui->chkOpencl->blockSignals(ui->tabWidget->currentIndex() != 2); 148 | ui->chkOpencl->setChecked(!openclSettings.isEmpty()); 149 | ui->chkOpencl->blockSignals(false); 150 | } 151 | 152 | void OptionsDialog::on_chkProcBatch_stateChanged(int arg1) 153 | { 154 | ui->txtProcBatch->setEnabled(ui->chkProcBatch->isChecked()); 155 | } 156 | void OptionsDialog::on_chkMemLimit_stateChanged(int arg1) 157 | { 158 | ui->txtMemLimit->setEnabled(ui->chkMemLimit->isChecked()); 159 | } 160 | void OptionsDialog::on_chkProcKernel_stateChanged(int arg1) 161 | { 162 | ui->cboProcKernel->setEnabled(ui->chkProcKernel->isChecked()); 163 | } 164 | void OptionsDialog::on_chkThreads_stateChanged(int arg1) 165 | { 166 | ui->txtThreads->setEnabled(ui->chkThreads->isChecked()); 167 | } 168 | 169 | void OptionsDialog::accept() 170 | { 171 | int packetRepMin = ui->txtPacketMin->value(); 172 | if(packetRepMin < 16 && packetRepMin > ui->txtPacketMax->value()) { 173 | QMessageBox::warning(this, tr("Update Options"), tr("The packet repetition minimum value cannot exceed the maximum value")); 174 | return; 175 | } 176 | 177 | quint64 defSliceAllocSize = 0; 178 | if(ui->cboAllocInMode->currentIndex() == SettingsDefaultAllocIn::ALLOC_IN_SIZE) { 179 | defSliceAllocSize = ui->txtAllocInSize->getBytes(); 180 | if(defSliceAllocSize == 0 || (defSliceAllocSize & 3)) { 181 | QMessageBox::warning(this, tr("Update Options"), tr("The slice size must be a multiple of 4 bytes")); 182 | return; 183 | } 184 | 185 | if(ui->cboAllocRecMode->currentIndex() == SettingsDefaultAllocRec::ALLOC_REC_SIZE) { 186 | if(ui->txtAllocRecSize->getBytes() % defSliceAllocSize) { 187 | QMessageBox::warning(this, tr("Update Options"), tr("The amount of recovery data should be a multiple of the slice size")); 188 | return; 189 | } 190 | } 191 | } 192 | if(ui->cboAllocInMode->currentIndex() == SettingsDefaultAllocIn::ALLOC_IN_COUNT) { 193 | if(ui->txtAllocInCount->value() > ui->txtSliceLimit->value()) { 194 | QMessageBox::warning(this, tr("Update Options"), tr("The number of source slices cannot exceed the slice limt")); 195 | return; 196 | } 197 | } 198 | 199 | if(ui->cboAllocInMode->currentIndex() == SettingsDefaultAllocIn::ALLOC_IN_RATIO) { 200 | if(ui->txtAllocInRatio->value() <= 0.0) { 201 | QMessageBox::warning(this, tr("Update Options"), tr("The slice count÷size ratio must be greater than 0")); 202 | return; 203 | } 204 | } 205 | 206 | bool useMultiple = ui->chkSliceMultiple->isChecked(); 207 | if(useMultiple) { 208 | auto multipleSize = ui->txtSliceMultiple->getBytes(); 209 | if(multipleSize == 0 || (multipleSize & 3)) { 210 | QMessageBox::warning(this, tr("Update Options"), tr("The slice size multiple must be a multiple of 4 bytes")); 211 | return; 212 | } 213 | if(defSliceAllocSize && defSliceAllocSize % multipleSize) { 214 | QMessageBox::warning(this, tr("Update Options"), tr("The selected slice size is not a multiple of the selected slice size multiple")); 215 | return; 216 | } 217 | } 218 | 219 | if(ui->txtMinChunk->getBytes() & 1) { 220 | QMessageBox::warning(this, tr("Update Options"), tr("The Min chunk size must be a multiple of 2 bytes")); 221 | return; 222 | } 223 | 224 | if(ui->txtCpuMinChunk->getBytes() & 1) { 225 | QMessageBox::warning(this, tr("Update Options"), tr("The CPU Min chunk size must be a multiple of 2 bytes")); 226 | return; 227 | } 228 | 229 | if(ui->txtChunkReadThreads->value() > ui->txtReadBufs->value()) { 230 | QMessageBox::warning(this, tr("Update Options"), tr("The number of chunking read threads cannot exceed the number of read buffers")); 231 | return; 232 | } 233 | 234 | QList oclDevices; 235 | if(ui->chkOpencl->isChecked()) { 236 | float totalAlloc = 0; 237 | QHashIterator it(openclSettings); 238 | while(it.hasNext()) { 239 | it.next(); 240 | const auto& dev = it.value(); 241 | if(dev.alloc <= 0.0) continue; 242 | oclDevices.append(dev); 243 | totalAlloc += dev.alloc; 244 | if(sizeToBytes(dev.minChunk) & 1) { 245 | QMessageBox::warning(this, tr("Update Options"), tr("The Min chunk size for device \"%1\" must be a multiple of 2 bytes").arg(dev.name)); 246 | return; 247 | } 248 | } 249 | if(totalAlloc > 100.0) { 250 | QMessageBox::warning(this, tr("Update Options"), tr("The total allocation across OpenCL devices cannot exceed 100%")); 251 | return; 252 | } 253 | } 254 | 255 | auto& settings = Settings::getInstance(); 256 | // we won't bother verifying the existence of the binary as the user could be using system binaries 257 | const auto& currentBin = settings.parparBin(); 258 | bool useNode = ui->chkPathNode->isChecked(); 259 | bool binChanged = useNode != currentBin.length() > 1; 260 | const auto& binParpar = ui->txtPathParPar->text(); 261 | if(useNode) { 262 | const auto& binNode = ui->txtPathNode->text(); 263 | if(!binChanged && (binParpar != currentBin[1] || binNode != currentBin[0])) 264 | binChanged = true; 265 | settings.setParparBin(binParpar, binNode); 266 | } else { 267 | if(!binChanged && binParpar != currentBin[0]) 268 | binChanged = true; 269 | settings.setParparBin(binParpar); 270 | } 271 | 272 | settings.setUnicode(static_cast(ui->cboUnicode->currentIndex())); 273 | settings.setCharset(ui->cboCharset->currentText()); 274 | settings.setPacketRepMin(packetRepMin); 275 | settings.setPacketRepMax(ui->txtPacketMax->value()); 276 | settings.setStdNaming(ui->chkStdNaming->isChecked()); 277 | 278 | settings.setSliceMultple(useMultiple ? ui->txtSliceMultiple->getSizeString() : ""); 279 | settings.setSliceLimit(ui->txtSliceLimit->value()); 280 | switch(ui->cboAllocInMode->currentIndex()) { 281 | case SettingsDefaultAllocIn::ALLOC_IN_SIZE: 282 | settings.setAllocSliceSize(ui->txtAllocInSize->text()); 283 | break; 284 | case SettingsDefaultAllocIn::ALLOC_IN_COUNT: 285 | settings.setAllocSliceCount(ui->txtAllocInCount->value()); 286 | break; 287 | case SettingsDefaultAllocIn::ALLOC_IN_RATIO: 288 | settings.setAllocSliceRatio(ui->txtAllocInRatio->value()); 289 | break; 290 | } 291 | switch(ui->cboAllocRecMode->currentIndex()) { 292 | case SettingsDefaultAllocRec::ALLOC_REC_RATIO: 293 | settings.setAllocRecoveryRatio(ui->txtAllocRecRatio->value()); 294 | break; 295 | case SettingsDefaultAllocRec::ALLOC_REC_COUNT: 296 | settings.setAllocRecoveryCount(ui->txtAllocRecCount->value()); 297 | break; 298 | case SettingsDefaultAllocRec::ALLOC_REC_SIZE: 299 | settings.setAllocRecoverySize(ui->txtAllocRecSize->text()); 300 | break; 301 | } 302 | 303 | settings.setRunClose(ui->chkRunClose->isChecked()); 304 | 305 | 306 | settings.setReadSize(ui->txtReadSize->getSizeString()); 307 | settings.setReadBuffers(ui->txtReadBufs->value()); 308 | settings.setHashQueue(ui->txtHashQueue->value()); 309 | settings.setHashMethod(ui->cboHashKernel->currentText()); 310 | settings.setMinChunk(ui->txtMinChunk->getSizeString()); 311 | settings.setChunkReadThreads(ui->txtChunkReadThreads->value()); 312 | settings.setRecBuffers(ui->txtRecBufs->value()); 313 | settings.setHashBatch(ui->txtRecHashBatch->value()); 314 | settings.setMd5Method(ui->cboMD5Kernel->currentText()); 315 | settings.setOutputSync(ui->chkOutputSync->isChecked()); 316 | 317 | settings.setProcBatch(ui->chkProcBatch->isChecked() ? ui->txtProcBatch->value() : -1); 318 | settings.setMemLimit(ui->chkMemLimit->isChecked() ? ui->txtMemLimit->getSizeString() : ""); 319 | settings.setGfMethod(ui->chkProcKernel->isChecked() ? ui->cboProcKernel->currentText() : ""); 320 | settings.setTileSize(ui->chkTileSize->isChecked() ? ui->txtTileSize->getSizeString() : ""); 321 | settings.setThreadNum(ui->chkThreads->isChecked() ? ui->txtThreads->value() : -1); 322 | settings.setCpuMinChunk(ui->txtCpuMinChunk->getSizeString()); 323 | 324 | settings.setOpenclDevices(oclDevices); 325 | 326 | QDialog::accept(); 327 | emit settingsUpdated(binChanged); 328 | 329 | } 330 | 331 | 332 | void OptionsDialog::on_btnReset_clicked() 333 | { 334 | Settings::getInstance().reset(); 335 | loadSettings(); 336 | } 337 | 338 | 339 | void OptionsDialog::on_chkPathNode_toggled(bool checked) 340 | { 341 | ui->txtPathNode->setEnabled(checked); 342 | ui->btnPathNode->setEnabled(checked); 343 | } 344 | 345 | 346 | void OptionsDialog::on_btnPathParPar_clicked() 347 | { 348 | auto file = QFileDialog::getOpenFileName(this, tr("Select ParPar script or binary"), ui->txtPathParPar->text(), tr("ParPar script/binary (parpar.js;parpar" 349 | #ifdef Q_OS_WINDOWS 350 | ".exe;parpar.cmd" 351 | #endif 352 | ");;All files (*.*)")); 353 | if(!file.isEmpty()) { 354 | ui->txtPathParPar->setText(file.replace("/", QDir::separator())); 355 | 356 | if(file.endsWith(".js", Qt::CaseInsensitive)) { 357 | #ifdef Q_OS_WINDOWS 358 | // Linux may have the ability to execute scripts directly 359 | ui->chkPathNode->setChecked(true); 360 | #endif 361 | } else { 362 | ui->chkPathNode->setChecked(false); 363 | } 364 | } 365 | } 366 | 367 | 368 | void OptionsDialog::on_btnPathNode_clicked() 369 | { 370 | auto file = QFileDialog::getOpenFileName(this, tr("Select Node.js binary"), ui->txtPathNode->text(), tr("Node.js binary (node" 371 | #ifdef Q_OS_WINDOWS 372 | ".exe" 373 | #endif 374 | ");;All files (*.*)")); 375 | if(!file.isEmpty()) { 376 | ui->txtPathNode->setText(file.replace("/", QDir::separator())); 377 | } 378 | } 379 | 380 | 381 | void OptionsDialog::on_chkSliceMultiple_toggled(bool checked) 382 | { 383 | ui->txtSliceMultiple->setEnabled(checked); 384 | } 385 | 386 | 387 | void OptionsDialog::on_chkTileSize_stateChanged(int arg1) 388 | { 389 | ui->txtTileSize->setEnabled(ui->chkTileSize->isChecked()); 390 | } 391 | 392 | 393 | void OptionsDialog::on_txtReadBufs_editingFinished() 394 | { 395 | ui->txtHashQueue->setMaximum(ui->txtReadBufs->value()); 396 | ui->txtChunkReadThreads->setMaximum(ui->txtReadBufs->value()); 397 | } 398 | 399 | 400 | void OptionsDialog::on_txtRecBufs_editingFinished() 401 | { 402 | ui->txtRecHashBatch->setMaximum(ui->txtRecBufs->value()); 403 | } 404 | 405 | void OptionsDialog::rescale() 406 | { 407 | int w = ui->treeDevices->width(); 408 | ui->treeDevices->header()->setUpdatesEnabled(false); 409 | ui->treeDevices->header()->resizeSection(0, w>190 ? w-70 : 120); 410 | ui->treeDevices->header()->resizeSection(1, 60); 411 | ui->treeDevices->header()->setUpdatesEnabled(true); 412 | } 413 | 414 | void OptionsDialog::showEvent(QShowEvent *event) { 415 | QDialog::showEvent(event); 416 | //rescale(); 417 | } 418 | void OptionsDialog::resizeEvent(QResizeEvent *event) 419 | { 420 | QDialog::resizeEvent(event); 421 | rescale(); 422 | } 423 | 424 | void OptionsDialog::on_tabWidget_currentChanged(int index) 425 | { 426 | if(index == 2 && !devicesListed) { 427 | devicesListed = true; 428 | rescale(); // doesn't seem to do it on window load :| 429 | on_chkOpencl_toggled(ui->chkOpencl->isChecked()); 430 | } 431 | } 432 | 433 | static bool updatingOpenclOpt = false; 434 | 435 | void OptionsDialog::on_treeDevices_currentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *previous) 436 | { 437 | if(!current) return; 438 | int type = current->data(0, Qt::UserRole).toInt(); 439 | ui->stkComputeOpt->setCurrentIndex(type); 440 | 441 | if(type == 1) { // OpenCL device 442 | // load data 443 | const auto& opts = openclSettings.value(current->data(1, Qt::UserRole).toString(), OpenclDevice()); 444 | updatingOpenclOpt = true; 445 | ui->txtOclAlloc->setValue(opts.alloc); 446 | ui->chkOclMemory->setChecked(!opts.memLimit.isEmpty()); 447 | if(!opts.memLimit.isEmpty()) 448 | ui->txtOclMemory->setText(opts.memLimit); 449 | else 450 | ui->txtOclMemory->setText("128M"); 451 | ui->txtOclMinChunk->setText(opts.minChunk); 452 | ui->cboOclKernel->setCurrentText(opts.gfMethod); 453 | ui->chkOclBatch->setChecked(opts.batch > 0); 454 | ui->txtOclBatch->setValue(opts.batch > 0 ? opts.batch : 4); 455 | ui->chkOclIters->setChecked(opts.iters > 0); 456 | ui->txtOclIters->setValue(opts.iters > 0 ? opts.iters : 1); 457 | ui->chkOclOutputs->setChecked(opts.outputs > 0); 458 | ui->txtOclOutputs->setValue(opts.outputs > 0 ? opts.outputs : 1); 459 | updatingOpenclOpt = false; 460 | } 461 | } 462 | 463 | 464 | void OptionsDialog::on_txtPacketMin_valueChanged(int arg1) 465 | { 466 | ui->txtPacketMax->setEnabled(arg1 < 16); 467 | } 468 | 469 | static constexpr struct { 470 | quint64 readSize; 471 | int readBuffers; 472 | int hashQueue; 473 | int chunkReadThreads; 474 | int recBuffers; 475 | } ioPresets[]{ 476 | {8*1048576, 4, 3, 2, 12}, // HDD 477 | {4*1048576, 8, 5, 2, 12}, // default 478 | {2*1048576, 16, 5, 4, 16} // SSD 479 | }; 480 | 481 | void OptionsDialog::checkIOPreset() 482 | { 483 | int idx = 0; 484 | for(const auto& preset : ioPresets) { 485 | if(ui->txtReadSize->getBytes() == preset.readSize 486 | && ui->txtReadBufs->value() == preset.readBuffers 487 | && ui->txtHashQueue->value() == preset.hashQueue 488 | && ui->txtChunkReadThreads->value() == preset.chunkReadThreads 489 | && ui->txtRecBufs->value() == preset.recBuffers) 490 | break; 491 | 492 | idx++; 493 | } 494 | if(idx != ui->cboIOPreset->currentIndex()) { 495 | ui->cboIOPreset->blockSignals(true); 496 | ui->cboIOPreset->setCurrentIndex(idx); 497 | ui->cboIOPreset->blockSignals(false); 498 | } 499 | } 500 | 501 | void OptionsDialog::on_cboIOPreset_currentIndexChanged(int index) 502 | { 503 | if(index >= ui->cboIOPreset->count()-1) { 504 | // re-eval 505 | checkIOPreset(); 506 | } else { 507 | const auto& preset = ioPresets[index]; 508 | const auto blockSignals = [this](bool block) { 509 | ui->txtReadSize->blockSignals(block); 510 | ui->txtReadBufs->blockSignals(block); 511 | ui->txtHashQueue->blockSignals(block); 512 | ui->txtChunkReadThreads->blockSignals(block); 513 | ui->txtRecBufs->blockSignals(block); 514 | }; 515 | blockSignals(true); 516 | ui->txtReadSize->setBytes(preset.readSize); 517 | ui->txtReadBufs->setValue(preset.readBuffers); 518 | on_txtReadBufs_editingFinished(); // update maximums 519 | ui->txtHashQueue->setValue(preset.hashQueue); 520 | ui->txtChunkReadThreads->setValue(preset.chunkReadThreads); 521 | ui->txtRecBufs->setValue(preset.recBuffers); 522 | on_txtRecBufs_editingFinished(); 523 | blockSignals(false); 524 | } 525 | } 526 | 527 | 528 | void OptionsDialog::on_txtReadSize_valueChanged(quint64 , bool ) 529 | { 530 | checkIOPreset(); 531 | } 532 | void OptionsDialog::on_txtReadBufs_valueChanged(int arg1) 533 | { 534 | checkIOPreset(); 535 | } 536 | void OptionsDialog::on_txtHashQueue_valueChanged(int arg1) 537 | { 538 | checkIOPreset(); 539 | } 540 | void OptionsDialog::on_txtChunkReadThreads_valueChanged(int arg1) 541 | { 542 | checkIOPreset(); 543 | } 544 | void OptionsDialog::on_txtRecBufs_valueChanged(int arg1) 545 | { 546 | checkIOPreset(); 547 | } 548 | 549 | 550 | static QJsonArray oclPlatforms; 551 | void OptionsDialog::fillDeviceList(bool opencl) 552 | { 553 | auto& tree = this->ui->treeDevices; 554 | // add items 555 | QList topItems; 556 | topItems.reserve(oclPlatforms.size() + 1); 557 | 558 | auto cpuItem = new QTreeWidgetItem(static_cast(nullptr), QStringList{"CPU", "100%"}); 559 | cpuItem->setTextAlignment(1, Qt::AlignRight); 560 | cpuItem->setData(0, Qt::UserRole, 0); 561 | topItems.append(cpuItem); 562 | 563 | if(opencl) { 564 | float cpuAlloc = 100; 565 | QSet seenDevices; 566 | for(const auto& _plat : oclPlatforms) { 567 | const auto plat = _plat.toObject(); 568 | auto platItem = new QTreeWidgetItem(static_cast(nullptr), QStringList{plat.value("name").toString(), ""}); 569 | platItem->setTextAlignment(1, Qt::AlignRight); 570 | platItem->setData(0, Qt::UserRole, 2); 571 | 572 | for(const auto& _dev : plat.value("devices").toArray()) { 573 | const auto& dev = _dev.toObject(); 574 | QString name = dev.value("name").toString(); 575 | 576 | // determine device key 577 | QString key = name.toLower(); 578 | if(seenDevices.contains(key)) { 579 | int idx = 1; 580 | QString idxStr("::%1"); 581 | while(seenDevices.contains(key + idxStr.arg(idx))) 582 | idx++; 583 | key += idxStr.arg(idx); 584 | } 585 | seenDevices.insert(key); 586 | // create backing store here as well 587 | if(!openclSettings.contains(key)) 588 | openclSettings.insert(key, OpenclDevice()); 589 | openclSettings[key].name = name; 590 | 591 | auto alloc = openclSettings[key].alloc; 592 | cpuAlloc -= alloc; 593 | QString allocStr; 594 | if(alloc > 0.0) allocStr = QLocale().toString(alloc, 'f', 2) + "%"; 595 | auto devItem = new QTreeWidgetItem(platItem, QStringList{name, allocStr}); 596 | devItem->setTextAlignment(1, Qt::AlignRight); 597 | devItem->setData(0, Qt::UserRole, 1); 598 | devItem->setData(1, Qt::UserRole, key); 599 | 600 | platItem->addChild(devItem); 601 | } 602 | topItems.append(platItem); 603 | } 604 | if(cpuAlloc > 0.0) 605 | topItems[0]->setText(1, QLocale().toString(cpuAlloc, 'f', 2) + "%"); 606 | 607 | // prune unrecognised devices 608 | QMutableHashIterator it(openclSettings); 609 | while(it.hasNext()) { 610 | it.next(); 611 | if(!seenDevices.contains(it.key())) 612 | it.remove(); 613 | } 614 | } 615 | 616 | tree->setUpdatesEnabled(false); 617 | tree->clear(); 618 | tree->addTopLevelItems(topItems); 619 | for(auto item : topItems) 620 | if(item->childCount() > 0) 621 | item->setExpanded(true); 622 | tree->setCurrentItem(topItems[0]); 623 | tree->setUpdatesEnabled(true); 624 | } 625 | 626 | static bool haveOclDevices = false; // TODO: should be class local, so it gets retried on dialog reopen 627 | void OptionsDialog::loadOpenclDevices() 628 | { 629 | if(haveOclDevices) return; 630 | haveOclDevices = true; 631 | 632 | auto progress = new QProgressDialog(this); 633 | progress->setLabelText(tr("Querying OpenCL devices...")); 634 | progress->setMinimumDuration(100); 635 | progress->setValue(0); 636 | progress->setWindowModality(Qt::WindowModal); 637 | auto* pb = new QProgressBar(progress); 638 | pb->setAlignment(Qt::AlignCenter); 639 | progress->setBar(pb); 640 | progress->setRange(0, 0); 641 | 642 | auto parpar = new ParParClient(this); 643 | connect(parpar, &ParParClient::failed, this, [=](const QString& error) { 644 | progress->setValue(0); // closes window 645 | delete progress; 646 | haveOclDevices = false; 647 | ui->chkOpencl->setChecked(false); 648 | parpar->deleteLater(); 649 | 650 | if(!error.isEmpty()) // isEmpty = cancelled 651 | QMessageBox::warning(this, tr("OpenCL Options"), tr("Failed to query list of OpenCL devices.\n%1").arg(error)); 652 | }); 653 | connect(parpar, &ParParClient::output, this, [=](const QJsonObject& obj) { 654 | oclPlatforms = obj.value("platforms").toArray(); 655 | progress->setValue(0); // closes window 656 | delete progress; 657 | fillDeviceList(ui->chkOpencl->isChecked()); 658 | parpar->deleteLater(); 659 | }); 660 | connect(progress, &QProgressDialog::canceled, this, [=]() { 661 | parpar->kill(); 662 | }); 663 | parpar->run({"--opencl-list"}); 664 | } 665 | 666 | void OptionsDialog::on_chkOpencl_toggled(bool checked) 667 | { 668 | // load OpenCL if not already 669 | if(checked && !haveOclDevices) 670 | loadOpenclDevices(); 671 | else 672 | fillDeviceList(checked); 673 | } 674 | 675 | void OptionsDialog::openclOptChanged() 676 | { 677 | ui->grpOpenclOpts->setEnabled(ui->txtOclAlloc->value() > 0); 678 | ui->txtOclMemory->setEnabled(ui->chkOclMemory->isChecked()); 679 | ui->txtOclBatch->setEnabled(ui->chkOclBatch->isChecked()); 680 | ui->txtOclIters->setEnabled(ui->chkOclIters->isChecked()); 681 | ui->txtOclOutputs->setEnabled(ui->chkOclOutputs->isChecked()); 682 | 683 | if(updatingOpenclOpt) return; 684 | const auto key = ui->treeDevices->currentItem()->data(1, Qt::UserRole).toString(); 685 | auto& dev = openclSettings[key]; 686 | dev.alloc = ui->txtOclAlloc->value(); 687 | dev.memLimit = ui->chkOclMemory->isChecked() ? ui->txtOclMemory->text() : ""; 688 | dev.minChunk = ui->txtOclMinChunk->text(); 689 | dev.gfMethod = ui->cboOclKernel->currentText(); 690 | dev.batch = ui->chkOclBatch->isChecked() ? ui->txtOclBatch->value() : 0; 691 | dev.iters = ui->chkOclIters->isChecked() ? ui->txtOclIters->value() : 0; 692 | dev.outputs = ui->chkOclOutputs->isChecked() ? ui->txtOclOutputs->value() : 0; 693 | 694 | QString allocStr; 695 | if(dev.alloc > 0.0) 696 | allocStr = QLocale().toString(dev.alloc, 'f', 2) + "%"; 697 | ui->treeDevices->currentItem()->setText(1, allocStr); 698 | 699 | float cpuAlloc = 100.0; 700 | for(const auto& dev : openclSettings) 701 | cpuAlloc -= dev.alloc; 702 | allocStr = ""; 703 | if(cpuAlloc > 0.0) 704 | allocStr = QLocale().toString(cpuAlloc, 'f', 2) + "%"; 705 | ui->treeDevices->topLevelItem(0)->setText(1, allocStr); 706 | } 707 | 708 | void OptionsDialog::on_txtOclAlloc_valueChanged(double arg1) 709 | { 710 | openclOptChanged(); 711 | } 712 | void OptionsDialog::on_chkOclMemory_toggled(bool checked) 713 | { 714 | openclOptChanged(); 715 | } 716 | void OptionsDialog::on_chkOclBatch_toggled(bool checked) 717 | { 718 | openclOptChanged(); 719 | } 720 | void OptionsDialog::on_chkOclIters_toggled(bool checked) 721 | { 722 | openclOptChanged(); 723 | } 724 | void OptionsDialog::on_chkOclOutputs_toggled(bool checked) 725 | { 726 | openclOptChanged(); 727 | } 728 | void OptionsDialog::on_txtOclBatch_editingFinished() 729 | { 730 | openclOptChanged(); 731 | } 732 | void OptionsDialog::on_txtOclMemory_textChanged(const QString &arg1) 733 | { 734 | openclOptChanged(); 735 | } 736 | void OptionsDialog::on_cboOclKernel_currentIndexChanged(int index) 737 | { 738 | openclOptChanged(); 739 | } 740 | void OptionsDialog::on_txtOclIters_editingFinished() 741 | { 742 | openclOptChanged(); 743 | } 744 | void OptionsDialog::on_txtOclOutputs_editingFinished() 745 | { 746 | openclOptChanged(); 747 | } 748 | void OptionsDialog::on_txtOclMinChunk_textChanged(const QString &arg1) 749 | { 750 | openclOptChanged(); 751 | } 752 | 753 | void OptionsDialog::on_cboAllocInMode_currentIndexChanged(int index) 754 | { 755 | ui->stkAllocIn->setCurrentIndex(index); 756 | } 757 | 758 | 759 | void OptionsDialog::on_cboAllocRecMode_currentIndexChanged(int index) 760 | { 761 | ui->stkAllocRec->setCurrentIndex(index); 762 | } 763 | 764 | 765 | 766 | -------------------------------------------------------------------------------- /optionsdialog.h: -------------------------------------------------------------------------------- 1 | #ifndef OPTIONSDIALOG_H 2 | #define OPTIONSDIALOG_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "settings.h" 9 | 10 | namespace Ui { 11 | class OptionsDialog; 12 | } 13 | 14 | class OptionsDialog : public QDialog 15 | { 16 | Q_OBJECT 17 | 18 | public: 19 | explicit OptionsDialog(QWidget *parent = nullptr); 20 | ~OptionsDialog(); 21 | 22 | signals: 23 | void settingsUpdated(bool binaryChanged); 24 | 25 | public slots: 26 | void accept() override; 27 | 28 | private slots: 29 | void on_chkProcBatch_stateChanged(int arg1); 30 | 31 | void on_chkMemLimit_stateChanged(int arg1); 32 | 33 | void on_chkProcKernel_stateChanged(int arg1); 34 | 35 | void on_chkThreads_stateChanged(int arg1); 36 | 37 | void on_btnReset_clicked(); 38 | 39 | void on_chkPathNode_toggled(bool checked); 40 | 41 | void on_btnPathParPar_clicked(); 42 | 43 | void on_btnPathNode_clicked(); 44 | 45 | void on_chkSliceMultiple_toggled(bool checked); 46 | 47 | void on_chkTileSize_stateChanged(int arg1); 48 | 49 | void on_txtReadBufs_editingFinished(); 50 | 51 | void on_txtRecBufs_editingFinished(); 52 | 53 | void on_tabWidget_currentChanged(int index); 54 | 55 | void on_treeDevices_currentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *previous); 56 | 57 | void on_txtPacketMin_valueChanged(int arg1); 58 | 59 | void on_cboIOPreset_currentIndexChanged(int index); 60 | 61 | void on_txtReadSize_valueChanged(quint64 , bool ); 62 | 63 | void on_txtReadBufs_valueChanged(int arg1); 64 | 65 | void on_txtHashQueue_valueChanged(int arg1); 66 | 67 | void on_txtChunkReadThreads_valueChanged(int arg1); 68 | 69 | void on_txtRecBufs_valueChanged(int arg1); 70 | 71 | void on_chkOpencl_toggled(bool checked); 72 | 73 | void on_txtOclAlloc_valueChanged(double arg1); 74 | 75 | void on_chkOclMemory_toggled(bool checked); 76 | 77 | void on_chkOclBatch_toggled(bool checked); 78 | 79 | void on_chkOclIters_toggled(bool checked); 80 | 81 | void on_chkOclOutputs_toggled(bool checked); 82 | 83 | void on_cboAllocInMode_currentIndexChanged(int index); 84 | 85 | void on_cboAllocRecMode_currentIndexChanged(int index); 86 | 87 | void on_txtOclBatch_editingFinished(); 88 | 89 | void on_txtOclMemory_textChanged(const QString &arg1); 90 | 91 | void on_cboOclKernel_currentIndexChanged(int index); 92 | 93 | void on_txtOclIters_editingFinished(); 94 | 95 | void on_txtOclOutputs_editingFinished(); 96 | 97 | void on_txtOclMinChunk_textChanged(const QString &arg1); 98 | 99 | private: 100 | Ui::OptionsDialog *ui; 101 | QHash openclSettings; 102 | 103 | void loadSettings(); 104 | void rescale(); 105 | void checkIOPreset(); 106 | bool devicesListed; 107 | void loadOpenclDevices(); 108 | void fillDeviceList(bool opencl); 109 | void openclOptChanged(); 110 | 111 | protected: 112 | void resizeEvent(QResizeEvent *event) override; 113 | void showEvent(QShowEvent *event) override; 114 | 115 | }; 116 | 117 | #endif // OPTIONSDIALOG_H 118 | -------------------------------------------------------------------------------- /outpreviewlistitem.cpp: -------------------------------------------------------------------------------- 1 | #include "outpreviewlistitem.h" 2 | 3 | bool OutPreviewListItem::operator<(const QTreeWidgetItem &other) const 4 | { 5 | int sortCol = treeWidget()->sortColumn(); 6 | if(sortCol == 0 || sortCol == 2) { 7 | // sort by slices 8 | return data(sortCol, Qt::UserRole).toInt() < other.data(sortCol, Qt::UserRole).toInt(); 9 | } 10 | if(sortCol == 1) { 11 | // sort by size 12 | return data(1, Qt::UserRole).toULongLong() < other.data(1, Qt::UserRole).toULongLong(); 13 | } 14 | // unknown column 15 | return text(sortCol) < other.text(sortCol); 16 | } 17 | -------------------------------------------------------------------------------- /outpreviewlistitem.h: -------------------------------------------------------------------------------- 1 | #ifndef OUTPREVIEWLISTITEM_H 2 | #define OUTPREVIEWLISTITEM_H 3 | 4 | #include 5 | 6 | class OutPreviewListItem : public QTreeWidgetItem 7 | { 8 | public: 9 | OutPreviewListItem(QTreeWidgetItem *tree) : QTreeWidgetItem(tree) {} 10 | OutPreviewListItem(QTreeWidgetItem *parent, const QStringList & strings) : QTreeWidgetItem(parent, strings) {} 11 | 12 | bool operator< (const QTreeWidgetItem &other) const override; 13 | }; 14 | 15 | #endif // OUTPREVIEWLISTITEM_H 16 | -------------------------------------------------------------------------------- /par2calc.cpp: -------------------------------------------------------------------------------- 1 | #include "par2calc.h" 2 | #include "settings.h" 3 | 4 | static quint64 simpleCountFromSize(quint64 size, const SrcFileList& files) 5 | { 6 | quint64 count = 0; 7 | for(const auto& file : files) { 8 | count += (file.size() + size-1) / size; 9 | } 10 | return count; 11 | } 12 | 13 | quint64 Par2Calc::sliceSizeFromCount(int& count, quint64 multiple, int limit, const SrcFileList& files, int fileCount, int moveDir) 14 | { 15 | if(files.isEmpty()) { // should never happen 16 | count = 0; 17 | return 0; 18 | } 19 | if(fileCount > limit) { // impossible to satisfy 20 | return 0; 21 | } 22 | if(count > limit) 23 | count = limit; 24 | 25 | // from par2gen.js/calcSliceSizeForFiles: 26 | // there may be a better algorithm to do this, but we'll use a binary search approach to find the correct number of slices to use 27 | quint64 lbound = multiple; 28 | quint64 ubound = 0; 29 | for(const auto& file : files) 30 | if(file.size() > ubound) 31 | ubound = file.size(); 32 | 33 | if(ubound == 0) // all files are empty 34 | return 0; 35 | 36 | if(count < fileCount) { 37 | count = fileCount; 38 | return ubound; 39 | } 40 | 41 | auto mod = ubound % multiple; 42 | if(mod) 43 | ubound += multiple - mod; 44 | 45 | while(lbound < ubound - multiple) { 46 | quint64 mid = ((ubound + lbound) / (multiple*2)) * multiple; 47 | quint64 testCount = simpleCountFromSize(mid, files); 48 | if(count >= testCount) 49 | ubound = mid; 50 | else 51 | lbound = mid; 52 | } 53 | 54 | quint64 lboundSlices = simpleCountFromSize(lbound, files); 55 | quint64 uboundSlices = simpleCountFromSize(ubound, files); 56 | if(lboundSlices == count) 57 | return lbound; 58 | if(uboundSlices == count) 59 | return ubound; 60 | 61 | if(lboundSlices > limit) { 62 | // higher slice count is invalid, must use lower count 63 | count = uboundSlices; 64 | return ubound; 65 | } 66 | 67 | // prefer closer target (as long as we're not specifically moving in a direction, i.e. SpinBox) 68 | if(moveDir == 0) { 69 | int lboundDiff = abs(static_cast(lboundSlices)-count); 70 | int uboundDiff = abs(static_cast(uboundSlices)-count); 71 | if(lboundDiff < uboundDiff) { 72 | count = lboundSlices; 73 | return lbound; 74 | } else if(uboundDiff < lboundDiff) { 75 | count = uboundSlices; 76 | return ubound; 77 | } 78 | } 79 | 80 | if(moveDir > 0) { 81 | count = lboundSlices; 82 | return lbound; 83 | } else { 84 | // we generally prefer the lower slice count, since we know we can't exceed limits that way 85 | count = uboundSlices; 86 | return ubound; 87 | } 88 | } 89 | 90 | int Par2Calc::sliceCountFromSize(quint64& size, quint64 multiple, int limit, const SrcFileList& files, int fileCount) 91 | { 92 | int mod = size % multiple; 93 | if(mod) 94 | size += multiple - mod; 95 | 96 | if(size == 0) 97 | size = 4; 98 | 99 | quint64 count = simpleCountFromSize(size, files); 100 | if(count > limit) { 101 | int limitedCount = limit; 102 | size = sliceSizeFromCount(limitedCount, multiple, limit, files, fileCount); 103 | count = limitedCount; 104 | } 105 | return count; 106 | } 107 | 108 | int Par2Calc::maxSliceCount(quint64 multiple, int limit, const SrcFileList& files) 109 | { 110 | quint64 count = 0; 111 | for(const auto& file : files) { 112 | count += (file.size() + multiple-1) / multiple; 113 | if(count > limit) return limit; 114 | } 115 | return count; 116 | } 117 | 118 | 119 | int Par2Calc::round_down_pow2(int v) { 120 | if((v & (v-1)) == 0) return v; // is a power of 2 (shortcut exists because this is the common case) 121 | 122 | // find target number via a float conversion 123 | // (usage of float over double does mean that this can be wrong for very large numbers, but we're not expecting these) 124 | union { float f; uint32_t u; } tmp; 125 | tmp.f = (float)v; // convert to float 126 | tmp.u &= 0xff800000; // discard mantissa 127 | return (int)tmp.f; // convert back to int 128 | } 129 | 130 | -------------------------------------------------------------------------------- /par2calc.h: -------------------------------------------------------------------------------- 1 | #ifndef PAR2CALC_H 2 | #define PAR2CALC_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include "sourcefile.h" 8 | 9 | class Par2Calc 10 | { 11 | public: 12 | static quint64 sliceSizeFromCount(int& count, quint64 multiple, int limit, const SrcFileList& files, int fileCount, int moveDir = 0); 13 | static int sliceCountFromSize(quint64& size, quint64 multiple, int limit, const SrcFileList& files, int fileCount); 14 | static int maxSliceCount(quint64 multiple, int limit, const SrcFileList& files); 15 | 16 | static int round_down_pow2(int v); 17 | }; 18 | 19 | #endif // PAR2CALC_H 20 | -------------------------------------------------------------------------------- /par2outinfo.cpp: -------------------------------------------------------------------------------- 1 | #include "par2outinfo.h" 2 | #include 3 | #include "settings.h" 4 | #include "mainwindow.h" 5 | #include "clientinfo.h" 6 | #include 7 | 8 | static int strLen(const QString& s) 9 | { 10 | const auto charset = Settings::getInstance().charset(); 11 | if(charset == "latin1" || charset == "ascii") 12 | return s.toLatin1().length(); 13 | return s.toUtf8().length(); 14 | } 15 | static bool hasUnicode(const QString& s) 16 | { 17 | for(const QChar& c : qAsConst(s)) { 18 | if(c.unicode() > 127) return true; 19 | } 20 | return false; 21 | } 22 | static int unicodeLen(const QString& s) 23 | { 24 | auto opt = Settings::getInstance().unicode(); 25 | if(opt == EXCLUDE) return 0; 26 | bool include = true; 27 | if(opt == AUTO) 28 | include = hasUnicode(s); 29 | if(!include) return 0; 30 | return s.length() * 2; 31 | } 32 | 33 | int Par2OutInfo::pktSizeFileDesc(const QString& filename) 34 | { 35 | int uniLen = unicodeLen(filename); 36 | int nameLen = strLen(filename); 37 | nameLen = (nameLen + 3) & ~3; // multiple of 4 padding 38 | 39 | int pktLen = 64 + 56 + nameLen; 40 | if(uniLen) { 41 | uniLen = (uniLen + 3) & ~3; 42 | pktLen += 64 + 16 + uniLen; 43 | } 44 | return pktLen; 45 | } 46 | 47 | int Par2OutInfo::pktSizeComment() const 48 | { 49 | int uniLen = unicodeLen(win->par2Comment); 50 | int len = strLen(win->par2Comment); 51 | len = (len + 3) & ~3; // multiple of 4 padding 52 | 53 | int pktLen = 64 + len; 54 | if(uniLen) { 55 | uniLen = (uniLen + 3) & ~3; 56 | pktLen += 64 + 16 + uniLen; 57 | } 58 | return pktLen; 59 | } 60 | 61 | int Par2OutInfo::pktSizeCreator() 62 | { 63 | int len = ClientInfo::creator().toUtf8().length(); 64 | len = (len + 3) & ~3; // multiple of 4 padding 65 | return 64 + len; 66 | } 67 | 68 | void Par2OutInfo::updateCritPacketSizes() 69 | { 70 | critPacketSizes.clear(); 71 | int numFiles = win->par2SrcFiles.size(); 72 | critPacketSizes.reserve(numFiles * 2 + 3); 73 | for(const auto& file : qAsConst(win->par2SrcFiles)) { 74 | // add fileDesc 75 | critPacketSizes.append(pktSizeFileDesc(file.par2name)); 76 | 77 | // add ifsc 78 | int slices = (file.size() + win->par2SliceSize-1) / win->par2SliceSize; 79 | if(slices > 0) 80 | critPacketSizes.append(64 + 16 + 20*slices); 81 | } 82 | 83 | // add comment packet 84 | if(!win->par2Comment.isEmpty()) { 85 | critPacketSizes.append(pktSizeComment()); 86 | } 87 | 88 | // add main packet 89 | critPacketSizes.append(64 + 12 + numFiles*16); 90 | 91 | // creator packet not included as it's not a critical packet 92 | 93 | totalCritSize = 0; 94 | for(const auto& size : critPacketSizes) 95 | totalCritSize += size; 96 | } 97 | 98 | QList Par2OutInfo::getOutputList(int sliceCount, int distMode, int sliceLimit, int sliceOffset) 99 | { 100 | updateCritPacketSizes(); 101 | QList ret; 102 | if(distMode == 0) { 103 | // single file 104 | ret.append(makeRecFile(sliceCount, sliceOffset)); 105 | return ret; 106 | } 107 | 108 | ret.append(makeRecFile(0, 0)); // index file 109 | 110 | if(distMode == 1) { // uniform 111 | int numFiles = sliceLimit; 112 | int slicePos = 0; 113 | ret.reserve(1 + numFiles); 114 | while(numFiles--) { 115 | int slicesThisFile = (sliceCount-slicePos + numFiles) / (numFiles+1); 116 | //if(slicesThisFile > sliceLimit) slicesThisFile = sliceLimit; 117 | ret.append(makeRecFile(slicesThisFile, slicePos+sliceOffset)); 118 | slicePos += slicesThisFile; 119 | } 120 | return ret; 121 | } 122 | // pow2 123 | if(sliceCount > 0) { 124 | ret.reserve(1 + 16 + sliceCount/sliceLimit); // rough over-estimate 125 | int slicePos = 0, slices = 1; 126 | int totalCount = sliceCount + sliceOffset; 127 | while(slicePos < totalCount) { 128 | if(slices > sliceLimit) slices = sliceLimit; 129 | 130 | int fSlices = slices; 131 | if(fSlices+slicePos > totalCount) fSlices = totalCount-slicePos; 132 | 133 | if(slicePos+fSlices > sliceOffset) { 134 | if(sliceOffset > slicePos) 135 | ret.append(makeRecFile(fSlices - (sliceOffset-slicePos), sliceOffset)); 136 | else 137 | ret.append(makeRecFile(fSlices, slicePos)); 138 | } 139 | 140 | slicePos += slices; 141 | slices *= 2; 142 | } 143 | } 144 | return ret; 145 | } 146 | 147 | 148 | // ported from par2gen.js/_rfPush 149 | Par2RecoveryFile Par2OutInfo::makeRecFile(int sliceCount, int sliceOffset) 150 | { 151 | int critTotalSize = totalCritSize; 152 | quint64 recvSize = 68 + win->par2SliceSize; 153 | 154 | if(1 /*pow2 repetition*/ && sliceCount) { 155 | int critCopies = static_cast(round(log(static_cast(sliceCount)) / log(2.0))); 156 | 157 | const auto& s = Settings::getInstance(); 158 | critCopies = std::max(critCopies, s.packetRepMin()); 159 | critCopies = std::min(critCopies, s.packetRepMax()); 160 | critTotalSize *= critCopies; 161 | } 162 | 163 | return { 164 | sliceOffset, sliceCount, critTotalSize + pktSizeCreator() + sliceCount*recvSize 165 | }; 166 | } 167 | 168 | QString Par2OutInfo::fileExt(int numSlices, int sliceOffset, int totalSlices) 169 | { 170 | if(numSlices == 0) return ".par2"; 171 | int digits = QString::number(totalSlices).length(); 172 | if(digits < 2) digits = 2; 173 | 174 | if(Settings::getInstance().stdNaming()) { 175 | return QString(".vol%1-%2.par2") 176 | .arg(sliceOffset, digits, 10, QChar('0')) 177 | .arg(sliceOffset + numSlices, digits, 10, QChar('0')); 178 | } else { 179 | return QString(".vol%1+%2.par2") 180 | .arg(sliceOffset, digits, 10, QChar('0')) 181 | .arg(numSlices, digits, 10, QChar('0')); 182 | } 183 | } 184 | 185 | QString Par2OutInfo::nameSafeLen(QString name) 186 | { 187 | // allocate enough space in the name for PAR2 extension (and our uniquifier) 188 | // assume a max of 255 bytes allowed for a filename, so if the byte length exceeds 235, we'll need to shorten the name 189 | auto name8 = (name + " - 2.vol12345+12345.par2").toLocal8Bit(); 190 | // the algorithm to shorten the string is somewhat dumb, but since we can't be too far above the max length, this shouldn't iterate too many times 191 | while(name8.length() > 255) { 192 | name.chop(1); 193 | name8 = (name + " - 2.vol12345+12345.par2").toLocal8Bit(); 194 | } 195 | return name; 196 | 197 | } 198 | -------------------------------------------------------------------------------- /par2outinfo.h: -------------------------------------------------------------------------------- 1 | #ifndef PAR2OUTINFO_H 2 | #define PAR2OUTINFO_H 3 | 4 | class MainWindow; 5 | #include 6 | #include 7 | 8 | class Par2RecoveryFile { 9 | public: 10 | int offset; 11 | int count; 12 | quint64 size; 13 | }; 14 | 15 | class Par2OutInfo 16 | { 17 | private: 18 | MainWindow* win; 19 | 20 | public: 21 | // ugly strong coupling, but who really cares? 22 | Par2OutInfo(MainWindow* mainWin) : win(mainWin) { 23 | } 24 | 25 | void updateCritPacketSizes(); 26 | QList getOutputList(int sliceCount, int distMode, int sliceLimit, int sliceOffset); 27 | 28 | static QString fileExt(int numSlices, int sliceOffset, int totalSlices); 29 | static QString nameSafeLen(QString name); 30 | 31 | private: 32 | // state variables because stateless went out of fashion 33 | QVector critPacketSizes; 34 | quint64 totalCritSize; 35 | 36 | static int pktSizeFileDesc(const QString& filename); 37 | int pktSizeComment() const; 38 | static int pktSizeCreator(); 39 | 40 | Par2RecoveryFile makeRecFile(int sliceCount, int sliceOffset); 41 | }; 42 | 43 | #endif // PAR2OUTINFO_H 44 | -------------------------------------------------------------------------------- /parparclient.cpp: -------------------------------------------------------------------------------- 1 | #include "parparclient.h" 2 | #include "settings.h" 3 | #include 4 | #include 5 | 6 | ParParClient::ParParClient(QObject *parent) 7 | : QObject{parent} 8 | { 9 | connect(&parpar, QOverload::of(&QProcess::finished), this, &ParParClient::finished); 10 | // treat failing to start, like a crash 11 | connect(&parpar, &QProcess::errorOccurred, this, [this](QProcess::ProcessError err) { 12 | if(err == QProcess::FailedToStart) { 13 | this->finished(0, QProcess::ExitStatus::CrashExit); 14 | } 15 | // let other handler handle other errors 16 | }); 17 | isRunning = false; 18 | isCancelled = false; 19 | timer.setSingleShot(true); 20 | connect(&timer, &QTimer::timeout, this, [this]() { 21 | this->parpar.kill(); 22 | }); 23 | } 24 | 25 | void ParParClient::run(const QStringList& _args, int timeout) 26 | { 27 | auto cmd = Settings::getInstance().parparBin(); 28 | QStringList args{"--json"}; 29 | args.append(_args); 30 | if(cmd.length() > 1) 31 | args.prepend(cmd[1]); 32 | isRunning = true; 33 | isCancelled = false; 34 | stdoutBuffer.clear(); 35 | if(timeout) 36 | { 37 | timer.setInterval(timeout); 38 | timer.start(); 39 | } 40 | parpar.start(cmd[0], args); 41 | } 42 | 43 | void ParParClient::finished(int exitCode, QProcess::ExitStatus exitStatus) 44 | { 45 | timer.stop(); 46 | isRunning = false; 47 | 48 | if(isCancelled) { 49 | emit failed(QString()); 50 | } else if(exitStatus != QProcess::ExitStatus::NormalExit) { 51 | emit failed(tr("ParPar process crashed or failed to start")); 52 | } else if(exitCode != 0) { 53 | emit failed(tr("ParPar failure (exit code: %1)").arg(exitCode)); 54 | } else { 55 | const auto data = QJsonDocument::fromJson(parpar.readAllStandardOutput()); 56 | 57 | if(data.isNull() || !data.isObject()) { 58 | auto message = QString::fromUtf8(parpar.readAllStandardError()); 59 | if(message.isEmpty()) message = "No output received"; 60 | emit failed(message); 61 | } else 62 | emit output(data.object()); 63 | } 64 | } 65 | 66 | void ParParClient::kill() 67 | { 68 | isCancelled = true; 69 | parpar.kill(); 70 | } 71 | -------------------------------------------------------------------------------- /parparclient.h: -------------------------------------------------------------------------------- 1 | #ifndef PARPARCLIENT_H 2 | #define PARPARCLIENT_H 3 | 4 | #include 5 | #include 6 | 7 | class ParParClient : public QObject 8 | { 9 | Q_OBJECT 10 | 11 | QProcess parpar; 12 | QTimer timer; 13 | bool isRunning; 14 | QString stdoutBuffer; 15 | bool isCancelled; 16 | 17 | public: 18 | explicit ParParClient(QObject *parent = nullptr); 19 | 20 | private slots: 21 | void finished(int exitCode, QProcess::ExitStatus exitStatus); 22 | 23 | signals: 24 | void output(const QJsonObject& m); 25 | void failed(const QString& error); 26 | 27 | public: 28 | void run(const QStringList& args, int timeout = 10000); 29 | void kill(); 30 | }; 31 | 32 | #endif // PARPARCLIENT_H 33 | -------------------------------------------------------------------------------- /progressdialog.cpp: -------------------------------------------------------------------------------- 1 | #include "progressdialog.h" 2 | #include 3 | 4 | ProgressDialog::ProgressDialog(QWidget* parent, const QString& labelText) 5 | : QProgressDialog(parent) 6 | { 7 | setLabelText(labelText); 8 | setMinimumDuration(1000); 9 | setWindowModality(Qt::WindowModal); 10 | 11 | auto* pb = new QProgressBar(this); 12 | pb->setAlignment(Qt::AlignCenter); 13 | setBar(pb); 14 | 15 | progressMask = 0xff; 16 | incValue = 0; 17 | } 18 | -------------------------------------------------------------------------------- /progressdialog.h: -------------------------------------------------------------------------------- 1 | #ifndef PROGRESSDIALOG_H 2 | #define PROGRESSDIALOG_H 3 | 4 | #include 5 | #include 6 | 7 | class ProgressDialog : public QProgressDialog 8 | { 9 | private: 10 | int incValue; 11 | public: 12 | explicit ProgressDialog(QWidget* parent, const QString& labelText); 13 | 14 | int progressMask; 15 | inline void inc() { 16 | incValue++; 17 | // setValue seems to be rather slow, so defer updating it 18 | if((incValue & progressMask) == progressMask) 19 | setValue(incValue); 20 | } 21 | 22 | inline void end() { 23 | setValue(maximum()); 24 | } 25 | }; 26 | 27 | #endif // PROGRESSDIALOG_H 28 | -------------------------------------------------------------------------------- /res.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | res/icon.ico 4 | 5 | 6 | -------------------------------------------------------------------------------- /res/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/animetosho/ParParGUI/3d7d4995d5e4c38528012a76d513cb6528dc075b/res/icon.ico -------------------------------------------------------------------------------- /res/parpargui.rc: -------------------------------------------------------------------------------- 1 | #include "winresrc.h" 2 | #include "config.h" 3 | 4 | 5 | // Application icon 6 | 1 ICON icon.ico 7 | 8 | 9 | // Version resource 10 | VS_VERSION_INFO VERSIONINFO 11 | FILEVERSION PROJECT_VERSION_MAJOR,PROJECT_VERSION_MINOR,0,0 12 | PRODUCTVERSION PROJECT_VERSION_MAJOR,PROJECT_VERSION_MINOR,0,0 13 | FILEFLAGSMASK 0x3f 14 | #ifdef _DEBUG 15 | FILEFLAGS VS_FF_DEBUG 16 | #else 17 | # ifdef VERSION_IS_RELEASE 18 | FILEFLAGS 0x0L 19 | # else 20 | FILEFLAGS VS_FF_PRERELEASE 21 | # endif 22 | #endif 23 | 24 | FILEOS VOS_NT_WINDOWS32 25 | FILETYPE VFT_APP 26 | FILESUBTYPE 0x0L 27 | BEGIN 28 | BLOCK "StringFileInfo" 29 | BEGIN 30 | BLOCK "040904b0" 31 | BEGIN 32 | VALUE "CompanyName", "Anime Tosho" 33 | VALUE "ProductName", "ParPar GUI" 34 | VALUE "FileDescription", "ParPar: high performance PAR2 create tool" 35 | VALUE "OriginalFilename", "parpargui.exe" 36 | VALUE "InternalName", "parpargui" 37 | END 38 | END 39 | BLOCK "VarFileInfo" 40 | BEGIN 41 | VALUE "Translation", 0x409, 1200 42 | END 43 | END 44 | -------------------------------------------------------------------------------- /settings.cpp: -------------------------------------------------------------------------------- 1 | #include "settings.h" 2 | #include 3 | #include 4 | 5 | static QString pathConverter(const QString& fn) 6 | { 7 | return QDir().absoluteFilePath(fn).replace("/", QDir::separator()); 8 | } 9 | QStringList Settings::parparBin(bool* isSystemExecutable) const 10 | { 11 | // check setting 12 | auto pathParPar = settings.value("ParPar/Bin", "").toString(); 13 | auto pathNode = settings.value("ParPar/Node", "").toString(); 14 | if(!pathParPar.isEmpty()) { 15 | if(isSystemExecutable) *isSystemExecutable = false; // well... I guess it's possibly true, but we wouldn't know 16 | if(!pathNode.isEmpty()) { 17 | if(pathNode != "node") // system executable - can't make absolute 18 | pathNode = pathConverter(pathNode); 19 | return {pathNode, pathConverter(pathParPar)}; 20 | } else { 21 | if(pathParPar != "parpar") 22 | pathParPar = pathConverter(pathParPar); 23 | return {pathParPar}; 24 | } 25 | } 26 | 27 | // if not set, scan for it 28 | 29 | QString exe = ""; 30 | QString curdir = "./"; 31 | #ifdef Q_OS_WINDOWS 32 | exe = ".exe"; 33 | curdir = ""; 34 | #endif 35 | 36 | if(isSystemExecutable) *isSystemExecutable = false; 37 | 38 | // compiled binary 39 | if(QFile::exists(QString("parpar") + exe)) 40 | return {pathConverter(QString("parpar") + exe)}; 41 | #ifdef Q_OS_WINDOWS 42 | // TODO: test if this works 43 | if(QFile::exists("parpar.cmd")) 44 | return {pathConverter("parpar.cmd")}; 45 | #endif 46 | 47 | if(QFile::exists("bin/parpar.js")) { 48 | // find included node 49 | if(QFile::exists(QString("bin/node") + exe)) 50 | return {pathConverter(QString("bin/node") + exe), pathConverter("bin/parpar.js")}; 51 | if(QFile::exists(QString("node") + exe)) 52 | return {pathConverter(QString("node") + exe), pathConverter("bin/parpar.js")}; 53 | // use system node 54 | #ifdef Q_OS_WINDOWS 55 | if(isSystemExecutable) *isSystemExecutable = true; 56 | return {QString("node"), pathConverter("bin/parpar.js")}; 57 | #else 58 | return {pathConverter("bin/parpar.js")}; 59 | #endif 60 | } 61 | 62 | if(isSystemExecutable) *isSystemExecutable = true; 63 | return {"parpar"}; 64 | } 65 | 66 | static QString relPathConverter(const QString& file) 67 | { 68 | if(file.isEmpty()) return file; 69 | 70 | QDir cd; 71 | QFileInfo info(file); 72 | if(info.isAbsolute()) { 73 | QString relPath = cd.relativeFilePath(file); 74 | #ifdef Q_OS_WINDOWS 75 | // on Windows, can't use relative paths if on different drives (or drive <> UNC path) 76 | if(cd.absolutePath().left(2).compare(file.left(2), Qt::CaseInsensitive) == 0) 77 | #else 78 | // on *nix, path in current dir should have preceeding './' 79 | // (we don't really require it, because we're not executing over a shell, but it distinguishes a local binary vs system binary) 80 | if(!relPath.contains("/")) 81 | return QString(".") + QDir::separator() + relPath; 82 | #endif 83 | if(!relPath.startsWith("../")) // only allow relative paths if in the same folder 84 | return relPath.replace("/", QDir::separator()); 85 | } 86 | return file; 87 | } 88 | void Settings::setParparBin(const QString& parpar, const QString& node) 89 | { 90 | // convert to relative paths to allow portability 91 | settings.setValue("ParPar/Bin", relPathConverter(parpar)); 92 | settings.setValue("ParPar/Node", relPathConverter(node)); 93 | } 94 | -------------------------------------------------------------------------------- /settings.h: -------------------------------------------------------------------------------- 1 | #ifndef SETTINGS_H 2 | #define SETTINGS_H 3 | 4 | #include 5 | 6 | enum /*class*/ SettingsUnicode { 7 | AUTO, EXCLUDE, INCLUDE 8 | }; 9 | enum SettingsDefaultAllocIn { 10 | ALLOC_IN_SIZE, ALLOC_IN_COUNT, ALLOC_IN_RATIO 11 | }; 12 | enum SettingsDefaultAllocRec { 13 | ALLOC_REC_RATIO, ALLOC_REC_COUNT, ALLOC_REC_SIZE 14 | }; 15 | 16 | 17 | struct OpenclDevice { 18 | QString name; 19 | float alloc; 20 | QString memLimit; 21 | QString minChunk; 22 | QString gfMethod; 23 | int batch; 24 | unsigned iters; 25 | int outputs; 26 | 27 | OpenclDevice() { 28 | alloc = 0.0; 29 | memLimit = ""; 30 | minChunk = "32K"; 31 | gfMethod = "lookup"; 32 | batch = 0; 33 | iters = 0; 34 | outputs = 0; 35 | } 36 | }; 37 | 38 | class Settings 39 | { 40 | private: 41 | QSettings settings; 42 | Settings() : settings("ParParGUI.ini", QSettings::IniFormat) {} 43 | public: 44 | static Settings& getInstance() 45 | { 46 | static Settings* instance = new Settings(); 47 | return *instance; 48 | } 49 | void reset() 50 | { 51 | settings.clear(); 52 | } 53 | 54 | SettingsUnicode unicode() const { 55 | auto value = settings.value("PAR2/Unicode", "auto").toString().toLower(); 56 | if(value == "true") return SettingsUnicode::INCLUDE; 57 | if(value == "false") return SettingsUnicode::EXCLUDE; 58 | return SettingsUnicode::AUTO; 59 | } 60 | void setUnicode(const SettingsUnicode value) { 61 | if(value == SettingsUnicode::INCLUDE) 62 | settings.setValue("PAR2/Unicode", "true"); 63 | else if(value == SettingsUnicode::EXCLUDE) 64 | settings.setValue("PAR2/Unicode", "false"); 65 | else 66 | settings.setValue("PAR2/Unicode", "auto"); 67 | } 68 | 69 | QString charset() const { 70 | return settings.value("PAR2/LocalCharset", "utf8").toString(); 71 | } 72 | void setCharset(const QString& value) { 73 | settings.setValue("PAR2/LocalCharset", value); 74 | } 75 | 76 | int packetRepMin() const { 77 | return settings.value("PAR2/PacketRepetitionMin", 1).toInt(); 78 | } 79 | void setPacketRepMin(int value) { 80 | settings.setValue("PAR2/PacketRepetitionMin", value); 81 | } 82 | 83 | int packetRepMax() const { 84 | return settings.value("PAR2/PacketRepetitionMax", 16).toInt(); 85 | } 86 | void setPacketRepMax(int value) { 87 | settings.setValue("PAR2/PacketRepetitionMax", value); 88 | } 89 | 90 | bool stdNaming() const { 91 | return settings.value("PAR2/StdNaming", false).toBool(); 92 | } 93 | void setStdNaming(const bool value) { 94 | settings.setValue("PAR2/StdNaming", value); 95 | } 96 | 97 | QString readSize() const { 98 | return settings.value("Tuning/ReadSize", "4M").toString(); 99 | } 100 | void setReadSize(const QString& value) { 101 | settings.setValue("Tuning/ReadSize", value); 102 | } 103 | 104 | int readBuffers() const { 105 | return settings.value("Tuning/ReadBuffers", 8).toInt(); 106 | } 107 | void setReadBuffers(const int value) { 108 | settings.setValue("Tuning/ReadBuffers", value); 109 | } 110 | 111 | int hashQueue() const { 112 | return settings.value("Tuning/HashQueue", 5).toInt(); 113 | } 114 | void setHashQueue(const int value) { 115 | settings.setValue("Tuning/HashQueue", value); 116 | } 117 | 118 | QString minChunk() const { 119 | return settings.value("Tuning/MinChunk", "128K").toString(); 120 | } 121 | void setMinChunk(const QString& value) { 122 | settings.setValue("Tuning/MinChunk", value); 123 | } 124 | 125 | int chunkReadThreads() const { 126 | return settings.value("Tuning/ChunkReadThreads", 2).toInt(); 127 | } 128 | void setChunkReadThreads(const int value) { 129 | settings.setValue("Tuning/ChunkReadThreads", value); 130 | } 131 | 132 | int recBuffers() const { 133 | return settings.value("Tuning/RecBuffers", 12).toInt(); 134 | } 135 | void setRecBuffers(const int value) { 136 | settings.setValue("Tuning/RecBuffers", value); 137 | } 138 | 139 | int hashBatch() const { 140 | return settings.value("Tuning/HashBatch", 8).toInt(); 141 | } 142 | void setHashBatch(const int value) { 143 | settings.setValue("Tuning/HashBatch", value); 144 | } 145 | 146 | int procBatch() const { 147 | const auto val = settings.value("Tuning/ProcBatch", "auto"); 148 | if(val.toString().toLower() == "auto") return -1; 149 | return val.toInt(); 150 | } 151 | void setProcBatch(const int value) { 152 | if(value < 0) 153 | settings.setValue("Tuning/ProcBatch", "auto"); 154 | else 155 | settings.setValue("Tuning/ProcBatch", value); 156 | } 157 | 158 | QString memLimit() const { 159 | const auto val = settings.value("Tuning/MemLimit", "auto").toString(); 160 | if(val.toLower() == "auto") return ""; 161 | return val; 162 | } 163 | void setMemLimit(const QString& value) { 164 | if(value.isEmpty()) 165 | settings.setValue("Tuning/MemLimit", "auto"); 166 | else 167 | settings.setValue("Tuning/MemLimit", value); 168 | } 169 | 170 | QString gfMethod() const { 171 | const auto val = settings.value("Tuning/GFMethod", "auto").toString(); 172 | if(val.toLower() == "auto") return ""; 173 | return val; 174 | } 175 | void setGfMethod(const QString& value) { 176 | if(value.isEmpty()) 177 | settings.setValue("Tuning/GFMethod", "auto"); 178 | else 179 | settings.setValue("Tuning/GFMethod", value); 180 | } 181 | 182 | QString tileSize() const { 183 | const auto val = settings.value("Tuning/LoopTileSize", "auto").toString(); 184 | if(val.toLower() == "auto") return ""; 185 | return val; 186 | } 187 | void setTileSize(const QString& value) { 188 | if(value.isEmpty()) 189 | settings.setValue("Tuning/LoopTileSize", "auto"); 190 | else 191 | settings.setValue("Tuning/LoopTileSize", value); 192 | } 193 | 194 | QString hashMethod() const { 195 | const auto val = settings.value("Tuning/HashMethod", "auto").toString(); 196 | if(val.isEmpty()) return "auto"; 197 | return val.toLower(); 198 | } 199 | void setHashMethod(const QString& value) { 200 | if(value.isEmpty()) 201 | settings.setValue("Tuning/HashMethod", "auto"); 202 | else 203 | settings.setValue("Tuning/HashMethod", value); 204 | } 205 | 206 | QString md5Method() const { 207 | const auto val = settings.value("Tuning/MD5Method", "auto").toString(); 208 | if(val.isEmpty()) return "auto"; 209 | return val.toLower(); 210 | } 211 | void setMd5Method(const QString& value) { 212 | if(value.isEmpty()) 213 | settings.setValue("Tuning/MD5Method", "auto"); 214 | else 215 | settings.setValue("Tuning/MD5Method", value); 216 | } 217 | 218 | bool outputSync() const { 219 | return settings.value("Tuning/OutputSync", false).toBool(); 220 | } 221 | void setOutputSync(bool value) { 222 | settings.setValue("Tuning/OutputSync", value); 223 | } 224 | 225 | 226 | int threadNum() const { 227 | const auto val = settings.value("Tuning/Threads", "auto"); 228 | if(val.toString().toLower() == "auto") return -1; 229 | return val.toInt(); 230 | } 231 | void setThreadNum(const int value) { 232 | if(value < 0) 233 | settings.setValue("Tuning/Threads", "auto"); 234 | else 235 | settings.setValue("Tuning/Threads", value); 236 | } 237 | 238 | QString cpuMinChunk() const { 239 | return settings.value("Tuning/CpuMinChunk", "128K").toString(); 240 | } 241 | void setCpuMinChunk(const QString& value) { 242 | settings.setValue("Tuning/CpuMinChunk", value); 243 | } 244 | 245 | QList openclDevices() const { 246 | QList devices; 247 | int i=0; 248 | while(1) { 249 | const auto skey = QString("OpenCL/%1_") + QString::number(i++); 250 | const auto& devKey = skey.arg("Device"); 251 | if(!settings.contains(devKey)) break; 252 | 253 | OpenclDevice dev; 254 | dev.name = settings.value(devKey).toString(); 255 | dev.alloc = settings.value(skey.arg("AllocationPercent"), dev.alloc).toFloat(); 256 | if(dev.alloc <= 0.0) continue; // invalid allocation 257 | dev.memLimit = settings.value(skey.arg("MemLimit"), dev.memLimit).toString(); 258 | dev.minChunk = settings.value(skey.arg("MinChunk"), dev.minChunk).toString(); 259 | dev.gfMethod = settings.value(skey.arg("GFMethod"), dev.gfMethod).toString(); 260 | dev.batch = settings.value(skey.arg("ProcBatch"), dev.batch).toInt(); 261 | dev.iters = settings.value(skey.arg("Iterations"), dev.iters).toUInt(); 262 | dev.outputs = settings.value(skey.arg("Outputs"), dev.outputs).toInt(); 263 | devices.append(dev); 264 | } 265 | return devices; 266 | } 267 | void setOpenclDevices(const QList& value) { 268 | settings.remove("OpenCL"); 269 | settings.beginGroup("OpenCL"); 270 | for(int i=0; i 3 | #include 4 | #include "util.h" 5 | #include 6 | #include 7 | 8 | SizeEdit::SizeEdit(QWidget *parent) : QLineEdit(parent) 9 | { 10 | QString dec = QRegularExpression::escape(QLocale().decimalPoint()); 11 | // validator is lax to allow users maximum flexibility when editing; fix it up on editingFinished 12 | setValidator(new QRegularExpressionValidator(QRegularExpression(QString("^\\d*(") + dec + "\\d*)?[BKMGTPE]?$"), this)); 13 | 14 | connect(this, &SizeEdit::editingFinished, this, &SizeEdit::onEditingFinished); 15 | updateActive = false; 16 | connect(this, &SizeEdit::textEdited, this, &SizeEdit::onTextEdited); 17 | } 18 | 19 | void SizeEdit::setUnit(QChar unit) 20 | { 21 | QLocale l; 22 | l.setNumberOptions(QLocale::OmitGroupSeparator); 23 | 24 | auto sVal = text(); 25 | if(sVal.isEmpty() || sVal == l.decimalPoint()) sVal = "1"; 26 | // strip unit from text if present 27 | auto cUnit = sVal.at(sVal.size()-1); 28 | if(cUnit < '0' || cUnit > '9') 29 | sVal = sVal.left(sVal.length()-1); 30 | 31 | double val = l.toDouble(sVal); 32 | if(unit == 'B') { 33 | val = floor(val); 34 | } 35 | auto newText = l.toString(val) + unit.toUpper(); 36 | setText(newText); 37 | setSelection(newText.length()-1, 1); 38 | 39 | emit textEdited(newText); 40 | } 41 | 42 | void SizeEdit::keyPressEvent(QKeyEvent* event) 43 | { 44 | switch(event->key()) { 45 | case Qt::Key_B: 46 | setUnit('B'); 47 | return; 48 | case Qt::Key_K: 49 | setUnit('K'); 50 | return; 51 | case Qt::Key_M: 52 | setUnit('M'); 53 | return; 54 | case Qt::Key_G: 55 | setUnit('G'); 56 | return; 57 | case Qt::Key_T: 58 | setUnit('T'); 59 | return; 60 | case Qt::Key_P: 61 | setUnit('P'); 62 | return; 63 | case Qt::Key_E: 64 | setUnit('E'); 65 | return; 66 | default: 67 | QLineEdit::keyPressEvent(event); 68 | } 69 | } 70 | 71 | quint64 SizeEdit::getBytes() const 72 | { 73 | return sizeToBytes(text()); 74 | } 75 | 76 | QString SizeEdit::getSizeString() const 77 | { 78 | auto ret = text(); 79 | QLocale l; 80 | ret.replace(l.groupSeparator(), "").replace(l.decimalPoint(), "."); 81 | 82 | // append B for pure sizes (needed for ParPar to know it's a size) 83 | if(ret.isEmpty()) return ret; 84 | 85 | auto cUnit = ret.at(ret.size()-1).toUpper(); 86 | if(cUnit >= 'A' && cUnit <= 'Z') 87 | return ret; 88 | return ret + "B"; 89 | } 90 | 91 | void SizeEdit::setBytesApprox(quint64 bytes, bool noTrigger) 92 | { 93 | QString s = friendlySize(bytes); 94 | s.replace(QLocale().groupSeparator(), ""); 95 | if(noTrigger) blockSignals(true); 96 | setText(s); 97 | if(noTrigger) blockSignals(false); 98 | } 99 | 100 | void SizeEdit::setBytes(quint64 bytes, bool noTrigger) 101 | { 102 | QLocale l; 103 | l.setNumberOptions(QLocale::OmitGroupSeparator); 104 | // modification to friendlySize: only pick a larger unit if it can be represented exactly 105 | QStringList units{"B", "K", "M", "G", "T", "P", "E"}; 106 | quint64 v = bytes; 107 | QString s; 108 | int ui = 0; 109 | for(; ui < units.size(); ui++) { 110 | if(v < 10000) break; 111 | if((v % 1024) != 0) { 112 | // if fractional part can be represented exactly, do the divide and stop 113 | if((v*100) % 1024 == 0) { 114 | v = (v*100) / 1024; 115 | s = l.toString(static_cast(v) / 100, 'f', 2) + units[ui+1]; 116 | } 117 | break; 118 | } 119 | 120 | v /= 1024; 121 | } 122 | if(s.isEmpty()) 123 | s = l.toString(v) + units[ui]; 124 | if(noTrigger) blockSignals(true); 125 | setText(s.replace(l.groupSeparator(), "")); 126 | if(noTrigger) blockSignals(false); 127 | } 128 | 129 | void SizeEdit::onTextEdited(const QString& text) 130 | { 131 | updateActive = true; 132 | emit valueChanged(getBytes(), false); 133 | } 134 | void SizeEdit::onEditingFinished() 135 | { 136 | if(updateActive) { 137 | updateActive = false; 138 | QLocale l; 139 | l.setNumberOptions(QLocale::OmitGroupSeparator); 140 | 141 | // fix text 142 | auto sVal = text(); 143 | if(sVal.isEmpty() || sVal == l.decimalPoint()) sVal = "1"; 144 | // strip unit from text if present 145 | auto cUnit = sVal.at(sVal.size()-1); 146 | if(cUnit < '0' || cUnit > '9') { 147 | sVal = sVal.left(sVal.length()-1); 148 | if(cUnit == '.') cUnit = QChar(0); 149 | if(sVal.isEmpty() || sVal == l.decimalPoint()) sVal = "1"; 150 | } else 151 | cUnit = QChar(0); 152 | 153 | double val = l.toDouble(sVal); 154 | if(cUnit == 'B' || cUnit == 0) { 155 | val = floor(val); 156 | } 157 | auto newText = l.toString(val); 158 | if(cUnit != 0) newText += cUnit.toUpper(); 159 | blockSignals(true); 160 | setText(newText); 161 | blockSignals(false); 162 | emit updateFinished(); 163 | emit valueChanged(getBytes(), true); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /sizeedit.h: -------------------------------------------------------------------------------- 1 | #ifndef SIZEEDIT_H 2 | #define SIZEEDIT_H 3 | 4 | #include 5 | 6 | class SizeEdit : public QLineEdit 7 | { 8 | Q_OBJECT 9 | public: 10 | SizeEdit(QWidget *parent = nullptr); 11 | 12 | quint64 getBytes() const; 13 | void setBytesApprox(quint64 bytes, bool noTrigger = false); 14 | void setBytes(quint64 bytes, bool noTrigger = false); 15 | QString getSizeString() const; 16 | 17 | protected: 18 | void keyPressEvent(QKeyEvent* event) override; 19 | 20 | private: 21 | void setUnit(QChar unit); 22 | bool updateActive; 23 | 24 | private slots: 25 | void onEditingFinished(); 26 | void onTextEdited(const QString& text); 27 | signals: 28 | void valueChanged(quint64 size, bool finished); 29 | void updateFinished(); 30 | }; 31 | 32 | #endif // SIZEEDIT_H 33 | -------------------------------------------------------------------------------- /slicecountspinbox.cpp: -------------------------------------------------------------------------------- 1 | #include "slicecountspinbox.h" 2 | #include "par2calc.h" 3 | #include "settings.h" 4 | 5 | SliceCountSpinBox::SliceCountSpinBox(QWidget *parent) 6 | : QSpinBox(parent), mainWin(nullptr) 7 | { 8 | 9 | } 10 | 11 | void SliceCountSpinBox::stepBy(int steps) 12 | { 13 | if(steps == 0) return; 14 | int target = value() + steps; 15 | // fix up target 16 | if(mainWin) 17 | Par2Calc::sliceSizeFromCount(target, mainWin->optionSliceMultiple, mainWin->optionSliceLimit, mainWin->par2SrcFiles, mainWin->par2FileCount, steps); 18 | 19 | if(target < minimum()) target = minimum(); 20 | else if(target > maximum()) target = maximum(); 21 | if(target != value()) 22 | setValue(target); 23 | } 24 | -------------------------------------------------------------------------------- /slicecountspinbox.h: -------------------------------------------------------------------------------- 1 | #ifndef SLICECOUNTSPINBOX_H 2 | #define SLICECOUNTSPINBOX_H 3 | 4 | #include 5 | #include 6 | #include "mainwindow.h" 7 | 8 | class SliceCountSpinBox : public QSpinBox 9 | { 10 | private: 11 | MainWindow* mainWin; 12 | public: 13 | SliceCountSpinBox(QWidget *parent = nullptr); 14 | inline void setMainWindow(MainWindow* win) { 15 | mainWin = win; 16 | } 17 | 18 | protected: 19 | void stepBy(int steps) override; 20 | }; 21 | 22 | #endif // SLICECOUNTSPINBOX_H 23 | -------------------------------------------------------------------------------- /sourcefile.cpp: -------------------------------------------------------------------------------- 1 | #include "sourcefile.h" 2 | 3 | void SourceFile::load(const QFileInfo& info) 4 | { 5 | _size = info.size(); 6 | _fileName = info.fileName(); 7 | _canonicalPath = info.canonicalPath(); 8 | _lastModified = info.lastModified(); 9 | _exists = info.exists(); 10 | } 11 | 12 | bool SourceFile::refresh() 13 | { 14 | bool changed = false; 15 | QFileInfo info(canonicalFilePath()); 16 | if(_size != info.size() || _lastModified != info.lastModified() || _exists != info.exists()) 17 | changed = true; 18 | load(info); 19 | return changed; 20 | } 21 | -------------------------------------------------------------------------------- /sourcefile.h: -------------------------------------------------------------------------------- 1 | #ifndef SOURCEFILE_H 2 | #define SOURCEFILE_H 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | class SourceFile 9 | { 10 | quint64 _size; 11 | QString _fileName; 12 | QString _canonicalPath; 13 | QDateTime _lastModified; 14 | bool _exists; 15 | 16 | void load(const QFileInfo& info); 17 | public: 18 | inline SourceFile(const QFileInfo& info) { 19 | load(info); 20 | } 21 | SourceFile() { 22 | _exists = false; 23 | _size = 0; 24 | } 25 | inline quint64 size() const { 26 | return _size; 27 | } 28 | inline QString fileName() const { 29 | return _fileName; 30 | } 31 | inline QString completeBaseName() const { 32 | int p = _fileName.lastIndexOf(QChar('.')); 33 | if(p == -1) return _fileName; 34 | return _fileName.left(p); 35 | } 36 | inline QString canonicalPath() const { 37 | return _canonicalPath; 38 | } 39 | inline QString canonicalFilePath() const { 40 | return QDir(_canonicalPath).absoluteFilePath(_fileName); 41 | } 42 | inline QDateTime lastModified() const { 43 | return _lastModified; 44 | } 45 | bool refresh(); 46 | inline bool exists() const { 47 | return _exists; 48 | } 49 | 50 | QString par2name; 51 | }; 52 | 53 | typedef QHash SrcFileList; 54 | 55 | #endif // SOURCEFILE_H 56 | -------------------------------------------------------------------------------- /sourcefileframe.cpp: -------------------------------------------------------------------------------- 1 | #include "sourcefileframe.h" 2 | #include 3 | #include 4 | 5 | void SourceFileFrame::dragEnterEvent(QDragEnterEvent* e) 6 | { 7 | QStackedWidget::dragEnterEvent(e); 8 | 9 | if(e->mimeData()->hasUrls()) { 10 | e->acceptProposedAction(); 11 | } 12 | } 13 | 14 | void SourceFileFrame::dropEvent(QDropEvent* e) 15 | { 16 | QStringList files; 17 | const auto& urls = e->mimeData()->urls(); 18 | files.reserve(urls.size()); 19 | for(const QUrl& url : urls) { 20 | QString file = url.toLocalFile(); 21 | files.append(file); 22 | } 23 | if(!files.isEmpty()) { 24 | emit filesDropped(files); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /sourcefileframe.h: -------------------------------------------------------------------------------- 1 | #ifndef SOURCEFILEFRAME_H 2 | #define SOURCEFILEFRAME_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | class SourceFileFrame : public QStackedWidget 10 | { 11 | Q_OBJECT 12 | 13 | public: 14 | SourceFileFrame(QWidget *parent = nullptr) : QStackedWidget(parent) {} 15 | 16 | protected: 17 | void dragEnterEvent(QDragEnterEvent* e); 18 | void dropEvent(QDropEvent* e); 19 | 20 | signals: 21 | void filesDropped(QStringList files); 22 | }; 23 | 24 | #endif // SOURCEFILEFRAME_H 25 | -------------------------------------------------------------------------------- /sourcefilelistitem.cpp: -------------------------------------------------------------------------------- 1 | #include "sourcefilelistitem.h" 2 | #include 3 | #include 4 | #include "util.h" 5 | 6 | SourceFileListItem* SourceFileListItem::create(QTreeWidgetItem *parent, const SourceFile& file) 7 | { 8 | auto* item = new SourceFileListItem(parent, QStringList{ 9 | file.fileName(), 10 | QLocale().toString(file.lastModified(), QLocale::ShortFormat), 11 | friendlySize(file.size()) 12 | }); 13 | item->setData(1, Qt::UserRole+1, file.lastModified()); 14 | item->setData(2, Qt::UserRole+1, file.size()); 15 | item->setTextAlignment(2, Qt::AlignRight); 16 | 17 | return item; 18 | } 19 | SourceFileListItem* SourceFileListItem::create(QTreeWidgetItem *parent, const QString& folder) 20 | { 21 | auto* item = new SourceFileListItem(parent, QStringList{ 22 | folder, 23 | "", "" 24 | }); 25 | return item; 26 | } 27 | 28 | bool SourceFileListItem::operator<(const QTreeWidgetItem &other) const 29 | { 30 | // always sort directories first 31 | if((childCount() > 0) != (other.childCount() > 0)) { 32 | return childCount() > 0; // true if this is a folder and the other isn't 33 | } 34 | 35 | int sortCol = treeWidget()->sortColumn(); 36 | if(sortCol == 0 || childCount() > 0) { 37 | // sort by name 38 | return text(0).compare(other.text(0), Qt::CaseInsensitive) < 0; 39 | } 40 | if(sortCol == 1) { 41 | // sort by date 42 | return data(1, Qt::UserRole+1).toDateTime() < other.data(1, Qt::UserRole+1).toDateTime(); 43 | } 44 | if(sortCol == 2) { 45 | // sort by size 46 | return data(2, Qt::UserRole+1).toULongLong() < other.data(2, Qt::UserRole+1).toULongLong(); 47 | } 48 | // unknown column 49 | return text(sortCol) < other.text(sortCol); 50 | } 51 | -------------------------------------------------------------------------------- /sourcefilelistitem.h: -------------------------------------------------------------------------------- 1 | #ifndef SOURCEFILELISTITEM_H 2 | #define SOURCEFILELISTITEM_H 3 | 4 | #include 5 | #include "sourcefile.h" 6 | 7 | class SourceFileListItem : public QTreeWidgetItem 8 | { 9 | SourceFileListItem(QTreeWidgetItem *tree) : QTreeWidgetItem(tree) {} 10 | SourceFileListItem(QTreeWidgetItem *parent, const QStringList & strings) : QTreeWidgetItem(parent, strings) {} 11 | public: 12 | static SourceFileListItem* create(QTreeWidgetItem *parent, const SourceFile &file); 13 | static SourceFileListItem* create(QTreeWidgetItem *parent, const QString &folder); 14 | 15 | bool operator< (const QTreeWidgetItem &other) const override; 16 | }; 17 | 18 | #endif // SOURCEFILELISTITEM_H 19 | -------------------------------------------------------------------------------- /util.cpp: -------------------------------------------------------------------------------- 1 | #include "util.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | QString friendlySize(quint64 s) { 8 | //const QVector units{'B', 'K', 'M', 'G', 'T', 'P', 'E'}; 9 | const QString units("BKMGTPE"); 10 | double v = s; 11 | int ui = 0; 12 | for(; ui < units.size(); ui++) { 13 | if(v < 10000) break; 14 | v /= 1024; 15 | } 16 | if(ui) 17 | return QLocale().toString(round(v*100) / 100, 'f', 2) + units.at(ui); 18 | else 19 | // bytes - no decimal needed 20 | return QLocale().toString((int)v) + units.at(ui); 21 | } 22 | 23 | quint64 sizeToBytes(QString size) { 24 | QLocale l; 25 | if(size.isEmpty() || size == l.decimalPoint()) return 0; 26 | // extract unit from text 27 | auto cUnit = size.at(size.size()-1); 28 | if(cUnit < '0' || cUnit > '9') 29 | size = size.left(size.length()-1); 30 | else 31 | cUnit = 'B'; 32 | 33 | double val = l.toDouble(size); 34 | switch(cUnit.toUpper().toLatin1()) { 35 | case 'E': 36 | val *= 1024; 37 | // fallthrough 38 | case 'P': 39 | val *= 1024; 40 | // fallthrough 41 | case 'T': 42 | val *= 1024; 43 | // fallthrough 44 | case 'G': 45 | val *= 1024; 46 | // fallthrough 47 | case 'M': 48 | val *= 1024; 49 | // fallthrough 50 | case 'K': 51 | val *= 1024; 52 | // fallthrough 53 | case 'B': 54 | break; 55 | } 56 | return val; 57 | } 58 | 59 | QString escapeShellArg(QString arg) { 60 | #ifdef Q_OS_WINDOWS 61 | QRegularExpression basic; 62 | // we'll special case our recovery % option 63 | if(arg.count('%') == 1) // % is only problematic if there's more than one of it 64 | basic.setPattern("^[a-zA-Z0-9\\-_=.,:/\\\\%]+$"); 65 | else 66 | basic.setPattern("^[a-zA-Z0-9\\-_=.,:/\\\\]+$"); 67 | if(basic.match(arg).hasMatch()) 68 | return arg; 69 | // I think this is correct... 70 | return QString("\"%1\"").arg(arg.replace("\"", "\"\"").replace("%", "\"^%\"").replace("!", "\"^!\"").replace("\\", "\\\\").replace("\n", "\"^\n\"").replace("\r", "\"^\r\"")); 71 | #else 72 | QRegularExpression basic("^[a-zA-Z0-9\\-_=.,:/]+$"); 73 | if(basic.match(arg).hasMatch()) 74 | return arg; 75 | return QString("'%1'").arg(arg.replace("'", "'\\''")); 76 | #endif 77 | } 78 | -------------------------------------------------------------------------------- /util.h: -------------------------------------------------------------------------------- 1 | #ifndef UTIL_H 2 | #define UTIL_H 3 | 4 | #include 5 | 6 | QString friendlySize(quint64 s); 7 | quint64 sizeToBytes(QString s); 8 | QString escapeShellArg(QString arg); 9 | 10 | #endif // UTIL_H 11 | --------------------------------------------------------------------------------