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.

495 lines
17KB

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