完善桌宠内核基础设施
This commit is contained in:
+2
-3
@@ -19,7 +19,6 @@ cmake-build-*/
|
|||||||
*.lib
|
*.lib
|
||||||
|
|
||||||
# Runtime/config files generated during development
|
# Runtime/config files generated during development
|
||||||
config/
|
/config/
|
||||||
logs/
|
/logs/
|
||||||
*.broken.json
|
*.broken.json
|
||||||
|
|
||||||
|
|||||||
@@ -14,12 +14,27 @@ find_package(Qt6 REQUIRED COMPONENTS Widgets)
|
|||||||
|
|
||||||
qt_add_executable(QtDesktopPet
|
qt_add_executable(QtDesktopPet
|
||||||
main.cpp
|
main.cpp
|
||||||
|
src/character/AnimationClip.h
|
||||||
|
src/character/AnimationClip.cpp
|
||||||
src/character/CharacterPackage.h
|
src/character/CharacterPackage.h
|
||||||
src/character/CharacterPackage.cpp
|
src/character/CharacterPackage.cpp
|
||||||
src/character/CharacterPackageLoader.h
|
src/character/CharacterPackageLoader.h
|
||||||
src/character/CharacterPackageLoader.cpp
|
src/character/CharacterPackageLoader.cpp
|
||||||
|
src/character/FrameAnimator.h
|
||||||
|
src/character/FrameAnimator.cpp
|
||||||
|
src/config/AppConfig.h
|
||||||
|
src/config/ConfigManager.h
|
||||||
|
src/config/ConfigManager.cpp
|
||||||
|
src/state/PetStateMachine.h
|
||||||
|
src/state/PetStateMachine.cpp
|
||||||
|
src/tray/TrayController.h
|
||||||
|
src/tray/TrayController.cpp
|
||||||
|
src/ui/PetView.h
|
||||||
|
src/ui/PetView.cpp
|
||||||
src/ui/PetWindow.h
|
src/ui/PetWindow.h
|
||||||
src/ui/PetWindow.cpp
|
src/ui/PetWindow.cpp
|
||||||
|
src/util/Logger.h
|
||||||
|
src/util/Logger.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_compile_definitions(QtDesktopPet
|
target_compile_definitions(QtDesktopPet
|
||||||
|
|||||||
+94
-10
@@ -102,13 +102,25 @@ shiroko/
|
|||||||
└── error/
|
└── error/
|
||||||
```
|
```
|
||||||
|
|
||||||
每个状态当前包含 4 帧 PNG。
|
当前素材版本:`2.1.0-stable`。
|
||||||
|
|
||||||
|
当前帧数:
|
||||||
|
|
||||||
|
```text
|
||||||
|
idle :30 帧
|
||||||
|
talk :20 帧
|
||||||
|
think :30 帧
|
||||||
|
sleep :30 帧
|
||||||
|
happy :20 帧
|
||||||
|
drag :30 帧
|
||||||
|
error :20 帧
|
||||||
|
```
|
||||||
|
|
||||||
需要注意:
|
需要注意:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
1. character.json 中 base.width/base.height 为 627x627,超过原开发文档建议的 512x512
|
1. character.json 中 base.width/base.height 当前为 512x512
|
||||||
2. 原型阶段可以接受,但后续需要观察内存、缩放缓存和低配设备表现
|
2. 当前实现会预加载当前角色包的全部状态帧,后续需要观察内存和低配设备表现
|
||||||
3. 如果项目公开发布或推送远程仓库,需要确认 shiroko 素材版权和再分发权限
|
3. 如果项目公开发布或推送远程仓库,需要确认 shiroko 素材版权和再分发权限
|
||||||
4. 版权不明确前,不应把它作为正式开源发布素材承诺
|
4. 版权不明确前,不应把它作为正式开源发布素材承诺
|
||||||
```
|
```
|
||||||
@@ -504,16 +516,88 @@ shiroko/
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 13. 当前未决问题
|
## 13. 当前项目进度
|
||||||
|
|
||||||
|
截至当前工作区,项目已经完成以下内容:
|
||||||
|
|
||||||
|
```text
|
||||||
|
1. 阶段 0 工程基础:
|
||||||
|
已有 .gitignore、CMakeLists.txt、main.cpp、src/、docs/、resources/ 等基础结构
|
||||||
|
|
||||||
|
2. 阶段 1 最小桌宠窗口:
|
||||||
|
已实现透明无边框窗口、拖动、右键退出、置顶切换
|
||||||
|
|
||||||
|
3. 阶段 2A 角色包最小读取:
|
||||||
|
已有 CharacterPackage / CharacterPackageLoader
|
||||||
|
能读取 shiroko/character.json 并收集状态帧路径
|
||||||
|
|
||||||
|
4. 阶段 2B idle 帧动画:
|
||||||
|
已新增 AnimationClip / FrameAnimator
|
||||||
|
当前实现会把当前角色包状态帧加载为 QPixmap 缓存,避免每帧读硬盘
|
||||||
|
|
||||||
|
5. 阶段 3 状态机初版:
|
||||||
|
已新增 PetStateMachine
|
||||||
|
已支持 idle / drag / think / talk / happy / sleep / error 的基础请求、拖动优先和缺失状态回退 idle
|
||||||
|
|
||||||
|
6. 阶段 4 基础设施的一部分:
|
||||||
|
已新增 PetView,拆分显示职责
|
||||||
|
已新增 TrayController,支持托盘显示、隐藏、退出
|
||||||
|
已新增 ConfigManager,保存窗口位置和置顶状态
|
||||||
|
已新增 Logger,支持文件日志和基础轮转
|
||||||
|
```
|
||||||
|
|
||||||
|
当前实现与计划仍存在差异:
|
||||||
|
|
||||||
|
```text
|
||||||
|
1. shiroko 角色包仍位于项目根目录 shiroko/,尚未移动到 resources/characters/shiroko
|
||||||
|
2. 当前未实现 AI 接入、AI 配置界面和 ChatBubble
|
||||||
|
3. 当前未实现 SettingsDialog
|
||||||
|
4. 当前 ConfigManager 只保存窗口位置和置顶状态,尚未保存缩放、性能模式、角色选择
|
||||||
|
5. 当前 Logger 只接入启动、退出、配置和角色包加载失败等低频日志
|
||||||
|
6. 当前 FrameAnimator 采用当前角色包全部状态帧预加载,尚未做懒加载
|
||||||
|
7. 本轮架构调整后尚未执行构建、编译或运行验证
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. 下一步建议
|
||||||
|
|
||||||
|
短期建议:
|
||||||
|
|
||||||
|
```text
|
||||||
|
1. 在用户确认后做一次构建验证
|
||||||
|
2. 修复构建或静态检查发现的问题
|
||||||
|
3. 补最小 README.md,记录当前开发状态、构建方式、素材版权提示
|
||||||
|
4. 确认 LICENSE 是否采用 MIT
|
||||||
|
5. 在进入 AI 前,做一次桌宠内核稳定性检查:
|
||||||
|
- 启动显示
|
||||||
|
- idle 动画
|
||||||
|
- 状态切换
|
||||||
|
- 托盘隐藏 / 显示
|
||||||
|
- 配置保存 / 恢复
|
||||||
|
- 日志写入 / 轮转路径
|
||||||
|
```
|
||||||
|
|
||||||
|
中期建议:
|
||||||
|
|
||||||
|
```text
|
||||||
|
1. 补 SettingsDialog 的最小框架
|
||||||
|
2. 补 ChatBubble
|
||||||
|
3. 再接入 OpenAI Compatible 非流式 AI 对话
|
||||||
|
4. AI 接入后再做 think -> talk -> idle / error -> idle 状态联动
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15. 当前未决问题
|
||||||
|
|
||||||
后续开始写代码前,需要逐项确认:
|
后续开始写代码前,需要逐项确认:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
1. 是否创建 .gitignore
|
1. 是否创建 MIT LICENSE
|
||||||
2. 是否创建 MIT LICENSE
|
2. 是否创建项目根 README.md
|
||||||
3. 是否把 shiroko 移动到 resources/characters/shiroko
|
3. 是否把 shiroko 移动到 resources/characters/shiroko
|
||||||
4. 是否使用 shiroko/preview.png 作为阶段 1 单图
|
4. 是否保持当前“预加载全部当前角色状态帧”的策略,还是改成按状态懒加载
|
||||||
5. 是否立即创建最小 Qt Widgets 工程
|
5. shiroko 素材是否允许作为正式开源发布素材继续保留在仓库中
|
||||||
6. 是否将 build/、.vs/、CMake 生成目录全部加入 .gitignore
|
6. 是否在下一步执行构建验证
|
||||||
7. shiroko 素材是否允许提交到远程仓库
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
#include "src/config/ConfigManager.h"
|
||||||
|
#include "src/tray/TrayController.h"
|
||||||
#include "src/ui/PetWindow.h"
|
#include "src/ui/PetWindow.h"
|
||||||
|
#include "src/util/Logger.h"
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
@@ -8,9 +13,25 @@ int main(int argc, char *argv[])
|
|||||||
QApplication::setApplicationName("QtDesktopPet");
|
QApplication::setApplicationName("QtDesktopPet");
|
||||||
QApplication::setOrganizationName("QtDesktopPet");
|
QApplication::setOrganizationName("QtDesktopPet");
|
||||||
|
|
||||||
|
Logger::info(QStringLiteral("Application started."));
|
||||||
|
|
||||||
|
ConfigManager configManager;
|
||||||
PetWindow window;
|
PetWindow window;
|
||||||
|
window.applyAppConfig(configManager.loadAppConfig());
|
||||||
|
|
||||||
|
TrayController trayController(&window);
|
||||||
|
trayController.show();
|
||||||
|
|
||||||
|
QObject::connect(&app, &QCoreApplication::aboutToQuit, [&configManager, &window]() {
|
||||||
|
if (!configManager.saveAppConfig(window.currentAppConfig()))
|
||||||
|
{
|
||||||
|
Logger::warning(QStringLiteral("Failed to save app config."));
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger::info(QStringLiteral("Application is exiting."));
|
||||||
|
});
|
||||||
|
|
||||||
window.show();
|
window.show();
|
||||||
|
|
||||||
return app.exec();
|
return app.exec();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
#include "AnimationClip.h"
|
||||||
|
|
||||||
|
#include <QSize>
|
||||||
|
|
||||||
|
AnimationClip AnimationClip::fromState(const CharacterState &state, const QSize &targetSize)
|
||||||
|
{
|
||||||
|
AnimationClip clip;
|
||||||
|
clip.stateName = state.name;
|
||||||
|
clip.fps = state.fps;
|
||||||
|
clip.loop = state.loop;
|
||||||
|
clip.nextState = state.nextState;
|
||||||
|
|
||||||
|
for (const QString &framePath : state.framePaths)
|
||||||
|
{
|
||||||
|
QPixmap pixmap(framePath);
|
||||||
|
if (pixmap.isNull())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetSize.isValid())
|
||||||
|
{
|
||||||
|
pixmap = pixmap.scaled(targetSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||||
|
}
|
||||||
|
|
||||||
|
clip.m_frames.append(pixmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
return clip;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AnimationClip::isValid() const
|
||||||
|
{
|
||||||
|
return fps > 0 && !m_frames.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
const QPixmap &AnimationClip::frameAt(int index) const
|
||||||
|
{
|
||||||
|
return m_frames.at(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
int AnimationClip::frameCount() const
|
||||||
|
{
|
||||||
|
return m_frames.size();
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CharacterPackage.h"
|
||||||
|
|
||||||
|
#include <QPixmap>
|
||||||
|
#include <QSize>
|
||||||
|
#include <QString>
|
||||||
|
#include <QVector>
|
||||||
|
|
||||||
|
class AnimationClip
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static AnimationClip fromState(const CharacterState &state, const QSize &targetSize);
|
||||||
|
|
||||||
|
bool isValid() const;
|
||||||
|
const QPixmap &frameAt(int index) const;
|
||||||
|
int frameCount() const;
|
||||||
|
|
||||||
|
QString stateName;
|
||||||
|
int fps = 0;
|
||||||
|
bool loop = true;
|
||||||
|
QString nextState;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QVector<QPixmap> m_frames;
|
||||||
|
};
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
#include "FrameAnimator.h"
|
||||||
|
|
||||||
|
#include <QtGlobal>
|
||||||
|
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
FrameAnimator::FrameAnimator()
|
||||||
|
{
|
||||||
|
QObject::connect(&m_timer, &QTimer::timeout, [this]() {
|
||||||
|
advanceFrame();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void FrameAnimator::setFrameChangedCallback(FrameChangedCallback callback)
|
||||||
|
{
|
||||||
|
m_frameChangedCallback = std::move(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FrameAnimator::setClipFinishedCallback(ClipFinishedCallback callback)
|
||||||
|
{
|
||||||
|
m_clipFinishedCallback = std::move(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FrameAnimator::play(const AnimationClip *clip)
|
||||||
|
{
|
||||||
|
if (clip == nullptr || !clip->isValid())
|
||||||
|
{
|
||||||
|
stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_timer.stop();
|
||||||
|
m_clip = clip;
|
||||||
|
m_frameIndex = 0;
|
||||||
|
m_paused = false;
|
||||||
|
|
||||||
|
if (m_frameChangedCallback)
|
||||||
|
{
|
||||||
|
m_frameChangedCallback(m_clip->frameAt(m_frameIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
const int intervalMs = qMax(1, 1000 / m_clip->fps);
|
||||||
|
m_timer.start(intervalMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FrameAnimator::pause()
|
||||||
|
{
|
||||||
|
if (m_clip == nullptr)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_timer.stop();
|
||||||
|
m_paused = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FrameAnimator::resume()
|
||||||
|
{
|
||||||
|
if (m_clip == nullptr || !m_clip->isValid() || !m_paused)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int intervalMs = qMax(1, 1000 / m_clip->fps);
|
||||||
|
m_timer.start(intervalMs);
|
||||||
|
m_paused = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FrameAnimator::stop()
|
||||||
|
{
|
||||||
|
m_timer.stop();
|
||||||
|
m_clip = nullptr;
|
||||||
|
m_frameIndex = 0;
|
||||||
|
m_paused = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FrameAnimator::isPlaying() const
|
||||||
|
{
|
||||||
|
return m_timer.isActive() && m_clip != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FrameAnimator::isPaused() const
|
||||||
|
{
|
||||||
|
return m_paused && m_clip != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString FrameAnimator::currentStateName() const
|
||||||
|
{
|
||||||
|
if (m_clip == nullptr)
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return m_clip->stateName;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FrameAnimator::advanceFrame()
|
||||||
|
{
|
||||||
|
if (m_clip == nullptr || !m_clip->isValid())
|
||||||
|
{
|
||||||
|
stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_frameIndex + 1 >= m_clip->frameCount())
|
||||||
|
{
|
||||||
|
if (m_clip->loop)
|
||||||
|
{
|
||||||
|
m_frameIndex = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const QString nextState = m_clip->nextState;
|
||||||
|
stop();
|
||||||
|
if (m_clipFinishedCallback)
|
||||||
|
{
|
||||||
|
m_clipFinishedCallback(nextState);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
++m_frameIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_frameChangedCallback)
|
||||||
|
{
|
||||||
|
m_frameChangedCallback(m_clip->frameAt(m_frameIndex));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "AnimationClip.h"
|
||||||
|
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
class FrameAnimator
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using FrameChangedCallback = std::function<void(const QPixmap &)>;
|
||||||
|
using ClipFinishedCallback = std::function<void(const QString &)>;
|
||||||
|
|
||||||
|
FrameAnimator();
|
||||||
|
|
||||||
|
void setFrameChangedCallback(FrameChangedCallback callback);
|
||||||
|
void setClipFinishedCallback(ClipFinishedCallback callback);
|
||||||
|
|
||||||
|
void play(const AnimationClip *clip);
|
||||||
|
void pause();
|
||||||
|
void resume();
|
||||||
|
void stop();
|
||||||
|
bool isPlaying() const;
|
||||||
|
bool isPaused() const;
|
||||||
|
QString currentStateName() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void advanceFrame();
|
||||||
|
|
||||||
|
QTimer m_timer;
|
||||||
|
const AnimationClip *m_clip = nullptr;
|
||||||
|
int m_frameIndex = 0;
|
||||||
|
bool m_paused = false;
|
||||||
|
FrameChangedCallback m_frameChangedCallback;
|
||||||
|
ClipFinishedCallback m_clipFinishedCallback;
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QPoint>
|
||||||
|
|
||||||
|
struct AppConfig
|
||||||
|
{
|
||||||
|
QPoint windowPosition = QPoint(100, 100);
|
||||||
|
bool hasWindowPosition = false;
|
||||||
|
bool alwaysOnTop = true;
|
||||||
|
};
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
#include "ConfigManager.h"
|
||||||
|
|
||||||
|
#include "../util/Logger.h"
|
||||||
|
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QJsonParseError>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
const QString AppConfigFileName = QStringLiteral("app_config.json");
|
||||||
|
|
||||||
|
QJsonObject windowObjectFromConfig(const AppConfig &config)
|
||||||
|
{
|
||||||
|
QJsonObject window;
|
||||||
|
window.insert(QStringLiteral("x"), config.windowPosition.x());
|
||||||
|
window.insert(QStringLiteral("y"), config.windowPosition.y());
|
||||||
|
window.insert(QStringLiteral("alwaysOnTop"), config.alwaysOnTop);
|
||||||
|
return window;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigManager::ConfigManager() = default;
|
||||||
|
|
||||||
|
AppConfig ConfigManager::loadAppConfig() const
|
||||||
|
{
|
||||||
|
AppConfig config;
|
||||||
|
|
||||||
|
QFile file(appConfigPath());
|
||||||
|
if (!file.exists())
|
||||||
|
{
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file.open(QIODevice::ReadOnly))
|
||||||
|
{
|
||||||
|
Logger::warning(QStringLiteral("Unable to read app config."));
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonParseError parseError;
|
||||||
|
const QJsonDocument document = QJsonDocument::fromJson(file.readAll(), &parseError);
|
||||||
|
if (parseError.error != QJsonParseError::NoError || !document.isObject())
|
||||||
|
{
|
||||||
|
file.close();
|
||||||
|
backupBrokenConfig(appConfigPath());
|
||||||
|
Logger::warning(QStringLiteral("App config is broken; default config will be used."));
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QJsonObject root = document.object();
|
||||||
|
const QJsonObject window = root.value(QStringLiteral("window")).toObject();
|
||||||
|
if (window.contains(QStringLiteral("x")) && window.contains(QStringLiteral("y")))
|
||||||
|
{
|
||||||
|
config.windowPosition = QPoint(
|
||||||
|
window.value(QStringLiteral("x")).toInt(config.windowPosition.x()),
|
||||||
|
window.value(QStringLiteral("y")).toInt(config.windowPosition.y()));
|
||||||
|
config.hasWindowPosition = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.contains(QStringLiteral("alwaysOnTop")))
|
||||||
|
{
|
||||||
|
config.alwaysOnTop = window.value(QStringLiteral("alwaysOnTop")).toBool(config.alwaysOnTop);
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ConfigManager::saveAppConfig(const AppConfig &config) const
|
||||||
|
{
|
||||||
|
const QString directoryPath = configDirectoryPath();
|
||||||
|
QDir directory(directoryPath);
|
||||||
|
if (!directory.exists() && !directory.mkpath(QStringLiteral(".")))
|
||||||
|
{
|
||||||
|
Logger::warning(QStringLiteral("Unable to create config directory."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject root;
|
||||||
|
root.insert(QStringLiteral("window"), windowObjectFromConfig(config));
|
||||||
|
|
||||||
|
QFile file(appConfigPath());
|
||||||
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate))
|
||||||
|
{
|
||||||
|
Logger::warning(QStringLiteral("Unable to open app config for writing."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QJsonDocument document(root);
|
||||||
|
return file.write(document.toJson(QJsonDocument::Indented)) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ConfigManager::appConfigPath() const
|
||||||
|
{
|
||||||
|
return QDir(configDirectoryPath()).filePath(AppConfigFileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ConfigManager::configDirectoryPath() const
|
||||||
|
{
|
||||||
|
const QString path = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation);
|
||||||
|
if (!path.isEmpty())
|
||||||
|
{
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
return QDir::currentPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConfigManager::backupBrokenConfig(const QString &filePath) const
|
||||||
|
{
|
||||||
|
QFile file(filePath);
|
||||||
|
if (!file.exists())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QFileInfo fileInfo(filePath);
|
||||||
|
const QString backupPath = fileInfo.dir().filePath(fileInfo.completeBaseName() + QStringLiteral(".broken.json"));
|
||||||
|
if (QFile::exists(backupPath))
|
||||||
|
{
|
||||||
|
QFile::remove(backupPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
file.rename(backupPath);
|
||||||
|
Logger::warning(QStringLiteral("Broken app config was backed up."));
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "AppConfig.h"
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
class ConfigManager
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ConfigManager();
|
||||||
|
|
||||||
|
AppConfig loadAppConfig() const;
|
||||||
|
bool saveAppConfig(const AppConfig &config) const;
|
||||||
|
QString appConfigPath() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString configDirectoryPath() const;
|
||||||
|
void backupBrokenConfig(const QString &filePath) const;
|
||||||
|
};
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
#include "PetStateMachine.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
const QString IdleState = QStringLiteral("idle");
|
||||||
|
const QString DragState = QStringLiteral("drag");
|
||||||
|
const QString ErrorState = QStringLiteral("error");
|
||||||
|
const QString ThinkState = QStringLiteral("think");
|
||||||
|
const QString TalkState = QStringLiteral("talk");
|
||||||
|
const QString HappyState = QStringLiteral("happy");
|
||||||
|
const QString SleepState = QStringLiteral("sleep");
|
||||||
|
}
|
||||||
|
|
||||||
|
void PetStateMachine::setAvailableStates(const QSet<QString> &states)
|
||||||
|
{
|
||||||
|
m_availableStates = states;
|
||||||
|
|
||||||
|
if (!m_currentState.isEmpty())
|
||||||
|
{
|
||||||
|
m_currentState = resolveState(m_currentState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString PetStateMachine::currentState() const
|
||||||
|
{
|
||||||
|
return m_currentState;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString PetStateMachine::start()
|
||||||
|
{
|
||||||
|
m_dragging = false;
|
||||||
|
m_currentState = resolveState(IdleState);
|
||||||
|
return m_currentState;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString PetStateMachine::requestState(const QString &stateName, StateRequestSource source)
|
||||||
|
{
|
||||||
|
if (m_dragging)
|
||||||
|
{
|
||||||
|
m_currentState = resolveState(DragState);
|
||||||
|
return m_currentState;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString requestedState = resolveState(stateName);
|
||||||
|
if (shouldKeepCurrentState(requestedState, source))
|
||||||
|
{
|
||||||
|
return m_currentState;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_currentState = requestedState;
|
||||||
|
return m_currentState;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString PetStateMachine::beginDrag()
|
||||||
|
{
|
||||||
|
m_dragging = true;
|
||||||
|
m_currentState = resolveState(DragState);
|
||||||
|
return m_currentState;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString PetStateMachine::endDrag()
|
||||||
|
{
|
||||||
|
m_dragging = false;
|
||||||
|
m_currentState = resolveState(IdleState);
|
||||||
|
return m_currentState;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString PetStateMachine::finishState(const QString &nextState)
|
||||||
|
{
|
||||||
|
if (m_dragging)
|
||||||
|
{
|
||||||
|
m_currentState = resolveState(DragState);
|
||||||
|
return m_currentState;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nextState.isEmpty())
|
||||||
|
{
|
||||||
|
m_currentState = resolveState(nextState);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_currentState = resolveState(IdleState);
|
||||||
|
}
|
||||||
|
|
||||||
|
return m_currentState;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString PetStateMachine::resolveState(const QString &stateName) const
|
||||||
|
{
|
||||||
|
if (m_availableStates.contains(stateName))
|
||||||
|
{
|
||||||
|
return stateName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_availableStates.contains(IdleState))
|
||||||
|
{
|
||||||
|
return IdleState;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
int PetStateMachine::priorityOf(const QString &stateName) const
|
||||||
|
{
|
||||||
|
if (stateName == DragState)
|
||||||
|
{
|
||||||
|
return 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateName == ErrorState)
|
||||||
|
{
|
||||||
|
return 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateName == ThinkState)
|
||||||
|
{
|
||||||
|
return 40;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateName == TalkState)
|
||||||
|
{
|
||||||
|
return 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateName == HappyState)
|
||||||
|
{
|
||||||
|
return 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateName == SleepState)
|
||||||
|
{
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PetStateMachine::shouldKeepCurrentState(const QString &requestedState, StateRequestSource source) const
|
||||||
|
{
|
||||||
|
if (requestedState.isEmpty() || m_currentState.isEmpty())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source == StateRequestSource::Manual || source == StateRequestSource::System)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_currentState == IdleState)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return priorityOf(m_currentState) > priorityOf(requestedState);
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QSet>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
enum class StateRequestSource
|
||||||
|
{
|
||||||
|
Manual,
|
||||||
|
Automatic,
|
||||||
|
System,
|
||||||
|
};
|
||||||
|
|
||||||
|
class PetStateMachine
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void setAvailableStates(const QSet<QString> &states);
|
||||||
|
|
||||||
|
QString currentState() const;
|
||||||
|
QString start();
|
||||||
|
QString requestState(const QString &stateName, StateRequestSource source = StateRequestSource::Manual);
|
||||||
|
QString beginDrag();
|
||||||
|
QString endDrag();
|
||||||
|
QString finishState(const QString &nextState);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString resolveState(const QString &stateName) const;
|
||||||
|
int priorityOf(const QString &stateName) const;
|
||||||
|
bool shouldKeepCurrentState(const QString &requestedState, StateRequestSource source) const;
|
||||||
|
|
||||||
|
QSet<QString> m_availableStates;
|
||||||
|
QString m_currentState;
|
||||||
|
bool m_dragging = false;
|
||||||
|
};
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
#include "TrayController.h"
|
||||||
|
|
||||||
|
#include "../ui/PetWindow.h"
|
||||||
|
|
||||||
|
#include <QAction>
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QIcon>
|
||||||
|
#include <QPixmap>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
QString trayIconPath()
|
||||||
|
{
|
||||||
|
return QStringLiteral(PET_SOURCE_DIR) + QStringLiteral("/shiroko/preview.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
QIcon loadTrayIcon()
|
||||||
|
{
|
||||||
|
const QPixmap pixmap(trayIconPath());
|
||||||
|
if (!pixmap.isNull())
|
||||||
|
{
|
||||||
|
return QIcon(pixmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
return QIcon();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TrayController::TrayController(PetWindow *petWindow)
|
||||||
|
: m_petWindow(petWindow)
|
||||||
|
{
|
||||||
|
m_trayIcon.setIcon(loadTrayIcon());
|
||||||
|
m_trayIcon.setToolTip(QStringLiteral("QtDesktopPet"));
|
||||||
|
|
||||||
|
createMenu();
|
||||||
|
m_trayIcon.setContextMenu(&m_menu);
|
||||||
|
|
||||||
|
QObject::connect(&m_trayIcon, &QSystemTrayIcon::activated, [this](QSystemTrayIcon::ActivationReason reason) {
|
||||||
|
if (reason == QSystemTrayIcon::Trigger || reason == QSystemTrayIcon::DoubleClick)
|
||||||
|
{
|
||||||
|
togglePetWindowVisibility();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TrayController::isAvailable() const
|
||||||
|
{
|
||||||
|
return QSystemTrayIcon::isSystemTrayAvailable();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TrayController::show()
|
||||||
|
{
|
||||||
|
if (!isAvailable())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_trayIcon.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TrayController::createMenu()
|
||||||
|
{
|
||||||
|
QAction *showAction = m_menu.addAction(QStringLiteral("显示桌宠"));
|
||||||
|
QObject::connect(showAction, &QAction::triggered, [this]() {
|
||||||
|
showPetWindow();
|
||||||
|
});
|
||||||
|
|
||||||
|
QAction *hideAction = m_menu.addAction(QStringLiteral("隐藏桌宠"));
|
||||||
|
QObject::connect(hideAction, &QAction::triggered, [this]() {
|
||||||
|
hidePetWindow();
|
||||||
|
});
|
||||||
|
|
||||||
|
m_menu.addSeparator();
|
||||||
|
|
||||||
|
QAction *quitAction = m_menu.addAction(QStringLiteral("退出"));
|
||||||
|
QObject::connect(quitAction, &QAction::triggered, [this]() {
|
||||||
|
quitApplication();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void TrayController::showPetWindow()
|
||||||
|
{
|
||||||
|
if (m_petWindow == nullptr)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_petWindow->show();
|
||||||
|
m_petWindow->raise();
|
||||||
|
m_petWindow->resumeAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TrayController::hidePetWindow()
|
||||||
|
{
|
||||||
|
if (m_petWindow == nullptr)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_petWindow->pauseAnimation();
|
||||||
|
m_petWindow->hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TrayController::togglePetWindowVisibility()
|
||||||
|
{
|
||||||
|
if (m_petWindow == nullptr)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_petWindow->isVisible())
|
||||||
|
{
|
||||||
|
hidePetWindow();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showPetWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TrayController::quitApplication()
|
||||||
|
{
|
||||||
|
if (m_petWindow != nullptr)
|
||||||
|
{
|
||||||
|
m_petWindow->pauseAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
QApplication::quit();
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QMenu>
|
||||||
|
#include <QSystemTrayIcon>
|
||||||
|
|
||||||
|
class PetWindow;
|
||||||
|
|
||||||
|
class TrayController
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit TrayController(PetWindow *petWindow);
|
||||||
|
|
||||||
|
bool isAvailable() const;
|
||||||
|
void show();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void createMenu();
|
||||||
|
void showPetWindow();
|
||||||
|
void hidePetWindow();
|
||||||
|
void togglePetWindowVisibility();
|
||||||
|
void quitApplication();
|
||||||
|
|
||||||
|
PetWindow *m_petWindow = nullptr;
|
||||||
|
QSystemTrayIcon m_trayIcon;
|
||||||
|
QMenu m_menu;
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#include "PetView.h"
|
||||||
|
|
||||||
|
PetView::PetView(QWidget *parent)
|
||||||
|
: QLabel(parent)
|
||||||
|
{
|
||||||
|
setAlignment(Qt::AlignCenter);
|
||||||
|
setAttribute(Qt::WA_TranslucentBackground);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PetView::setFrame(const QPixmap &pixmap)
|
||||||
|
{
|
||||||
|
clear();
|
||||||
|
setPixmap(pixmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PetView::showFallbackText(const QString &text)
|
||||||
|
{
|
||||||
|
clear();
|
||||||
|
setText(text);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QPixmap>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
class PetView : public QLabel
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit PetView(QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
void setFrame(const QPixmap &pixmap);
|
||||||
|
void showFallbackText(const QString &text);
|
||||||
|
};
|
||||||
+139
-76
@@ -1,17 +1,20 @@
|
|||||||
#include "PetWindow.h"
|
#include "PetWindow.h"
|
||||||
|
|
||||||
#include "../character/CharacterPackageLoader.h"
|
#include "../character/CharacterPackageLoader.h"
|
||||||
|
#include "../util/Logger.h"
|
||||||
|
#include "PetView.h"
|
||||||
|
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
#include <QContextMenuEvent>
|
#include <QContextMenuEvent>
|
||||||
#include <QCursor>
|
#include <QCursor>
|
||||||
#include <QFileInfo>
|
|
||||||
#include <QGuiApplication>
|
#include <QGuiApplication>
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
#include <QMouseEvent>
|
#include <QMouseEvent>
|
||||||
#include <QPixmap>
|
#include <QPixmap>
|
||||||
#include <QRandomGenerator>
|
#include <QRandomGenerator>
|
||||||
#include <QScreen>
|
#include <QScreen>
|
||||||
|
#include <QSet>
|
||||||
|
#include <QStringList>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
@@ -29,24 +32,26 @@ QString previewImagePath()
|
|||||||
|
|
||||||
PetWindow::PetWindow(QWidget *parent)
|
PetWindow::PetWindow(QWidget *parent)
|
||||||
: QWidget(parent)
|
: QWidget(parent)
|
||||||
, m_imageLabel(new QLabel(this))
|
, m_petView(new PetView(this))
|
||||||
, m_currentFrameIndex(0)
|
|
||||||
, m_dragging(false)
|
, m_dragging(false)
|
||||||
, m_alwaysOnTop(true)
|
, m_alwaysOnTop(true)
|
||||||
|
, m_centerNextFrame(false)
|
||||||
{
|
{
|
||||||
setAttribute(Qt::WA_TranslucentBackground);
|
setAttribute(Qt::WA_TranslucentBackground);
|
||||||
setWindowFlags(Qt::FramelessWindowHint | Qt::Tool | Qt::WindowStaysOnTopHint);
|
setWindowFlags(Qt::FramelessWindowHint | Qt::Tool | Qt::WindowStaysOnTopHint);
|
||||||
setMouseTracking(true);
|
setMouseTracking(true);
|
||||||
|
|
||||||
m_imageLabel->setAlignment(Qt::AlignCenter);
|
|
||||||
m_imageLabel->setAttribute(Qt::WA_TranslucentBackground);
|
|
||||||
|
|
||||||
auto *layout = new QVBoxLayout(this);
|
auto *layout = new QVBoxLayout(this);
|
||||||
layout->setContentsMargins(0, 0, 0, 0);
|
layout->setContentsMargins(0, 0, 0, 0);
|
||||||
layout->addWidget(m_imageLabel);
|
layout->addWidget(m_petView);
|
||||||
|
|
||||||
connect(&m_animationTimer, &QTimer::timeout, this, [this]() {
|
m_frameAnimator.setFrameChangedCallback([this](const QPixmap &pixmap) {
|
||||||
advanceStateFrame();
|
setDisplayPixmap(pixmap, m_centerNextFrame);
|
||||||
|
m_centerNextFrame = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
m_frameAnimator.setClipFinishedCallback([this](const QString &nextState) {
|
||||||
|
playResolvedState(m_stateMachine.finishState(nextState), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
m_idleBehaviorTimer.setSingleShot(true);
|
m_idleBehaviorTimer.setSingleShot(true);
|
||||||
@@ -62,6 +67,49 @@ PetWindow::PetWindow(QWidget *parent)
|
|||||||
loadInitialImage();
|
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)
|
void PetWindow::contextMenuEvent(QContextMenuEvent *event)
|
||||||
{
|
{
|
||||||
QMenu menu(this);
|
QMenu menu(this);
|
||||||
@@ -108,7 +156,7 @@ void PetWindow::mousePressEvent(QMouseEvent *event)
|
|||||||
{
|
{
|
||||||
m_dragging = true;
|
m_dragging = true;
|
||||||
m_dragOffset = event->globalPosition().toPoint() - frameGeometry().topLeft();
|
m_dragOffset = event->globalPosition().toPoint() - frameGeometry().topLeft();
|
||||||
playState(QStringLiteral("drag"), false);
|
playResolvedState(m_stateMachine.beginDrag(), false);
|
||||||
event->accept();
|
event->accept();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -121,7 +169,7 @@ void PetWindow::mouseReleaseEvent(QMouseEvent *event)
|
|||||||
if (event->button() == Qt::LeftButton)
|
if (event->button() == Qt::LeftButton)
|
||||||
{
|
{
|
||||||
m_dragging = false;
|
m_dragging = false;
|
||||||
playState(QStringLiteral("idle"), false);
|
playResolvedState(m_stateMachine.endDrag(), false);
|
||||||
event->accept();
|
event->accept();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -133,15 +181,43 @@ void PetWindow::loadInitialImage()
|
|||||||
{
|
{
|
||||||
QString loadError;
|
QString loadError;
|
||||||
m_characterPackage = CharacterPackageLoader::load(characterPackagePath(), &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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setDisplayImage(previewImagePath(), true);
|
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)
|
void PetWindow::addStateTestActions(QMenu *menu)
|
||||||
{
|
{
|
||||||
QMenu *stateMenu = menu->addMenu(QStringLiteral("State Test"));
|
QMenu *stateMenu = menu->addMenu(QStringLiteral("State Test"));
|
||||||
@@ -157,7 +233,7 @@ void PetWindow::addStateTestActions(QMenu *menu)
|
|||||||
|
|
||||||
for (const QString &stateName : stateNames)
|
for (const QString &stateName : stateNames)
|
||||||
{
|
{
|
||||||
if (!m_characterPackage.hasState(stateName))
|
if (!m_clips.contains(stateName))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -169,84 +245,48 @@ void PetWindow::addStateTestActions(QMenu *menu)
|
|||||||
stateMenu->setEnabled(!stateMenu->actions().isEmpty());
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CharacterState *state = m_characterPackage.state(stateName);
|
if (m_frameAnimator.currentStateName() == stateName && m_frameAnimator.isPlaying())
|
||||||
if (state == nullptr || state->framePaths.isEmpty() || state->fps <= 0)
|
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto clipIterator = m_clips.constFind(stateName);
|
||||||
|
if (clipIterator == m_clips.constEnd())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnimationClip *clip = &clipIterator.value();
|
||||||
m_idleBehaviorTimer.stop();
|
m_idleBehaviorTimer.stop();
|
||||||
m_behaviorReturnTimer.stop();
|
m_behaviorReturnTimer.stop();
|
||||||
m_animationTimer.stop();
|
m_centerNextFrame = centerWindow;
|
||||||
m_currentStateName = stateName;
|
m_frameAnimator.play(clip);
|
||||||
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);
|
|
||||||
|
|
||||||
if (stateName == QStringLiteral("idle"))
|
if (stateName == QStringLiteral("idle"))
|
||||||
{
|
{
|
||||||
scheduleIdleBehavior();
|
scheduleIdleBehavior();
|
||||||
}
|
}
|
||||||
else if (autoReturn && state->loop)
|
else if (clip->loop)
|
||||||
{
|
{
|
||||||
m_behaviorReturnTimer.start(4000);
|
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()
|
void PetWindow::scheduleIdleBehavior()
|
||||||
{
|
{
|
||||||
if (!m_characterPackage.hasState(QStringLiteral("idle")))
|
if (!m_clips.contains(QStringLiteral("idle")))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -257,7 +297,7 @@ void PetWindow::scheduleIdleBehavior()
|
|||||||
|
|
||||||
void PetWindow::playIdleBehavior()
|
void PetWindow::playIdleBehavior()
|
||||||
{
|
{
|
||||||
if (m_dragging || m_currentStateName != QStringLiteral("idle"))
|
if (m_dragging || m_stateMachine.currentState() != QStringLiteral("idle"))
|
||||||
{
|
{
|
||||||
scheduleIdleBehavior();
|
scheduleIdleBehavior();
|
||||||
return;
|
return;
|
||||||
@@ -272,7 +312,7 @@ void PetWindow::playIdleBehavior()
|
|||||||
|
|
||||||
for (const QString &stateName : preferredStates)
|
for (const QString &stateName : preferredStates)
|
||||||
{
|
{
|
||||||
if (m_characterPackage.hasState(stateName))
|
if (m_clips.contains(stateName))
|
||||||
{
|
{
|
||||||
candidateStates.append(stateName);
|
candidateStates.append(stateName);
|
||||||
}
|
}
|
||||||
@@ -285,14 +325,14 @@ void PetWindow::playIdleBehavior()
|
|||||||
}
|
}
|
||||||
|
|
||||||
const int stateIndex = QRandomGenerator::global()->bounded(candidateStates.size());
|
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()
|
void PetWindow::returnToIdleFromBehavior()
|
||||||
{
|
{
|
||||||
if (!m_dragging)
|
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);
|
QPixmap pixmap(imagePath);
|
||||||
if (pixmap.isNull())
|
if (pixmap.isNull())
|
||||||
{
|
{
|
||||||
m_imageLabel->setText(QStringLiteral("QtDesktopPet"));
|
m_petView->showFallbackText(QStringLiteral("QtDesktopPet"));
|
||||||
resize(240, 160);
|
resize(240, 160);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QSize targetSize(320, 320);
|
const QSize targetSize(320, 320);
|
||||||
const QPixmap scaled = pixmap.scaled(targetSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
const QPixmap scaled = pixmap.scaled(targetSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||||
m_imageLabel->setPixmap(scaled);
|
setDisplayPixmap(scaled, centerWindow);
|
||||||
resize(scaled.size());
|
}
|
||||||
|
|
||||||
|
void PetWindow::setDisplayPixmap(const QPixmap &pixmap, bool centerWindow)
|
||||||
|
{
|
||||||
|
m_petView->setFrame(pixmap);
|
||||||
|
resize(pixmap.size());
|
||||||
|
|
||||||
if (centerWindow)
|
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)
|
void PetWindow::setAlwaysOnTop(bool enabled)
|
||||||
{
|
{
|
||||||
m_alwaysOnTop = enabled;
|
m_alwaysOnTop = enabled;
|
||||||
|
const bool wasVisible = isVisible();
|
||||||
|
|
||||||
Qt::WindowFlags flags = windowFlags();
|
Qt::WindowFlags flags = windowFlags();
|
||||||
if (enabled)
|
if (enabled)
|
||||||
@@ -336,5 +396,8 @@ void PetWindow::setAlwaysOnTop(bool enabled)
|
|||||||
}
|
}
|
||||||
|
|
||||||
setWindowFlags(flags);
|
setWindowFlags(flags);
|
||||||
|
if (wasVisible)
|
||||||
|
{
|
||||||
show();
|
show();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-9
@@ -1,20 +1,30 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "../character/AnimationClip.h"
|
||||||
#include "../character/CharacterPackage.h"
|
#include "../character/CharacterPackage.h"
|
||||||
|
#include "../character/FrameAnimator.h"
|
||||||
|
#include "../config/AppConfig.h"
|
||||||
|
#include "../state/PetStateMachine.h"
|
||||||
|
|
||||||
#include <QLabel>
|
#include <QMap>
|
||||||
#include <QPoint>
|
#include <QPoint>
|
||||||
#include <QStringList>
|
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
class QMenu;
|
class QMenu;
|
||||||
|
class QPixmap;
|
||||||
|
class PetView;
|
||||||
|
|
||||||
class PetWindow : public QWidget
|
class PetWindow : public QWidget
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
explicit PetWindow(QWidget *parent = nullptr);
|
explicit PetWindow(QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
void applyAppConfig(const AppConfig &config);
|
||||||
|
AppConfig currentAppConfig() const;
|
||||||
|
void pauseAnimation();
|
||||||
|
void resumeAnimation();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void contextMenuEvent(QContextMenuEvent *event) override;
|
void contextMenuEvent(QContextMenuEvent *event) override;
|
||||||
void mouseMoveEvent(QMouseEvent *event) override;
|
void mouseMoveEvent(QMouseEvent *event) override;
|
||||||
@@ -23,24 +33,28 @@ protected:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
void loadInitialImage();
|
void loadInitialImage();
|
||||||
|
void buildAnimationClips();
|
||||||
void addStateTestActions(QMenu *menu);
|
void addStateTestActions(QMenu *menu);
|
||||||
void playState(const QString &stateName, bool centerWindow, bool autoReturn = false);
|
void playState(const QString &stateName, bool centerWindow);
|
||||||
void advanceStateFrame();
|
void playResolvedState(const QString &stateName, bool centerWindow);
|
||||||
void scheduleIdleBehavior();
|
void scheduleIdleBehavior();
|
||||||
void playIdleBehavior();
|
void playIdleBehavior();
|
||||||
void returnToIdleFromBehavior();
|
void returnToIdleFromBehavior();
|
||||||
void setDisplayImage(const QString &imagePath, bool centerWindow);
|
void setDisplayImage(const QString &imagePath, bool centerWindow);
|
||||||
|
void setDisplayPixmap(const QPixmap &pixmap, bool centerWindow);
|
||||||
|
bool isPointVisibleOnScreen(const QPoint &point) const;
|
||||||
void setAlwaysOnTop(bool enabled);
|
void setAlwaysOnTop(bool enabled);
|
||||||
|
|
||||||
QLabel *m_imageLabel;
|
PetView *m_petView;
|
||||||
QTimer m_animationTimer;
|
|
||||||
QTimer m_idleBehaviorTimer;
|
QTimer m_idleBehaviorTimer;
|
||||||
QTimer m_behaviorReturnTimer;
|
QTimer m_behaviorReturnTimer;
|
||||||
CharacterPackage m_characterPackage;
|
CharacterPackage m_characterPackage;
|
||||||
QString m_currentStateName;
|
QMap<QString, AnimationClip> m_clips;
|
||||||
QStringList m_currentFrames;
|
FrameAnimator m_frameAnimator;
|
||||||
|
PetStateMachine m_stateMachine;
|
||||||
QPoint m_dragOffset;
|
QPoint m_dragOffset;
|
||||||
int m_currentFrameIndex;
|
|
||||||
bool m_dragging;
|
bool m_dragging;
|
||||||
bool m_alwaysOnTop;
|
bool m_alwaysOnTop;
|
||||||
|
bool m_centerNextFrame;
|
||||||
|
bool m_returnToIdleAfterResume = false;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
#include "Logger.h"
|
||||||
|
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
#include <QTextStream>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
const QString LogFileName = QStringLiteral("QtDesktopPet.log");
|
||||||
|
constexpr qint64 MaxLogFileSize = 2 * 1024 * 1024;
|
||||||
|
constexpr int MaxRotatedLogFiles = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Logger::info(const QString &message)
|
||||||
|
{
|
||||||
|
write(QStringLiteral("INFO"), message);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Logger::warning(const QString &message)
|
||||||
|
{
|
||||||
|
write(QStringLiteral("WARN"), message);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Logger::error(const QString &message)
|
||||||
|
{
|
||||||
|
write(QStringLiteral("ERROR"), message);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Logger::write(const QString &level, const QString &message)
|
||||||
|
{
|
||||||
|
QDir directory(logDirectoryPath());
|
||||||
|
if (!directory.exists() && !directory.mkpath(QStringLiteral(".")))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rotateIfNeeded();
|
||||||
|
|
||||||
|
QFile file(logFilePath());
|
||||||
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTextStream stream(&file);
|
||||||
|
stream << QDateTime::currentDateTime().toString(Qt::ISODate)
|
||||||
|
<< " [" << level << "] "
|
||||||
|
<< message
|
||||||
|
<< '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Logger::logDirectoryPath()
|
||||||
|
{
|
||||||
|
const QString configPath = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation);
|
||||||
|
if (!configPath.isEmpty())
|
||||||
|
{
|
||||||
|
return QDir(configPath).filePath(QStringLiteral("logs"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return QDir(QDir::currentPath()).filePath(QStringLiteral("logs"));
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Logger::logFilePath()
|
||||||
|
{
|
||||||
|
return QDir(logDirectoryPath()).filePath(LogFileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Logger::rotateIfNeeded()
|
||||||
|
{
|
||||||
|
const QString currentPath = logFilePath();
|
||||||
|
QFile currentFile(currentPath);
|
||||||
|
if (!currentFile.exists() || currentFile.size() < MaxLogFileSize)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int index = MaxRotatedLogFiles; index >= 1; --index)
|
||||||
|
{
|
||||||
|
const QString oldPath = currentPath + QStringLiteral(".") + QString::number(index);
|
||||||
|
const QString nextPath = currentPath + QStringLiteral(".") + QString::number(index + 1);
|
||||||
|
|
||||||
|
if (!QFile::exists(oldPath))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index == MaxRotatedLogFiles)
|
||||||
|
{
|
||||||
|
QFile::remove(oldPath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
QFile::remove(nextPath);
|
||||||
|
QFile::rename(oldPath, nextPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QFile::remove(currentPath + QStringLiteral(".1"));
|
||||||
|
QFile::rename(currentPath, currentPath + QStringLiteral(".1"));
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
class Logger
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static void info(const QString &message);
|
||||||
|
static void warning(const QString &message);
|
||||||
|
static void error(const QString &message);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static void write(const QString &level, const QString &message);
|
||||||
|
static QString logDirectoryPath();
|
||||||
|
static QString logFilePath();
|
||||||
|
static void rotateIfNeeded();
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user