完善桌宠内核基础设施

This commit is contained in:
2026-05-28 21:16:08 +08:00
parent 6ff904c2df
commit 2d831fbc2d
21 changed files with 1190 additions and 100 deletions
+140 -77
View File
@@ -1,17 +1,20 @@
#include "PetWindow.h"
#include "../character/CharacterPackageLoader.h"
#include "../util/Logger.h"
#include "PetView.h"
#include <QAction>
#include <QContextMenuEvent>
#include <QCursor>
#include <QFileInfo>
#include <QGuiApplication>
#include <QMenu>
#include <QMouseEvent>
#include <QPixmap>
#include <QRandomGenerator>
#include <QScreen>
#include <QSet>
#include <QStringList>
#include <QVBoxLayout>
namespace
@@ -29,24 +32,26 @@ QString previewImagePath()
PetWindow::PetWindow(QWidget *parent)
: QWidget(parent)
, m_imageLabel(new QLabel(this))
, m_currentFrameIndex(0)
, m_petView(new PetView(this))
, m_dragging(false)
, m_alwaysOnTop(true)
, m_centerNextFrame(false)
{
setAttribute(Qt::WA_TranslucentBackground);
setWindowFlags(Qt::FramelessWindowHint | Qt::Tool | Qt::WindowStaysOnTopHint);
setMouseTracking(true);
m_imageLabel->setAlignment(Qt::AlignCenter);
m_imageLabel->setAttribute(Qt::WA_TranslucentBackground);
auto *layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
layout->addWidget(m_imageLabel);
layout->addWidget(m_petView);
connect(&m_animationTimer, &QTimer::timeout, this, [this]() {
advanceStateFrame();
m_frameAnimator.setFrameChangedCallback([this](const QPixmap &pixmap) {
setDisplayPixmap(pixmap, m_centerNextFrame);
m_centerNextFrame = false;
});
m_frameAnimator.setClipFinishedCallback([this](const QString &nextState) {
playResolvedState(m_stateMachine.finishState(nextState), false);
});
m_idleBehaviorTimer.setSingleShot(true);
@@ -62,6 +67,49 @@ PetWindow::PetWindow(QWidget *parent)
loadInitialImage();
}
void PetWindow::applyAppConfig(const AppConfig &config)
{
setAlwaysOnTop(config.alwaysOnTop);
if (config.hasWindowPosition && isPointVisibleOnScreen(config.windowPosition))
{
move(config.windowPosition);
}
}
AppConfig PetWindow::currentAppConfig() const
{
AppConfig config;
config.windowPosition = pos();
config.hasWindowPosition = true;
config.alwaysOnTop = m_alwaysOnTop;
return config;
}
void PetWindow::pauseAnimation()
{
m_returnToIdleAfterResume = m_behaviorReturnTimer.isActive();
m_idleBehaviorTimer.stop();
m_behaviorReturnTimer.stop();
m_frameAnimator.pause();
}
void PetWindow::resumeAnimation()
{
m_frameAnimator.resume();
if (m_stateMachine.currentState() == QStringLiteral("idle"))
{
scheduleIdleBehavior();
}
else if (m_returnToIdleAfterResume)
{
m_behaviorReturnTimer.start(4000);
}
m_returnToIdleAfterResume = false;
}
void PetWindow::contextMenuEvent(QContextMenuEvent *event)
{
QMenu menu(this);
@@ -108,7 +156,7 @@ void PetWindow::mousePressEvent(QMouseEvent *event)
{
m_dragging = true;
m_dragOffset = event->globalPosition().toPoint() - frameGeometry().topLeft();
playState(QStringLiteral("drag"), false);
playResolvedState(m_stateMachine.beginDrag(), false);
event->accept();
return;
}
@@ -121,7 +169,7 @@ void PetWindow::mouseReleaseEvent(QMouseEvent *event)
if (event->button() == Qt::LeftButton)
{
m_dragging = false;
playState(QStringLiteral("idle"), false);
playResolvedState(m_stateMachine.endDrag(), false);
event->accept();
return;
}
@@ -133,15 +181,43 @@ void PetWindow::loadInitialImage()
{
QString loadError;
m_characterPackage = CharacterPackageLoader::load(characterPackagePath(), &loadError);
if (m_characterPackage.hasState(QStringLiteral("idle")))
if (!loadError.isEmpty())
{
playState(QStringLiteral("idle"), true);
Logger::warning(QStringLiteral("Character package load failed: ") + loadError);
}
buildAnimationClips();
if (m_clips.contains(QStringLiteral("idle")))
{
playResolvedState(m_stateMachine.start(), true);
return;
}
setDisplayImage(previewImagePath(), true);
}
void PetWindow::buildAnimationClips()
{
m_clips.clear();
QSet<QString> availableStates;
const QSize targetSize(320, 320);
for (auto iterator = m_characterPackage.states.constBegin(); iterator != m_characterPackage.states.constEnd(); ++iterator)
{
AnimationClip clip = AnimationClip::fromState(iterator.value(), targetSize);
if (!clip.isValid())
{
continue;
}
availableStates.insert(iterator.key());
m_clips.insert(iterator.key(), clip);
}
m_stateMachine.setAvailableStates(availableStates);
}
void PetWindow::addStateTestActions(QMenu *menu)
{
QMenu *stateMenu = menu->addMenu(QStringLiteral("State Test"));
@@ -157,7 +233,7 @@ void PetWindow::addStateTestActions(QMenu *menu)
for (const QString &stateName : stateNames)
{
if (!m_characterPackage.hasState(stateName))
if (!m_clips.contains(stateName))
{
continue;
}
@@ -169,84 +245,48 @@ void PetWindow::addStateTestActions(QMenu *menu)
stateMenu->setEnabled(!stateMenu->actions().isEmpty());
}
void PetWindow::playState(const QString &stateName, bool centerWindow, bool autoReturn)
void PetWindow::playState(const QString &stateName, bool centerWindow)
{
if (m_currentStateName == stateName && !m_currentFrames.isEmpty())
playResolvedState(m_stateMachine.requestState(stateName), centerWindow);
}
void PetWindow::playResolvedState(const QString &stateName, bool centerWindow)
{
if (stateName.isEmpty())
{
return;
}
const CharacterState *state = m_characterPackage.state(stateName);
if (state == nullptr || state->framePaths.isEmpty() || state->fps <= 0)
if (m_frameAnimator.currentStateName() == stateName && m_frameAnimator.isPlaying())
{
return;
}
auto clipIterator = m_clips.constFind(stateName);
if (clipIterator == m_clips.constEnd())
{
return;
}
const AnimationClip *clip = &clipIterator.value();
m_idleBehaviorTimer.stop();
m_behaviorReturnTimer.stop();
m_animationTimer.stop();
m_currentStateName = stateName;
m_currentFrames = state->framePaths;
m_currentFrameIndex = 0;
setDisplayImage(m_currentFrames.at(m_currentFrameIndex), centerWindow);
const int intervalMs = qMax(1, 1000 / state->fps);
m_animationTimer.start(intervalMs);
m_centerNextFrame = centerWindow;
m_frameAnimator.play(clip);
if (stateName == QStringLiteral("idle"))
{
scheduleIdleBehavior();
}
else if (autoReturn && state->loop)
else if (clip->loop)
{
m_behaviorReturnTimer.start(4000);
}
}
void PetWindow::advanceStateFrame()
{
if (m_currentFrames.isEmpty())
{
m_animationTimer.stop();
return;
}
const CharacterState *state = m_characterPackage.state(m_currentStateName);
if (state == nullptr)
{
m_animationTimer.stop();
return;
}
if (m_currentFrameIndex + 1 >= m_currentFrames.size())
{
if (state->loop)
{
m_currentFrameIndex = 0;
}
else if (!state->nextState.isEmpty())
{
playState(state->nextState, false);
return;
}
else
{
m_animationTimer.stop();
return;
}
}
else
{
++m_currentFrameIndex;
}
setDisplayImage(m_currentFrames.at(m_currentFrameIndex), false);
}
void PetWindow::scheduleIdleBehavior()
{
if (!m_characterPackage.hasState(QStringLiteral("idle")))
if (!m_clips.contains(QStringLiteral("idle")))
{
return;
}
@@ -257,7 +297,7 @@ void PetWindow::scheduleIdleBehavior()
void PetWindow::playIdleBehavior()
{
if (m_dragging || m_currentStateName != QStringLiteral("idle"))
if (m_dragging || m_stateMachine.currentState() != QStringLiteral("idle"))
{
scheduleIdleBehavior();
return;
@@ -272,7 +312,7 @@ void PetWindow::playIdleBehavior()
for (const QString &stateName : preferredStates)
{
if (m_characterPackage.hasState(stateName))
if (m_clips.contains(stateName))
{
candidateStates.append(stateName);
}
@@ -285,14 +325,14 @@ void PetWindow::playIdleBehavior()
}
const int stateIndex = QRandomGenerator::global()->bounded(candidateStates.size());
playState(candidateStates.at(stateIndex), false, true);
playResolvedState(m_stateMachine.requestState(candidateStates.at(stateIndex), StateRequestSource::Automatic), false);
}
void PetWindow::returnToIdleFromBehavior()
{
if (!m_dragging)
{
playState(QStringLiteral("idle"), false);
playResolvedState(m_stateMachine.finishState(QStringLiteral("idle")), false);
}
}
@@ -301,15 +341,20 @@ void PetWindow::setDisplayImage(const QString &imagePath, bool centerWindow)
QPixmap pixmap(imagePath);
if (pixmap.isNull())
{
m_imageLabel->setText(QStringLiteral("QtDesktopPet"));
m_petView->showFallbackText(QStringLiteral("QtDesktopPet"));
resize(240, 160);
return;
}
const QSize targetSize(320, 320);
const QPixmap scaled = pixmap.scaled(targetSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
m_imageLabel->setPixmap(scaled);
resize(scaled.size());
setDisplayPixmap(scaled, centerWindow);
}
void PetWindow::setDisplayPixmap(const QPixmap &pixmap, bool centerWindow)
{
m_petView->setFrame(pixmap);
resize(pixmap.size());
if (centerWindow)
{
@@ -321,9 +366,24 @@ void PetWindow::setDisplayImage(const QString &imagePath, bool centerWindow)
}
}
bool PetWindow::isPointVisibleOnScreen(const QPoint &point) const
{
const QList<QScreen *> screens = QGuiApplication::screens();
for (const QScreen *screen : screens)
{
if (screen != nullptr && screen->availableGeometry().contains(point))
{
return true;
}
}
return false;
}
void PetWindow::setAlwaysOnTop(bool enabled)
{
m_alwaysOnTop = enabled;
const bool wasVisible = isVisible();
Qt::WindowFlags flags = windowFlags();
if (enabled)
@@ -336,5 +396,8 @@ void PetWindow::setAlwaysOnTop(bool enabled)
}
setWindowFlags(flags);
show();
if (wasVisible)
{
show();
}
}