tracktion-engine 3.0-10-g034fdde4aa5
Tracktion Engine — High level data model for audio applications

« « « Anklang Documentation
Loading...
Searching...
No Matches
tracktion_SelectionManager.cpp
Go to the documentation of this file.
1 /*
2 ,--. ,--. ,--. ,--.
3 ,-' '-.,--.--.,--,--.,---.| |,-.,-' '-.`--' ,---. ,--,--, Copyright 2024
4 '-. .-'| .--' ,-. | .--'| /'-. .-',--.| .-. || \ Tracktion Software
5 | | | | \ '-' \ `--.| \ \ | | | |' '-' '| || | Corporation
6 `---' `--' `--`--'`---'`--'`--' `---' `--' `---' `--''--' www.tracktion.com
7
8 Tracktion Engine uses a GPL/commercial licence - see LICENCE.md for details.
9*/
10
11namespace tracktion { inline namespace engine
12{
13
16{
17 SelectableUpdateTimer (std::function<void()> onDelete_)
18 : onDelete (onDelete_) {}
19
20 ~SelectableUpdateTimer() override
21 {
22 if (onDelete)
23 onDelete();
24 }
25 void add (Selectable* s)
26 {
27 const juce::ScopedLock sl (lock);
28 selectables.add (s);
29 }
30
31 void remove (Selectable* s)
32 {
33 const juce::ScopedLock sl (lock);
34 selectables.removeValue (s);
35 }
36
37 bool isValid (const Selectable* s) const
38 {
39 const juce::ScopedLock sl (lock);
40 return selectables.contains (const_cast<Selectable*> (s));
41 }
42
43 void handleAsyncUpdate() override
44 {
46 std::vector<Selectable*> needingUpdate;
47
48 {
49 const juce::ScopedLock sl (lock);
50
51 for (auto s : selectables)
52 if (s->needsAnUpdate)
53 needingUpdate.push_back (s);
54 }
55
56 for (auto s : needingUpdate)
57 if (isValid (s))
58 s->sendChangeCallbackToListenersIfNeeded();
59 }
60
61 juce::CriticalSection listenerLock;
62
63private:
66 std::function<void()> onDelete;
67
69};
70
71static SelectableUpdateTimer* updateTimerInstance = nullptr;
72
73void Selectable::initialise()
74{
75 if (updateTimerInstance == nullptr)
76 updateTimerInstance = new SelectableUpdateTimer ([]() { updateTimerInstance = nullptr; });
77}
78
79//==============================================================================
80Selectable::Selectable()
81{
82 if (updateTimerInstance)
83 updateTimerInstance->add (this);
84}
85
86Selectable::~Selectable()
87{
88 masterReference.clear();
89
90 if (! hasNotifiedListenersOfDeletion)
91 {
92 // must call notifyListenersOfDeletion() in the innermost subclass's destructor!
94
95 notifyListenersOfDeletion();
96 }
97}
98
102
104{
105 if (selectableListeners.size() > 0)
106 {
107 needsAnUpdate = true;
108 updateTimerInstance->triggerAsyncUpdate();
109 }
110}
111
113{
114 needsAnUpdate = false;
115}
116
118{
119 return s != nullptr && updateTimerInstance->isValid (s);
120}
121
122void Selectable::addListener (SelectableListener* l)
123{
124 addSelectableListener (l);
125}
126
127void Selectable::removeListener (SelectableListener* l)
128{
129 removeSelectableListener (l);
130}
131
132void Selectable::addSelectableListener (SelectableListener* l)
133{
134 TRACKTION_ASSERT_MESSAGE_THREAD
135 jassert (l != nullptr);
136 jassert (! isCallingListeners);
137 jassert (! hasNotifiedListenersOfDeletion);
138 selectableListeners.add (l);
139}
140
141void Selectable::removeSelectableListener (SelectableListener* l)
142{
143 TRACKTION_ASSERT_MESSAGE_THREAD
144 jassert (! isCallingListeners);
145 selectableListeners.remove (l);
146}
147
148void Selectable::sendChangeCallbackToListenersIfNeeded()
149{
150 TRACKTION_ASSERT_MESSAGE_THREAD
151 needsAnUpdate = false;
152
153 const juce::ScopedValueSetter<bool> svs (isCallingListeners, true);
154
155 selectableListeners.call ([self = makeSafeRef (*this)] (SelectableListener& l)
156 {
157 if (auto s = self.get())
158 l.selectableObjectChanged (s);
159 else
161 });
162}
163
164void Selectable::propertiesChanged()
165{
166 for (SelectionManager::Iterator sm; sm.next();)
167 {
168 if (sm->isSelected (this))
169 {
170 auto list = sm->getSelectedObjects();
171 sm->deselectAll();
172
173 for (auto s : list)
174 sm->select (s, true);
175 }
176 }
177}
178
179void Selectable::notifyListenersOfDeletion()
180{
181 if (! hasNotifiedListenersOfDeletion)
182 {
183 hasNotifiedListenersOfDeletion = true;
184
185 updateTimerInstance->remove (this);
186
187 if (! selectableListeners.isEmpty())
188 {
190
191 // Use a local copy in case any listeners get deleted during the callbacks
193
194 for (auto l : selectableListeners.getListeners())
195 copy.add (l);
196
197 copy.call ([self = makeSafeRef (*this)] (SelectableListener& l)
198 {
199 if (auto s = self.get())
200 {
201 if (! s->selectableListeners.contains (&l))
202 return;
203
204 l.selectableObjectAboutToBeDeleted (s);
205 }
206 else
207 {
208 jassertfalse;
209 }
210 });
211 }
212
214 deselect();
215 }
216}
217
218void Selectable::deselect()
219{
220 for (SelectionManager::Iterator sm; sm.next();)
221 sm->deselect (this);
222}
223
224//==============================================================================
225Selectable::Listener::Listener (Selectable& s)
226{
227 selectable = &s;
228 if (selectable)
229 selectable->addSelectableListener (this);
230}
231
232Selectable::Listener::~Listener()
233{
234 if (selectable)
235 selectable->removeSelectableListener (this);
236}
237
238//==============================================================================
239SelectableClass::SelectableClass() {}
240SelectableClass::~SelectableClass() {}
241
242static juce::Array<SelectableClass::ClassInstanceBase*>& getAllSelectableClasses()
243{
245 return classes;
246}
247
248static inline std::unordered_map<std::type_index, SelectableClass*>& getSelectableClassCache()
249{
251 return cache;
252}
253
254SelectableClass::ClassInstanceBase::ClassInstanceBase() { getAllSelectableClasses().add (this); }
255SelectableClass::ClassInstanceBase::~ClassInstanceBase() { getAllSelectableClasses().removeAllInstancesOf (this); }
256
257SelectableClass* SelectableClass::findClassFor (const Selectable& s)
258{
259 #if ! JUCE_DEBUG
260 auto& cache = getSelectableClassCache();
261 const std::type_index typeIndex (typeid (s));
262
263 if (auto found = cache.find (typeIndex); found != cache.end())
264 return found->second;
265
266 for (auto cls : getAllSelectableClasses())
267 {
268 if (auto c = cls->getClassForObject (&s))
269 {
270 cache[typeIndex] = c;
271 return c;
272 }
273 }
274 #else
275 SelectableClass* result = nullptr;
276
277 for (auto cls : getAllSelectableClasses())
278 {
279 if (auto c = cls->getClassForObject (&s))
280 {
281 if (result == nullptr)
282 result = c;
283 else
284 jassertfalse; // more than one SelectableClass thinks it applies to this object
285 }
286 }
287
288 if (result != nullptr)
289 return result;
290 #endif
291
292 return {};
293}
294
295SelectableClass* SelectableClass::findClassFor (const Selectable* s)
296{
297 return s != nullptr ? findClassFor (*s) : nullptr;
298}
299
300//==============================================================================
302{
303 if (selectedObjects.size() == 1)
304 if (auto s = selectedObjects.getFirst())
305 return s->getSelectableDescription();
306
307 juce::StringArray names;
308
309 for (auto o : selectedObjects)
310 if (o != nullptr)
311 names.addIfNotAlreadyThere (o->getSelectableDescription());
312
313 if (names.size() == 1)
314 return juce::String (TRANS("123 Objects of Type: XYYZ"))
315 .replace ("123", juce::String (selectedObjects.size()))
316 .replace ("XYYZ", names[0]);
317
318 return juce::String (TRANS("123 Objects"))
319 .replace ("123", juce::String (selectedObjects.size()));
320}
321
322bool SelectableClass::canBeSelected (const Selectable&) { return true; }
325
327{
328 return false;
329}
330
332{
333 return otherClass == this;
334}
335
340
344
348
349bool SelectableClass::canCutSelected (const SelectableList&)
350{
351 return true;
352}
353
354bool SelectableClass::areAllObjectsOfUniformType (const SelectableList& list)
355{
356 if (list.size() <= 1)
357 return true;
358
359 auto s1 = list.getUnchecked (0);
360
361 for (int i = list.size(); --i > 0;)
362 {
363 auto s2 = list.getUnchecked (i);
364
365 if (typeid (*s1) != typeid (*s2))
366 return false;
367 }
368
369 return true;
370}
371
372//==============================================================================
373static juce::Array<SelectionManager*> allManagers;
374
375SelectionManager::SelectionManager (Engine& e) : ChangeBroadcaster(), engine (e)
376{
377 allManagers.add (this);
378}
379
380SelectionManager::~SelectionManager()
381{
382 clearList();
383 allManagers.removeAllInstancesOf (this);
384}
385
386void SelectionManager::selectionChanged()
387{
388 ++selectionChangeCount;
390}
391
392SelectableClass* SelectionManager::getFirstSelectableClass() const
393{
394 return SelectableClass::findClassFor (selected.getFirst());
395}
396
397void SelectionManager::clearList()
398{
399 for (auto s : makeSafeVector (selected))
400 if (s != nullptr)
401 s->removeSelectableListener (this);
402
403 selected.clear();
404}
405
406SelectionManager::Iterator::Iterator() {}
407
408bool SelectionManager::Iterator::next()
409{
410 return juce::isPositiveAndBelow (++index, allManagers.size());
411}
412
413SelectionManager* SelectionManager::Iterator::get() const
414{
415 return allManagers[index];
416}
417
418int SelectionManager::getNumObjectsSelected() const
419{
420 return selected.size();
421}
422
423Selectable* SelectionManager::getSelectedObject (int index) const
424{
425 const juce::ScopedLock sl (lock);
426 return selected[index];
427}
428
429const SelectableList& SelectionManager::getSelectedObjects() const
430{
431 return selected;
432}
433
434bool SelectionManager::isSelected (const Selectable* object) const
435{
436 return object != nullptr && isSelected (*object);
437}
438
439bool SelectionManager::isSelected (const Selectable& object) const
440{
441 return selected.contains (const_cast<Selectable*> (&object));
442}
443
444void SelectionManager::deselectAll()
445{
446 const juce::ScopedLock sl (lock);
447
448 if (selected.size() > 0)
449 {
450 for (auto s : makeSafeVector (selected))
451 if (s != nullptr)
452 s->selectionStatusChanged (false);
453
454 clearList();
455 selectionChanged();
456 }
457}
458
459static bool canBeSelected (Selectable& newItem)
460{
461 if (auto newItemClass = SelectableClass::findClassFor (newItem))
462 return newItemClass->canBeSelected (newItem);
463 return true;
464}
465
466static bool canSelectAtTheSameTime (const SelectableList& selected, Selectable& newItem)
467{
468 auto newItemClass = SelectableClass::findClassFor (newItem);
469
470 for (int i = 0; i < selected.size(); ++i)
471 {
472 auto s = selected.getUnchecked (i);
473
474 if (auto currentClass = selected.getSelectableClass (i))
475 if (! (currentClass->canClassesBeSelectedAtTheSameTime (newItemClass)
476 && currentClass->canObjectsBeSelectedAtTheSameTime (*s, newItem)))
477 return false;
478 }
479
480 return true;
481}
482
483void SelectionManager::selectOnly (Selectable* s) { select (s, false); }
484void SelectionManager::selectOnly (Selectable& s) { select (s, false); }
485void SelectionManager::addToSelection (Selectable* s) { select (s, true); }
486void SelectionManager::addToSelection (Selectable& s) { select (s, true); }
487
488void SelectionManager::select (Selectable* s, bool addToCurrentSelection)
489{
490 if (s != nullptr)
491 select (*s, addToCurrentSelection);
492}
493
494void SelectionManager::select (Selectable& s, bool addToCurrentSelection)
495{
496 const juce::ScopedLock sl (lock);
497
499 {
500 selected.removeAllInstancesOf (&s);
501 return;
502 }
503
504 if (! canBeSelected (s))
505 return;
506
507 if (! selected.contains (&s))
508 {
509 addToCurrentSelection = addToCurrentSelection
510 && canSelectAtTheSameTime (selected, s);
511
512 if (! addToCurrentSelection)
513 deselectAll();
514
515 selected.add (&s);
516 s.selectionStatusChanged (true);
517 selectionChanged();
518 s.addSelectableListener (this);
519 }
520 else if (! addToCurrentSelection)
521 {
522 for (int i = selected.size(); --i >= 0;)
523 if (selected.getUnchecked (i) != &s)
524 deselect (selected.getUnchecked (i));
525 }
526}
527
528void SelectionManager::select (const SelectableList& listSrc)
529{
530 SelectableList list;
531 for (auto s : listSrc)
532 if (Selectable::isSelectableValid (s) && canBeSelected (*s))
533 list.add (s);
534
535 if (list != selected)
536 {
537 deselectAll();
538
539 for (auto s : list)
540 select (s, true);
541 }
542}
543
544void SelectionManager::deselect (Selectable* s)
545{
546 const juce::ScopedLock sl (lock);
547 auto index = selected.indexOf (s);
548
549 if (index >= 0)
550 {
551 s->removeSelectableListener (this);
552 selected.remove (index);
553 s->selectionStatusChanged (false);
554 selectionChanged();
555 }
556}
557
558void SelectionManager::selectableObjectChanged (Selectable*)
559{
560}
561
562void SelectionManager::selectableObjectAboutToBeDeleted (Selectable* s)
563{
564 const juce::ScopedLock sl (lock);
565 auto index = selected.indexOf (s);
566
567 if (index >= 0)
568 {
569 selected.remove (index);
570 selectionChanged();
571 }
572}
573
574void SelectionManager::refreshPropertyPanel()
575{
576 selectionChanged();
577}
578
579void SelectionManager::refreshAllPropertyPanels()
580{
581 for (SelectionManager::Iterator sm; sm.next();)
582 sm->refreshPropertyPanel();
583}
584
585void SelectionManager::refreshAllPropertyPanelsShowing (Selectable& s)
586{
587 for (SelectionManager::Iterator sm; sm.next();)
588 if (sm->isSelected (s))
589 sm->refreshPropertyPanel();
590}
591
592void SelectionManager::deselectAllFromAllWindows()
593{
594 for (SelectionManager::Iterator sm; sm.next();)
595 sm->deselectAll();
596}
597
598SelectionManager* SelectionManager::findSelectionManagerContaining (const Selectable* s)
599{
600 for (SelectionManager::Iterator sm; sm.next();)
601 if (sm->isSelected (s))
602 return sm.get();
603
604 return {};
605}
606
607SelectionManager* SelectionManager::findSelectionManagerContaining (const Selectable& s)
608{
609 return findSelectionManagerContaining (&s);
610}
611
612//==============================================================================
613bool SelectionManager::copySelected()
614{
615 const juce::ScopedLock sl (lock);
616
617 // only allow groups of the same type to be selected at once.
618 if (auto cls = getFirstSelectableClass())
619 {
620 auto& clipboard = *Clipboard::getInstance();
621
622 SelectableClass::AddClipboardEntryParams clipboardParams (clipboard);
623 clipboardParams.items = selected;
624
625 if (editViewID != -1)
626 {
627 clipboardParams.edit = getEdit();
628 clipboardParams.editViewID = editViewID;
629 }
630
631 clipboard.clear();
632 cls->addClipboardEntriesFor (clipboardParams);
633
634 return ! clipboard.isEmpty();
635 }
636
637 return false;
638}
639
640void SelectionManager::deleteSelected()
641{
642 const juce::ScopedLock sl (lock);
643
644 if (auto cls = getFirstSelectableClass())
645 // use a local copy of the list, as it will change as things get deleted + deselected
646 cls->deleteSelected ({this, selected, false});
647}
648
649bool SelectionManager::cutSelected()
650{
651 const juce::ScopedLock sl (lock);
652
653 if (auto cls = getFirstSelectableClass())
654 {
655 if (cls->canCutSelected (selected) && copySelected())
656 {
657 // use a local copy of the list, as it will change as things get deleted + deselected
658 cls->deleteSelected ({this, selected, true});
659 return true;
660 }
661 }
662
663 return false;
664}
665
667{
668 const juce::ScopedLock sl (lock);
669
670 if (auto cls = getFirstSelectableClass())
671 // use a local copy of the list, as it will change as things get added + selected
672 return cls->pasteClipboard (SelectableList (selected), editViewID);
673
674 return false;
675}
676
677void SelectionManager::selectOtherObjects (SelectableClass::Relationship relationship, bool keepOldItemsSelected)
678{
679 const juce::ScopedLock sl (lock);
680
681 if (auto cls = getFirstSelectableClass())
682 {
684 {
685 *this,
686 selected,
687 relationship,
688 keepOldItemsSelected,
689 editViewID
690 };
691
692 cls->selectOtherObjects (params);
693 }
694}
695
697{
698 const juce::ScopedLock sl (lock);
699
700 if (auto cls = getFirstSelectableClass())
701 cls->keepSelectedObjectOnScreen (selected);
702}
703
704Edit* SelectionManager::getEdit() const
705{
706 return edit.get();
707}
708
709SelectionManager* SelectionManager::findSelectionManager (const juce::Component* c)
710{
711 if (auto smc = c->findParentComponentOfClass<ComponentWithSelectionManager>())
712 return smc->getSelectionManager();
713
714 return {};
715}
716
717SelectionManager* SelectionManager::findSelectionManager (const juce::Component& c)
718{
719 return findSelectionManager (&c);
720}
721
722SelectionManager::ScopedSelectionState::ScopedSelectionState (SelectionManager& m)
723 : manager (m), selected (manager.selected)
724{
725}
726
727SelectionManager::ScopedSelectionState::~ScopedSelectionState()
728{
729 manager.select (selected);
730}
731
732//==============================================================================
733bool SelectionManager::ChangedSelectionDetector::isFirstChangeSinceSelection (SelectionManager* sm)
734{
735 if (sm != nullptr)
736 {
737 int newCount = sm->selectionChangeCount;
738
739 if (lastSelectionChangeCount != newCount)
740 {
741 lastSelectionChangeCount = newCount;
742 return true;
743 }
744 }
745
746 return false;
747}
748
749void SelectionManager::ChangedSelectionDetector::reset()
750{
751 lastSelectionChangeCount = 0;
752}
753
754//==============================================================================
756{
757 if (index >= items.size())
758 return {};
759
760 if (index < classes.size())
761 if (auto sc = classes.getUnchecked (index))
762 if (sc != nullptr)
763 return sc;
764
765 classes.resize (items.size());
766 auto selectableClass = SelectableClass::findClassFor (items[index]);
767 classes.setUnchecked (index, selectableClass);
768
769 return selectableClass;
770}
771
773{
774 return std::pair<Selectable*, SelectableClass*> (items[index], getSelectableClass (index));
775}
776
777}} // namespace tracktion { inline namespace engine
select
int size() const noexcept
bool addIfNotAlreadyThere(const String &stringToAdd, bool ignoreCase=false)
String replace(StringRef stringToReplace, StringRef stringToInsertInstead, bool ignoreCase=false) const
The Tracktion Edit class!
Represents a type of object that can be selected.
virtual bool pasteClipboard(const SelectableList &currentlySelectedItems, int editViewID)
This gives the selected items a first chance to paste the clipboard contents when the user presses ct...
virtual bool canObjectsBeSelectedAtTheSameTime(Selectable &object1, Selectable &object2)
This is only called if canClassesBeSelectedAtTheSameTime() has already returned true for the other ob...
virtual void addClipboardEntriesFor(AddClipboardEntryParams &)
A class should use this to create XML clipboard entries for the given set of items.
virtual void keepSelectedObjectOnScreen(const SelectableList &currentlySelectedObjects)
if implemented, this should do whatever is appropriate to make these objects visible - e....
virtual void selectOtherObjects(const SelectOtherObjectsParams &)
Must try to find and select objects that are related to these ones in the specified way.
virtual bool canBeSelected(const Selectable &object)
If it's possible for an object to be selected.
virtual bool canClassesBeSelectedAtTheSameTime(SelectableClass *otherClass)
if it's possible for an object of this class to be selected at the same time as an object of the clas...
virtual juce::String getDescriptionOfSelectedGroup(const SelectableList &)
Must return a description of this particular group of objects.
virtual void deleteSelected(const DeleteSelectedParams &params)
Deletes this set of objects.
Base class for things that can be selected, and whose properties can appear in the properties panel.
virtual void changed()
This should be called to send a change notification to any SelectableListeners that are registered wi...
virtual void selectionStatusChanged(bool isNowSelected)
Can be overridden to tell this object that it has just been selected or deselected.
static bool isSelectableValid(const Selectable *) noexcept
checks whether this object has been deleted.
virtual void selectableAboutToBeDeleted()
Called just before the selectable is about to be deleted so any subclasses should still be valid at t...
void cancelAnyPendingUpdates()
If changed() has been called, this will cancel any pending async change notificaions.
Manages a list of items that are currently selected.
SafeSelectable< Edit > edit
If this SelectionManager is being used to represent items inside a particular view of an edit,...
void selectOtherObjects(SelectableClass::Relationship, bool keepOldItemsSelected)
Selects related objects, e.g.
bool pasteSelected()
Offers the selected things the chance to paste the contents of the clipboard onto themselves,...
void keepSelectedObjectsOnScreen()
Scrolls whatever is necessary to keep the selected stuff visible.
T is_pointer_v
#define TRANS(stringLiteral)
#define jassert(expression)
#define JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(className)
#define jassertfalse
bool isPositiveAndBelow(Type1 valueToTest, Type2 upperLimit) noexcept
auto makeSafeVector(const Iterable &selectables) -> std::vector< SafeSelectable< typename std::remove_reference< decltype(*selectables[0])>::type > >
Creates a std::vector<SafeSelectable<Something>> for a given juce::Array of selectable objects.
SafeSelectable< SelectableType > makeSafeRef(SelectableType &selectable)
Creates a SafeSelectable for a given selectable object.
A list of Selectables, similar to a juce::Array but contains a cached list of the SelectableClasses f...
std::pair< Selectable *, SelectableClass * > getSelectableAndClass(int index) const
Returns the selectable and it's associated class.
SelectableClass * getSelectableClass(int index) const
Returns the selectable class for a given Selectable in the list.
#define CRASH_TRACER
This macro adds the current location to a stack which gets logged if a crash happens.