Files
Qt_DesktopPet/src/ui/PetWindow.cpp
T

2596 lines
78 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "PetWindow.h"
#include "../ai/AIProviderFactory.h"
#include "../ai/ConversationManager.h"
#include "../ai/ConversationStore.h"
#include "../assistant/CommandDispatcher.h"
#include "../character/CharacterPackageLoader.h"
#include "../character/CharacterPackageRepository.h"
#include "../config/ConfigManager.h"
#include "../fileops/FileOperationManager.h"
#include "../launcher/AppLaunchManager.h"
#include "../launcher/AppLaunchStore.h"
#include "../notification/NotificationDispatcher.h"
#include "../reminder/ReminderCommandHandler.h"
#include "../reminder/ReminderManager.h"
#include "../reminder/ReminderSoundPlayer.h"
#include "../reminder/ReminderSoundRepository.h"
#include "../system/StartupManager.h"
#include "../util/Logger.h"
#include "../web/WebCapabilityDetector.h"
#include "../web/WebChatManager.h"
#include "../web/WebStore.h"
#include "../weather/WeatherManager.h"
#include "../weather/WeatherStore.h"
#include "ChatBubble.h"
#include "ChatHistoryPanel.h"
#include "ChatInputDialog.h"
#include "PetView.h"
#include "SettingsDialog.h"
#include <QAction>
#include <QApplication>
#include <QContextMenuEvent>
#include <QCursor>
#include <QDialog>
#include <QDialogButtonBox>
#include <QFileDialog>
#include <QFileInfo>
#include <QFont>
#include <QFrame>
#include <QGuiApplication>
#include <QHideEvent>
#include <QInputDialog>
#include <QLabel>
#include <QList>
#include <QLineEdit>
#include <QMenu>
#include <QMessageBox>
#include <QMouseEvent>
#include <QPixmap>
#include <QPointF>
#include <QPointer>
#include <QPushButton>
#include <QCheckBox>
#include <QRandomGenerator>
#include <QScreen>
#include <QSet>
#include <QShowEvent>
#include <QStringList>
#include <QVBoxLayout>
#include <QtGlobal>
#include <algorithm>
#include <memory>
#include <utility>
namespace
{
constexpr int MaxUserMessageLength = 4000;
constexpr int ChatInputLowerOffsetY = 48;
constexpr int StreamBubbleUpdateIntervalMs = 80;
constexpr int MinAnimationTargetSide = 32;
constexpr int MaxAnimationTargetSide = 2048;
constexpr int LowPowerFpsCap = 6;
constexpr int ChatFinishedReturnDelayMs = 1500;
constexpr int StandardPrewarmIntervalMs = 800;
constexpr int LowPowerPrewarmIntervalMs = 1500;
constexpr qint64 BytesPerMegabyte = 1024 * 1024;
int boundedAnimationTargetSide(double sideLength)
{
const double boundedSideLength = qBound(
static_cast<double>(MinAnimationTargetSide),
sideLength,
static_cast<double>(MaxAnimationTargetSide));
return qRound(boundedSideLength);
}
int evenBoundedHistoryLimit(int value, int minimum, int maximum)
{
const int boundedValue = qBound(minimum, value, maximum);
return boundedValue - (boundedValue % 2);
}
QString megabytesText(qint64 bytes)
{
return QString::number(static_cast<double>(bytes) / static_cast<double>(BytesPerMegabyte), 'f', 1);
}
AppConfig normalizedAppConfig(AppConfig config)
{
config.launchAtStartup = StartupManager::isLaunchAtStartupEnabled();
config.scale = qBound(0.5, config.scale, 2.0);
if (config.performanceMode != QStringLiteral("standard")
&& config.performanceMode != QStringLiteral("low-power"))
{
config.performanceMode = QStringLiteral("standard");
}
config.animationCacheLimitMb = qBound(64, config.animationCacheLimitMb, 1024);
config.requestContextMessageLimit = qBound(0, config.requestContextMessageLimit, 200);
config.memoryHistoryMessageLimit = evenBoundedHistoryLimit(config.memoryHistoryMessageLimit, 2, 5000);
config.savedHistoryMessageLimit = evenBoundedHistoryLimit(config.savedHistoryMessageLimit, 2, 10000);
config.characterId = config.characterId.trimmed();
if (!CharacterPackageRepository::hasPackage(config.characterId))
{
config.characterId = CharacterPackageRepository::defaultCharacterId();
}
config.reminderSoundId = ReminderSoundRepository::soundInfo(config.reminderSoundId).id;
config.reminderSoundVolume = qBound(0.0, config.reminderSoundVolume, 1.0);
return config;
}
QScreen *screenForPopup(const QWidget *reference)
{
if (reference != nullptr)
{
if (QScreen *screen = QGuiApplication::screenAt(reference->frameGeometry().center()))
{
return screen;
}
}
if (QScreen *screen = QGuiApplication::screenAt(QCursor::pos()))
{
return screen;
}
return QGuiApplication::primaryScreen();
}
void centerDialogOnScreen(QDialog *dialog, const QWidget *reference)
{
if (dialog == nullptr)
{
return;
}
QScreen *screen = screenForPopup(reference);
if (screen == nullptr)
{
return;
}
const QRect availableGeometry = screen->availableGeometry();
const QSize dialogSize = dialog->size().isValid()
? dialog->size()
: dialog->sizeHint();
const int x = availableGeometry.left() + qMax(0, (availableGeometry.width() - dialogSize.width()) / 2);
const int y = availableGeometry.top() + qMax(0, (availableGeometry.height() - dialogSize.height()) / 2);
dialog->move(QPoint(x, y));
}
QString userVisibleErrorMessage(const ChatResponse &response)
{
QString message = response.errorMessage.trimmed();
if (message.isEmpty())
{
message = QStringLiteral("未知错误。");
}
if (response.httpStatus > 0)
{
message = QStringLiteral("HTTP ") + QString::number(response.httpStatus) + QStringLiteral("") + message;
}
return message;
}
QString webCitationListText(const QVector<WebCitation> &citations)
{
QStringList lines;
const int count = qMin(citations.size(), 10);
for (int index = 0; index < count; ++index)
{
const WebCitation &citation = citations.at(index);
lines.append(QStringLiteral("[%1] %2\n%3")
.arg(index + 1)
.arg(citation.title.trimmed().isEmpty() ? QStringLiteral("来源") : citation.title.trimmed())
.arg(citation.url));
}
return lines.join(QStringLiteral("\n"));
}
}
PetWindow::PetWindow(QWidget *parent)
: QWidget(parent)
, m_chatBubble(std::make_unique<ChatBubble>())
, m_chatHistoryPanel(std::make_unique<ChatHistoryPanel>(this))
, m_chatInputDialog(std::make_unique<ChatInputDialog>(MaxUserMessageLength, this))
, m_conversationManager(std::make_unique<ConversationManager>())
, m_conversationStore(std::make_unique<ConversationStore>(ConfigManager().conversationHistoryPath()))
, m_fileOperationManager(std::make_unique<FileOperationManager>())
, m_appLaunchManager(std::make_unique<AppLaunchManager>())
, m_notificationDispatcher(std::make_unique<NotificationDispatcher>())
, m_reminderManager(std::make_unique<ReminderManager>())
, m_reminderSoundPlayer(std::make_unique<ReminderSoundPlayer>())
, m_webChatManager(std::make_unique<WebChatManager>())
, m_weatherManager(std::make_unique<WeatherManager>())
, 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);
auto *layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
layout->addWidget(m_petView);
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);
connect(&m_idleBehaviorTimer, &QTimer::timeout, this, [this]() {
playIdleBehavior();
});
m_behaviorReturnTimer.setSingleShot(true);
connect(&m_behaviorReturnTimer, &QTimer::timeout, this, [this]() {
returnToIdleFromBehavior();
});
m_streamBubbleUpdateTimer.setSingleShot(true);
connect(&m_streamBubbleUpdateTimer, &QTimer::timeout, this, [this]() {
flushStreamingBubble(false);
});
m_animationPrewarmTimer.setSingleShot(true);
connect(&m_animationPrewarmTimer, &QTimer::timeout, this, [this]() {
processAnimationPrewarm();
});
QPointer<PetWindow> window(this);
refreshChatInputWebToggle();
m_chatInputDialog->setSubmitCallback([window](const QString &message, bool webEnabled) {
return !window.isNull() && window->submitChatMessage(message, webEnabled);
});
m_reminderManager->setTriggeredCallback([window](const ReminderItem &item) {
if (!window.isNull())
{
window->handleTriggeredReminder(item);
}
});
loadInitialImage();
}
PetWindow::~PetWindow()
{
saveConversationHistoryIfNeeded();
}
void PetWindow::applyAppConfig(const AppConfig &config)
{
const AppConfig normalizedConfig = normalizedAppConfig(config);
const bool characterChanged = m_appConfig.characterId != normalizedConfig.characterId;
const bool rebuildClips = !qFuzzyCompare(m_appConfig.scale, normalizedConfig.scale)
|| m_appConfig.performanceMode != normalizedConfig.performanceMode
|| m_appConfig.enableLazyLoad != normalizedConfig.enableLazyLoad;
const bool animationCachePolicyChanged =
m_appConfig.enableAnimationPrewarm != normalizedConfig.enableAnimationPrewarm
|| m_appConfig.animationCacheLimitMb != normalizedConfig.animationCacheLimitMb
|| m_appConfig.unloadAnimationsWhenHidden != normalizedConfig.unloadAnimationsWhenHidden;
const bool loadPersistedHistory = !m_appConfig.saveConversationHistory
&& normalizedConfig.saveConversationHistory;
m_appConfig = normalizedConfig;
setAlwaysOnTop(m_appConfig.alwaysOnTop);
configureConversation(loadPersistedHistory);
if (m_appConfig.hasWindowPosition && isPointVisibleOnScreen(m_appConfig.windowPosition))
{
move(m_appConfig.windowPosition);
}
if (characterChanged)
{
m_frameAnimator.stop();
loadCharacterPackage(m_appConfig.characterId, false);
}
else if (rebuildClips && !m_characterPackage.states.isEmpty())
{
const QString previousState = m_stateMachine.currentState().isEmpty()
? QStringLiteral("idle")
: m_stateMachine.currentState();
m_frameAnimator.stop();
buildAnimationClips();
const QString nextState = m_clips.contains(previousState)
? previousState
: QStringLiteral("idle");
if (m_clips.contains(nextState))
{
playResolvedState(m_stateMachine.requestState(nextState), false);
}
}
if (isAnimationCacheManagementEnabled())
{
if (animationCachePolicyChanged && !rebuildClips)
{
m_animationPrewarmAttemptedStates.clear();
m_animationPrewarmQueue.clear();
trimAnimationCache(QStringLiteral("config updated"));
}
scheduleAnimationPrewarm();
}
else
{
stopAnimationPrewarm();
}
}
AppConfig PetWindow::currentAppConfig() const
{
AppConfig config = m_appConfig;
config.windowPosition = pos();
config.hasWindowPosition = true;
config.alwaysOnTop = m_alwaysOnTop;
config.launchAtStartup = StartupManager::isLaunchAtStartupEnabled();
return config;
}
void PetWindow::pauseAnimation()
{
if (!m_appConfig.pauseWhenHidden)
{
return;
}
m_returnToIdleAfterResume = m_behaviorReturnTimer.isActive();
m_idleBehaviorTimer.stop();
m_behaviorReturnTimer.stop();
stopAnimationPrewarm();
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;
scheduleAnimationPrewarm();
}
void PetWindow::showBubbleMessage(const QString &message)
{
hideReminderActions();
m_chatBubble->showMessage(message, bubbleAnchorPosition());
}
void PetWindow::openSettingsDialog()
{
ConfigManager configManager;
WeatherStore weatherStore;
WebStore webStore;
AppLaunchStore appLaunchStore;
QString weatherConfigError;
const WeatherConfig weatherConfig = weatherStore.load(&weatherConfigError);
if (!weatherConfigError.isEmpty())
{
Logger::warning(QStringLiteral("Weather config load warning: ") + weatherConfigError);
}
QString webConfigError;
const WebConfig webConfig = webStore.load(&webConfigError);
if (!webConfigError.isEmpty())
{
Logger::warning(QStringLiteral("Web config load warning: ") + webConfigError);
}
QString appLaunchConfigError;
const AppLaunchConfig appLaunchConfig = appLaunchStore.load(&appLaunchConfigError);
if (!appLaunchConfigError.isEmpty())
{
Logger::warning(QStringLiteral("Launcher config load warning: ") + appLaunchConfigError);
}
SettingsDialog dialog(
configManager.loadAIConfigStore(),
currentAppConfig(),
weatherConfig,
webConfig,
appLaunchConfig,
m_conversationManager ? m_conversationManager->history() : QVector<ChatMessage>(),
m_reminderManager ? m_reminderManager->allReminders() : QVector<ReminderItem>(),
[this]() {
return isManualStateSwitchLocked();
},
[this]() {
clearConversation();
},
[this](const QString &reminderId, QString *errorMessage) {
return m_reminderManager && m_reminderManager->cancelReminder(reminderId, errorMessage);
},
[this](const QString &reminderId, const QString &title, const QDateTime &remindAt, const ReminderRecurrence &recurrence, ReminderItem *updatedItem, QString *errorMessage) {
if (!m_reminderManager)
{
if (errorMessage != nullptr)
{
*errorMessage = QStringLiteral("提醒功能初始化失败。");
}
return false;
}
if (!m_reminderManager->updateReminder(reminderId, title, remindAt, recurrence, errorMessage))
{
return false;
}
if (updatedItem != nullptr)
{
const QVector<ReminderItem> reminders = m_reminderManager->allReminders();
bool found = false;
for (const ReminderItem &item : reminders)
{
if (item.id == reminderId)
{
*updatedItem = item;
found = true;
break;
}
}
if (!found)
{
if (errorMessage != nullptr)
{
*errorMessage = QStringLiteral("提醒已更新,但没有找到更新后的记录。");
}
return false;
}
}
return true;
},
[this](QString *errorMessage) {
return m_reminderManager && m_reminderManager->pruneFinishedReminders(20, errorMessage);
},
[this](const QString &soundId, double volume) {
if (m_reminderSoundPlayer)
{
m_reminderSoundPlayer->play(soundId, volume);
}
},
this);
centerDialogOnScreen(&dialog, this);
if (dialog.exec() != QDialog::Accepted)
{
return;
}
if (!configManager.saveAIConfigStore(dialog.aiConfigStore()))
{
Logger::warning(QStringLiteral("Failed to save AI config from settings dialog."));
}
AppConfig acceptedAppConfig = dialog.appConfig();
QString startupError;
if (!StartupManager::setLaunchAtStartupEnabled(acceptedAppConfig.launchAtStartup, &startupError))
{
Logger::warning(QStringLiteral("Failed to update startup setting: ") + startupError);
acceptedAppConfig.launchAtStartup = StartupManager::isLaunchAtStartupEnabled();
QMessageBox::warning(
this,
QStringLiteral("开机自启动"),
startupError.isEmpty() ? QStringLiteral("开机自启动设置保存失败。") : startupError);
}
applyAppConfig(acceptedAppConfig);
if (!configManager.saveAppConfig(currentAppConfig()))
{
Logger::warning(QStringLiteral("Failed to save app config from settings dialog."));
}
QString saveWeatherConfigError;
if (!weatherStore.save(dialog.weatherConfig(), &saveWeatherConfigError))
{
Logger::warning(QStringLiteral("Failed to save weather config from settings dialog: ") + saveWeatherConfigError);
}
QString saveWebConfigError;
if (!webStore.save(dialog.webConfig(), &saveWebConfigError))
{
Logger::warning(QStringLiteral("Failed to save web config from settings dialog: ") + saveWebConfigError);
}
refreshChatInputWebToggle();
QString saveLauncherConfigError;
if (!appLaunchStore.save(dialog.appLaunchConfig(), &saveLauncherConfigError))
{
Logger::warning(QStringLiteral("Failed to save launcher config from settings dialog: ") + saveLauncherConfigError);
}
}
void PetWindow::activateFromExternalInstance()
{
if (!isVisible())
{
show();
resumeAnimation();
}
if (isMinimized())
{
setWindowState(windowState() & ~Qt::WindowMinimized);
}
if (QWidget *modalWidget = QApplication::activeModalWidget())
{
modalWidget->raise();
modalWidget->activateWindow();
return;
}
raise();
activateWindow();
if (m_reminderManager)
{
m_reminderManager->checkDueRemindersNow();
}
updateBubblePosition();
}
void PetWindow::setSettingsFallbackInContextMenuEnabled(bool enabled)
{
m_settingsFallbackInContextMenuEnabled = enabled;
}
void PetWindow::setTrayNotificationCallback(std::function<bool(const QString &, const QString &)> callback)
{
if (m_notificationDispatcher)
{
m_notificationDispatcher->setShowCallback(std::move(callback));
}
}
void PetWindow::contextMenuEvent(QContextMenuEvent *event)
{
resetBubbleAutoHideTimer();
QMenu menu(this);
QAction *topAction = menu.addAction(QStringLiteral("取消置顶"));
topAction->setCheckable(true);
topAction->setChecked(m_alwaysOnTop);
const bool aiRequestRunning = hasActiveAIRequest();
QAction *chatAction = menu.addAction(QStringLiteral("聊天"));
chatAction->setEnabled(!aiRequestRunning);
QAction *showConversationAction = menu.addAction(QStringLiteral("显示对话"));
QAction *cancelAIAction = menu.addAction(QStringLiteral("取消当前请求"));
cancelAIAction->setEnabled(aiRequestRunning);
QAction *clearConversationAction = menu.addAction(QStringLiteral("清空对话"));
clearConversationAction->setEnabled(m_conversationManager && m_conversationManager->hasHistory());
QAction *settingsAction = nullptr;
if (m_settingsFallbackInContextMenuEnabled)
{
settingsAction = menu.addAction(QStringLiteral("设置"));
}
addStateTestActions(&menu);
menu.addSeparator();
QAction *exitAction = menu.addAction(QStringLiteral("退出"));
QAction *selectedAction = menu.exec(event->globalPos());
if (selectedAction == topAction)
{
setAlwaysOnTop(!m_alwaysOnTop);
}
else if (selectedAction == chatAction)
{
startChat();
}
else if (selectedAction == showConversationAction)
{
showConversationHistory();
}
else if (selectedAction == cancelAIAction)
{
cancelActiveAIRequest();
}
else if (selectedAction == clearConversationAction)
{
clearConversation();
}
else if (settingsAction != nullptr && selectedAction == settingsAction)
{
openSettingsDialog();
}
else if (selectedAction == exitAction)
{
close();
}
else if (selectedAction != nullptr && selectedAction->data().isValid())
{
if (isManualStateSwitchLocked())
{
return;
}
playState(selectedAction->data().toString(), false);
}
}
void PetWindow::startChat()
{
if (!m_chatInputDialog)
{
return;
}
refreshChatInputWebToggle();
m_chatInputDialog->showAt(chatInputAnchorPosition());
}
bool PetWindow::submitChatMessage(const QString &message, bool webEnabled)
{
const QString normalizedMessage = message.trimmed();
if (normalizedMessage.isEmpty())
{
return false;
}
if (normalizedMessage.size() > MaxUserMessageLength)
{
playState(QStringLiteral("error"), false);
showBubbleMessage(QStringLiteral("消息太长,请控制在 4000 字以内。"));
return false;
}
CommandDispatcher dispatcher;
const CommandDispatchResult result = dispatcher.dispatch(normalizedMessage);
if (result.action == CommandDispatchAction::Reminder)
{
return handleReminderChatMessage(result.message);
}
if (result.action == CommandDispatchAction::Weather)
{
return handleWeatherChatMessage(result.message);
}
if (result.action == CommandDispatchAction::FileOperation)
{
return handleFileOperationChatMessage(result.message);
}
if (result.action == CommandDispatchAction::LaunchApp)
{
return handleLaunchAppChatMessage(result.message);
}
if (result.action == CommandDispatchAction::UnsupportedTool)
{
playState(QStringLiteral("talk"), false);
showBubbleMessage(result.message);
return true;
}
saveWebTogglePreference(webEnabled);
return webEnabled ? submitWebChatMessage(result.message) : submitAiChatMessage(result.message);
}
bool PetWindow::handleReminderChatMessage(const QString &message)
{
if (!m_reminderManager)
{
playState(QStringLiteral("error"), false);
showBubbleMessage(QStringLiteral("提醒功能初始化失败。"));
return false;
}
const ReminderCommandResult result = ReminderCommandHandler::handle(
message,
*m_reminderManager);
playState(result.success ? QStringLiteral("talk") : QStringLiteral("error"), false);
showBubbleMessage(result.message);
return result.success;
}
bool PetWindow::handleWeatherChatMessage(const QString &message)
{
if (!m_weatherManager)
{
playState(QStringLiteral("error"), false);
showBubbleMessage(QStringLiteral("天气功能初始化失败。"));
return false;
}
if (hasActiveAIRequest() || m_streamingChatActive)
{
playState(QStringLiteral("talk"), false);
showBubbleMessage(QStringLiteral("当前请求正在进行,请稍后再查天气。"));
return false;
}
if (m_weatherManager->isBusy())
{
playState(QStringLiteral("talk"), false);
showBubbleMessage(QStringLiteral("天气查询正在进行,请稍后。"));
return false;
}
WeatherStore weatherStore;
QString weatherConfigError;
const WeatherConfig weatherConfig = weatherStore.load(&weatherConfigError);
if (!weatherConfigError.isEmpty())
{
Logger::warning(QStringLiteral("Weather config load warning: ") + weatherConfigError);
}
stopAnimationPrewarm();
playState(QStringLiteral("think"), false);
hideReminderActions();
showBubbleMessage(QStringLiteral("正在查询天气..."));
QPointer<PetWindow> window(this);
m_weatherManager->queryWeather(message, weatherConfig, [window](const WeatherQueryResult &result) {
if (window.isNull())
{
return;
}
if (result.success)
{
window->playState(QStringLiteral("talk"), false);
window->showBubbleMessage(result.message);
window->m_behaviorReturnTimer.start(ChatFinishedReturnDelayMs);
return;
}
window->playState(QStringLiteral("error"), false);
window->showBubbleMessage(result.errorMessage.isEmpty() ? QStringLiteral("天气查询失败。") : result.errorMessage);
window->m_behaviorReturnTimer.start(ChatFinishedReturnDelayMs);
});
return true;
}
bool PetWindow::handleFileOperationChatMessage(const QString &message)
{
if (!m_fileOperationManager)
{
playState(QStringLiteral("error"), false);
showBubbleMessage(QStringLiteral("文件操作功能初始化失败。"));
return false;
}
const QString text = message.trimmed();
const auto contains = [&text](const QString &keyword) {
return text.contains(keyword, Qt::CaseInsensitive);
};
if (contains(QStringLiteral("删除")) || contains(QStringLiteral("移动")) || contains(QStringLiteral("覆盖"))
|| contains(QStringLiteral("执行")) || contains(QStringLiteral("运行")) || contains(QStringLiteral("脚本")) || contains(QStringLiteral("命令")))
{
playState(QStringLiteral("error"), false);
showBubbleMessage(QStringLiteral("文件操作 v1 不支持删除、覆盖、移动、执行脚本或运行命令。"));
return false;
}
if (contains(QStringLiteral("截图")) || contains(QStringLiteral("保存到")))
{
playState(QStringLiteral("talk"), false);
showBubbleMessage(QStringLiteral("文件操作 v1 暂不支持截图或把当前内容保存到指定位置。"));
return false;
}
if (contains(QStringLiteral("打包")) || contains(QStringLiteral("压缩")) || contains(QStringLiteral("zip")))
{
playState(QStringLiteral("talk"), false);
showBubbleMessage(QStringLiteral("zip 打包需要额外稳定的压缩实现,本版暂不启用;可以先使用复制或备份。"));
return false;
}
const auto confirmPlan = [this](const FileOperationPlan &plan) {
QString messageText = plan.description;
if (!plan.warnings.isEmpty())
{
messageText += QStringLiteral("\n\n注意:\n") + plan.warnings.join(QLatin1Char('\n'));
}
messageText += QStringLiteral("\n\n请确认是否执行。");
return QMessageBox::warning(
this,
plan.title,
messageText,
QMessageBox::Yes | QMessageBox::Cancel,
QMessageBox::Cancel) == QMessageBox::Yes;
};
FileOperationResult operationResult;
if (contains(QStringLiteral("列出")) || contains(QStringLiteral("目录")) || contains(QStringLiteral("文件夹")))
{
const QString directoryPath = QFileDialog::getExistingDirectory(
this,
QStringLiteral("选择要列出的文件夹"),
QString(),
QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
if (directoryPath.isEmpty())
{
return false;
}
operationResult = m_fileOperationManager->executeListDirectory(m_fileOperationManager->listDirectoryPlan(directoryPath));
}
else if (contains(QStringLiteral("复制")))
{
const QString sourceFilePath = QFileDialog::getOpenFileName(this, QStringLiteral("选择要复制的文件"));
if (sourceFilePath.isEmpty())
{
return false;
}
const QString targetDirectoryPath = QFileDialog::getExistingDirectory(
this,
QStringLiteral("选择复制到的文件夹"),
QString(),
QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
if (targetDirectoryPath.isEmpty())
{
return false;
}
const FileOperationPlan plan = m_fileOperationManager->copyFilePlan(sourceFilePath, targetDirectoryPath);
if (!confirmPlan(plan))
{
return false;
}
operationResult = m_fileOperationManager->executeCopyFile(plan);
}
else if (contains(QStringLiteral("备份")))
{
const QString sourceFilePath = QFileDialog::getOpenFileName(this, QStringLiteral("选择要备份的文件"));
if (sourceFilePath.isEmpty())
{
return false;
}
const FileOperationPlan plan = m_fileOperationManager->backupFilePlan(sourceFilePath);
if (!confirmPlan(plan))
{
return false;
}
operationResult = m_fileOperationManager->executeBackupFile(plan);
}
else if (contains(QStringLiteral("重命名")))
{
const QString sourceFilePath = QFileDialog::getOpenFileName(this, QStringLiteral("选择要重命名的文件"));
if (sourceFilePath.isEmpty())
{
return false;
}
bool ok = false;
const QString newFileName = QInputDialog::getText(
this,
QStringLiteral("重命名文件"),
QStringLiteral("新文件名"),
QLineEdit::Normal,
QFileInfo(sourceFilePath).fileName(),
&ok).trimmed();
if (!ok || newFileName.isEmpty())
{
return false;
}
const FileOperationPlan plan = m_fileOperationManager->renameFilePlan(sourceFilePath, newFileName);
if (!confirmPlan(plan))
{
return false;
}
operationResult = m_fileOperationManager->executeRenameFile(plan);
}
else
{
const QString sourceFilePath = QFileDialog::getOpenFileName(
this,
QStringLiteral("选择要读取的文本文件"),
QString(),
QStringLiteral("Text Files (*.txt *.md *.markdown *.log *.json *.csv *.ini *.xml *.yaml *.yml);;All Files (*)"));
if (sourceFilePath.isEmpty())
{
return false;
}
operationResult = m_fileOperationManager->executeReadTextFile(m_fileOperationManager->readTextFilePlan(sourceFilePath));
}
if (!operationResult.success)
{
playState(QStringLiteral("error"), false);
showBubbleMessage(operationResult.errorMessage.isEmpty() ? QStringLiteral("文件操作失败。") : operationResult.errorMessage);
return false;
}
playState(QStringLiteral("talk"), false);
const QString output = operationResult.outputText.trimmed();
showBubbleMessage(output.isEmpty() ? operationResult.message : output);
return true;
}
bool PetWindow::handleLaunchAppChatMessage(const QString &message)
{
if (!m_appLaunchManager)
{
playState(QStringLiteral("error"), false);
showBubbleMessage(QStringLiteral("应用启动功能初始化失败。"));
return false;
}
AppLaunchStore store;
QString loadError;
AppLaunchConfig config = store.load(&loadError);
if (!loadError.isEmpty())
{
Logger::warning(QStringLiteral("Launcher config load warning: ") + loadError);
}
AppLaunchPlan plan = m_appLaunchManager->resolveLaunchPlan(message, config);
if (plan.needsManualSelection)
{
const QString executablePath = QFileDialog::getOpenFileName(
this,
QStringLiteral("选择要启动的应用"),
QString(),
QStringLiteral("Applications (*.exe)"));
if (executablePath.isEmpty())
{
return false;
}
plan = m_appLaunchManager->manualSelectionPlan(message, executablePath);
}
if (!plan.success)
{
playState(QStringLiteral("error"), false);
showBubbleMessage(plan.errorMessage.isEmpty() ? QStringLiteral("没有找到可启动的应用。") : plan.errorMessage);
return false;
}
QString confirmText = QStringLiteral("应用:%1\n来源:%2")
.arg(plan.displayName.isEmpty() ? plan.requestedName : plan.displayName,
plan.matchSource.isEmpty() ? QStringLiteral("未知") : plan.matchSource);
if (!plan.executablePath.trimmed().isEmpty())
{
confirmText += QStringLiteral("\n路径:") + plan.executablePath;
}
if (!plan.shortcutPath.trimmed().isEmpty())
{
confirmText += QStringLiteral("\n快捷方式:") + plan.shortcutPath;
}
if (!plan.workingDirectory.trimmed().isEmpty())
{
confirmText += QStringLiteral("\n工作目录:") + plan.workingDirectory;
}
QDialog confirmDialog(this);
confirmDialog.setWindowTitle(QStringLiteral("启动应用"));
confirmDialog.setModal(true);
auto *confirmLayout = new QVBoxLayout(&confirmDialog);
confirmLayout->setContentsMargins(18, 18, 18, 14);
confirmLayout->setSpacing(12);
auto *questionLabel = new QLabel(QStringLiteral("确认启动该应用?"), &confirmDialog);
QFont questionFont = questionLabel->font();
questionFont.setBold(true);
questionLabel->setFont(questionFont);
confirmLayout->addWidget(questionLabel);
auto *detailLabel = new QLabel(confirmText, &confirmDialog);
detailLabel->setWordWrap(true);
detailLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
detailLabel->setMinimumWidth(520);
confirmLayout->addWidget(detailLabel);
auto *rememberCheckBox = new QCheckBox(QStringLiteral("记住为此名称,下次直接匹配"), &confirmDialog);
rememberCheckBox->setVisible(plan.canRemember);
confirmLayout->addWidget(rememberCheckBox);
auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &confirmDialog);
if (QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok))
{
okButton->setText(QStringLiteral("启动"));
}
if (QPushButton *cancelButton = buttonBox->button(QDialogButtonBox::Cancel))
{
cancelButton->setText(QStringLiteral("取消"));
}
QObject::connect(buttonBox, &QDialogButtonBox::accepted, &confirmDialog, &QDialog::accept);
QObject::connect(buttonBox, &QDialogButtonBox::rejected, &confirmDialog, &QDialog::reject);
confirmLayout->addWidget(buttonBox);
if (confirmDialog.exec() != QDialog::Accepted)
{
return false;
}
const AppLaunchResult result = m_appLaunchManager->executeLaunchPlan(plan);
if (!result.success)
{
playState(QStringLiteral("error"), false);
showBubbleMessage(result.errorMessage.isEmpty() ? QStringLiteral("启动应用失败。") : result.errorMessage);
return false;
}
QString bubbleText = result.message;
if (plan.canRemember && rememberCheckBox->isChecked())
{
RegisteredApp app = m_appLaunchManager->registeredAppFromPlan(
plan,
{plan.requestedName, plan.displayName});
bool replaced = false;
for (RegisteredApp &existingApp : config.apps)
{
if (existingApp.id == app.id)
{
existingApp = app;
replaced = true;
break;
}
}
if (!replaced)
{
config.apps.append(app);
}
QString saveError;
if (!store.save(config, &saveError))
{
Logger::warning(QStringLiteral("Failed to save launcher config after manual app selection: ") + saveError);
bubbleText += QStringLiteral("\n但保存应用别名失败。");
}
}
playState(QStringLiteral("talk"), false);
showBubbleMessage(bubbleText.isEmpty() ? QStringLiteral("应用已启动。") : bubbleText);
return true;
}
bool PetWindow::submitWebChatMessage(const QString &message)
{
if (!m_webChatManager)
{
playState(QStringLiteral("error"), false);
showBubbleMessage(QStringLiteral("联网模式初始化失败。"));
return false;
}
if (hasActiveWebRequest())
{
playState(QStringLiteral("talk"), false);
showBubbleMessage(QStringLiteral("联网请求正在进行,请稍后。"));
return false;
}
if ((m_conversationManager && m_conversationManager->isBusy()) || m_streamingChatActive)
{
playState(QStringLiteral("talk"), false);
showBubbleMessage(QStringLiteral("AI 回复正在进行,请稍后再使用联网模式。"));
return false;
}
ConfigManager configManager;
AIConfig aiConfig = configManager.loadAIConfig();
WebStore webStore;
QString webConfigError;
WebConfig webConfig = webStore.load(&webConfigError);
if (!webConfigError.isEmpty())
{
Logger::warning(QStringLiteral("Web config load warning: ") + webConfigError);
}
const WebCapability capability = WebCapabilityDetector::detect(aiConfig, webConfig);
if (!capability.supported)
{
playState(QStringLiteral("talk"), false);
showBubbleMessage(capability.userMessage);
refreshChatInputWebToggle();
return false;
}
QString runtimeError;
AIConfig runtimeConfig = aiConfig;
if (!AIProviderFactory::prepareRuntimeConfig(runtimeConfig, &runtimeError))
{
playState(QStringLiteral("error"), false);
showBubbleMessage(runtimeError);
return false;
}
const QString chatMessage = message.trimmed();
if (chatMessage.isEmpty())
{
return false;
}
if (!m_conversationManager)
{
playState(QStringLiteral("error"), false);
showBubbleMessage(QStringLiteral("AI 对话功能初始化失败。"));
return false;
}
m_conversationManager->setConversationMetadata(runtimeConfig.provider, runtimeConfig.model);
ChatRequest request = m_conversationManager->buildRequestForUserMessage(chatMessage);
if (request.messages.isEmpty())
{
playState(QStringLiteral("error"), false);
showBubbleMessage(QStringLiteral("联网请求内容为空。"));
return false;
}
stopAnimationPrewarm();
playState(QStringLiteral("think"), false);
hideReminderActions();
showBubbleMessage(QStringLiteral("正在联网思考..."));
QPointer<PetWindow> window(this);
WebChatRequest webRequest;
webRequest.chatRequest = request;
webRequest.aiConfig = runtimeConfig;
webRequest.webConfig = webConfig;
m_webChatManager->sendWebChat(webRequest, [window, chatMessage](const WebChatResponse &response) {
if (window.isNull())
{
return;
}
if (!response.success)
{
window->playState(QStringLiteral("error"), false);
window->showBubbleMessage(response.errorMessage.isEmpty()
? QStringLiteral("联网请求失败。")
: response.errorMessage);
window->m_behaviorReturnTimer.start(ChatFinishedReturnDelayMs);
return;
}
window->playState(QStringLiteral("talk"), false);
const QString displayText = window->formatWebChatResponseForDisplay(response);
if (window->m_conversationManager)
{
window->m_conversationManager->appendExternalExchange(chatMessage, displayText);
}
window->saveConversationHistoryIfNeeded();
window->refreshChatHistoryPanel();
window->showBubbleMessage(displayText);
window->m_behaviorReturnTimer.start(ChatFinishedReturnDelayMs);
});
return true;
}
bool PetWindow::hasActiveWebRequest() const
{
return m_webChatManager && m_webChatManager->isBusy();
}
QString PetWindow::formatWebChatResponseForDisplay(const WebChatResponse &response) const
{
QString message = response.content.trimmed();
if (message.isEmpty())
{
message = QStringLiteral("联网请求已完成,但没有返回内容。");
}
WebStore webStore;
const WebConfig webConfig = webStore.load();
if (!webConfig.showCitations)
{
return message;
}
if (!response.citations.isEmpty())
{
message += QStringLiteral("\n\n来源:\n") + webCitationListText(response.citations);
}
else if (!response.usedWeb)
{
message += QStringLiteral("\n\n(模型未使用联网来源)");
}
else
{
message += QStringLiteral("\n\n(模型使用了联网能力,但未返回可展示来源)");
}
return message;
}
void PetWindow::refreshChatInputWebToggle()
{
if (!m_chatInputDialog)
{
return;
}
WebStore webStore;
QString errorMessage;
const WebConfig webConfig = webStore.load(&errorMessage);
if (!errorMessage.isEmpty())
{
Logger::warning(QStringLiteral("Web config load warning: ") + errorMessage);
}
ConfigManager configManager;
const AIConfig aiConfig = configManager.loadAIConfig();
const WebCapability capability = WebCapabilityDetector::detect(aiConfig, webConfig);
const bool available = webConfig.enabled;
m_chatInputDialog->setWebToggleAvailable(available, capability.userMessage);
const bool checked = webConfig.rememberLastToggle ? webConfig.lastToggleOn : webConfig.defaultToggleOn;
m_chatInputDialog->setWebEnabled(available && checked);
}
void PetWindow::saveWebTogglePreference(bool webEnabled)
{
WebStore webStore;
QString loadError;
WebConfig webConfig = webStore.load(&loadError);
if (!loadError.isEmpty())
{
Logger::warning(QStringLiteral("Web config load warning: ") + loadError);
}
if (!webConfig.rememberLastToggle)
{
return;
}
webConfig.lastToggleOn = webEnabled;
QString saveError;
if (!webStore.save(webConfig, &saveError))
{
Logger::warning(QStringLiteral("Failed to save web toggle preference: ") + saveError);
}
}
void PetWindow::handleTriggeredReminder(const ReminderItem &item)
{
playReminderSound();
if (shouldNotifyOnlyForReminder())
{
showReminderNotification(item);
return;
}
enqueueVisibleTriggeredReminder(item);
}
void PetWindow::playReminderSound()
{
if (m_appConfig.reminderSoundEnabled && m_reminderSoundPlayer)
{
m_reminderSoundPlayer->play(m_appConfig.reminderSoundId, m_appConfig.reminderSoundVolume);
}
}
void PetWindow::showReminderNotification(const ReminderItem &item)
{
if (m_notificationDispatcher)
{
const bool shown = m_notificationDispatcher->showReminder(QStringLiteral("定时提醒"), QStringLiteral("到时间啦:%1").arg(item.title));
if (!shown)
{
Logger::warning(QStringLiteral("Reminder notification backend unavailable: id=%1").arg(item.id));
}
return;
}
Logger::warning(QStringLiteral("Reminder notification dispatcher is unavailable: id=%1").arg(item.id));
}
bool PetWindow::shouldNotifyOnlyForReminder() const
{
return !isVisible() || hasActiveAIRequest() || m_streamingChatActive;
}
void PetWindow::enqueueVisibleTriggeredReminder(const ReminderItem &item)
{
m_pendingVisibleTriggeredReminders.append(item);
showNextTriggeredReminder();
}
void PetWindow::showNextTriggeredReminder()
{
if (m_hasActiveTriggeredReminder || m_dragging || m_pendingVisibleTriggeredReminders.isEmpty())
{
return;
}
const ReminderItem item = m_pendingVisibleTriggeredReminders.takeFirst();
showTriggeredReminder(item);
}
void PetWindow::finishActiveTriggeredReminder(bool hideBubble)
{
hideReminderActions();
m_hasActiveTriggeredReminder = false;
if (hideBubble && m_chatBubble)
{
m_chatBubble->hideBubble();
}
showNextTriggeredReminder();
}
void PetWindow::showTriggeredReminder(const ReminderItem &item)
{
const QString reminderState = m_clips.contains(QStringLiteral("happy"))
? QStringLiteral("happy")
: QStringLiteral("talk");
playState(reminderState, false);
hideReminderActions();
m_chatBubble->showMessage(QStringLiteral("到时间啦:%1").arg(item.title), bubbleAnchorPosition(), 0);
showReminderActions(item);
}
void PetWindow::ensureReminderActionPanel()
{
if (m_reminderActionPanel)
{
return;
}
auto *panel = new QFrame();
panel->setObjectName(QStringLiteral("ReminderActionPanel"));
panel->setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint);
panel->setAttribute(Qt::WA_TranslucentBackground);
panel->setAttribute(Qt::WA_ShowWithoutActivating);
panel->setStyleSheet(QStringLiteral(
"QFrame#ReminderActionPanel {"
"background: #ffffff;"
"border: 1px solid #c7cdd4;"
"border-radius: 8px;"
"}"
"QPushButton {"
"background: #f1f4f7;"
"border: 1px solid #aeb6bf;"
"border-radius: 6px;"
"padding: 7px 12px;"
"color: #202124;"
"}"
"QPushButton:hover {"
"background: #e4e9ee;"
"}"
"QPushButton:pressed {"
"background: #d5dce3;"
"}"));
auto *layout = new QVBoxLayout(panel);
layout->setContentsMargins(8, 8, 8, 8);
layout->setSpacing(8);
auto *dismissButton = new QPushButton(QStringLiteral("知道了"), panel);
auto *snoozeButton = new QPushButton(QStringLiteral("5分钟后再提醒"), panel);
layout->addWidget(dismissButton);
layout->addWidget(snoozeButton);
connect(dismissButton, &QPushButton::clicked, this, [this]() {
finishActiveTriggeredReminder(true);
});
connect(snoozeButton, &QPushButton::clicked, this, [this]() {
if (!m_hasActiveTriggeredReminder)
{
finishActiveTriggeredReminder(true);
return;
}
const ReminderItem item = m_activeTriggeredReminder;
hideReminderActions();
snoozeTriggeredReminder(item);
});
m_reminderActionPanel.reset(panel);
}
void PetWindow::showReminderActions(const ReminderItem &item)
{
m_activeTriggeredReminder = item;
m_hasActiveTriggeredReminder = true;
ensureReminderActionPanel();
if (!m_reminderActionPanel)
{
return;
}
m_reminderActionPanel->adjustSize();
updateReminderActionPosition();
m_reminderActionPanel->show();
m_reminderActionPanel->raise();
}
void PetWindow::hideReminderActions()
{
m_hasActiveTriggeredReminder = false;
if (m_reminderActionPanel)
{
m_reminderActionPanel->hide();
}
}
void PetWindow::updateReminderActionPosition()
{
if (!m_reminderActionPanel)
{
return;
}
m_reminderActionPanel->adjustSize();
const QSize panelSize = m_reminderActionPanel->sizeHint();
const QRect petGeometry = frameGeometry();
constexpr int PanelSideSpacing = 8;
QPoint position(
petGeometry.right() + PanelSideSpacing,
petGeometry.center().y() - panelSize.height() / 2);
if (QScreen *screen = screenForPopup(this))
{
const QRect availableGeometry = screen->availableGeometry();
const int maxX = qMax(availableGeometry.left(), availableGeometry.right() - panelSize.width());
const int maxY = qMax(availableGeometry.top(), availableGeometry.bottom() - panelSize.height());
if (position.x() > maxX)
{
position.setX(petGeometry.left() - panelSize.width() - PanelSideSpacing);
}
position.setX(qBound(
availableGeometry.left(),
position.x(),
maxX));
position.setY(qBound(
availableGeometry.top(),
position.y(),
maxY));
}
m_reminderActionPanel->move(position);
}
void PetWindow::snoozeTriggeredReminder(const ReminderItem &item)
{
m_hasActiveTriggeredReminder = false;
if (!m_reminderManager)
{
playState(QStringLiteral("error"), false);
showBubbleMessage(QStringLiteral("提醒功能初始化失败。"));
QTimer::singleShot(1200, this, [this]() {
showNextTriggeredReminder();
});
return;
}
ReminderItem snoozedItem;
QString errorMessage;
if (!m_reminderManager->snoozeReminder(item, 5, &snoozedItem, &errorMessage))
{
playState(QStringLiteral("error"), false);
showBubbleMessage(errorMessage.isEmpty() ? QStringLiteral("创建稍后提醒失败。") : errorMessage);
QTimer::singleShot(1200, this, [this]() {
showNextTriggeredReminder();
});
return;
}
playState(QStringLiteral("talk"), false);
showBubbleMessage(QStringLiteral("已延后提醒:%1,时间:%2").arg(snoozedItem.title, reminderDisplayTime(snoozedItem.remindAt)));
QTimer::singleShot(1200, this, [this]() {
showNextTriggeredReminder();
});
}
bool PetWindow::submitAiChatMessage(const QString &message)
{
if (hasActiveWebRequest())
{
showBubbleMessage(QStringLiteral("当前联网请求正在进行,请稍后。"));
return false;
}
if (!m_conversationManager || m_conversationManager->isBusy())
{
showBubbleMessage(QStringLiteral("AI 回复正在进行。"));
return false;
}
const QString chatMessage = message.trimmed();
if (chatMessage.isEmpty())
{
return false;
}
ConfigManager configManager;
AIConfig config = configManager.loadAIConfig();
QString errorMessage;
if (!AIProviderFactory::prepareRuntimeConfig(config, &errorMessage))
{
playState(QStringLiteral("error"), false);
showBubbleMessage(errorMessage);
return false;
}
std::unique_ptr<LLMProvider> provider = AIProviderFactory::createProvider(config);
if (!provider)
{
playState(QStringLiteral("error"), false);
showBubbleMessage(QStringLiteral("当前 Provider 协议暂未接入。"));
return false;
}
m_conversationManager->setConversationMetadata(config.provider, config.model);
if (!m_conversationManager->setProvider(std::move(provider)))
{
showBubbleMessage(QStringLiteral("AI 回复正在进行。"));
return false;
}
stopAnimationPrewarm();
playState(QStringLiteral("think"), false);
hideReminderActions();
m_streamingAssistantText.clear();
m_streamBubbleUpdateTimer.stop();
m_streamingChatActive = true;
m_streamingTalkStarted = false;
m_chatBubble->showMessage(QStringLiteral("正在思考..."), bubbleAnchorPosition(), 0);
QPointer<PetWindow> window(this);
m_conversationManager->sendUserMessageStreaming(
chatMessage,
[window](const QString &delta) {
if (!window.isNull())
{
window->handleChatStreamDelta(delta);
}
},
[window](const ChatResponse &response) {
if (window.isNull())
{
return;
}
window->m_streamBubbleUpdateTimer.stop();
if (response.success)
{
const bool shouldReturnToIdleAfterChat =
window->m_streamingTalkStarted
|| window->m_stateMachine.currentState() == QStringLiteral("think")
|| window->m_stateMachine.currentState() == QStringLiteral("talk")
|| window->m_frameAnimator.currentStateName() == QStringLiteral("think")
|| window->m_frameAnimator.currentStateName() == QStringLiteral("talk");
window->finishStreamingChat();
window->m_streamingAssistantText = response.content;
window->flushStreamingBubble(true);
window->saveConversationHistoryIfNeeded();
window->refreshChatHistoryPanel();
if (shouldReturnToIdleAfterChat)
{
window->m_behaviorReturnTimer.start(ChatFinishedReturnDelayMs);
}
return;
}
window->cancelStreamingChat();
window->m_streamingAssistantText.clear();
window->playState(QStringLiteral("error"), false);
window->showBubbleMessage(QStringLiteral("AI 回复失败:") + userVisibleErrorMessage(response));
});
return true;
}
void PetWindow::clearConversation()
{
if (!m_conversationManager)
{
return;
}
const bool hadActiveRequest = hasActiveAIRequest();
m_conversationManager->clear();
if (m_conversationStore && !m_conversationStore->clear())
{
Logger::warning(QStringLiteral("Failed to clear persisted conversation history."));
}
cancelStreamingChat();
if (m_webChatManager && m_webChatManager->isBusy())
{
m_webChatManager->cancel();
}
refreshChatHistoryPanel();
showBubbleMessage(hadActiveRequest
? QStringLiteral("已取消当前请求,并清空对话。")
: QStringLiteral("对话已清空。"));
playState(QStringLiteral("idle"), false);
}
void PetWindow::cancelActiveAIRequest()
{
if (m_conversationManager && m_conversationManager->isBusy())
{
m_conversationManager->cancel();
cancelStreamingChat();
showBubbleMessage(QStringLiteral("AI 请求已取消。"));
playState(QStringLiteral("idle"), false);
return;
}
if (m_webChatManager && m_webChatManager->isBusy())
{
m_webChatManager->cancel();
showBubbleMessage(QStringLiteral("联网请求已取消。"));
playState(QStringLiteral("idle"), false);
return;
}
showBubbleMessage(QStringLiteral("没有正在进行的 AI 或联网请求。"));
}
bool PetWindow::hasActiveAIRequest() const
{
return (m_conversationManager && m_conversationManager->isBusy()) || hasActiveWebRequest();
}
bool PetWindow::isManualStateSwitchLocked() const
{
if (m_streamingChatActive || hasActiveAIRequest())
{
return true;
}
const QString currentState = m_stateMachine.currentState();
return currentState == QStringLiteral("think") || currentState == QStringLiteral("talk");
}
void PetWindow::showConversationHistory()
{
const QVector<ChatMessage> history = m_conversationManager ? m_conversationManager->history() : QVector<ChatMessage>();
const int prunedCount = m_conversationManager ? m_conversationManager->prunedHistoryMessageCount() : 0;
m_chatHistoryPanel->setMessages(history, prunedCount);
m_chatHistoryPanel->showNear(frameGeometry());
}
void PetWindow::refreshChatHistoryPanel()
{
if (!m_chatHistoryPanel || !m_chatHistoryPanel->isVisible())
{
return;
}
const QVector<ChatMessage> history = m_conversationManager ? m_conversationManager->history() : QVector<ChatMessage>();
const int prunedCount = m_conversationManager ? m_conversationManager->prunedHistoryMessageCount() : 0;
m_chatHistoryPanel->setMessages(history, prunedCount);
}
void PetWindow::configureConversation(bool loadPersistedHistory)
{
if (!m_conversationManager)
{
return;
}
m_conversationManager->setRequestContextMessageLimit(m_appConfig.requestContextMessageLimit);
m_conversationManager->setMemoryHistoryMessageLimit(m_appConfig.memoryHistoryMessageLimit);
if (loadPersistedHistory)
{
loadConversationHistoryIfNeeded();
}
saveConversationHistoryIfNeeded();
refreshChatHistoryPanel();
}
void PetWindow::loadConversationHistoryIfNeeded()
{
if (!m_appConfig.saveConversationHistory || !m_conversationManager || !m_conversationStore)
{
return;
}
if (m_conversationManager->hasHistory())
{
return;
}
QString loadError;
const QVector<ChatMessage> history = m_conversationStore->load(m_appConfig.savedHistoryMessageLimit, &loadError);
if (!loadError.isEmpty())
{
Logger::warning(QStringLiteral("Failed to load conversation history: ") + loadError);
}
if (!history.isEmpty())
{
m_conversationManager->setHistory(history);
}
}
void PetWindow::saveConversationHistoryIfNeeded()
{
if (!m_appConfig.saveConversationHistory || !m_conversationManager || !m_conversationStore)
{
return;
}
if (!m_conversationManager->hasHistory())
{
if (!m_conversationStore->clear())
{
Logger::warning(QStringLiteral("Failed to clear empty conversation history."));
}
return;
}
if (!m_conversationStore->save(m_conversationManager->history(), m_appConfig.savedHistoryMessageLimit))
{
Logger::warning(QStringLiteral("Failed to save conversation history."));
}
}
void PetWindow::handleChatStreamDelta(const QString &delta)
{
if (delta.isEmpty())
{
return;
}
m_streamingAssistantText += delta;
if (m_streamingChatActive && !m_streamingTalkStarted)
{
m_streamingTalkStarted = true;
playState(QStringLiteral("talk"), false);
if (m_stateMachine.currentState() == QStringLiteral("talk"))
{
m_returnToIdleAfterResume = false;
}
}
if (!isVisible())
{
return;
}
if (!m_streamBubbleUpdateTimer.isActive())
{
m_streamBubbleUpdateTimer.start(StreamBubbleUpdateIntervalMs);
}
}
void PetWindow::flushStreamingBubble(bool finalUpdate)
{
if (!isVisible() || m_streamingAssistantText.trimmed().isEmpty())
{
return;
}
hideReminderActions();
m_chatBubble->showMessage(
m_streamingAssistantText,
bubbleAnchorPosition(),
finalUpdate ? 10000 : 0,
true);
}
void PetWindow::finishStreamingChat()
{
m_streamingChatActive = false;
m_streamingTalkStarted = false;
scheduleAnimationPrewarm();
}
void PetWindow::cancelStreamingChat()
{
m_streamBubbleUpdateTimer.stop();
m_streamingAssistantText.clear();
m_streamingChatActive = false;
m_streamingTalkStarted = false;
scheduleAnimationPrewarm();
}
void PetWindow::resetBubbleAutoHideTimer()
{
if (m_chatBubble)
{
m_chatBubble->resetAutoHideTimer();
}
}
QPoint PetWindow::chatInputAnchorPosition() const
{
return frameGeometry().topLeft() + QPoint(width() / 2, height() / 2 + ChatInputLowerOffsetY);
}
void PetWindow::hideEvent(QHideEvent *event)
{
m_streamBubbleUpdateTimer.stop();
stopAnimationPrewarm();
if (m_chatBubble)
{
m_chatBubble->hideBubble();
}
hideReminderActions();
if (m_chatInputDialog)
{
m_chatInputDialog->hide();
}
if (m_chatHistoryPanel)
{
m_chatHistoryPanel->hide();
}
if (isAnimationCacheManagementEnabled() && m_appConfig.unloadAnimationsWhenHidden)
{
unloadNonProtectedAnimationCache(QStringLiteral("window hidden"));
}
QWidget::hideEvent(event);
}
void PetWindow::showEvent(QShowEvent *event)
{
QWidget::showEvent(event);
if (m_reminderManager)
{
m_reminderManager->start();
m_reminderManager->checkDueRemindersNow();
}
scheduleAnimationPrewarm();
}
void PetWindow::mouseDoubleClickEvent(QMouseEvent *event)
{
resetBubbleAutoHideTimer();
if (event->button() == Qt::LeftButton)
{
m_dragging = false;
playResolvedState(m_stateMachine.endDrag(), false);
startChat();
event->accept();
return;
}
QWidget::mouseDoubleClickEvent(event);
}
void PetWindow::mouseMoveEvent(QMouseEvent *event)
{
if (m_dragging && (event->buttons() & Qt::LeftButton))
{
resetBubbleAutoHideTimer();
move(event->globalPosition().toPoint() - m_dragOffset);
updateBubblePosition();
event->accept();
return;
}
QWidget::mouseMoveEvent(event);
}
void PetWindow::moveEvent(QMoveEvent *event)
{
QWidget::moveEvent(event);
updateBubblePosition();
}
void PetWindow::mousePressEvent(QMouseEvent *event)
{
resetBubbleAutoHideTimer();
if (event->button() == Qt::LeftButton)
{
m_dragging = true;
m_dragOffset = event->globalPosition().toPoint() - frameGeometry().topLeft();
stopAnimationPrewarm();
playResolvedState(m_stateMachine.beginDrag(), false);
event->accept();
return;
}
QWidget::mousePressEvent(event);
}
void PetWindow::mouseReleaseEvent(QMouseEvent *event)
{
resetBubbleAutoHideTimer();
if (event->button() == Qt::LeftButton)
{
m_dragging = false;
playResolvedState(m_stateMachine.endDrag(), false);
scheduleAnimationPrewarm();
showNextTriggeredReminder();
event->accept();
return;
}
QWidget::mouseReleaseEvent(event);
}
void PetWindow::loadInitialImage()
{
loadCharacterPackage(m_appConfig.characterId, true);
}
bool PetWindow::loadCharacterPackage(const QString &characterId, bool centerWindow)
{
const QString requestedCharacterId = CharacterPackageRepository::hasPackage(characterId)
? characterId
: CharacterPackageRepository::defaultCharacterId();
QString loadError;
CharacterPackage package = CharacterPackageLoader::load(CharacterPackageRepository::packagePath(requestedCharacterId), &loadError);
if (!loadError.isEmpty() && requestedCharacterId != CharacterPackageRepository::defaultCharacterId())
{
Logger::warning(QStringLiteral("Character package load failed: id=%1 error=%2")
.arg(requestedCharacterId, loadError));
loadError.clear();
package = CharacterPackageLoader::load(CharacterPackageRepository::defaultPackagePath(), &loadError);
m_appConfig.characterId = CharacterPackageRepository::defaultCharacterId();
}
if (!loadError.isEmpty())
{
Logger::warning(QStringLiteral("Character package load failed: ") + loadError);
}
m_characterPackage = package;
buildAnimationClips();
if (m_clips.contains(QStringLiteral("idle")))
{
playResolvedState(m_stateMachine.start(), centerWindow);
return true;
}
setDisplayImage(CharacterPackageRepository::previewPath(m_appConfig.characterId), centerWindow);
return !m_characterPackage.states.isEmpty();
}
void PetWindow::buildAnimationClips()
{
stopAnimationPrewarm();
m_animationPrewarmQueue.clear();
m_animationPrewarmAttemptedStates.clear();
m_clipLastAccessSerial.clear();
m_clipAccessSerial = 0;
m_clips.clear();
QSet<QString> availableStates;
const QSize targetSize = animationTargetSize();
const bool loadFramesImmediately = !m_appConfig.enableLazyLoad;
for (auto iterator = m_characterPackage.states.constBegin(); iterator != m_characterPackage.states.constEnd(); ++iterator)
{
AnimationClip clip = AnimationClip::fromState(iterator.value(), targetSize, loadFramesImmediately);
clip.fps = effectiveAnimationFps(clip.fps);
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"));
const bool stateSwitchLocked = isManualStateSwitchLocked();
const QStringList stateNames = {
QStringLiteral("idle"),
QStringLiteral("talk"),
QStringLiteral("think"),
QStringLiteral("sleep"),
QStringLiteral("happy"),
QStringLiteral("error"),
QStringLiteral("drag"),
};
for (const QString &stateName : stateNames)
{
if (!m_clips.contains(stateName))
{
continue;
}
QAction *stateAction = stateMenu->addAction(stateName);
stateAction->setData(stateName);
}
stateMenu->setEnabled(!stateSwitchLocked && !stateMenu->actions().isEmpty());
}
void PetWindow::updateBubblePosition()
{
m_chatBubble->updateAnchorPosition(bubbleAnchorPosition());
updateReminderActionPosition();
}
QPoint PetWindow::bubbleAnchorPosition() const
{
const CharacterBase &base = m_characterPackage.base;
const CharacterBubble &bubble = m_characterPackage.bubble;
const double baseWidth = base.width > 0 ? static_cast<double>(base.width) : static_cast<double>(width());
const double baseHeight = base.height > 0 ? static_cast<double>(base.height) : static_cast<double>(height());
const double scaleX = baseWidth > 0.0 ? static_cast<double>(width()) / baseWidth : 1.0;
const double scaleY = baseHeight > 0.0 ? static_cast<double>(height()) / baseHeight : 1.0;
const QPointF localAnchor(
static_cast<double>(width()) * base.anchorX + bubble.offsetX * scaleX,
static_cast<double>(height()) * base.anchorY + bubble.offsetY * scaleY);
return frameGeometry().topLeft() + QPoint(qRound(localAnchor.x()), qRound(localAnchor.y()));
}
void PetWindow::playState(const QString &stateName, bool centerWindow)
{
playResolvedState(m_stateMachine.requestState(stateName), centerWindow);
}
void PetWindow::playResolvedState(const QString &stateName, bool centerWindow)
{
if (stateName.isEmpty())
{
return;
}
if (m_streamingChatActive
&& stateName != QStringLiteral("error")
&& stateName != QStringLiteral("drag"))
{
const QString preferredHeldState = m_streamingTalkStarted
? QStringLiteral("talk")
: QStringLiteral("think");
const QString heldState = m_stateMachine.requestState(preferredHeldState, StateRequestSource::System);
if (stateName != heldState)
{
if (!heldState.isEmpty() && m_clips.contains(heldState))
{
playResolvedState(heldState, centerWindow);
}
return;
}
}
if (m_frameAnimator.currentStateName() == stateName && m_frameAnimator.isPlaying())
{
noteAnimationClipAccess(stateName);
return;
}
auto clipIterator = m_clips.find(stateName);
if (clipIterator == m_clips.end())
{
return;
}
AnimationClip *clip = &clipIterator.value();
const bool wasLoaded = clip->isLoaded();
if (!clip->ensureLoaded())
{
Logger::warning(QStringLiteral("Animation state failed to load: state=%1").arg(stateName));
return;
}
noteAnimationClipAccess(stateName);
if (!wasLoaded)
{
Logger::info(QStringLiteral("Animation state loaded: state=%1 frames=%2 cacheMb=%3")
.arg(stateName)
.arg(QString::number(clip->loadedFrameCount()))
.arg(megabytesText(clip->estimatedMemoryBytes())));
}
m_idleBehaviorTimer.stop();
m_behaviorReturnTimer.stop();
m_centerNextFrame = centerWindow;
m_frameAnimator.play(clip);
if (stateName == QStringLiteral("idle"))
{
scheduleIdleBehavior();
}
else if (clip->loop)
{
m_behaviorReturnTimer.start(4000);
}
trimAnimationCache(QStringLiteral("state played"));
scheduleAnimationPrewarm();
}
QSize PetWindow::animationTargetSize() const
{
const CharacterBase &base = m_characterPackage.base;
const double totalScale = base.scale * m_appConfig.scale;
return QSize(
boundedAnimationTargetSide(static_cast<double>(base.width) * totalScale),
boundedAnimationTargetSide(static_cast<double>(base.height) * totalScale));
}
int PetWindow::effectiveAnimationFps(int fps) const
{
if (isLowPowerMode())
{
return qMax(1, qMin(fps, LowPowerFpsCap));
}
return qMax(1, fps);
}
bool PetWindow::isLowPowerMode() const
{
return m_appConfig.performanceMode == QStringLiteral("low-power");
}
bool PetWindow::isAnimationCacheManagementEnabled() const
{
return m_appConfig.enableLazyLoad;
}
void PetWindow::rebuildAnimationPrewarmQueue()
{
m_animationPrewarmQueue.clear();
if (!isAnimationCacheManagementEnabled() || !m_appConfig.enableAnimationPrewarm)
{
return;
}
const auto appendStateIfNeeded = [this](const QString &stateName) {
if (stateName == QStringLiteral("idle")
|| m_animationPrewarmQueue.contains(stateName)
|| m_animationPrewarmAttemptedStates.contains(stateName))
{
return;
}
const auto iterator = m_clips.constFind(stateName);
if (iterator != m_clips.constEnd() && !iterator.value().isLoaded())
{
m_animationPrewarmQueue.append(stateName);
}
};
appendStateIfNeeded(QStringLiteral("drag"));
appendStateIfNeeded(QStringLiteral("think"));
appendStateIfNeeded(QStringLiteral("talk"));
QStringList stateNames = m_clips.keys();
stateNames.sort();
for (const QString &stateName : stateNames)
{
appendStateIfNeeded(stateName);
}
}
void PetWindow::scheduleAnimationPrewarm()
{
if (!isAnimationCacheManagementEnabled() || !m_appConfig.enableAnimationPrewarm)
{
stopAnimationPrewarm();
return;
}
if (!isVisible() || m_dragging || m_streamingChatActive || hasActiveAIRequest())
{
stopAnimationPrewarm();
return;
}
if (m_animationPrewarmQueue.isEmpty())
{
rebuildAnimationPrewarmQueue();
}
if (m_animationPrewarmQueue.isEmpty() || m_animationPrewarmTimer.isActive())
{
return;
}
m_animationPrewarmTimer.start(isLowPowerMode() ? LowPowerPrewarmIntervalMs : StandardPrewarmIntervalMs);
}
void PetWindow::stopAnimationPrewarm()
{
m_animationPrewarmTimer.stop();
}
void PetWindow::processAnimationPrewarm()
{
m_animationPrewarmTimer.stop();
if (!isAnimationCacheManagementEnabled() || !m_appConfig.enableAnimationPrewarm)
{
return;
}
if (!isVisible() || m_dragging || m_streamingChatActive || hasActiveAIRequest())
{
return;
}
while (!m_animationPrewarmQueue.isEmpty())
{
const QString stateName = m_animationPrewarmQueue.takeFirst();
m_animationPrewarmAttemptedStates.insert(stateName);
auto clipIterator = m_clips.find(stateName);
if (clipIterator == m_clips.end() || clipIterator.value().isLoaded())
{
continue;
}
AnimationClip &clip = clipIterator.value();
if (!clip.ensureLoaded())
{
Logger::warning(QStringLiteral("Animation state prewarm failed: state=%1").arg(stateName));
continue;
}
noteAnimationClipAccess(stateName);
Logger::info(QStringLiteral("Animation state prewarmed: state=%1 frames=%2 cacheMb=%3")
.arg(stateName)
.arg(QString::number(clip.loadedFrameCount()))
.arg(megabytesText(clip.estimatedMemoryBytes())));
trimAnimationCache(QStringLiteral("prewarm"));
break;
}
scheduleAnimationPrewarm();
}
void PetWindow::noteAnimationClipAccess(const QString &stateName)
{
if (!m_clips.contains(stateName))
{
return;
}
++m_clipAccessSerial;
m_clipLastAccessSerial.insert(stateName, m_clipAccessSerial);
}
qint64 PetWindow::animationCacheLimitBytes() const
{
return static_cast<qint64>(m_appConfig.animationCacheLimitMb) * BytesPerMegabyte;
}
qint64 PetWindow::loadedAnimationCacheBytes() const
{
qint64 totalBytes = 0;
for (auto iterator = m_clips.constBegin(); iterator != m_clips.constEnd(); ++iterator)
{
totalBytes += iterator.value().estimatedMemoryBytes();
}
return totalBytes;
}
int PetWindow::loadedAnimationClipCount() const
{
int count = 0;
for (auto iterator = m_clips.constBegin(); iterator != m_clips.constEnd(); ++iterator)
{
if (iterator.value().isLoaded())
{
++count;
}
}
return count;
}
QSet<QString> PetWindow::protectedAnimationStates() const
{
QSet<QString> states;
states.insert(QStringLiteral("idle"));
const QString animatorState = m_frameAnimator.currentStateName();
if (!animatorState.isEmpty())
{
states.insert(animatorState);
}
const QString stateMachineState = m_stateMachine.currentState();
if (!stateMachineState.isEmpty())
{
states.insert(stateMachineState);
}
if (m_streamingChatActive || hasActiveAIRequest())
{
states.insert(QStringLiteral("think"));
states.insert(QStringLiteral("talk"));
}
if (m_dragging)
{
states.insert(QStringLiteral("drag"));
}
return states;
}
void PetWindow::trimAnimationCache(const QString &reason)
{
if (!isAnimationCacheManagementEnabled())
{
return;
}
qint64 totalBytes = loadedAnimationCacheBytes();
const qint64 limitBytes = animationCacheLimitBytes();
if (totalBytes <= limitBytes)
{
return;
}
struct UnloadCandidate
{
QString stateName;
qint64 lastAccessSerial = 0;
};
const QSet<QString> protectedStates = protectedAnimationStates();
QList<UnloadCandidate> candidates;
QStringList protectedLoadedStates;
for (auto iterator = m_clips.constBegin(); iterator != m_clips.constEnd(); ++iterator)
{
if (!iterator.value().isLoaded())
{
continue;
}
if (protectedStates.contains(iterator.key()))
{
protectedLoadedStates.append(iterator.key());
continue;
}
candidates.append({iterator.key(), m_clipLastAccessSerial.value(iterator.key(), 0)});
}
std::sort(candidates.begin(), candidates.end(), [](const UnloadCandidate &left, const UnloadCandidate &right) {
if (left.lastAccessSerial == right.lastAccessSerial)
{
return left.stateName < right.stateName;
}
return left.lastAccessSerial < right.lastAccessSerial;
});
for (const UnloadCandidate &candidate : candidates)
{
if (totalBytes <= limitBytes)
{
break;
}
auto clipIterator = m_clips.find(candidate.stateName);
if (clipIterator == m_clips.end() || !clipIterator.value().isLoaded())
{
continue;
}
const qint64 freedBytes = clipIterator.value().estimatedMemoryBytes();
clipIterator.value().unloadFrames();
m_clipLastAccessSerial.remove(candidate.stateName);
totalBytes -= freedBytes;
Logger::info(QStringLiteral("Animation state unloaded: state=%1 reason=%2 freedMb=%3 cacheMb=%4 loadedStates=%5")
.arg(candidate.stateName)
.arg(reason)
.arg(megabytesText(freedBytes))
.arg(megabytesText(totalBytes))
.arg(QString::number(loadedAnimationClipCount())));
}
if (totalBytes > limitBytes)
{
protectedLoadedStates.sort();
Logger::warning(QStringLiteral("Animation cache remains over limit: reason=%1 cacheMb=%2 limitMb=%3 protectedStates=%4")
.arg(reason)
.arg(megabytesText(totalBytes))
.arg(QString::number(m_appConfig.animationCacheLimitMb))
.arg(protectedLoadedStates.join(QStringLiteral(","))));
}
}
void PetWindow::unloadNonProtectedAnimationCache(const QString &reason)
{
if (!isAnimationCacheManagementEnabled())
{
return;
}
const QSet<QString> protectedStates = protectedAnimationStates();
for (auto iterator = m_clips.begin(); iterator != m_clips.end(); ++iterator)
{
if (!iterator.value().isLoaded())
{
continue;
}
if (protectedStates.contains(iterator.key()))
{
Logger::info(QStringLiteral("Animation state kept loaded: state=%1 reason=%2")
.arg(iterator.key())
.arg(reason));
continue;
}
const qint64 freedBytes = iterator.value().estimatedMemoryBytes();
iterator.value().unloadFrames();
m_clipLastAccessSerial.remove(iterator.key());
Logger::info(QStringLiteral("Animation state unloaded: state=%1 reason=%2 freedMb=%3 cacheMb=%4 loadedStates=%5")
.arg(iterator.key())
.arg(reason)
.arg(megabytesText(freedBytes))
.arg(megabytesText(loadedAnimationCacheBytes()))
.arg(QString::number(loadedAnimationClipCount())));
}
}
void PetWindow::scheduleIdleBehavior()
{
if (!m_clips.contains(QStringLiteral("idle")))
{
return;
}
const int idleDelayMs = isLowPowerMode()
? QRandomGenerator::global()->bounded(16000, 30001)
: QRandomGenerator::global()->bounded(8000, 16001);
m_idleBehaviorTimer.start(idleDelayMs);
}
void PetWindow::playIdleBehavior()
{
if (m_dragging || m_stateMachine.currentState() != QStringLiteral("idle"))
{
scheduleIdleBehavior();
return;
}
QStringList candidateStates;
const QStringList preferredStates = {
QStringLiteral("think"),
QStringLiteral("sleep"),
QStringLiteral("happy"),
};
for (const QString &stateName : preferredStates)
{
if (m_clips.contains(stateName))
{
candidateStates.append(stateName);
}
}
if (candidateStates.isEmpty())
{
scheduleIdleBehavior();
return;
}
const int stateIndex = QRandomGenerator::global()->bounded(candidateStates.size());
playResolvedState(m_stateMachine.requestState(candidateStates.at(stateIndex), StateRequestSource::Automatic), false);
}
void PetWindow::returnToIdleFromBehavior()
{
if (m_streamingChatActive)
{
const QString heldState = m_streamingTalkStarted
? QStringLiteral("talk")
: QStringLiteral("think");
if (m_clips.contains(heldState))
{
playResolvedState(heldState, false);
}
return;
}
if (!m_dragging)
{
playResolvedState(m_stateMachine.finishState(QStringLiteral("idle")), false);
}
}
void PetWindow::setDisplayImage(const QString &imagePath, bool centerWindow)
{
QPixmap pixmap(imagePath);
if (pixmap.isNull())
{
m_petView->showFallbackText(QStringLiteral("QtDesktopPet"));
resize(240, 160);
return;
}
const QSize targetSize = animationTargetSize();
const QPixmap scaled = pixmap.scaled(targetSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
setDisplayPixmap(scaled, centerWindow);
}
void PetWindow::setDisplayPixmap(const QPixmap &pixmap, bool centerWindow)
{
const QSize previousSize = size();
m_petView->setFrame(pixmap);
resize(pixmap.size());
if (size() != previousSize)
{
updateBubblePosition();
}
if (centerWindow)
{
if (const QScreen *screen = QGuiApplication::primaryScreen())
{
const QRect available = screen->availableGeometry();
move(available.center() - rect().center());
}
}
}
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)
{
const bool currentlyOnTop = windowFlags().testFlag(Qt::WindowStaysOnTopHint);
if (m_alwaysOnTop == enabled && currentlyOnTop == enabled)
{
return;
}
m_alwaysOnTop = enabled;
const bool wasVisible = isVisible();
Qt::WindowFlags flags = windowFlags();
if (enabled)
{
flags |= Qt::WindowStaysOnTopHint;
}
else
{
flags &= ~Qt::WindowStaysOnTopHint;
}
setWindowFlags(flags);
if (wasVisible)
{
show();
}
}