-
Notifications
You must be signed in to change notification settings - Fork 8.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refactor Pane to be able to host non-terminal content #16170
Conversation
event Windows.Foundation.TypedEventHandler<Object, Object> TitleChanged; | ||
event Windows.Foundation.TypedEventHandler<Object, Object> TabColorChanged; | ||
event Windows.Foundation.TypedEventHandler<Object, Object> TaskbarProgressChanged; | ||
event Windows.Foundation.TypedEventHandler<Object, Object> ConnectionStateChanged; | ||
event Windows.Foundation.TypedEventHandler<Object, Object> ReadOnlyChanged; | ||
event Windows.Foundation.TypedEventHandler<Object, Object> FocusRequested; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another option would be to have a "something changed" event and just recalculate all of the values on the next UI event loop tick (via Dispatcher.RunAsync
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm still concerned about the rising number of events. I currently don't quite see the benefit of this fine granularity. 🤔
void TerminalPaneContent::_controlTitleChanged(const IInspectable&, const IInspectable&) | ||
{ | ||
TitleChanged.raise(*this, nullptr); | ||
} | ||
void TerminalPaneContent::_controlTabColorChanged(const IInspectable&, const IInspectable&) | ||
{ | ||
TabColorChanged.raise(*this, nullptr); | ||
} | ||
void TerminalPaneContent::_controlSetTaskbarProgress(const IInspectable&, const IInspectable&) | ||
{ | ||
TaskbarProgressChanged.raise(*this, nullptr); | ||
} | ||
void TerminalPaneContent::_controlReadOnlyChanged(const IInspectable&, const IInspectable&) | ||
{ | ||
ReadOnlyChanged.raise(*this, nullptr); | ||
} | ||
void TerminalPaneContent::_controlFocusFollowMouseRequested(const IInspectable&, const IInspectable&) | ||
{ | ||
FocusRequested.raise(*this, nullptr); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: it feels like we could use a macro for these
void TerminalPage::_restartPaneConnection(const std::shared_ptr<Pane>& pane) | ||
void TerminalPage::_restartPaneConnection( | ||
const TerminalApp::TerminalPaneContent& paneContent, | ||
const winrt::Windows::Foundation::IInspectable&) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const winrt::Windows::Foundation::IInspectable&) | |
) |
What do we need this for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a typed event that we're just plumbing through. RestartTerminalRequested
doesn't currently actually raise the event with args
but it could? I dunno, that's just how these are generally done.
@@ -73,7 +73,7 @@ class Pane : public std::enable_shared_from_this<Pane> | |||
|
|||
std::shared_ptr<Pane> GetActivePane(); | |||
winrt::Microsoft::Terminal::Control::TermControl GetLastFocusedTerminalControl(); | |||
winrt::Microsoft::Terminal::Control::TermControl GetTerminalControl(); | |||
winrt::Microsoft::Terminal::Control::TermControl GetTerminalControl() const; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a few of these like GetTerminalControl()
and GetProfile()
that kinda don't make sense here anymore. Why not do this:
- move them to
TerminalPaneContent
- force callers to
GetContent().GetTerminalControl()
...or something like that? That way it feels like a cleaner abstraction/separation between the Pane and underlying content.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You know what happened here - the remaining callers got merged into main mostly after I had written most of this. The broadcast input stuff? All merged after I did most of this initially 🤦
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanna say that this is probably okay, mainly for the reason that when I went to go solve the last few cases here (mainly broadcasting related, and also one for detaching termcontrol's from a window), I ended up with a bunch of
TermControl& _termControlFromPane(const auto& pane)
{
if (const auto content{ pane->GetContent() })
{
if (const auto termContent{ content.try_as<winrt::TerminalApp::TerminalPaneContent>() })
{
return termContent.GetTerminal();
}
}
return nullptr;
}
helpers scattered through the code. Almost seemed more sensible to just leave it in Pane
. Maybe rename it to MaybeGetTerminalControl
? idk
…rminal-panes-2023
…rminal-panes-2023
|
||
runtimeclass BellEventArgs | ||
{ | ||
Boolean FlashTaskbar { get; }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(why is this decision per-event?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose this is a bit of a change, yea.
Before, in Pane::_ControlWarningBellHandler
, the Pane
would figure out what to do based on the Profile
for the control that raised the event.
Now, that logic moved into TerminalPaneContent::_controlWarningBellHandler
. But we've moved the param from just a parame on the pane->page event, to a property raised from any content->page...
(okay but also as you mentioned, the bell plumbing clearly got lost in a merge)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Theoretically, some other kind of pane could also ask for a taskbar flash, not just a terminal
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But that's not based on what is inside the tab, that's based on the user's settings...
@@ -1332,9 +1113,16 @@ TermControl Pane::GetLastFocusedTerminalControl() | |||
// - <none> | |||
// Return Value: | |||
// - nullptr if this Pane is a parent, otherwise the TermControl of this Pane. | |||
TermControl Pane::GetTerminalControl() | |||
TermControl Pane::GetTerminalControl() const |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI the contract of this API has changed w.r.t. how it works for leaves. Is that intended?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea? It's basically an added "if this leaf is a terminal pane" check, so now callers need to be prepared to handle null as "No, this pane was not a terminal"
@@ -333,7 +333,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation | |||
// (The window has a min. size that ensures that there's always a scrollbar thumb.) | |||
if (drawableRange < 0) | |||
{ | |||
assert(false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this keeps showing up in your pull requests.. what is it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
context here: #16809 (comment)
since I've got marks in my prompt, they hit that assert literally every time I debug the Terminal. We concurred it's not really needed
This PR automagically finds and replaces all[^1] usages of our TYPED_EVENT macro with `til::event`. Benefits include: * less macro magic * editors are more easily able to figure out the relationship between `til::event<> Foo;`, `Foo.raise(...)`, and `bar.Foo({this, &Bar::FooHandler})` (whereas before the relationship between `_FooHandlers(...)` and `bar.Foo({...})`) couldn't be figured out by vscode & sublime. Other find & replace work that had to be done: * I added the `til::typed_event<>` == `<IInspectable, IInspectable>` thing from #16170, since that is goodness * I actually fixed `til::property_changed_event`, so you can use that for your property changed events. They're all the same anyways. * events had to come before `WINRT_PROPERTY`s, since the latter macro leaves us in a `private:` block * `Pane::SetupChildCloseHandlers` I had to swap _back_, because the script thought that was an event 🤦 * `ProfileViewModel::DeleteProfile` had to be renamed `DeleteProfileRequested`, since there was already a `DeleteProfile` method on it. * WindowManager.cpp was directly wiring up it's `winrt::event`s to the monarch & peasant. That doesn't work with `til::event`s and I'm kinda surprised it ever did <details> <summary>The script in question</summary> ```py import os import re def replace_in_file(file_path, file_name): with open(file_path, 'r', encoding="utf8") as file: content = file.read() found_matches = False # Define the pattern for matching pattern = r' WINRT_CALLBACK\((\w+),\s*(.*?)\);' event_matches = re.findall(pattern, content) if event_matches: found_matches = True print(f'found events in {file_path}:') for match in event_matches: name = match[0] args = match[1] if name == "newConnection" and file_name == "ConptyConnection.cpp": # This one is special continue old_declaration = 'WINRT_CALLBACK(' + name + ', ' + args + ');' new_declaration = 'til::event<' + args + '> ' + name + ';' if name != "PropertyChanged" else 'til::property_changed_event PropertyChanged;' print(f' {old_declaration} -> {new_declaration}') content = content.replace(old_declaration, new_declaration) typed_event_pattern = r' TYPED_EVENT\((\w+),\s*(.*?)\);' typed_matches = re.findall(typed_event_pattern, content) if typed_matches: found_matches = True print(f'found typed_events in {file_path}:') for match in typed_matches: name = match[0] args = match[1] if name == "newConnection" and file_name == "ConptyConnection.cpp": # This one is special continue old_declaration = f'TYPED_EVENT({name}, {args});' was_inspectable = (args == "winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable" ) or (args == "IInspectable, IInspectable" ) new_declaration = f'til::typed_event<{args}> {name};' if not was_inspectable else f"til::typed_event<> {name};" print(f' {old_declaration} -> {new_declaration}') content = content.replace(old_declaration, new_declaration) handlers_pattern = r'_(\w+)Handlers\(' handler_matches = re.findall(handlers_pattern, content) if handler_matches: found_matches = True print(f'found handlers in {file_path}:') for match in handler_matches: name = match if name == "newConnection" and file_name == "ConptyConnection.cpp": # This one is special continue old_declaration = f'_{name}Handlers(' new_declaration = f'{name}.raise(' print(f' {old_declaration} -> {new_declaration}') content = content.replace(old_declaration, new_declaration) if found_matches: with open(file_path, 'w', encoding="utf8") as file: file.write(content) def find_and_replace(directory): for root, dirs, files in os.walk(directory): if 'Generated Files' in dirs: dirs.remove('Generated Files') # Exclude the "Generated Files" directory for file in files: if file.endswith('.cpp') or file.endswith('.h') or file.endswith('.hpp'): file_path = os.path.join(root, file) try: replace_in_file(file_path, file) except Exception as e: print(f"error reading {file_path}") if file == "TermControl.cpp": print(e) # raise e # Replace in files within a specific directory directory_path = 'D:\\dev\\public\\terminal\\src' find_and_replace(directory_path) ``` </details> [^1]: there are other macros we use that were also using this macro, those weren't replaced. --------- Co-authored-by: Dustin Howett <[email protected]> Co-authored-by: Leonard Hecker <[email protected]>
…rminal-panes-2023
## Summary of the Pull Request Builds upon #16170. This PR simply adds a singly type of non-terminal pane - a "scratchpad pane". This is literally just a single text box, in a pane. It's on the `{ "command": "experimental.openScratchpad" }` action. ## References and Relevant Issues See: #997 ## Detailed Description of the Pull Request / Additional comments I also put it behind velocity so it won't even go into preview while this bakes. This is really just here to demonstrate that this works, and is viable. The next PR is much more interesting. ## Validation Steps Performed Screenshot below. ## other PRs * #16170 * #16171 <-- you are here * #16172
events.CloseRequested = content.CloseRequested( | ||
winrt::auto_revoke, | ||
[dispatcher, weakThis](auto sender, auto&&) -> winrt::fire_and_forget { | ||
// Don't forget! this ^^^^^^^^ sender can't be a reference, this is a async callback. | ||
|
||
// The lambda lives in the `std::function`-style container owned by `control`. That is, when the | ||
// `control` gets destroyed the lambda struct also gets destroyed. In other words, we need to | ||
// copy `weakThis` onto the stack, because that's the only thing that gets captured in coroutines. | ||
// See: https://devblogs.microsoft.com/oldnewthing/20211103-00/?p=105870 | ||
const auto weakThisCopy = weakThis; | ||
co_await wil::resume_foreground(dispatcher); | ||
// Check if Tab's lifetime has expired | ||
if (auto tab{ weakThisCopy.get() }) | ||
{ | ||
if (const auto content{ sender.try_as<TerminalApp::IPaneContent>() }) | ||
{ | ||
tab->_rootPane->WalkTree([content](std::shared_ptr<Pane> pane) { | ||
if (pane->GetContent() == content) | ||
{ | ||
pane->Close(); | ||
} | ||
}); | ||
} | ||
} | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI this addition breaks #16598 because it closes all tabs when it receives a AppHost::_CloseRequested
event and then serializes them to disk. This code however asynchronously emits another AppHost::_CloseRequested
event which causes the entire process to restart but now there are no tabs left and so it ends up with an empty persistence state.
I think this issue also exists without my persistence PR, however there it's not apparent since nothing of importance happens in AppHost::_CloseRequested
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've fixed the issue by simply not shutting down tabs on exit.
However, I believe that this is a flaw in this changeset regardless. This is because this event hook creates a loop in my opinion: When the tab closes, it closes the pane which closes the content which closes the pane which closes the tab. This recursion may end up emitting events twice, but definitely at least does things redundantly (even if the 2nd time around may not have any obvious effect).
For instance, TerminalPage::_RemoveTab
is called when you explicitly close a tab. This will Shutdown()
the root pane which will close the content. This will asynchronously emit a Closed
to the pane which, if it still exists for whatever reason, bubbles it up to the TerminalPage
which got the Closes
event hooked up to _RemoveTab
.
I believe this can be fixed by bringing some clarity into the flow. For instance, _RemoveTab
could be split into _CloseTab
and _RemoveTab
. The former gets called by everyone and the latter is exclusively hooked to the Pane's Closed event. The former calls Pane::Shutdown
which closes the content which emits the closed event and we can let it bubble up which then removes the tab. If we did that, there would be no ambiguity and there's only 1 way a tab can be closed and only 1 way the tab can be removed.
... technically. We still won't let it actually _be_ a pane, but now it acts like one. It's hosted in a `SettingsPaneContent`. There's no more `SettingsTab`. It totally _can_ be in a pane (but don't?) ## Validation Steps Performed * Still opens the settings * Only opens a single settings tab, or re-activates the existing one * Session restores! * Updates the title of the tab appropriately * I previously _did_ use the scratchpad action to open the settings in a pane, and that worked. ## Related PRs * #16170 * #16171 * #16172 <-- you are here * #16895 Refs #997 Closes #8452
…splitPane`, `newTab` (#16914) This changes `NewTabArgs`, `SplitPaneArgs`, and `NewWindowArgs` to accept a `INewContentArgs`, rather than just a `NewTerminalArgs`. This allows a couple things: * Users can open arbitrary types of panes with the existing `splitPane`, `newWindow` actions, just by passing `"type": "scartchpad"` (for example). This is a lot more flexible than re-defining different `"openScratchpad"`, `"openTasksPane"`, etc, etc actions for every kind of pane. * This allows us to use the existing machinery of session restore to also restore non-terminal panes. The `type` property was added to `newTab`, `splitPane`, `newWindow`. When omitted, we still just treat the json as a blob of NewTerminalArgs. There's not actually any other kinds of `INewContentArgs` in this PR (other than the placeholder `GenericContentArgs`). In [`dev/migrie/fhl/md-pane`](dev/migrie/f/tasks-pane...dev/migrie/fhl/md-pane), I have a type of pane that would LOVE to add some args here. So that's forward-thinking. There's really just two stealth types of pane for now: `settings`, and `scratchpad`. Those I DON'T have as constants or anything in this PR. They probably should be? Though, I suspect around the time of the tasks & MD panes, I'll come up with whatever structure I actually want them to take. ### future considerations here * In the future, this should allow extensions to say "I know how to host `foo` content", for 3p content. * The `wt` CLI args were not yet updated to also accept `--type` yet. There's no reason we couldn't easily do that. * I considered adding `ICanHasCommandline` to allow arbitrary content to generate a `wt` commandline-serializable string. Punted on that for now. ## other PRs * #16170 * #16171 * #16172 * #16895 * #16914 <-- you are here Closes #17014
Instead of
Pane
hosting aTermControl
directly, it now hosts anIPaneContent
. This is an abstraction between the TermControl and the pane itself, to allow for arbitrary implementations ofIPaneContent
, with things that might not be terminals.References and Relevant Issues
Detailed Description of the Pull Request / Additional comments
This PR by itself doesn't do much. It's just a refactoring.
xyzChanged
events onIPaneContent
).There are two follow-up PRs to this PR:
In addition, there's more work to be done after these merge:
IPaneContent::xyzChanged
withPropertyChanged
events"NewTerminalArgs
"Validation Steps Performed
enter
to restart the first pane in a split #16001 still worksPR Checklist
other PRs