* Merged PR 10748585: Recall | Connect UserActivity to support restoring from snapshots

## What
Thanks to @<Brendan Elliott ⌨>  for his PoC !10573092
This PR is going to use `UserActivity` APIs to connect the app to Recall so that we can take a snapshot when asked and then retore our states from the snapshot later.

## How
- Add a new type `SnapshotLaunchArguments` to identify a protocol launch requested by Recall.
- Add an extension `LaunchExtensions` as helper to retrieve key information from fundamental types.
- Refactor `App::OnActivated()` and `MainPage::OnNavigatedTo()` to handle different protocols properly.
- Create or parse `UserActivity` in `MainPage` in the way like what the PoC is doing.
- Improve the coding style a bit for `MainPage`.

## Note
The serialization work and restoring from JSON is going to be done in a separate PR.

## Testing
Manually tested.
Some typical test cases:
- Launch the app from *Start menu*
- Launch the app from *Task bar*
- Launch the app from *Command line*
- Launch with the protocol for Recall
- Launch with the protocol that is injected with evil data.

Related work items: #50854714

* Merged PR 10741448: [Recall] Snapshot saving and restoring

####What
According to PM [Spec](https://microsoft.sharepoint-df.com/:w:/t/PAXEssentialExperiences421/EcpP5tGRtFdIsRrP84ueRfUBjb6tfayxWtF9ujvJuNx6Dg?e=AeRzVf), saving and restoring Calculator snapshot when required.

The current snapshot supports to:
- Restore the calculator mode.
- Restore the current calculation (display value and expression).
- Restore the history of calculations (either in Standard mode or Scientific mode) shown at the time the snapshot was taken.
- Restore the current calculation error state, if applicable.

####How
- Added `SnapshotHelper` to help save and restore snapshots.
- Besides the existing snapshot information from view models, added an extra field `SnapshotVersion` in `ApplicationSnapshot` for backward compatibility.

#### Note
Unit tests will be added in a separate PR.

Related work items: #50701758

* Merged PR 10772614: Recall | Update the LaunchURI design

## What
Since `System.Uri` already has the `Segment` property which contains the parsed path blocks, Query in Uri looks too heavy and intrusive in implementation to retrieve the activity id.

## Changes
Changed the launch URI from something like `ms-calculator:///snapshot?activityId=<a guid>` to `ms-calculator:snapshots/<a guid>`

Related work items: #50854714

* Merged PR 10778666: Recall | Show error dialog if launching from snapshot has failed

## What
Per Figma design, we can show an error dialog with messages for the failures happens during snapshot launch.

## Notes
- Fixed a crash about taking a snapshot when Calculator hasn't initialized a standard calculator.
- Simplified the restore path.

## Screenshot
![image.png](https://dev.azure.com/microsoft/d1a24475-535d-4c83-988a-9491e6dbc858/_apis/git/repositories/6255259e-4ead-4d8d-b165-55eeacc5ca48/pullRequests/10778666/attachments/image.png)

Related work items: #50858262

* Merged PR 10790341: [Recall] Update calculator engine with snapshot for further calculations

####What
When restoring from snapshot, we need to set calculator engine properly to make further calculations correct.

####How
Update calculator engine by a serial of corresponding commands from snapshot.

To get the commands for the display area when saving snapshot,
1. If the expression is not from history and the primary display is the result of the expression, `DisplayCommands` of `StandardCalculatorSnapshot` will be empty, and we will use the commands from `ExpressionDisplay` for restoring in the future.
2. If the expression is not from history and the primary display is not the result of the expression, `DisplayCommands` of `StandardCalculatorSnapshot` will be the commands from the history collector in addition to the operand command in the primary display, and it will be used for restoring in the future.
3. If the expression and primary display are from history, `DisplayCommands` will be incomplete with the operand command in the primary display missing as by current design of history, and the commands from `ExpressionDisplay` will be used for restoring in the future.

Related work items: #51002745

* Merged PR 10802927: Recall | Add threat model

## What
The support for Recall introduced a URI activation process allowing the Recall app to launch Calculator with desired snapshot metadata. This led to a potential security problem, and we need to address it by providing justifications.

Threat model is a well-known practice among Inbox Apps. After offline discussion, we decide to add diagrams for this Recall feature to prepare for the security review.

## Diagram preview
![image.png](https://dev.azure.com/microsoft/d1a24475-535d-4c83-988a-9491e6dbc858/_apis/git/repositories/6255259e-4ead-4d8d-b165-55eeacc5ca48/pullRequests/10802927/attachments/image.png)

![image (2).png](https://dev.azure.com/microsoft/d1a24475-535d-4c83-988a-9491e6dbc858/_apis/git/repositories/6255259e-4ead-4d8d-b165-55eeacc5ca48/pullRequests/10802927/attachments/image%20%282%29.png)

Related work items: #51165486

* Merged PR 10794979: Add Recall Telemetry

**Snapshot**
EventName: `RecallSnapshot`
Payload example:` { "CalcMode": "Standard"}`
**_Fires when a snapshot (UserActivityRequested) is triggered_**

**Restore**
EventName: `RecallRestore`
Payload example:`{ "CalcMode": "Standard"}`
**_Fires when launching by snapshot (recall restore)**

**Error**
EventName: `Exception`
Payload example: `{ "CalcMode": "Standard", "FunctionName" : "MainPage::ShowSnapshotLaunchErrorAsync", "Message": "SnapshotRestoreError" }`
**_Fires when launching by snapshot failed_**

Related work items: #51114542

---------

Co-authored-by: Tian Liao  <tilia@microsoft.com>
Co-authored-by: Jian Zhang <zjian@microsoft.com>
This commit is contained in:
hanzhang54 2024-05-28 16:08:27 +08:00 коммит произвёл GitHub
Родитель e0a17f43b0
Коммит 90af5adcd2
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
18 изменённых файлов: 1106 добавлений и 140 удалений

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -3,7 +3,6 @@
#include "Header Files/CalcEngine.h"
#include "Command.h"
#include "ExpressionCommand.h"
#include "winerror_cross_platform.h"
constexpr int ASCII_0 = 48;
@ -66,47 +65,7 @@ CHistoryCollector::~CHistoryCollector()
void CHistoryCollector::AddOpndToHistory(wstring_view numStr, Rational const& rat, bool fRepetition)
{
std::shared_ptr<std::vector<int>> commands = std::make_shared<vector<int>>();
// Check for negate
bool fNegative = (numStr[0] == L'-');
bool fSciFmt = false;
bool fDecimal = false;
for (size_t i = (fNegative ? 1 : 0); i < numStr.length(); i++)
{
if (numStr[i] == m_decimalSymbol)
{
commands->push_back(IDC_PNT);
if (!fSciFmt)
{
fDecimal = true;
}
}
else if (numStr[i] == L'e')
{
commands->push_back(IDC_EXP);
fSciFmt = true;
}
else if (numStr[i] == L'-')
{
commands->push_back(IDC_SIGN);
}
else if (numStr[i] == L'+')
{
// Ignore.
}
// Number
else
{
int num = static_cast<int>(numStr[i]) - ASCII_0;
num += IDC_0;
commands->push_back(num);
}
}
auto operandCommand = std::make_shared<COpndCommand>(commands, fNegative, fDecimal, fSciFmt);
operandCommand->Initialize(rat);
int iCommandEnd = AddCommand(operandCommand);
int iCommandEnd = AddCommand(GetOperandCommandsFromString(numStr, rat));
m_lastOpStartIndex = IchAddSzToEquationSz(numStr, iCommandEnd);
if (fRepetition)
@ -201,7 +160,7 @@ void CHistoryCollector::EnclosePrecInversionBrackets()
IchAddSzToEquationSz(CCalcEngine::OpCodeToString(IDC_CLOSEP), -1);
}
bool CHistoryCollector::FOpndAddedToHistory()
bool CHistoryCollector::FOpndAddedToHistory() const
{
return (-1 != m_lastOpStartIndex);
}
@ -465,7 +424,7 @@ void CHistoryCollector::SetDecimalSymbol(wchar_t decimalSymbol)
}
// Update the commands corresponding to the passed string Number
std::shared_ptr<std::vector<int>> CHistoryCollector::GetOperandCommandsFromString(wstring_view numStr)
std::shared_ptr<std::vector<int>> CHistoryCollector::GetOperandCommandsFromString(wstring_view numStr) const
{
std::shared_ptr<std::vector<int>> commands = std::make_shared<std::vector<int>>();
// Check for negate
@ -505,3 +464,58 @@ std::shared_ptr<std::vector<int>> CHistoryCollector::GetOperandCommandsFromStrin
}
return commands;
}
std::shared_ptr<COpndCommand> CHistoryCollector::GetOperandCommandsFromString(std::wstring_view numStr, Rational const& rat) const
{
std::shared_ptr<std::vector<int>> commands = std::make_shared<vector<int>>();
// Check for negate
bool fNegative = (numStr[0] == L'-');
bool fSciFmt = false;
bool fDecimal = false;
for (size_t i = (fNegative ? 1 : 0); i < numStr.length(); i++)
{
if (numStr[i] == m_decimalSymbol)
{
commands->push_back(IDC_PNT);
if (!fSciFmt)
{
fDecimal = true;
}
}
else if (numStr[i] == L'e')
{
commands->push_back(IDC_EXP);
fSciFmt = true;
}
else if (numStr[i] == L'-')
{
commands->push_back(IDC_SIGN);
}
else if (numStr[i] == L'+')
{
// Ignore.
}
// Number
else
{
int num = static_cast<int>(numStr[i]) - ASCII_0;
num += IDC_0;
commands->push_back(num);
}
}
auto operandCommand = std::make_shared<COpndCommand>(commands, fNegative, fDecimal, fSciFmt);
operandCommand->Initialize(rat);
return operandCommand;
}
std::vector<std::shared_ptr<IExpressionCommand>> CHistoryCollector::GetCommands() const
{
std::vector<std::shared_ptr<IExpressionCommand>> commands;
if (m_spCommands != nullptr)
{
commands = *m_spCommands;
}
return commands;
}

Просмотреть файл

@ -202,3 +202,13 @@ wchar_t CCalcEngine::DecimalSeparator() const
{
return m_decimalSeparator;
}
std::vector<std::shared_ptr<IExpressionCommand>> CCalcEngine::GetHistoryCollectorCommandsSnapshot() const
{
auto commands = m_HistoryCollector.GetCommands();
if (!m_HistoryCollector.FOpndAddedToHistory() && m_bRecord)
{
commands.push_back(m_HistoryCollector.GetOperandCommandsFromString(m_numberString, m_currentVal));
}
return commands;
}

Просмотреть файл

@ -477,16 +477,25 @@ namespace CalculationManager
}
}
vector<shared_ptr<HISTORYITEM>> const& CalculatorManager::GetHistoryItems()
vector<shared_ptr<HISTORYITEM>> const& CalculatorManager::GetHistoryItems() const
{
return m_pHistory->GetHistory();
}
vector<shared_ptr<HISTORYITEM>> const& CalculatorManager::GetHistoryItems(_In_ CalculatorMode mode)
vector<shared_ptr<HISTORYITEM>> const& CalculatorManager::GetHistoryItems(_In_ CalculatorMode mode) const
{
return (mode == CalculatorMode::Standard) ? m_pStdHistory->GetHistory() : m_pSciHistory->GetHistory();
}
void CalculatorManager::SetHistoryItems(_In_ std::vector<std::shared_ptr<HISTORYITEM>> const& historyItems)
{
for (auto const& historyItem : historyItems)
{
auto index = m_pHistory->AddItem(historyItem);
OnHistoryItemAdded(index);
}
}
shared_ptr<HISTORYITEM> const& CalculatorManager::GetHistoryItem(_In_ unsigned int uIdx)
{
return m_pHistory->GetHistoryItem(uIdx);
@ -588,4 +597,9 @@ namespace CalculationManager
{
m_inHistoryItemLoadMode = isHistoryItemLoadMode;
}
std::vector<std::shared_ptr<IExpressionCommand>> CalculatorManager::GetDisplayCommandsSnapshot() const
{
return m_currentCalculatorEngine->GetHistoryCollectorCommandsSnapshot();
}
}

