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.

542 lines
19KB

  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. class ModulesPanel : public Component,
  18. private TableListBoxModel,
  19. private ValueTree::Listener,
  20. private Button::Listener
  21. {
  22. public:
  23. ModulesPanel (Project& p)
  24. : project (p),
  25. modulesValueTree (p.getModules().state),
  26. addWebModuleButton ("Download and add a module..."),
  27. updateModuleButton ("Install updates to modules..."),
  28. setCopyModeButton ("Set copy-mode for all modules..."),
  29. copyPathButton ("Set paths for all modules...")
  30. {
  31. table.getHeader().addColumn ("Module", nameCol, 180, 100, 400, TableHeaderComponent::notSortable);
  32. table.getHeader().addColumn ("Installed Version", versionCol, 100, 100, 100, TableHeaderComponent::notSortable);
  33. table.getHeader().addColumn ("Available Version", updateCol, 100, 100, 100, TableHeaderComponent::notSortable);
  34. table.getHeader().addColumn ("Make Local Copy", copyCol, 100, 100, 100, TableHeaderComponent::notSortable);
  35. table.getHeader().addColumn ("Paths", pathCol, 250, 100, 600, TableHeaderComponent::notSortable);
  36. table.setModel (this);
  37. table.setColour (TableListBox::backgroundColourId, Colours::transparentBlack);
  38. addAndMakeVisible (table);
  39. table.updateContent();
  40. table.setRowHeight (20);
  41. addAndMakeVisible (addWebModuleButton);
  42. addAndMakeVisible (updateModuleButton);
  43. addAndMakeVisible (setCopyModeButton);
  44. addAndMakeVisible (copyPathButton);
  45. addWebModuleButton.addListener (this);
  46. updateModuleButton.addListener (this);
  47. updateModuleButton.setEnabled (false);
  48. setCopyModeButton.addListener (this);
  49. setCopyModeButton.setTriggeredOnMouseDown (true);
  50. copyPathButton.addListener (this);
  51. copyPathButton.setTriggeredOnMouseDown (true);
  52. modulesValueTree.addListener (this);
  53. lookAndFeelChanged();
  54. }
  55. void paint (Graphics& g) override
  56. {
  57. if (webUpdateThread == nullptr)
  58. webUpdateThread = new WebsiteUpdateFetchThread (*this);
  59. IntrojucerLookAndFeel::fillWithBackgroundTexture (*this, g);
  60. }
  61. void resized() override
  62. {
  63. Rectangle<int> r (getLocalBounds().reduced (5, 4));
  64. table.setBounds (r.removeFromTop (table.getRowPosition (getNumRows() - 1, true).getBottom() + 20));
  65. Rectangle<int> buttonRow (r.removeFromTop (32).removeFromBottom (28));
  66. addWebModuleButton.setBounds (buttonRow.removeFromLeft (jmin (260, r.getWidth() / 3)));
  67. buttonRow.removeFromLeft (8);
  68. updateModuleButton.setBounds (buttonRow.removeFromLeft (jmin (260, r.getWidth() / 3)));
  69. buttonRow.removeFromLeft (8);
  70. buttonRow = r.removeFromTop (34).removeFromBottom (28);
  71. setCopyModeButton.setBounds (buttonRow.removeFromLeft (jmin (260, r.getWidth() / 3)));
  72. buttonRow.removeFromLeft (8);
  73. copyPathButton.setBounds (buttonRow.removeFromLeft (jmin (260, r.getWidth() / 3)));
  74. }
  75. int getNumRows() override
  76. {
  77. return project.getModules().getNumModules();
  78. }
  79. void paintRowBackground (Graphics& g, int /*rowNumber*/, int width, int height, bool rowIsSelected) override
  80. {
  81. g.setColour (rowIsSelected ? Colours::lightblue.withAlpha (0.4f)
  82. : Colours::white.withAlpha (0.4f));
  83. g.fillRect (0, 0, width, height - 1);
  84. }
  85. void paintCell (Graphics& g, int rowNumber, int columnId, int width, int height, bool /*rowIsSelected*/) override
  86. {
  87. String text;
  88. const String moduleID (project.getModules().getModuleID (rowNumber));
  89. if (columnId == nameCol)
  90. {
  91. text = moduleID;
  92. }
  93. else if (columnId == versionCol)
  94. {
  95. text = project.getModules().getModuleInfo (moduleID).getVersion();
  96. if (text.isEmpty())
  97. text = "?";
  98. }
  99. else if (columnId == updateCol)
  100. {
  101. if (listFromWebsite != nullptr)
  102. {
  103. if (const ModuleDescription* m = listFromWebsite->getModuleWithID (moduleID))
  104. {
  105. if (m->getVersion() != project.getModules().getModuleInfo (moduleID).getVersion())
  106. text = m->getVersion() + " available";
  107. else
  108. text = "Up-to-date";
  109. }
  110. else
  111. text = "?";
  112. }
  113. else
  114. {
  115. text = "-";
  116. }
  117. }
  118. else if (columnId == copyCol)
  119. {
  120. text = project.getModules().shouldCopyModuleFilesLocally (moduleID).getValue()
  121. ? "Yes" : "No";
  122. }
  123. else if (columnId == pathCol)
  124. {
  125. StringArray paths;
  126. for (Project::ExporterIterator exporter (project); exporter.next();)
  127. paths.addIfNotAlreadyThere (exporter->getPathForModuleString (moduleID).trim());
  128. text = paths.joinIntoString (", ");
  129. }
  130. g.setColour (Colours::black);
  131. g.setFont (height * 0.65f);
  132. g.drawText (text, Rectangle<int> (width, height).reduced (4, 0), Justification::centredLeft, true);
  133. }
  134. void cellDoubleClicked (int rowNumber, int, const MouseEvent&) override
  135. {
  136. const String moduleID (project.getModules().getModuleID (rowNumber));
  137. if (moduleID.isNotEmpty())
  138. if (ProjectContentComponent* pcc = findParentComponentOfClass<ProjectContentComponent>())
  139. pcc->showModule (moduleID);
  140. }
  141. void deleteKeyPressed (int row) override
  142. {
  143. project.getModules().removeModule (project.getModules().getModuleID (row));
  144. }
  145. void webUpdateFinished (const ModuleList& newList)
  146. {
  147. listFromWebsite = new ModuleList (newList);
  148. table.updateContent();
  149. table.repaint();
  150. updateModuleButton.setEnabled (getUpdatableModules().size() != 0);
  151. }
  152. void buttonClicked (Button* b)
  153. {
  154. if (b == &addWebModuleButton) showAddModuleMenu();
  155. else if (b == &updateModuleButton) showUpdateModulesMenu();
  156. else if (b == &setCopyModeButton) showCopyModeMenu();
  157. else if (b == &copyPathButton) showSetPathsMenu();
  158. }
  159. private:
  160. enum
  161. {
  162. nameCol = 1,
  163. versionCol,
  164. updateCol,
  165. copyCol,
  166. pathCol
  167. };
  168. Project& project;
  169. ValueTree modulesValueTree;
  170. TableListBox table;
  171. TextButton addWebModuleButton, updateModuleButton, setCopyModeButton, copyPathButton;
  172. ScopedPointer<ModuleList> listFromWebsite;
  173. void valueTreePropertyChanged (ValueTree&, const Identifier&) override { itemChanged(); }
  174. void valueTreeChildAdded (ValueTree&, ValueTree&) override { itemChanged(); }
  175. void valueTreeChildRemoved (ValueTree&, ValueTree&) override { itemChanged(); }
  176. void valueTreeChildOrderChanged (ValueTree&) override { itemChanged(); }
  177. void valueTreeParentChanged (ValueTree&) override { itemChanged(); }
  178. void itemChanged()
  179. {
  180. table.updateContent();
  181. resized();
  182. repaint();
  183. }
  184. StringArray getUpdatableModules() const
  185. {
  186. StringArray result;
  187. if (listFromWebsite != nullptr)
  188. {
  189. for (int i = 0; i < listFromWebsite->modules.size(); ++i)
  190. {
  191. const ModuleDescription* m = listFromWebsite->modules.getUnchecked(i);
  192. const String v1 (m->getVersion());
  193. const String v2 (project.getModules().getModuleInfo (m->getID()).getVersion());
  194. if (v1 != v2 && v1.isNotEmpty() && v2.isNotEmpty())
  195. result.add (m->getID());
  196. }
  197. }
  198. return result;
  199. }
  200. StringArray getAddableModules() const
  201. {
  202. StringArray result;
  203. if (listFromWebsite != nullptr)
  204. {
  205. for (int i = 0; i < listFromWebsite->modules.size(); ++i)
  206. {
  207. const ModuleDescription* m = listFromWebsite->modules.getUnchecked(i);
  208. if (! project.getModules().isModuleEnabled (m->getID()))
  209. result.add (m->getID());
  210. }
  211. }
  212. return result;
  213. }
  214. void showUpdateModulesMenu()
  215. {
  216. StringArray mods (getUpdatableModules());
  217. PopupMenu m;
  218. m.addItem (1000, "Update all modules");
  219. m.addSeparator();
  220. for (int i = 0; i < mods.size(); ++i)
  221. m.addItem (1 + i, "Update " + mods[i]);
  222. int res = m.showAt (&updateModuleButton);
  223. if (res > 0 && listFromWebsite != nullptr)
  224. {
  225. if (res != 1000)
  226. mods = StringArray (mods[res - 1]);
  227. Array<ModuleDescription> modsToUpdate;
  228. for (int i = 0; i < mods.size(); ++i)
  229. {
  230. if (const ModuleDescription* md = listFromWebsite->getModuleWithID (mods[i]))
  231. {
  232. ModuleDescription modToUpdate (*md);
  233. modToUpdate.manifestFile = project.getModules().getModuleInfo (modToUpdate.getID()).manifestFile;
  234. modsToUpdate.add (modToUpdate);
  235. }
  236. }
  237. DownloadAndInstallThread::updateModulesFromWeb (project, modsToUpdate);
  238. }
  239. }
  240. void showAddModuleMenu()
  241. {
  242. if (listFromWebsite == nullptr)
  243. {
  244. AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon,
  245. "Couldn't contact the website!",
  246. "Failed to get the latest module list from juce.com - "
  247. "maybe network or server problems - try again soon!");
  248. return;
  249. }
  250. StringArray mods (getAddableModules());
  251. if (mods.size() == 0)
  252. {
  253. AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon,
  254. "No modules to add!",
  255. "Couldn't find any new modules that aren't already in your project!");
  256. return;
  257. }
  258. PopupMenu m;
  259. for (int i = 0; i < mods.size(); ++i)
  260. m.addItem (i + 1, "Install " + mods[i]);
  261. int res = m.showAt (&addWebModuleButton);
  262. if (res > 0 && listFromWebsite != nullptr)
  263. if (const ModuleDescription* md = listFromWebsite->getModuleWithID (mods[res - 1]))
  264. DownloadAndInstallThread::addModuleFromWebsite (project, *md);
  265. }
  266. void showCopyModeMenu()
  267. {
  268. PopupMenu m;
  269. m.addItem (1, "Set all modules to copy locally");
  270. m.addItem (2, "Set all modules to not copy locally");
  271. int res = m.showAt (&setCopyModeButton);
  272. if (res != 0)
  273. project.getModules().setLocalCopyModeForAllModules (res == 1);
  274. }
  275. void showSetPathsMenu()
  276. {
  277. EnabledModuleList& moduleList = project.getModules();
  278. const String moduleToCopy (moduleList.getModuleID (table.getSelectedRow()));
  279. if (moduleToCopy.isNotEmpty())
  280. {
  281. PopupMenu m;
  282. m.addItem (1, "Copy the paths from the module '" + moduleToCopy + "' to all other modules");
  283. int res = m.showAt (&copyPathButton);
  284. if (res != 0)
  285. {
  286. for (Project::ExporterIterator exporter (project); exporter.next();)
  287. {
  288. for (int i = 0; i < moduleList.getNumModules(); ++i)
  289. {
  290. String modID = moduleList.getModuleID (i);
  291. if (modID != moduleToCopy)
  292. exporter->getPathForModuleValue (modID) = exporter->getPathForModuleValue (moduleToCopy).getValue();
  293. }
  294. }
  295. }
  296. table.repaint();
  297. }
  298. else
  299. {
  300. PopupMenu m;
  301. m.addItem (1, "Copy the paths from the selected module to all other modules", false);
  302. m.showAt (&copyPathButton);
  303. }
  304. }
  305. struct WebsiteUpdateFetchThread : private Thread,
  306. private AsyncUpdater
  307. {
  308. WebsiteUpdateFetchThread (ModulesPanel& p) : Thread ("Web Updater"), panel (p)
  309. {
  310. startThread (3);
  311. }
  312. ~WebsiteUpdateFetchThread()
  313. {
  314. stopThread (15000);
  315. }
  316. void run() override
  317. {
  318. static Time lastDownloadTime;
  319. static ModuleList lastList;
  320. if (Time::getCurrentTime() < lastDownloadTime + RelativeTime::minutes (2.0))
  321. {
  322. list = lastList;
  323. triggerAsyncUpdate();
  324. }
  325. else
  326. {
  327. if (list.loadFromWebsite() && ! threadShouldExit())
  328. {
  329. lastList = list;
  330. lastDownloadTime = Time::getCurrentTime();
  331. triggerAsyncUpdate();
  332. }
  333. }
  334. }
  335. void handleAsyncUpdate() override
  336. {
  337. panel.webUpdateFinished (list);
  338. }
  339. private:
  340. ModuleList list;
  341. ModulesPanel& panel;
  342. };
  343. ScopedPointer<WebsiteUpdateFetchThread> webUpdateThread;
  344. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ModulesPanel)
  345. };
  346. //==============================================================================
  347. class DownloadAndInstallThread : public ThreadWithProgressWindow
  348. {
  349. public:
  350. DownloadAndInstallThread (const Array<ModuleDescription>& modulesToInstall)
  351. : ThreadWithProgressWindow ("Installing New Modules", true, true),
  352. result (Result::ok()),
  353. modules (modulesToInstall)
  354. {
  355. }
  356. static void updateModulesFromWeb (Project& project, const Array<ModuleDescription>& mods)
  357. {
  358. DownloadAndInstallThread d (mods);
  359. if (d.runThread())
  360. {
  361. if (d.result.failed())
  362. {
  363. AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon,
  364. "Module Install Failed",
  365. d.result.getErrorMessage());
  366. }
  367. else
  368. {
  369. for (int i = 0; i < d.modules.size(); ++i)
  370. project.getModules().addModule (d.modules.getReference(i).manifestFile,
  371. project.getModules().areMostModulesCopiedLocally());
  372. }
  373. }
  374. }
  375. static void addModuleFromWebsite (Project& project, const ModuleDescription& module)
  376. {
  377. Array<ModuleDescription> mods;
  378. mods.add (module);
  379. static File lastLocation (EnabledModuleList::findDefaultModulesFolder (project));
  380. FileChooser fc ("Select the parent folder for the new module...", lastLocation, String::empty, false);
  381. if (fc.browseForDirectory())
  382. {
  383. lastLocation = fc.getResult();
  384. if (lastLocation.getChildFile (ModuleDescription::getManifestFileName()).exists())
  385. {
  386. AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon,
  387. "Adding Module",
  388. "You chose a folder that appears to be a module.\n\n"
  389. "You need to select the *parent* folder inside which the new modules will be created.");
  390. return;
  391. }
  392. for (int i = 0; i < mods.size(); ++i)
  393. mods.getReference(i).manifestFile = lastLocation.getChildFile (mods.getReference(i).getID())
  394. .getChildFile (ModuleDescription::getManifestFileName());
  395. updateModulesFromWeb (project, mods);
  396. }
  397. }
  398. void run() override
  399. {
  400. for (int i = 0; i < modules.size(); ++i)
  401. {
  402. const ModuleDescription& m = modules.getReference(i);
  403. setProgress (i / (double) modules.size());
  404. MemoryBlock downloaded;
  405. result = download (m, downloaded);
  406. if (result.failed() || threadShouldExit())
  407. break;
  408. result = unzip (m, downloaded);
  409. if (result.failed() || threadShouldExit())
  410. break;
  411. }
  412. }
  413. Result download (const ModuleDescription& m, MemoryBlock& dest)
  414. {
  415. setStatusMessage ("Downloading " + m.getID() + "...");
  416. const ScopedPointer<InputStream> in (m.url.createInputStream (false, nullptr, nullptr, String::empty, 10000));
  417. if (in != nullptr && in->readIntoMemoryBlock (dest))
  418. return Result::ok();
  419. return Result::fail ("Failed to download from: " + m.url.toString (false));
  420. }
  421. Result unzip (const ModuleDescription& m, const MemoryBlock& data)
  422. {
  423. setStatusMessage ("Installing " + m.getID() + "...");
  424. MemoryInputStream input (data, false);
  425. ZipFile zip (input);
  426. if (zip.getNumEntries() == 0)
  427. return Result::fail ("The downloaded file wasn't a valid module file!");
  428. if (! m.getFolder().deleteRecursively())
  429. return Result::fail ("Couldn't delete the existing folder:\n" + m.getFolder().getFullPathName());
  430. return zip.uncompressTo (m.getFolder().getParentDirectory(), true);
  431. }
  432. Result result;
  433. Array<ModuleDescription> modules;
  434. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DownloadAndInstallThread)
  435. };