Skip to content
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

Automation Node fine tuning with double-click (Rewrites #5292) #5923

Merged
merged 70 commits into from Mar 28, 2021

Conversation

IanCaio
Copy link
Contributor

@IanCaio IanCaio commented Feb 28, 2021

This PR rewrites #5292 under the new framework of Automation Patterns (where Automation Nodes are used).

Double clicking the node's inValue will open a dialog to edit the inValue and double clicking the node's outValue will open a dialog to edit the outValue.

	This commit introduces the use of AutomationNodes instead of just floats for keeping the automation values. The AutomationNodes will give more flexibility when it comes to storing extra information about the values (and make it possible to have multiple progression types in the future). Now the tangents are stored on the node, requiring one less timeMap on the automation pattern. Some methods had to be changed for that, since before they used const_iterator, which wouldn't allow us to change the tangent values.

TODO:
	- Check TODO comments that were added in places of the code I had some doubts about.
	- Maybe change the timeMap to hold pointers to AutomationNodes and not actual instances.
	- In some pieces of the code, I check if the AutomationNode already exists before setting its value or creating another node. I think that's unnecessary since if I create another node and assign it to the current existing one it could just clone its value. (That would not be valid for pointers to AutomationNodes, so that's something to consider when deciding between the two).
	- I still didn't find a good solution for renaming QMap::iterator method from "value()" to something else, so now we have lines that look a bit odd like "it.value().getValue()", because value() is actually returning the node, and getValue() is getting the node's automation value.
	This commit is a big change in comparison to the previous one. Automation nodes now have a inValue and outValue instead of a single value. The inValue represents the core value of the node, which is used for incoming progressions. The outValue represents the value of the node for progressions starting from that node on. In practice, the true value of the node is the inValue and outValue represents a way of creating discrete jumps in a node's position.
	By default inValue and outValue are the same. The user will then be allowed to change the outValue to create those discrete jumps. Because their values might be different, we now need two tangents for a single node: One for the curve coming before the node and one for the curve coming after the node. So instead of a single tangent variable, we now have inTangent and outTangent. If inValue and outValue are the same, so are inTangent and outTangent, but if they are different both are calculated according to the curve before and after the node.
	On the Automation Editor, the inValue of the node is represented by our default blue circle, while the outValue is represented by a red circle with 80% alpha (we should add a variable to the Automation Editor class to hold the color of this second circle in the future).
	Lots of comments were added on the modified files to explain the existing methods where changes were required (not only explaining the logic of the methods but the reason behind using inValue or outValue on them). Lots of TODO comments were also added as placeholders for changes that could or should be done before the finishing of this PR (or after it).
	For now only the logic for the nodes was added, but there's still no way for the user to change outValues (on the next commit a small placeholder shortcut will be added to do that for testing purposes).
	The drawing of the notes detuning on the piano roll was updated to account for the discrete jumps.
	The drawing of the pattern TCO was updated to account for the discrete jumps.
	The drawing of the pattern on the AutomationEditor was updated to account for the discrete jumps.

	IMPORTANT:
		- There were changes to the loadSettings and saveSettings of AutomationPatterns, to account for inValues and outValues, but I didn't create an upgrade routine yet.

	Some behavior that is important to mention:
		- Most operations on nodes (drawing, moving, X/Y flipping, and even selection copy/paste, apparently not fully finished) ignore the outValue, basically reseting it to the inValue. So when an user moves a node with a discrete jump, for example, that discrete jump will be lost and the user will need to set it again. Obviously in the future we might want to keep that information, but that isn't a critical issue, just a behavior that can be improved later by upgrading the code.
		- Later on we might want to connect a signal to the AutomationNode class, so it calls generateTangents when node data is changed.
		- When an object is disconnected from an automation pattern and it has to rescale the values so they fit the new range of values (max and min) the outValue is reset, meaning all discrete jumps are lost. This behavior will be changed in the future.

	Things that I think are also important noting:
		- Mainly in the src/gui/editor/AutomationEditor.cpp file there were lots of codes that apparently are related to a feature that is not yet finished (moving/cutting/copying/pasting selections of automation values). This doesn't sound good unless it's currently being worked on. I tried my best to update the current code to comply to the use of AutomationNodes so their developing can be picked up from a unbroken state. As with other operations involving AutomationNodes, they only account for the inValue discarding any discrete jumps that were present.
		- I added comments on some logic that seemed flawed in the src/gui/editor/AutomationEditor.cpp file so it can be reviewed. It's beyond the scope of this PR, but since I had to read and change a lot on that file I thought it was pertinent to at least comment those observations.
	While implementing the automation nodes, I noticed AutomatinEditor.cpp had some issues regarding flawed logic, code style convention, code that could comply to DRY paradigm, conditions that resulted in Undefined Behavior and unused legacy code. That's probably due to how old some changes are, they probably reflect a much different state of LMMS's code base. To make the transition to automation nodes a better one and avoid having to rework everything later I'm using the fact I have to get in touch with most of this code to try to fix some things.

	This commit starts refactoring AutomationEditor::mousePressEvent and AutomationEditor::mouseMoveEvent. There are still things to be improved on both but I'll slowly commit them so I can have better versioning control of the PR.

	Some changes worth noting:
		- A new action was created in the AutomationEditor class for drawing lines, since its logic was too mixed up with the logic of drawing and dragging a single node.
		- Changed most variable names to fit the current code style (just very few left to change).
		- Improved comments explaining the code.
		- Created a separate method for checking if the mouse position is sitting over an existing node (previously this code was repeated inside the event method and it had flaws on its logic, most of the times returning that the user didn't click a node even when he/she clicked one). Method is called getNodeAt(x,y,r), r being the "radius" to be considered (not actually a radius, the area is actually a square).

	Some changes that are still planned:
		- Removing legacy code for features that weren't finished (select and move selection) if it's agreed.
		- Adding some logic to the DRAW_LINE action so it can be even improved.
		- Not forgetting the main focus of the PR, adding a way for the user to edit the outValue of nodes.
	When adding a value to a particular time, instead of checking if there's a node there already and manually setting its value, we just assign it with a new AutomationNode. QMap silently removes the existing node and adds the new one.
	Adds an upgrade method for the change in the automation nodes settings. The "value" attribute of the <time> element is deleted and assigned to the "inValue" and "outValue" attributes.

	Now older project files can be loaded properly.
	This commit introduces a way for the user to drag outValues on the Automation Editor, by Alt+clicking the outValue red node and dragging up and down.

	A new action was added for that purpose, called MOVE_OUTVALUE. When the user clicks a outValue sphere with the Alt modifier, m_action is set to MOVE_OUTVALUE, and the time position of the node being affected is stored on m_draggedOutValueKey. That is later used on the mouseMoveEvent to update the outValue of the node.

	Removed repeated code on the mouseReleaseEvent and removed excessive blank lines after a method.

	Things to keep in mind when testing through this commit's build:
		- Creating/Moving an automation node resets the outValue
		- When the outValue is changed, generateTangents isn't called automatically. So you'll notice that after adding another automation node the curves change (that's because after the new node being added the tangents are then recalculated).

	Still lots of work ahead!
	Unnecessary loop was removed with call to appropriate method.
	Creates a separate header and source file for the AutomationNode class.
	Adds 2 new members to the AutomationNode class:
		m_pattern - A pointer to the pattern this node belongs to
		m_key - The time position (timeMap key) of this node

	The constructors (and places they were used) were updated accordingly. AutomationNode was also made a friend class of AutomationPattern so it can access private/protected methods from its pointer. This will be later used to allow AutomationNode's to call generateTangents once their values are updated.

	Small fix on a code block related to moving selections inside the mouseMoveEvent from AutomationEditor.
	This commit removes code that was not currently used in the AutomationEditor. Most of it was related to a feature I believe was once functional but broke along the way, but the code was not cleaned up in an effort to fix it later. The feature allowed selecting and moving/cutting/copying/pasting/removing values from the automation pattern. It added up to lots of lines of code, which so far I was keeping up to date to the changes. However, I believe (and others devs agree) that rewritting this code later might be a better approach than trying to fix what we currently have, so I'm removing the obsolete code. The git history will allow us to reference back to it when implementing the feature again and this will make it harder for this PR to introduce bugs because a certain affected feature couldn't be tested.

	It also makes reviewing easier, for there are less affected code to cover.

For reviewing purposes:
	I used a single commit for removing the mentioned code, so its diff in relation to the previous commit should give a good idea of everything that was removed.
	The methods that change the inValue and outValue of nodes now also generate new tangents for the previous, current and next nodes (the ones affected), so now when the user drags the node outValue the curve is updated accordingly.
	Adds a method to the automation node that returns the valueOffset between inValue and outValue. AutomationPattern::putValue now has an extra parameter called outValueOffset (defaults to 0), so when it adds a node to the timeMap the outValue is set according to this offset. flipY() will calculate the offset and invert it, so the discrete jumps are kept but flipped vertically as well. flipX() doesn't need to calculate this offset because it uses the outValue itself.

Obs:
	I believe cleanObjects() was meant to be called everytime AutomationPattern::flipX() is called, but it was inside a conditional that would only call it on some situations. I moved it outside of the conditional.
	User can now reset the outValue of nodes on a very analogous way to removing nodes but with the Alt key pressed. Alt + right clicking over the node (or dragging over an area), or Alt + clicking on it on Erase mode (or dragging over an area) will reset the outValues of the nodes.
	To do that, two new actions were created: ERASE_VALUES (for the regular node removing) and RESET_OUTVALUES (for the outValues reseting). Those are checked for in the moveMouseEvent, which acts accordingly. The removePoints() method was removed and two new methods were added instead: removeNodes() and resetNodes(), which will remove nodes on a tick range or reset them respectivately.

	The AutomationNode.h and AutomationNode.cpp files were fixed to fit the current code style conventions. The other files were kept as is, so they can be changed all at once at the end of the PR.

	Change requests made by Veratil were done.
	This commit makes a small change to setDragValue. When starting the drag (m_dragging == false), it checks if the time position being dragged already has a node. If it does, then the offset between inValue and offValue is stored on m_dragOutValueOffset so it can be used on the putValue calls, keeping the offset. If there isn't a node in the time position, m_dragOutValueOffset is set to 0.
	Fixes the modified code to comply to current code style convention.

	I tried to keep the changes exclusive to the lines modified by this PR (to keep the diff cleaner), but I might have fixed a couple of other because either they were hard to differentiate on the current diff or because they were too close in context. But still, tried to keep changes mostly to the lines actually changed by the PR.
	Adds a CSS property for the outValue color and renames the one used for the inValue color so they are consistent.

	Colors were added to classic and default themes. The original inValue colors were kept, but to fit with the outValue node they had a little bit of transparency added.
	Adds doxygen comments explaining methods that were either introduced by this PR or which had parameters modified by this PR.

	Changes valueAt(timeMap::iterator, int offset) method, so it can handle offsets equal to 0 properly. This method is currently never used with an offset of 0 (because this case scenario is handled before this method is called), but it was a simply modification so I just added the conditional to make it possible to use an offset of 0.
	AutomationPattern::flipX and AutomationPattern::flipY had some issues to the logic that caused UBs (from accessing an iterator past QMap::end()) and possibly misbehaviors when flipping an empty pattern.
	Both were refactored to fix those noted issues.
	Adds another editing mode to Automation Editor (DRAW_OUTVALUES), specific to deal with node outValues. The Pixmap being used is the same as the DRAW edit mode for now.

	The way it works now:

DRAW Mode (Shortcut SHIFT+D)
	Shift + Left click = Draws lines of nodes
	Left click = Draws/Drag node
	Right click = Remove nodes

ERASE Mode (Shortcut SHIFT+E)
	Left click = Remove nodes
	Right click = Reset outValues

DRAW_OUTVALUES Mode (Shortcut SHIFT+C)
	Left click = Drags outValue
	Right click = Reset outValues

	Now using a switch statement on the events to make things more organized.
	Now, instead of only being able to change an outValue by clicking over the sphere representing it, the user can also click on any time on the pattern: If the quantized time of the place he clicked has a node, the outValue of its node will be set to the value where the mouse click happened and the outValue will start being dragged. Very similar to the way it works for the node itself on the draw mode.
	Adds a recursive mutex to the AutomationPattern class and locks it on every method that access the member variables. Also rename the mutex from the AutomationEditor class and add locks to some methods that didn't have it before (except on methods that don't access member variables).
	Applies changes requested by Veratil:
		- Replaces NULL with nullptr where necessary on AutomationEditor.cpp
		- Fixes spacing on the mutex commit (plus some other places)
		- Changes some if blocks to one liners
		- Replace while with do-while on some places, since the condition was already checked for earlier on the method.
		- Moves getNodeAt call a level up on the block, since it's called on both conditionals below.
		- Fixes identation on some code inside AutomationEditor::mousePressEvent.
		- Adds explicits blocks on a switch statement. Even though this was not necessary for that particular one (because there was no variable declaration inside it) it helps keeping it consistent with another switch statement that happened earlier.

I also added a break statement to the last case of a switch (even though it was not needed, it's safer to avoid mistakes in the future with new cases being added).
	The red sphere representing the outValue was drawed after the blue sphere representing the inValue. Because of that, if they had the same value the red sphere would be on top. For the user, it makes more sense to be able to see the blue sphere representing the input value on top instead. This commit changes the order of the drawing.
	Fixes some comments pointed out on Spekular's review. Changes the AutomationNode's variable m_key to m_pos (leaving a comment on the header reminding that it matches the timeMap key). Removes comments related to removed code. Fixes code style on a pointer declaration.
	Instead of creating a getter and setter for each QProperty, we use MEMBER instead and access those variables directly.
	Overloads compound assignment operators +=, -=, *= and /= for AutomationNodes, making it so they affect the inValue and outValue of the node being assigned. Changes AutomationPattern::flipY() so it uses the new operators.
	Makes the AutomationEditor::getNodeAt method more efficient, by exiting if the node we are checking is already past the position we given (since the nodes are ordered in the timeMap, all subsequent nodes will also be past the position). Now instead of returning the last node that is inside the coordinates, it returns the first.
	Also improves AutomationEditor::mousePressEvent to avoid getNodeAt being called twice unnecessarily.
	Now, instead of keeping the offset between the inValue and outValue while dragging a node, setDragValue will either keep the current outValue intact, or move it together with the inValue if they are the same.
	The putValue method now doesn't have an offset parameter. If we want to put a node with an outValue different from the inValue we use putValues instead.
	Creates a boolean before the m_editMode switch, that will be true if the action being processed affects outValues and false if it affects inValues. That way we can move the statement clickedNode=getNodeAt() before the switch-case, reducing repeated lines.
@LmmsBot
Copy link

LmmsBot commented Feb 28, 2021

🤖 Hey, I'm @LmmsBot from github.com/lmms/bot and I made downloads for this pull request, click me to make them magically appear! 🎩

Linux

Windows

macOS

🤖
{"platform_name_to_artifacts": {"Linux": [{"artifact": {"title": {"title": "(AppImage)", "platform_name": "Linux"}, "link": {"link": "https://13268-15778896-gh.circle-artifacts.com/0/lmms-1.3.0-alpha.1.155%2Bg1da2783-linux-x86_64.AppImage"}}, "build_link": "https://circleci.com/gh/LMMS/lmms/13268?utm_campaign=vcs-integration-link&utm_medium=referral&utm_source=github-build-link"}], "Windows": [{"artifact": {"title": {"title": "32-bit", "platform_name": "Windows"}, "link": {"link": "https://13270-15778896-gh.circle-artifacts.com/0/lmms-1.3.0-alpha.1.155%2Bg1da2783bb-mingw-win32.exe"}}, "build_link": "https://circleci.com/gh/LMMS/lmms/13270?utm_campaign=vcs-integration-link&utm_medium=referral&utm_source=github-build-link"}, {"artifact": {"title": {"title": "64-bit", "platform_name": "Windows"}, "link": {"link": "https://13269-15778896-gh.circle-artifacts.com/0/lmms-1.3.0-alpha.1.155%2Bg1da2783bb-mingw-win64.exe"}}, "build_link": "https://circleci.com/gh/LMMS/lmms/13269?utm_campaign=vcs-integration-link&utm_medium=referral&utm_source=github-build-link"}], "macOS": [{"artifact": {"title": {"title": "", "platform_name": "macOS"}, "link": {"link": "https://13266-15778896-gh.circle-artifacts.com/0/lmms-1.3.0-alpha.1.155%2Bg1da2783bb-mac10.14.dmg"}}, "build_link": "https://circleci.com/gh/LMMS/lmms/13266?utm_campaign=vcs-integration-link&utm_medium=referral&utm_source=github-build-link"}]}, "commit_sha": "7eae19c0644e11142f474a7a8c6a4f2a7a4a43c7"}

	Double clicking on Erase mode caused a seg fault because only
Draw and Draw Outvalues modes were being handled but later code expected
variables to have been set on either of those.
	This commit now uses a lambda function instead to fine tune
values, fixes the bug, removes a warning of unhandled cases (by adding a
default case) and makes it so update() is only called if something
changed.
@IanCaio
Copy link
Contributor Author

IanCaio commented Feb 28, 2021

Fixed the mentioned bug, should be good for testing and reviewing.

@Spekular
Copy link
Member

Why not open a dialogue that exposes both the in and out values (potentially highlighting the one that was clicked)? This would be convenient if one wants to adjust both, or struggles to click the correct one (in and out values are the same or very close).

@IanCaio
Copy link
Contributor Author

IanCaio commented Feb 28, 2021

Why not open a dialogue that exposes both the in and out values (potentially highlighting the one that was clicked)? This would be convenient if one wants to adjust both, or struggles to click the correct one (in and out values are the same or very close).

That sounds good in terms of UX, but I don't think Qt has a convenient input dialog for 2 floats, so I think a custom one would have to be made. I'm not very experienced with Qt, so if that custom dialog isn't too complex to write and someone want to give it a go it's cool.

But as for the issue you mentioned, what defines whether the inValue or outValue will be adjusted is the mode you're in, so it isn't a problem if they are in the same level as the Draw mode will always edit the inValue and the Draw OutValues mode will always edit the outValue. One thing that I probably should change is that if the inValue is equal to the outValue, setting the former should adjust the latter as well (kind of like how dragging works now).

	Now, if the offset between inValue and outValue of a node is 0
(meaning they are both the same), fine tuning an inValue will set the
outValue to the same. In terms of UX that's more desirable (it's also
how the dragging of nodes work).
@IanCaio
Copy link
Contributor Author

IanCaio commented Feb 28, 2021

Now fine tuning the inValue of a node that has inValue equal to the outValue will change both, the same way as dragging works.

@superpaik
Copy link
Contributor

It works ok on windows.
Some thoughts.
-In Erase Mode it's only possible to delete "normal" automation points, not outValues. Maybe it'd be ok to erase also outValues, I don't see the point of adding a new tool option to do that. And thinking about that, why do not have just one tool for creating automation points? Normal points are like they are now, outValues are created with a modifier key (like "Ctrl" or "Shift" and mouse action). Also right click would delete the point you are over, regardless of the selected tool.
-As for the project xml I notice a strange value "" inside the automation track. For sure it's not related to this PR, but just in case.
Capture

@IanCaio
Copy link
Contributor Author

IanCaio commented Mar 2, 2021

Thanks for testing man!

In Erase mode you should be able to reset outValues right clicking, while left clicking removes the node completely. You can also reset the outValues on the Draw OutValue mode with the right mouse button as well.

The Draw OutValue mode and mouse behavior was set on #5712 . Originally, the idea was to be able to edit the outValues through the Draw edit mode using alt as a modifier to drag them, as ctrl and shift are already used ("Keep surrounding nodes" and "Draw line" respectively). The problem is that in some OSs it's problematic to use alt as a modifier key as @Spekular pointed out here, so among the available alternatives I thought the extra mode was the better one. It's possible that we review the controls and change them in the future, but for now it would be complicated because it involves changing the other shortcuts as well.

As for the removing of nodes and reseting of outValues with the same mouse click, it could be done. But I think the workflow with LMB to remove nodes and RMB to reset outValues is faster because you don't have to worry about clicking on the top of the right sphere, and you can also quickly click and drag to remove/reset multiple nodes. I rarely use the Erase mode tbh though.

I didn't notice which strange value you mean on the XML. The nodes are stored as <time>, and the inValue is stored on value (for backwards compatibility I used value instead of inValue) and the outValue is stored on outValue. The time position is stored under pos. So <time outValue="115.3" value="152.6" pos="120"> means that you have a node 120 ticks from the beginning of the pattern, with the inValue equal to 152.6 and the outValue equal to 115.3.

@superpaik
Copy link
Contributor

Oh, sorry. I didn't get the right-click to delete outValues while on Erase Mode. That's perfect! Can't we have the same behaviour for creating nodes? I mean left click to create normal nodes, right-click for outValue Nodes? I don't know if that it's problematic with OSs other than Windows, but it's kind of mimicking the current Erase behaviour.

As for the XML file, sorry there's a misspelling in my comment. The thing it's related to what it's highlighted on the image, the "" element without content. As I said, I doubt it's related to this PR and maybe it's there for future implementations. The information regarding points/time it is correct.

@superpaik
Copy link
Contributor

superpaik commented Mar 2, 2021

Sorry again. Don't worry about the XML file. It seems that this element is on version 1.2 as well. Maybe it is to indicate that the track is an automation track, but I believe that "type=5" is for automation tracks.
Note: again the xml "automationtrack" tag has been deleted by github when saving ¿?

@IanCaio
Copy link
Contributor Author

IanCaio commented Mar 2, 2021

Oh, sorry. I didn't get the right-click to delete outValues while on Erase Mode. That's perfect! Can't we have the same behaviour for creating nodes? I mean left click to create normal nodes, right-click for outValue Nodes? I don't know if that it's problematic with OSs other than Windows, but it's kind of mimicking the current Erase behaviour.

That would work, would just need to have a more broad opinion survey, as I think many people might be used to the convenience of removing with the right click (I think I only remove nodes that way haha). But if it's compensated by a faster workflow with outValues it could be changed with no problems!

Sorry again. Don't worry about the XML file. It seems that this element is on version 1.2 as well. Maybe it is to indicate that the track is an automation track, but I believe that "type=5" is for automation tracks.
Note: again the xml "automationtrack" tag has been deleted by github when saving ¿?

No worries, I realized what you were referring to now. It's weird indeed there's a tag with no information or children. The track type is marked on the attributes of <track>, maybe it's just some visual aid so people reading the XML can quickly see it's an Automation Track, without having to know the meaning of type = 5? Or maybe there's some information that could go there but it just happens your project didn't have it? Idk to be honest

src/gui/editors/AutomationEditor.cpp Outdated Show resolved Hide resolved
src/gui/editors/AutomationEditor.cpp Outdated Show resolved Hide resolved
	Moves fineTuneValue to a method inside AutomationEditor so it
can be accessed from other places in the future.
	Removes needsUpdate variable and instead just calls for update
where necessary inside the double click event.
@IanCaio
Copy link
Contributor Author

IanCaio commented Mar 26, 2021

Moved fineTuneValue to a separate method and removed that needsUpdate variable.

Copy link
Member

@Spekular Spekular left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

src/gui/editors/AutomationEditor.cpp Outdated Show resolved Hide resolved
src/gui/editors/AutomationEditor.cpp Outdated Show resolved Hide resolved
src/gui/editors/AutomationEditor.cpp Outdated Show resolved Hide resolved
src/gui/editors/AutomationEditor.cpp Outdated Show resolved Hide resolved
src/gui/editors/AutomationEditor.cpp Outdated Show resolved Hide resolved
src/gui/editors/AutomationEditor.cpp Outdated Show resolved Hide resolved
@IanCaio
Copy link
Contributor Author

IanCaio commented Mar 26, 2021

Done! Did a quick test, everything looks fine

@Veratil
Copy link
Contributor

Veratil commented Mar 27, 2021

Done! Did a quick test, everything looks fine

If all is working the same as before, LGTM. 👍

	Uses an inverse check on the "ok" variable to reduce the
indentation on fineTuneValue.
@IanCaio IanCaio merged commit 3ab86fa into LMMS:master Mar 28, 2021
devnexen pushed a commit to devnexen/lmms that referenced this pull request Apr 10, 2021
…MMS#5923)

Co-authored-by: tecknixia <50790262+tecknixia@users.noreply.github.com>
sdasda7777 pushed a commit to sdasda7777/lmms that referenced this pull request Jun 28, 2022
…MMS#5923)

Co-authored-by: tecknixia <50790262+tecknixia@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants