├── .gitignore ├── Source ├── MainComponent.cpp ├── MainComponent.h ├── jcf_multi_component_dragger_demo.h ├── Main.cpp └── jcf_multi_component_dragger.h ├── LICENSE └── MultiSelection.jucer /.gitignore: -------------------------------------------------------------------------------- 1 | Builds/* 2 | installer/build.windows/* 3 | tmp.iss 4 | JuceLibraryCode/* 5 | -------------------------------------------------------------------------------- /Source/MainComponent.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | ============================================================================== 3 | 4 | This file was auto-generated! 5 | 6 | ============================================================================== 7 | */ 8 | 9 | #include "MainComponent.h" 10 | #include 11 | 12 | 13 | //============================================================================== 14 | MainContentComponent::MainContentComponent() 15 | { 16 | addAndMakeVisible(demo); 17 | setSize (600, 400); 18 | } 19 | 20 | MainContentComponent::~MainContentComponent() 21 | { 22 | 23 | } 24 | 25 | void MainContentComponent::paint (Graphics& g) 26 | { 27 | g.fillAll (Colour (0xff001F36)); 28 | 29 | } 30 | 31 | void MainContentComponent::resized() 32 | { 33 | demo.setBounds(getLocalBounds()); 34 | } 35 | -------------------------------------------------------------------------------- /Source/MainComponent.h: -------------------------------------------------------------------------------- 1 | /* 2 | ============================================================================== 3 | 4 | This file was auto-generated! 5 | 6 | ============================================================================== 7 | */ 8 | 9 | #ifndef MAINCOMPONENT_H_INCLUDED 10 | #define MAINCOMPONENT_H_INCLUDED 11 | 12 | #include "../JuceLibraryCode/JuceHeader.h" 13 | 14 | #include "jcf_multi_component_dragger_demo.h" 15 | 16 | //============================================================================== 17 | /* 18 | This component lives inside our window, and this is where you should put all 19 | your controls and content. 20 | */ 21 | class MainContentComponent : public Component 22 | { 23 | public: 24 | //============================================================================== 25 | MainContentComponent(); 26 | ~MainContentComponent(); 27 | 28 | void paint (Graphics&); 29 | void resized(); 30 | 31 | 32 | private: 33 | MultiComponentDraggerDemo demo; 34 | //============================================================================== 35 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainContentComponent) 36 | }; 37 | 38 | 39 | #endif // MAINCOMPONENT_H_INCLUDED 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jim Credland 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE.http://www.gnu.org/philosophy/why-not-lgpl.html>. 22 | -------------------------------------------------------------------------------- /Source/jcf_multi_component_dragger_demo.h: -------------------------------------------------------------------------------- 1 | /* 2 | ============================================================================== 3 | 4 | jcf_multi_selection_demo.h 5 | Created: 25 Feb 2016 9:00:56am 6 | Author: Jim Credland 7 | 8 | ============================================================================== 9 | */ 10 | 11 | #ifndef JCF_MULTI_SELECTION_DEMO_H_INCLUDED 12 | #define JCF_MULTI_SELECTION_DEMO_H_INCLUDED 13 | 14 | #include "../JuceLibraryCode/JuceHeader.h" 15 | #include "jcf_multi_component_dragger.h" 16 | 17 | class MultiComponentDraggerDemo : public Component 18 | { 19 | public: 20 | MultiComponentDraggerDemo() 21 | { 22 | dragger.setShiftConstrainsDirection(true); 23 | dragger.setConstrainBoundsToParent(true, {0, 0, 10, 10} /* amount permitted offscreen. */); 24 | 25 | Random random; 26 | 27 | for (int i = 0; i < 10; ++i) 28 | { 29 | auto c = new ExampleComponent(dragger); 30 | c->setBounds(random.nextInt(570), random.nextInt(370), 30, 30); 31 | addAndMakeVisible(c); 32 | } 33 | } 34 | 35 | ~MultiComponentDraggerDemo() 36 | { 37 | deleteAllChildren(); 38 | } 39 | 40 | class ExampleComponent : public Component 41 | { 42 | public: 43 | ExampleComponent(MultiComponentDragger & dragger_) 44 | : 45 | dragger(dragger_) 46 | { 47 | Random random; 48 | colour = Colours::red.withHue(random.nextFloat()); 49 | } 50 | 51 | void paint(Graphics & g) override 52 | { 53 | g.fillAll(colour.withSaturation(0.5f).withMultipliedBrightness(0.5f)); 54 | 55 | if (dragger.isSelected(this)) 56 | { 57 | g.setColour(colour); 58 | g.drawRect(getLocalBounds(), 4.0f); 59 | } 60 | } 61 | 62 | void mouseDown(const MouseEvent & e) override 63 | { 64 | dragger.handleMouseDown(this, e); 65 | } 66 | 67 | void mouseUp(const MouseEvent & e) override 68 | { 69 | dragger.handleMouseUp(this, e); 70 | } 71 | 72 | void mouseDrag(const MouseEvent & e) override 73 | { 74 | dragger.handleMouseDrag(e); 75 | } 76 | 77 | MultiComponentDragger & dragger; 78 | Colour colour; 79 | }; 80 | 81 | void mouseUp(const MouseEvent & e) 82 | { 83 | dragger.deselectAll(); 84 | } 85 | 86 | private: 87 | MultiComponentDragger dragger; 88 | 89 | }; 90 | 91 | #endif // JCF_MULTI_SELECTION_DEMO_H_INCLUDED 92 | -------------------------------------------------------------------------------- /MultiSelection.jucer: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 10 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 23 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /Source/Main.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | ============================================================================== 3 | 4 | This file was auto-generated by the Introjucer! 5 | 6 | It contains the basic startup code for a Juce application. 7 | 8 | ============================================================================== 9 | */ 10 | 11 | #include "../JuceLibraryCode/JuceHeader.h" 12 | #include "MainComponent.h" 13 | 14 | 15 | //============================================================================== 16 | class MultiSelectionApplication : public JUCEApplication 17 | { 18 | public: 19 | //============================================================================== 20 | MultiSelectionApplication() {} 21 | 22 | const String getApplicationName() override { return ProjectInfo::projectName; } 23 | const String getApplicationVersion() override { return ProjectInfo::versionString; } 24 | bool moreThanOneInstanceAllowed() override { return true; } 25 | 26 | //============================================================================== 27 | void initialise (const String& commandLine) override 28 | { 29 | // This method is where you should put your application's initialisation code.. 30 | 31 | mainWindow = new MainWindow (getApplicationName()); 32 | } 33 | 34 | void shutdown() override 35 | { 36 | // Add your application's shutdown code here.. 37 | 38 | mainWindow = nullptr; // (deletes our window) 39 | } 40 | 41 | //============================================================================== 42 | void systemRequestedQuit() override 43 | { 44 | // This is called when the app is being asked to quit: you can ignore this 45 | // request and let the app carry on running, or call quit() to allow the app to close. 46 | quit(); 47 | } 48 | 49 | void anotherInstanceStarted (const String& commandLine) override 50 | { 51 | // When another instance of the app is launched while this one is running, 52 | // this method is invoked, and the commandLine parameter tells you what 53 | // the other instance's command-line arguments were. 54 | } 55 | 56 | //============================================================================== 57 | /* 58 | This class implements the desktop window that contains an instance of 59 | our MainContentComponent class. 60 | */ 61 | class MainWindow : public DocumentWindow 62 | { 63 | public: 64 | MainWindow (String name) : DocumentWindow (name, 65 | Colours::lightgrey, 66 | DocumentWindow::allButtons) 67 | { 68 | setUsingNativeTitleBar (true); 69 | setContentOwned (new MainContentComponent(), true); 70 | 71 | centreWithSize (getWidth(), getHeight()); 72 | setVisible (true); 73 | } 74 | 75 | void closeButtonPressed() override 76 | { 77 | // This is called when the user tries to close this window. Here, we'll just 78 | // ask the app to quit when this happens, but you can change this to do 79 | // whatever you need. 80 | JUCEApplication::getInstance()->systemRequestedQuit(); 81 | } 82 | 83 | /* Note: Be careful if you override any DocumentWindow methods - the base 84 | class uses a lot of them, so by overriding you might break its functionality. 85 | It's best to do all your work in your content component instead, but if 86 | you really have to override any DocumentWindow methods, make sure your 87 | subclass also calls the superclass's method. 88 | */ 89 | 90 | private: 91 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainWindow) 92 | }; 93 | 94 | private: 95 | ScopedPointer mainWindow; 96 | }; 97 | 98 | //============================================================================== 99 | // This macro generates the main() routine that launches the app. 100 | START_JUCE_APPLICATION (MultiSelectionApplication) 101 | -------------------------------------------------------------------------------- /Source/jcf_multi_component_dragger.h: -------------------------------------------------------------------------------- 1 | /* 2 | ============================================================================== 3 | 4 | jcf_multi_selection.h 5 | Created: 25 Feb 2016 9:00:28am 6 | Author: Jim Credland 7 | 8 | ============================================================================== 9 | */ 10 | 11 | #ifndef JCF_MULTI_SELECTION_H_INCLUDED 12 | #define JCF_MULTI_SELECTION_H_INCLUDED 13 | 14 | 15 | #include "../JuceLibraryCode/JuceHeader.h" 16 | 17 | /** 18 | * MultiComponentDragger allows the user to select objects and drag them around the 19 | * screen. Multiple objects can be selected and dragged at once. The behaviour 20 | * is similar to Microsoft PowerPoint and probably lots of other applications. 21 | * 22 | * Holding down Command(Control) or Shift allows multiple selection. Holding down 23 | * shift can optionally also constrain the objects movement to only the left or 24 | * right axis. 25 | * 26 | * The movement can be constrained to be within the bounds of the parent component. 27 | * 28 | * Objects directly attached to the desktop are not supported. 29 | * 30 | * Using: see handleMouseUp, handleMouseDown and handleMouseDrag 31 | * 32 | * You will probably also want to check isSelected() in your objects paint(Graphics &) 33 | * routine and ensure selected objects are highlighted. Repaints are triggered 34 | * automatically if the selection status changes. 35 | * 36 | * @TODO: Add 'grid' support. 37 | */ 38 | class MultiComponentDragger 39 | { 40 | public: 41 | virtual ~MultiComponentDragger() {} 42 | 43 | void setConstrainBoundsToParent(bool shouldConstrainToParentSize, 44 | BorderSize amountPermittedOffscreen_) 45 | { 46 | constrainToParent = shouldConstrainToParentSize; 47 | amountPermittedOffscreen = amountPermittedOffscreen_; 48 | } 49 | 50 | /** 51 | If this flag is set then the dragging behaviour when shift 52 | is held down will be constrained to the vertical or horizontal 53 | direction. This the the behaviour of Microsoft PowerPoint. 54 | */ 55 | void setShiftConstrainsDirection(bool constrainDirection) 56 | { 57 | shiftConstrainsDirection = constrainDirection; 58 | } 59 | 60 | /** 61 | * Adds a specified component as being selected. 62 | */ 63 | void setSelected(Component * component, bool shouldNowBeSelected) 64 | { 65 | /* Asserts here? This class is only designed to work for components that have a common parent. */ 66 | jassert(selectedComponents.size() == 0 || component->getParentComponent() == selectedComponents[0]->getParentComponent()); 67 | 68 | bool isAlreadySelected = isSelected(component); 69 | 70 | if (! isAlreadySelected && shouldNowBeSelected) 71 | selectedComponents.push_back(component); 72 | 73 | if (isAlreadySelected && ! shouldNowBeSelected) 74 | removeSelectedComponent(component); 75 | } 76 | 77 | /** Toggles the selected status of a particular component. */ 78 | void toggleSelection(Component * component) 79 | { 80 | setSelected(component, ! isSelected(component)); 81 | } 82 | 83 | /** 84 | You should call this when the user clicks on the background of the 85 | parent component. 86 | */ 87 | void deselectAll() 88 | { 89 | for (auto c: selectedComponents) 90 | if (c) 91 | c->repaint(); 92 | 93 | selectedComponents.clear(); 94 | } 95 | 96 | /** 97 | Find out if a component is marked as selected. 98 | */ 99 | bool isSelected(Component * component) const 100 | { 101 | return std::find(selectedComponents.begin(), 102 | selectedComponents.end(), 103 | component) != selectedComponents.end(); 104 | } 105 | 106 | /** 107 | Call this from your components mouseDown event. 108 | */ 109 | void handleMouseDown (Component* component, const MouseEvent & e) 110 | { 111 | jassert (component != nullptr); 112 | 113 | if (! isSelected(component)) 114 | { 115 | if (! (e.mods.isShiftDown() || e.mods.isCommandDown())) 116 | deselectAll(); 117 | 118 | setSelected(component, true); 119 | didJustSelect = true; 120 | } 121 | 122 | if (component != nullptr) 123 | mouseDownWithinTarget = e.getEventRelativeTo (component).getMouseDownPosition(); 124 | 125 | componentBeingDragged = component; 126 | 127 | totalDragDelta = {0, 0}; 128 | 129 | constrainedDirection = noConstraint; 130 | 131 | component->repaint(); 132 | } 133 | 134 | /** 135 | Call this from your components mouseUp event. 136 | */ 137 | void handleMouseUp (Component* component, const MouseEvent & e) 138 | { 139 | if (didStartDragging) 140 | didStartDragging = false; 141 | else 142 | if (!didJustSelect && isSelected(component)) 143 | setSelected(component, false); 144 | 145 | didJustSelect = false; 146 | 147 | component->repaint(); 148 | } 149 | 150 | /** 151 | Call this from your components mouseDrag event. 152 | */ 153 | void handleMouseDrag (const MouseEvent& e) 154 | { 155 | 156 | jassert (e.mods.isAnyMouseButtonDown()); // The event has to be a drag event! 157 | 158 | /** Ensure tiny movements don't start a drag. */ 159 | if (!didStartDragging && e.getDistanceFromDragStart() < minimumMovementToStartDrag) 160 | return; 161 | 162 | didStartDragging = true; 163 | 164 | Point delta = e.getEventRelativeTo (componentBeingDragged).getPosition() - mouseDownWithinTarget; 165 | 166 | if (constrainToParent) 167 | { 168 | auto targetArea = getAreaOfSelectedComponents() + delta; 169 | auto limit = componentBeingDragged->getParentComponent()->getBounds(); 170 | 171 | amountPermittedOffscreen.subtractFrom(targetArea); 172 | 173 | if (targetArea.getX() < 0) 174 | delta.x -= targetArea.getX(); 175 | 176 | if (targetArea.getY() < 0) 177 | delta.y -= targetArea.getY(); 178 | 179 | if (targetArea.getBottom() > limit.getBottom()) 180 | delta.y -= targetArea.getBottom() - limit.getBottom(); 181 | 182 | if (targetArea.getRight() > limit.getRight()) 183 | delta.x -= targetArea.getRight() - limit.getRight(); 184 | } 185 | 186 | applyDirectionConstraints(e, delta); 187 | 188 | for (auto comp: selectedComponents) 189 | { 190 | if (comp != nullptr) 191 | { 192 | Rectangle bounds (comp->getBounds()); 193 | 194 | bounds += delta; 195 | 196 | comp->setBounds (bounds); 197 | } 198 | } 199 | totalDragDelta += delta; 200 | } 201 | 202 | private: 203 | 204 | Rectangle getAreaOfSelectedComponents() 205 | { 206 | if (selectedComponents.size() == 0) 207 | return Rectangle(0, 0, 0, 0); 208 | 209 | Rectangle a = selectedComponents[0]->getBounds(); 210 | 211 | for (auto comp: selectedComponents) 212 | if (comp) 213 | a = a.getUnion(comp->getBounds()); 214 | 215 | return a; 216 | } 217 | 218 | 219 | void applyDirectionConstraints(const MouseEvent &e, Point &delta) 220 | { 221 | if (shiftConstrainsDirection && e.mods.isShiftDown()) 222 | { 223 | /* xy > 0 == movement mainly X direction, xy < 0 == movement mainly Y direction. */ 224 | int xy = abs(totalDragDelta.x + delta.x) - abs(totalDragDelta.y + delta.y); 225 | 226 | /* big movements remove the lock to a particular axis */ 227 | 228 | if (xy > minimumMovementToStartDrag) 229 | constrainedDirection = xAxisOnly; 230 | 231 | if (xy < -minimumMovementToStartDrag) 232 | constrainedDirection = yAxisOnly; 233 | 234 | if ((xy > 0 && constrainedDirection != yAxisOnly) 235 | || 236 | (constrainedDirection == xAxisOnly)) 237 | { 238 | delta.y = -totalDragDelta.y; /* move X direction only. */ 239 | constrainedDirection = xAxisOnly; 240 | } 241 | else if ((xy <= 0 && constrainedDirection != xAxisOnly) 242 | || 243 | constrainedDirection == yAxisOnly) 244 | { 245 | delta.x = -totalDragDelta.x; /* move Y direction only. */ 246 | constrainedDirection = yAxisOnly; 247 | } 248 | else 249 | { 250 | delta = {0, 0}; 251 | } 252 | } 253 | else 254 | { 255 | constrainedDirection = noConstraint; 256 | 257 | } 258 | } 259 | 260 | void removeSelectedComponent(Component * component) 261 | { 262 | selectedComponents.erase(std::remove(selectedComponents.begin(), 263 | selectedComponents.end(), 264 | component), selectedComponents.end()); 265 | } 266 | 267 | enum 268 | { 269 | noConstraint, 270 | xAxisOnly, 271 | yAxisOnly 272 | } constrainedDirection; 273 | 274 | const int minimumMovementToStartDrag = 10; 275 | 276 | bool constrainToParent {true}; 277 | bool shiftConstrainsDirection {false}; 278 | 279 | bool didJustSelect {false}; 280 | bool didStartDragging {false}; 281 | 282 | Point mouseDownWithinTarget; 283 | Point totalDragDelta; 284 | 285 | std::vector> selectedComponents; 286 | Component * componentBeingDragged { nullptr }; 287 | 288 | BorderSize amountPermittedOffscreen; 289 | }; 290 | 291 | 292 | 293 | #endif // JCF_MULTI_SELECTION_H_INCLUDED 294 | --------------------------------------------------------------------------------