|
| 1 | +//============================================================================== |
| 2 | +/** Demo showing how to use the MarkdownComponent. |
| 3 | +
|
| 4 | + This demonstrates parsing and rendering markdown from both strings and files, |
| 5 | + with auto-reload functionality and customizable styling. |
| 6 | +*/ |
| 7 | +class MarkdownDemo final : public DemoBase, |
| 8 | + private Button::Listener, |
| 9 | + private FileDragAndDropTarget |
| 10 | +{ |
| 11 | +public: |
| 12 | + /** Constructor. */ |
| 13 | + MarkdownDemo (SharedObjects& sharedObjs) : |
| 14 | + DemoBase (sharedObjs, "Markdown") |
| 15 | + { |
| 16 | + SQUAREPINE_CRASH_TRACER |
| 17 | + |
| 18 | + setSize (800, 600); |
| 19 | + |
| 20 | + // Create the markdown component and viewport |
| 21 | + viewport.setViewedComponent (&markdownComponent, false); |
| 22 | + viewport.setScrollBarsShown (true, false); // Vertical scrolling only |
| 23 | + addAndMakeVisible (viewport); |
| 24 | + |
| 25 | + // Create demo buttons |
| 26 | + loadStringButton.setButtonText (TRANS ("Load Sample Text")); |
| 27 | + loadStringButton.addListener (this); |
| 28 | + addAndMakeVisible (loadStringButton); |
| 29 | + |
| 30 | + loadFileButton.setButtonText (TRANS ("Load File...")); |
| 31 | + loadFileButton.addListener (this); |
| 32 | + addAndMakeVisible (loadFileButton); |
| 33 | + |
| 34 | + autoReloadToggle.setButtonText (TRANS ("Auto-reload file")); |
| 35 | + autoReloadToggle.setToggleable (true); |
| 36 | + autoReloadToggle.addListener (this); |
| 37 | + addAndMakeVisible (autoReloadToggle); |
| 38 | + |
| 39 | + fontSizeSlider.setSliderStyle (Slider::LinearHorizontal); |
| 40 | + fontSizeSlider.setRange (8.0, 24.0, 1.0); |
| 41 | + fontSizeSlider.setValue (14.0); |
| 42 | + fontSizeSlider.setTextBoxStyle (Slider::TextBoxRight, false, 50, 50); |
| 43 | + fontSizeSlider.onValueChange = [this]() |
| 44 | + { |
| 45 | + markdownComponent.setBaseFontSize ((float) fontSizeSlider.getValue()); |
| 46 | + updateMarkdownSize(); |
| 47 | + }; |
| 48 | + |
| 49 | + addAndMakeVisible (fontSizeSlider); |
| 50 | + |
| 51 | + fontSizeLabel.setText (TRANS ("Font Size:"), dontSendNotification); |
| 52 | + fontSizeLabel.attachToComponent (&fontSizeSlider, true); |
| 53 | + addAndMakeVisible (fontSizeLabel); |
| 54 | + |
| 55 | + loadSampleMarkdown(); |
| 56 | + } |
| 57 | + |
| 58 | + //============================================================================== |
| 59 | + /** @internal */ |
| 60 | + void paint (Graphics& g) override |
| 61 | + { |
| 62 | + g.fillAll (getLookAndFeel().findColour (ResizableWindow::backgroundColourId)); |
| 63 | + |
| 64 | + // Draw drag and drop hint |
| 65 | + if (isDragOver) |
| 66 | + { |
| 67 | + g.setColour (Colours::lightblue.withAlpha (0.3f)); |
| 68 | + g.fillAll(); |
| 69 | + |
| 70 | + g.setColour (Colours::lightblue); |
| 71 | + g.drawRect (getLocalBounds(), 2); |
| 72 | + |
| 73 | + g.setFont (16.0f); |
| 74 | + g.drawText (TRANS ("Drop a markdown file here!"), getLocalBounds(), Justification::centred); |
| 75 | + } |
| 76 | + } |
| 77 | + |
| 78 | + /** @internal */ |
| 79 | + void resized() override |
| 80 | + { |
| 81 | + auto bounds = getLocalBounds(); |
| 82 | + auto buttonArea = bounds.removeFromTop (80).reduced (10); |
| 83 | + |
| 84 | + // First row of controls |
| 85 | + auto firstRow = buttonArea.removeFromTop (30); |
| 86 | + loadStringButton.setBounds (firstRow.removeFromLeft (120)); |
| 87 | + firstRow.removeFromLeft (10); |
| 88 | + loadFileButton.setBounds (firstRow.removeFromLeft (120)); |
| 89 | + firstRow.removeFromLeft (10); |
| 90 | + autoReloadToggle.setBounds (firstRow.removeFromLeft (120)); |
| 91 | + |
| 92 | + // Second row for font size |
| 93 | + buttonArea.removeFromTop (10); |
| 94 | + auto secondRow = buttonArea.removeFromTop (30); |
| 95 | + secondRow.removeFromLeft (70); // Space for label |
| 96 | + fontSizeSlider.setBounds (secondRow.removeFromLeft (200)); |
| 97 | + |
| 98 | + viewport.setBounds (bounds.reduced (10)); |
| 99 | + updateMarkdownSize(); |
| 100 | + } |
| 101 | + |
| 102 | + //============================================================================== |
| 103 | + /** @internal */ |
| 104 | + bool isInterestedInFileDrag (const StringArray& files) override |
| 105 | + { |
| 106 | + for (const auto& file : files) |
| 107 | + if (File (file).hasFileExtension (".md;.markdown;.txt")) |
| 108 | + return true; |
| 109 | + |
| 110 | + return false; |
| 111 | + } |
| 112 | + |
| 113 | + /** @internal */ |
| 114 | + void fileDragEnter (const StringArray&, int, int) override |
| 115 | + { |
| 116 | + isDragOver = true; |
| 117 | + repaint(); |
| 118 | + } |
| 119 | + |
| 120 | + /** @internal */ |
| 121 | + void fileDragExit (const StringArray&) override |
| 122 | + { |
| 123 | + isDragOver = false; |
| 124 | + repaint(); |
| 125 | + } |
| 126 | + |
| 127 | + /** @internal */ |
| 128 | + void filesDropped (const StringArray& files, int, int) override |
| 129 | + { |
| 130 | + isDragOver = false; |
| 131 | + |
| 132 | + if (const File file = files.strings.getFirst(); file.existsAsFile()) |
| 133 | + { |
| 134 | + markdownComponent.setMarkdownFile (file); |
| 135 | + autoReloadToggle.setToggleState (false, dontSendNotification); |
| 136 | + updateMarkdownSize(); |
| 137 | + } |
| 138 | + |
| 139 | + repaint(); |
| 140 | + } |
| 141 | + |
| 142 | +private: |
| 143 | + //============================================================================== |
| 144 | + Viewport viewport; |
| 145 | + sp::MarkdownComponent markdownComponent; |
| 146 | + TextButton loadStringButton, loadFileButton; |
| 147 | + ToggleButton autoReloadToggle; |
| 148 | + Slider fontSizeSlider; |
| 149 | + Label fontSizeLabel; |
| 150 | + std::unique_ptr<FileChooser> fileChooser; |
| 151 | + bool isDragOver = false; |
| 152 | + |
| 153 | + //============================================================================== |
| 154 | + void updateMarkdownSize() |
| 155 | + { |
| 156 | + if (viewport.getWidth() > 0) |
| 157 | + markdownComponent.setPreferredWidth (viewport.getWidth()); |
| 158 | + } |
| 159 | + |
| 160 | + /** @internal */ |
| 161 | + void buttonClicked (Button* button) override |
| 162 | + { |
| 163 | + SQUAREPINE_CRASH_TRACER |
| 164 | + |
| 165 | + if (button == &loadStringButton) |
| 166 | + { |
| 167 | + loadSampleMarkdown(); |
| 168 | + } |
| 169 | + else if (button == &loadFileButton) |
| 170 | + { |
| 171 | + if (fileChooser != nullptr) |
| 172 | + return; |
| 173 | + |
| 174 | + fileChooser = std::make_unique<FileChooser> (TRANS ("Choose a markdown file..."), File(), "*.md;*.markdown;*.txt"); |
| 175 | + |
| 176 | + fileChooser->launchAsync (FileBrowserComponent::openMode | FileBrowserComponent::canSelectFiles, |
| 177 | + [this] (const FileChooser& chooser) |
| 178 | + { |
| 179 | + if (const auto file = chooser.getResult(); file.existsAsFile()) |
| 180 | + { |
| 181 | + markdownComponent.setMarkdownFile (file); |
| 182 | + autoReloadToggle.setToggleState (false, dontSendNotification); |
| 183 | + updateMarkdownSize(); |
| 184 | + } |
| 185 | + }); |
| 186 | + } |
| 187 | + else if (button == &autoReloadToggle) |
| 188 | + { |
| 189 | + markdownComponent.setAutoReload (autoReloadToggle.getToggleState()); |
| 190 | + } |
| 191 | + } |
| 192 | + |
| 193 | + void loadSampleMarkdown() |
| 194 | + { |
| 195 | + const char* const sampleMarkdown = R"(# SquarePine Markdown Demo |
| 196 | +
|
| 197 | +This is a demonstration of the **MarkdownComponent** for JUCE. |
| 198 | +
|
| 199 | +## Features |
| 200 | +
|
| 201 | +The component supports many markdown features: |
| 202 | +
|
| 203 | +- **Bold text** using double asterisks |
| 204 | +- *Italic text* using single asterisks |
| 205 | +- `Inline code` using backticks |
| 206 | +- [Links](https://www.squarepine.io) with clickable URLs |
| 207 | +- Headers at multiple levels (from H1, H2, H3, H4, H5, and maximum H6) |
| 208 | +- Unordered lists like this one |
| 209 | +- Ordered lists (see below) |
| 210 | +- Code blocks with syntax highlighting |
| 211 | +
|
| 212 | +### Images |
| 213 | +
|
| 214 | +The component supports both raster and vector images: |
| 215 | +
|
| 216 | + |
| 217 | +
|
| 218 | +*Example: Sample image demonstrating web URL loading* |
| 219 | +
|
| 220 | +You can also specify size constraints: |
| 221 | +- `` - Original size |
| 222 | +- `` - Max width 300px |
| 223 | +- `` - Max height 200px |
| 224 | +- `` - Max 300x200px |
| 225 | +
|
| 226 | +**Local Images**: When loading from files, images are resolved relative to the markdown file's directory. |
| 227 | +
|
| 228 | +### Code Example |
| 229 | +
|
| 230 | +Here's some C++ code showing how to use the component: |
| 231 | +
|
| 232 | +```cpp |
| 233 | +// Create and configure a MarkdownComponent |
| 234 | +sp::MarkdownComponent markdown; |
| 235 | +markdown.setMarkdownText ("# Hello World\n\nThis is **bold** text."); |
| 236 | +markdown.setBaseFontSize (16.0f); |
| 237 | +markdown.setAutoReload (true); |
| 238 | +
|
| 239 | +// Add to your component |
| 240 | +addAndMakeVisible (markdown); |
| 241 | +``` |
| 242 | +
|
| 243 | +## Usage Instructions |
| 244 | +
|
| 245 | +1. **Load Sample Text**: Click to see this demo content |
| 246 | +2. **Load File**: Browse for a `.md`, `.markdown`, or `.txt` file |
| 247 | +3. **Auto-reload**: Enable to automatically reload files when they change |
| 248 | +4. **Font Size**: Adjust the base font size with the slider |
| 249 | +5. **Drag & Drop**: Drop markdown files directly onto this demo |
| 250 | +
|
| 251 | +### Customisation Options |
| 252 | +
|
| 253 | +The `MarkdownComponent` provides several customisation methods: |
| 254 | +
|
| 255 | +- `setBaseFontSize()` - Adjust the base font size |
| 256 | +- `setColours()` - Customize text, heading, code, and link colors |
| 257 | +- `setAutoReload()` - Enable/disable file change monitoring |
| 258 | +- `setMarkdownText()` - Set content from a string |
| 259 | +- `setMarkdownFile()` - Load content from a file |
| 260 | +
|
| 261 | +### Supported Markdown Syntax |
| 262 | +
|
| 263 | +| Element | Syntax | Example | |
| 264 | +|---------|--------|---------| |
| 265 | +| Headers | `# ## ###` | # Heading 1 | |
| 266 | +| Bold | `**text**` | **bold text** | |
| 267 | +| Italic | `*text*` | *italic text* | |
| 268 | +| Code | `` `text` `` | `inline code` | |
| 269 | +| Links | `[text](url)` | [SquarePine](https://squarepine.io) | |
| 270 | +| Images | `` |  | |
| 271 | +| Lists | `- item` or `1. item` | • Bullet or 1. Numbered | |
| 272 | +
|
| 273 | +That's it! The component handles parsing and rendering automatically with proper JUCE integration. |
| 274 | +
|
| 275 | +--- |
| 276 | +
|
| 277 | +*Try dragging a markdown file onto this demo to see it in action!* |
| 278 | +)"; |
| 279 | + |
| 280 | + auto sm = String::fromUTF8 (sampleMarkdown) |
| 281 | + .replace ("{SAMPLE_SVG}", "https://raw.githubusercontent.com/SquarePine/squarepine_core/main/demo/assets/logos/squarepine_logo_colour.svg") |
| 282 | + .replace ("{SAMPLE_PNG}", "https://raw.githubusercontent.com/SquarePine/squarepine_core/main/demo/assets/logos/squarepine_icon_32.png"); |
| 283 | + |
| 284 | + markdownComponent.setMarkdownText (sm); |
| 285 | + autoReloadToggle.setToggleState (false, dontSendNotification); |
| 286 | + updateMarkdownSize(); |
| 287 | + } |
| 288 | + |
| 289 | + //============================================================================== |
| 290 | + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MarkdownDemo) |
| 291 | +}; |
0 commit comments