Просмотреть файл

@ -107,8 +107,9 @@ namespace CalculationManager
void UpdateMaxIntDigits();
wchar_t DecimalSeparator();
std::vector<std::shared_ptr<HISTORYITEM>> const& GetHistoryItems();
std::vector<std::shared_ptr<HISTORYITEM>> const& GetHistoryItems(_In_ CalculatorMode mode);
std::vector<std::shared_ptr<HISTORYITEM>> const& GetHistoryItems() const;
std::vector<std::shared_ptr<HISTORYITEM>> const& GetHistoryItems(_In_ CalculatorMode mode) const;
void SetHistoryItems(_In_ std::vector<std::shared_ptr<HISTORYITEM>> const& historyItems);
std::shared_ptr<HISTORYITEM> const& GetHistoryItem(_In_ unsigned int uIdx);
bool RemoveHistoryItem(_In_ unsigned int uIdx);
void ClearHistory();
@ -118,5 +119,6 @@ namespace CalculationManager
}
CalculationManager::Command GetCurrentDegreeMode();
void SetInHistoryItemLoadMode(_In_ bool isHistoryItemLoadMode);
std::vector<std::shared_ptr<IExpressionCommand>> GetDisplayCommandsSnapshot() const;
};
}

Просмотреть файл

@ -89,6 +89,8 @@ public:
void UpdateMaxIntDigits();
wchar_t DecimalSeparator() const;
std::vector<std::shared_ptr<IExpressionCommand>> GetHistoryCollectorCommandsSnapshot() const;
// Static methods for the instance
static void
InitialOneTimeOnlySetup(CalculationManager::IResourceProvider& resourceProvider); // Once per load time to call to initialize all shared global variables

Просмотреть файл

@ -4,10 +4,13 @@
#pragma once
#include <array>
#include "ExpressionCommand.h"
#include "ICalcDisplay.h"
#include "IHistoryDisplay.h"
#include "Rational.h"
class COpndCommand;
// maximum depth you can get by precedence. It is just an array's size limit.
static constexpr size_t MAXPRECDEPTH = 25;
@ -29,13 +32,15 @@ public:
void PushLastOpndStart(int ichOpndStart = -1);
void PopLastOpndStart();
void EnclosePrecInversionBrackets();
bool FOpndAddedToHistory();
bool FOpndAddedToHistory() const;
void CompleteHistoryLine(std::wstring_view numStr);
void CompleteEquation(std::wstring_view numStr);
void ClearHistoryLine(std::wstring_view errStr);
int AddCommand(_In_ const std::shared_ptr<IExpressionCommand>& spCommand);
void UpdateHistoryExpression(uint32_t radix, int32_t precision);
void SetDecimalSymbol(wchar_t decimalSymbol);
std::shared_ptr<COpndCommand> GetOperandCommandsFromString(std::wstring_view numStr, CalcEngine::Rational const& rat) const;
std::vector<std::shared_ptr<IExpressionCommand>> GetCommands() const;
private:
std::shared_ptr<IHistoryDisplay> m_pHistoryDisplay;
@ -60,5 +65,5 @@ private:
void TruncateEquationSzFromIch(int ich);
void SetExpressionDisplay();
void InsertSzInEquationSz(std::wstring_view str, int icommandIndex, int ich);
std::shared_ptr<std::vector<int>> GetOperandCommandsFromString(std::wstring_view numStr);
std::shared_ptr<std::vector<int>> GetOperandCommandsFromString(std::wstring_view numStr) const;
};

Просмотреть файл

