1 //  SuperTuxKart - a fun racing game with go-kart
2 //  Copyright (C) 2010-2015 Lucas Baudin, Joerg Henrichs
3 //
4 //  This program is free software; you can redistribute it and/or
5 //  modify it under the terms of the GNU General Public License
6 //  as published by the Free Software Foundation; either version 3
7 //  of the License, or (at your option) any later version.
8 //
9 //  This program is distributed in the hope that it will be useful,
10 //  but WITHOUT ANY WARRANTY; without even the implied warranty of
11 //  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 //  GNU General Public License for more details.
13 //
14 //  You should have received a copy of the GNU General Public License
15 //  along with this program; if not, write to the Free Software
16 //  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
17 
18 #include "states_screens/addons_screen.hpp"
19 
20 #include "addons/addons_manager.hpp"
21 #include "addons/news_manager.hpp"
22 #include "config/user_config.hpp"
23 #include "guiengine/CGUISpriteBank.hpp"
24 #include "guiengine/modaldialog.hpp"
25 #include "guiengine/scalable_font.hpp"
26 #include "guiengine/widget.hpp"
27 #include "guiengine/widgets/ribbon_widget.hpp"
28 #include "io/file_manager.hpp"
29 #include "online/request_manager.hpp"
30 #include "states_screens/dialogs/addons_loading.hpp"
31 #include "states_screens/dialogs/message_dialog.hpp"
32 #include "states_screens/state_manager.hpp"
33 #include "utils/string_utils.hpp"
34 #include "utils/ptr_vector.hpp"
35 #include "utils/translation.hpp"
36 
37 #include <iostream>
38 
39 using namespace Online;
40 // ----------------------------------------------------------------------------
41 
AddonsScreen()42 AddonsScreen::AddonsScreen() : Screen("addons_screen.stkgui")
43 {
44     m_selected_index = -1;
45 
46     // Add date filters.
47     // I18N: Time filters for add-ons
48     DateFilter filter_all = {_("All"), 0, 0, 0};
49     DateFilter filter_1w = {_("1 week"), 0, 0, 7};
50     DateFilter filter_2w = {_("2 weeks"), 0, 0, 14};
51     DateFilter filter_1m = {_("1 month"), 0, 1, 0};
52     DateFilter filter_3m = {_("3 months"), 0, 3, 0};
53     DateFilter filter_6m = {_("6 months"), 0, 6, 0};
54     DateFilter filter_9m = {_("9 months"), 0, 9, 0};
55     DateFilter filter_1y = {_("1 year"), 1, 0, 0};
56     DateFilter filter_2y = {_("2 years"), 2, 0, 0};
57     m_date_filters.push_back(filter_all);
58     m_date_filters.push_back(filter_1w);
59     m_date_filters.push_back(filter_2w);
60     m_date_filters.push_back(filter_1m);
61     m_date_filters.push_back(filter_3m);
62     m_date_filters.push_back(filter_6m);
63     m_date_filters.push_back(filter_9m);
64     m_date_filters.push_back(filter_1y);
65     m_date_filters.push_back(filter_2y);
66 }   // AddonsScreen
67 
68 // ----------------------------------------------------------------------------
69 
loadedFromFile()70 void AddonsScreen::loadedFromFile()
71 {
72     video::ITexture* icon1 = irr_driver->getTexture( file_manager->getAsset(FileManager::GUI_ICON,
73                                                      "package.png"         ));
74     video::ITexture* icon2 = irr_driver->getTexture( file_manager->getAsset(FileManager::GUI_ICON,
75                                                      "no-package.png"      ));
76     video::ITexture* icon3 = irr_driver->getTexture( file_manager->getAsset(FileManager::GUI_ICON,
77                                                      "package-update.png"  ));
78     video::ITexture* icon4 = irr_driver->getTexture( file_manager->getAsset(FileManager::GUI_ICON,
79                                                      "package-featured.png"));
80     video::ITexture* icon5 = irr_driver->getTexture( file_manager->getAsset(FileManager::GUI_ICON,
81                                                   "no-package-featured.png"));
82     video::ITexture* icon6 = irr_driver->getTexture( file_manager->getAsset(FileManager::GUI_ICON,
83                                                      "loading.png"));
84 
85     m_icon_bank = new irr::gui::STKModifiedSpriteBank( GUIEngine::getGUIEnv());
86     m_icon_installed     = m_icon_bank->addTextureAsSprite(icon1);
87     m_icon_not_installed = m_icon_bank->addTextureAsSprite(icon2);
88     m_icon_bank->addTextureAsSprite(icon4);
89     m_icon_bank->addTextureAsSprite(icon5);
90     m_icon_loading = m_icon_bank->addTextureAsSprite(icon6);
91     m_icon_needs_update  = m_icon_bank->addTextureAsSprite(icon3);
92 
93     GUIEngine::ListWidget* w_list =
94         getWidget<GUIEngine::ListWidget>("list_addons");
95     w_list->setColumnListener(this);
96 }   // loadedFromFile
97 
98 
99 // ----------------------------------------------------------------------------
100 
beforeAddingWidget()101 void AddonsScreen::beforeAddingWidget()
102 {
103     GUIEngine::ListWidget* w_list =
104         getWidget<GUIEngine::ListWidget>("list_addons");
105     assert(w_list != NULL);
106     w_list->clearColumns();
107     w_list->addColumn( _("Add-on name"), 3 );
108     w_list->addColumn( _("Updated date"), 1 );
109 
110     GUIEngine::SpinnerWidget* w_filter_date =
111                         getWidget<GUIEngine::SpinnerWidget>("filter_date");
112     w_filter_date->m_properties[GUIEngine::PROP_MIN_VALUE] = "0";
113     w_filter_date->m_properties[GUIEngine::PROP_MAX_VALUE] =
114                             StringUtils::toString(m_date_filters.size() - 1);
115 
116     for (unsigned int n = 0; n < m_date_filters.size(); n++)
117     {
118         w_filter_date->addLabel(m_date_filters[n].label);
119     }
120 
121     GUIEngine::SpinnerWidget* w_filter_rating =
122                         getWidget<GUIEngine::SpinnerWidget>("filter_rating");
123     w_filter_rating->m_properties[GUIEngine::PROP_MIN_VALUE] = "0";
124     w_filter_rating->m_properties[GUIEngine::PROP_MAX_VALUE] = "6";
125 
126     for (int n = 0; n < 7; n++)
127     {
128         w_filter_rating->addLabel(StringUtils::toWString(n / 2.0));
129     }
130 
131     GUIEngine::SpinnerWidget* w_filter_installation =
132                         getWidget<GUIEngine::SpinnerWidget>("filter_installation");
133     w_filter_installation->m_properties[GUIEngine::PROP_MIN_VALUE] = "0";
134     w_filter_installation->m_properties[GUIEngine::PROP_MAX_VALUE] = "2";
135     w_filter_installation->addLabel(_("All"));
136     w_filter_installation->addLabel(_("Installed"));
137     //I18N: Addon not installed for fillter
138     w_filter_installation->addLabel(_("Not installed"));
139 }
140 // ----------------------------------------------------------------------------
141 
init()142 void AddonsScreen::init()
143 {
144     Screen::init();
145 
146     m_sort_desc = false;
147     m_reloading = false;
148 
149     getWidget<GUIEngine::RibbonWidget>("category")->setActive(false);
150 
151     if(UserConfigParams::logAddons())
152         Log::info("addons", "Using directory <%s>", file_manager->getAddonsDir().c_str());
153 
154     GUIEngine::ListWidget* w_list =
155         getWidget<GUIEngine::ListWidget>("list_addons");
156 
157     // This defines the row height !
158     m_icon_height = GUIEngine::getFontHeight() * 2;
159     // 128 is the height of the image file
160     m_icon_bank->setScale((float)GUIEngine::getFontHeight() / 72.0f);
161     m_icon_bank->setTargetIconSize(128,128);
162     w_list->setIcons(m_icon_bank, (int)(m_icon_height));
163 
164     m_type = "kart";
165 
166     bool ip = UserConfigParams::m_internet_status == RequestManager::IPERM_ALLOWED;
167     getWidget<GUIEngine::IconButtonWidget>("reload")->setActive(ip);
168 
169     // Reset filter.
170     GUIEngine::TextBoxWidget* w_filter_name =
171                         getWidget<GUIEngine::TextBoxWidget>("filter_name");
172     w_filter_name->setText(L"");
173     GUIEngine::SpinnerWidget* w_filter_date =
174                         getWidget<GUIEngine::SpinnerWidget>("filter_date");
175     w_filter_date->setValue(0);
176     GUIEngine::SpinnerWidget* w_filter_rating =
177                         getWidget<GUIEngine::SpinnerWidget>("filter_rating");
178     w_filter_rating->setValue(0);
179 
180     GUIEngine::SpinnerWidget* w_filter_installation =
181                         getWidget<GUIEngine::SpinnerWidget>("filter_installation");
182     w_filter_installation->setValue(0);
183 
184     // Set the default sort order
185     Addon::setSortOrder(Addon::SO_DEFAULT);
186     loadList();
187 }   // init
188 
189 // ----------------------------------------------------------------------------
190 
unloaded()191 void AddonsScreen::unloaded()
192 {
193     delete m_icon_bank;
194     m_icon_bank = NULL;
195 }
196 
197 // ----------------------------------------------------------------------------
198 
tearDown()199 void AddonsScreen::tearDown()
200 {
201 }
202 
203 // ----------------------------------------------------------------------------
204 /** Loads the list of all addons of the given type. The gui element will be
205  *  updated.
206  */
loadList()207 void AddonsScreen::loadList()
208 {
209 #ifndef SERVER_ONLY
210     // Get the filter by words.
211     GUIEngine::TextBoxWidget* w_filter_name =
212                         getWidget<GUIEngine::TextBoxWidget>("filter_name");
213     core::stringw words = w_filter_name->getText();
214 
215     // Get the filter by date.
216     GUIEngine::SpinnerWidget* w_filter_date =
217                         getWidget<GUIEngine::SpinnerWidget>("filter_date");
218     int date_index = w_filter_date->getValue();
219     StkTime::TimeType date = StkTime::getTimeSinceEpoch();
220     date = StkTime::addInterval(date,
221                 -m_date_filters[date_index].year,
222                 -m_date_filters[date_index].month,
223                 -m_date_filters[date_index].day);
224 
225     // Get the filter by rating.
226     GUIEngine::SpinnerWidget* w_filter_rating =
227                         getWidget<GUIEngine::SpinnerWidget>("filter_rating");
228     float rating = w_filter_rating->getValue() / 2.0f;
229 
230     GUIEngine::SpinnerWidget* w_filter_installation =
231                         getWidget<GUIEngine::SpinnerWidget>("filter_installation");
232 
233     // First create a list of sorted entries
234     PtrVector<const Addon, REF> sorted_list;
235     for(unsigned int i=0; i<addons_manager->getNumAddons(); i++)
236     {
237         const Addon & addon = addons_manager->getAddon(i);
238         // Ignore not installed addons if the checkbox is enabled
239         if(   (w_filter_installation->getValue() == 1 && !addon.isInstalled())
240            || (w_filter_installation->getValue() == 2 &&  addon.isInstalled()))
241             continue;
242         // Ignore addons of a different type
243         if(addon.getType()!=m_type) continue;
244         // Ignore invisible addons
245         if(addon.testStatus(Addon::AS_INVISIBLE))
246             continue;
247         if(!UserConfigParams::m_artist_debug_mode &&
248             !addon.testStatus(Addon::AS_APPROVED)    )
249             continue;
250         if (!addon.isInstalled() && (addons_manager->wasError() ||
251                                      UserConfigParams::m_internet_status !=
252                                      RequestManager::IPERM_ALLOWED ))
253             continue;
254 
255         // Filter by rating.
256         if (addon.getRating() < rating)
257             continue;
258 
259         // Filter by date.
260         if (date_index != 0 && StkTime::compareTime(date, addon.getDate()) > 0)
261             continue;
262 
263         // Filter by name, designer and description.
264         if (!addon.filterByWords(words))
265             continue;
266 
267         sorted_list.push_back(&addon);
268     }
269     sorted_list.insertionSort(/*start=*/0, m_sort_desc);
270 
271     GUIEngine::ListWidget* w_list =
272         getWidget<GUIEngine::ListWidget>("list_addons");
273     w_list->clear();
274 
275     for(unsigned int i=0; i<sorted_list.size(); i++)
276     {
277         const Addon *addon = &(sorted_list[i]);
278         // Ignore addons of a different type
279         if(addon->getType()!=m_type) continue;
280         if(!UserConfigParams::m_artist_debug_mode &&
281             !addon->testStatus(Addon::AS_APPROVED)    )
282             continue;
283 
284         // Get the right icon to display
285         int icon;
286         if(addon->isInstalled())
287             icon = addon->needsUpdate() ? m_icon_needs_update
288                                         : m_icon_installed;
289         else
290             icon = m_icon_not_installed;
291 
292         core::stringw s;
293         if (addon->getDesigner().size()==0)
294         {
295             s = (addon->getName()+L"\t" +
296                     core::stringc(addon->getDateAsString().c_str())).c_str();
297         }
298 
299        //FIXME I'd like to move this to CGUISTKListBox.cpp
300 
301        /* gui::IGUIFont* font = GUIEngine::getFont();
302 
303         // first column is 0.666% of the list's width.
304         // and icon width == icon height.
305 
306         const unsigned int available_width = (int)(w_list->m_w*0.6666f
307                                                    - m_icon_height);
308         if (font->getDimension(s.c_str()).Width > available_width)
309         {
310             s = s.subString(0, int(AddonsScreen::getWidth()*0.018)+20);
311             s.append("...");
312         }
313         else
314         {*/
315             if (addon->getDesigner().size() == 0)
316             {
317                 s = addon->getName();
318             }
319             else
320             {
321                 //I18N: as in: The Old Island by Johannes Sjolund
322                 s = _C("addons", "%s by %s", addon->getName().c_str(),
323                         addon->getDesigner().c_str());
324             }
325             /*
326             // check if text is too long to fit
327             if (font->getDimension(s.c_str()).Width >  available_width)
328             {
329                 // start by splitting on 2 lines
330 
331                 //I18N: as in: The Old Island by Johannes Sjolund
332                 s = _("%s\nby %s",addon->getName().c_str(),
333                       addon->getDesigner().c_str());
334 
335                 core::stringw final_string;
336 
337                 // then check if each line is now short enough.
338                 std::vector<irr::core::stringw> lines =
339                     StringUtils::split(s, '\n');
340                 for (unsigned int n=0; n<lines.size(); n++)
341                 {
342                     if (font->getDimension(lines[n].c_str()).Width
343                           > available_width)
344                     {
345                         // arg, still too long! cut the text so that it fits.
346                         core::stringw line = lines[n];
347 
348                         // leave a margin of 14 pixels to account for the "..."
349                         // that will be appended
350                         int split_at = font->getCharacterFromPos(line.c_str(),
351                                                          available_width - 14);
352                         line = line.subString(0, split_at);
353                         line.append("...");
354                         if (final_string.size() > 0) final_string.append("\n");
355                         final_string.append(line);
356                     }
357                     else
358                     {
359                         if (final_string.size() > 0) final_string.append("\n");
360                         final_string.append(lines[n]);
361                     }
362                 }   // for nlines.size()
363 
364                 s = final_string;
365                 */
366             //}   // if
367         //}
368 
369         // we have no icon for featured+updateme, so if an add-on is updatable
370         // forget about the featured icon
371         if (addon->testStatus(Addon::AS_FEATURED) &&
372             icon != m_icon_needs_update)
373         {
374             icon += 2;
375         }
376 
377         std::vector<GUIEngine::ListWidget::ListCell> row;
378         row.push_back(GUIEngine::ListWidget::ListCell(s.c_str(), icon, 3, false));
379         row.push_back(GUIEngine::ListWidget::ListCell(addon->getDateAsString().c_str(), -1, 1, true));
380         w_list->addItem(addon->getId(), row);
381 
382         // Highlight if it's not approved in artists debug mode.
383         if(UserConfigParams::m_artist_debug_mode &&
384             !addon->testStatus(Addon::AS_APPROVED))
385         {
386             w_list->markItemRed(addon->getId(), true);
387         }
388     }
389 
390     getWidget<GUIEngine::RibbonWidget>("category")->setActive(true);
391     if(m_type == "kart")
392         getWidget<GUIEngine::RibbonWidget>("category")->select("tab_kart",
393                                                         PLAYER_ID_GAME_MASTER);
394     else if(m_type == "track")
395         getWidget<GUIEngine::RibbonWidget>("category")->select("tab_track",
396                                                         PLAYER_ID_GAME_MASTER);
397     else
398         getWidget<GUIEngine::RibbonWidget>("category")->select("tab_update",
399                                                         PLAYER_ID_GAME_MASTER);
400 #endif
401 }   // loadList
402 
403 // ----------------------------------------------------------------------------
onColumnClicked(int column_id,bool sort_desc,bool sort_default)404 void AddonsScreen::onColumnClicked(int column_id, bool sort_desc, bool sort_default)
405 {
406     switch(column_id)
407     {
408     case 0:
409         Addon::setSortOrder(sort_default ? Addon::SO_DEFAULT : Addon::SO_NAME);
410         break;
411     case 1:
412         Addon::setSortOrder(sort_default ? Addon::SO_DEFAULT : Addon::SO_DATE);
413         break;
414     default: assert(0); break;
415     }   // switch
416     /** \brief Toggle the sort order after column click **/
417     m_sort_desc = sort_desc && !sort_default;
418     loadList();
419 }   // onColumnClicked
420 
421 // ----------------------------------------------------------------------------
eventCallback(GUIEngine::Widget * widget,const std::string & name,const int playerID)422 void AddonsScreen::eventCallback(GUIEngine::Widget* widget,
423                                  const std::string& name, const int playerID)
424 {
425 #ifndef SERVER_ONLY
426     if (name == "back")
427     {
428         StateManager::get()->escapePressed();
429     }
430 
431     else if (name == "reload")
432     {
433         if (!m_reloading)
434         {
435             m_reloading = true;
436             NewsManager::get()->init(true);
437 
438             GUIEngine::ListWidget* w_list =
439                        getWidget<GUIEngine::ListWidget>("list_addons");
440             w_list->clear();
441 
442             w_list->addItem("spacer", L"");
443             w_list->addItem("loading",_("Please wait while addons are updated"), m_icon_loading);
444         }
445     }
446 
447     else if (name == "list_addons")
448     {
449         GUIEngine::ListWidget* list =
450             getWidget<GUIEngine::ListWidget>("list_addons");
451         std::string id = list->getSelectionInternalName();
452 
453         if (!id.empty() && addons_manager->getAddon(id) != NULL)
454         {
455             m_selected_index = list->getSelectionID();
456             new AddonsLoading(id);
457         }
458     }
459     if (name == "category")
460     {
461         std::string selection = ((GUIEngine::RibbonWidget*)widget)
462                          ->getSelectionIDString(PLAYER_ID_GAME_MASTER).c_str();
463         if (selection == "tab_track")
464         {
465             m_type = "track";
466             loadList();
467         }
468         else if (selection == "tab_kart")
469         {
470             m_type = "kart";
471             loadList();
472         }
473         else if (selection == "tab_arena")
474         {
475             m_type = "arena";
476             loadList();
477         }
478     }
479     else if (name == "filter_search" || name == "filter_installation")
480     {
481         loadList();
482     }
483 #endif
484 }   // eventCallback
485 
486 // ----------------------------------------------------------------------------
487 /** Selects the last selected item on the list (which is the item that
488  *  is just being installed) again. This function is used from the
489  *  addons_loading screen: when it is closed, it will reset the
490  *  select item so that people can keep on installing from that
491  *  point on.
492 */
setLastSelected()493 void AddonsScreen::setLastSelected()
494 {
495     if(m_selected_index>-1)
496     {
497         GUIEngine::ListWidget* list =
498             getWidget<GUIEngine::ListWidget>("list_addons");
499         list->setFocusForPlayer(PLAYER_ID_GAME_MASTER);
500         list->setSelectionID(m_selected_index);
501     }
502 }   // setLastSelected
503 
504 // ----------------------------------------------------------------------------
505 
onUpdate(float dt)506 void AddonsScreen::onUpdate(float dt)
507 {
508 #ifndef SERVER_ONLY
509     NewsManager::get()->joinDownloadThreadIfExit();
510 
511     if (m_reloading)
512     {
513         if(UserConfigParams::m_internet_status!=RequestManager::IPERM_ALLOWED)
514         {
515             // not allowed to access the net. how did you get to this menu in
516             // the first place??
517             loadList();
518             m_reloading = false;
519         }
520         else if (addons_manager->wasError())
521         {
522             m_reloading = false;
523             new MessageDialog( _("Sorry, an error occurred while contacting "
524                                  "the add-ons website. Make sure you are "
525                                  "connected to the Internet and that "
526                                  "SuperTuxKart is not blocked by a firewall"));
527             loadList();
528         }
529         else if (addons_manager->onlineReady())
530         {
531             m_reloading = false;
532             loadList();
533         }
534         else
535         {
536             // Addons manager is still initialising/downloading.
537         }
538     }
539 #endif
540 }   // onUpdate
541