/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2017 - ROLI Ltd. JUCE is an open source library subject to commercial or open-source licensing. By using JUCE, you agree to the terms of both the JUCE 5 End-User License Agreement and JUCE 5 Privacy Policy (both updated and effective as of the 27th April 2017). End User License Agreement: www.juce.com/juce-5-licence Privacy Policy: www.juce.com/juce-5-privacy-policy Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ #include "../JuceDemoHeader.h" static void showBubbleMessage (Component* targetComponent, const String& textToShow, ScopedPointer& bmc) { bmc = new BubbleMessageComponent(); if (Desktop::canUseSemiTransparentWindows()) { bmc->setAlwaysOnTop (true); bmc->addToDesktop (0); } else { targetComponent->getTopLevelComponent()->addChildComponent (bmc); } AttributedString text (textToShow); text.setJustification (Justification::centred); text.setColour (targetComponent->findColour (TextButton::textColourOffId)); bmc->showAt (targetComponent, text, 2000, true, false); } //============================================================================== /** To demonstrate how sliders can have custom snapping applied to their values, this simple class snaps the value to 50 if it comes near. */ struct SnappingSlider : public Slider { double snapValue (double attemptedValue, DragMode dragMode) override { if (dragMode == notDragging) return attemptedValue; // if they're entering the value in the text-box, don't mess with it. if (attemptedValue > 40 && attemptedValue < 60) return 50.0; return attemptedValue; } }; /** A TextButton that pops up a colour chooser to change its colours. */ class ColourChangeButton : public TextButton, public ChangeListener { public: ColourChangeButton() : TextButton ("Click to change colour...") { setSize (10, 24); changeWidthToFitText(); } void clicked() override { ColourSelector* colourSelector = new ColourSelector(); colourSelector->setName ("background"); colourSelector->setCurrentColour (findColour (TextButton::buttonColourId)); colourSelector->addChangeListener (this); colourSelector->setColour (ColourSelector::backgroundColourId, Colours::transparentBlack); colourSelector->setSize (300, 400); CallOutBox::launchAsynchronously (colourSelector, getScreenBounds(), nullptr); } void changeListenerCallback (ChangeBroadcaster* source) override { if (ColourSelector* cs = dynamic_cast (source)) setColour (TextButton::buttonColourId, cs->getCurrentColour()); } }; //============================================================================== struct SlidersPage : public Component { SlidersPage() : hintLabel ("hint", "Try right-clicking on a slider for an options menu. \n\n" "Also, holding down CTRL while dragging will turn on a slider's velocity-sensitive mode") { Slider* s = createSlider (false); s->setSliderStyle (Slider::LinearVertical); s->setTextBoxStyle (Slider::TextBoxBelow, false, 100, 20); s->setBounds (10, 25, 70, 200); s->setDoubleClickReturnValue (true, 50.0); // double-clicking this slider will set it to 50.0 s->setTextValueSuffix (" units"); s = createSlider (false); s->setSliderStyle (Slider::LinearVertical); s->setVelocityBasedMode (true); s->setSkewFactor (0.5); s->setTextBoxStyle (Slider::TextBoxAbove, true, 100, 20); s->setBounds (85, 25, 70, 200); s->setTextValueSuffix (" rels"); s = createSlider (true); s->setSliderStyle (Slider::LinearHorizontal); s->setTextBoxStyle (Slider::TextBoxLeft, false, 80, 20); s->setBounds (180, 35, 150, 20); s = createSlider (false); s->setSliderStyle (Slider::LinearHorizontal); s->setTextBoxStyle (Slider::NoTextBox, false, 0, 0); s->setBounds (180, 65, 150, 20); s->setPopupDisplayEnabled (true, this); s->setTextValueSuffix (" nuns required to change a lightbulb"); s = createSlider (false); s->setSliderStyle (Slider::IncDecButtons); s->setTextBoxStyle (Slider::TextBoxLeft, false, 50, 20); s->setBounds (180, 105, 100, 20); s->setIncDecButtonsMode (Slider::incDecButtonsDraggable_Vertical); s = createSlider (false); s->setSliderStyle (Slider::Rotary); s->setRotaryParameters (float_Pi * 1.2f, float_Pi * 2.8f, false); s->setTextBoxStyle (Slider::TextBoxRight, false, 70, 20); s->setBounds (190, 145, 120, 40); s->setTextValueSuffix (" mm"); s = createSlider (false); s->setSliderStyle (Slider::LinearBar); s->setBounds (180, 195, 100, 30); s->setTextValueSuffix (" gallons"); s = createSlider (false); s->setSliderStyle (Slider::TwoValueHorizontal); s->setBounds (360, 20, 160, 40); s = createSlider (false); s->setSliderStyle (Slider::TwoValueVertical); s->setBounds (360, 110, 40, 160); s = createSlider (false); s->setSliderStyle (Slider::ThreeValueHorizontal); s->setBounds (360, 70, 160, 40); s = createSlider (false); s->setSliderStyle (Slider::ThreeValueVertical); s->setBounds (440, 110, 40, 160); s = createSlider (false); s->setSliderStyle (Slider::LinearBarVertical); s->setTextBoxStyle (Slider::NoTextBox, false, 0, 0); s->setBounds (540, 35, 20, 230); s->setPopupDisplayEnabled (true, this); s->setTextValueSuffix (" mickles in a muckle"); for (int i = 7; i <= 10; ++i) { sliders.getUnchecked(i)->setTextBoxStyle (Slider::NoTextBox, false, 0, 0); sliders.getUnchecked(i)->setPopupDisplayEnabled (true, this); } /* Here, we'll create a Value object, and tell a bunch of our sliders to use it as their value source. By telling them all to share the same Value, they'll stay in sync with each other. We could also optionally keep a copy of this Value elsewhere, and by changing it, cause all the sliders to automatically update. */ Value sharedValue; sharedValue = Random::getSystemRandom().nextDouble() * 100; for (int i = 0; i < 7; ++i) sliders.getUnchecked(i)->getValueObject().referTo (sharedValue); // ..and now we'll do the same for all our min/max slider values.. Value sharedValueMin, sharedValueMax; sharedValueMin = Random::getSystemRandom().nextDouble() * 40.0; sharedValueMax = Random::getSystemRandom().nextDouble() * 40.0 + 60.0; for (int i = 7; i <= 10; ++i) { sliders.getUnchecked(i)->getMaxValueObject().referTo (sharedValueMax); sliders.getUnchecked(i)->getMinValueObject().referTo (sharedValueMin); } hintLabel.setBounds (20, 245, 350, 150); addAndMakeVisible (hintLabel); } private: OwnedArray sliders; Label hintLabel; Slider* createSlider (bool isSnapping) { Slider* s = isSnapping ? new SnappingSlider() : new Slider(); sliders.add (s); addAndMakeVisible (s); s->setRange (0.0, 100.0, 0.1); s->setPopupMenuEnabled (true); s->setValue (Random::getSystemRandom().nextDouble() * 100.0, dontSendNotification); return s; } JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SlidersPage) }; //============================================================================== struct ButtonsPage : public Component, public ButtonListener { ButtonsPage() { { GroupComponent* group = addToList (new GroupComponent ("group", "Radio buttons")); group->setBounds (20, 20, 220, 140); } for (int i = 0; i < 4; ++i) { ToggleButton* tb = addToList (new ToggleButton ("Radio Button #" + String (i + 1))); tb->setRadioGroupId (1234); tb->setBounds (45, 46 + i * 22, 180, 22); tb->setTooltip ("A set of mutually-exclusive radio buttons"); if (i == 0) tb->setToggleState (true, dontSendNotification); } for (int i = 0; i < 4; ++i) { DrawablePath normal, over; Path p; p.addStar (Point(), i + 5, 20.0f, 50.0f, -0.2f); normal.setPath (p); normal.setFill (Colours::lightblue); normal.setStrokeFill (Colours::black); normal.setStrokeThickness (4.0f); over.setPath (p); over.setFill (Colours::blue); over.setStrokeFill (Colours::black); over.setStrokeThickness (4.0f); DrawableButton* db = addToList (new DrawableButton (String (i + 5) + " points", DrawableButton::ImageAboveTextLabel)); db->setImages (&normal, &over, 0); db->setClickingTogglesState (true); db->setRadioGroupId (23456); const int buttonSize = 50; db->setBounds (25 + i * buttonSize, 180, buttonSize, buttonSize); if (i == 0) db->setToggleState (true, dontSendNotification); } for (int i = 0; i < 4; ++i) { TextButton* tb = addToList (new TextButton ("Button " + String (i + 1))); tb->setClickingTogglesState (true); tb->setRadioGroupId (34567); tb->setColour (TextButton::textColourOffId, Colours::black); tb->setColour (TextButton::textColourOnId, Colours::black); tb->setColour (TextButton::buttonColourId, Colours::white); tb->setColour (TextButton::buttonOnColourId, Colours::blueviolet.brighter()); tb->setBounds (20 + i * 55, 260, 55, 24); tb->setConnectedEdges (((i != 0) ? Button::ConnectedOnLeft : 0) | ((i != 3) ? Button::ConnectedOnRight : 0)); if (i == 0) tb->setToggleState (true, dontSendNotification); } { ColourChangeButton* colourChangeButton = new ColourChangeButton(); components.add (colourChangeButton); addAndMakeVisible (colourChangeButton); colourChangeButton->setTopLeftPosition (20, 320); } { HyperlinkButton* hyperlink = addToList (new HyperlinkButton ("This is a HyperlinkButton", URL ("http://www.juce.com"))); hyperlink->setBounds (260, 20, 200, 24); } // create some drawables to use for our drawable buttons... DrawablePath normal, over; { Path p; p.addStar (Point(), 5, 20.0f, 50.0f, 0.2f); normal.setPath (p); normal.setFill (getRandomDarkColour()); } { Path p; p.addStar (Point(), 9, 25.0f, 50.0f, 0.0f); over.setPath (p); over.setFill (getRandomBrightColour()); over.setStrokeFill (getRandomDarkColour()); over.setStrokeThickness (5.0f); } DrawableImage down; down.setImage (ImageCache::getFromMemory (BinaryData::juce_icon_png, BinaryData::juce_icon_pngSize)); down.setOverlayColour (Colours::black.withAlpha (0.3f)); { // create an image-above-text button from these drawables.. DrawableButton* db = addToList (new DrawableButton ("Button 1", DrawableButton::ImageAboveTextLabel)); db->setImages (&normal, &over, &down); db->setBounds (260, 60, 80, 80); db->setTooltip ("This is a DrawableButton with a label"); db->addListener (this); } { // create an image-only button from these drawables.. DrawableButton* db = addToList (new DrawableButton ("Button 2", DrawableButton::ImageFitted)); db->setImages (&normal, &over, &down); db->setClickingTogglesState (true); db->setBounds (370, 60, 80, 80); db->setTooltip ("This is an image-only DrawableButton"); db->addListener (this); } { // create an image-on-button-shape button from the same drawables.. DrawableButton* db = addToList (new DrawableButton ("Button 3", DrawableButton::ImageOnButtonBackground)); db->setImages (&normal, 0, 0); db->setBounds (260, 160, 110, 25); db->setTooltip ("This is a DrawableButton on a standard button background"); db->addListener (this); } { DrawableButton* db = addToList (new DrawableButton ("Button 4", DrawableButton::ImageOnButtonBackground)); db->setImages (&normal, &over, &down); db->setClickingTogglesState (true); db->setColour (DrawableButton::backgroundColourId, Colours::white); db->setColour (DrawableButton::backgroundOnColourId, Colours::yellow); db->setBounds (400, 150, 50, 50); db->setTooltip ("This is a DrawableButton on a standard button background"); db->addListener (this); } { ShapeButton* sb = addToList (new ShapeButton ("ShapeButton", getRandomDarkColour(), getRandomDarkColour(), getRandomDarkColour())); sb->setShape (MainAppWindow::getJUCELogoPath(), false, true, false); sb->setBounds (260, 220, 200, 120); } { ImageButton* ib = addToList (new ImageButton ("ImageButton")); Image juceImage = ImageCache::getFromMemory (BinaryData::juce_icon_png, BinaryData::juce_icon_pngSize); ib->setImages (true, true, true, juceImage, 0.7f, Colours::transparentBlack, juceImage, 1.0f, Colours::transparentBlack, juceImage, 1.0f, getRandomBrightColour().withAlpha (0.8f), 0.5f); ib->setBounds (260, 350, 100, 100); ib->setTooltip ("ImageButton - showing alpha-channel hit-testing and colour overlay when clicked"); } } private: OwnedArray components; ScopedPointer bubbleMessage; // This little function avoids a bit of code-duplication by adding a component to // our list as well as calling addAndMakeVisible on it.. template ComponentType* addToList (ComponentType* newComp) { components.add (newComp); addAndMakeVisible (newComp); return newComp; } void buttonClicked (Button* button) override { showBubbleMessage (button, "This is a demo of the BubbleMessageComponent, which lets you pop up a message pointing " "at a component or somewhere on the screen.\n\n" "The message bubbles will disappear after a timeout period, or when the mouse is clicked.", bubbleMessage); } JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ButtonsPage) }; //============================================================================== struct MiscPage : public Component { MiscPage() : textEditor2 ("Password", (juce_wchar) 0x2022), comboBox ("Combo") { addAndMakeVisible (textEditor1); textEditor1.setBounds (10, 25, 200, 24); textEditor1.setText ("Single-line text box"); addAndMakeVisible (textEditor2); textEditor2.setBounds (10, 55, 200, 24); textEditor2.setText ("Password"); addAndMakeVisible (comboBox); comboBox.setBounds (10, 85, 200, 24); comboBox.setEditableText (true); comboBox.setJustificationType (Justification::centred); for (int i = 1; i < 100; ++i) comboBox.addItem ("combo box item " + String (i), i); comboBox.setSelectedId (1); } TextEditor textEditor1, textEditor2; ComboBox comboBox; }; //============================================================================== class ToolbarDemoComp : public Component, public SliderListener, public ButtonListener { public: ToolbarDemoComp() : depthLabel (String(), "Toolbar depth:"), infoLabel (String(), "As well as showing off toolbars, this demo illustrates how to store " "a set of SVG files in a Zip file, embed that in your application, and read " "them back in at runtime.\n\nThe icon images here are taken from the open-source " "Tango icon project."), orientationButton ("Vertical/Horizontal"), customiseButton ("Customise...") { // Create and add the toolbar... addAndMakeVisible (toolbar); // And use our item factory to add a set of default icons to it... toolbar.addDefaultItems (factory); // Now we'll just create the other sliders and buttons on the demo page, which adjust // the toolbar's properties... addAndMakeVisible (infoLabel); infoLabel.setJustificationType (Justification::topLeft); infoLabel.setBounds (80, 80, 450, 100); infoLabel.setInterceptsMouseClicks (false, false); addAndMakeVisible (depthSlider); depthSlider.setRange (10.0, 200.0, 1.0); depthSlider.setValue (50, dontSendNotification); depthSlider.setSliderStyle (Slider::LinearHorizontal); depthSlider.setTextBoxStyle (Slider::TextBoxLeft, false, 80, 20); depthSlider.addListener (this); depthSlider.setBounds (80, 210, 300, 22); depthLabel.attachToComponent (&depthSlider, false); addAndMakeVisible (orientationButton); orientationButton.addListener (this); orientationButton.changeWidthToFitText (22); orientationButton.setTopLeftPosition (depthSlider.getX(), depthSlider.getBottom() + 20); addAndMakeVisible (customiseButton); customiseButton.addListener (this); customiseButton.changeWidthToFitText (22); customiseButton.setTopLeftPosition (orientationButton.getRight() + 20, orientationButton.getY()); } void resized() override { int toolbarThickness = (int) depthSlider.getValue(); if (toolbar.isVertical()) toolbar.setBounds (getLocalBounds().removeFromLeft (toolbarThickness)); else toolbar.setBounds (getLocalBounds().removeFromTop (toolbarThickness)); } void sliderValueChanged (Slider*) override { resized(); } void buttonClicked (Button* button) override { if (button == &orientationButton) { toolbar.setVertical (! toolbar.isVertical()); resized(); } else if (button == &customiseButton) { toolbar.showCustomisationDialog (factory); } } private: Toolbar toolbar; Slider depthSlider; Label depthLabel, infoLabel; TextButton orientationButton, customiseButton; //============================================================================== class DemoToolbarItemFactory : public ToolbarItemFactory { public: DemoToolbarItemFactory() {} //============================================================================== // Each type of item a toolbar can contain must be given a unique ID. These // are the ones we'll use in this demo. enum DemoToolbarItemIds { doc_new = 1, doc_open = 2, doc_save = 3, doc_saveAs = 4, edit_copy = 5, edit_cut = 6, edit_paste = 7, juceLogoButton = 8, customComboBox = 9 }; void getAllToolbarItemIds (Array& ids) override { // This returns the complete list of all item IDs that are allowed to // go in our toolbar. Any items you might want to add must be listed here. The // order in which they are listed will be used by the toolbar customisation panel. ids.add (doc_new); ids.add (doc_open); ids.add (doc_save); ids.add (doc_saveAs); ids.add (edit_copy); ids.add (edit_cut); ids.add (edit_paste); ids.add (juceLogoButton); ids.add (customComboBox); // If you're going to use separators, then they must also be added explicitly // to the list. ids.add (separatorBarId); ids.add (spacerId); ids.add (flexibleSpacerId); } void getDefaultItemSet (Array& ids) override { // This returns an ordered list of the set of items that make up a // toolbar's default set. Not all items need to be on this list, and // items can appear multiple times (e.g. the separators used here). ids.add (doc_new); ids.add (doc_open); ids.add (doc_save); ids.add (doc_saveAs); ids.add (spacerId); ids.add (separatorBarId); ids.add (edit_copy); ids.add (edit_cut); ids.add (edit_paste); ids.add (separatorBarId); ids.add (flexibleSpacerId); ids.add (customComboBox); ids.add (flexibleSpacerId); ids.add (separatorBarId); ids.add (juceLogoButton); } ToolbarItemComponent* createItem (int itemId) override { switch (itemId) { case doc_new: return createButtonFromZipFileSVG (itemId, "new", "document-new.svg"); case doc_open: return createButtonFromZipFileSVG (itemId, "open", "document-open.svg"); case doc_save: return createButtonFromZipFileSVG (itemId, "save", "document-save.svg"); case doc_saveAs: return createButtonFromZipFileSVG (itemId, "save as", "document-save-as.svg"); case edit_copy: return createButtonFromZipFileSVG (itemId, "copy", "edit-copy.svg"); case edit_cut: return createButtonFromZipFileSVG (itemId, "cut", "edit-cut.svg"); case edit_paste: return createButtonFromZipFileSVG (itemId, "paste", "edit-paste.svg"); case juceLogoButton: return new ToolbarButton (itemId, "juce!", Drawable::createFromImageData (BinaryData::juce_icon_png, BinaryData::juce_icon_pngSize), 0); case customComboBox: return new CustomToolbarComboBox (itemId); default: break; } return nullptr; } private: StringArray iconNames; OwnedArray iconsFromZipFile; // This is a little utility to create a button with one of the SVG images in // our embedded ZIP file "icons.zip" ToolbarButton* createButtonFromZipFileSVG (const int itemId, const String& text, const String& filename) { if (iconsFromZipFile.size() == 0) { // If we've not already done so, load all the images from the zip file.. MemoryInputStream iconsFileStream (BinaryData::icons_zip, BinaryData::icons_zipSize, false); ZipFile icons (&iconsFileStream, false); for (int i = 0; i < icons.getNumEntries(); ++i) { ScopedPointer svgFileStream (icons.createStreamForEntry (i)); if (svgFileStream != nullptr) { iconNames.add (icons.getEntry(i)->filename); iconsFromZipFile.add (Drawable::createFromImageDataStream (*svgFileStream)); } } } Drawable* image = iconsFromZipFile [iconNames.indexOf (filename)]->createCopy(); return new ToolbarButton (itemId, text, image, 0); } // Demonstrates how to put a custom component into a toolbar - this one contains // a ComboBox. class CustomToolbarComboBox : public ToolbarItemComponent { public: CustomToolbarComboBox (const int toolbarItemId) : ToolbarItemComponent (toolbarItemId, "Custom Toolbar Item", false), comboBox ("demo toolbar combo box") { addAndMakeVisible (comboBox); for (int i = 1; i < 20; ++i) comboBox.addItem ("Toolbar ComboBox item " + String (i), i); comboBox.setSelectedId (1); comboBox.setEditableText (true); } bool getToolbarItemSizes (int /*toolbarDepth*/, bool isVertical, int& preferredSize, int& minSize, int& maxSize) override { if (isVertical) return false; preferredSize = 250; minSize = 80; maxSize = 300; return true; } void paintButtonArea (Graphics&, int, int, bool, bool) override { } void contentAreaChanged (const Rectangle& newArea) override { comboBox.setSize (newArea.getWidth() - 2, jmin (newArea.getHeight() - 2, 22)); comboBox.setCentrePosition (newArea.getCentreX(), newArea.getCentreY()); } private: ComboBox comboBox; }; }; DemoToolbarItemFactory factory; }; //============================================================================== /** This class shows how to implement a TableListBoxModel to show in a TableListBox. */ class TableDemoComponent : public Component, public TableListBoxModel { public: TableDemoComponent() : font (14.0f) { // Load some data from an embedded XML file.. loadData(); // Create our table component and add it to this component.. addAndMakeVisible (table); table.setModel (this); // give it a border table.setColour (ListBox::outlineColourId, Colours::grey); table.setOutlineThickness (1); // Add some columns to the table header, based on the column list in our database.. forEachXmlChildElement (*columnList, columnXml) { table.getHeader().addColumn (columnXml->getStringAttribute ("name"), columnXml->getIntAttribute ("columnId"), columnXml->getIntAttribute ("width"), 50, 400, TableHeaderComponent::defaultFlags); } // we could now change some initial settings.. table.getHeader().setSortColumnId (1, true); // sort forwards by the ID column table.getHeader().setColumnVisible (7, false); // hide the "length" column until the user shows it // un-comment this line to have a go of stretch-to-fit mode // table.getHeader().setStretchToFitActive (true); table.setMultipleSelectionEnabled (true); } // This is overloaded from TableListBoxModel, and must return the total number of rows in our table int getNumRows() override { return numRows; } // This is overloaded from TableListBoxModel, and should fill in the background of the whole row void paintRowBackground (Graphics& g, int rowNumber, int /*width*/, int /*height*/, bool rowIsSelected) override { const Colour alternateColour (getLookAndFeel().findColour (ListBox::backgroundColourId) .interpolatedWith (getLookAndFeel().findColour (ListBox::textColourId), 0.03f)); if (rowIsSelected) g.fillAll (Colours::lightblue); else if (rowNumber % 2) g.fillAll (alternateColour); } // This is overloaded from TableListBoxModel, and must paint any cells that aren't using custom // components. void paintCell (Graphics& g, int rowNumber, int columnId, int width, int height, bool /*rowIsSelected*/) override { g.setColour (getLookAndFeel().findColour (ListBox::textColourId)); g.setFont (font); if (const XmlElement* rowElement = dataList->getChildElement (rowNumber)) { const String text (rowElement->getStringAttribute (getAttributeNameForColumnId (columnId))); g.drawText (text, 2, 0, width - 4, height, Justification::centredLeft, true); } g.setColour (getLookAndFeel().findColour (ListBox::backgroundColourId)); g.fillRect (width - 1, 0, 1, height); } // This is overloaded from TableListBoxModel, and tells us that the user has clicked a table header // to change the sort order. void sortOrderChanged (int newSortColumnId, bool isForwards) override { if (newSortColumnId != 0) { DemoDataSorter sorter (getAttributeNameForColumnId (newSortColumnId), isForwards); dataList->sortChildElements (sorter); table.updateContent(); } } // This is overloaded from TableListBoxModel, and must update any custom components that we're using Component* refreshComponentForCell (int rowNumber, int columnId, bool /*isRowSelected*/, Component* existingComponentToUpdate) override { if (columnId == 1 || columnId == 7) // The ID and Length columns do not have a custom component { jassert (existingComponentToUpdate == nullptr); return nullptr; } if (columnId == 5) // For the ratings column, we return the custom combobox component { RatingColumnCustomComponent* ratingsBox = static_cast (existingComponentToUpdate); // If an existing component is being passed-in for updating, we'll re-use it, but // if not, we'll have to create one. if (ratingsBox == nullptr) ratingsBox = new RatingColumnCustomComponent (*this); ratingsBox->setRowAndColumn (rowNumber, columnId); return ratingsBox; } // The other columns are editable text columns, for which we use the custom Label component EditableTextCustomComponent* textLabel = static_cast (existingComponentToUpdate); // same as above... if (textLabel == nullptr) textLabel = new EditableTextCustomComponent (*this); textLabel->setRowAndColumn (rowNumber, columnId); return textLabel; } // This is overloaded from TableListBoxModel, and should choose the best width for the specified // column. int getColumnAutoSizeWidth (int columnId) override { if (columnId == 5) return 100; // (this is the ratings column, containing a custom combobox component) int widest = 32; // find the widest bit of text in this column.. for (int i = getNumRows(); --i >= 0;) { if (const XmlElement* rowElement = dataList->getChildElement (i)) { const String text (rowElement->getStringAttribute (getAttributeNameForColumnId (columnId))); widest = jmax (widest, font.getStringWidth (text)); } } return widest + 8; } // A couple of quick methods to set and get cell values when the user changes them int getRating (const int rowNumber) const { return dataList->getChildElement (rowNumber)->getIntAttribute ("Rating"); } void setRating (const int rowNumber, const int newRating) { dataList->getChildElement (rowNumber)->setAttribute ("Rating", newRating); } String getText (const int columnNumber, const int rowNumber) const { return dataList->getChildElement (rowNumber)->getStringAttribute ( getAttributeNameForColumnId(columnNumber)); } void setText (const int columnNumber, const int rowNumber, const String& newText) { const String& columnName = table.getHeader().getColumnName (columnNumber); dataList->getChildElement (rowNumber)->setAttribute (columnName, newText); } //============================================================================== void resized() override { // position our table with a gap around its edge table.setBoundsInset (BorderSize (8)); } private: TableListBox table; // the table component itself Font font; ScopedPointer demoData; // This is the XML document loaded from the embedded file "demo table data.xml" XmlElement* columnList; // A pointer to the sub-node of demoData that contains the list of columns XmlElement* dataList; // A pointer to the sub-node of demoData that contains the list of data rows int numRows; // The number of rows of data we've got //============================================================================== // This is a custom Label component, which we use for the table's editable text columns. class EditableTextCustomComponent : public Label { public: EditableTextCustomComponent (TableDemoComponent& td) : owner (td) { // double click to edit the label text; single click handled below setEditable (false, true, false); } void mouseDown (const MouseEvent& event) override { // single click on the label should simply select the row owner.table.selectRowsBasedOnModifierKeys (row, event.mods, false); Label::mouseDown (event); } void textWasEdited() override { owner.setText (columnId, row, getText()); } // Our demo code will call this when we may need to update our contents void setRowAndColumn (const int newRow, const int newColumn) { row = newRow; columnId = newColumn; setText (owner.getText(columnId, row), dontSendNotification); } void paint (Graphics& g) override { auto& lf = getLookAndFeel(); if (! dynamic_cast (&lf)) lf.setColour (textColourId, Colours::black); Label::paint (g); } private: TableDemoComponent& owner; int row, columnId; Colour textColour; }; //============================================================================== // This is a custom component containing a combo box, which we're going to put inside // our table's "rating" column. class RatingColumnCustomComponent : public Component, private ComboBoxListener { public: RatingColumnCustomComponent (TableDemoComponent& td) : owner (td) { // just put a combo box inside this component addAndMakeVisible (comboBox); comboBox.addItem ("fab", 1); comboBox.addItem ("groovy", 2); comboBox.addItem ("hep", 3); comboBox.addItem ("mad for it", 4); comboBox.addItem ("neat", 5); comboBox.addItem ("swingin", 6); comboBox.addItem ("wild", 7); // when the combo is changed, we'll get a callback. comboBox.addListener (this); comboBox.setWantsKeyboardFocus (false); } void resized() override { comboBox.setBoundsInset (BorderSize (2)); } // Our demo code will call this when we may need to update our contents void setRowAndColumn (int newRow, int newColumn) { row = newRow; columnId = newColumn; comboBox.setSelectedId (owner.getRating (row), dontSendNotification); } void comboBoxChanged (ComboBox*) override { owner.setRating (row, comboBox.getSelectedId()); } private: TableDemoComponent& owner; ComboBox comboBox; int row, columnId; }; //============================================================================== // A comparator used to sort our data when the user clicks a column header class DemoDataSorter { public: DemoDataSorter (const String& attributeToSortBy, bool forwards) : attributeToSort (attributeToSortBy), direction (forwards ? 1 : -1) { } int compareElements (XmlElement* first, XmlElement* second) const { int result = first->getStringAttribute (attributeToSort) .compareNatural (second->getStringAttribute (attributeToSort)); if (result == 0) result = first->getStringAttribute ("ID") .compareNatural (second->getStringAttribute ("ID")); return direction * result; } private: String attributeToSort; int direction; }; //============================================================================== // this loads the embedded database XML file into memory void loadData() { demoData = XmlDocument::parse (BinaryData::demo_table_data_xml); dataList = demoData->getChildByName ("DATA"); columnList = demoData->getChildByName ("COLUMNS"); numRows = dataList->getNumChildElements(); } // (a utility method to search our XML for the attribute that matches a column ID) String getAttributeNameForColumnId (const int columnId) const { forEachXmlChildElement (*columnList, columnXml) { if (columnXml->getIntAttribute ("columnId") == columnId) return columnXml->getStringAttribute ("name"); } return String(); } JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TableDemoComponent) }; //============================================================================== class DragAndDropDemo : public Component, public DragAndDropContainer { public: DragAndDropDemo() : sourceListBox ("D+D source", nullptr) { setName ("Drag-and-Drop"); sourceListBox.setModel (&sourceModel); sourceListBox.setMultipleSelectionEnabled (true); addAndMakeVisible (sourceListBox); addAndMakeVisible (target); } void resized() override { Rectangle r (getLocalBounds().reduced (8)); sourceListBox.setBounds (r.withSize (250, 180)); target.setBounds (r.removeFromBottom (150).removeFromRight (250)); } private: //============================================================================== struct SourceItemListboxContents : public ListBoxModel { // The following methods implement the necessary virtual functions from ListBoxModel, // telling the listbox how many rows there are, painting them, etc. int getNumRows() override { return 30; } void paintListBoxItem (int rowNumber, Graphics& g, int width, int height, bool rowIsSelected) override { if (rowIsSelected) g.fillAll (Colours::lightblue); g.setColour (LookAndFeel::getDefaultLookAndFeel().findColour (Label::textColourId)); g.setFont (height * 0.7f); g.drawText ("Draggable Thing #" + String (rowNumber + 1), 5, 0, width, height, Justification::centredLeft, true); } var getDragSourceDescription (const SparseSet& selectedRows) override { // for our drag description, we'll just make a comma-separated list of the selected row // numbers - this will be picked up by the drag target and displayed in its box. StringArray rows; for (int i = 0; i < selectedRows.size(); ++i) rows.add (String (selectedRows[i] + 1)); return rows.joinIntoString (", "); } }; //============================================================================== // and this is a component that can have things dropped onto it.. class DragAndDropDemoTarget : public Component, public DragAndDropTarget, public FileDragAndDropTarget, public TextDragAndDropTarget { public: DragAndDropDemoTarget() : message ("Drag-and-drop some rows from the top-left box onto this component!\n\n" "You can also drag-and-drop files and text from other apps"), somethingIsBeingDraggedOver (false) { } void paint (Graphics& g) override { g.fillAll (Colours::green.withAlpha (0.2f)); // draw a red line around the comp if the user's currently dragging something over it.. if (somethingIsBeingDraggedOver) { g.setColour (Colours::red); g.drawRect (getLocalBounds(), 3); } g.setColour (getLookAndFeel().findColour (Label::textColourId)); g.setFont (14.0f); g.drawFittedText (message, getLocalBounds().reduced (10, 0), Justification::centred, 4); } //============================================================================== // These methods implement the DragAndDropTarget interface, and allow our component // to accept drag-and-drop of objects from other Juce components.. bool isInterestedInDragSource (const SourceDetails& /*dragSourceDetails*/) override { // normally you'd check the sourceDescription value to see if it's the // sort of object that you're interested in before returning true, but for // the demo, we'll say yes to anything.. return true; } void itemDragEnter (const SourceDetails& /*dragSourceDetails*/) override { somethingIsBeingDraggedOver = true; repaint(); } void itemDragMove (const SourceDetails& /*dragSourceDetails*/) override { } void itemDragExit (const SourceDetails& /*dragSourceDetails*/) override { somethingIsBeingDraggedOver = false; repaint(); } void itemDropped (const SourceDetails& dragSourceDetails) override { message = "Items dropped: " + dragSourceDetails.description.toString(); somethingIsBeingDraggedOver = false; repaint(); } //============================================================================== // These methods implement the FileDragAndDropTarget interface, and allow our component // to accept drag-and-drop of files.. bool isInterestedInFileDrag (const StringArray& /*files*/) override { // normally you'd check these files to see if they're something that you're // interested in before returning true, but for the demo, we'll say yes to anything.. return true; } void fileDragEnter (const StringArray& /*files*/, int /*x*/, int /*y*/) override { somethingIsBeingDraggedOver = true; repaint(); } void fileDragMove (const StringArray& /*files*/, int /*x*/, int /*y*/) override { } void fileDragExit (const StringArray& /*files*/) override { somethingIsBeingDraggedOver = false; repaint(); } void filesDropped (const StringArray& files, int /*x*/, int /*y*/) override { message = "Files dropped: " + files.joinIntoString ("\n"); somethingIsBeingDraggedOver = false; repaint(); } //============================================================================== // These methods implement the TextDragAndDropTarget interface, and allow our component // to accept drag-and-drop of text.. bool isInterestedInTextDrag (const String& /*text*/) override { return true; } void textDragEnter (const String& /*text*/, int /*x*/, int /*y*/) override { somethingIsBeingDraggedOver = true; repaint(); } void textDragMove (const String& /*text*/, int /*x*/, int /*y*/) override { } void textDragExit (const String& /*text*/) override { somethingIsBeingDraggedOver = false; repaint(); } void textDropped (const String& text, int /*x*/, int /*y*/) override { message = "Text dropped:\n" + text; somethingIsBeingDraggedOver = false; repaint(); } private: String message; bool somethingIsBeingDraggedOver; }; //============================================================================== ListBox sourceListBox; SourceItemListboxContents sourceModel; DragAndDropDemoTarget target; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DragAndDropDemo) }; //============================================================================== class MenusDemo : public Component, public MenuBarModel, public ChangeBroadcaster, private Button::Listener { public: MenusDemo() { addAndMakeVisible (menuBar = new MenuBarComponent (this)); popupButton.setButtonText ("Show Popup Menu"); popupButton.setTriggeredOnMouseDown (true); popupButton.addListener (this); addAndMakeVisible (popupButton); setApplicationCommandManagerToWatch (&MainAppWindow::getApplicationCommandManager()); } ~MenusDemo() { #if JUCE_MAC MenuBarModel::setMacMainMenu (nullptr); #endif PopupMenu::dismissAllActiveMenus(); popupButton.removeListener (this); } void resized() override { Rectangle area (getLocalBounds()); menuBar->setBounds (area.removeFromTop (LookAndFeel::getDefaultLookAndFeel().getDefaultMenuBarHeight())); area.removeFromTop (20); area = area.removeFromTop (33); popupButton.setBounds (area.removeFromLeft (200).reduced (5)); } //============================================================================== StringArray getMenuBarNames() override { const char* const names[] = { "Demo", "Look-and-feel", "Tabs", "Misc", nullptr }; return StringArray (names); } PopupMenu getMenuForIndex (int menuIndex, const String& /*menuName*/) override { ApplicationCommandManager* commandManager = &MainAppWindow::getApplicationCommandManager(); PopupMenu menu; if (menuIndex == 0) { menu.addCommandItem (commandManager, MainAppWindow::showPreviousDemo); menu.addCommandItem (commandManager, MainAppWindow::showNextDemo); menu.addSeparator(); menu.addCommandItem (commandManager, StandardApplicationCommandIDs::quit); } else if (menuIndex == 1) { menu.addCommandItem (commandManager, MainAppWindow::useLookAndFeelV1); menu.addCommandItem (commandManager, MainAppWindow::useLookAndFeelV2); menu.addCommandItem (commandManager, MainAppWindow::useLookAndFeelV3); PopupMenu v4SubMenu; v4SubMenu.addCommandItem (commandManager, MainAppWindow::useLookAndFeelV4Dark); v4SubMenu.addCommandItem (commandManager, MainAppWindow::useLookAndFeelV4Midnight); v4SubMenu.addCommandItem (commandManager, MainAppWindow::useLookAndFeelV4Grey); v4SubMenu.addCommandItem (commandManager, MainAppWindow::useLookAndFeelV4Light); menu.addSubMenu ("Use LookAndFeel_V4", v4SubMenu); menu.addSeparator(); menu.addCommandItem (commandManager, MainAppWindow::useNativeTitleBar); #if JUCE_MAC menu.addItem (6000, "Use Native Menu Bar"); #endif #if ! JUCE_LINUX menu.addCommandItem (commandManager, MainAppWindow::goToKioskMode); #endif if (MainAppWindow* mainWindow = MainAppWindow::getMainAppWindow()) { StringArray engines (mainWindow->getRenderingEngines()); if (engines.size() > 1) { menu.addSeparator(); for (int i = 0; i < engines.size(); ++i) menu.addCommandItem (commandManager, MainAppWindow::renderingEngineOne + i); } } } else if (menuIndex == 2) { if (TabbedComponent* tabs = findParentComponentOfClass()) { menu.addItem (3000, "Tabs at Top", true, tabs->getOrientation() == TabbedButtonBar::TabsAtTop); menu.addItem (3001, "Tabs at Bottom", true, tabs->getOrientation() == TabbedButtonBar::TabsAtBottom); menu.addItem (3002, "Tabs on Left", true, tabs->getOrientation() == TabbedButtonBar::TabsAtLeft); menu.addItem (3003, "Tabs on Right", true, tabs->getOrientation() == TabbedButtonBar::TabsAtRight); } } else if (menuIndex == 3) { return getDummyPopupMenu(); } return menu; } void menuItemSelected (int menuItemID, int /*topLevelMenuIndex*/) override { // most of our menu items are invoked automatically as commands, but we can handle the // other special cases here.. if (menuItemID == 6000) { #if JUCE_MAC if (MenuBarModel::getMacMainMenu() != nullptr) { MenuBarModel::setMacMainMenu (nullptr); menuBar->setModel (this); } else { menuBar->setModel (nullptr); MenuBarModel::setMacMainMenu (this); } #endif } else if (menuItemID >= 3000 && menuItemID <= 3003) { if (TabbedComponent* tabs = findParentComponentOfClass()) { TabbedButtonBar::Orientation o = TabbedButtonBar::TabsAtTop; if (menuItemID == 3001) o = TabbedButtonBar::TabsAtBottom; if (menuItemID == 3002) o = TabbedButtonBar::TabsAtLeft; if (menuItemID == 3003) o = TabbedButtonBar::TabsAtRight; tabs->setOrientation (o); } } else if (menuItemID >= 12298 && menuItemID <= 12305) { sendChangeMessage(); } } private: TextButton popupButton; ScopedPointer menuBar; PopupMenu getDummyPopupMenu() { PopupMenu m; m.addItem (1, "Normal item"); m.addItem (2, "Disabled item", false); m.addItem (3, "Ticked item", true, true); m.addColouredItem (4, "Coloured item", Colours::green); m.addSeparator(); m.addCustomItem (5, new CustomMenuComponent()); m.addSeparator(); for (int i = 0; i < 8; ++i) { PopupMenu subMenu; for (int s = 0; s < 8; ++s) { PopupMenu subSubMenu; for (int item = 0; item < 8; ++item) subSubMenu.addItem (1000 + (i * s * item), "Item " + String (item + 1)); subMenu.addSubMenu ("Sub-sub menu " + String (s + 1), subSubMenu); } m.addSubMenu ("Sub menu " + String (i + 1), subMenu); } return m; } //============================================================================== void buttonClicked (Button* button) override { if (button == &popupButton) getDummyPopupMenu().showMenuAsync (PopupMenu::Options().withTargetComponent (&popupButton), nullptr); } //============================================================================== class CustomMenuComponent : public PopupMenu::CustomComponent, private Timer { public: CustomMenuComponent() { // set off a timer to move a blob around on this component every // 300 milliseconds - see the timerCallback() method. startTimer (300); } void getIdealSize (int& idealWidth, int& idealHeight) override { // tells the menu how big we'd like to be.. idealWidth = 200; idealHeight = 60; } void paint (Graphics& g) override { g.fillAll (Colours::yellow.withAlpha (0.3f)); g.setColour (Colours::pink); g.fillEllipse (blobPosition); g.setFont (Font (14.0f, Font::italic)); g.setColour (Colours::black); g.drawFittedText ("This is a customised menu item (also demonstrating the Timer class)...", getLocalBounds().reduced (4, 0), Justification::centred, 3); } private: void timerCallback() override { Random random; blobPosition.setBounds ((float) random.nextInt (getWidth()), (float) random.nextInt (getHeight()), 40.0f, 30.0f); repaint(); } Rectangle blobPosition; }; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MenusDemo) }; //============================================================================== class DemoTabbedComponent : public TabbedComponent, private ChangeListener { public: DemoTabbedComponent() : TabbedComponent (TabbedButtonBar::TabsAtTop) { // Register this class as a ChangeListener to the menus demo so we can update the tab colours when the LookAndFeel is changed menusDemo = new MenusDemo(); menusDemo->addChangeListener (this); const Colour c; addTab ("Menus", c, menusDemo, true); addTab ("Buttons", c, new ButtonsPage(), true); addTab ("Sliders", c, new SlidersPage(), true); addTab ("Toolbars", c, new ToolbarDemoComp(), true); addTab ("Misc", c, new MiscPage(), true); addTab ("Tables", c, new TableDemoComponent(), true); addTab ("Drag & Drop", c, new DragAndDropDemo(), true); updateTabColours(); getTabbedButtonBar().getTabButton (5)->setExtraComponent (new CustomTabButton(), TabBarButton::afterText); } void changeListenerCallback (ChangeBroadcaster* source) override { if (dynamic_cast (source) != nullptr) updateTabColours(); } // This is a small star button that is put inside one of the tabs. You can // use this technique to create things like "close tab" buttons, etc. class CustomTabButton : public Component { public: CustomTabButton() { setSize (20, 20); } void paint (Graphics& g) override { Path star; star.addStar (Point(), 7, 1.0f, 2.0f); g.setColour (Colours::green); g.fillPath (star, star.getTransformToScaleToFit (getLocalBounds().reduced (2).toFloat(), true)); } void mouseDown (const MouseEvent&) override { showBubbleMessage (this, "This is a custom tab component\n" "\n" "You can use these to implement things like close-buttons " "or status displays for your tabs.", bubbleMessage); } private: ScopedPointer bubbleMessage; }; private: ScopedPointer menusDemo; //need to have keep a pointer around to register this class as a ChangeListener void updateTabColours() { bool randomiseColours = ! dynamic_cast (&LookAndFeel::getDefaultLookAndFeel()); for (int i = 0; i < getNumTabs(); ++i) { if (randomiseColours) setTabBackgroundColour (i, Colour (Random::getSystemRandom().nextFloat(), 0.1f, 0.97f, 1.0f)); else setTabBackgroundColour (i, getLookAndFeel().findColour (ResizableWindow::backgroundColourId)); } } }; //============================================================================== struct WidgetsDemo : public Component { WidgetsDemo() { setOpaque (true); addAndMakeVisible (tabs); } void paint (Graphics& g) override { g.fillAll (Colours::white); } void resized() override { tabs.setBounds (getLocalBounds().reduced (4)); } DemoTabbedComponent tabs; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WidgetsDemo) }; // This static object will register this demo type in a global list of demos.. static JuceDemoType demo ("09 Components: Tabs & Widgets");