新增windows支持
This commit is contained in:
@@ -76,19 +76,27 @@ if(DTS_BUILD_GUI)
|
||||
if(DTS_BUILD_RUNTIME_PLUGINS)
|
||||
add_dependencies(DateToSpine
|
||||
dts_runtime_38 dts_runtime_40 dts_runtime_41 dts_runtime_42 dts_runtime_43)
|
||||
# Runtime plugin destination differs by platform:
|
||||
# - macOS: inside the .app bundle (Contents/PlugIns/runtimes)
|
||||
# - Windows/other: alongside the executable
|
||||
if(APPLE)
|
||||
set(DTS_RUNTIME_PLUGIN_DIR "$<TARGET_BUNDLE_DIR:DateToSpine>/Contents/PlugIns/runtimes")
|
||||
else()
|
||||
set(DTS_RUNTIME_PLUGIN_DIR "$<TARGET_FILE_DIR:DateToSpine>/runtimes")
|
||||
endif()
|
||||
foreach(runtime_line IN ITEMS 38 40 41 42 43)
|
||||
add_custom_command(TARGET dts_runtime_${runtime_line} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory
|
||||
"$<TARGET_BUNDLE_DIR:DateToSpine>/Contents/PlugIns/runtimes"
|
||||
"${DTS_RUNTIME_PLUGIN_DIR}"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"$<TARGET_FILE:dts_runtime_${runtime_line}>"
|
||||
"$<TARGET_BUNDLE_DIR:DateToSpine>/Contents/PlugIns/runtimes/")
|
||||
"${DTS_RUNTIME_PLUGIN_DIR}/")
|
||||
add_custom_command(TARGET DateToSpine POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory
|
||||
"$<TARGET_BUNDLE_DIR:DateToSpine>/Contents/PlugIns/runtimes"
|
||||
"${DTS_RUNTIME_PLUGIN_DIR}"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"$<TARGET_FILE:dts_runtime_${runtime_line}>"
|
||||
"$<TARGET_BUNDLE_DIR:DateToSpine>/Contents/PlugIns/runtimes/")
|
||||
"${DTS_RUNTIME_PLUGIN_DIR}/")
|
||||
endforeach()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
@@ -39,9 +39,11 @@ EditorProbeResult SpineOutputParser::parse(
|
||||
static const std::regex launcher(R"(Spine Launcher\s+(\d+\.\d+(?:\.\d+)?))");
|
||||
static const std::regex launching(
|
||||
R"((?:Launching:|Starting:)\s*Spine\s+(\d+\.\d+(?:\.\d+)?)(?:\s+([^\r\n]+))?)");
|
||||
// MSVC's std::regex does not implement std::regex_constants::multiline,
|
||||
// so instead of relying on the '^' line anchor with the multiline flag,
|
||||
// search for "Spine <version>" anywhere (regex_search scans the whole text).
|
||||
static const std::regex directEditor(
|
||||
R"(^Spine\s+(\d+\.\d+(?:\.\d+)?)(?:\s+([^\r\n]+))?)",
|
||||
std::regex_constants::multiline);
|
||||
R"(Spine\s+(\d+\.\d+(?:\.\d+)?)(?:\s+([^\r\n]+))?)");
|
||||
|
||||
result.launcherVersion = firstVersion(process.output, launcher);
|
||||
result.editorVersion = firstVersion(process.output, launching, &result.edition);
|
||||
|
||||
@@ -17,6 +17,10 @@ std::vector<fs::path> SpineVersionAvailability::defaultCacheRoots() const {
|
||||
roots.emplace_back(fs::path(roaming) / "Spine/updates");
|
||||
if (const char* local = std::getenv("LOCALAPPDATA"))
|
||||
roots.emplace_back(fs::path(local) / "Spine/updates");
|
||||
// Spine Launcher also keeps its update cache directly under the user's
|
||||
// profile directory (e.g. C:\Users\<name>\Spine\updates).
|
||||
if (const char* userProfile = std::getenv("USERPROFILE"))
|
||||
roots.emplace_back(fs::path(userProfile) / "Spine/updates");
|
||||
#endif
|
||||
return roots;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
#include "atlas/AtlasParser.hpp"
|
||||
#include "conversion/ConversionJob.hpp"
|
||||
#include "editor/SpineEditorLocator.hpp"
|
||||
#include "editor/SpineProbe.hpp"
|
||||
#include "editor/SpineVersionAvailability.hpp"
|
||||
#include "platform/ProcessRunner.hpp"
|
||||
#include "scan/SpineDataVersionDetector.hpp"
|
||||
#include "ui/SpinePreviewWidget.hpp"
|
||||
|
||||
@@ -23,6 +25,7 @@
|
||||
#include <QPushButton>
|
||||
#include <QSettings>
|
||||
#include <QTimer>
|
||||
#include <QtConcurrent/QtConcurrent>
|
||||
#include <QUrl>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
@@ -134,38 +137,69 @@ void MainWindow::setupUi() {
|
||||
connect(convertButton_, &QPushButton::clicked, this, &MainWindow::convertCurrent);
|
||||
previewTimer_ = new QTimer(this);
|
||||
previewTimer_->setInterval(16);
|
||||
previewTimer_->setTimerType(Qt::PreciseTimer);
|
||||
connect(previewTimer_, &QTimer::timeout, this, &MainWindow::advancePreview);
|
||||
|
||||
editorProbeWatcher_ = new QFutureWatcher<QPair<int, QString>>(this);
|
||||
connect(editorProbeWatcher_,
|
||||
&QFutureWatcher<QPair<int, QString>>::finished,
|
||||
this, &MainWindow::onEditorProbeFinished);
|
||||
}
|
||||
|
||||
void MainWindow::showEmptyState() {
|
||||
previewWidget_->resetViewport();
|
||||
previewWidget_->setMessage(
|
||||
tr("把 .json / .skel、.atlas 和图片拖到这里\n\n也可以拖入整个资源文件夹"));
|
||||
tr("把 .json / .skel / .skel.bytes、.atlas / .atlas.txt 和图片拖到这里\n\n也可以拖入整个资源文件夹"));
|
||||
statusLabel_->clear();
|
||||
}
|
||||
|
||||
void MainWindow::chooseResources() {
|
||||
const auto paths = QFileDialog::getOpenFileNames(this, tr("选择导出资源"), {},
|
||||
tr("Spine 资源 (*.json *.skel *.atlas *.png *.jpg *.jpeg *.webp);;所有文件 (*)"));
|
||||
tr("Spine 资源 (*.json *.skel *.skel.bytes *.atlas *.atlas.txt *.png *.jpg *.jpeg *.webp);;所有文件 (*)"));
|
||||
for (const auto& path : paths) addInput(path);
|
||||
}
|
||||
|
||||
void MainWindow::addInput(const QString& path) {
|
||||
const QFileInfo info(path);
|
||||
const auto scanDirectory = [this](const QString& directory, bool recursive) {
|
||||
QDirIterator iterator(directory, {"*.json", "*.skel"}, QDir::Files,
|
||||
const auto scanDirectory = [this](const QString& directory, bool recursive, bool warnUnsupported) {
|
||||
QDirIterator iterator(directory, {"*.json", "*.skel", "*.skel.bytes"}, QDir::Files,
|
||||
recursive ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags);
|
||||
while (iterator.hasNext()) addSkeleton(iterator.next());
|
||||
while (iterator.hasNext()) addSkeleton(iterator.next(), warnUnsupported);
|
||||
};
|
||||
if (info.isDir()) scanDirectory(path, true);
|
||||
// Only the primary skeleton data (.json / .skel / .skel.bytes) carries a
|
||||
// Spine version and should trigger the "unsupported version" warning.
|
||||
// Companion files (.atlas / .atlas.txt / images) have no version of their
|
||||
// own, so they must not cause a version check when scanned for a skeleton.
|
||||
if (info.isDir()) scanDirectory(path, true, true);
|
||||
else if (info.suffix().compare("json", Qt::CaseInsensitive) == 0
|
||||
|| info.suffix().compare("skel", Qt::CaseInsensitive) == 0) addSkeleton(path);
|
||||
else scanDirectory(info.absolutePath(), false);
|
||||
|| info.suffix().compare("skel", Qt::CaseInsensitive) == 0
|
||||
|| info.completeSuffix().compare("skel.bytes", Qt::CaseInsensitive) == 0) addSkeleton(path, true);
|
||||
else scanDirectory(info.absolutePath(), false, false);
|
||||
scheduleAvailabilityCheck();
|
||||
}
|
||||
|
||||
void MainWindow::addSkeleton(const QString& path) {
|
||||
void MainWindow::addSkeleton(const QString& path, bool warnInvalid) {
|
||||
const auto result = SpineDataVersionDetector{}.inspect(localPath(path));
|
||||
if (result.status == VersionDetectionStatus::UnsupportedVersion) {
|
||||
if (warnInvalid) {
|
||||
const auto version = result.version
|
||||
? QString::fromStdString(result.version->toString()) : tr("未知");
|
||||
pendingVersionWarnings_.append(
|
||||
tr("%1(版本 %2)").arg(QFileInfo(path).fileName(), version));
|
||||
// Defer to the event loop so multiple unsupported files in a single
|
||||
// import are collected and reported with one dialog.
|
||||
QTimer::singleShot(0, this, &MainWindow::showUnsupportedVersionWarning);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (result.status == VersionDetectionStatus::InvalidData
|
||||
|| result.status == VersionDetectionStatus::FileMissing) {
|
||||
if (warnInvalid) {
|
||||
pendingInvalidAssets_.append(QFileInfo(path).fileName());
|
||||
QTimer::singleShot(0, this, &MainWindow::showInvalidAssetWarning);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (result.status != VersionDetectionStatus::Detected || skeletonPaths_.contains(path)) return;
|
||||
skeletonPaths_.append(path);
|
||||
resourceCombo_->addItem(QFileInfo(path).completeBaseName(), path);
|
||||
@@ -173,6 +207,32 @@ void MainWindow::addSkeleton(const QString& path) {
|
||||
resourceCombo_->setCurrentIndex(resourceCombo_->count() - 1);
|
||||
}
|
||||
|
||||
void MainWindow::showUnsupportedVersionWarning() {
|
||||
if (pendingVersionWarnings_.isEmpty()) return;
|
||||
// Deduplicate so the same resource reported multiple times (e.g. repeated
|
||||
// drag-and-drop events) is only listed once.
|
||||
QStringList unique;
|
||||
for (const auto& item : pendingVersionWarnings_)
|
||||
if (!unique.contains(item)) unique.append(item);
|
||||
pendingVersionWarnings_.clear();
|
||||
const auto details = unique.join(QLatin1String("\n"));
|
||||
QMessageBox::warning(this, tr("版本过低"),
|
||||
tr("以下资源的 Spine 版本不在支持的范围内(支持的版本:3.8、4.0、4.1、4.2、4.3):\n\n%1")
|
||||
.arg(details));
|
||||
}
|
||||
|
||||
void MainWindow::showInvalidAssetWarning() {
|
||||
if (pendingInvalidAssets_.isEmpty()) return;
|
||||
// Deduplicate so the same file reported multiple times is only listed once.
|
||||
QStringList unique;
|
||||
for (const auto& item : pendingInvalidAssets_)
|
||||
if (!unique.contains(item)) unique.append(item);
|
||||
pendingInvalidAssets_.clear();
|
||||
const auto details = unique.join(QLatin1String("\n"));
|
||||
QMessageBox::warning(this, tr("未识别到正确的 Spine 资产"),
|
||||
tr("以下文件未能识别为有效的 Spine 资产:\n\n%1").arg(details));
|
||||
}
|
||||
|
||||
QString MainWindow::findAtlas(const QString& skeletonPath) const {
|
||||
const QFileInfo skeleton(skeletonPath);
|
||||
QDir directory(skeleton.absolutePath());
|
||||
@@ -256,13 +316,18 @@ void MainWindow::animationChanged(const QString& name) {
|
||||
statusLabel_->setText(QString::fromStdString(error));
|
||||
return;
|
||||
}
|
||||
previewClock_.restart();
|
||||
advancePreview();
|
||||
previewTimer_->start();
|
||||
}
|
||||
|
||||
void MainWindow::advancePreview() {
|
||||
std::string error;
|
||||
auto frame = runtime_.updatePreview(0.016F, error);
|
||||
// Use the real elapsed wall-clock time as the frame delta so the animation
|
||||
// matches the actual playback speed even if the timer interval drifts
|
||||
// (Windows timers are not exact). Falls back to 16ms on the first frame.
|
||||
const float delta = previewClock_.isValid() ? previewClock_.restart() / 1000.0F : 0.016F;
|
||||
auto frame = runtime_.updatePreview(delta, error);
|
||||
if (!frame) {
|
||||
previewTimer_->stop();
|
||||
previewWidget_->setMessage(tr("动画预览暂时不可用"));
|
||||
@@ -351,9 +416,13 @@ void MainWindow::refreshAvailability() {
|
||||
}
|
||||
currentEditorVersion_ = availableVersions_.value(line);
|
||||
if (currentEditorVersion_.isEmpty()) {
|
||||
showAvailability(tr("缺少 Spine %1").arg(line), QStringLiteral("#ef6b73"));
|
||||
convertButton_->setText(tr("指定 Spine %1 路径").arg(line));
|
||||
convertButton_->setEnabled(true);
|
||||
// Run the version check (cache scan + editor probe) in the background
|
||||
// (bounded by the probe timeout) so the UI does not block. If no
|
||||
// matching version is found, the user is warned with a dialog.
|
||||
showAvailability(tr("正在检查 Editor ..."), QStringLiteral("#f2b84b"));
|
||||
convertButton_->setText(tr("检查中..."));
|
||||
convertButton_->setEnabled(false);
|
||||
startEditorProbe(line);
|
||||
return;
|
||||
}
|
||||
showAvailability(tr("可转换 · Spine %1").arg(currentEditorVersion_),
|
||||
@@ -362,6 +431,47 @@ void MainWindow::refreshAvailability() {
|
||||
convertButton_->setEnabled(true);
|
||||
}
|
||||
|
||||
void MainWindow::startEditorProbe(const QString& line) {
|
||||
const std::string lineStr = line.toStdString();
|
||||
const auto editorPath = localPath(editorPath_);
|
||||
const int generation = ++editorProbeGeneration_;
|
||||
auto future = QtConcurrent::run([lineStr, editorPath, generation]() -> QPair<int, QString> {
|
||||
// Scan the Spine update cache first; if no matching release is cached,
|
||||
// probe the explicitly selected Editor executable (bounded timeout).
|
||||
SpineVersionAvailability availability;
|
||||
if (const auto cached = availability.newestForLine(lineStr))
|
||||
return {generation, QString::fromStdString(cached->toString())};
|
||||
const auto runner = createDefaultProcessRunner();
|
||||
const SpineProbe probe(*runner);
|
||||
const auto result = probe.inspect(editorPath, std::chrono::seconds(3));
|
||||
if (result.editorVersion && result.editorVersion->line() == lineStr)
|
||||
return {generation, QString::fromStdString(result.editorVersion->toString())};
|
||||
return {generation, QString{}};
|
||||
});
|
||||
editorProbeWatcher_->setFuture(future);
|
||||
}
|
||||
|
||||
void MainWindow::onEditorProbeFinished() {
|
||||
if (!editorProbeWatcher_ || !editorProbeWatcher_->isFinished()) return;
|
||||
const auto outcome = editorProbeWatcher_->result();
|
||||
if (outcome.first != editorProbeGeneration_) return;
|
||||
const QString version = outcome.second;
|
||||
if (version.isEmpty()) {
|
||||
showAvailability(tr("未找到对应版本"), QStringLiteral("#ef6b73"));
|
||||
convertButton_->setText(tr("指定 Spine 路径"));
|
||||
convertButton_->setEnabled(true);
|
||||
// Warn the user one second after the scan when no matching version is found.
|
||||
QTimer::singleShot(1000, this, [this] {
|
||||
QMessageBox::warning(this, tr("未找到对应版本"), tr("未找到对应版本"));
|
||||
});
|
||||
} else {
|
||||
currentEditorVersion_ = version;
|
||||
showAvailability(tr("可转换 · Spine %1").arg(version), QStringLiteral("#55d68b"));
|
||||
convertButton_->setText(tr("一键转换为 .spine"));
|
||||
convertButton_->setEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
bool MainWindow::chooseEditorPath() {
|
||||
const auto selected = QFileDialog::getOpenFileName(this,
|
||||
tr("指定 Spine 程序"), {}, tr("所有文件 (*)"));
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
#include <QMainWindow>
|
||||
#include <QHash>
|
||||
#include <QStringList>
|
||||
#include <QElapsedTimer>
|
||||
#include <QFutureWatcher>
|
||||
#include <QPair>
|
||||
|
||||
class QComboBox;
|
||||
class QCheckBox;
|
||||
@@ -36,11 +39,12 @@ private slots:
|
||||
const QString& error);
|
||||
void animationChanged(const QString& name);
|
||||
void advancePreview();
|
||||
void onEditorProbeFinished();
|
||||
|
||||
private:
|
||||
void setupUi();
|
||||
void addInput(const QString& path);
|
||||
void addSkeleton(const QString& path);
|
||||
void addSkeleton(const QString& path, bool warnInvalid = true);
|
||||
void loadCurrentPreview();
|
||||
QString findAtlas(const QString& skeletonPath) const;
|
||||
QString runtimeLibrary(const QString& versionLine) const;
|
||||
@@ -51,6 +55,9 @@ private:
|
||||
void showAvailability(const QString& text, const QString& color);
|
||||
void showEmptyState();
|
||||
void setBusy(bool busy);
|
||||
void startEditorProbe(const QString& line);
|
||||
void showUnsupportedVersionWarning();
|
||||
void showInvalidAssetWarning();
|
||||
|
||||
QComboBox* resourceCombo_ = nullptr;
|
||||
QComboBox* animationCombo_ = nullptr;
|
||||
@@ -68,6 +75,11 @@ private:
|
||||
RuntimePlugin runtime_;
|
||||
ConversionJob* conversionJob_ = nullptr;
|
||||
QTimer* previewTimer_ = nullptr;
|
||||
QElapsedTimer previewClock_;
|
||||
QFutureWatcher<QPair<int, QString>>* editorProbeWatcher_ = nullptr;
|
||||
int editorProbeGeneration_ = 0;
|
||||
QStringList pendingVersionWarnings_;
|
||||
QStringList pendingInvalidAssets_;
|
||||
};
|
||||
|
||||
} // namespace dts
|
||||
|
||||
Reference in New Issue
Block a user