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.

626 lines
21KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2013 - Raw Material Software Ltd.
  5. Permission is granted to use this software under the terms of either:
  6. a) the GPL v2 (or any later version)
  7. b) the Affero GPL v3
  8. Details of these licenses can be found at: www.gnu.org/licenses
  9. JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
  10. WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
  11. A PARTICULAR PURPOSE. See the GNU General Public License for more details.
  12. ------------------------------------------------------------------------------
  13. To release a closed-source product which uses JUCE, commercial licenses are
  14. available: visit www.juce.com for more information.
  15. ==============================================================================
  16. */
  17. #include "jucer_Application.h"
  18. #include "jucer_AppearanceSettings.h"
  19. namespace AppearanceColours
  20. {
  21. struct ColourInfo
  22. {
  23. const char* name;
  24. int colourID;
  25. bool mustBeOpaque;
  26. bool applyToEditorOnly;
  27. };
  28. static const ColourInfo colours[] =
  29. {
  30. { "Main Window Bkgd", mainBackgroundColourId, true, false },
  31. { "Treeview Highlight", TreeView::selectedItemBackgroundColourId, false, false },
  32. { "Code Background", CodeEditorComponent::backgroundColourId, true, false },
  33. { "Line Number Bkgd", CodeEditorComponent::lineNumberBackgroundId, false, false },
  34. { "Line Numbers", CodeEditorComponent::lineNumberTextId, false, false },
  35. { "Plain Text", CodeEditorComponent::defaultTextColourId, false, false },
  36. { "Selected Text Bkgd", CodeEditorComponent::highlightColourId, false, false },
  37. { "Caret", CaretComponent::caretColourId, false, true }
  38. };
  39. enum
  40. {
  41. numColours = sizeof (AppearanceColours::colours) / sizeof (AppearanceColours::colours[0])
  42. };
  43. }
  44. //==============================================================================
  45. AppearanceSettings::AppearanceSettings (bool updateAppWhenChanged)
  46. : settings ("COLOUR_SCHEME")
  47. {
  48. IntrojucerLookAndFeel lf;
  49. for (int i = 0; i < AppearanceColours::numColours; ++i)
  50. getColourValue (AppearanceColours::colours[i].name) = lf.findColour (AppearanceColours::colours[i].colourID).toString();
  51. CodeDocument doc;
  52. CPlusPlusCodeTokeniser tokeniser;
  53. CodeEditorComponent editor (doc, &tokeniser);
  54. const CodeEditorComponent::ColourScheme cs (editor.getColourScheme());
  55. for (int i = cs.types.size(); --i >= 0;)
  56. {
  57. CodeEditorComponent::ColourScheme::TokenType& t = cs.types.getReference(i);
  58. getColourValue (t.name) = t.colour.toString();
  59. }
  60. getCodeFontValue() = getDefaultCodeFont().toString();
  61. if (updateAppWhenChanged)
  62. settings.addListener (this);
  63. }
  64. File AppearanceSettings::getSchemesFolder()
  65. {
  66. File f (getGlobalProperties().getFile().getSiblingFile ("Schemes"));
  67. f.createDirectory();
  68. return f;
  69. }
  70. void AppearanceSettings::writeDefaultSchemeFile (const String& xmlString, const String& name)
  71. {
  72. const File file (getSchemesFolder().getChildFile (name).withFileExtension (getSchemeFileSuffix()));
  73. AppearanceSettings settings (false);
  74. ScopedPointer<XmlElement> xml (XmlDocument::parse (xmlString));
  75. if (xml != nullptr)
  76. settings.readFromXML (*xml);
  77. settings.writeToFile (file);
  78. }
  79. void AppearanceSettings::refreshPresetSchemeList()
  80. {
  81. writeDefaultSchemeFile (BinaryData::colourscheme_dark_xml, "Default (Dark)");
  82. writeDefaultSchemeFile (BinaryData::colourscheme_light_xml, "Default (Light)");
  83. Array<File> newSchemes;
  84. getSchemesFolder().findChildFiles (newSchemes, File::findFiles, false, String ("*") + getSchemeFileSuffix());
  85. if (newSchemes != presetSchemeFiles)
  86. {
  87. presetSchemeFiles.swapWith (newSchemes);
  88. IntrojucerApp::getCommandManager().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. IntrojucerApp::getApp().mainWindowList.sendLookAndFeelChange();
  150. }
  151. void AppearanceSettings::applyToLookAndFeel (LookAndFeel& lf) const
  152. {
  153. for (int i = 0; i < AppearanceColours::numColours; ++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, lf.findColour (mainBackgroundColourId).contrasting().withAlpha (0.13f));
  165. }
  166. void AppearanceSettings::applyToCodeEditor (CodeEditorComponent& editor) const
  167. {
  168. CodeEditorComponent::ColourScheme cs (editor.getColourScheme());
  169. for (int i = cs.types.size(); --i >= 0;)
  170. {
  171. CodeEditorComponent::ColourScheme::TokenType& t = cs.types.getReference(i);
  172. getColour (t.name, t.colour);
  173. }
  174. editor.setColourScheme (cs);
  175. editor.setFont (getCodeFont());
  176. for (int i = 0; i < AppearanceColours::numColours; ++i)
  177. {
  178. if (AppearanceColours::colours[i].applyToEditorOnly)
  179. {
  180. Colour col;
  181. if (getColour (AppearanceColours::colours[i].name, col))
  182. editor.setColour (AppearanceColours::colours[i].colourID, col);
  183. }
  184. }
  185. editor.setColour (ScrollBar::thumbColourId, editor.findColour (CodeEditorComponent::backgroundColourId)
  186. .contrasting()
  187. .withAlpha (0.13f));
  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. //==============================================================================
  222. struct AppearanceEditor
  223. {
  224. class FontScanPanel : public Component,
  225. private Timer
  226. {
  227. public:
  228. FontScanPanel()
  229. {
  230. fontsToScan = Font::findAllTypefaceNames();
  231. startTimer (1);
  232. }
  233. void paint (Graphics& g) override
  234. {
  235. g.fillAll (Colours::darkgrey);
  236. g.setFont (14.0f);
  237. g.setColour (Colours::white);
  238. g.drawFittedText ("Scanning for fonts..", getLocalBounds(), Justification::centred, 2);
  239. const int size = 30;
  240. getLookAndFeel().drawSpinningWaitAnimation (g, Colours::white, (getWidth() - size) / 2, getHeight() / 2 - 50, size, size);
  241. }
  242. void timerCallback() override
  243. {
  244. repaint();
  245. if (fontsToScan.size() == 0)
  246. {
  247. getAppSettings().monospacedFontNames = fontsFound;
  248. if (DialogWindow* w = findParentComponentOfClass<DialogWindow>())
  249. w->setContentOwned (new EditorPanel(), false);
  250. }
  251. else
  252. {
  253. if (isMonospacedTypeface (fontsToScan[0]))
  254. fontsFound.add (fontsToScan[0]);
  255. fontsToScan.remove (0);
  256. }
  257. }
  258. // A rather hacky trick to select only the fixed-pitch fonts..
  259. // This is unfortunately a bit slow, but will work on all platforms.
  260. static bool isMonospacedTypeface (const String& name)
  261. {
  262. const Font font (name, 20.0f, Font::plain);
  263. const int width = font.getStringWidth ("....");
  264. return width == font.getStringWidth ("WWWW")
  265. && width == font.getStringWidth ("0000")
  266. && width == font.getStringWidth ("1111")
  267. && width == font.getStringWidth ("iiii");
  268. }
  269. StringArray fontsToScan, fontsFound;
  270. };
  271. //==============================================================================
  272. class EditorPanel : public Component,
  273. private ButtonListener
  274. {
  275. public:
  276. EditorPanel()
  277. : loadButton ("Load Scheme..."),
  278. saveButton ("Save Scheme...")
  279. {
  280. rebuildProperties();
  281. addAndMakeVisible (panel);
  282. loadButton.setColour (TextButton::buttonColourId, Colours::lightgrey.withAlpha (0.5f));
  283. saveButton.setColour (TextButton::buttonColourId, Colours::lightgrey.withAlpha (0.5f));
  284. loadButton.setColour (TextButton::textColourOffId, Colours::white);
  285. saveButton.setColour (TextButton::textColourOffId, Colours::white);
  286. addAndMakeVisible (loadButton);
  287. addAndMakeVisible (saveButton);
  288. loadButton.addListener (this);
  289. saveButton.addListener (this);
  290. }
  291. void rebuildProperties()
  292. {
  293. AppearanceSettings& scheme = getAppSettings().appearance;
  294. Array <PropertyComponent*> props;
  295. Value fontValue (scheme.getCodeFontValue());
  296. props.add (FontNameValueSource::createProperty ("Code Editor Font", fontValue));
  297. props.add (FontSizeValueSource::createProperty ("Font Size", fontValue));
  298. const StringArray colourNames (scheme.getColourNames());
  299. for (int i = 0; i < colourNames.size(); ++i)
  300. props.add (new ColourPropertyComponent (nullptr, colourNames[i],
  301. scheme.getColourValue (colourNames[i]),
  302. Colours::white, false));
  303. panel.clear();
  304. panel.addProperties (props);
  305. }
  306. void resized() override
  307. {
  308. Rectangle<int> r (getLocalBounds());
  309. panel.setBounds (r.removeFromTop (getHeight() - 28).reduced (4, 2));
  310. loadButton.setBounds (r.removeFromLeft (getWidth() / 2).reduced (10, 4));
  311. saveButton.setBounds (r.reduced (10, 3));
  312. }
  313. private:
  314. PropertyPanel panel;
  315. TextButton loadButton, saveButton;
  316. void buttonClicked (Button* b) override
  317. {
  318. if (b == &loadButton)
  319. loadScheme();
  320. else
  321. saveScheme();
  322. }
  323. void saveScheme()
  324. {
  325. FileChooser fc ("Select a file in which to save this colour-scheme...",
  326. getAppSettings().appearance.getSchemesFolder()
  327. .getNonexistentChildFile ("Scheme", AppearanceSettings::getSchemeFileSuffix()),
  328. AppearanceSettings::getSchemeFileWildCard());
  329. if (fc.browseForFileToSave (true))
  330. {
  331. File file (fc.getResult().withFileExtension (AppearanceSettings::getSchemeFileSuffix()));
  332. getAppSettings().appearance.writeToFile (file);
  333. getAppSettings().appearance.refreshPresetSchemeList();
  334. }
  335. }
  336. void loadScheme()
  337. {
  338. FileChooser fc ("Please select a colour-scheme file to load...",
  339. getAppSettings().appearance.getSchemesFolder(),
  340. AppearanceSettings::getSchemeFileWildCard());
  341. if (fc.browseForFileToOpen())
  342. {
  343. if (getAppSettings().appearance.readFromFile (fc.getResult()))
  344. rebuildProperties();
  345. }
  346. }
  347. JUCE_DECLARE_NON_COPYABLE (EditorPanel)
  348. };
  349. //==============================================================================
  350. class FontNameValueSource : public ValueSourceFilter
  351. {
  352. public:
  353. FontNameValueSource (const Value& source) : ValueSourceFilter (source) {}
  354. var getValue() const
  355. {
  356. return Font::fromString (sourceValue.toString()).getTypefaceName();
  357. }
  358. void setValue (const var& newValue)
  359. {
  360. Font font (Font::fromString (sourceValue.toString()));
  361. font.setTypefaceName (newValue.toString().isEmpty() ? Font::getDefaultMonospacedFontName()
  362. : newValue.toString());
  363. sourceValue = font.toString();
  364. }
  365. static ChoicePropertyComponent* createProperty (const String& title, const Value& value)
  366. {
  367. StringArray fontNames = getAppSettings().monospacedFontNames;
  368. Array<var> values;
  369. values.add (Font::getDefaultMonospacedFontName());
  370. values.add (var());
  371. for (int i = 0; i < fontNames.size(); ++i)
  372. values.add (fontNames[i]);
  373. StringArray names;
  374. names.add ("<Default Monospaced>");
  375. names.add (String::empty);
  376. names.addArray (getAppSettings().monospacedFontNames);
  377. return new ChoicePropertyComponent (Value (new FontNameValueSource (value)),
  378. title, names, values);
  379. }
  380. };
  381. //==============================================================================
  382. class FontSizeValueSource : public ValueSourceFilter
  383. {
  384. public:
  385. FontSizeValueSource (const Value& source) : ValueSourceFilter (source) {}
  386. var getValue() const override
  387. {
  388. return Font::fromString (sourceValue.toString()).getHeight();
  389. }
  390. void setValue (const var& newValue) override
  391. {
  392. sourceValue = Font::fromString (sourceValue.toString()).withHeight (newValue).toString();
  393. }
  394. static PropertyComponent* createProperty (const String& title, const Value& value)
  395. {
  396. return new SliderPropertyComponent (Value (new FontSizeValueSource (value)),
  397. title, 5.0, 40.0, 0.1, 0.5);
  398. }
  399. };
  400. };
  401. void AppearanceSettings::showEditorWindow (ScopedPointer<Component>& ownerPointer)
  402. {
  403. if (ownerPointer != nullptr)
  404. {
  405. ownerPointer->toFront (true);
  406. }
  407. else
  408. {
  409. Component* content;
  410. if (getAppSettings().monospacedFontNames.size() == 0)
  411. content = new AppearanceEditor::FontScanPanel();
  412. else
  413. content = new AppearanceEditor::EditorPanel();
  414. const int width = 350;
  415. new FloatingToolWindow ("Appearance Settings",
  416. "colourSchemeEditorPos",
  417. content, ownerPointer,
  418. width, 500,
  419. width, 200, width, 1000);
  420. }
  421. }
  422. //==============================================================================
  423. IntrojucerLookAndFeel::IntrojucerLookAndFeel()
  424. {
  425. setColour (mainBackgroundColourId, Colour::greyLevel (0.8f));
  426. }
  427. int IntrojucerLookAndFeel::getTabButtonBestWidth (TabBarButton&, int) { return 120; }
  428. static Colour getTabBackgroundColour (TabBarButton& button)
  429. {
  430. const Colour bkg (button.findColour (mainBackgroundColourId).contrasting (0.15f));
  431. if (button.isFrontTab())
  432. return bkg.overlaidWith (Colours::yellow.withAlpha (0.5f));
  433. return bkg;
  434. }
  435. void IntrojucerLookAndFeel::drawTabButton (TabBarButton& button, Graphics& g, bool isMouseOver, bool isMouseDown)
  436. {
  437. const Rectangle<int> activeArea (button.getActiveArea());
  438. const Colour bkg (getTabBackgroundColour (button));
  439. g.setGradientFill (ColourGradient (bkg.brighter (0.1f), 0, (float) activeArea.getY(),
  440. bkg.darker (0.1f), 0, (float) activeArea.getBottom(), false));
  441. g.fillRect (activeArea);
  442. g.setColour (button.findColour (mainBackgroundColourId).darker (0.3f));
  443. g.drawRect (activeArea);
  444. const float alpha = button.isEnabled() ? ((isMouseOver || isMouseDown) ? 1.0f : 0.8f) : 0.3f;
  445. const Colour col (bkg.contrasting().withMultipliedAlpha (alpha));
  446. TextLayout textLayout;
  447. LookAndFeel_V3::createTabTextLayout (button, (float) activeArea.getWidth(), (float) activeArea.getHeight(), col, textLayout);
  448. textLayout.draw (g, button.getTextArea().toFloat());
  449. }
  450. static Range<float> getBrightnessRange (const Image& im)
  451. {
  452. float minB = 1.0f, maxB = 0;
  453. const int w = im.getWidth();
  454. const int h = im.getHeight();
  455. for (int y = 0; y < h; ++y)
  456. {
  457. for (int x = 0; x < w; ++x)
  458. {
  459. const float b = im.getPixelAt (x, y).getBrightness();
  460. minB = jmin (minB, b);
  461. maxB = jmax (maxB, b);
  462. }
  463. }
  464. return Range<float> (minB, maxB);
  465. }
  466. void IntrojucerLookAndFeel::fillWithBackgroundTexture (Graphics& g)
  467. {
  468. const Colour bkg (findColour (mainBackgroundColourId));
  469. if (backgroundTextureBaseColour != bkg)
  470. {
  471. backgroundTextureBaseColour = bkg;
  472. const Image original (ImageCache::getFromMemory (BinaryData::background_tile_png,
  473. BinaryData::background_tile_pngSize));
  474. const int w = original.getWidth();
  475. const int h = original.getHeight();
  476. backgroundTexture = Image (Image::RGB, w, h, false);
  477. const Range<float> brightnessRange (getBrightnessRange (original));
  478. const float brightnessOffset = (brightnessRange.getStart() + brightnessRange.getEnd()) / 2.0f;
  479. const float brightnessScale = 0.025f / brightnessRange.getLength();
  480. const float bkgB = bkg.getBrightness();
  481. for (int y = 0; y < h; ++y)
  482. {
  483. for (int x = 0; x < w; ++x)
  484. {
  485. const float b = (original.getPixelAt (x, y).getBrightness() - brightnessOffset) * brightnessScale;
  486. backgroundTexture.setPixelAt (x, y, bkg.withBrightness (jlimit (0.0f, 1.0f, bkgB + b)));
  487. }
  488. }
  489. }
  490. g.setTiledImageFill (backgroundTexture, 0, 0, 1.0f);
  491. g.fillAll();
  492. }
  493. void IntrojucerLookAndFeel::fillWithBackgroundTexture (Component& c, Graphics& g)
  494. {
  495. dynamic_cast<IntrojucerLookAndFeel&> (c.getLookAndFeel()).fillWithBackgroundTexture (g);
  496. }