@ -18,7 +18,6 @@ using namespace CalculatorApp::ViewModel;
using namespace CalculationManager;
using namespace Platform;
using namespace Platform::Collections;
using namespace std;
using namespace Windows::System;
using namespace Windows::Storage;
using namespace Utils;
@ -38,6 +37,446 @@ namespace
{
StringReference CategoriesPropertyName(L"Categories");
StringReference ClearMemoryVisibilityPropertyName(L"ClearMemoryVisibility");
struct SnapshotHelper
{
static constexpr int SnapshotVersion = 0;
static Windows::Data::Json::JsonObject ^ SaveSnapshotToJson(const ApplicationSnapshot& value)
{
auto jsonObject = ref new Windows::Data::Json::JsonObject();
jsonObject->SetNamedValue(L"SnapshotVersion", Windows::Data::Json::JsonValue::CreateNumberValue(value.SnapshotVersion));
jsonObject->SetNamedValue(L"Mode", Windows::Data::Json::JsonValue::CreateNumberValue(value.Mode));
if (value.StandardCalc.has_value())
{
jsonObject->SetNamedValue(L"StandardCalculatorSnapshot", SaveSnapshotToJson(*value.StandardCalc));
}
return jsonObject;
}
static Windows::Data::Json::JsonObject ^ SaveSnapshotToJson(const StandardCalculatorSnapshot& value)
{
auto jsonObject = ref new Windows::Data::Json::JsonObject();
jsonObject->SetNamedValue(L"CalculatorManagerSnapshot", SaveSnapshotToJson(value.CalcManager));
jsonObject->SetNamedValue(L"PrimaryDisplay", SaveSnapshotToJson(value.PrimaryDisplay));
if (value.ExpressionDisplay.has_value())
{
jsonObject->SetNamedValue(L"ExpressionDisplay", SaveSnapshotToJson(*value.ExpressionDisplay));
}
auto commandsJsonArray = ref new Windows::Data::Json::JsonArray();
for (const auto& command : value.DisplayCommands)
{
commandsJsonArray->Append(SaveSnapshotToJson(command));
}
jsonObject->SetNamedValue(L"DisplayCommands", commandsJsonArray);
return jsonObject;
}
static Windows::Data::Json::JsonObject ^ SaveSnapshotToJson(const PrimaryDisplaySnapshot& value)
{
auto jsonObject = ref new Windows::Data::Json::JsonObject();
jsonObject->SetNamedValue(L"DisplayValue", Windows::Data::Json::JsonValue::CreateStringValue(value.DisplayValue));
jsonObject->SetNamedValue(L"IsError", Windows::Data::Json::JsonValue::CreateBooleanValue(value.IsError));
return jsonObject;
}
static Windows::Data::Json::JsonObject ^ SaveSnapshotToJson(const ExpressionDisplaySnapshot& value)
{
auto jsonObject = ref new Windows::Data::Json::JsonObject();
auto tokensJsonArray = ref new Windows::Data::Json::JsonArray();
for (const auto& token : value.Tokens)
{
auto tokenJsonArray = ref new Windows::Data::Json::JsonArray();
tokenJsonArray->Append(Windows::Data::Json::JsonValue::CreateStringValue(ref new Platform::String(token.first.c_str())));
tokenJsonArray->Append(Windows::Data::Json::JsonValue::CreateNumberValue(token.second));
tokensJsonArray->Append(tokenJsonArray);
}
jsonObject->SetNamedValue(L"Tokens", tokensJsonArray);
auto commandsJsonArray = ref new Windows::Data::Json::JsonArray();
for (const auto& command : value.Commands)
{
commandsJsonArray->Append(SaveSnapshotToJson(command));
}
jsonObject->SetNamedValue(L"Commands", commandsJsonArray);
return jsonObject;
}
static Windows::Data::Json::JsonObject ^ SaveSnapshotToJson(const CalculatorManagerSnapshot& value)
{
auto jsonObject = ref new Windows::Data::Json::JsonObject();
if (value.HistoryItems.has_value())
{
auto historyJsonArray = ref new Windows::Data::Json::JsonArray();
for (const auto& item : *value.HistoryItems)
{
historyJsonArray->Append(SaveSnapshotToJson(*item));
}
jsonObject->SetNamedValue(L"HistoryItems", historyJsonArray);
}
return jsonObject;
}
static Windows::Data::Json::JsonObject ^ SaveSnapshotToJson(const CalculationManager::HISTORYITEM& value)
{
auto jsonObject = ref new Windows::Data::Json::JsonObject();
jsonObject->SetNamedValue(L"Expression", Windows::Data::Json::JsonValue::CreateStringValue(ref new Platform::String(value.historyItemVector.expression.c_str())));
jsonObject->SetNamedValue(L"Result", Windows::Data::Json::JsonValue::CreateStringValue(ref new Platform::String(value.historyItemVector.result.c_str())));
auto tokensJsonArray = ref new Windows::Data::Json::JsonArray();
for (const auto& token : *value.historyItemVector.spTokens)
{
auto tokenJsonArray = ref new Windows::Data::Json::JsonArray();
tokenJsonArray->Append(Windows::Data::Json::JsonValue::CreateStringValue(ref new Platform::String(token.first.c_str())));
tokenJsonArray->Append(Windows::Data::Json::JsonValue::CreateNumberValue(token.second));
tokensJsonArray->Append(tokenJsonArray);
}
jsonObject->SetNamedValue(L"Tokens", tokensJsonArray);
auto commandsJsonArray = ref new Windows::Data::Json::JsonArray();
for (const auto& command : *value.historyItemVector.spCommands)
{
commandsJsonArray->Append(SaveSnapshotToJson(command));
}
jsonObject->SetNamedValue(L"Commands", commandsJsonArray);
return jsonObject;
}
static Windows::Data::Json::JsonObject ^ SaveSnapshotToJson(const std::shared_ptr<IExpressionCommand>& value)
{
auto jsonObject = ref new Windows::Data::Json::JsonObject();
auto opndCommand = dynamic_cast<COpndCommand*>(value.get());
if (opndCommand != nullptr)
{
jsonObject = SaveSnapshotToJson(*opndCommand);
}
auto unaryCommand = dynamic_cast<CUnaryCommand*>(value.get());
if (unaryCommand != nullptr)
{
jsonObject = SaveSnapshotToJson(*unaryCommand);
}
auto binaryCommand = dynamic_cast<CBinaryCommand*>(value.get());
if (binaryCommand != nullptr)
{
jsonObject = SaveSnapshotToJson(*binaryCommand);
}
auto parenthesesCommand = dynamic_cast<CParentheses*>(value.get());
if (parenthesesCommand != nullptr)
{
jsonObject = SaveSnapshotToJson(*parenthesesCommand);
}
return jsonObject;
}
static Windows::Data::Json::JsonObject ^ SaveSnapshotToJson(const COpndCommand& value)
{
auto jsonObject = ref new Windows::Data::Json::JsonObject();
jsonObject->SetNamedValue(L"CommandType", Windows::Data::Json::JsonValue::CreateNumberValue(static_cast<double>(value.GetCommandType())));
jsonObject->SetNamedValue(L"IsNegative", Windows::Data::Json::JsonValue::CreateBooleanValue(value.IsNegative()));
jsonObject->SetNamedValue(L"IsDecimalPresent", Windows::Data::Json::JsonValue::CreateBooleanValue(value.IsDecimalPresent()));
jsonObject->SetNamedValue(L"IsSciFmt", Windows::Data::Json::JsonValue::CreateBooleanValue(value.IsSciFmt()));
auto commandsJsonArray = ref new Windows::Data::Json::JsonArray();
for (const auto& command : *value.GetCommands())
{
commandsJsonArray->Append(Windows::Data::Json::JsonValue::CreateNumberValue(command));
}
jsonObject->SetNamedValue(L"Commands", commandsJsonArray);
return jsonObject;
}
static Windows::Data::Json::JsonObject ^ SaveSnapshotToJson(const CUnaryCommand& value)
{
auto jsonObject = ref new Windows::Data::Json::JsonObject();
jsonObject->SetNamedValue(L"CommandType", Windows::Data::Json::JsonValue::CreateNumberValue(static_cast<double>(value.GetCommandType())));
auto commandsJsonArray = ref new Windows::Data::Json::JsonArray();
for (const auto& command : *value.GetCommands())
{
commandsJsonArray->Append(Windows::Data::Json::JsonValue::CreateNumberValue(command));
}
jsonObject->SetNamedValue(L"Commands", commandsJsonArray);
return jsonObject;
}
static Windows::Data::Json::JsonObject ^ SaveSnapshotToJson(const CBinaryCommand& value)
{
auto jsonObject = ref new Windows::Data::Json::JsonObject();
jsonObject->SetNamedValue(L"CommandType", Windows::Data::Json::JsonValue::CreateNumberValue(static_cast<double>(value.GetCommandType())));
jsonObject->SetNamedValue(L"Command", Windows::Data::Json::JsonValue::CreateNumberValue(value.GetCommand()));
return jsonObject;
}
static Windows::Data::Json::JsonObject ^ SaveSnapshotToJson(const CParentheses& value)
{
auto jsonObject = ref new Windows::Data::Json::JsonObject();
jsonObject->SetNamedValue(L"CommandType", Windows::Data::Json::JsonValue::CreateNumberValue(static_cast<double>(value.GetCommandType())));
jsonObject->SetNamedValue(L"Command", Windows::Data::Json::JsonValue::CreateNumberValue(value.GetCommand()));
return jsonObject;
}
static void RestoreJsonToSnapshot(Windows::Data::Json::JsonObject ^ jsonObject, std::optional<ApplicationSnapshot>& value)
{
ApplicationSnapshot applicationSnapshot;
applicationSnapshot.SnapshotVersion = static_cast<int>(jsonObject->GetNamedNumber(L"SnapshotVersion"));
if (applicationSnapshot.SnapshotVersion > SnapshotVersion)
{
return;
}
applicationSnapshot.Mode = static_cast<int>(jsonObject->GetNamedNumber(L"Mode"));
if (jsonObject->HasKey(L"StandardCalculatorSnapshot"))
{
std::optional<StandardCalculatorSnapshot> standardCalculatorSnapshot;
RestoreJsonToSnapshot(jsonObject->GetNamedObject(L"StandardCalculatorSnapshot"), standardCalculatorSnapshot);
if (standardCalculatorSnapshot.has_value())
{
applicationSnapshot.StandardCalc = std::move(*standardCalculatorSnapshot);
}
else
{
return;
}
}
value = std::move(applicationSnapshot);
}
static void RestoreJsonToSnapshot(Windows::Data::Json::JsonObject ^ jsonObject, std::optional<StandardCalculatorSnapshot>& value)
{
StandardCalculatorSnapshot standardCalculatorSnapshot;
std::optional<CalculatorManagerSnapshot> calcManagerSnapshot;
RestoreJsonToSnapshot(jsonObject->GetNamedObject(L"CalculatorManagerSnapshot"), calcManagerSnapshot);
if (calcManagerSnapshot.has_value())
{
standardCalculatorSnapshot.CalcManager = std::move(*calcManagerSnapshot);
}
else
{
return;
}
std::optional<PrimaryDisplaySnapshot> primaryDisplaySnapshot;
RestoreJsonToSnapshot(jsonObject->GetNamedObject(L"PrimaryDisplay"), primaryDisplaySnapshot);
if (primaryDisplaySnapshot.has_value())
{
standardCalculatorSnapshot.PrimaryDisplay = std::move(*primaryDisplaySnapshot);
}
else
{
return;
}
if (jsonObject->HasKey(L"ExpressionDisplay"))
{
std::optional<ExpressionDisplaySnapshot> expressionDisplaySnapshot;
RestoreJsonToSnapshot(jsonObject->GetNamedObject(L"ExpressionDisplay"), expressionDisplaySnapshot);
if (expressionDisplaySnapshot.has_value())
{
standardCalculatorSnapshot.ExpressionDisplay = std::move(*expressionDisplaySnapshot);
}
else
{
return;
}
}
standardCalculatorSnapshot.DisplayCommands = RestoreExpressionCommandsFromJsonArray(jsonObject->GetNamedArray(L"DisplayCommands"));
value = std::move(standardCalculatorSnapshot);
}
static void RestoreJsonToSnapshot(Windows::Data::Json::JsonObject ^ jsonObject, std::optional<PrimaryDisplaySnapshot>& value)
{
value = PrimaryDisplaySnapshot{ jsonObject->GetNamedString(L"DisplayValue"), jsonObject->GetNamedBoolean(L"IsError") };
}
static void RestoreJsonToSnapshot(Windows::Data::Json::JsonObject ^ jsonObject, std::optional<ExpressionDisplaySnapshot>& value)
{
ExpressionDisplaySnapshot expressionDisplaySnapshot;
expressionDisplaySnapshot.Tokens = RestoreExpressionTokensFromJsonArray(jsonObject->GetNamedArray(L"Tokens"));
if (expressionDisplaySnapshot.Tokens.empty())
{
return;
}
expressionDisplaySnapshot.Commands = RestoreExpressionCommandsFromJsonArray(jsonObject->GetNamedArray(L"Commands"));
if (expressionDisplaySnapshot.Commands.empty())
{
return;
}
value = std::move(expressionDisplaySnapshot);
}
static void RestoreJsonToSnapshot(Windows::Data::Json::JsonObject ^ jsonObject, std::optional<CalculatorManagerSnapshot>& value)
{
CalculatorManagerSnapshot calcManagerSnapshot;
if (jsonObject->HasKey(L"HistoryItems"))
{
std::vector<std::shared_ptr<CalculationManager::HISTORYITEM>> historyItems;
auto historyJsonArray = jsonObject->GetNamedArray(L"HistoryItems");
for (uint32_t i = 0; i < historyJsonArray->Size; ++i)
{
std::optional<CalculationManager::HISTORYITEM> historyItem;
RestoreJsonToSnapshot(historyJsonArray->GetObjectAt(i), historyItem);
if (historyItem.has_value())
{
historyItems.push_back(std::make_shared<CalculationManager::HISTORYITEM>(*historyItem));
}
else
{
return;
}
}
calcManagerSnapshot.HistoryItems = std::move(historyItems);
}
value = std::move(calcManagerSnapshot);
}
static void RestoreJsonToSnapshot(Windows::Data::Json::JsonObject ^ jsonObject, std::optional<CalculationManager::HISTORYITEM>& value)
{
CalculationManager::HISTORYITEM historyItem;
historyItem.historyItemVector.expression = std::wstring(jsonObject->GetNamedString(L"Expression")->Data());
historyItem.historyItemVector.result = std::wstring(jsonObject->GetNamedString(L"Result")->Data());
historyItem.historyItemVector.spTokens =
std::make_shared<std::vector<std::pair<std::wstring, int>>>(RestoreExpressionTokensFromJsonArray(jsonObject->GetNamedArray(L"Tokens")));
if (historyItem.historyItemVector.spTokens->empty())
{
return;
}
historyItem.historyItemVector.spCommands = std::make_shared<std::vector<std::shared_ptr<IExpressionCommand>>>(
RestoreExpressionCommandsFromJsonArray(jsonObject->GetNamedArray(L"Commands")));
if (historyItem.historyItemVector.spCommands->empty())
{
return;
}
value = std::move(historyItem);
}
static void RestoreJsonToSnapshot(Windows::Data::Json::JsonObject ^ jsonObject, std::optional<std::shared_ptr<IExpressionCommand>>& value)
{
auto commandType = static_cast<CalculationManager::CommandType>(jsonObject->GetNamedNumber(L"CommandType"));
switch (commandType)
{
case CalculationManager::CommandType::OperandCommand:
{
std::optional<COpndCommand> opndCommand;
RestoreJsonToSnapshot(jsonObject, opndCommand);
if (opndCommand.has_value())
{
value = std::make_shared<COpndCommand>(*opndCommand);
}
break;
}
case CalculationManager::CommandType::UnaryCommand:
{
std::optional<CUnaryCommand> unaryCommand;
RestoreJsonToSnapshot(jsonObject, unaryCommand);
if (unaryCommand.has_value())
{
value = std::make_shared<CUnaryCommand>(*unaryCommand);
}
break;
}
case CalculationManager::CommandType::BinaryCommand:
{
std::optional<CBinaryCommand> binaryCommand;
RestoreJsonToSnapshot(jsonObject, binaryCommand);
if (binaryCommand.has_value())
{
value = std::make_shared<CBinaryCommand>(*binaryCommand);
}
break;
}
case CalculationManager::CommandType::Parentheses:
{
std::optional<CParentheses> parenthesesCommand;
RestoreJsonToSnapshot(jsonObject, parenthesesCommand);
if (parenthesesCommand.has_value())
{
value = std::make_shared<CParentheses>(*parenthesesCommand);
}
break;
}
default:
throw std::logic_error{ "c8cba597-dfec-447a-bd1c-e78a9ffaad95" };
}
}
static void RestoreJsonToSnapshot(Windows::Data::Json::JsonObject ^ jsonObject, std::optional<COpndCommand>& value)
{
auto isNegative = jsonObject->GetNamedBoolean(L"IsNegative");
auto isDecimalPresent = jsonObject->GetNamedBoolean(L"IsDecimalPresent");
auto isSciFmt = jsonObject->GetNamedBoolean(L"IsSciFmt");
std::vector<int> commands;
auto commandsJsonArray = jsonObject->GetNamedArray(L"Commands");
for (uint32_t i = 0; i < commandsJsonArray->Size; ++i)
{
commands.push_back(static_cast<int>(commandsJsonArray->GetNumberAt(i)));
}
value = COpndCommand(std::make_shared<std::vector<int>>(std::move(commands)), isNegative, isDecimalPresent, isSciFmt);
}
static void RestoreJsonToSnapshot(Windows::Data::Json::JsonObject ^ jsonObject, std::optional<CUnaryCommand>& value)
{
std::vector<int> commands;
auto commandsJsonArray = jsonObject->GetNamedArray(L"Commands");
if (commandsJsonArray->Size == 1)
{
value = CUnaryCommand(static_cast<int>(commandsJsonArray->GetNumberAt(0)));
}
else if (commandsJsonArray->Size == 2)
{
value = CUnaryCommand(static_cast<int>(commandsJsonArray->GetNumberAt(0)), static_cast<int>(commandsJsonArray->GetNumberAt(1)));
}
}
static void RestoreJsonToSnapshot(Windows::Data::Json::JsonObject ^ jsonObject, std::optional<CBinaryCommand>& value)
{
value = CBinaryCommand(static_cast<int>(jsonObject->GetNamedNumber(L"Command")));
}
static void RestoreJsonToSnapshot(Windows::Data::Json::JsonObject ^ jsonObject, std::optional<CParentheses>& value)
{
value = CParentheses(static_cast<int>(jsonObject->GetNamedNumber(L"Command")));
}
static std::vector<std::pair<std::wstring, int>> RestoreExpressionTokensFromJsonArray(Windows::Data::Json::JsonArray ^ jsonArray)
{
std::vector<std::pair<std::wstring, int>> tokens;
for (uint32_t i = 0; i < jsonArray->Size; ++i)
{
auto tokenJsonArray = jsonArray->GetArrayAt(i);
if (tokenJsonArray->Size == 2 && tokenJsonArray->GetAt(0)->ValueType == Windows::Data::Json::JsonValueType::String
&& tokenJsonArray->GetAt(1)->ValueType == Windows::Data::Json::JsonValueType::Number)
{
tokens.emplace_back(std::wstring(tokenJsonArray->GetAt(0)->GetString()->Data()), static_cast<int>(tokenJsonArray->GetAt(1)->GetNumber()));
}
else
{
return {};
}
}
return tokens;
}
static std::vector<std::shared_ptr<IExpressionCommand>> RestoreExpressionCommandsFromJsonArray(Windows::Data::Json::JsonArray ^ jsonArray)
{
std::vector<std::shared_ptr<IExpressionCommand>> commands;
for (uint32_t i = 0; i < jsonArray->Size; ++i)
{
std::optional<std::shared_ptr<IExpressionCommand>> command;
RestoreJsonToSnapshot(jsonArray->GetObjectAt(i), command);
if (command.has_value())
{
commands.push_back(*command);
}
else
{
return {};
}
}
return commands;
}
static bool IsJsonParsingException(Platform::COMException ^ e)
{
return e->HResult == WEB_E_JSON_VALUE_NOT_FOUND || e->HResult == E_ILLEGAL_METHOD_CALL;
}
};
}
ApplicationViewModel::ApplicationViewModel()
@ -151,9 +590,9 @@ void ApplicationViewModel::OnModeChanged()
{
if (!m_ConverterViewModel)
{
auto dataLoader = make_shared<UnitConverterDataLoader>(ref new GeographicRegion());
auto currencyDataLoader = make_shared<CurrencyDataLoader>(make_unique<CurrencyHttpClient>());
m_ConverterViewModel = ref new UnitConverterViewModel(make_shared<UnitConversionManager::UnitConverter>(dataLoader, currencyDataLoader));
auto dataLoader = std::make_shared<UnitConverterDataLoader>(ref new GeographicRegion());
auto currencyDataLoader = std::make_shared<CurrencyDataLoader>(std::make_unique<CurrencyHttpClient>());
m_ConverterViewModel = ref new UnitConverterViewModel(std::make_shared<UnitConversionManager::UnitConverter>(dataLoader, currencyDataLoader));
}
m_ConverterViewModel->Mode = m_mode;
@ -272,3 +711,48 @@ void ApplicationViewModel::SetDisplayNormalAlwaysOnTopOption()
DisplayNormalAlwaysOnTopOption =
m_mode == ViewMode::Standard && ApplicationView::GetForCurrentView()->IsViewModeSupported(ApplicationViewMode::CompactOverlay) && !IsAlwaysOnTop;
}
Windows::Data::Json::JsonObject ^ ApplicationViewModel::SaveApplicationSnapshot()
{
ApplicationSnapshot applicationSnapshot;
applicationSnapshot.SnapshotVersion = SnapshotHelper::SnapshotVersion;
applicationSnapshot.Mode = static_cast<int>(Mode);
if (m_CalculatorViewModel != nullptr && m_mode == ViewMode::Standard)
{
// Standard calculator is the only supported mode so far.
applicationSnapshot.StandardCalc = m_CalculatorViewModel->GetStandardCalculatorSnapshot();
}
return SnapshotHelper::SaveSnapshotToJson(applicationSnapshot);
}
bool ApplicationViewModel::TryRestoreFromSnapshot(Windows::Data::Json::JsonObject ^ jsonObject)
{
std::optional<ApplicationSnapshot> applicationSnapshot;
try
{
SnapshotHelper::RestoreJsonToSnapshot(jsonObject, applicationSnapshot);
}
catch (Platform::COMException ^ e)
{
if (SnapshotHelper::IsJsonParsingException(e))
{
return false;
}
throw;
}
if (applicationSnapshot.has_value())
{
Mode = static_cast<ViewMode>(applicationSnapshot->Mode);
if (applicationSnapshot->StandardCalc.has_value())
{
if (m_CalculatorViewModel == nullptr)
{
m_CalculatorViewModel = ref new StandardCalculatorViewModel();
}
m_CalculatorViewModel->SetStandardCalculatorSnapshot(applicationSnapshot->StandardCalc.value());
}
return true;
}
return false;
}

