From c61edd530bd8b41e1bfebec0b546909248853f66 Mon Sep 17 00:00:00 2001 From: "ren.ji" Date: Wed, 21 Oct 2026 11:12:32 +0800 Subject: [PATCH 1/2] feat(windows): edit task details inline --- docs/UX-BENCHMARKS.md | 9 +- scripts/e2e/e2e-windows.ps1 | 75 ++++++++- windows/MainWindow.xaml.cpp | 315 +++++++++++++++++++++++++++++++++--- windows/MainWindow.xaml.h | 16 ++ 4 files changed, 387 insertions(+), 28 deletions(-) diff --git a/docs/UX-BENCHMARKS.md b/docs/UX-BENCHMARKS.md index 7be32f7..1479e68 100644 --- a/docs/UX-BENCHMARKS.md +++ b/docs/UX-BENCHMARKS.md @@ -80,9 +80,10 @@ Scheduled(有日期的)。 - ✅ macOS 已落地:⌘/⇧ 多选 + 批量完成/排期/移动/删除,删除走单条撤销横幅 (PRODUCT-SPEC §5b 已收录批量语义)。 -- ✅ Windows 列表选择基础:WinUI Extended 模式支持 Ctrl/Shift - 多选、方向键与范围选择;Enter 打开当前选中任务的编辑对话框,Esc - 清除选择。按任务 ID 在列表刷新后恢复选择(普通列表与搜索结果)。 +- ✅ Windows 列表选择与行内详情:WinUI Extended 模式支持 Ctrl/Shift + 多选、方向键与范围选择;Enter、双击或 ⓘ 在当前行展开标题、备注、日期与时间, + 不再调用 PromptAsync。Enter 保存、Esc 取消;切换行时保存。按任务 ID 在列表刷新后 + 恢复选择与编辑草稿(普通列表与搜索结果)。 - ✅ Windows 批量操作(分阶段):选择多条任务时显示共享操作栏; 支持一次批量标记完成、批量移动到目标列表;使用稳定任务 ID,列表刷新 与搜索不会把操作错误应用到其他任务。打包验收覆盖两条任务批量完成。 @@ -91,7 +92,7 @@ Scheduled(有日期的)。 - ✅ Linux:基于 GTK 原生多选(#33)增加可见的批量完成和批量移动操作栏, 同时适用于当前列表和全局搜索。刷新时按任务 ID 保留选择。 - ✅ Windows 单条/批量删除采用 6 秒延后提交:先隐藏选中任务,期间撤销可直接恢复原任务 ID 与元数据,超时才发送后端删除 RPC;失败时恢复任务。 -- 🔜 **尚未完成**:Windows 行内详情展开;Linux 批量删除及撤销;以及三平台按真实快捷键进行实机回归。 +- 🔜 **尚未完成**:三平台按真实快捷键进行实机回归。 ### 2.5 Quick Find diff --git a/scripts/e2e/e2e-windows.ps1 b/scripts/e2e/e2e-windows.ps1 index a148790..4e1d5c9 100644 --- a/scripts/e2e/e2e-windows.ps1 +++ b/scripts/e2e/e2e-windows.ps1 @@ -185,6 +185,78 @@ try { throw 'Windows task list must support Ctrl/Shift multi-selection' } + # Task details must expand in place. A modal PromptAsync text box is not + # acceptable here because it cannot edit notes/date/time and breaks the + # Reminders-style Return/Esc flow. + $taskInfo = Wait-Until 'inline task details button' { + Find-ById $window 'task-info-2' + } + $taskInfo.GetCurrentPattern( + [System.Windows.Automation.InvokePattern]::Pattern).Invoke() + $inlineTitle = Wait-Until 'inline task title editor' { + Find-ById $window 'inline-task-title-2' + } + if ($null -ne (Find-ById $window 'prompt-input')) { + throw 'task details opened the legacy PromptAsync dialog' + } + $inlineNotes = Wait-Until 'inline task notes editor' { + Find-ById $window 'inline-task-notes-2' + } + $inlineDate = Wait-Until 'inline task date editor' { + Find-ById $window 'inline-task-date-2' + } + $inlineTime = Wait-Until 'inline task time editor' { + Find-ById $window 'inline-task-time-2' + } + $editedPlainTask = 'Life-only task edited inline' + $editedNotes = 'Inline notes survive a changed-event refresh' + $inlineTitle.GetCurrentPattern( + [System.Windows.Automation.ValuePattern]::Pattern).SetValue($editedPlainTask) + $inlineNotes.GetCurrentPattern( + [System.Windows.Automation.ValuePattern]::Pattern).SetValue($editedNotes) + $inlineDate.GetCurrentPattern( + [System.Windows.Automation.ValuePattern]::Pattern).SetValue('2099-12-31') + $inlineTime.GetCurrentPattern( + [System.Windows.Automation.ValuePattern]::Pattern).SetValue('10:30') + $inlineTitle.SetFocus() + [System.Windows.Forms.SendKeys]::SendWait('{ENTER}') + [void](Wait-Until 'inline task edit persisted' { + if ($null -ne (Find-ByName $window $editedPlainTask) -and + $null -ne (Find-ById $window 'task-notes-2')) { return $true } + return $false + }) + $plainTask = $editedPlainTask + + # Reopen after the backend changed-event rebuild and verify every field + # was read back from persistence, not merely retained in the visual tree. + $taskInfo = Wait-Until 'reopen inline task details' { + Find-ById $window 'task-info-2' + } + $taskInfo.GetCurrentPattern( + [System.Windows.Automation.InvokePattern]::Pattern).Invoke() + $persistedNotes = Wait-Until 'persisted inline notes' { + Find-ById $window 'inline-task-notes-2' + } + $persistedDate = Wait-Until 'persisted inline date' { + Find-ById $window 'inline-task-date-2' + } + $persistedTime = Wait-Until 'persisted inline time' { + Find-ById $window 'inline-task-time-2' + } + if ($persistedNotes.GetCurrentPattern( + [System.Windows.Automation.ValuePattern]::Pattern).Current.Value -ne $editedNotes -or + $persistedDate.GetCurrentPattern( + [System.Windows.Automation.ValuePattern]::Pattern).Current.Value -ne '2099-12-31' -or + $persistedTime.GetCurrentPattern( + [System.Windows.Automation.ValuePattern]::Pattern).Current.Value -ne '10:30') { + throw 'inline task details were not persisted across the backend refresh' + } + $taskInfo = Wait-Until 'collapse inline task details' { + Find-ById $window 'task-info-2' + } + $taskInfo.GetCurrentPattern( + [System.Windows.Automation.InvokePattern]::Pattern).Invoke() + Select-TaskList 1 '工作' [void](Wait-Until '工作 view selected' { if ($paneTitle.Current.Name -eq '工作') { return $true } @@ -324,7 +396,7 @@ try { [ordered]@{ platform = 'windows' artifact = $executable.FullName - actions = @('set:quick-add', 'invoke:quick-add-submit', 'toggle:task-complete-1', 'create-list:生活', 'select-list:2', 'enter:quick-add', 'select-list:1', 'select-list:2') + actions = @('set:quick-add', 'invoke:quick-add-submit', 'toggle:task-complete-1', 'create-list:生活', 'select-list:2', 'enter:quick-add', 'inline:edit-task-2', 'select-list:1', 'select-list:2') assertions = @( 'packaged WinUI window activated', 'stable UI Automation identifiers exposed', @@ -333,6 +405,7 @@ try { 'new task submitted by Enter is owned by 生活, not 工作', 'task checkbox and plain-text title are vertically aligned', 'Windows task list exposes native multiple selection', + 'task details expand inline without PromptAsync and persist title, notes, date, and time', 'view switches preserve task list ownership', 'global search shows the real owning list for every task', 'Ctrl/Shift multi-selection completes two tasks in one command', diff --git a/windows/MainWindow.xaml.cpp b/windows/MainWindow.xaml.cpp index e82b518..ddcf584 100644 --- a/windows/MainWindow.xaml.cpp +++ b/windows/MainWindow.xaml.cpp @@ -194,6 +194,13 @@ std::wstring due_tail(std::string const& due) { return wide(due.substr(5, 5)); } +std::wstring trim_wide(std::wstring text) { + auto const first = text.find_first_not_of(L" \t\r\n"); + if (first == std::wstring::npos) return {}; + auto const last = text.find_last_not_of(L" \t\r\n"); + return text.substr(first, last - first + 1); +} + template T const* find_by_id(std::vector const& items, std::int64_t id) { for (auto const& item : items) { @@ -966,6 +973,18 @@ void MainWindow::RenderSidebar() { } void MainWindow::RenderTasks() { + if (inline_task_id_.has_value() && !inline_task_in_search_) { + CaptureInlineTaskDraft(); + inline_title_box_ = nullptr; + inline_notes_box_ = nullptr; + inline_date_box_ = nullptr; + inline_time_box_ = nullptr; + } + rebuilding_task_rows_ = true; + struct TaskRowRestore { + MainWindow* window; + ~TaskRowRestore() { window->rebuilding_task_rows_ = false; } + } restore{this}; PaneTitle().Text(view_title()); PaneSubtitle().Text(view_subtitle()); @@ -1006,6 +1025,18 @@ void MainWindow::RenderTasks() { } void MainWindow::RenderSearchResults() { + if (inline_task_id_.has_value() && inline_task_in_search_) { + CaptureInlineTaskDraft(); + inline_title_box_ = nullptr; + inline_notes_box_ = nullptr; + inline_date_box_ = nullptr; + inline_time_box_ = nullptr; + } + rebuilding_task_rows_ = true; + struct SearchRowRestore { + MainWindow* window; + ~SearchRowRestore() { window->rebuilding_task_rows_ = false; } + } restore{this}; std::set selected_ids; for (auto const& selected : SearchResults().SelectedItems()) { if (auto row = selected.try_as()) { @@ -1026,16 +1057,23 @@ void MainWindow::RenderSearchResults() { mxc::Grid MainWindow::MakeTaskRow(rivet_app::Task const& task, bool search_result) { + auto const expanded_here = + inline_task_id_.has_value() && *inline_task_id_ == task.id && + inline_task_in_search_ == search_result; auto row = mxc::Grid{}; row.ColumnDefinitions().Append(mxc::ColumnDefinition{}); row.ColumnDefinitions().Append(mxc::ColumnDefinition{}); + row.ColumnDefinitions().Append(mxc::ColumnDefinition{}); row.ColumnDefinitions().GetAt(0).Width(mx::GridLength{0, mx::GridUnitType::Auto}); row.ColumnDefinitions().GetAt(1).Width( mx::GridLength{1, mx::GridUnitType::Star}); + row.ColumnDefinitions().GetAt(2).Width( + mx::GridLength{0, mx::GridUnitType::Auto}); // The checkbox and title must be centered in the SAME first row. Metadata // and notes (when present) occupy a second row under the title only. row.RowDefinitions().Append(mxc::RowDefinition{}); row.RowDefinitions().Append(mxc::RowDefinition{}); + row.RowDefinitions().Append(mxc::RowDefinition{}); row.Padding(mx::ThicknessHelper::FromLengths(4, 4, 4, 4)); row.Tag(winrt::box_value(task.id)); winrt::Microsoft::UI::Xaml::Automation::AutomationProperties::SetAutomationId( @@ -1067,13 +1105,31 @@ mxc::Grid MainWindow::MakeTaskRow(rivet_app::Task const& task, text.TextDecorations( winrt::Windows::UI::Text::TextDecorations::Strikethrough); } - row.Children().Append(text); + if (!expanded_here) row.Children().Append(text); + + auto info = mxc::Button{}; + info.Content(winrt::box_value(L"\u24D8")); + info.Padding(mx::ThicknessHelper::FromLengths(6, 2, 6, 2)); + info.MinWidth(28); + info.VerticalAlignment(mx::VerticalAlignment::Center); + info.Tag(winrt::box_value(task.id)); + winrt::Microsoft::UI::Xaml::Automation::AutomationProperties::SetAutomationId( + info, L"task-info-" + std::to_wstring(task.id)); + winrt::Microsoft::UI::Xaml::Automation::AutomationProperties::SetName( + info, t("contextMenuDetails")); + info.Click([weak = get_weak(), id = task.id](auto const&, auto const&) { + if (auto window = weak.get()) window->OpenInlineTaskEditor(id); + }); + mxc::Grid::SetColumn(info, 2); + mxc::Grid::SetRow(info, 0); + row.Children().Append(info); auto details = mxc::StackPanel{}; details.Margin(mx::ThicknessHelper::FromLengths(8, 0, 0, 0)); details.Spacing(2); mxc::Grid::SetColumn(details, 1); mxc::Grid::SetRow(details, 1); + mxc::Grid::SetColumnSpan(details, 2); // Meta line (PRODUCT-SPEC §5, DESIGN-TOKENS task row): owning list name // with its color dot in the smart views, then the due chip; the notes @@ -1144,16 +1200,126 @@ mxc::Grid MainWindow::MakeTaskRow(rivet_app::Task const& task, if (task.notes.has_value() && !task.notes->empty()) { auto notes_line = mxc::TextBlock{}; notes_line.Text(wide(*task.notes)); + winrt::Microsoft::UI::Xaml::Automation::AutomationProperties::SetAutomationId( + notes_line, L"task-notes-" + std::to_wstring(task.id)); notes_line.FontSize(12); notes_line.TextTrimming(mx::TextTrimming::CharacterEllipsis); notes_line.Foreground(ThemeBrush(L"TasklyMutedTextBrush")); details.Children().Append(notes_line); } - if (details.Children().Size() > 0) { + if (!expanded_here && details.Children().Size() > 0) { row.Children().Append(details); } + if (expanded_here) { + if (!inline_task_draft_.has_value() || + inline_task_draft_->id != task.id) { + inline_task_draft_ = task; + } + auto const& draft = *inline_task_draft_; + auto editor = mxc::StackPanel{}; + editor.Spacing(8); + editor.Margin(mx::ThicknessHelper::FromLengths(8, 0, 8, 4)); + mxc::Grid::SetColumn(editor, 1); + mxc::Grid::SetRow(editor, 0); + mxc::Grid::SetRowSpan(editor, 3); + + inline_title_box_ = mxc::TextBox{}; + inline_title_box_.Text(wide(draft.text)); + inline_title_box_.FontSize(15); + inline_title_box_.AcceptsReturn(false); + inline_title_box_.UseSystemFocusVisuals(false); + winrt::Microsoft::UI::Xaml::Automation::AutomationProperties::SetAutomationId( + inline_title_box_, L"inline-task-title-" + std::to_wstring(task.id)); + inline_title_box_.KeyDown( + [weak = get_weak()](auto const&, auto const& args) { + if (auto window = weak.get()) { + if (args.Key() == Windows::System::VirtualKey::Enter) { + args.Handled(true); + window->CommitInlineTaskEditor(); + } else if (args.Key() == Windows::System::VirtualKey::Escape) { + args.Handled(true); + window->CancelInlineTaskEditor(); + } + } + }); + editor.Children().Append(inline_title_box_); + + auto schedule = mxc::StackPanel{}; + schedule.Orientation(mxc::Orientation::Horizontal); + schedule.Spacing(8); + + inline_date_box_ = mxc::TextBox{}; + inline_date_box_.Width(128); + inline_date_box_.PlaceholderText(L"yyyy-MM-dd"); + inline_date_box_.Text(draft.due_date.has_value() + ? wide(*draft.due_date).substr(0, 10) + : L""); + inline_date_box_.UseSystemFocusVisuals(false); + winrt::Microsoft::UI::Xaml::Automation::AutomationProperties::SetAutomationId( + inline_date_box_, L"inline-task-date-" + std::to_wstring(task.id)); + schedule.Children().Append(inline_date_box_); + + inline_time_box_ = mxc::TextBox{}; + inline_time_box_.Width(88); + inline_time_box_.PlaceholderText(L"HH:mm"); + inline_time_box_.Text(draft.due_time.has_value() + ? wide(*draft.due_time).substr(0, 5) + : L""); + inline_time_box_.UseSystemFocusVisuals(false); + winrt::Microsoft::UI::Xaml::Automation::AutomationProperties::SetAutomationId( + inline_time_box_, L"inline-task-time-" + std::to_wstring(task.id)); + schedule.Children().Append(inline_time_box_); + + auto today = mxc::Button{}; + today.Content(winrt::box_value(t("navToday"))); + today.Click([weak = get_weak()](auto const&, auto const&) { + if (auto window = weak.get()) { + window->inline_date_box_.Text(date_string(0)); + } + }); + schedule.Children().Append(today); + auto tomorrow = mxc::Button{}; + tomorrow.Content(winrt::box_value(t("dateTomorrow"))); + tomorrow.Click([weak = get_weak()](auto const&, auto const&) { + if (auto window = weak.get()) { + window->inline_date_box_.Text(date_string(1)); + } + }); + schedule.Children().Append(tomorrow); + auto clear = mxc::Button{}; + clear.Content(winrt::box_value(t("dialogClear"))); + clear.Click([weak = get_weak()](auto const&, auto const&) { + if (auto window = weak.get()) { + window->inline_date_box_.Text(L""); + window->inline_time_box_.Text(L""); + } + }); + schedule.Children().Append(clear); + editor.Children().Append(schedule); + + inline_notes_box_ = mxc::TextBox{}; + inline_notes_box_.Text( + draft.notes.has_value() ? wide(*draft.notes) : L""); + inline_notes_box_.PlaceholderText(t("hintAddNotes")); + inline_notes_box_.AcceptsReturn(true); + inline_notes_box_.TextWrapping(mx::TextWrapping::Wrap); + inline_notes_box_.MinHeight(62); + inline_notes_box_.UseSystemFocusVisuals(false); + winrt::Microsoft::UI::Xaml::Automation::AutomationProperties::SetAutomationId( + inline_notes_box_, L"inline-task-notes-" + std::to_wstring(task.id)); + inline_notes_box_.KeyDown( + [weak = get_weak()](auto const&, auto const& args) { + if (args.Key() == Windows::System::VirtualKey::Escape) { + args.Handled(true); + if (auto window = weak.get()) window->CancelInlineTaskEditor(); + } + }); + editor.Children().Append(inline_notes_box_); + row.Children().Append(editor); + } + auto menu = mxc::MenuFlyout{}; auto due_today = mxc::MenuFlyoutItem{}; due_today.Text(t("navToday")); @@ -1227,6 +1393,78 @@ rivet_app::Task const* MainWindow::FindTaskForAction( return find_by_id(tasks_, id); } +void MainWindow::CaptureInlineTaskDraft() { + if (!inline_task_draft_.has_value() || !inline_title_box_) return; + auto& draft = *inline_task_draft_; + draft.text = winrt::to_string(trim_wide(std::wstring(inline_title_box_.Text()))); + auto const notes = trim_wide(std::wstring(inline_notes_box_.Text())); + draft.notes = notes.empty() + ? std::nullopt + : std::optional(winrt::to_string(notes)); + auto const date = trim_wide(std::wstring(inline_date_box_.Text())); + auto const time = trim_wide(std::wstring(inline_time_box_.Text())); + draft.due_date = date.empty() + ? std::nullopt + : std::optional(winrt::to_string(date)); + draft.due_time = date.empty() || time.empty() + ? std::nullopt + : std::optional(winrt::to_string(time)); +} + +void MainWindow::ResetInlineTaskEditor() { + inline_task_id_.reset(); + inline_task_draft_.reset(); + inline_task_in_search_ = false; + inline_title_box_ = nullptr; + inline_notes_box_ = nullptr; + inline_date_box_ = nullptr; + inline_time_box_ = nullptr; +} + +void MainWindow::OpenInlineTaskEditor(std::int64_t id) { + if (inline_task_id_.has_value() && *inline_task_id_ == id) { + CommitInlineTaskEditor(); + return; + } + if (inline_task_id_.has_value()) CommitInlineTaskEditor(); + auto const* task = FindTaskForAction(id); + if (task == nullptr) return; + inline_task_id_ = id; + inline_task_draft_ = *task; + inline_task_in_search_ = !current_search_.empty(); + RenderTasks(); + RenderSearchResults(); + if (inline_title_box_) { + inline_title_box_.Focus(mx::FocusState::Programmatic); + inline_title_box_.SelectAll(); + } +} + +void MainWindow::CommitInlineTaskEditor() { + if (!inline_task_id_.has_value() || !inline_task_draft_.has_value()) return; + CaptureInlineTaskDraft(); + auto updated = *inline_task_draft_; + auto const* stored = FindTaskForAction(updated.id); + if (trim_wide(wide(updated.text)).empty() && stored != nullptr) { + updated.text = stored->text; + } + auto const changed = + stored == nullptr || updated.text != stored->text || + updated.notes != stored->notes || updated.due_date != stored->due_date || + updated.due_time != stored->due_time; + ResetInlineTaskEditor(); + RenderTasks(); + RenderSearchResults(); + if (changed) UpdateTaskAsync(std::move(updated)); +} + +void MainWindow::CancelInlineTaskEditor() { + if (!inline_task_id_.has_value()) return; + ResetInlineTaskEditor(); + RenderTasks(); + RenderSearchResults(); +} + winrt::hstring MainWindow::DueLabel(std::string const& due_date) const { auto const today = date_string(0); auto const compare = date_compare(due_date, today); @@ -1796,6 +2034,33 @@ winrt::fire_and_forget MainWindow::SetTaskDueAsync(std::int64_t id, } } +winrt::fire_and_forget MainWindow::UpdateTaskAsync(rivet_app::Task task) { + if (backend_ == nullptr || !backend_->running()) co_return; + auto const dispatcher = DispatcherQueue(); + auto const weak = get_weak(); + auto backend = backend_; + try { + rivet_app::API api(*backend); + (void)api.update_task_async( + std::move(task), + [dispatcher, weak](rivet_app::Result result) { + try { + result.get(); + dispatcher.TryEnqueue([weak] { + if (auto window = weak.get()) window->RefreshSearchIfActive(); + }); + } catch (std::exception const& e) { + auto message = std::string(e.what()); + dispatcher.TryEnqueue([weak, message = std::move(message)] { + if (auto window = weak.get()) window->SetError(message); + }); + } + }); + } catch (std::exception const& e) { + SetError(e.what()); + } +} + winrt::fire_and_forget MainWindow::MoveTaskToListAsync( std::int64_t id, std::int64_t target_list_id) { if (backend_ == nullptr || !backend_->running()) { @@ -2202,6 +2467,9 @@ void MainWindow::OnSearchTextChanged( winrt::Windows::Foundation::IInspectable const&, Microsoft::UI::Xaml::Controls::TextChangedEventArgs const&) { auto const keyword = std::wstring(SearchBox().Text()); + if (inline_task_id_.has_value() && keyword != current_search_) { + CommitInlineTaskEditor(); + } if (keyword.empty()) { current_search_.clear(); SearchPane().Visibility(mx::Visibility::Collapsed); @@ -2223,6 +2491,7 @@ void MainWindow::OnSmartTileClick( if (view_ == kind) { return; } + if (inline_task_id_.has_value()) CommitInlineTaskEditor(); view_ = kind; suppress_selection_ = true; UserList().SelectedIndex(-1); @@ -2238,6 +2507,7 @@ void MainWindow::OnUserListSelectionChanged( if (suppress_selection_ || UserList().SelectedIndex() < 0) { return; } + if (inline_task_id_.has_value()) CommitInlineTaskEditor(); auto const tag = UserList() .Items() @@ -2355,7 +2625,19 @@ void MainWindow::RefreshBulkActionBar() { void MainWindow::OnTaskSelectionChanged( winrt::Windows::Foundation::IInspectable const&, - Microsoft::UI::Xaml::Controls::SelectionChangedEventArgs const&) { + Microsoft::UI::Xaml::Controls::SelectionChangedEventArgs const& args) { + if (rebuilding_task_rows_) return; + if (inline_task_id_.has_value()) { + for (auto const& added : args.AddedItems()) { + if (auto row = added.try_as()) { + auto const id = winrt::unbox_value(row.Tag()); + if (id != *inline_task_id_) { + CommitInlineTaskEditor(); + break; + } + } + } + } RefreshBulkActionBar(); } @@ -2518,6 +2800,11 @@ void MainWindow::OnTaskListKeyDown( auto list = sender.try_as(); if (!list) return; if (args.Key() == Windows::System::VirtualKey::Escape) { + if (inline_task_id_.has_value()) { + CancelInlineTaskEditor(); + args.Handled(true); + return; + } if (!list.SelectedItems().Size()) return; list.SelectedItems().Clear(); args.Handled(true); @@ -2533,16 +2820,8 @@ void MainWindow::OnTaskListKeyDown( auto row = list.SelectedItems().GetAt(0).try_as(); if (!row) return; auto const id = winrt::unbox_value(row.Tag()); - auto const* task = FindTaskForAction(id); - if (!task) return; args.Handled(true); - auto const weak = get_weak(); - PromptAsync(t("dialogTaskDetail"), L"", wide(task->text), - [weak, id](std::wstring const& updated) { - if (auto window = weak.get()) { - window->RenameTaskAsync(id, updated); - } - }); + OpenInlineTaskEditor(id); } void MainWindow::OnNewTaskKeyDown( @@ -2738,17 +3017,7 @@ void MainWindow::OnEditTask( } else { return; } - auto const* task = FindTaskForAction(id); - if (task == nullptr) { - return; - } - auto const weak = get_weak(); - PromptAsync(t("dialogTaskDetail"), L"", wide(task->text), - [weak, id](std::wstring const& text) { - if (auto window = weak.get()) { - window->RenameTaskAsync(id, text); - } - }); + OpenInlineTaskEditor(id); } void MainWindow::OnDeleteTask( diff --git a/windows/MainWindow.xaml.h b/windows/MainWindow.xaml.h index 132ed37..1557215 100644 --- a/windows/MainWindow.xaml.h +++ b/windows/MainWindow.xaml.h @@ -132,6 +132,11 @@ struct MainWindow : MainWindowT { void StageTaskDeletion(std::vector const& ids); void CommitPendingDeletion(); void UndoPendingDeletion(); + void OpenInlineTaskEditor(std::int64_t id); + void CaptureInlineTaskDraft(); + void CommitInlineTaskEditor(); + void CancelInlineTaskEditor(); + void ResetInlineTaskEditor(); void RefreshQuickAddPreview(); void ApplyTitleBarTheme(); void OnEffectiveThemeChanged(); @@ -150,6 +155,7 @@ struct MainWindow : MainWindowT { winrt::fire_and_forget ToggleTaskAsync(std::int64_t id, bool completed); winrt::fire_and_forget RenameTaskAsync(std::int64_t id, std::wstring const& text); winrt::fire_and_forget SetTaskDueAsync(std::int64_t id, std::wstring const& due_date); + winrt::fire_and_forget UpdateTaskAsync(rivet_app::Task task); winrt::fire_and_forget DeleteTaskAsync(std::int64_t id); winrt::fire_and_forget MoveTaskToListAsync( std::int64_t id, std::int64_t target_list_id); @@ -215,6 +221,16 @@ struct MainWindow : MainWindowT { std::set suppressed_deleted_task_ids_; std::vector pending_delete_ids_; Microsoft::UI::Dispatching::DispatcherQueueTimer delete_undo_timer_{nullptr}; + // Exactly one task can be expanded inline. Draft values live outside the + // visual tree so a changed-event rebuild cannot discard in-progress edits. + std::optional inline_task_id_; + std::optional inline_task_draft_; + bool inline_task_in_search_{false}; + bool rebuilding_task_rows_{false}; + winrt::Microsoft::UI::Xaml::Controls::TextBox inline_title_box_{nullptr}; + winrt::Microsoft::UI::Xaml::Controls::TextBox inline_notes_box_{nullptr}; + winrt::Microsoft::UI::Xaml::Controls::TextBox inline_date_box_{nullptr}; + winrt::Microsoft::UI::Xaml::Controls::TextBox inline_time_box_{nullptr}; std::wstring current_search_; ViewKind view_{ViewKind::All}; std::int64_t view_list_id_{0}; From c8d908a36c5f036b94f238d71c82d5caaa965507 Mon Sep 17 00:00:00 2001 From: "ren.ji" Date: Wed, 21 Oct 2026 16:47:29 +0800 Subject: [PATCH 2/2] test(windows): tolerate packaged startup latency --- scripts/e2e/e2e-windows.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/e2e/e2e-windows.ps1 b/scripts/e2e/e2e-windows.ps1 index 4e1d5c9..5577333 100644 --- a/scripts/e2e/e2e-windows.ps1 +++ b/scripts/e2e/e2e-windows.ps1 @@ -27,7 +27,7 @@ last-update-check = $epoch "@ | Set-Content -LiteralPath (Join-Path $tasklyHome 'config.ini') -Encoding utf8 function Wait-Until { - param([string]$Description, [scriptblock]$Probe, [int]$TimeoutSeconds = 30) + param([string]$Description, [scriptblock]$Probe, [int]$TimeoutSeconds = 60) $deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds) while ([DateTime]::UtcNow -lt $deadline) { $value = & $Probe