diff --git a/sources/dialogs.cpp b/sources/dialogs.cpp index 561f9bc..68b6f12 100644 --- a/sources/dialogs.cpp +++ b/sources/dialogs.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -19,9 +20,12 @@ #include #include #include +#include #include #include #include +#include +#include namespace { void frame2SecKoma(int frame, int& sec, int& koma) { @@ -822,6 +826,106 @@ void PreferencesDialog::onSuffixEdited() { //----------------------------------------------------------------------------- +CspLinkChooserDialog::CspLinkChooserDialog(const QStringList& openPaths, + QWidget* parent) + : QDialog(parent) + , m_useOpenRadio(nullptr) + , m_openFilesCombo(nullptr) + , m_needsOverwriteConfirm(false) { + setWindowTitle(tr("Sync Clip Studio Paint Timesheet")); + + QVBoxLayout* layout = new QVBoxLayout(); + layout->addWidget(new QLabel( + tr("Choose how to sync the Clip Studio Paint export with XDTS Viewer."), + this)); + + QButtonGroup* group = new QButtonGroup(this); + + // Only offered when at least one window is already open. + // æ—¢ã�«é–‹ã�„ã�¦ã�„るウィンドウã�Œ1ã�¤ä»¥ä¸Šã�‚ã‚‹å ´å�ˆã�®ã�¿æ��示ã�™ã‚‹ã€‚ + if (!openPaths.isEmpty()) { + m_useOpenRadio = + new QRadioButton(tr("Sync with an already-open file:"), this); + group->addButton(m_useOpenRadio); + layout->addWidget(m_useOpenRadio); + + m_openFilesCombo = new QComboBox(this); + for (const QString& path : openPaths) + m_openFilesCombo->addItem(QFileInfo(path).fileName(), path); + layout->addWidget(m_openFilesCombo); + + m_useOpenRadio->setChecked(true); + } + + // A single field covers both "sync with a different existing file" and + // "export as a new file": which one applies is decided in onAccept() from + // whether the entered path exists on disk, not from how it was entered. + // 「別ã�®æ—¢å­˜ãƒ•ァイルã�¨å�ŒæœŸã€�ã�¨ã€Œæ–°è¦�ファイルã�¨ã�—ã�¦ã‚¨ã‚¯ã‚¹ãƒ�ートã€�ã‚’å�˜ä¸€ã�® + // フィールドã�«ã�¾ã�¨ã‚�る。ã�©ã�¡ã‚‰ã�«ã�ªã‚‹ã�‹ã�¯onAccept()ã�§å…¥åŠ›ãƒ‘ã‚¹ã�Œãƒ‡ã‚£ã‚¹ã‚¯ä¸Š + // ã�«å®Ÿåœ¨ã�™ã‚‹ã�‹ã�©ã�†ã�‹ã�‹ã‚‰åˆ¤å®šã�—ã€�入力方法ã�§ã�¯åŒºåˆ¥ã�—ã�ªã�„。 + m_useFileRadio = new QRadioButton( + tr("Sync with an existing file, or export as a new file:"), this); + group->addButton(m_useFileRadio); + layout->addWidget(m_useFileRadio); + { + QHBoxLayout* row = new QHBoxLayout(); + m_filePathField = new QLineEdit(this); + m_filePathField->setReadOnly(true); + QPushButton* browseBtn = new QPushButton("...", this); + row->addWidget(m_filePathField); + row->addWidget(browseBtn); + layout->addLayout(row); + connect(browseBtn, SIGNAL(clicked()), this, SLOT(onBrowseFile())); + } + + if (!m_useOpenRadio) m_useFileRadio->setChecked(true); + + QHBoxLayout* buttonRow = new QHBoxLayout(); + buttonRow->addStretch(); + QPushButton* okButton = new QPushButton(tr("OK"), this); + QPushButton* cancelButton = new QPushButton(tr("Cancel"), this); + buttonRow->addWidget(okButton); + buttonRow->addWidget(cancelButton); + layout->addLayout(buttonRow); + + connect(okButton, SIGNAL(clicked()), this, SLOT(onAccept())); + connect(cancelButton, SIGNAL(clicked()), this, SLOT(reject())); + + setLayout(layout); +} + +void CspLinkChooserDialog::onBrowseFile() { + // DontConfirmOverwrite: our own confirmation in InstanceManager (which + // explains this will be overwritten with the CSP export) replaces the + // native one, rather than showing both back to back. + // DontConfirmOverwrite: + // ãƒ�イティブã�®ä¸Šæ›¸ã��確èª�ã�®ä»£ã‚�りã�«ã€�InstanceManagerå�´ã�®ï¼ˆCSP書ã��出ã�—ã�§ + // 上書ã��ã�•れる旨を説明ã�™ã‚‹ï¼‰ç¢ºèª�を表示ã�™ã‚‹ã�Ÿã‚�ã€�二é‡�ã�«ç¢ºèª�ã�—ã�ªã�„。 + QString path = QFileDialog::getSaveFileName( + this, tr("Select or Enter XDTS File"), QString(), + tr("XDTS files (*.xdts)"), nullptr, QFileDialog::DontConfirmOverwrite); + if (path.isEmpty()) return; + if (!path.endsWith(".xdts")) path += ".xdts"; + m_filePathField->setText(path); + m_useFileRadio->setChecked(true); +} + +void CspLinkChooserDialog::onAccept() { + if (m_useOpenRadio && m_useOpenRadio->isChecked()) { + m_resultPath = m_openFilesCombo->currentData().toString(); + m_needsOverwriteConfirm = true; + } else if (m_useFileRadio->isChecked()) { + if (m_filePathField->text().isEmpty()) return; + m_resultPath = m_filePathField->text(); + m_needsOverwriteConfirm = QFileInfo(m_resultPath).exists(); + } else { + return; + } + accept(); +} + +//----------------------------------------------------------------------------- + AboutDialog::AboutDialog(QWidget* parent) : QDialog(parent) { setFixedSize(500, 500); setWindowTitle(tr("About %1").arg(qApp->applicationName())); diff --git a/sources/dialogs.h b/sources/dialogs.h index bcd27ce..05af67b 100644 --- a/sources/dialogs.h +++ b/sources/dialogs.h @@ -1,9 +1,10 @@ -#pragma once +#pragma once #ifndef DIALOGS_H #define DIALOGS_H #include #include +#include class QComboBox; class QLineEdit; @@ -12,6 +13,7 @@ class QColorDialog; class QFontComboBox; class QCheckBox; class QGroupBox; +class QRadioButton; class SettingsDialog : public QDialog { Q_OBJECT @@ -98,6 +100,41 @@ protected slots: void onSuffixEdited(); }; +// Lets the user choose how to sync a Clip Studio Paint timesheet export +// (always written to the same fixed path) with a real XDTS file: reuse an +// already-open window, or sync/export to a file path they enter (an +// existing path is overwritten in place; a not-yet-existing path is +// exported as new — decided from disk state at OK time, not from how the +// path was entered). Used only by InstanceManager (the mothership process). +// Clip Studio Paintã�®ã‚¿ã‚¤ãƒ ã‚·ãƒ¼ãƒˆæ›¸ã��出ã�—(常ã�«å�Œä¸€ã�®å›ºå®šãƒ‘スã�«æ›¸ã�‹ã‚Œã‚‹ï¼‰ã‚’ +// 実体ã�®ã�‚ã‚‹XDTSファイルã�¨å�ŒæœŸã�™ã‚‹æ–¹æ³•をユーザーã�«é�¸ã�°ã�›ã‚‹ãƒ€ã‚¤ã‚¢ãƒ­ã‚°ã€‚ +// æ—¢ã�«é–‹ã�„ã�¦ã�„るウィンドウã�®å†�利用ã€�ã�¾ã�Ÿã�¯å…¥åŠ›ã�—ã�Ÿãƒ‘スã�¸ã�®å�ŒæœŸï¼�æ–°è¦� +// エクスãƒ�ート(既存パスã�ªã‚‰ã��ã�®å ´ã�§ä¸Šæ›¸ã��ã€�未存在ã�®ãƒ‘スã�ªã‚‰æ–°è¦� +// エクスãƒ�ート——OKクリック時ã�®ãƒ‡ã‚£ã‚¹ã‚¯ä¸Šã�®çŠ¶æ…‹ã�§åˆ¤å®šã�—ã€�入力方法ã�§ã�¯ +// 区別ã�—ã�ªã�„)ã�‹ã‚‰é�¸ã�¶ã€‚InstanceManager(æ¯�艦プロセス)専用。 +class CspLinkChooserDialog : public QDialog { + Q_OBJECT + + QRadioButton* m_useOpenRadio; + QComboBox* m_openFilesCombo; + QRadioButton* m_useFileRadio; + QLineEdit* m_filePathField; + + QString m_resultPath; + bool m_needsOverwriteConfirm; + +public: + CspLinkChooserDialog(const QStringList& openPaths, QWidget* parent = nullptr); + + // Valid only once exec() returns QDialog::Accepted. + QString resultPath() const { return m_resultPath; } + bool needsOverwriteConfirm() const { return m_needsOverwriteConfirm; } + +protected slots: + void onBrowseFile(); + void onAccept(); +}; + class AboutDialog : public QDialog { Q_OBJECT diff --git a/sources/instancemanager.cpp b/sources/instancemanager.cpp index 30e69c0..47cbe3f 100644 --- a/sources/instancemanager.cpp +++ b/sources/instancemanager.cpp @@ -1,5 +1,7 @@ #include "instancemanager.h" #include "pathutils.h" +#include "xdtsio.h" +#include "dialogs.h" #include #include @@ -8,9 +10,21 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include #include +#ifdef WIN32 +#include +#endif + namespace { // Fixed identifiers shared by every process of this app so they can find // each other. Kept in one place rather than as class statics to avoid @@ -66,7 +80,25 @@ InstanceManager::Role InstanceManager::bootstrap(const QStringList& args, QLocalSocket socket; socket.connectToServer(kServerName); if (socket.waitForConnected(1000)) { - sendFrame(&socket, Command::Open, requestedPath); + bool sent = sendFrame(&socket, Command::Open, requestedPath); + // This process exits right after returning (the Forwarded role skips + // a.exec() entirely in main.cpp), tearing down the pipe handle almost + // immediately after the write above. On Windows this occasionally + // races the mothership's asynchronous read of that same data: if the + // pipe's "closing" notification is dispatched before the "data + // arrived" one, the write can be silently lost even though it + // reached the OS-level buffer. A brief pause here gives the + // mothership's event loop time to actually read the frame before + // this process's handle goes away. + // ‚±‚̃vƒ�ƒZƒX‚Íreturn’¼Œã‚É�I—¹‚·‚é�iForwardedƒ��[ƒ‹‚Ímain.cpp‚Å + // a.exec()Ž©‘Ì‚ðŒÄ‚΂Ȃ¢‚½‚ß�j‚½‚ß�A�ã‚Ìwrite’¼Œã‚ɂقڑ¦�À‚ɃpƒCƒv + // ƒnƒ“ƒhƒ‹‚ª•‚¶‚ç‚ê‚é�BWindows‚Å‚Í�A•êŠÍ‘¤‚Ì”ñ“¯Šú“ǂݎæ‚è‚Æ‚±‚ꂪ + // ‹H‚É‹£�‡‚·‚邱‚Æ‚ª‚ ‚é: �u�Ø’f�v‚Ì’Ê’m‚ª�uƒf�[ƒ^“ž’…�v‚Ì’Ê’m‚æ‚è + // �æ‚É”z‘—‚³‚ê‚邯�AOSƒŒƒxƒ‹‚̃oƒbƒtƒ@‚ɂ͓͂¢‚Ä‚¢‚½�‘‚«�ž‚Ý‚ª + // �©‚ÉŽ¸‚í‚ê‚邱‚Æ‚ª‚ ‚é�B‚±‚±‚ňêŒÄ‹z’u‚­‚±‚Æ‚Å�A‚±‚̃vƒ�ƒZƒX‚Ì + // ƒnƒ“ƒhƒ‹‚ª�Á‚¦‚é‘O‚É•êŠÍ‚̃Cƒxƒ“ƒgƒ‹�[ƒv‚ªŠmŽÀ‚ɃtƒŒ�[ƒ€‚ð“Ç‚Ý + // Žæ‚ê‚邿‚¤‚É‚·‚é�B + if (sent) QThread::msleep(50); socket.disconnectFromServer(); m_role = Role::Forwarded; return m_role; @@ -124,10 +156,41 @@ void InstanceManager::startMothership(const QString& initialPath) { connect(m_server, &QLocalServer::newConnection, this, &InstanceManager::onNewConnection); m_role = Role::Mothership; + + // The mothership has no MyWindow of its own; its only ever-visible + // window is a transient CSP sync dialog. Qt's default + // quitOnLastWindowClosed=true would otherwise quit the whole process + // (killing the QLocalServer and orphaning every worker) the moment that + // dialog closes. This process's lifetime is instead driven explicitly by + // onServerSocketDisconnected() (quits once m_workers is empty). + // •êŠÍ‚ÍMyWindow‚ðŽ�‚½‚¸�A—Bˆê•\ަ‚³‚ꂤ‚é‚Ì‚ÍCSP“¯Šú—p‚̈ꎞ“I‚È + // ƒ_ƒCƒAƒ�ƒO‚Ì‚Ý�BQt‚ÌŠù’è‚Å‚ ‚équitOnLastWindowClosed=true‚̂܂܂¾‚Æ�A + // ‚»‚̃_ƒCƒAƒ�ƒO‚ª•‚¶‚½�uŠÔ‚Ƀvƒ�ƒZƒX‘S‘Ì‚ª�I—¹‚µ‚Ä‚µ‚Ü‚¢ + // �iQLocalServer‚à�Á‚¦�A‘Sƒ��[ƒJ�[‚ªŒÇ—§‚·‚é�j�B‚±‚̃vƒ�ƒZƒX‚ÌŽõ–½‚Í + // ‘ã‚í‚è‚ÉonServerSocketDisconnected()�im_workers‚ª‹ó‚ɂȂÁ‚½‚ç�I—¹�j‚Å + // –¾Ž¦“I‚É�§Œä‚·‚é�B + qApp->setQuitOnLastWindowClosed(false); + // Treat our own launch argument the same way as a request coming from // another process (always spawns a worker, since nothing is registered // yet at this point). handleOpenRequest(initialPath); + + // If the initial request ended without spawning any worker (e.g. the user + // cancelled the CSP sync dialog above), this invisible process has nothing + // to manage and must not linger: setQuitOnLastWindowClosed(false) above + // also disabled the implicit quit that used to end it, and a lingering + // mothership would keep holding the shared-memory lock and the IPC pipe. + // quit() called before exec() is ignored, so post it to run as soon as + // exec() starts instead. + // �Å�‰‚Ì—v‹�‚ªƒ��[ƒJ�[‚ð1‚‚à‹N“®‚¹‚¸‚É�I‚í‚Á‚½�ê�‡�i—á:�ã‚ÌCSP“¯Šú + // ƒ_ƒCƒAƒ�ƒO‚ðƒ†�[ƒU�[‚ªƒLƒƒƒ“ƒZƒ‹‚µ‚½�ê�‡�j�A‚±‚Ì•s‰ÂŽ‹ƒvƒ�ƒZƒX‚É‚Í + // ŠÇ—�‘Î�Û‚ª‚È‚­�AŽc—¯‚³‚¹‚Ă͂Ȃç‚È‚¢�B�ã‚ÌsetQuitOnLastWindowClosed + // (false)‚É‚æ‚è�]—ˆ‚̈ÖÙ�I—¹‚à–³Œø‰»‚³‚ê‚Ä‚¨‚è�AŽc—¯‚µ‚½•êŠÍ‚Í‹¤—L + // ƒ�ƒ‚ƒŠƒ�ƒbƒN‚ÆIPCƒpƒCƒv‚ðˆ¬‚è‘±‚¯‚Ä‚µ‚Ü‚¤�Bexec()‘O‚Ìquit()‚Í–³Ž‹ + // ‚³‚ê‚邽‚ß�Aexec()ŠJŽn’¼Œã‚ÉŽÀ�s‚³‚ê‚邿‚¤“Š”Ÿ‚µ‚Ä‚¨‚­�B + if (!m_spawnedAnyWorker && m_workers.isEmpty()) + QTimer::singleShot(0, qApp, &QCoreApplication::quit); } void InstanceManager::onNewConnection() { @@ -165,11 +228,48 @@ void InstanceManager::onServerSocketReadyRead() { // Registers one path this worker currently has open. A paired // genga/douga sheet sends both paths, so either one resolves to this // window. - m_workers.insert(PathUtils::canonicalizePath(path), socket); + QString registeredPath = PathUtils::canonicalizePath(path); + m_workers.insert(registeredPath, socket); + // Covers the case where this socket belongs to a worker just spawned + // for a CSP link (performCspSync() couldn't notify it directly since + // it wasn't registered yet at that point). + // performCspSync()‚ªCSP˜AŒg‚Ì‚½‚߂ɋN“®‚µ‚½‚΂©‚è‚̃��[ƒJ�[‚Í�A‚»‚Ì + // Žž“_‚ł͖¢“o˜^‚Ì‚½‚ß’¼�Ú’Ê’m‚Å‚«‚È‚¢�B‚»‚̃P�[ƒX‚ð‚±‚±‚ŃJƒo�[ + // ‚·‚é�B + if (!m_linkedDestPath.isEmpty() && registeredPath == m_linkedDestPath) { + m_linkedWorkerSocket = socket; + sendFrame(socket, Command::LinkStatusChanged, QStringLiteral("1")); + } } else if (cmd == Command::Open) { // A short-lived forwarder is relaying an open request; it will - // disconnect on its own right after this. - handleOpenRequest(path); + // disconnect on its own right after this. Defer the handling to the + // event loop instead of calling it here: handleOpenRequest() can show + // a modal CSP dialog, and that dialog's nested event loop delivers + // this very socket's disconnected() signal while this slot is still + // on the stack. onServerSocketDisconnected() then destroys `buffer` + // and deletes `socket`, so resuming the loop below after the dialog + // would touch freed memory and hang/crash the mothership (which then + // silently stops serving IPC while its pipe still accepts connects). + // ’Z–½‚Ì“]‘—ƒvƒ�ƒZƒX‚ªƒI�[ƒvƒ“—v‹�‚𒆌p‚µ‚Ä‚¨‚è�A‘—�M’¼Œã‚ÉŽ©•ª‚©‚ç + // �Ø’f‚µ‚Ä‚­‚é�B‚±‚±‚Å’¼�Ú�ˆ—�‚¹‚¸�AƒCƒxƒ“ƒgƒ‹�[ƒv‚Ö’x‰„‚³‚¹‚é: + // handleOpenRequest()‚̓‚�[ƒ_ƒ‹‚ÈCSPƒ_ƒCƒAƒ�ƒO‚ð•\ަ‚·‚邱‚Æ‚ª‚ ‚è�A + // ‚»‚̃_ƒCƒAƒ�ƒO‚̃lƒXƒg‚µ‚½ƒCƒxƒ“ƒgƒ‹�[ƒv‚ª�A‚±‚̃Xƒ�ƒbƒg‚ÌŽÀ�s’†‚É + // ‚Ü‚³‚É‚±‚̃\ƒPƒbƒg‚Ìdisconnected()‚ð”z‘—‚µ‚Ä‚µ‚Ü‚¤�B‚·‚邯 + // onServerSocketDisconnected()‚ª`buffer`‚ð”jŠü‚µ`socket`‚ð�í�œ‚·‚é + // ‚½‚ß�Aƒ_ƒCƒAƒ�ƒOŒã‚ɉº‚̃‹�[ƒv‚ð�ÄŠJ‚·‚邯‰ð•ú�ς݃�ƒ‚ƒŠ‚É�G‚ê�A + // •êŠÍ‚ªƒnƒ“ƒO�^ƒNƒ‰ƒbƒVƒ…‚·‚é�iƒpƒCƒv‚Í�Ú‘±‚ðŽó‚¯•t‚¯‚½‚Ü‚Ü�A + // IPC‚ւ̉ž“š‚¾‚¯‚ª’¾–Ù‚·‚é�j�B + const QString requestedPath = path; + QTimer::singleShot(0, this, [this, requestedPath]() { + handleOpenRequest(requestedPath); + }); + } else if (cmd == Command::Unlink) { + // Explicit CSP unlink request from a worker window's menu action. + if (m_linkedWorkerSocket == socket) { + m_linkedWorkerSocket = nullptr; + m_linkedDestPath.clear(); + sendFrame(socket, Command::LinkStatusChanged, QString()); + } } } } @@ -187,6 +287,15 @@ void InstanceManager::onServerSocketDisconnected() { it.next(); if (it.value() == socket) it.remove(); } + + // This window closing also breaks the CSP link if it was the linked one. + // ‚±‚̃EƒBƒ“ƒhƒE‚ª•‚¶‚邱‚Æ‚Å�A‚»‚ꂪCSP˜AŒg‚Ì•R‚¯�悾‚Á‚½�ê�‡‚Í + // •R‚¯‚à‰ð�œ‚³‚ê‚é�B + if (m_linkedWorkerSocket == socket) { + m_linkedWorkerSocket = nullptr; + m_linkedDestPath.clear(); + } + socket->deleteLater(); // All windows are closed: the mothership has no reason to keep running. @@ -194,6 +303,11 @@ void InstanceManager::onServerSocketDisconnected() { } void InstanceManager::handleOpenRequest(const QString& path) { + if (isCspExchangePath(path)) { + handleCspSyncRequest(path); + return; + } + QString canonical = PathUtils::canonicalizePath(path); if (!canonical.isEmpty() && m_workers.contains(canonical)) { // Already open in a worker window: ask it to reload and come forward @@ -205,6 +319,7 @@ void InstanceManager::handleOpenRequest(const QString& path) { } void InstanceManager::spawnWorker(const QString& path) { + m_spawnedAnyWorker = true; QStringList args; args << kWorkerFlag; if (!path.isEmpty()) args << path; @@ -239,13 +354,21 @@ void InstanceManager::notifyPathChanged(const QStringList& paths) { } } +void InstanceManager::requestUnlink() { + if (m_role != Role::Worker || !m_workerSocket) return; + sendFrame(m_workerSocket, Command::Unlink, QString()); +} + void InstanceManager::onWorkerSocketReadyRead() { m_workerRecvBuffer.append(m_workerSocket->readAll()); Command cmd; QString path; while (tryReadFrame(m_workerRecvBuffer, cmd, path)) { - if (cmd == Command::Reload) emit reloadAndActivateRequested(); + if (cmd == Command::Reload) + emit reloadAndActivateRequested(); + else if (cmd == Command::LinkStatusChanged) + emit cspLinkStatusChanged(!path.isEmpty()); } } @@ -269,8 +392,19 @@ bool InstanceManager::sendFrame(QLocalSocket* socket, Command cmd, frame.append(reinterpret_cast(&len), sizeof(len)); frame.append(payload); - socket->write(frame); - return socket->waitForBytesWritten(1000); + if (socket->write(frame) != frame.size()) return false; + + // write() for a small local-socket message often completes synchronously, + // leaving nothing buffered to flush. waitForBytesWritten() has nothing to + // wait for in that case and can return false immediately -- that must not + // be mistaken for a failure, which is why bytesToWrite() is checked first. + // write()‚̓��[ƒJƒ‹ƒ\ƒPƒbƒgŒü‚¯‚Ì�¬‚³‚ȃ�ƒbƒZ�[ƒW‚Å‚ ‚ê‚Γ¯Šú“I‚ÉŠ®—¹ + // ‚·‚邱‚Æ‚ª‘½‚­�A‚»‚Ì�ê�‡ƒtƒ‰ƒbƒVƒ…‚·‚ׂ«ƒf�[ƒ^‚ÍŽc‚Á‚Ä‚¢‚È‚¢�B‚»‚Ì + // �ó‘Ô‚Å‚ÍwaitForBytesWritten()‚ɂ͑҂‚ׂ«‚à‚Ì‚ª‚È‚­‘¦�À‚Éfalse‚ð + // •Ô‚µ‚¤‚邪�A‚»‚ê‚ð‘—�Mޏ”s‚ÆŒë”F‚µ‚Ă͂¢‚¯‚È‚¢‚½‚ß�A�æ‚É + // bytesToWrite()‚ðŠm”F‚·‚é�B + if (socket->bytesToWrite() == 0) return true; + return socket->waitForBytesWritten(1000) || socket->bytesToWrite() == 0; } bool InstanceManager::tryReadFrame(QByteArray& buffer, Command& cmd, @@ -290,3 +424,196 @@ bool InstanceManager::tryReadFrame(QByteArray& buffer, Command& cmd, path = QString::fromUtf8(payload.mid(1)); return true; } +//----------------------------------------------------------------------------- +// CLIP STUDIO PAINT integration +//----------------------------------------------------------------------------- + +namespace { +// Forces a just-shown top-level widget (e.g. a QMessageBox/QDialog) to the +// real foreground. The mothership has no window of its own and isn't the +// active process, so it hits the same Windows foreground-lock restriction +// worked around in MyWindow::reloadAndActivate() (mywindow.cpp): a Z-order +// change via the native HWND's topmost flag isn't subject to that +// restriction, unlike activateWindow()/SetForegroundWindow(). +// •êŠÍƒvƒ�ƒZƒX‚ɂ̓EƒBƒ“ƒhƒE‚ª‚È‚­�AƒAƒNƒeƒBƒuƒvƒ�ƒZƒX‚Å‚à‚È‚¢‚½‚ß�A +// mywindow.cpp‚ÌreloadAndActivate()‚Æ“¯‚¶WindowsƒtƒHƒAƒOƒ‰ƒEƒ“ƒhƒ�ƒbƒN‚Ì +// �§ŒÀ‚ðŽó‚¯‚é�BactivateWindow()/SetForegroundWindow‚ƈقȂè�AƒlƒCƒeƒBƒu +// HWND‚Ì�Å‘O–ʃtƒ‰ƒO‚É‚æ‚éZ�‡�˜‚Ì•Ï�X‚Í‚±‚Ì�§ŒÀ‚ðŽó‚¯‚È‚¢�B +void forceToForeground(QWidget* widget) { +#ifdef WIN32 + widget->show(); + HWND hwnd = reinterpret_cast(widget->winId()); + SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); + SetWindowPos(hwnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); +#endif +} + +// Reads src's raw bytes and writes them to dst as-is (no XDTS +// parse/re-serialize), overwriting dst if it already exists. +// src‚Ì�¶ƒoƒCƒg‚ð‚»‚Ì‚Ü‚Üdst‚Ö�‘‚«�ž‚Þ�iXDTS‚̉ð�Í�E�ăVƒŠƒAƒ‰ƒCƒY‚Í +// �s‚í‚È‚¢�j�Bdst‚ªŠù‚É‘¶�Ý‚·‚é�ê�‡‚Í�ã�‘‚«‚·‚é�B +bool copyFileOverwrite(const QString& src, const QString& dst) { + QFile in(src); + if (!in.open(QIODevice::ReadOnly)) return false; + QByteArray data = in.readAll(); + in.close(); + + QFile out(dst); + if (!out.open(QIODevice::WriteOnly | QIODevice::Truncate)) return false; + out.write(data); + return true; +} +} // namespace + +// CSP always writes its export to this same fixed path (see +// "WindowsƒfƒWƒ^ƒ‹ƒ^ƒCƒ€ƒV�[ƒg˜AŒg•û–@.pdf"). Computed once via +// QDir::tempPath(), which matches the GetTempPath() API the document +// describes. +// CSP‚Í�í‚É‚±‚̌ŒèƒpƒX‚Ö�‘‚«�o‚·�i"WindowsƒfƒWƒ^ƒ‹ƒ^ƒCƒ€ƒV�[ƒg˜AŒg•û–@.pdf" +// ŽQ�Æ�j�BƒhƒLƒ…ƒ�ƒ“ƒg‚É‹L�Ú‚ÌGetTempPath() API‚ɑΉž‚·‚éQDir::tempPath()‚Å +// ˆê“x‚¾‚¯ŒvŽZ‚·‚é�B +bool InstanceManager::isCspExchangePath(const QString& path) const { + static const QString kCspExchangePath = PathUtils::canonicalizePath( + QDir::tempPath() + "/ExchangeDigitalTimeSheet/animation.xdts"); + return !path.isEmpty() && + PathUtils::canonicalizePath(path) == kCspExchangePath; +} + +bool InstanceManager::getXdtsMetadata(const QString& path, int& layerCount, + int& duration) const { + XdtsData data; + if (!loadXdtsScene(&data, path)) return false; + if (data.isEmpty() || data.timeTable().isEmpty()) return false; + layerCount = data.timeTable().getCellHeader().getLayerNames().size(); + duration = data.timeTable().getDuration(); + return true; +} + +// One representative path per distinct open window (a paired genga/douga +// window contributes two entries to m_workers for the same socket). +// ŠJ‚¢‚Ä‚¢‚éƒEƒBƒ“ƒhƒE1‚‚ɂ‚«‘ã•\ƒpƒX‚ð1‚•Ԃ·�iŒ´‰æ�^“®‰æƒyƒA‚Ì +// ƒEƒBƒ“ƒhƒE‚Í“¯‚¶ƒ\ƒPƒbƒg‚ɑ΂µm_workers‚É2ƒGƒ“ƒgƒŠ‚ðŽ�‚‚½‚ß�j�B +QStringList InstanceManager::currentlyOpenDestPaths() const { + QStringList result; + QSet seen; + for (auto it = m_workers.constBegin(); it != m_workers.constEnd(); ++it) { + if (seen.contains(it.value())) continue; + seen.insert(it.value()); + result << it.key(); + } + return result; +} + +void InstanceManager::handleCspSyncRequest(const QString& animationXdtsPath) { + int newLayerCount, newDuration; + if (!getXdtsMetadata(animationXdtsPath, newLayerCount, newDuration)) { + QMessageBox box(QMessageBox::Critical, tr("CSP Sync"), + tr("Failed to read the Clip Studio Paint export (%1).") + .arg(animationXdtsPath), + QMessageBox::Ok); + forceToForeground(&box); + box.exec(); + return; + } + + bool hasActiveLink = (m_linkedWorkerSocket != nullptr); + + if (hasActiveLink) { + int oldLayerCount, oldDuration; + bool ok = getXdtsMetadata(m_linkedDestPath, oldLayerCount, oldDuration); + // ƒŒƒCƒ„�[�”‚ª2ˆÈ�ã�A‚Ü‚½‚Íduration‚ª6ƒtƒŒ�[ƒ€ˆÈ�ãˆÙ‚È‚é�ê�‡‚Í + // •Ê‚ÌCSPƒvƒ�ƒWƒFƒNƒg‚̉”\�«‚ª‚ ‚邯‚݂Ȃµ�AŠm”F‚ð‹²‚Þ�B + bool differsSignificantly = !ok || + qAbs(newLayerCount - oldLayerCount) >= 2 || + qAbs(newDuration - oldDuration) >= 6; + + if (!differsSignificantly) { + // Quiet auto-update: content looks like the same clip, no dialog. + performCspSync(m_linkedDestPath, animationXdtsPath); + return; + } + + QMessageBox box(QMessageBox::Question, tr("CSP Sync"), + tr("The Clip Studio Paint export looks very different " + "from the currently linked file (%1). Continue " + "updating it, or choose a different file?") + .arg(QFileInfo(m_linkedDestPath).fileName())); + QPushButton* continueBtn = + box.addButton(tr("Continue"), QMessageBox::AcceptRole); + QPushButton* differentBtn = + box.addButton(tr("Choose Different File"), QMessageBox::ActionRole); + box.addButton(QMessageBox::Cancel); + forceToForeground(&box); + box.exec(); + + if (box.clickedButton() == continueBtn) { + performCspSync(m_linkedDestPath, animationXdtsPath); + return; + } + if (box.clickedButton() != differentBtn) return; // Cancel: do nothing + if (m_linkedWorkerSocket) { + sendFrame(m_linkedWorkerSocket, Command::LinkStatusChanged, QString()); + } + m_linkedWorkerSocket = nullptr; + m_linkedDestPath.clear(); + // Fall through to the picker below. + } + + CspLinkChooserDialog dlg(currentlyOpenDestPaths()); + forceToForeground(&dlg); + if (dlg.exec() != QDialog::Accepted) return; + + QString destPath = dlg.resultPath(); + if (destPath.isEmpty()) return; + + if (dlg.needsOverwriteConfirm()) { + QMessageBox confirm( + QMessageBox::Question, tr("CSP Sync"), + tr("This will overwrite %1 with the Clip Studio Paint export. " + "Continue?") + .arg(QFileInfo(destPath).fileName()), + QMessageBox::Yes | QMessageBox::No); + forceToForeground(&confirm); + if (confirm.exec() != QMessageBox::Yes) return; + } + + performCspSync(destPath, animationXdtsPath); +} + +void InstanceManager::performCspSync(QString destPath, + const QString& animationXdtsPath) { + if (!copyFileOverwrite(animationXdtsPath, destPath)) { + QMessageBox box(QMessageBox::Critical, tr("CSP Sync"), + tr("Failed to write %1.").arg(destPath), QMessageBox::Ok); + forceToForeground(&box); + box.exec(); + return; + } + + QLocalSocket* previousLinkedSocket = m_linkedWorkerSocket; + m_linkedDestPath = PathUtils::canonicalizePath(destPath); + QLocalSocket* targetSocket = m_workers.value(m_linkedDestPath, nullptr); + + // Tell the previously linked window (if it's a different one, and still + // open) that it's no longer linked. + // ˆÈ‘O•R‚¢‚Ä‚¢‚½ƒEƒBƒ“ƒhƒE�idestPath‚ƈقȂè�A‚Ü‚¾ŠJ‚¢‚Ä‚¢‚é�ê�‡�j‚Ö�A + // •R‚¯‚ª‰ð�œ‚³‚ꂽ‚±‚Æ‚ð’Ê’m‚·‚é�B + if (previousLinkedSocket && previousLinkedSocket != targetSocket) { + sendFrame(previousLinkedSocket, Command::LinkStatusChanged, QString()); + } + + if (targetSocket) { + m_linkedWorkerSocket = targetSocket; + sendFrame(targetSocket, Command::Reload, QString()); + sendFrame(targetSocket, Command::LinkStatusChanged, QStringLiteral("1")); + } else { + // The new worker isn't registered yet; the Command::Register handler + // in onServerSocketReadyRead() will notify it (and set + // m_linkedWorkerSocket) once it connects. + // �V‚µ‚¢ƒ��[ƒJ�[‚͂܂¾–¢“o˜^‚Ì‚½‚ß�AonServerSocketReadyRead()“à‚Ì + // Command::Registerƒnƒ“ƒhƒ‰‚ª�A�Ú‘±Žž‚ɉü‚߂Ēʒm‚·‚é + // �im_linkedWorkerSocket‚à‚»‚±‚Å�ݒ肳‚ê‚é�j�B + m_linkedWorkerSocket = nullptr; + spawnWorker(destPath); + } +} diff --git a/sources/instancemanager.h b/sources/instancemanager.h index c20a016..ed0e781 100644 --- a/sources/instancemanager.h +++ b/sources/instancemanager.h @@ -57,6 +57,14 @@ class InstanceManager : public QObject { // ‚©‚çŒÄ‚Ñ�o‚·�B void notifyPathChanged(const QStringList& paths); + // Clears the CSP link if it currently points at this worker window. + // No-op unless this process's role is Worker. Non-destructive (only + // affects future CSP syncs), so callers need not confirm before calling. + // ‚±‚̃��[ƒJ�[ƒEƒBƒ“ƒhƒE‚ªCSP˜AŒg‚Ì•R‚¯�æ‚Å‚ ‚é�ê�‡�A‚»‚Ì•R‚¯‚ð + // ‰ð�œ‚·‚é�B–ðŠ„‚ªWorkerˆÈŠO‚Ì�ê�‡‚͉½‚à‚µ‚È‚¢�B”ñ”j‰ó“I‚È‘€�ì + // �iˆÈŒã‚ÌCSP“¯Šú‚ɂ̂݉e‹¿�j‚Ȃ̂Å�AŒÄ‚Ñ�o‚µ‘O‚ÌŠm”F‚Í•s—v�B + void requestUnlink(); + signals: // Worker role only: emitted when the mothership asks this window to // reload its XDTS data and come to the foreground, because another @@ -66,6 +74,14 @@ class InstanceManager : public QObject { // Žwަ‚³‚ꂽ�ۂɃVƒOƒiƒ‹‚³‚ê‚é�B void reloadAndActivateRequested(); + // Worker role only: emitted when the mothership reports that this + // window's CSP link status changed (established, moved to another + // window, or explicitly/implicitly unlinked). + // Worker‘¤‚ł̂ݔ­‰Î‚·‚é�B‚±‚̃EƒBƒ“ƒhƒE‚ÌCSP˜AŒg‚Ì•R‚¯�ó‘Ô‚ª•ω» + // ‚µ‚½�i�¬—§�E‘¼ƒEƒBƒ“ƒhƒE‚ւ̈ړ®�E‰ð�œ‚Ì‚¢‚¸‚ê‚©�j‚±‚Æ‚ð•êŠÍ‚©‚ç + // ’Ê’m‚³‚ꂽ�ۂɃVƒOƒiƒ‹‚³‚ê‚é�B + void cspLinkStatusChanged(bool linked); + private slots: void onNewConnection(); // mothership: a new incoming connection void onServerSocketReadyRead(); // mothership: data from a worker/forwarder @@ -79,7 +95,9 @@ private slots: Register = 0, Open = 1, Reload = 2, - ClearRegistration = 3 + ClearRegistration = 3, + Unlink = 4, + LinkStatusChanged = 5 }; bool tryAcquireLock(); @@ -90,6 +108,26 @@ private slots: static bool tryReadFrame(QByteArray& buffer, Command& cmd, QString& path); void spawnWorker(const QString& path); + // CLIP STUDIO PAINT integration: CSP always writes its export to the + // same fixed path and launches XDTS Viewer with that path as argv[1]. + // This is treated as a request to sync a real XDTS file (m_linkedDestPath) + // rather than a request to open that fixed path directly. + // CLIP STUDIO PAINT˜AŒg: CSP‚Í�í‚É“¯ˆê‚̌ŒèƒpƒX‚Ö�‘‚«�o‚µ�A‚»‚̃pƒX‚ð + // ˆø�”‚ÉXDTS Viewer‚ð‹N“®‚·‚é�B‚±‚ê‚ðŒÅ’èƒpƒXŽ©‘Ì‚ðŠJ‚­—v‹�‚Æ‚µ‚Ăł͂Ȃ­�A + // ŽÀ‘̂̂ ‚éXDTSƒtƒ@ƒCƒ‹�im_linkedDestPath�j‚𓯊ú‚·‚é—v‹�‚Æ‚µ‚Ĉµ‚¤�B + bool isCspExchangePath(const QString& path) const; + void handleCspSyncRequest(const QString& animationXdtsPath); + // destPath is taken by value (not const&): callers sometimes pass + // m_linkedDestPath itself, and this function reassigns m_linkedDestPath + // partway through, so a reference parameter would alias it. + // destPath‚Í’l“n‚µ�iconstŽQ�Ƃł͂Ȃ¢�j: ŒÄ‚Ñ�o‚µŒ³‚ªm_linkedDestPath + // Ž©�g‚ð“n‚·‚±‚Æ‚ª‚ ‚è�A–{ŠÖ�”‚Ì“r’†‚Åm_linkedDestPath‚ð�Ä‘ã“ü‚·‚é + // ‚½‚ß�AŽQ�Æ“n‚µ‚¾‚ƃGƒCƒŠƒAƒVƒ“ƒO‚ª”­�¶‚µ‚Ä‚µ‚Ü‚¤�B + void performCspSync(QString destPath, const QString& animationXdtsPath); + bool getXdtsMetadata(const QString& path, int& layerCount, + int& duration) const; + QStringList currentlyOpenDestPaths() const; + Role m_role = Role::Standalone; QSharedMemory* m_lock = nullptr; QLocalServer* m_server = nullptr; @@ -103,6 +141,28 @@ private slots: // Worker-side receive buffer for messages pushed by the mothership. QByteArray m_workerRecvBuffer; + + // CLIP STUDIO PAINT integration: canonical path of the XDTS file + // currently linked to CSP's export, and the worker socket that owns it + // (nullptr if unlinked). m_linkedWorkerSocket is the authoritative + // "is there an active link" signal (a plain pointer check, kept in sync + // eagerly on link/unlink/disconnect) -- m_linkedDestPath is only for + // display and metadata comparison. + // CLIP STUDIO PAINT˜AŒg: CSP‚Ì�‘‚«�o‚µ‚ÉŒ»�Ý•R‚¢‚Ä‚¢‚éXDTSƒtƒ@ƒCƒ‹‚Ì + // �³‹K‰»ƒpƒX‚Æ�A‚»‚ê‚ð�Š—L‚·‚éƒ��[ƒJ�[‚̃\ƒPƒbƒg�i–¢•R‚¯‚È‚çnullptr�j�B + // �uŒ»�Ý•R‚¯’†‚©�v‚Ì”»’è‚Ím_linkedWorkerSocket‚Ì’P�ƒ‚ȃ|ƒCƒ“ƒ^”»’è‚ð + // �³‚Æ‚µ�i•R‚¯�^‰ð�œ�^�Ø’f‚Ì‚½‚тɑ¦Žž�X�V‚·‚é�j�Am_linkedDestPath‚Í + // •\ަ‚ƃ�ƒ^ƒf�[ƒ^”äŠr‚Ì‚½‚ß‚¾‚¯‚ÉŽg‚¤�B + QString m_linkedDestPath; + QLocalSocket* m_linkedWorkerSocket = nullptr; + + // Whether this mothership has ever spawned a worker process. Used only + // by startMothership() to detect the "initial request ended with nothing + // to manage" case (see the comment there). + // ‚±‚Ì•êŠÍ‚ªƒ��[ƒJ�[ƒvƒ�ƒZƒX‚ðˆê“x‚Å‚à‹N“®‚µ‚½‚©‚Ç‚¤‚©�BstartMothership() + // ‚Å�u�Å�‰‚Ì—v‹�‚ªŠÇ—�‘Î�Û‚È‚µ‚Å�I‚í‚Á‚½�v�ê�‡‚ÌŒŸ�o‚݂̂Ɏg‚¤ + // �i�Ú�ׂ͂»‚¿‚ç‚̃Rƒ�ƒ“ƒg‚ðŽQ�Æ�j�B + bool m_spawnedAnyWorker = false; }; #endif diff --git a/sources/loc/XDTS_Viewer_ja.qm b/sources/loc/XDTS_Viewer_ja.qm index 8e16098..9dda1b6 100644 Binary files a/sources/loc/XDTS_Viewer_ja.qm and b/sources/loc/XDTS_Viewer_ja.qm differ diff --git a/sources/main.cpp b/sources/main.cpp index 67e76bd..3808b98 100644 --- a/sources/main.cpp +++ b/sources/main.cpp @@ -27,6 +27,19 @@ int main(int argc, char* argv[]) { a.setAttribute(Qt::AA_CompressHighFrequencyEvents); a.setAttribute(Qt::AA_CompressTabletEvents); + MyParams::instance()->initialize(); + + QTranslator tra; + QString lang = MyParams::instance()->language(); + if (lang != "en") { + QString qmFileName = QString("XDTS_Viewer_%1").arg(lang); + if (QDir(PathUtils::getTranslationFolderPath()) + .exists(qmFileName + ".qm")) { + tra.load(qmFileName, PathUtils::getTranslationFolderPath()); + a.installTranslator(&tra); + } + } + // Single-instance / multi-window coordination. Use QApplication::arguments() // (not raw argv) so non-ASCII paths survive on Windows. A Forwarded process // has already handed its request to the running mothership and must exit @@ -43,18 +56,6 @@ int main(int argc, char* argv[]) { if (role == InstanceManager::Role::Forwarded) return 0; if (role == InstanceManager::Role::Mothership) return a.exec(); - MyParams::instance()->initialize(); - - QTranslator tra; - QString lang = MyParams::instance()->language(); - if (lang != "en") { - QString qmFileName = QString("XDTS_Viewer_%1").arg(lang); - if (QDir(PathUtils::getTranslationFolderPath()) - .exists(qmFileName + ".qm")) { - tra.load(qmFileName, PathUtils::getTranslationFolderPath()); - a.installTranslator(&tra); - } - } MyParams::instance()->initStamps(); #ifndef __MACOS__ @@ -73,6 +74,9 @@ int main(int argc, char* argv[]) { QObject::connect(InstanceManager::instance(), &InstanceManager::reloadAndActivateRequested, &w, &MyWindow::reloadAndActivate); + QObject::connect(InstanceManager::instance(), + &InstanceManager::cspLinkStatusChanged, &w, + &MyWindow::onCspLinkStatusChanged); } QRect geometry = MyParams::instance()->loadWindowGeometry(); diff --git a/sources/myparams.cpp b/sources/myparams.cpp index a910503..392e2ca 100644 --- a/sources/myparams.cpp +++ b/sources/myparams.cpp @@ -842,9 +842,6 @@ void MyParams::setCurrentXdtsPath(const QString& path) { MyParams* p = MyParams::instance(); m_currentXdtsPaths.clear(); QString fileName = QFileInfo(path).fileName(); - std::cout << "fileName = " << fileName.toStdString() << std::endl; - std::cout << "p->suffix(Genga) = " << p->suffix(Genga).toStdString() - << std::endl; if (fileName.contains(p->suffix(Genga))) { m_currentXdtsPaths.insert(Area_Actions, path); fileName = fileName.replace(p->suffix(Genga), p->suffix(Douga), diff --git a/sources/mywindow.cpp b/sources/mywindow.cpp index efbb9c6..bf5c59e 100644 --- a/sources/mywindow.cpp +++ b/sources/mywindow.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -67,7 +68,10 @@ class MyToolButtonProxyStyle : public QProxyStyle { //----------------------------- MyWindow::MyWindow() - : m_data(nullptr), m_settingsDialog(nullptr), m_preferencesDialog(nullptr) { + : m_data(nullptr) + , m_settingsDialog(nullptr) + , m_preferencesDialog(nullptr) + , m_isCspLinked(false) { setAcceptDrops(true); m_previewPane = new XsheetPdfPreviewPane(this); @@ -341,6 +345,9 @@ MyWindow::MyWindow() QMenu* menu = new QMenu(this); QAction* prefAct = menu->addAction(tr("Preferences")); QAction* aboutAct = menu->addAction(tr("About")); + m_unlinkCspAct = menu->addAction(tr("Unlink from CSP")); + m_unlinkCspAct->setVisible( + false); // shown only while this window is CSP-linked QToolButton* menuBtn = qobject_cast(toolBar->widgetForAction(menuAct)); menuBtn->setMenu(menu); @@ -413,6 +420,7 @@ MyWindow::MyWindow() connect(settingsAct, SIGNAL(triggered()), m_settingsDialog, SLOT(show())); connect(prefAct, SIGNAL(triggered()), m_preferencesDialog, SLOT(show())); connect(aboutAct, SIGNAL(triggered()), this, SLOT(onAbout())); + connect(m_unlinkCspAct, SIGNAL(triggered()), this, SLOT(onUnlinkCsp())); connect(currentColorGroup, SIGNAL(triggered(QAction*)), this, SLOT(onCurrentColorActionTriggered(QAction*))); @@ -978,6 +986,7 @@ void MyWindow::updateTitleBar() { else str = QFileInfo(MyParams::instance()->currentXdtsPath()).fileName(); if (MyParams::instance()->somethingIsDirty()) str += " *"; + if (m_isCspLinked) str += tr(" [Linked with Clip Studio Paint]"); str += " - " + qApp->applicationName(); setWindowTitle(str); @@ -1246,7 +1255,22 @@ void MyWindow::reloadXdtsDataOnly() { QString path = MyParams::instance()->currentXdtsPath(areaId); + // A CSP sync notification can arrive an instant after the mothership + // process finishes writing this same file; on Windows, briefly + // reopening a just-written file can transiently fail (e.g. real-time + // antivirus scanning triggered by the write) even though the write + // itself is already complete. A short retry absorbs that window + // instead of surfacing a spurious error to the user. + // CSP“¯Šú‚Ì’Ê’m‚Í�A•êŠÍƒvƒ�ƒZƒX‚ª“¯‚¶ƒtƒ@ƒCƒ‹‚Ö‚Ì�‘‚«�ž‚Ý‚ð�I‚¦‚½ + // ’¼Œã‚ɓ͂­‚±‚Æ‚ª‚ ‚é�BWindows‚Å‚Í�A�‘‚«�ž‚ÝŽ©‘̂͊®—¹‚µ‚Ä‚¢‚Ä‚à�A + // ’¼Œã‚Ì�ăI�[ƒvƒ“‚ªˆê‰ß�«‚ÉŽ¸”s‚·‚邱‚Æ‚ª‚ ‚é�i—á:�‘‚«�ž‚Ý‚ðŒ_‹@‚Æ + // ‚µ‚½ƒEƒCƒ‹ƒX‘Î�ô‚ÌƒŠƒAƒ‹ƒ^ƒCƒ€ƒXƒLƒƒƒ“�j�B‚±‚Ì—P—\‚ðƒŠƒgƒ‰ƒC‚Å + // ‹zŽû‚µ�Aƒ†�[ƒU�[‚É‹U—z�«‚̃Gƒ‰�[‚ðŒ©‚¹‚È‚¢‚悤‚É‚·‚é�B bool ok = loadXdtsScene(m_data, path); + for (int retry = 0; !ok && retry < 5; ++retry) { + QThread::msleep(100); + ok = loadXdtsScene(m_data, path); + } if (!ok) { QMessageBox::critical(this, tr("Error"), tr("%1 is not a valid XDTS file.").arg(path)); @@ -1308,4 +1332,16 @@ void MyWindow::reloadAndActivate() { #endif raise(); activateWindow(); +} + +// Non-destructive: only affects future CSP syncs, so no confirmation is +// asked here (see InstanceManager::requestUnlink()). +// ”ñ”j‰ó“I‚È‘€�ì�iˆÈŒã‚ÌCSP“¯Šú‚ɂ̂݉e‹¿�j‚Ì‚½‚ß�A‚±‚±‚ł͊m”F‚ð +// �s‚í‚È‚¢�iInstanceManager::requestUnlink()‚ðŽQ�Æ�j�B +void MyWindow::onUnlinkCsp() { InstanceManager::instance()->requestUnlink(); } + +void MyWindow::onCspLinkStatusChanged(bool linked) { + m_isCspLinked = linked; + m_unlinkCspAct->setVisible(linked); + updateTitleBar(); } \ No newline at end of file diff --git a/sources/mywindow.h b/sources/mywindow.h index 404a76e..33020d5 100644 --- a/sources/mywindow.h +++ b/sources/mywindow.h @@ -43,6 +43,11 @@ class MyWindow : public QMainWindow { QAction* m_currentColorAction; + // CLIP STUDIO PAINT integration UI state. + // CLIP STUDIO PAINT˜AŒg—p‚ÌUI�ó‘Ô�B + QAction* m_unlinkCspAct; + bool m_isCspLinked; + // Re-reads the XDTS timing data only, leaving hand-drawn overlays / undo // history / format settings untouched. Used by reloadAndActivate(). // Žè•`‚«ƒI�[ƒo�[ƒŒƒC�EUndo—š—ð�EƒtƒH�[ƒ}ƒbƒg�Ý’è‚Í‚»‚̂܂܂É�AXDTS‚Ì @@ -65,6 +70,12 @@ class MyWindow : public QMainWindow { // ƒf�[ƒ^‚͕ێ�‚µ‚½‚Ü‚Ü�j�AƒEƒBƒ“ƒhƒE‚ð‘O–ʉ»‚·‚é�B void reloadAndActivate(); + // Updates the title bar / Unlink menu visibility to reflect the current + // CSP link status (InstanceManager::cspLinkStatusChanged()). + // Œ»�Ý‚ÌCSP˜AŒg‚Ì•R‚¯�ó‘Ô‚ð�Aƒ^ƒCƒgƒ‹ƒo�[‚ÆUnlinkƒ�ƒjƒ…�[‚Ì•\ަ‚É + // ”½‰f‚·‚é�iInstanceManager::cspLinkStatusChanged()�j�B + void onCspLinkStatusChanged(bool linked); + protected: void closeEvent(QCloseEvent* event) override; @@ -105,6 +116,12 @@ protected slots: void onDelete(); void onBacksideImgPathChanged(); void onAbout(); + + // Explicitly unlinks this window from a Clip Studio Paint sync, if it + // is currently the linked window (see InstanceManager::requestUnlink()). + // ‚±‚̃EƒBƒ“ƒhƒE‚ªClip Studio Paint˜AŒg‚Ì•R‚¯�æ‚Å‚ ‚é�ê�‡�A–¾Ž¦“I‚É + // •R‚¯‚ð‰ð�œ‚·‚é�iInstanceManager::requestUnlink()‚ðŽQ�Æ�j�B + void onUnlinkCsp(); }; #endif \ No newline at end of file diff --git a/sources/pathutils.cpp b/sources/pathutils.cpp index 7519a85..974d9f4 100644 --- a/sources/pathutils.cpp +++ b/sources/pathutils.cpp @@ -37,16 +37,22 @@ QString getLicenseFolderPath() { return getResourceDirPath() + "/LICENSE"; } QString getTranslationFolderPath() { return getResourceDirPath() + "/loc"; } -// Falls back to a cleaned absolute path when the file doesn't exist yet, and -// folds case on Windows (NTFS is case-insensitive). -// ƒtƒ@ƒCƒ‹‚ª‚Ü‚¾‘¶�Ý‚µ‚È‚¢�ê�‡‚Í�â‘΃pƒX‚ðƒNƒŠ�[ƒ“ƒAƒbƒv‚µ‚½’l‚ɃtƒH�[ƒ‹ƒoƒbƒN‚µ�A -// Windows ‚Å‚Í‘å•¶Žš�¬•¶Žš‚𖳎‹‚·‚é�iNTFS‚Í‘å•¶Žš�¬•¶Žš‚ð‹æ•Ê‚µ‚È‚¢‚½‚ß�j�B +// Always a pure string normalization (no OS round-trip via +// QFileInfo::canonicalFilePath()), so two processes calling this for the +// same path get identical results regardless of timing or whether the file +// exists yet in either process (this matters for InstanceManager, where the +// mothership and a worker process each canonicalize the same path +// independently). Folds case on Windows (NTFS is case-insensitive). +// QFileInfo::canonicalFilePath()‚É‚æ‚éOS‚Ö‚Ì–â‚¢�‡‚킹‚Í�s‚í‚È‚¢�A�ƒ�ˆ‚È•¶Žš—ñ +// �³‹K‰»‚݂̂ð�s‚¤�B‚±‚ê‚É‚æ‚è�A•Ê�X‚̃vƒ�ƒZƒX‚ª“¯‚¶ƒpƒX‚ɑ΂µ‚Ä‚±‚ÌŠÖ�”‚ð +// ŒÄ‚ñ‚Å‚à�Aƒ^ƒCƒ~ƒ“ƒO‚âƒtƒ@ƒCƒ‹‚Ì‘¶�Ý—L–³‚ÉŠÖ‚í‚炸“¯ˆê‚ÌŒ‹‰Ê‚ª“¾‚ç‚ê‚é +// �iInstanceManager‚Å•êŠÍ‚ƃ��[ƒJ�[‚ª“¯‚¶ƒpƒX‚ð‚»‚ê‚¼‚ê“Æ—§‚É�³‹K‰»‚·‚é�Û‚É +// �d—v�j�BWindows ‚Å‚Í‘å•¶Žš�¬•¶Žš‚𖳎‹‚·‚é�iNTFS‚Í‘å•¶Žš�¬•¶Žš‚ð‹æ•Ê‚µ +// ‚È‚¢‚½‚ß�j�B QString canonicalizePath(const QString& path) { if (path.isEmpty()) return QString(); - QString canonical = QFileInfo(path).canonicalFilePath(); - if (canonical.isEmpty()) - canonical = QDir::cleanPath(QFileInfo(path).absoluteFilePath()); + QString canonical = QDir::cleanPath(QFileInfo(path).absoluteFilePath()); #ifndef __MACOS__ canonical = canonical.toLower(); diff --git a/sources/translations/XDTS_Viewer_ja.ts b/sources/translations/XDTS_Viewer_ja.ts index 1ec2273..23b5dbe 100644 --- a/sources/translations/XDTS_Viewer_ja.ts +++ b/sources/translations/XDTS_Viewer_ja.ts @@ -4,31 +4,31 @@ AETool - + The following frames were not copied because they contain non-numeric strings. - %1 以下ã�®ãƒ•レームã�«ã�¯æ•°å€¤ä»¥å¤–ã�®æ–‡å­—列ã�Œå�«ã�¾ã‚Œã�¦ã�„ã�Ÿã�Ÿã‚�ã€�コピーã�•れã�¾ã�›ã‚“ã�§ã�—ã�Ÿã€‚ - %1 - + There is a cell with the same number as the one reserved for empty frames. 空コマã�®ã�Ÿã‚�ã�«äºˆç´„ã�•れã�¦ã�„る番å�·ã�¨å�Œã�˜ç•ªå�·ã�Œå…¥ã�£ã�Ÿã‚³ãƒžã�Œã�‚りã�¾ã�™ã€‚ - - - + + + Copy After Effects Keyframe Data To Clipboard AEタイムリマップデータをクリップボードã�«ã‚³ãƒ”ー - + This tool requires XDTS to be displayed in the Cell area. ã�“ã�®ãƒ„ールを使用ã�™ã‚‹ã�Ÿã‚�ã�«ã�¯ã€�セル(動画)欄ã�«XDTSを表示ã�™ã‚‹å¿…è¦�ã�Œã�‚りã�¾ã�™ã€‚ - + There are mix-up columns in the Cell area. Please turn off the "Mix-up Columns" checkbox in the Settings in order to use this tool. セル(動画)欄ã�«ãƒ†ãƒ¬ã‚³åˆ—ã�Œã�‚りã�¾ã�™ã€‚ @@ -38,21 +38,105 @@ Please turn off the "Mix-up Columns" checkbox in the Settings in order AboutDialog - + About %1 %1 ã�«ã�¤ã�„ã�¦ - + Built %1 ビルドã�—ã�Ÿæ—¥ %1 - + Licenses ライセンス表示 + + CspLinkChooserDialog + + + Sync Clip Studio Paint Timesheet + Clip Studio Paintã�®ã‚¿ã‚¤ãƒ ã‚·ãƒ¼ãƒˆæƒ…å ±ã�®èª­ã�¿è¾¼ã�¿ã�¨å�ŒæœŸ + + + + Choose how to sync the Clip Studio Paint export with XDTS Viewer. + Clip Studio Paintã�‹ã‚‰å‡ºåŠ›ã�•れã�Ÿã‚¿ã‚¤ãƒ ã‚·ãƒ¼ãƒˆæƒ…報をã€�ã�©ã�®ãƒ•ァイルã�«é�©ç”¨ã�™ã‚‹ã�‹é�¸æŠžã�—ã�¦ã��ã� ã�•ã�„。 + + + + Sync with an already-open file: + æ—¢ã�«é–‹ã�„ã�¦ã�„るファイルã�«ä¸Šæ›¸ã��ã�—ã�¦é�©ç”¨ã�™ã‚‹ï¼š + + + + Sync with an existing file, or export as a new file: + 既存ã�®ãƒ•ァイルをé�¸æŠžã�—ã�¦é�©ç”¨ã�™ã‚‹ã�‹ã€�æ–°è¦�XDTSファイルã�¨ã�—ã�¦ã‚¨ã‚¯ã‚¹ãƒ�ートã�™ã‚‹ï¼š + + + + OK + OK + + + + Cancel + キャンセル + + + + Select or Enter XDTS File + XDTSファイルをé�¸æŠžã�™ã‚‹ã�‹ã€�ファイルå��を入力ã�—ã�¦ã��ã� ã�•ã�„ + + + + XDTS files (*.xdts) + XDTSファイル (*.xdts) + + + + InstanceManager + + + + + + CSP Sync + CSPã�¨ã�®å�ŒæœŸ + + + + Failed to read the Clip Studio Paint export (%1). + Clip Studio Paintã�‹ã‚‰å‡ºåŠ›ã�•れã�ŸXDTSファイルを読ã�¿è¾¼ã‚�ã�¾ã�›ã‚“ã�§ã�—ã�Ÿï¼ˆ%1)。 + + + + The Clip Studio Paint export looks very different from the currently linked file (%1). Continue updating it, or choose a different file? + Clip Studio Paintã�‹ã‚‰å‡ºåŠ›ã�•れã�Ÿã‚¿ã‚¤ãƒ ã‚·ãƒ¼ãƒˆæƒ…å ±ã�Œã€�ç�¾åœ¨ãƒªãƒ³ã‚¯ã�—ã�¦ã�„るファイル(%1)ã�¨å¤§ã��ã��ç•°ã�ªã�£ã�¦ã�„ã�¾ã�™ã€‚å�ŒæœŸã�—ã�¦æ›´æ–°ã‚’ç¶šã�‘ã�¾ã�™ã�‹ã€�ã��れã�¨ã‚‚リンクã�™ã‚‹ãƒ•ァイルを変更ã�—ã�¾ã�™ã�‹ï¼Ÿ + + + + Continue + å�ŒæœŸã‚’継続ã�™ã‚‹ + + + + Choose Different File + 別ã�®ãƒ•ァイルã�§ãƒªãƒ³ã‚¯ã‚’å†�設定ã�™ã‚‹ + + + + This will overwrite %1 with the Clip Studio Paint export. Continue? + Clip Studio Paintã�‹ã‚‰å‡ºåŠ›ã�•れã�ŸXDTSデータã�§ã€�%1を上書ã��ã�—ã�¾ã�™ã€‚ç¶šã�‘ã�¾ã�™ã�‹ï¼Ÿ + + + + Failed to write %1. + %1ã�®æ›¸ã��è¾¼ã�¿ã�«å¤±æ•—ã�—ã�¾ã�—ã�Ÿã€‚ + + MixupKeyDialog @@ -114,51 +198,51 @@ Please turn off the "Mix-up Columns" checkbox in the Settings in order MyParams - + Circle 丸 - + Triangle 三角 - + Approve 承èª�å�° - + Save Empty XDTS File 空ã�®XDTSファイルをä¿�å­˜ã�™ã‚‹ - + XDTS Files (*.xdts) XDTSファイル (*.xdts) - + The file %1 already exists. ファイル%1ã�¯æ—¢ã�«å­˜åœ¨ã�—ã�¾ã�™ã€‚ - + The folder %1 already exists. フォルダ%1ã�¯æ—¢ã�«å­˜åœ¨ã�—ã�¾ã�™ã€‚ - + Do you want to overwrite it? 上書ã��ã�—ã�¦ã‚ˆã‚�ã�—ã�„ã�§ã�™ã�‹ï¼Ÿ - + Question 質å•� @@ -166,262 +250,274 @@ Please turn off the "Mix-up Columns" checkbox in the Settings in order MyWindow - + < Prev < å‰�ページ - + Next > 次ページ > - + Load (Ctrl+O) é–‹ã�� (Ctrl+O) - + Save (Ctrl+S) ä¿�å­˜ (Ctrl+S) - - + + Export Image (Ctrl+E) シート画åƒ�を書ã��出ã�— (Ctrl+E) - + Undo (Ctrl+Z) å…ƒã�«æˆ»ã�™ (Ctrl+Z) - + Redo (Ctrl+Y) やり直ã�™ (Ctrl+Y) - + Zoom In (Ctrl+Plus) ズームイン (Ctrl+Plus) - + Zoom Out (Ctrl+Minus) ズームアウト (Ctrl+Minus) - + Current Color æ��画色 - + Brush (B) ペン (B) - + Large 大サイズ - + Small å°�サイズ - + Eraser (E) 消ã�—ゴム (E) - + Selection (M) é�¸æŠž (M) - + Rectangle 四角形 - + Freehand ã�ªã�’ã�ªã‚� - + Line (U) ç·š (U) - + Thin Line ç´°ã�„ç·š - + Thick Line 太ã�„ç·š - + Thin Arrow ç´°ã�„矢å�° - + Thick Arrow 太ã�„矢å�° - + Double Line 二é‡�ç·š - + Dotted Line 点線 - + Stamp (S) スタンプ (S) - + Dye Numbers (D) 動画番å�·ã‚’ç�€è‰² (D) - + Copy After Effects Keyframe Data To Clipboard AEタイムリマップデータをクリップボードã�«ã‚³ãƒ”ー - + Settings タイムシート設定 - + Menu メニュー - + Preferences 環境設定 - + About XDTS Viewerã�«ã�¤ã�„ã�¦ - + + Unlink from CSP + CSPã�¨ã�®ãƒªãƒ³ã‚¯ã‚’解除 + + + Cut 切りå�–り - + Copy コピー - + Paste 貼り付ã�‘ - + Delete 消去 - + Previous Page å‰�ページ - + Next Page 次ページ - - + + + Error エラー - + The preset file %1 is not valid. プリセットファイル %1 ã‚’æ­£ã�—ã��読ã�¿è¾¼ã‚�ã�¾ã�›ã‚“。 - + Undo å…ƒã�«æˆ»ã�™ - + No more Undo operations available. ã�“れ以上元ã�«æˆ»ã�™æ“�作ã�Œã�‚りã�¾ã�›ã‚“。 - + Redo やり直ã�™ - + No more Redo operations available. ã�“れ以上やり直ã�™æ“�作ã�Œã�‚りã�¾ã�›ã‚“。 - + Open XDTS File XDTSファイルを開ã�� - + XDTS files (*.xdts) XDTSファイル (*.xdts) - + + %1 is not a valid XDTS file. %1 ã�¯æ­£ã�—ã��XDTSファイルã�¨ã�—ã�¦èª­ã�¿è¾¼ã‚�ã�¾ã�›ã‚“。 - + Untitled å��称未設定 - + + [Linked with Clip Studio Paint] + [Clip Studio Paint ã�¨ãƒªãƒ³ã‚¯ä¸­] + + + PNG (*.png);;PSD files (*.psd) PNG (*.png);;Photoshop (*.psd) - - - + + + Export Images シート画åƒ�を書ã��出ã�— - + PNG (*.png) PNG (*.png) - + Following file(s) are already exist. Do you want to overwrite them? 以下ã�®ãƒ•ァイルã�¯ã�™ã�§ã�«å­˜åœ¨ã�—ã�¾ã�™ã€‚上書ã��ã�—ã�¦ã‚ˆã‚�ã�—ã�„ã�§ã�™ã�‹ï¼Ÿ @@ -434,31 +530,31 @@ Do you want to overwrite them? - + OK OK - + Open containing folder フォルダを開ã�� - + Xsheet images are exported properly. シート画åƒ�ã�®æ›¸ã��出ã�—ã�Œå®Œäº†ã�—ã�¾ã�—ã�Ÿã€‚ - - + + Question 質å•� - + The document has been modified. Do you want to save your changes? ドキュメントã�¯å¤‰æ›´ã�•れã�¾ã�—ã�Ÿã€‚ä¿�å­˜ã�—ã�¾ã�™ã�‹ï¼Ÿ @@ -467,143 +563,143 @@ Do you want to save your changes? PreferencesDialog - + Preferences 環境設定 - + Put Serial Frame Numbers Over Pages ç§’æ•°ã€�フレーム番å�·ã‚’通ã�—ã�§è¡¨ç¤ºã�™ã‚‹ - + Print Level Names On The Bottom レベルå��をページ下部ã�«ã‚‚出力ã�™ã‚‹ - + Capitalize the first letter of level name (ACTIONS area only) セルå��ã�®1文字目を大文字ã�«ã�™ã‚‹ï¼ˆåŽŸç”»æ¬„ã�®ã�¿ï¼‰ - + Empty Frame for AE Keyframe Data: AEタイムリマップã�® 空コマ用ã�®ãƒ•レーム: - + OK OK - + Always 常ã�«æ��ã�� - + More Than 3 Continuous Cells 4コマ以上連続ã�—ã�Ÿã‚³ãƒžã�«æ��ã�� - + None 常ã�«æ��ã�‹ã�ªã�„ - + When using "Copy After Effects Keyframe Data To Clipboard" tool, this value will be the frame number for empty cells. 「AEタイムリマップデータをクリップボードã�«ã‚³ãƒ”ーã€�ツールを使ã�†ã�¨ã��ã€� ã�“ã�®å€¤ã�Œç©ºã‚³ãƒžã�®ãƒ•レーム番å�·ã�¨ã�—ã�¦ä½¿ç”¨ã�•れã�¾ã�™ã€‚ - + Language: 言語: - + Line Color: ç·šã�®è‰²ï¼š - + Continuous Line: 連続コマã�®ç·šï¼š - + Minimum Frame Length for "REPEAT" symbol: 「リピートã€�表示ã�® 最å°�繰り返ã�—é•·ã�•: - + User Stamps Folder: ユーザースタンプフォルダ: - + Approval Stamp Name: 承èª�å�°ã�®å��å‰�: - + File Suffixes ファイル接尾辞 - + LO: LO: - + RoughGen: ラフ原: - + Genga: 原画: - + Douga: 動画: - + Language has been changed. 言語ã�Œå¤‰æ›´ã�•れã�¾ã�—ã�Ÿã€‚ - + Changes will take effect the next time you run XDTS Viewer. 変更ã�¯æ¬¡å›žã�®XDTSViewer起動時ã�«é�©ç”¨ã�•れã�¾ã�™ã€‚ - + Specify User Stamp Folder ユーザースタンプフォルダを指定ã�—ã�¦ä¸‹ã�•ã�„ - + Stamp Folder Has Specified. スタンプフォルダã�ŒæŒ‡å®šã�•れã�¾ã�—ã�Ÿã€‚ - - + + The Change Will Be Applied When You Launch the Application Next Time. 変更ã�¯æ¬¡å›žã‚¢ãƒ—リケーション起動時ã�«é�©ç”¨ã�•れã�¾ã�™ã€‚ - + Approval Stamp Name Has Specified. 承èª�å�°ã�®å��å‰�ã�Œè¨­å®šã�•れã�¾ã�—ã�Ÿã€‚ @@ -659,50 +755,50 @@ for "REPEAT" symbol: セル - + CAMERA XSheetPDF カメラ - + HOLD 止メ - + REPEAT リピ|ト - + (Start OL %1+%2) (å‰�O.L. %1ç§’+%2コマ) - + (End OL %1+%2) (後O.L. %1ç§’+%2コマ) - + Warning 警告 - + The saved mix-up columns keyframes does not match the currently displayed columns, so the mix-up columns keyframes were automatically adjusted. You may need to verify whether the adjustment was performed correctly. ä¿�å­˜ã�•れã�¦ã�„るテレコ列キーフレームã�Œç�¾åœ¨è¡¨ç¤ºã�•れã�¦ã�„る列ã�¨ä¸€è‡´ã�—ã�ªã�„ã�Ÿã‚�ã€�テレコキーフレームã�Œè‡ªå‹•çš„ã�«èª¿æ•´ã�•れã�¾ã�—ã�Ÿã€‚ 調整ã�Œæ­£ã�—ã��行ã‚�れã�Ÿã�‹ã�©ã�†ã�‹ã€�確èª�ã�Œå¿…è¦�ã�‹ã‚‚ã�—れã�¾ã�›ã‚“。 - + %1-%2 %1~%2 - + Skipped Frames: 動画欠番 @@ -714,37 +810,37 @@ The keyframe will be cleared. キーフレームã�¯ã‚¯ãƒªã‚¢ã�•れã�¾ã�™ã€‚ - + EPISODE XSheetPDF 第  話 - + SEQ. XSheetPDF シーン - + SCENE XSheetPDF カット - + TIME XSheetPDF タイム - + NAME XSheetPDF 原画 - + SHEET XSheetPDF シート @@ -753,17 +849,17 @@ The keyframe will be cleared. SelectionTool - + Fill Selection é�¸æŠžç¯„囲を塗りã�¤ã�¶ã�™ - + Remove Matte Color 地色ã�®æ¶ˆåŽ» - + Choose Matte Color 地色をé�¸æŠžã�—ã�¦ã��ã� ã�•ã�„ @@ -771,7 +867,7 @@ The keyframe will be cleared. SettingsDialog - + Adaptively Expand Columns セル数ã�«å�ˆã‚�ã�›ã�¦åˆ—を増やã�™ @@ -780,7 +876,7 @@ The keyframe will be cleared. テレコ表示 - + Attach Composite Voucher シートè£�é�¢ã�«å�ˆæˆ�ä¼�票を表示ã�™ã‚‹ @@ -789,82 +885,82 @@ The keyframe will be cleared. 紙原画シートを使用 (原画欄ã�®ç½«ç·šã‚’éš ã�™ï¼‰ - + OK OK - + ACTIONS アクション - + CELLS セル - + Settings タイムシート設定 - + Edit Mix-up Keys キーフレームã�®ç·¨é›† - + Show Skipped Drawings Information 動画欠番情報を表示ã�™ã‚‹ - + Scanned Genga Sheet Mode ( Hide Genga && Camera Area Lines ) 紙原画シートを使用 (原画欄ã�¨ã‚«ãƒ¡ãƒ©æ¬„ã�®ç½«ç·šã‚’éš ã�™ï¼‰ - + Manually Specify Mix-up Keys テレコ列キーフレームを手動ã�§æŒ‡å®šã�™ã‚‹ - + Automatically Detect from Data XDTSデータã�‹ã‚‰ãƒ†ãƒ¬ã‚³åˆ—を自動ã�§åˆ¤åˆ¥ã�™ã‚‹ - + Template: テンプレート: - + Mix-up Columns: テレコ表示: - + Logo Image: ロゴ画åƒ�: - + Backside Image: シートè£�é�¢ã�®ç”»åƒ�: - + Output area: 出力エリア: - + Skipped Levels: 表示ã�—ã�ªã�„セルå��: - + Genga Levels Count: 原画セル数: @@ -873,64 +969,64 @@ The keyframe will be cleared. 動画列ã�®ã‚ªãƒ•セット: - + Page Amount: ページ数: - + Camera Column Addition: カメラ列ã�®è¿½åŠ ï¼š - + Overlap At the Start: カット頭 O.L.: - - + + sec + ç§’ + - - + + k コマ - + Overlap At the End: カット尻 O.L.: - - - + + + Warning 警告 - + No columns found! 表示ã�•れる列ã�Œã�‚りã�¾ã�›ã‚“ï¼� - - + + Overlap length cannot be longer than XDTS duration. O.L.ã�®å°ºã�¯XDTSã�®ãƒ•レーム長より長ã��ã�™ã‚‹ã�“ã�¨ã�¯ã�§ã��ã�¾ã�›ã‚“。 - - + + Images ç”»åƒ�ファイル - - + + Specify logo image file ロゴ画åƒ�ã�®ãƒ•ァイルを指定ã�—ã�¦ä¸‹ã�•ã�„ @@ -938,7 +1034,7 @@ The keyframe will be cleared. XsheetPdfPreviewArea - + Fit To Window ウィンドウã�«ãƒ•ィット diff --git a/sources/translations/XDTS_Viewer_zh_CN.ts b/sources/translations/XDTS_Viewer_zh_CN.ts index 6fbaca0..8b51a82 100644 --- a/sources/translations/XDTS_Viewer_zh_CN.ts +++ b/sources/translations/XDTS_Viewer_zh_CN.ts @@ -4,31 +4,31 @@ AETool - + The following frames were not copied because they contain non-numeric strings. - %1 由于包å�«é�žæ•°å­—的字符,以下帧未能å¤�制 - %1 - + There is a cell with the same number as the one reserved for empty frames. 存在一个或多个帧格的编å�·ä¸Žä¸ºç©ºç™½å¸§é¢„留的编å�·ç›¸å�Œã€‚ - - - + + + Copy After Effects Keyframe Data To Clipboard å¤�制AE关键帧数æ�®åˆ°å‰ªè´´æ�¿ - + This tool requires XDTS to be displayed in the Cell area. 使用该工具需è¦�加载动画的xdts。 - + There are mix-up columns in the Cell area. Please turn off the "Mix-up Columns" checkbox in the Settings in order to use this tool. 在动画æ �中有混å�ˆåˆ—。 @@ -38,21 +38,105 @@ Please turn off the "Mix-up Columns" checkbox in the Settings in order AboutDialog - + About %1 关于 %1 - + Built %1 构建于 %1 - + Licenses 许å�¯è¯� + + CspLinkChooserDialog + + + Sync Clip Studio Paint Timesheet + + + + + Choose how to sync the Clip Studio Paint export with XDTS Viewer. + + + + + Sync with an already-open file: + + + + + Sync with an existing file, or export as a new file: + + + + + OK + 确认 + + + + Cancel + + + + + Select or Enter XDTS File + + + + + XDTS files (*.xdts) + XDTS 文件 (*.xdts) + + + + InstanceManager + + + + + + CSP Sync + + + + + Failed to read the Clip Studio Paint export (%1). + + + + + The Clip Studio Paint export looks very different from the currently linked file (%1). Continue updating it, or choose a different file? + + + + + Continue + + + + + Choose Different File + + + + + This will overwrite %1 with the Clip Studio Paint export. Continue? + + + + + Failed to write %1. + + + MixupKeyDialog @@ -114,51 +198,51 @@ Please turn off the "Mix-up Columns" checkbox in the Settings in order MyParams - + Circle 圆 - + Triangle 三角形 - + Approve 确认 - + Save Empty XDTS File ä¿�存空的xdts文件 - + XDTS Files (*.xdts) XDTS 文件 (*.xdts) - + The file %1 already exists. 文件%1å·²ç»�存在。 - + The folder %1 already exists. 文件夹%1å·²ç»�存在。 - + Do you want to overwrite it? 确定è¦�覆盖它å�—? - + Question 问题 @@ -173,293 +257,305 @@ Do you want to overwrite them? - - + + Question 问题 - + OK 确认 - + Open containing folder 打开文件夹 - - - + + + Export Images 导出图åƒ� - + Xsheet images are exported properly. 摄影表图片已导出。 - + < Prev < 上一页 - + Next > 下一页 > - + Load (Ctrl+O) 加载(Ctrl+O) - + Save (Ctrl+S) ä¿�存(Ctrl+S) - - + + Export Image (Ctrl+E) 导出图åƒ�(Ctrl+E) - + Undo (Ctrl+Z) 撤销(Ctrl+Z) - + Redo (Ctrl+Y) é‡�å�šï¼ˆCtrl+Y) - + Zoom In (Ctrl+Plus) 缩å°�(Ctrl+加å�·ï¼‰ - + Zoom Out (Ctrl+Minus) 放大(Ctrl+å‡�å�·ï¼‰ - + Current Color 当å‰�颜色 - + Brush (B) 画笔 - + Large 大 - + Small å°� - + Eraser (E) æ©¡çš® - + Selection (M) 选择工具(M) - + Rectangle 长方形 - + Freehand 手形 - + Thin Line 细线 - + Thick Line 粗线 - + Thin Arrow 细箭头 - + Thick Arrow 粗箭头 - + Double Line å�Œçº¿ - + Dotted Line 虚线 - + Line (U) 线(U) - + Stamp (S) å�°ç« ï¼ˆS) - + Dye Numbers (D) ç¼–å�·æŸ“色(D) - + Copy After Effects Keyframe Data To Clipboard å¤�制AE关键帧数æ�®åˆ°å‰ªè´´æ�¿ - + Settings 设置 - + Menu è�œå�• - + Preferences 首选项 - + About 关于 - + + Unlink from CSP + + + + Cut 剪切 - + Copy å¤�制 - + Paste 粘贴 - + Delete 删除 - + Previous Page 上一页 - + Next Page 下一页 - - + + + Error 错误 - + The preset file %1 is not valid. %1是无效的预设文件。 - + Undo 撤销 - + No more Undo operations available. 没有å�¯ç”¨çš„æ’¤é”€æ“�作。 - + Redo é‡�å�š - + No more Redo operations available. 没有å�¯ç”¨çš„é‡�å�šæ“�作。 - + Open XDTS File 打开xdts文件 - + XDTS files (*.xdts) XDTS 文件 (*.xdts) - + + %1 is not a valid XDTS file. %1是无效的xdts文件。 - + Untitled 未命å�� - + + [Linked with Clip Studio Paint] + + + + The document has been modified. Do you want to save your changes? 文件已ç»�被修改。 需è¦�ä¿�å­˜å�—? - + PNG (*.png);;PSD files (*.psd) PNG (*.png);;PSD 文件 (*.psd) - + PNG (*.png) PNG(*.png) - + Following file(s) are already exist. Do you want to overwrite them? 以下文件已ç»�存在。是å�¦è¦†ç›–? @@ -467,141 +563,141 @@ Do you want to save your changes? PreferencesDialog - + Preferences 首选项 - + Put Serial Frame Numbers Over Pages 跨页显示帧åº�å�· - + Print Level Names On The Bottom 在底部显示列å�� - + Capitalize the first letter of level name (ACTIONS area only) 层级首字æ¯�自动大写(仅é™�原画) - + Empty Frame for AE Keyframe Data: å¤�制AE关键帧时空帧默认值: - + OK 确认 - + Always 总是 - + More Than 3 Continuous Cells 超过3个é‡�å¤�帧 - + None æ—  - + When using "Copy After Effects Keyframe Data To Clipboard" tool, this value will be the frame number for empty cells. 当“å¤�制AE关键帧数æ�®åˆ°å‰ªè´´æ�¿â€�时, 该值为空帧的默认值。 - + Language: 语言: - + Line Color: 框线颜色: - + Continuous Line: 连续线: - + Minimum Frame Length for "REPEAT" symbol: é‡�å¤�(REPEAT)符å�·çš„æœ€å°�帧长度: - + User Stamps Folder: 用户å�°ç« æ–‡ä»¶å¤¹: - + Approval Stamp Name: å�°ç« å��: - + File Suffixes 文件å�Žç¼€ - + LO: LO(设计稿): - + RoughGen: è�‰ç¨¿åŽŸç”»ï¼š - + Genga: 原画: - + Douga: 动画: - + Language has been changed. 语言已å�˜æ›´ã€‚ - + Changes will take effect the next time you run XDTS Viewer. 更改将在下次å�¯åŠ¨ç”Ÿæ•ˆã€‚ - + Specify User Stamp Folder 指定用户å�°ç« æ–‡ä»¶å¤¹ - + Stamp Folder Has Specified. å�°ç« æ–‡ä»¶å¤¹å·²æŒ‡å®šã€‚ - - + + The Change Will Be Applied When You Launch the Application Next Time. 更改将在下次å�¯åŠ¨ç”Ÿæ•ˆã€‚ - + Approval Stamp Name Has Specified. å�°ç« å��已指定。 @@ -657,85 +753,85 @@ for "REPEAT" symbol: 动画 - + CAMERA XSheetPDF 相机 - + REPEAT é‡�å¤� - + HOLD - + (Start OL %1+%2) (开始OL(交å� ï¼‰ %1+%2) - + (End OL %1+%2) (结æ�ŸOL(交å� ï¼‰ %1+%2) - + Warning 警告 - + The saved mix-up columns keyframes does not match the currently displayed columns, so the mix-up columns keyframes were automatically adjusted. You may need to verify whether the adjustment was performed correctly. å·²ä¿�存的列混å�ˆå…³é”®å¸§ä¸Žå½“å‰�显示的列ä¸�匹é…�,已自动调整关键帧。 请确认调整是å�¦æ­£ç¡®ã€‚ - + %1-%2 - + Skipped Frames: 已跳过的帧: - + EPISODE XSheetPDF 集数 - + SEQ. XSheetPDF 镜头å�· - + SCENE XSheetPDF 镜头 - + TIME XSheetPDF æ—¶é—´ - + NAME XSheetPDF å��ç§° - + SHEET XSheetPDF 页数 @@ -744,17 +840,17 @@ You may need to verify whether the adjustment was performed correctly. SelectionTool - + Fill Selection 填充已选择区域 - + Remove Matte Color 移除哑光色 - + Choose Matte Color 选择哑光色 @@ -762,154 +858,154 @@ You may need to verify whether the adjustment was performed correctly. SettingsDialog - + Settings 设置 - + Adaptively Expand Columns 自动扩展列数 - + Edit Mix-up Keys 编辑列混å�ˆå…³é”®å¸§ - + Attach Composite Voucher 附加å�ˆæˆ�传票 - + Show Skipped Drawings Information 显示被跳过的绘图(原画/动画)的信æ�¯ - + Scanned Genga Sheet Mode ( Hide Genga && Camera Area Lines ) 扫æ��原画表模å¼�(éš�è—�原画与相机列的框线) - + OK 确认 - + ACTIONS 原画 - + CELLS 动画 - + Manually Specify Mix-up Keys 手动指定列混å�ˆå…³é”®å¸§ - + Automatically Detect from Data 从数æ�®ä¸­è‡ªåŠ¨è¯†åˆ« - + Template: 模版: - + Mix-up Columns: 列混å�ˆï¼š - + Logo Image: Logo图åƒ�: - + Backside Image: 背é�¢å›¾åƒ�: - + Output area: 帧格加载区域: - + Skipped Levels: 忽略层级: - + Genga Levels Count: 原画层级总数: - + Page Amount: 页数: - + Camera Column Addition: 增加相机列宽度: - + Overlap At the Start: 开头的交å� æ•ˆæžœæŒ�续时间: - - + + sec + ç§’ + - - + + k 帧 - + Overlap At the End: 结尾的交å� æ•ˆæžœæŒ�续时间: - - - + + + Warning 警告 - + No columns found! 未找到任何列ï¼� - - + + Overlap length cannot be longer than XDTS duration. 交å� æ•ˆæžœæ—¶é•¿ä¸�能超过xdts的总时长 - - + + Images 图åƒ�文件 - - + + Specify logo image file 指定Logo图åƒ�文件 @@ -917,7 +1013,7 @@ You may need to verify whether the adjustment was performed correctly. XsheetPdfPreviewArea - + Fit To Window 适应窗å�£ diff --git a/xdts_viewer_resources/loc/XDTS_Viewer_ja.qm b/xdts_viewer_resources/loc/XDTS_Viewer_ja.qm index 365e2e5..9dda1b6 100644 Binary files a/xdts_viewer_resources/loc/XDTS_Viewer_ja.qm and b/xdts_viewer_resources/loc/XDTS_Viewer_ja.qm differ