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