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.

647 lines
22KB

  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. if (! IntrojucerApp::getApp().isRunningCommandLine)
  49. {
  50. IntrojucerLookAndFeel lf;
  51. for (int i = 0; i < AppearanceColours::numColours; ++i)
  52. getColourValue (AppearanceColours::colours[i].name) = lf.findColour (AppearanceColours::colours[i].colourID).toString();
  53. CodeDocument doc;
  54. CPlusPlusCodeTokeniser tokeniser;
  55. CodeEditorComponent editor (doc, &tokeniser);
  56. const CodeEditorComponent::ColourScheme cs (editor.getColourScheme());
  57. for (int i = cs.types.size(); --i >= 0;)
  58. {
  59. CodeEditorComponent::ColourScheme::TokenType& t = cs.types.getReference(i);
  60. getColourValue (t.name) = t.colour.toString();
  61. }
  62. getCodeFontValue() = getDefaultCodeFont().toString();
  63. if (updateAppWhenChanged)
  64. settings.addListener (this);
  65. }
  66. }
  67. File AppearanceSettings::getSchemesFolder()
  68. {
  69. File f (getGlobalProperties().getFile().getSiblingFile ("Schemes"));
  70. f.createDirectory();
  71. return f;
  72. }
  73. void AppearanceSettings::writeDefaultSchemeFile (const String& xmlString, const String& name)
  74. {
  75. const File file (getSchemesFolder().getChildFile (name).withFileExtension (getSchemeFileSuffix()));
  76. AppearanceSettings settings (false);
  77. ScopedPointer<XmlElement> xml (XmlDocument::parse (xmlString));
  78. if (xml != nullptr)
  79. settings.readFromXML (*xml);
  80. settings.writeToFile (file);
  81. }
  82. void AppearanceSettings::refreshPresetSchemeList()
  83. {
  84. writeDefaultSchemeFile (BinaryData::colourscheme_dark_xml, "Default (Dark)");
  85. writeDefaultSchemeFile (BinaryData::colourscheme_light_xml, "Default (Light)");
  86. Array<File> newSchemes;
  87. getSchemesFolder().findChildFiles (newSchemes, File::findFiles, false, String ("*") + getSchemeFileSuffix());
  88. if (newSchemes != presetSchemeFiles)
  89. {
  90. presetSchemeFiles.swapWith (newSchemes);
  91. IntrojucerApp::getCommandManager().commandStatusChanged();
  92. }
  93. }
  94. StringArray AppearanceSettings::getPresetSchemes()
  95. {
  96. StringArray s;
  97. for (int i = 0; i < presetSchemeFiles.size(); ++i)
  98. s.add (presetSchemeFiles.getReference(i).getFileNameWithoutExtension());
  99. return s;
  100. }
  101. void AppearanceSettings::selectPresetScheme (int index)
  102. {
  103. readFromFile (presetSchemeFiles [index]);
  104. }
  105. bool AppearanceSettings::readFromXML (const XmlElement& xml)
  106. {
  107. if (xml.hasTagName (settings.getType().toString()))
  108. {
  109. const ValueTree newSettings (ValueTree::fromXml (xml));
  110. // we'll manually copy across the new properties to the existing tree so that
  111. // any open editors will be kept up to date..
  112. settings.copyPropertiesFrom (newSettings, nullptr);
  113. for (int i = settings.getNumChildren(); --i >= 0;)
  114. {
  115. ValueTree c (settings.getChild (i));
  116. const ValueTree newValue (newSettings.getChildWithProperty (Ids::name, c.getProperty (Ids::name)));
  117. if (newValue.isValid())
  118. c.copyPropertiesFrom (newValue, nullptr);
  119. }
  120. return true;
  121. }
  122. return false;
  123. }
  124. bool AppearanceSettings::readFromFile (const File& file)
  125. {
  126. const ScopedPointer<XmlElement> xml (XmlDocument::parse (file));
  127. return xml != nullptr && readFromXML (*xml);
  128. }
  129. bool AppearanceSettings::writeToFile (const File& file) const
  130. {
  131. const ScopedPointer<XmlElement> xml (settings.createXml());
  132. return xml != nullptr && xml->writeToFile (file, String::empty);
  133. }
  134. Font AppearanceSettings::getDefaultCodeFont()
  135. {
  136. return Font (Font::getDefaultMonospacedFontName(), Font::getDefaultStyle(), 13.0f);
  137. }
  138. StringArray AppearanceSettings::getColourNames() const
  139. {
  140. StringArray s;
  141. for (int i = 0; i < settings.getNumChildren(); ++i)
  142. {
  143. const ValueTree c (settings.getChild(i));
  144. if (c.hasType ("COLOUR"))
  145. s.add (c [Ids::name]);
  146. }
  147. return s;
  148. }
  149. void AppearanceSettings::updateColourScheme()
  150. {
  151. applyToLookAndFeel (LookAndFeel::getDefaultLookAndFeel());
  152. IntrojucerApp::getApp().mainWindowList.sendLookAndFeelChange();
  153. }
  154. void AppearanceSettings::applyToLookAndFeel (LookAndFeel& lf) const
  155. {
  156. for (int i = 0; i < AppearanceColours::numColours; ++i)
  157. {
  158. Colour col;
  159. if (getColour (AppearanceColours::colours[i].name, col))
  160. {
  161. if (AppearanceColours::colours[i].mustBeOpaque)
  162. col = Colours::white.overlaidWith (col);
  163. if (! AppearanceColours::colours[i].applyToEditorOnly)
  164. lf.setColour (AppearanceColours::colours[i].colourID, col);
  165. }
  166. }
  167. lf.setColour (ScrollBar::thumbColourId, lf.findColour (mainBackgroundColourId).contrasting().withAlpha (0.13f));
  168. }
  169. void AppearanceSettings::applyToCodeEditor (CodeEditorComponent& editor) const
  170. {
  171. CodeEditorComponent::ColourScheme cs (editor.getColourScheme());
  172. for (int i = cs.types.size(); --i >= 0;)
  173. {
  174. CodeEditorComponent::ColourScheme::TokenType& t = cs.types.getReference(i);
  175. getColour (t.name, t.colour);
  176. }
  177. editor.setColourScheme (cs);
  178. editor.setFont (getCodeFont());
  179. for (int i = 0; i < AppearanceColours::numColours; ++i)
  180. {
  181. if (AppearanceColours::colours[i].applyToEditorOnly)
  182. {
  183. Colour col;
  184. if (getColour (AppearanceColours::colours[i].name, col))
  185. editor.setColour (AppearanceColours::colours[i].colourID, col);
  186. }
  187. }
  188. editor.setColour (ScrollBar::thumbColourId, editor.findColour (CodeEditorComponent::backgroundColourId)
  189. .contrasting()
  190. .withAlpha (0.13f));
  191. }
  192. Font AppearanceSettings::getCodeFont() const
  193. {
  194. const String fontString (settings [Ids::font].toString());
  195. if (fontString.isEmpty())
  196. return getDefaultCodeFont();
  197. return Font::fromString (fontString);
  198. }
  199. Value AppearanceSettings::getCodeFontValue()
  200. {
  201. return settings.getPropertyAsValue (Ids::font, nullptr);
  202. }
  203. Value AppearanceSettings::getColourValue (const String& colourName)
  204. {
  205. ValueTree c (settings.getChildWithProperty (Ids::name, colourName));
  206. if (! c.isValid())
  207. {
  208. c = ValueTree ("COLOUR");
  209. c.setProperty (Ids::name, colourName, nullptr);
  210. settings.addChild (c, -1, nullptr);
  211. }
  212. return c.getPropertyAsValue (Ids::colour, nullptr);
  213. }
  214. bool AppearanceSettings::getColour (const String& name, Colour& result) const
  215. {
  216. const ValueTree colour (settings.getChildWithProperty (Ids::name, name));
  217. if (colour.isValid())
  218. {
  219. result = Colour::fromString (colour [Ids::colour].toString());
  220. return true;
  221. }
  222. return false;
  223. }
  224. //==============================================================================
  225. struct AppearanceEditor
  226. {
  227. class FontScanPanel : public Component,
  228. private Timer
  229. {
  230. public:
  231. FontScanPanel()
  232. {
  233. fontsToScan = Font::findAllTypefaceNames();
  234. startTimer (1);
  235. }
  236. void paint (Graphics& g) override
  237. {
  238. g.fillAll (Colours::darkgrey);
  239. g.setFont (14.0f);
  240. g.setColour (Colours::white);
  241. g.drawFittedText ("Scanning for fonts..", getLocalBounds(), Justification::centred, 2);
  242. const int size = 30;
  243. getLookAndFeel().drawSpinningWaitAnimation (g, Colours::white, (getWidth() - size) / 2, getHeight() / 2 - 50, size, size);
  244. }
  245. void timerCallback() override
  246. {
  247. repaint();
  248. if (fontsToScan.size() == 0)
  249. {
  250. getAppSettings().monospacedFontNames = fontsFound;
  251. if (DialogWindow* w = findParentComponentOfClass<DialogWindow>())
  252. w->setContentOwned (new EditorPanel(), false);
  253. }
  254. else
  255. {
  256. if (isMonospacedTypeface (fontsToScan[0]))
  257. fontsFound.add (fontsToScan[0]);
  258. fontsToScan.remove (0);
  259. }
  260. }
  261. // A rather hacky trick to select only the fixed-pitch fonts..
  262. // This is unfortunately a bit slow, but will work on all platforms.
  263. static bool isMonospacedTypeface (const String& name)
  264. {
  265. const Font font (name, 20.0f, Font::plain);
  266. const int width = font.getStringWidth ("....");
  267. return width == font.getStringWidth ("WWWW")
  268. && width == font.getStringWidth ("0000")
  269. && width == font.getStringWidth ("1111")
  270. && width == font.getStringWidth ("iiii");
  271. }
  272. StringArray fontsToScan, fontsFound;
  273. };
  274. //==============================================================================
  275. class EditorPanel : public Component,
  276. private ButtonListener
  277. {
  278. public:
  279. EditorPanel()
  280. : loadButton ("Load Scheme..."),
  281. saveButton ("Save Scheme...")
  282. {
  283. rebuildProperties();
  284. addAndMakeVisible (panel);
  285. loadButton.setColour (TextButton::buttonColourId, Colours::lightgrey.withAlpha (0.5f));
  286. saveButton.setColour (TextButton::buttonColourId, Colours::lightgrey.withAlpha (0.5f));
  287. loadButton.setColour (TextButton::textColourOffId, Colours::white);
  288. saveButton.setColour (TextButton::textColourOffId, Colours::white);
  289. addAndMakeVisible (loadButton);
  290. addAndMakeVisible (saveButton);
  291. loadButton.addListener (this);
  292. saveButton.addListener (this);
  293. }
  294. void rebuildProperties()
  295. {
  296. AppearanceSettings& scheme = getAppSettings().appearance;
  297. Array <PropertyComponent*> props;
  298. Value fontValue (scheme.getCodeFontValue());
  299. props.add (FontNameValueSource::createProperty ("Code Editor Font", fontValue));
  300. props.add (FontSizeValueSource::createProperty ("Font Size", fontValue));
  301. const StringArray colourNames (scheme.getColourNames());
  302. for (int i = 0; i < colourNames.size(); ++i)
  303. props.add (new ColourPropertyComponent (nullptr, colourNames[i],
  304. scheme.getColourValue (colourNames[i]),
  305. Colours::white, false));
  306. panel.clear();
  307. panel.addProperties (props);
  308. }
  309. void resized() override
  310. {
  311. Rectangle<int> r (getLocalBounds());
  312. panel.setBounds (r.removeFromTop (getHeight() - 28).reduced (4, 2));
  313. loadButton.setBounds (r.removeFromLeft (getWidth() / 2).reduced (10, 4));
  314. saveButton.setBounds (r.reduced (10, 3));
  315. }
  316. private:
  317. PropertyPanel panel;
  318. TextButton loadButton, saveButton;
  319. void buttonClicked (Button* b) override
  320. {
  321. if (b == &loadButton)
  322. loadScheme();
  323. else
  324. saveScheme();
  325. }
  326. void saveScheme()
  327. {
  328. FileChooser fc ("Select a file in which to save this colour-scheme...",
  329. getAppSettings().appearance.getSchemesFolder()
  330. .getNonexistentChildFile ("Scheme", AppearanceSettings::getSchemeFileSuffix()),
  331. AppearanceSettings::getSchemeFileWildCard());
  332. if (fc.browseForFileToSave (true))
  333. {
  334. File file (fc.getResult().withFileExtension (AppearanceSettings::getSchemeFileSuffix()));
  335. getAppSettings().appearance.writeToFile (file);
  336. getAppSettings().appearance.refreshPresetSchemeList();
  337. }
  338. }
  339. void loadScheme()
  340. {
  341. FileChooser fc ("Please select a colour-scheme file to load...",
  342. getAppSettings().appearance.getSchemesFolder(),
  343. AppearanceSettings::getSchemeFileWildCard());
  344. if (fc.browseForFileToOpen())
  345. if (getAppSettings().appearance.readFromFile (fc.getResult()))
  346. rebuildProperties();
  347. }
  348. JUCE_DECLARE_NON_COPYABLE (EditorPanel)
  349. };
  350. //==============================================================================
  351. class FontNameValueSource : public ValueSourceFilter
  352. {
  353. public:
  354. FontNameValueSource (const Value& source) : ValueSourceFilter (source) {}
  355. var getValue() const override
  356. {
  357. return Font::fromString (sourceValue.toString()).getTypefaceName();
  358. }
  359. void setValue (const var& newValue) override
  360. {
  361. Font font (Font::fromString (sourceValue.toString()));
  362. font.setTypefaceName (newValue.toString().isEmpty() ? Font::getDefaultMonospacedFontName()
  363. : newValue.toString());
  364. sourceValue = font.toString();
  365. }
  366. static ChoicePropertyComponent* createProperty (const String& title, const Value& value)
  367. {
  368. StringArray fontNames = getAppSettings().monospacedFontNames;
  369. Array<var> values;
  370. values.add (Font::getDefaultMonospacedFontName());
  371. values.add (var());
  372. for (int i = 0; i < fontNames.size(); ++i)
  373. values.add (fontNames[i]);
  374. StringArray names;
  375. names.add ("<Default Monospaced>");
  376. names.add (String::empty);
  377. names.addArray (getAppSettings().monospacedFontNames);
  378. return new ChoicePropertyComponent (Value (new FontNameValueSource (value)),
  379. title, names, values);
  380. }
  381. };
  382. //==============================================================================
  383. class FontSizeValueSource : public ValueSourceFilter
  384. {
  385. public:
  386. FontSizeValueSource (const Value& source) : ValueSourceFilter (source) {}
  387. var getValue() const override
  388. {
  389. return Font::fromString (sourceValue.toString()).getHeight();
  390. }
  391. void setValue (const var& newValue) override
  392. {
  393. sourceValue = Font::fromString (sourceValue.toString()).withHeight (newValue).toString();
  394. }
  395. static PropertyComponent* createProperty (const String& title, const Value& value)
  396. {
  397. return new SliderPropertyComponent (Value (new FontSizeValueSource (value)),
  398. title, 5.0, 40.0, 0.1, 0.5);
  399. }
  400. };
  401. };
  402. void AppearanceSettings::showEditorWindow (ScopedPointer<Component>& ownerPointer)
  403. {
  404. if (ownerPointer != nullptr)
  405. {
  406. ownerPointer->toFront (true);
  407. }
  408. else
  409. {
  410. Component* content;
  411. if (getAppSettings().monospacedFontNames.size() == 0)
  412. content = new AppearanceEditor::FontScanPanel();
  413. else
  414. content = new AppearanceEditor::EditorPanel();
  415. const int width = 350;
  416. new FloatingToolWindow ("Appearance Settings",
  417. "colourSchemeEditorPos",
  418. content, ownerPointer,
  419. width, 500,
  420. width, 200, width, 1000);
  421. }
  422. }
  423. //==============================================================================
  424. IntrojucerLookAndFeel::IntrojucerLookAndFeel()
  425. {
  426. setColour (mainBackgroundColourId, Colour::greyLevel (0.8f));
  427. }
  428. int IntrojucerLookAndFeel::getTabButtonBestWidth (TabBarButton&, int) { return 120; }
  429. Colour IntrojucerLookAndFeel::getTabBackgroundColour (TabBarButton& button)
  430. {
  431. const Colour bkg (button.findColour (mainBackgroundColourId).contrasting (0.15f));
  432. if (button.isFrontTab())
  433. return bkg.overlaidWith (Colours::yellow.withAlpha (0.5f));
  434. return bkg;
  435. }
  436. void IntrojucerLookAndFeel::drawTabButton (TabBarButton& button, Graphics& g, bool isMouseOver, bool isMouseDown)
  437. {
  438. const Rectangle<int> activeArea (button.getActiveArea());
  439. const Colour bkg (getTabBackgroundColour (button));
  440. g.setGradientFill (ColourGradient (bkg.brighter (0.1f), 0, (float) activeArea.getY(),
  441. bkg.darker (0.1f), 0, (float) activeArea.getBottom(), false));
  442. g.fillRect (activeArea);
  443. g.setColour (button.findColour (mainBackgroundColourId).darker (0.3f));
  444. g.drawRect (activeArea);
  445. const float alpha = button.isEnabled() ? ((isMouseOver || isMouseDown) ? 1.0f : 0.8f) : 0.3f;
  446. const Colour col (bkg.contrasting().withMultipliedAlpha (alpha));
  447. TextLayout textLayout;
  448. LookAndFeel_V3::createTabTextLayout (button, (float) activeArea.getWidth(), (float) activeArea.getHeight(), col, textLayout);
  449. textLayout.draw (g, button.getTextArea().toFloat());
  450. }
  451. void IntrojucerLookAndFeel::drawConcertinaPanelHeader (Graphics& g, const Rectangle<int>& area,
  452. bool isMouseOver, bool /*isMouseDown*/,
  453. ConcertinaPanel&, Component& panel)
  454. {
  455. const Colour bkg (Colours::grey);
  456. g.setGradientFill (ColourGradient (Colour::greyLevel (isMouseOver ? 0.6f : 0.5f), 0, (float) area.getY(),
  457. Colour::greyLevel (0.4f), 0, (float) area.getBottom(), false));
  458. g.fillAll();
  459. g.setColour (bkg.contrasting().withAlpha (0.1f));
  460. g.fillRect (area.withHeight (1));
  461. g.fillRect (area.withTop (area.getBottom() - 1));
  462. g.setColour (bkg.contrasting());
  463. g.setFont (Font (area.getHeight() * 0.6f).boldened());
  464. g.drawFittedText (panel.getName(), 4, 0, area.getWidth() - 6, area.getHeight(), Justification::centredLeft, 1);
  465. }
  466. static Range<float> getBrightnessRange (const Image& im)
  467. {
  468. float minB = 1.0f, maxB = 0;
  469. const int w = im.getWidth();
  470. const int h = im.getHeight();
  471. for (int y = 0; y < h; ++y)
  472. {
  473. for (int x = 0; x < w; ++x)
  474. {
  475. const float b = im.getPixelAt (x, y).getBrightness();
  476. minB = jmin (minB, b);
  477. maxB = jmax (maxB, b);
  478. }
  479. }
  480. return Range<float> (minB, maxB);
  481. }
  482. void IntrojucerLookAndFeel::fillWithBackgroundTexture (Graphics& g)
  483. {
  484. const Colour bkg (findColour (mainBackgroundColourId));
  485. if (backgroundTextureBaseColour != bkg)
  486. {
  487. backgroundTextureBaseColour = bkg;
  488. const Image original (ImageCache::getFromMemory (BinaryData::background_tile_png,
  489. BinaryData::background_tile_pngSize));
  490. const int w = original.getWidth();
  491. const int h = original.getHeight();
  492. backgroundTexture = Image (Image::RGB, w, h, false);
  493. const Range<float> brightnessRange (getBrightnessRange (original));
  494. const float brightnessOffset = (brightnessRange.getStart() + brightnessRange.getEnd()) / 2.0f;
  495. const float brightnessScale = 0.025f / brightnessRange.getLength();
  496. const float bkgB = bkg.getBrightness();
  497. for (int y = 0; y < h; ++y)
  498. {
  499. for (int x = 0; x < w; ++x)
  500. {
  501. const float b = (original.getPixelAt (x, y).getBrightness() - brightnessOffset) * brightnessScale;
  502. backgroundTexture.setPixelAt (x, y, bkg.withBrightness (jlimit (0.0f, 1.0f, bkgB + b)));
  503. }
  504. }
  505. }
  506. g.setTiledImageFill (backgroundTexture, 0, 0, 1.0f);
  507. g.fillAll();
  508. }
  509. void IntrojucerLookAndFeel::fillWithBackgroundTexture (Component& c, Graphics& g)
  510. {
  511. dynamic_cast<IntrojucerLookAndFeel&> (c.getLookAndFeel()).fillWithBackgroundTexture (g);
  512. }