Просмотреть файл

@ -12,6 +12,13 @@ namespace CalculatorApp
{
namespace ViewModel
{
struct ApplicationSnapshot
{
int SnapshotVersion;
int Mode;
std::optional<StandardCalculatorSnapshot> StandardCalc;
};
[Windows::UI::Xaml::Data::Bindable] public ref class ApplicationViewModel sealed : public Windows::UI::Xaml::Data::INotifyPropertyChanged
{
public:
@ -96,6 +103,9 @@ namespace CalculatorApp
void ToggleAlwaysOnTop(float width, float height);
Windows::Data::Json::JsonObject ^ SaveApplicationSnapshot();
bool TryRestoreFromSnapshot(Windows::Data::Json::JsonObject ^ jsonObject);
private:
bool TryRecoverFromNavigationModeFailure();

Просмотреть файл

@ -43,6 +43,8 @@ namespace CalculatorApp
constexpr auto EVENT_NAME_VARIABLE_SETTING_CHANGED = L"VariableSettingChanged";
constexpr auto EVENT_NAME_GRAPH_SETTINGS_CHANGED = L"GraphSettingsChanged";
constexpr auto EVENT_NAME_GRAPH_THEME = L"GraphTheme";
constexpr auto EVENT_NAME_RECALL_SNAPSHOT = L"RecallSnapshot";
constexpr auto EVENT_NAME_RECALL_RESTORE = L"RecallRestore";
constexpr auto EVENT_NAME_EXCEPTION = L"Exception";
@ -328,4 +330,27 @@ namespace CalculatorApp
TraceLoggingCommon::GetInstance()->LogLevel2Event(StringReference(EVENT_NAME_GRAPH_THEME), fields);
}
void TraceLogger::LogRecallSnapshot(ViewMode mode)
{
auto fields = ref new LoggingFields();
fields->AddString(StringReference(CALC_MODE), NavCategoryStates::GetFriendlyName(mode));
TraceLoggingCommon::GetInstance()->LogLevel2Event(StringReference(EVENT_NAME_RECALL_SNAPSHOT), fields);
}
void TraceLogger::LogRecallRestore(ViewMode mode)
{
auto fields = ref new LoggingFields();
fields->AddString(StringReference(CALC_MODE), NavCategoryStates::GetFriendlyName(mode));
TraceLoggingCommon::GetInstance()->LogLevel2Event(StringReference(EVENT_NAME_RECALL_RESTORE), fields);
}
void TraceLogger::LogRecallError(CalculatorApp::ViewModel::Common::ViewMode mode, Platform::String^ message)
{
auto fields = ref new LoggingFields();
fields->AddString(StringReference(CALC_MODE), NavCategoryStates::GetFriendlyName(mode));
fields->AddString(StringReference(L"FunctionName"), L"Recall");
fields->AddString(StringReference(L"Message"), message);
TraceLoggingCommon::GetInstance()->LogLevel2Event(StringReference(EVENT_NAME_EXCEPTION), fields);
}
}

Просмотреть файл

@ -85,6 +85,9 @@ namespace CalculatorApp::ViewModel::Common
void LogGraphTheme(Platform::String ^ graphTheme);
void LogInputPasted(CalculatorApp::ViewModel::Common::ViewMode mode);
void LogPlatformExceptionInfo(CalculatorApp::ViewModel::Common::ViewMode mode, Platform::String ^ functionName, Platform::String ^ message, int hresult);
void LogRecallSnapshot(CalculatorApp::ViewModel::Common::ViewMode mode);
void LogRecallRestore(CalculatorApp::ViewModel::Common::ViewMode mode);
void LogRecallError(CalculatorApp::ViewModel::Common::ViewMode mode, Platform::String ^ message);
internal:
void LogPlatformException(CalculatorApp::ViewModel::Common::ViewMode mode, Platform::String ^ functionName, Platform::Exception ^ e);

Просмотреть файл

@ -42,6 +42,57 @@ namespace
StringReference IsBitFlipCheckedPropertyName(L"IsBitFlipChecked");
StringReference CalcAlwaysOnTop(L"CalcAlwaysOnTop");
StringReference CalcBackToFullView(L"CalcBackToFullView");
std::vector<int> GetCommandsFromExpressionCommands(const std::vector<std::shared_ptr<IExpressionCommand>>& expressionCommands)
{
vector<int> commands;
for (const auto& command : expressionCommands)
{
CommandType commandType = command->GetCommandType();
if (commandType == CommandType::UnaryCommand)
{
shared_ptr<IUnaryCommand> spCommand = dynamic_pointer_cast<IUnaryCommand>(command);
const shared_ptr<vector<int>>& unaryCommands = spCommand->GetCommands();
for (int nUCode : *unaryCommands)
{
commands.push_back(nUCode);
}
}
if (commandType == CommandType::BinaryCommand)
{
shared_ptr<IBinaryCommand> spCommand = dynamic_pointer_cast<IBinaryCommand>(command);
commands.push_back(spCommand->GetCommand());
}
if (commandType == CommandType::Parentheses)
{
shared_ptr<IParenthesisCommand> spCommand = dynamic_pointer_cast<IParenthesisCommand>(command);
commands.push_back(spCommand->GetCommand());
}
if (commandType == CommandType::OperandCommand)
{
shared_ptr<IOpndCommand> spCommand = dynamic_pointer_cast<IOpndCommand>(command);
const shared_ptr<vector<int>>& opndCommands = spCommand->GetCommands();
bool fNeedIDCSign = spCommand->IsNegative();
for (int nOCode : *opndCommands)
{
commands.push_back(nOCode);
if (fNeedIDCSign && nOCode != IDC_0)
{
commands.push_back(static_cast<int>(CalculationManager::Command::CommandSIGN));
fNeedIDCSign = false;
}
}
}
}
return commands;
}
}
namespace CalculatorResourceKeys
@ -1388,55 +1439,8 @@ void StandardCalculatorViewModel::Recalculate(bool fromHistory)
{
// Recalculate
Command currentDegreeMode = m_standardCalculatorManager.GetCurrentDegreeMode();
shared_ptr<vector<shared_ptr<IExpressionCommand>>> savedCommands = make_shared<vector<shared_ptr<IExpressionCommand>>>();
vector<int> currentCommands;
for (const auto& command : *m_commands)
{
savedCommands->push_back(command);
CommandType commandType = command->GetCommandType();
if (commandType == CommandType::UnaryCommand)
{
shared_ptr<IUnaryCommand> spCommand = dynamic_pointer_cast<IUnaryCommand>(command);
const shared_ptr<vector<int>>& unaryCommands = spCommand->GetCommands();
for (int nUCode : *unaryCommands)
{
currentCommands.push_back(nUCode);
}
}
if (commandType == CommandType::BinaryCommand)
{
shared_ptr<IBinaryCommand> spCommand = dynamic_pointer_cast<IBinaryCommand>(command);
currentCommands.push_back(spCommand->GetCommand());
}
if (commandType == CommandType::Parentheses)
{
shared_ptr<IParenthesisCommand> spCommand = dynamic_pointer_cast<IParenthesisCommand>(command);
currentCommands.push_back(spCommand->GetCommand());
}
if (commandType == CommandType::OperandCommand)
{
shared_ptr<IOpndCommand> spCommand = dynamic_pointer_cast<IOpndCommand>(command);
const shared_ptr<vector<int>>& opndCommands = spCommand->GetCommands();
bool fNeedIDCSign = spCommand->IsNegative();
for (int nOCode : *opndCommands)
{
currentCommands.push_back(nOCode);
if (fNeedIDCSign && nOCode != IDC_0)
{
currentCommands.push_back(static_cast<int>(CalculationManager::Command::CommandSIGN));
fNeedIDCSign = false;
}
}
}
}
shared_ptr<vector<shared_ptr<IExpressionCommand>>> savedCommands = std::make_shared<std::vector<shared_ptr<IExpressionCommand>>>(*m_commands);
vector<int> currentCommands = GetCommandsFromExpressionCommands(*m_commands);
shared_ptr<vector<pair<wstring, int>>> savedTokens = make_shared<vector<pair<wstring, int>>>();
@ -1782,3 +1786,50 @@ void StandardCalculatorViewModel::SetBitshiftRadioButtonCheckedAnnouncement(Plat
{
Announcement = CalculatorAnnouncement::GetBitShiftRadioButtonCheckedAnnouncement(announcement);
}
StandardCalculatorSnapshot StandardCalculatorViewModel::GetStandardCalculatorSnapshot() const
{
StandardCalculatorSnapshot snapshot;
auto& historyItems = m_standardCalculatorManager.GetHistoryItems();
if (!historyItems.empty())
{
snapshot.CalcManager.HistoryItems = std::move(historyItems);
}
snapshot.PrimaryDisplay = PrimaryDisplaySnapshot{ m_DisplayValue, m_IsInError };
if (!m_tokens->empty() && !m_commands->empty())
{
snapshot.ExpressionDisplay = { *m_tokens, *m_commands };
}
snapshot.DisplayCommands = m_standardCalculatorManager.GetDisplayCommandsSnapshot();
return snapshot;
}
void StandardCalculatorViewModel::SetStandardCalculatorSnapshot(const StandardCalculatorSnapshot& snapshot)
{
if (snapshot.CalcManager.HistoryItems.has_value())
{
m_standardCalculatorManager.SetHistoryItems(snapshot.CalcManager.HistoryItems.value());
}
std::vector<int> commands;
if (snapshot.ExpressionDisplay.has_value() && snapshot.ExpressionDisplay->Tokens.back().first == L"=")
{
commands = GetCommandsFromExpressionCommands(snapshot.ExpressionDisplay->Commands);
}
if (commands.empty() && !snapshot.DisplayCommands.empty())
{
commands = GetCommandsFromExpressionCommands(snapshot.DisplayCommands);
}
for (const auto& command : commands)
{
m_standardCalculatorManager.SendCommand(static_cast<Command>(command));
}
if (snapshot.ExpressionDisplay.has_value())
{
SetExpressionDisplay(
std::make_shared<std::vector<std::pair<std::wstring, int>>>(snapshot.ExpressionDisplay->Tokens),
std::make_shared<std::vector<std::shared_ptr<IExpressionCommand>>>(snapshot.ExpressionDisplay->Commands));
}
SetPrimaryDisplay(snapshot.PrimaryDisplay.DisplayValue, snapshot.PrimaryDisplay.IsError);
}

Просмотреть файл

@ -33,6 +33,31 @@ namespace CalculatorApp
bool canSendNegate;
};
struct CalculatorManagerSnapshot
{
std::optional<std::vector<std::shared_ptr<CalculationManager::HISTORYITEM>>> HistoryItems;
};
struct PrimaryDisplaySnapshot
{
Platform::String ^ DisplayValue;
bool IsError = false;
};
struct ExpressionDisplaySnapshot
{
std::vector<std::pair<std::wstring, int>> Tokens;
std::vector<std::shared_ptr<IExpressionCommand>> Commands;
};
struct StandardCalculatorSnapshot
{
CalculatorManagerSnapshot CalcManager;
PrimaryDisplaySnapshot PrimaryDisplay;
std::optional<ExpressionDisplaySnapshot> ExpressionDisplay;
std::vector<std::shared_ptr<IExpressionCommand>> DisplayCommands;
};
[Windows::UI::Xaml::Data::Bindable] public ref class StandardCalculatorViewModel sealed : public Windows::UI::Xaml::Data::INotifyPropertyChanged
{
public:
@ -294,6 +319,9 @@ namespace CalculatorApp
{
return m_CurrentAngleType;
}
StandardCalculatorSnapshot GetStandardCalculatorSnapshot() const;
void SetStandardCalculatorSnapshot(const StandardCalculatorSnapshot& state);
private:
void SetMemorizedNumbers(const std::vector<std::wstring>& memorizedNumbers);

Просмотреть файл

@ -75,13 +75,26 @@ namespace CalculatorApp
{
if (args.Kind == ActivationKind.Protocol)
{
// We currently don't pass the uri as an argument,
// and handle any protocol launch as a normal app launch.
OnAppLaunch(args, null, false);
if (args.IsSnapshotProtocol())
{
var protoArgs = (IProtocolActivatedEventArgs)args;
OnAppLaunch(args,
new SnapshotLaunchArguments
{
ActivityId = protoArgs.Uri.GetActivityId(),
LaunchUri = protoArgs.Uri
},
false);
}
else
{
// handle any unknown protocol launch as a normal app launch.
OnAppLaunch(args, null, false);
}
}
}
private void OnAppLaunch(IActivatedEventArgs args, string argument, bool isPreLaunch)
private void OnAppLaunch(IActivatedEventArgs args, object arguments, bool isPreLaunch)
{
// Uncomment the following lines to display frame-rate and per-frame CPU usage info.
//#if DEBUG
@ -132,7 +145,7 @@ namespace CalculatorApp
// When the navigation stack isn't restored navigate to the first page,
// configuring the new page by passing required information as a navigation
// parameter
if (rootFrame.Content == null && !rootFrame.Navigate(typeof(MainPage), argument))
if (rootFrame.Content == null && !rootFrame.Navigate(typeof(MainPage), arguments))
{
// We couldn't navigate to the main page, kill the app so we have a good
// stack to debug

Просмотреть файл

@ -144,6 +144,7 @@
<Compile Include="Common\AlwaysSelectedCollectionView.cs" />
<Compile Include="Common\AppLifecycleLogger.cs" />
<Compile Include="Common\KeyboardShortcutManager.cs" />
<Compile Include="Common\LaunchArguments.cs" />
<Compile Include="Common\ValidatingConverters.cs" />
<Compile Include="Controls\CalculationResult.cs" />
<Compile Include="Controls\CalculationResultAutomationPeer.cs" />

Просмотреть файл

@ -0,0 +1,48 @@
using System;
using Windows.ApplicationModel.Activation;
using Windows.ApplicationModel.UserActivities;
namespace CalculatorApp
{
internal class SnapshotLaunchArguments
{
public string ActivityId { get; set; }
public Uri LaunchUri { get; set; }
}
internal static class LaunchExtensions
{
public static bool IsSnapshotProtocol(this IActivatedEventArgs args) =>
args is IProtocolActivatedEventArgs protoArgs &&
protoArgs.Uri != null &&
protoArgs.Uri.Segments != null &&
protoArgs.Uri.Segments.Length == 2 &&
protoArgs.Uri.Segments[0] == "snapshots/";
/// <summary>
/// GetActivityId() requires the parameter `launchUri` to be a well-formed
/// snapshot URI.
/// </summary>
/// <param name="launchUri">the Uri to launch with a snapshot context.</param>
/// <returns>Activity ID</returns>
public static string GetActivityId(this Uri launchUri)
{
return launchUri.Segments[1].Trim();
}
public static bool VerifyIncomingActivity(this SnapshotLaunchArguments launchArgs, UserActivity activity)
{
if (activity.State != UserActivityState.Published ||
string.IsNullOrEmpty(activity.ActivityId) ||
activity.ActivationUri == null ||
activity.ActivationUri.Segments == null ||
activity.ActivationUri.Segments.Length != 2 ||
activity.ActivationUri.Segments[0] != "snapshots/")
{
return false;
}
return activity.ActivityId == GetActivityId(launchArgs.LaunchUri);
}
}
}

Просмотреть файл

@ -1773,7 +1773,7 @@
<value>J</value>
<comment>An abbreviation for a measurement unit of energy</comment>
</data>
<data name="UnitAbbreviation_Kilowatthour" xml:space="preserve">
<data name="UnitAbbreviation_Kilowatthour" xml:space="preserve">
<value>kWh</value>
<comment>An abbreviation for a measurement unit of electricity consumption</comment>
</data>
@ -2145,7 +2145,7 @@
<value>Joules</value>
<comment>A measurement unit for energy. (Please choose the most appropriate plural form to fit any number between 0 and 999,999,999,999,999)</comment>
</data>
<data name="UnitName_Kilowatthour" xml:space="preserve">
<data name="UnitName_Kilowatthour" xml:space="preserve">
<value>Kilowatt-hours</value>
<comment>A measurement unit for electricity consumption. (Please choose the most appropriate plural form to fit any number between 0 and 999,999,999,999,999)</comment>
</data>
@ -4754,4 +4754,12 @@
<value>Open the context menu for available actions</value>
<comment>Screen reader prompt for the context menu of the expression box</comment>
</data>
</root>
<data name="ErrorButtonOk" xml:space="preserve">
<value>OK</value>
<comment>The text of OK button to dismiss an error dialog.</comment>
</data>
<data name="SnapshotRestoreError" xml:space="preserve">
<value>Couldn't restore this snapshot.</value>
<comment>The error message to notify user that restoring from snapshot has failed.</comment>
</data>
</root>

Просмотреть файл

@ -1,13 +1,10 @@
using CalculatorApp.Common;
using CalculatorApp.Converters;
using CalculatorApp.ViewModel;
using CalculatorApp.ViewModel.Common;
using CalculatorApp.ViewModel.Common.Automation;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading.Tasks;
using Windows.ApplicationModel.UserActivities;
using Windows.Data.Json;
using Windows.Foundation;
using Windows.Graphics.Display;
using Windows.Storage;
@ -15,19 +12,21 @@ using Windows.UI.Core;
using Windows.UI.ViewManagement;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Automation;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Navigation;
using Microsoft.UI.Xaml.Controls;
using MUXC = Microsoft.UI.Xaml.Controls;
using CalculatorApp.Common;
using CalculatorApp.Converters;
using CalculatorApp.ViewModel;
using CalculatorApp.ViewModel.Common;
using CalculatorApp.ViewModel.Common.Automation;
using wuxc = Windows.UI.Xaml.Controls;
namespace CalculatorApp
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainPage : Page
public sealed partial class MainPage : wuxc.Page
{
public static readonly DependencyProperty NavViewCategoriesSourceProperty =
DependencyProperty.Register(nameof(NavViewCategoriesSource), typeof(List<object>), typeof(MainPage), new PropertyMetadata(default));
@ -59,6 +58,28 @@ namespace CalculatorApp
DisplayInformation.AutoRotationPreferences = DisplayOrientations.Portrait | DisplayOrientations.PortraitFlipped;
}
}
UserActivityRequestManager.GetForCurrentView().UserActivityRequested += async (_, args) =>
{
var deferral = args.GetDeferral();
if (deferral == null)
{
// Windows Bug in ni_moment won't return the deferral propoerly, see https://microsoft.visualstudio.com/DefaultCollection/OS/_workitems/edit/47775705/
return;
}
var channel = UserActivityChannel.GetDefault();
var activity = await channel.GetOrCreateUserActivityAsync($"{Guid.NewGuid()}");
activity.ActivationUri = new Uri($"ms-calculator:snapshots/{activity.ActivityId}");
activity.ContentInfo = UserActivityContentInfo.FromJson(Model.SaveApplicationSnapshot().Stringify());
activity.IsRoamable = false;
var resProvider = AppResourceProvider.GetInstance();
activity.VisualElements.DisplayText =
$"{resProvider.GetResourceString("AppName")} - {resProvider.GetResourceString(NavCategoryStates.GetNameResourceKey(Model.Mode))}";
await activity.SaveAsync();
args.Request.SetUserActivity(activity);
deferral.Complete();
TraceLogger.GetInstance().LogRecallSnapshot(Model.Mode);
};
}
public void UnregisterEventHandlers()
@ -121,23 +142,83 @@ namespace CalculatorApp
protected override void OnNavigatedTo(NavigationEventArgs e)
{
ViewMode initialMode = ViewMode.Standard;
string stringParameter = (e.Parameter as string);
if (!string.IsNullOrEmpty(stringParameter))
var initialMode = ViewMode.Standard;
var localSettings = ApplicationData.Current.LocalSettings;
if (localSettings.Values.ContainsKey(ApplicationViewModel.ModePropertyName))
{
initialMode = (ViewMode)Convert.ToInt32(stringParameter);
initialMode = NavCategoryStates.Deserialize(localSettings.Values[ApplicationViewModel.ModePropertyName]);
}
if (e.Parameter == null)
{
Model.Initialize(initialMode);
return;
}
if (e.Parameter is string legacyArgs)
{
if (legacyArgs.Length > 0)
{
initialMode = (ViewMode)Convert.ToInt32(legacyArgs);
}
Model.Initialize(initialMode);
}
else if (e.Parameter is SnapshotLaunchArguments snapshotArgs)
{
_ = Window.Current.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, async () =>
{
var channel = UserActivityChannel.GetDefault();
var activity = await channel.GetOrCreateUserActivityAsync(snapshotArgs.ActivityId);
if (TryRestoreFromActivity(snapshotArgs, activity, out var errorMessage))
{
TraceLogger.GetInstance().LogRecallRestore(Model.Mode);
SelectNavigationItemByModel();
}
else
{
TraceLogger.GetInstance().LogRecallError(Model.Mode, errorMessage);
await ShowSnapshotLaunchErrorAsync();
}
});
Model.Initialize(initialMode);
}
else
{
ApplicationDataContainer localSettings = ApplicationData.Current.LocalSettings;
if (localSettings.Values.ContainsKey(ApplicationViewModel.ModePropertyName))
{
initialMode = NavCategoryStates.Deserialize(localSettings.Values[ApplicationViewModel.ModePropertyName]);
}
Environment.FailFast("cd75d5af-0f47-4cc2-910c-ed792ed16fe6");
}
}
private bool TryRestoreFromActivity(SnapshotLaunchArguments snapshotArgs, UserActivity activity, out string errorMessage)
{
if (!snapshotArgs.VerifyIncomingActivity(activity))
{
errorMessage = "IncomingActivityFailed";
return false;
}
Model.Initialize(initialMode);
// Work around for bug https://microsoft.visualstudio.com/DefaultCollection/OS/_workitems/edit/48931227
// where ContentInfo can't be directly accessed.
if (!JsonObject.TryParse(activity.ToJson(), out var activityJson))
{
errorMessage = "ParseJsonError";
return false;
}
if (!activityJson.ContainsKey("contentInfo"))
{
errorMessage = "ContentInfoNotExist";
return false;
}
if (!Model.TryRestoreFromSnapshot(activityJson.GetNamedObject("contentInfo")))
{
errorMessage = "RestoreFromSnapshotFailed";
return false;
}
errorMessage = string.Empty;
return true;
}
private void InitializeNavViewCategoriesSource()
@ -302,13 +383,13 @@ namespace CalculatorApp
NavView.SetValue(KeyboardShortcutManager.VirtualKeyControlChordProperty, MyVirtualKey.E);
}
private void OnNavPaneOpened(MUXC.NavigationView sender, object args)
private void OnNavPaneOpened(NavigationView sender, object args)
{
KeyboardShortcutManager.HonorShortcuts(false);
TraceLogger.GetInstance().LogNavBarOpened();
}
private void OnNavPaneClosed(MUXC.NavigationView sender, object args)
private void OnNavPaneClosed(NavigationView sender, object args)
{
if (Popup.IsOpen)
{
@ -360,7 +441,7 @@ namespace CalculatorApp
KeyboardShortcutManager.HonorShortcuts(!NavView.IsPaneOpen);
}
private void OnNavSelectionChanged(object sender, MUXC.NavigationViewSelectionChangedEventArgs e)
private void OnNavSelectionChanged(object sender, NavigationViewSelectionChangedEventArgs e)
{
if (e.IsSettingsSelected)
{
@ -368,13 +449,13 @@ namespace CalculatorApp
return;
}
if (e.SelectedItemContainer is MUXC.NavigationViewItem item)
if (e.SelectedItemContainer is NavigationViewItem item)
{
Model.Mode = (ViewMode)item.Tag;
}
}
private void OnNavItemInvoked(MUXC.NavigationView sender, MUXC.NavigationViewItemInvokedEventArgs e)
private void OnNavItemInvoked(NavigationView sender, NavigationViewItemInvokedEventArgs e)
{
NavView.IsPaneOpen = false;
}
@ -610,6 +691,19 @@ namespace CalculatorApp
CloseSettingsPopup();
}
private async Task ShowSnapshotLaunchErrorAsync()
{
var resProvider = AppResourceProvider.GetInstance();
var dialog = new wuxc.ContentDialog
{
Title = resProvider.GetResourceString("AppName"),
Content = new wuxc.TextBlock { Text = resProvider.GetResourceString("SnapshotRestoreError") },
CloseButtonText = resProvider.GetResourceString("ErrorButtonOk"),
DefaultButton = wuxc.ContentDialogButton.Close
};
await dialog.ShowAsync();
}
private Calculator m_calculator;
private GraphingCalculator m_graphingCalculator;
private UnitConverter m_converter;