The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

668 lines
23KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library - "Jules' Utility Class Extensions"
  4. Copyright 2004-11 by Raw Material Software Ltd.
  5. ------------------------------------------------------------------------------
  6. JUCE can be redistributed and/or modified under the terms of the GNU General
  7. Public License (Version 2), as published by the Free Software Foundation.
  8. A copy of the license is included in the JUCE distribution, or can be found
  9. online at www.gnu.org/licenses.
  10. JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
  11. WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
  12. A PARTICULAR PURPOSE. See the GNU General Public License for more details.
  13. ------------------------------------------------------------------------------
  14. To release a closed-source product which uses JUCE, commercial licenses are
  15. available: visit www.rawmaterialsoftware.com/juce for more information.
  16. ==============================================================================
  17. */
  18. #include "jucer_Application.h"
  19. #include "jucer_AppearanceSettings.h"
  20. namespace AppearanceColours
  21. {
  22. struct ColourInfo
  23. {
  24. const char* name;
  25. uint32 colourID;
  26. bool mustBeOpaque;
  27. bool applyToEditorOnly;
  28. };
  29. static const ColourInfo colours[] =
  30. {
  31. { "Main Window Bkgd", mainBackgroundColourId, true, false },
  32. { "Treeview Highlight", treeviewHighlightColourId, false, false },
  33. { "Code Background", CodeEditorComponent::backgroundColourId, true, false },
  34. { "Line Number Bkgd", CodeEditorComponent::lineNumberBackgroundId, false, false },
  35. { "Line Numbers", CodeEditorComponent::lineNumberTextId, false, false },
  36. { "Plain Text", CodeEditorComponent::defaultTextColourId, false, false },
  37. { "Selected Text Bkgd", CodeEditorComponent::highlightColourId, false, false },
  38. { "Caret", CaretComponent::caretColourId, false, true }
  39. };
  40. }
  41. //==============================================================================
  42. AppearanceSettings::AppearanceSettings (bool updateAppWhenChanged)
  43. : settings ("COLOUR_SCHEME")
  44. {
  45. IntrojucerLookAndFeel lf;
  46. for (int i = 0; i < sizeof (AppearanceColours::colours) / sizeof (AppearanceColours::colours[0]); ++i)
  47. getColourValue (AppearanceColours::colours[i].name) = lf.findColour (AppearanceColours::colours[i].colourID).toString();
  48. CodeDocument doc;
  49. CPlusPlusCodeTokeniser tokeniser;
  50. CodeEditorComponent editor (doc, &tokeniser);
  51. const CodeEditorComponent::ColourScheme cs (editor.getColourScheme());
  52. for (int i = cs.types.size(); --i >= 0;)
  53. {
  54. CodeEditorComponent::ColourScheme::TokenType& t = cs.types.getReference(i);
  55. getColourValue (t.name) = t.colour.toString();
  56. }
  57. getCodeFontValue() = getDefaultCodeFont().toString();
  58. if (updateAppWhenChanged)
  59. settings.addListener (this);
  60. }
  61. File AppearanceSettings::getSchemesFolder()
  62. {
  63. File f (getAppProperties().getFile().getSiblingFile ("Schemes"));
  64. f.createDirectory();
  65. return f;
  66. }
  67. void AppearanceSettings::writeDefaultSchemeFile (const String& xmlString, const String& name)
  68. {
  69. const File file (getSchemesFolder().getChildFile (name).withFileExtension (getSchemeFileSuffix()));
  70. if (! file.exists())
  71. {
  72. AppearanceSettings settings (false);
  73. ScopedPointer<XmlElement> xml (XmlDocument::parse (xmlString));
  74. if (xml != nullptr)
  75. settings.readFromXML (*xml);
  76. settings.writeToFile (file);
  77. }
  78. }
  79. void AppearanceSettings::refreshPresetSchemeList()
  80. {
  81. writeDefaultSchemeFile (String::empty, "Default (Light)");
  82. writeDefaultSchemeFile (BinaryData::dark_scheme_xml, "Default (Dark)");
  83. Array<File> newSchemes;
  84. getSchemesFolder().findChildFiles (newSchemes, File::findFiles, false, String ("*") + getSchemeFileSuffix());
  85. if (newSchemes != presetSchemeFiles)
  86. {
  87. presetSchemeFiles.swapWithArray (newSchemes);
  88. commandManager->commandStatusChanged();
  89. }
  90. }
  91. StringArray AppearanceSettings::getPresetSchemes()
  92. {
  93. StringArray s;
  94. for (int i = 0; i < presetSchemeFiles.size(); ++i)
  95. s.add (presetSchemeFiles.getReference(i).getFileNameWithoutExtension());
  96. return s;
  97. }
  98. void AppearanceSettings::selectPresetScheme (int index)
  99. {
  100. readFromFile (presetSchemeFiles [index]);
  101. }
  102. bool AppearanceSettings::readFromXML (const XmlElement& xml)
  103. {
  104. if (xml.hasTagName (settings.getType().toString()))
  105. {
  106. const ValueTree newSettings (ValueTree::fromXml (xml));
  107. // we'll manually copy across the new properties to the existing tree so that
  108. // any open editors will be kept up to date..
  109. settings.copyPropertiesFrom (newSettings, nullptr);
  110. for (int i = settings.getNumChildren(); --i >= 0;)
  111. {
  112. ValueTree c (settings.getChild (i));
  113. const ValueTree newValue (newSettings.getChildWithProperty (Ids::name, c.getProperty (Ids::name)));
  114. if (newValue.isValid())
  115. c.copyPropertiesFrom (newValue, nullptr);
  116. }
  117. return true;
  118. }
  119. return false;
  120. }
  121. bool AppearanceSettings::readFromFile (const File& file)
  122. {
  123. const ScopedPointer<XmlElement> xml (XmlDocument::parse (file));
  124. return xml != nullptr && readFromXML (*xml);
  125. }
  126. bool AppearanceSettings::writeToFile (const File& file) const
  127. {
  128. const ScopedPointer<XmlElement> xml (settings.createXml());
  129. return xml != nullptr && xml->writeToFile (file, String::empty);
  130. }
  131. Font AppearanceSettings::getDefaultCodeFont()
  132. {
  133. return Font (Font::getDefaultMonospacedFontName(), Font::getDefaultStyle(), 13.0f);
  134. }
  135. StringArray AppearanceSettings::getColourNames() const
  136. {
  137. StringArray s;
  138. for (int i = 0; i < settings.getNumChildren(); ++i)
  139. {
  140. const ValueTree c (settings.getChild(i));
  141. if (c.hasType ("COLOUR"))
  142. s.add (c [Ids::name]);
  143. }
  144. return s;
  145. }
  146. void AppearanceSettings::updateColourScheme()
  147. {
  148. applyToLookAndFeel (LookAndFeel::getDefaultLookAndFeel());
  149. JucerApplication::getApp().mainWindowList.sendLookAndFeelChange();
  150. }
  151. void AppearanceSettings::applyToLookAndFeel (LookAndFeel& lf) const
  152. {
  153. for (int i = 0; i < sizeof (AppearanceColours::colours) / sizeof (AppearanceColours::colours[0]); ++i)
  154. {
  155. Colour col;
  156. if (getColour (AppearanceColours::colours[i].name, col))
  157. {
  158. if (AppearanceColours::colours[i].mustBeOpaque)
  159. col = Colours::white.overlaidWith (col);
  160. if (! AppearanceColours::colours[i].applyToEditorOnly)
  161. lf.setColour (AppearanceColours::colours[i].colourID, col);
  162. }
  163. }
  164. lf.setColour (ScrollBar::thumbColourId,
  165. getScrollbarColourForBackground (lf.findColour (mainBackgroundColourId)));
  166. }
  167. void AppearanceSettings::applyToCodeEditor (CodeEditorComponent& editor) const
  168. {
  169. CodeEditorComponent::ColourScheme cs (editor.getColourScheme());
  170. for (int i = cs.types.size(); --i >= 0;)
  171. {
  172. CodeEditorComponent::ColourScheme::TokenType& t = cs.types.getReference(i);
  173. getColour (t.name, t.colour);
  174. }
  175. editor.setColourScheme (cs);
  176. editor.setFont (getCodeFont());
  177. for (int i = 0; i < sizeof (AppearanceColours::colours) / sizeof (AppearanceColours::colours[0]); ++i)
  178. {
  179. if (AppearanceColours::colours[i].applyToEditorOnly)
  180. {
  181. Colour col;
  182. if (getColour (AppearanceColours::colours[i].name, col))
  183. editor.setColour (AppearanceColours::colours[i].colourID, col);
  184. }
  185. }
  186. editor.setColour (ScrollBar::thumbColourId,
  187. getScrollbarColourForBackground (editor.findColour (CodeEditorComponent::backgroundColourId)));
  188. }
  189. Font AppearanceSettings::getCodeFont() const
  190. {
  191. const String fontString (settings [Ids::font].toString());
  192. if (fontString.isEmpty())
  193. return getDefaultCodeFont();
  194. return Font::fromString (fontString);
  195. }
  196. Value AppearanceSettings::getCodeFontValue()
  197. {
  198. return settings.getPropertyAsValue (Ids::font, nullptr);
  199. }
  200. Value AppearanceSettings::getColourValue (const String& colourName)
  201. {
  202. ValueTree c (settings.getChildWithProperty (Ids::name, colourName));
  203. if (! c.isValid())
  204. {
  205. c = ValueTree ("COLOUR");
  206. c.setProperty (Ids::name, colourName, nullptr);
  207. settings.addChild (c, -1, nullptr);
  208. }
  209. return c.getPropertyAsValue (Ids::colour, nullptr);
  210. }
  211. bool AppearanceSettings::getColour (const String& name, Colour& result) const
  212. {
  213. const ValueTree colour (settings.getChildWithProperty (Ids::name, name));
  214. if (colour.isValid())
  215. {
  216. result = Colour::fromString (colour [Ids::colour].toString());
  217. return true;
  218. }
  219. return false;
  220. }
  221. Colour AppearanceSettings::getScrollbarColourForBackground (const Colour& background)
  222. {
  223. return background.contrasting().withAlpha (0.13f);
  224. }
  225. //==============================================================================
  226. struct AppearanceEditor
  227. {
  228. class Window : public DialogWindow
  229. {
  230. public:
  231. Window() : DialogWindow ("Appearance Settings", Colours::darkgrey, true, true)
  232. {
  233. setUsingNativeTitleBar (true);
  234. if (getAppSettings().monospacedFontNames.size() == 0)
  235. setContentOwned (new FontScanPanel(), false);
  236. else
  237. setContentOwned (new EditorPanel(), false);
  238. setResizable (true, true);
  239. const int width = 350;
  240. setResizeLimits (width, 200, width, 1000);
  241. String windowState (getAppProperties().getValue (getWindowPosName()));
  242. if (windowState.isNotEmpty())
  243. restoreWindowStateFromString (windowState);
  244. else
  245. centreAroundComponent (Component::getCurrentlyFocusedComponent(), width, 500);
  246. setVisible (true);
  247. }
  248. ~Window()
  249. {
  250. getAppProperties().setValue (getWindowPosName(), getWindowStateAsString());
  251. }
  252. void closeButtonPressed()
  253. {
  254. JucerApplication::getApp().appearanceEditorWindow = nullptr;
  255. }
  256. private:
  257. static const char* getWindowPosName() { return "colourSchemeEditorPos"; }
  258. JUCE_DECLARE_NON_COPYABLE (Window);
  259. };
  260. //==============================================================================
  261. class FontScanPanel : public Component,
  262. private Timer
  263. {
  264. public:
  265. FontScanPanel()
  266. {
  267. fontsToScan = Font::findAllTypefaceNames();
  268. startTimer (1);
  269. }
  270. void paint (Graphics& g)
  271. {
  272. g.fillAll (Colours::darkgrey);
  273. g.setFont (14.0f);
  274. g.setColour (Colours::white);
  275. g.drawFittedText ("Scanning for fonts..", getLocalBounds(), Justification::centred, 2);
  276. const int size = 30;
  277. getLookAndFeel().drawSpinningWaitAnimation (g, Colours::white, (getWidth() - size) / 2, getHeight() / 2 - 50, size, size);
  278. }
  279. void timerCallback()
  280. {
  281. repaint();
  282. if (fontsToScan.size() == 0)
  283. {
  284. getAppSettings().monospacedFontNames = fontsFound;
  285. Window* w = findParentComponentOfClass<Window>();
  286. if (w != nullptr)
  287. w->setContentOwned (new EditorPanel(), false);
  288. }
  289. else
  290. {
  291. if (isMonospacedTypeface (fontsToScan[0]))
  292. fontsFound.add (fontsToScan[0]);
  293. fontsToScan.remove (0);
  294. }
  295. }
  296. // A rather hacky trick to select only the fixed-pitch fonts..
  297. // This is unfortunately a bit slow, but will work on all platforms.
  298. static bool isMonospacedTypeface (const String& name)
  299. {
  300. const Font font (name, 20.0f, Font::plain);
  301. const int width = font.getStringWidth ("....");
  302. return width == font.getStringWidth ("WWWW")
  303. && width == font.getStringWidth ("0000")
  304. && width == font.getStringWidth ("1111")
  305. && width == font.getStringWidth ("iiii");
  306. }
  307. StringArray fontsToScan, fontsFound;
  308. };
  309. //==============================================================================
  310. class EditorPanel : public Component,
  311. private Button::Listener
  312. {
  313. public:
  314. EditorPanel()
  315. : loadButton ("Load Scheme..."),
  316. saveButton ("Save Scheme...")
  317. {
  318. rebuildProperties();
  319. addAndMakeVisible (&panel);
  320. loadButton.setColour (TextButton::buttonColourId, Colours::darkgrey.withAlpha (0.5f));
  321. saveButton.setColour (TextButton::buttonColourId, Colours::darkgrey.withAlpha (0.5f));
  322. loadButton.setColour (TextButton::textColourOffId, Colours::white);
  323. saveButton.setColour (TextButton::textColourOffId, Colours::white);
  324. addAndMakeVisible (&loadButton);
  325. addAndMakeVisible (&saveButton);
  326. loadButton.addListener (this);
  327. saveButton.addListener (this);
  328. }
  329. void rebuildProperties()
  330. {
  331. AppearanceSettings& scheme = getAppSettings().appearance;
  332. Array <PropertyComponent*> props;
  333. Value fontValue (scheme.getCodeFontValue());
  334. props.add (FontNameValueSource::createProperty ("Code Editor Font", fontValue));
  335. props.add (FontSizeValueSource::createProperty ("Font Size", fontValue));
  336. const StringArray colourNames (scheme.getColourNames());
  337. for (int i = 0; i < colourNames.size(); ++i)
  338. props.add (new ColourPropertyComponent (nullptr, colourNames[i],
  339. scheme.getColourValue (colourNames[i]),
  340. Colours::white, false));
  341. panel.clear();
  342. panel.addProperties (props);
  343. }
  344. void resized()
  345. {
  346. Rectangle<int> r (getLocalBounds());
  347. panel.setBounds (r.removeFromTop (getHeight() - 28).reduced (4, 2));
  348. loadButton.setBounds (r.removeFromLeft (getWidth() / 2).reduced (10, 4));
  349. saveButton.setBounds (r.reduced (10, 3));
  350. }
  351. private:
  352. PropertyPanel panel;
  353. TextButton loadButton, saveButton;
  354. void buttonClicked (Button* b)
  355. {
  356. if (b == &loadButton)
  357. loadScheme();
  358. else
  359. saveScheme();
  360. }
  361. void saveScheme()
  362. {
  363. FileChooser fc ("Select a file in which to save this colour-scheme...",
  364. getAppSettings().appearance.getSchemesFolder()
  365. .getNonexistentChildFile ("Scheme", AppearanceSettings::getSchemeFileSuffix()),
  366. AppearanceSettings::getSchemeFileWildCard());
  367. if (fc.browseForFileToSave (true))
  368. {
  369. File file (fc.getResult().withFileExtension (AppearanceSettings::getSchemeFileSuffix()));
  370. getAppSettings().appearance.writeToFile (file);
  371. getAppSettings().appearance.refreshPresetSchemeList();
  372. }
  373. }
  374. void loadScheme()
  375. {
  376. FileChooser fc ("Please select a colour-scheme file to load...",
  377. getAppSettings().appearance.getSchemesFolder(),
  378. AppearanceSettings::getSchemeFileWildCard());
  379. if (fc.browseForFileToOpen())
  380. {
  381. if (getAppSettings().appearance.readFromFile (fc.getResult()))
  382. rebuildProperties();
  383. }
  384. }
  385. JUCE_DECLARE_NON_COPYABLE (EditorPanel);
  386. };
  387. //==============================================================================
  388. class FontNameValueSource : public ValueSourceFilter
  389. {
  390. public:
  391. FontNameValueSource (const Value& source) : ValueSourceFilter (source) {}
  392. var getValue() const
  393. {
  394. return Font::fromString (sourceValue.toString()).getTypefaceName();
  395. }
  396. void setValue (const var& newValue)
  397. {
  398. Font font (Font::fromString (sourceValue.toString()));
  399. font.setTypefaceName (newValue.toString().isEmpty() ? Font::getDefaultMonospacedFontName()
  400. : newValue.toString());
  401. sourceValue = font.toString();
  402. }
  403. static ChoicePropertyComponent* createProperty (const String& title, const Value& value)
  404. {
  405. StringArray fontNames = getAppSettings().monospacedFontNames;
  406. Array<var> values;
  407. values.add (Font::getDefaultMonospacedFontName());
  408. values.add (var());
  409. for (int i = 0; i < fontNames.size(); ++i)
  410. values.add (fontNames[i]);
  411. StringArray names;
  412. names.add ("<Default Monospaced>");
  413. names.add (String::empty);
  414. names.addArray (getAppSettings().monospacedFontNames);
  415. return new ChoicePropertyComponent (Value (new FontNameValueSource (value)),
  416. title, names, values);
  417. }
  418. };
  419. //==============================================================================
  420. class FontSizeValueSource : public ValueSourceFilter
  421. {
  422. public:
  423. FontSizeValueSource (const Value& source) : ValueSourceFilter (source) {}
  424. var getValue() const
  425. {
  426. return Font::fromString (sourceValue.toString()).getHeight();
  427. }
  428. void setValue (const var& newValue)
  429. {
  430. sourceValue = Font::fromString (sourceValue.toString()).withHeight (newValue).toString();
  431. }
  432. static PropertyComponent* createProperty (const String& title, const Value& value)
  433. {
  434. return new SliderPropertyComponent (Value (new FontSizeValueSource (value)),
  435. title, 5.0, 40.0, 0.1, 0.5);
  436. }
  437. };
  438. };
  439. Component* AppearanceSettings::createEditorWindow()
  440. {
  441. return new AppearanceEditor::Window();
  442. }
  443. //==============================================================================
  444. IntrojucerLookAndFeel::IntrojucerLookAndFeel()
  445. {
  446. setColour (mainBackgroundColourId, Colour::greyLevel (0.8f));
  447. setColour (treeviewHighlightColourId, Colour (0x401111ee));
  448. }
  449. Rectangle<int> IntrojucerLookAndFeel::getPropertyComponentContentPosition (PropertyComponent& component)
  450. {
  451. if (component.findParentComponentOfClass<AppearanceEditor::EditorPanel>() != nullptr)
  452. return component.getLocalBounds().reduced (1, 1).removeFromRight (component.getWidth() / 2);
  453. return LookAndFeel::getPropertyComponentContentPosition (component);
  454. }
  455. int IntrojucerLookAndFeel::getTabButtonOverlap (int tabDepth) { return -1; }
  456. int IntrojucerLookAndFeel::getTabButtonSpaceAroundImage() { return 1; }
  457. int IntrojucerLookAndFeel::getTabButtonBestWidth (TabBarButton& button, int tabDepth) { return 120; }
  458. void IntrojucerLookAndFeel::createTabTextLayout (const TabBarButton& button, const Rectangle<int>& textArea, GlyphArrangement& textLayout)
  459. {
  460. Font font (textArea.getHeight() * 0.5f);
  461. font.setUnderline (button.hasKeyboardFocus (false));
  462. textLayout.addFittedText (font, button.getButtonText().trim(),
  463. (float) textArea.getX(), (float) textArea.getY(), (float) textArea.getWidth(), (float) textArea.getHeight(),
  464. Justification::centred, 1);
  465. }
  466. Colour IntrojucerLookAndFeel::getTabBackgroundColour (TabBarButton& button)
  467. {
  468. const Colour normalBkg (button.getTabBackgroundColour());
  469. Colour bkg (normalBkg.contrasting (0.15f));
  470. if (button.isFrontTab())
  471. bkg = bkg.overlaidWith (Colours::yellow.withAlpha (0.5f));
  472. return bkg;
  473. }
  474. void IntrojucerLookAndFeel::drawTabButton (TabBarButton& button, Graphics& g, bool isMouseOver, bool isMouseDown)
  475. {
  476. const Rectangle<int> activeArea (button.getActiveArea());
  477. const Colour bkg (getTabBackgroundColour (button));
  478. g.setGradientFill (ColourGradient (bkg.brighter (0.1f), 0, (float) activeArea.getY(),
  479. bkg.darker (0.1f), 0, (float) activeArea.getBottom(), false));
  480. g.fillRect (activeArea);
  481. g.setColour (button.getTabBackgroundColour().darker (0.3f));
  482. g.drawRect (activeArea);
  483. GlyphArrangement textLayout;
  484. createTabTextLayout (button, button.getTextArea(), textLayout);
  485. const float alpha = button.isEnabled() ? ((isMouseOver || isMouseDown) ? 1.0f : 0.8f) : 0.3f;
  486. g.setColour (bkg.contrasting().withMultipliedAlpha (alpha));
  487. textLayout.draw (g);
  488. }
  489. Rectangle<int> IntrojucerLookAndFeel::getTabButtonExtraComponentBounds (const TabBarButton& button, Rectangle<int>& textArea, Component& comp)
  490. {
  491. GlyphArrangement textLayout;
  492. createTabTextLayout (button, textArea, textLayout);
  493. const int textWidth = (int) textLayout.getBoundingBox (0, -1, false).getWidth();
  494. const int extraSpace = jmax (0, textArea.getWidth() - (textWidth + comp.getWidth())) / 2;
  495. textArea.removeFromRight (extraSpace);
  496. textArea.removeFromLeft (extraSpace);
  497. return textArea.removeFromRight (comp.getWidth());
  498. }
  499. void IntrojucerLookAndFeel::drawStretchableLayoutResizerBar (Graphics& g, int /*w*/, int /*h*/, bool /*isVerticalBar*/, bool isMouseOver, bool isMouseDragging)
  500. {
  501. if (isMouseOver || isMouseDragging)
  502. g.fillAll (Colours::yellow.withAlpha (0.4f));
  503. }
  504. void IntrojucerLookAndFeel::drawScrollbar (Graphics& g, ScrollBar& scrollbar, int x, int y, int width, int height,
  505. bool isScrollbarVertical, int thumbStartPosition, int thumbSize,
  506. bool isMouseOver, bool isMouseDown)
  507. {
  508. Path thumbPath;
  509. if (thumbSize > 0)
  510. {
  511. const float thumbIndent = (isScrollbarVertical ? width : height) * 0.25f;
  512. const float thumbIndentx2 = thumbIndent * 2.0f;
  513. if (isScrollbarVertical)
  514. thumbPath.addRoundedRectangle (x + thumbIndent, thumbStartPosition + thumbIndent,
  515. width - thumbIndentx2, thumbSize - thumbIndentx2, (width - thumbIndentx2) * 0.5f);
  516. else
  517. thumbPath.addRoundedRectangle (thumbStartPosition + thumbIndent, y + thumbIndent,
  518. thumbSize - thumbIndentx2, height - thumbIndentx2, (height - thumbIndentx2) * 0.5f);
  519. }
  520. Colour thumbCol (scrollbar.findColour (ScrollBar::thumbColourId, true));
  521. if (isMouseOver || isMouseDown)
  522. thumbCol = thumbCol.withMultipliedAlpha (2.0f);
  523. g.setColour (thumbCol);
  524. g.fillPath (thumbPath);
  525. g.setColour (thumbCol.contrasting ((isMouseOver || isMouseDown) ? 0.2f : 0.1f));
  526. g.strokePath (thumbPath, PathStrokeType (1.0f));
  527. }