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