1 /******************************************************************************
2  *
3  * Project:  OpenCPN
4  *
5  ***************************************************************************
6  *   Copyright (C) 2019 Alec Leamas                                        *
7  *                                                                         *
8  *   This program is free software; you can redistribute it and/or modify  *
9  *   it under the terms of the GNU General Public License as published by  *
10  *   the Free Software Foundation; either version 2 of the License, or     *
11  *   (at your option) any later version.                                   *
12  *                                                                         *
13  *   This program is distributed in the hope that it will be useful,       *
14  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
15  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
16  *   GNU General Public License for more details.                          *
17  *                                                                         *
18  *   You should have received a copy of the GNU General Public License     *
19  *   along with this program; if not, write to the                         *
20  *   Free Software Foundation, Inc.,                                       *
21  *   51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,  USA.         *
22  ***************************************************************************
23  */
24 
25 /** Updates install and optional selection dialog. */
26 
27 
28 #include "config.h"
29 
30 #include <set>
31 #include <sstream>
32 
33 #include <wx/bitmap.h>
34 #include <wx/button.h>
35 #include <wx/debug.h>
36 #include <wx/file.h>
37 #include <wx/image.h>
38 #include <wx/log.h>
39 #include <wx/panel.h>
40 #include <wx/progdlg.h>
41 #include <wx/sizer.h>
42 #include <wx/statline.h>
43 #include <wx/textwrapper.h>
44 
45 #include "catalog_mgr.h"
46 #include "update_mgr.h"
47 #include "Downloader.h"
48 #include "OCPNPlatform.h"
49 #include "PluginHandler.h"
50 #include "pluginmanager.h"
51 #include "semantic_vers.h"
52 #include "styles.h"
53 #include "options.h"
54 
55 extern PlugInManager*           g_pi_manager;
56 extern ocpnStyle::StyleManager* g_StyleManager;
57 extern OCPNPlatform*            g_Platform;
58 extern options                 *g_options;
59 
60 extern wxImage LoadSVGIcon( wxString filename, int width, int height );
61 
62 #undef major                // walk around gnu's major() and minor() macros.
63 #undef minor
64 
65 class HardBreakWrapper : public wxTextWrapper
66     {
67     public:
HardBreakWrapper(wxWindow * win,const wxString & text,int widthMax)68         HardBreakWrapper(wxWindow *win, const wxString& text, int widthMax)
69         {
70             m_lineCount = 0;
71             Wrap(win, text, widthMax);
72         }
GetWrapped() const73         wxString const& GetWrapped() const { return m_wrapped; }
GetLineCount() const74         int const GetLineCount() const { return m_lineCount; }
75     protected:
OnOutputLine(const wxString & line)76         virtual void OnOutputLine(const wxString& line)
77         {
78             m_wrapped += line;
79         }
OnNewLine()80         virtual void OnNewLine()
81         {
82             m_wrapped += '\n';
83             m_lineCount++;
84         }
85     private:
86         wxString m_wrapped;
87         int m_lineCount;
88     };
89 
90 //    HardBreakWrapper wrapper(win, text, widthMax);
91 //    return wrapper.GetWrapped();
92 
93 
94 namespace update_mgr {
95 
96 
97 /**
98  * Return index in ArrayOfPlugins for plugin with given name,
99  * or -1 if not found.
100  */
PlugInIxByName(const std::string name,ArrayOfPlugIns * plugins)101 static ssize_t PlugInIxByName(const std::string name, ArrayOfPlugIns* plugins)
102 {
103     for (unsigned i = 0; i < plugins->GetCount(); i += 1) {
104         if (name == plugins->Item(i)->m_common_name.Lower().ToStdString()) {
105             return i;
106         }
107     }
108     return -1;
109 }
110 
111 
112 /** Return PlugInContainer with given name or 0 if not found. */
113 static PlugInContainer*
PlugInByName(const std::string name,ArrayOfPlugIns * plugins)114     PlugInByName(const std::string name, ArrayOfPlugIns* plugins)
115 {
116     auto ix = PlugInIxByName(name, plugins);
117     return ix == -1 ? 0 : plugins->Item(ix);
118 }
119 
120 
121 /** Load a png icon rescaled to size x size. */
LoadPNGIcon(const char * path,int size,wxBitmap & bitmap)122 static void LoadPNGIcon(const char* path, int size, wxBitmap& bitmap)
123 {
124     wxPNGHandler handler;
125     if (!wxImage::FindHandler(handler.GetName())) {
126         wxImage::AddHandler(new wxPNGHandler());
127     }
128     auto img = new wxImage();
129     bool ok = img->LoadFile(path, wxBITMAP_TYPE_PNG);
130     if (!ok) {
131         bitmap = wxBitmap();
132         return;
133     }
134     img->Rescale(size, size);
135     bitmap = wxBitmap(*img);
136 }
137 
138 
139 /** Load a svg icon rescaled to size x size. */
LoadSVGIcon(wxFileName path,int size,wxBitmap & bitmap)140 static void LoadSVGIcon(wxFileName path, int size, wxBitmap& bitmap)
141 {
142     wxImage img = LoadSVGIcon(path.GetFullPath(), size, size);
143     bitmap = wxBitmap(img);
144 }
145 
146 
147 /**
148  * A plugin icon, scaled to about 2/3 of available space
149  *
150  * Load icons from .../uidata/plugins, on the form plugin.svg or
151  * plugin.png. If neither exists, display a default  icon.
152  */
153 class PluginIconPanel: public wxPanel
154 {
155     public:
PluginIconPanel(wxWindow * parent,std::string plugin_name)156         PluginIconPanel(wxWindow* parent, std::string plugin_name)
157             :wxPanel(parent), m_plugin_name(plugin_name)
158         {
159             auto size = GetClientSize();
160             auto minsize = GetTextExtent("OpenCPN");
161             SetMinClientSize(wxSize(minsize.GetWidth(), size.GetHeight()));
162             Layout();
163             Bind(wxEVT_PAINT, &PluginIconPanel::OnPaint, this);
164         }
165 
OnPaint(wxPaintEvent & event)166         void OnPaint(wxPaintEvent& event)
167         {
168             auto size = GetClientSize();
169             int minsize = wxMin(size.GetHeight(), size.GetWidth());
170             auto offset = minsize / 10;
171 
172             LoadIcon("packageBox.svg", m_bitmap,  2 * minsize / 3);
173             wxPaintDC dc(this);
174             if (!m_bitmap.IsOk()) {
175                 wxLogMessage("AddPluginPanel: bitmap is not OK!");
176                 return;
177             }
178             dc.DrawBitmap(m_bitmap, offset, offset, true);
179          }
180 
181     protected:
182         wxBitmap m_bitmap;
183         const std::string m_plugin_name;
184 
LoadIcon(const char * plugin_name,wxBitmap & bitmap,int size=32)185         void LoadIcon(const char* plugin_name, wxBitmap& bitmap, int size=32)
186         {
187             wxFileName path(g_Platform->GetSharedDataDir(), plugin_name);
188             path.AppendDir("uidata");
189             path.AppendDir("traditional");
190             bool ok = false;
191 
192 
193             if (path.IsFileReadable()) {
194                 wxImage img = LoadSVGIcon(path.GetFullPath(), size, size);
195                 bitmap = wxBitmap(img);
196                 ok = bitmap.IsOk();
197             }
198 
199             if (!ok) {
200                 auto style = g_StyleManager->GetCurrentStyle();
201                 bitmap = wxBitmap(style->GetIcon( _T("default_pi"), size, size));
202                 wxLogMessage("Icon: %s not found.", path.GetFullPath());
203             }
204 
205 /*
206             wxFileName path(g_Platform->GetSharedDataDir(), plugin_name);
207             path.AppendDir("uidata");
208             bool ok = false;
209             path.SetExt("png");
210             if (path.IsFileReadable()) {
211                 LoadPNGIcon(path.GetFullPath(), size, bitmap);
212                 ok = bitmap.IsOk();
213             }
214             if (!ok) {
215                 auto style = g_StyleManager->GetCurrentStyle();
216                 bitmap = wxBitmap(style->GetIcon( _T("default_pi")));
217             }
218 */
219         }
220 };
221 
222 
223 /** Download and install a PluginMetadata item when clicked. */
224 class InstallButton: public wxPanel
225 {
226     public:
InstallButton(wxWindow * parent,PluginMetadata metadata)227         InstallButton(wxWindow* parent, PluginMetadata metadata)
228             :wxPanel(parent), m_metadata(metadata), m_remove(false)
229         {
230             PlugInContainer* found =
231                 PlugInByName(metadata.name, g_pi_manager->GetPlugInArray());
232             std::string label(_("Install"));
233             if (found && ((found->m_version_major > 0) || (found->m_version_minor > 0))) {
234                 label = getUpdateLabel(found, metadata);
235                 m_remove = true;
236             }
237             auto button = new wxButton(this, wxID_ANY, label);
238             auto pluginHandler = PluginHandler::getInstance();
239             auto box = new wxBoxSizer(wxHORIZONTAL);
240             box->Add(button);
241             SetSizer(box);
242             Bind(wxEVT_COMMAND_BUTTON_CLICKED, &InstallButton::OnClick, this);
243         }
244 
OnClick(wxCommandEvent & event)245         void OnClick(wxCommandEvent& event) {
246             wxLogMessage("Selected update: %s", m_metadata.name.c_str());
247             auto top_parent = GetParent()->GetParent()->GetParent();
248             auto dialog = dynamic_cast<UpdateDialog*>(top_parent);
249             wxASSERT(dialog != 0);
250             dialog->SetUpdate(m_metadata);
251             dialog->EndModal(wxID_OK);
252         }
253 
254     private:
255         PluginMetadata m_metadata;
256         bool m_remove;
257 
getUpdateLabel(PlugInContainer * pic,PluginMetadata metadata)258         const char* getUpdateLabel(PlugInContainer* pic,
259                                    PluginMetadata metadata)
260         {
261             SemanticVersion currentVersion(pic->m_version_major,
262                                            pic->m_version_minor);
263             if (pic->m_version_str != "") {
264                 currentVersion =
265                     SemanticVersion::parse(pic->m_version_str.ToStdString());
266             }
267             auto newVersion = SemanticVersion::parse(metadata.version);
268             if (newVersion > currentVersion) {
269                 return _("Update");
270             }
271             else if (newVersion == currentVersion) {
272                 return _("Reinstall");
273             }
274             else {
275                 return _("Downgrade");
276             }
277         }
278 };
279 
280 
281 /** Invokes client browser on plugin info_url when clicked. */
282 class WebsiteButton: public wxPanel
283 {
284     public:
WebsiteButton(wxWindow * parent,const char * url)285         WebsiteButton(wxWindow* parent, const char* url)
286             :wxPanel(parent), m_url(url)
287         {
288             auto vbox = new wxBoxSizer(wxVERTICAL);
289             auto button = new wxButton(this, wxID_ANY, _("Website"));
290             button->Enable(strlen(url) > 0);
291             vbox->Add(button);
292             SetSizer(vbox);
293             Bind(wxEVT_COMMAND_BUTTON_CLICKED,
294                  [=](wxCommandEvent&) {wxLaunchDefaultBrowser(m_url);});
295         }
296 
297     protected:
298         const std::string m_url;
299 };
300 
301 
302 /** The two buttons 'install' and 'website', the latter optionally hidden. */
303 class CandidateButtonsPanel: public wxPanel
304 {
305     public:
306 
CandidateButtonsPanel(wxWindow * parent,const PluginMetadata * plugin)307         CandidateButtonsPanel(wxWindow* parent, const PluginMetadata* plugin)
308             :wxPanel(parent)
309         {
310             auto flags = wxSizerFlags().Border();
311 
312             auto vbox = new wxBoxSizer(wxVERTICAL);
313             vbox->Add(new InstallButton(this, *plugin),
314                                         flags.DoubleBorder().Top().Right());
315             vbox->Add(1, 1, 1, wxEXPAND);   // Expanding, stretchable spacer
316             m_info_btn = new WebsiteButton(this, plugin->info_url.c_str());
317             m_info_btn->Hide();
318             vbox->Add(m_info_btn, flags.DoubleBorder().Bottom().Right());
319             SetSizer(vbox);
320             Fit();
321         }
322 
HideDetails(bool hide)323         void HideDetails(bool hide)
324         {
325             m_info_btn->Show(!hide);
326             GetParent()->Layout();
327         }
328 
329     private:
330         WebsiteButton* m_info_btn;
331 };
332 
333 
334 /** Plugin name, version, summary + an optionally shown description. */
335 class PluginTextPanel: public wxPanel
336 {
337     public:
PluginTextPanel(wxWindow * parent,const PluginMetadata * plugin,CandidateButtonsPanel * buttons)338         PluginTextPanel(wxWindow* parent,
339                         const PluginMetadata* plugin,
340                         CandidateButtonsPanel* buttons)
341             : wxPanel(parent), m_descr(0), m_buttons(buttons)
342         {
343             auto flags = wxSizerFlags().Border();
344 
345             auto sum_hbox = new wxBoxSizer(wxHORIZONTAL);
346             m_widthDescription = g_options->GetSize().x / 2;
347 
348             //m_summary = staticText(plugin->summary);
349             m_summary = new wxStaticText( this, wxID_ANY, _T(""), wxDefaultPosition, wxSize( m_widthDescription, -1)/*, wxST_NO_AUTORESIZE*/ );
350             m_summaryText = wxString(plugin->summary.c_str());
351             m_summary->SetLabel( m_summaryText );
352             m_summary->Wrap( m_widthDescription );
353 
354             HardBreakWrapper wrapper(this, m_summaryText, m_widthDescription);
355             m_summaryLineCount = wrapper.GetLineCount() + 1;
356 
357             sum_hbox->Add(m_summary);
358             sum_hbox->AddSpacer(10);
359             m_more = staticText("");
360             m_more->SetLabelMarkup(MORE);
361             sum_hbox->Add(m_more, wxSizerFlags());
362 
363             auto vbox = new wxBoxSizer(wxVERTICAL);
364             SetSizer(vbox);
365             auto name = staticText(plugin->name + "    " + plugin->version);
366 
367             m_descr = new wxStaticText( this, wxID_ANY, _T(""), wxDefaultPosition, wxSize( m_widthDescription, -1)/*, wxST_NO_AUTORESIZE*/ );
368             m_descText = wxString(plugin->description.c_str());
369             m_descr->SetLabel( m_descText );
370             m_descr->Wrap( m_widthDescription );
371             m_descr->Hide();
372             vbox->Add(name, flags);
373             vbox->Add(sum_hbox, flags);
374             vbox->Add(m_descr, 0);
375             Fit();
376 
377             m_more->Bind(wxEVT_LEFT_DOWN, &PluginTextPanel::OnClick, this);
378             m_descr->Bind(wxEVT_LEFT_DOWN, &PluginTextPanel::OnClick, this);
379         }
380 
OnClick(wxMouseEvent & event)381         void OnClick(wxMouseEvent& event)
382         {
383             m_descr->Show(!m_descr->IsShown());
384             m_descr->SetLabel( _T("") );
385             m_descr->SetLabel( m_descText );
386             m_descr->Wrap( m_widthDescription );
387             Layout();
388             wxSize asize = GetEffectiveMinSize();
389 
390             m_more->SetLabelMarkup(m_descr->IsShown() ? LESS : MORE);
391             m_buttons->HideDetails(!m_descr->IsShown());
392 
393             GetGrandParent()->SetSize(-1, asize.GetHeight() + 8 * GetCharHeight());
394 //             GetParent()->SendSizeEvent();
395 //             GetParent()->GetParent()->GetParent()->Layout();
396 //             GetParent()->GetParent()->GetParent()->Refresh(true);
397 //             GetParent()->GetParent()->GetParent()->Update();
398         }
399 
400         int m_summaryLineCount;
401     protected:
402         const char* const MORE = _("<span foreground='blue'>More...</span>");
403         const char* const LESS = _("<span foreground='blue'>Less...</span>");
404 
staticText(const wxString & text)405         wxStaticText* staticText(const wxString& text)
406         {
407             return new wxStaticText(this, wxID_ANY, text, wxDefaultPosition,
408                                     wxDefaultSize, wxALIGN_LEFT);
409         }
410 
411         wxStaticText* m_descr;
412         wxStaticText* m_more;
413         wxStaticText* m_summary;
414         CandidateButtonsPanel* m_buttons;
415         int m_widthDescription;
416         wxString m_descText;
417         wxString m_summaryText;
418 };
419 
420 
421 /**
422  * The list of download candidates in a scrolled window + OK and
423  * Settings  button.
424  */
425 class OcpnScrolledWindow : public wxScrolledWindow
426 {
427     public:
OcpnScrolledWindow(wxWindow * parent,const std::vector<PluginMetadata> & updates)428         OcpnScrolledWindow(wxWindow* parent,
429                            const std::vector<PluginMetadata>& updates)
430             :wxScrolledWindow(parent),
431             m_updates(updates),
432             m_grid(new wxFlexGridSizer(3, 0, 0))
433         {
434             m_twidth = 0;
435             auto box = new wxBoxSizer(wxVERTICAL);
436             populateGrid(m_grid);
437             box->Add(m_grid, wxSizerFlags().Proportion(0).Expand());
438             auto butt_box = new wxBoxSizer(wxHORIZONTAL);
439             auto cancel_btn = new wxButton(this, wxID_CANCEL, _("Dismiss"));
440             butt_box->Add(1, 1, 1, wxEXPAND);  // Expanding, stretchable spacer
441             butt_box->Add(cancel_btn,  wxSizerFlags().Right().Border());
442             box->Add(butt_box, wxSizerFlags().Proportion(0).Expand());
443 
444 
445             SetSizer(box);
446             //FitInside();
447             // TODO: Compute size using wxWindow::GetEffectiveMinSize()
448             SetScrollRate(1, 1);
449         };
450 
populateGrid(wxFlexGridSizer * grid)451         void populateGrid(wxFlexGridSizer* grid)
452         {
453             /** Compare two PluginMetadata objects, a named c++ requirement. */
454             struct metadata_compare{
455                 bool operator() (const PluginMetadata& lhs,
456                                  const PluginMetadata& rhs) const
457                 {
458                     return lhs.key() < rhs.key();
459                 }
460             };
461 
462             auto flags = wxSizerFlags();
463             grid->SetCols(3);
464             grid->AddGrowableCol(2);
465             m_TextLineCount = 0;
466             for (auto plugin: m_updates) {
467                 grid->Add(
468                     new PluginIconPanel(this, plugin.name), flags.Expand());
469                 auto buttons = new CandidateButtonsPanel(this, &plugin);
470                 PluginTextPanel *tpanel = new PluginTextPanel(this, &plugin, buttons);
471                 grid->Add(tpanel, flags.Proportion(1).Right());
472                 wxSize tsize = tpanel->GetEffectiveMinSize();
473                 m_twidth = wxMax(tsize.GetWidth(), m_twidth);
474                 grid->Add(buttons, flags.DoubleBorder());
475                 grid->Add(new wxStaticLine(this), wxSizerFlags(0).Expand());
476                 grid->Add(new wxStaticLine(this), wxSizerFlags(0).Expand());
477                 grid->Add(new wxStaticLine(this), wxSizerFlags(0).Expand());
478                 m_TextLineCount += tpanel->m_summaryLineCount;
479             }
480         }
481 
Reload()482         void Reload()
483         {
484             Hide();
485             m_grid->Clear();
486             populateGrid(m_grid);
487             Layout();
488             Show();
489             FitInside();
490             Refresh(true);
491         }
492 
493         int m_twidth;
494         int m_TextLineCount;
495 
496     private:
497         const std::vector<PluginMetadata> m_updates;
498         wxFlexGridSizer* m_grid;
499 };
500 
501 }  // namespace update_mgr
502 
503 /** Top-level install plugins dialog. */
UpdateDialog(wxWindow * parent,const std::vector<PluginMetadata> & updates)504 UpdateDialog::UpdateDialog(wxWindow* parent,
505                            const std::vector<PluginMetadata>& updates)
506     :wxDialog(parent, wxID_ANY, _("Plugin Manager"),
507               wxDefaultPosition , wxDefaultSize,
508               wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER)
509 {
510     auto vbox = new wxBoxSizer(wxVERTICAL);
511     SetSizer(vbox);
512 
513     auto scrwin = new update_mgr::OcpnScrolledWindow(this, updates);
514     vbox->Add(scrwin, wxSizerFlags(1).Expand());
515 
516     // The list has no natural height. Allocate 8 lines of text so some
517     // items are displayed initially in Layout()
518     int min_height = GetCharHeight() * 6;
519     min_height = GetCharHeight() * (scrwin->m_TextLineCount + 9);
520 
521     // There seem to be no way have dynamic, wrapping text:
522     // https://forums.wxwidgets.org/viewtopic.php?f=1&t=46662
523     //int width = GetParent()->GetClientSize().GetWidth();
524   //  SetMinClientSize(wxSize(width, min_height));
525     int width = scrwin->m_twidth * 2;
526     width = wxMin(width, g_Platform->getDisplaySize().x);
527     scrwin->SetMinSize(wxSize(width, min_height));
528 
529     SetMaxSize(g_Platform->getDisplaySize());
530 
531 #ifdef __OCPN__ANDROID__
532     SetMinSize(g_Platform->getDisplaySize());
533 #endif
534 
535     Fit();
536     Layout();
537     Center();
538 }
539