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.

478 lines
16KB

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