Merge pull request #4539 from Matricom/amcodec
[vuplus_xbmc] / xbmc / filesystem / PluginDirectory.cpp
1 /*
2  *      Copyright (C) 2005-2013 Team XBMC
3  *      http://xbmc.org
4  *
5  *  This Program is free software; you can redistribute it and/or modify
6  *  it under the terms of the GNU General Public License as published by
7  *  the Free Software Foundation; either version 2, or (at your option)
8  *  any later version.
9  *
10  *  This Program is distributed in the hope that it will be useful,
11  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  *  GNU General Public License for more details.
14  *
15  *  You should have received a copy of the GNU General Public License
16  *  along with XBMC; see the file COPYING.  If not, see
17  *  <http://www.gnu.org/licenses/>.
18  *
19  */
20
21
22 #include "threads/SystemClock.h"
23 #include "system.h"
24 #include "PluginDirectory.h"
25 #include "utils/URIUtils.h"
26 #include "addons/AddonManager.h"
27 #include "addons/AddonInstaller.h"
28 #include "addons/IAddon.h"
29 #include "interfaces/generic/ScriptInvocationManager.h"
30 #include "threads/SingleLock.h"
31 #include "guilib/GUIWindowManager.h"
32 #include "dialogs/GUIDialogProgress.h"
33 #include "settings/Settings.h"
34 #include "FileItem.h"
35 #include "video/VideoInfoTag.h"
36 #include "guilib/LocalizeStrings.h"
37 #include "utils/log.h"
38 #include "utils/TimeUtils.h"
39 #include "utils/StringUtils.h"
40 #include "ApplicationMessenger.h"
41 #include "Application.h"
42 #include "URL.h"
43
44 using namespace XFILE;
45 using namespace std;
46 using namespace ADDON;
47
48 map<int, CPluginDirectory *> CPluginDirectory::globalHandles;
49 int CPluginDirectory::handleCounter = 0;
50 CCriticalSection CPluginDirectory::m_handleLock;
51
52 CPluginDirectory::CPluginDirectory()
53 {
54   m_listItems = new CFileItemList;
55   m_fileResult = new CFileItem;
56 }
57
58 CPluginDirectory::~CPluginDirectory(void)
59 {
60   delete m_listItems;
61   delete m_fileResult;
62 }
63
64 int CPluginDirectory::getNewHandle(CPluginDirectory *cp)
65 {
66   CSingleLock lock(m_handleLock);
67   int handle = ++handleCounter;
68   globalHandles[handle] = cp;
69   return handle;
70 }
71
72 void CPluginDirectory::removeHandle(int handle)
73 {
74   CSingleLock lock(m_handleLock);
75   if (!globalHandles.erase(handle))
76     CLog::Log(LOGWARNING, "Attempt to erase invalid handle %i", handle);
77 }
78
79 CPluginDirectory *CPluginDirectory::dirFromHandle(int handle)
80 {
81   CSingleLock lock(m_handleLock);
82   map<int, CPluginDirectory *>::iterator i = globalHandles.find(handle);
83   if (i != globalHandles.end())
84     return i->second;
85   CLog::Log(LOGWARNING, "Attempt to use invalid handle %i", handle);
86   return NULL;
87 }
88
89 bool CPluginDirectory::StartScript(const CStdString& strPath, bool retrievingDir)
90 {
91   CURL url(strPath);
92
93   // try the plugin type first, and if not found, try an unknown type
94   if (!CAddonMgr::Get().GetAddon(url.GetHostName(), m_addon, ADDON_PLUGIN) &&
95       !CAddonMgr::Get().GetAddon(url.GetHostName(), m_addon, ADDON_UNKNOWN) &&
96       !CAddonInstaller::Get().PromptForInstall(url.GetHostName(), m_addon))
97   {
98     CLog::Log(LOGERROR, "Unable to find plugin %s", url.GetHostName().c_str());
99     return false;
100   }
101
102   // get options
103   CStdString options = url.GetOptions();
104   URIUtils::RemoveSlashAtEnd(options); // This MAY kill some scripts (eg though with a URL ending with a slash), but
105                                     // is needed for all others, as XBMC adds slashes to "folders"
106   url.SetOptions(""); // do this because we can then use the url to generate the basepath
107                       // which is passed to the plugin (and represents the share)
108
109   CStdString basePath(url.Get());
110   // reset our wait event, and grab a new handle
111   m_fetchComplete.Reset();
112   int handle = getNewHandle(this);
113
114   // clear out our status variables
115   m_fileResult->Reset();
116   m_listItems->Clear();
117   m_listItems->SetPath(strPath);
118   m_listItems->SetLabel(m_addon->Name());
119   m_cancelled = false;
120   m_success = false;
121   m_totalItems = 0;
122
123   // setup our parameters to send the script
124   CStdString strHandle = StringUtils::Format("%i", handle);
125   vector<string> argv;
126   argv.push_back(basePath);
127   argv.push_back(strHandle);
128   argv.push_back(options);
129
130   // run the script
131   CLog::Log(LOGDEBUG, "%s - calling plugin %s('%s','%s','%s')", __FUNCTION__, m_addon->Name().c_str(), argv[0].c_str(), argv[1].c_str(), argv[2].c_str());
132   bool success = false;
133   CStdString file = m_addon->LibPath();
134   int id = CScriptInvocationManager::Get().Execute(file, m_addon, argv);
135   if (id >= 0)
136   { // wait for our script to finish
137     CStdString scriptName = m_addon->Name();
138     success = WaitOnScriptResult(file, id, scriptName, retrievingDir);
139   }
140   else
141     CLog::Log(LOGERROR, "Unable to run plugin %s", m_addon->Name().c_str());
142
143   // free our handle
144   removeHandle(handle);
145
146   return success;
147 }
148
149 bool CPluginDirectory::GetPluginResult(const CStdString& strPath, CFileItem &resultItem)
150 {
151   CURL url(strPath);
152   CPluginDirectory* newDir = new CPluginDirectory();
153
154   bool success = newDir->StartScript(strPath, false);
155
156   if (success)
157   { // update the play path and metadata, saving the old one as needed
158     if (!resultItem.HasProperty("original_listitem_url"))
159       resultItem.SetProperty("original_listitem_url", resultItem.GetPath());
160     resultItem.SetPath(newDir->m_fileResult->GetPath());
161     resultItem.SetMimeType(newDir->m_fileResult->GetMimeType());
162     resultItem.UpdateInfo(*newDir->m_fileResult);
163     if (newDir->m_fileResult->HasVideoInfoTag() && newDir->m_fileResult->GetVideoInfoTag()->m_resumePoint.IsSet())
164       resultItem.m_lStartOffset = STARTOFFSET_RESUME; // resume point set in the resume item, so force resume
165   }
166   delete newDir;
167
168   return success;
169 }
170
171 bool CPluginDirectory::AddItem(int handle, const CFileItem *item, int totalItems)
172 {
173   CSingleLock lock(m_handleLock);
174   CPluginDirectory *dir = dirFromHandle(handle);
175   if (!dir)
176     return false;
177
178   CFileItemPtr pItem(new CFileItem(*item));
179   dir->m_listItems->Add(pItem);
180   dir->m_totalItems = totalItems;
181
182   return !dir->m_cancelled;
183 }
184
185 bool CPluginDirectory::AddItems(int handle, const CFileItemList *items, int totalItems)
186 {
187   CSingleLock lock(m_handleLock);
188   CPluginDirectory *dir = dirFromHandle(handle);
189   if (!dir)
190     return false;
191
192   CFileItemList pItemList;
193   pItemList.Copy(*items);
194   dir->m_listItems->Append(pItemList);
195   dir->m_totalItems = totalItems;
196
197   return !dir->m_cancelled;
198 }
199
200 void CPluginDirectory::EndOfDirectory(int handle, bool success, bool replaceListing, bool cacheToDisc)
201 {
202   CSingleLock lock(m_handleLock);
203   CPluginDirectory *dir = dirFromHandle(handle);
204   if (!dir)
205     return;
206
207   // set cache to disc
208   dir->m_listItems->SetCacheToDisc(cacheToDisc ? CFileItemList::CACHE_IF_SLOW : CFileItemList::CACHE_NEVER);
209
210   dir->m_success = success;
211   dir->m_listItems->SetReplaceListing(replaceListing);
212
213   if (!dir->m_listItems->HasSortDetails())
214     dir->m_listItems->AddSortMethod(SortByNone, 552, LABEL_MASKS("%L", "%D"));
215
216   // set the event to mark that we're done
217   dir->m_fetchComplete.Set();
218 }
219
220 void CPluginDirectory::AddSortMethod(int handle, SORT_METHOD sortMethod, const CStdString &label2Mask)
221 {
222   CSingleLock lock(m_handleLock);
223   CPluginDirectory *dir = dirFromHandle(handle);
224   if (!dir)
225     return;
226
227   // TODO: Add all sort methods and fix which labels go on the right or left
228   switch(sortMethod)
229   {
230     case SORT_METHOD_LABEL:
231     case SORT_METHOD_LABEL_IGNORE_THE:
232       {
233         dir->m_listItems->AddSortMethod(SortByLabel, 551, LABEL_MASKS("%T", label2Mask), CSettings::Get().GetBool("filelists.ignorethewhensorting") ? SortAttributeIgnoreArticle : SortAttributeNone);
234         break;
235       }
236     case SORT_METHOD_TITLE:
237     case SORT_METHOD_TITLE_IGNORE_THE:
238       {
239         dir->m_listItems->AddSortMethod(SortByTitle, 556, LABEL_MASKS("%T", label2Mask), CSettings::Get().GetBool("filelists.ignorethewhensorting") ? SortAttributeIgnoreArticle : SortAttributeNone);
240         break;
241       }
242     case SORT_METHOD_ARTIST:
243     case SORT_METHOD_ARTIST_IGNORE_THE:
244       {
245         dir->m_listItems->AddSortMethod(SortByArtist, 557, LABEL_MASKS("%T", "%A"), CSettings::Get().GetBool("filelists.ignorethewhensorting") ? SortAttributeIgnoreArticle : SortAttributeNone);
246         break;
247       }
248     case SORT_METHOD_ALBUM:
249     case SORT_METHOD_ALBUM_IGNORE_THE:
250       {
251         dir->m_listItems->AddSortMethod(SortByAlbum, 558, LABEL_MASKS("%T", "%B"), CSettings::Get().GetBool("filelists.ignorethewhensorting") ? SortAttributeIgnoreArticle : SortAttributeNone);
252         break;
253       }
254     case SORT_METHOD_DATE:
255       {
256         dir->m_listItems->AddSortMethod(SortByDate, 552, LABEL_MASKS("%T", "%J"));
257         break;
258       }
259     case SORT_METHOD_BITRATE:
260       {
261         dir->m_listItems->AddSortMethod(SortByBitrate, 623, LABEL_MASKS("%T", "%X"));
262         break;
263       }             
264     case SORT_METHOD_SIZE:
265       {
266         dir->m_listItems->AddSortMethod(SortBySize, 553, LABEL_MASKS("%T", "%I"));
267         break;
268       }
269     case SORT_METHOD_FILE:
270       {
271         dir->m_listItems->AddSortMethod(SortByFile, 561, LABEL_MASKS("%T", label2Mask));
272         break;
273       }
274     case SORT_METHOD_TRACKNUM:
275       {
276         dir->m_listItems->AddSortMethod(SortByTrackNumber, 554, LABEL_MASKS("[%N. ]%T", label2Mask));
277         break;
278       }
279     case SORT_METHOD_DURATION:
280     case SORT_METHOD_VIDEO_RUNTIME:
281       {
282         dir->m_listItems->AddSortMethod(SortByTime, 180, LABEL_MASKS("%T", "%D"));
283         break;
284       }
285     case SORT_METHOD_VIDEO_RATING:
286     case SORT_METHOD_SONG_RATING:
287       {
288         dir->m_listItems->AddSortMethod(SortByRating, 563, LABEL_MASKS("%T", "%R"));
289         break;
290       }
291     case SORT_METHOD_YEAR:
292       {
293         dir->m_listItems->AddSortMethod(SortByYear, 562, LABEL_MASKS("%T", "%Y"));
294         break;
295       }
296     case SORT_METHOD_GENRE:
297       {
298         dir->m_listItems->AddSortMethod(SortByGenre, 515, LABEL_MASKS("%T", "%G"));
299         break;
300       }
301     case SORT_METHOD_COUNTRY:
302       {
303         dir->m_listItems->AddSortMethod(SortByCountry, 574, LABEL_MASKS("%T", "%G"));
304         break;
305       }
306     case SORT_METHOD_VIDEO_TITLE:
307       {
308         dir->m_listItems->AddSortMethod(SortByTitle, 369, LABEL_MASKS("%T", label2Mask));
309         break;
310       }
311     case SORT_METHOD_VIDEO_SORT_TITLE:
312     case SORT_METHOD_VIDEO_SORT_TITLE_IGNORE_THE:
313       {
314         dir->m_listItems->AddSortMethod(SortBySortTitle, 556, LABEL_MASKS("%T", label2Mask), CSettings::Get().GetBool("filelists.ignorethewhensorting") ? SortAttributeIgnoreArticle : SortAttributeNone);
315         break;
316       }
317     case SORT_METHOD_MPAA_RATING:
318       {
319         dir->m_listItems->AddSortMethod(SortByMPAA, 20074, LABEL_MASKS("%T", "%O"));
320         break;
321       }
322     case SORT_METHOD_STUDIO:
323     case SORT_METHOD_STUDIO_IGNORE_THE:
324       {
325         dir->m_listItems->AddSortMethod(SortByStudio, 572, LABEL_MASKS("%T", "%U"), CSettings::Get().GetBool("filelists.ignorethewhensorting") ? SortAttributeIgnoreArticle : SortAttributeNone);
326         break;
327       }
328     case SORT_METHOD_PROGRAM_COUNT:
329       {
330         dir->m_listItems->AddSortMethod(SortByProgramCount, 567, LABEL_MASKS("%T", "%C"));
331         break;
332       }
333     case SORT_METHOD_UNSORTED:
334       {
335         dir->m_listItems->AddSortMethod(SortByNone, 571, LABEL_MASKS("%T", label2Mask));
336         break;
337       }
338     case SORT_METHOD_NONE:
339       {
340         dir->m_listItems->AddSortMethod(SortByNone, 552, LABEL_MASKS("%T", label2Mask));
341         break;
342       }
343     case SORT_METHOD_DRIVE_TYPE:
344       {
345         dir->m_listItems->AddSortMethod(SortByDriveType, 564, LABEL_MASKS()); // Preformatted
346         break;
347       }
348     case SORT_METHOD_PLAYLIST_ORDER:
349       {
350         CStdString strTrackLeft=CSettings::Get().GetString("musicfiles.trackformat");
351         CStdString strTrackRight=CSettings::Get().GetString("musicfiles.trackformatright");
352
353         dir->m_listItems->AddSortMethod(SortByPlaylistOrder, 559, LABEL_MASKS(strTrackLeft, strTrackRight));
354         break;
355       }
356     case SORT_METHOD_EPISODE:
357       {
358         dir->m_listItems->AddSortMethod(SortByEpisodeNumber, 20359, LABEL_MASKS("%E. %T","%R"));
359         break;
360       }
361     case SORT_METHOD_PRODUCTIONCODE:
362       {
363         //dir->m_listItems.AddSortMethod(SORT_METHOD_PRODUCTIONCODE,20368,LABEL_MASKS("%E. %T","%P", "%E. %T","%P"));
364         dir->m_listItems->AddSortMethod(SortByProductionCode, 20368, LABEL_MASKS("%H. %T","%P", "%H. %T","%P"));
365         break;
366       }
367     case SORT_METHOD_LISTENERS:
368       {
369        dir->m_listItems->AddSortMethod(SortByListeners, 20455, LABEL_MASKS("%T","%W"));
370        break;
371       }
372     case SORT_METHOD_DATEADDED:
373       {
374         dir->m_listItems->AddSortMethod(SortByDateAdded, 570, LABEL_MASKS("%T", "%a"));
375         break;
376       }
377     case SORT_METHOD_FULLPATH:
378       {
379         dir->m_listItems->AddSortMethod(SortByPath, 573, LABEL_MASKS("%T", label2Mask));
380         break;
381       }
382     case SORT_METHOD_LABEL_IGNORE_FOLDERS:
383       {
384         dir->m_listItems->AddSortMethod(SortByLabel, SortAttributeIgnoreFolders, 551, LABEL_MASKS("%T", label2Mask));
385         break;
386       }
387     case SORT_METHOD_LASTPLAYED:
388       {
389         dir->m_listItems->AddSortMethod(SortByLastPlayed, 568, LABEL_MASKS("%T", "%G"));
390         break;
391       }
392     case SORT_METHOD_PLAYCOUNT:
393       {
394         dir->m_listItems->AddSortMethod(SortByPlaycount, 567, LABEL_MASKS("%T", "%V"));
395         break;
396       }
397     case SORT_METHOD_CHANNEL:
398       {
399         dir->m_listItems->AddSortMethod(SortByChannel, 19029, LABEL_MASKS("%T", label2Mask));
400         break;
401       }
402    
403     default:
404       break;
405   }
406 }
407
408 bool CPluginDirectory::GetDirectory(const CStdString& strPath, CFileItemList& items)
409 {
410   CURL url(strPath);
411
412   bool success = StartScript(strPath, true);
413
414   // append the items to the list
415   items.Assign(*m_listItems, true); // true to keep the current items
416   m_listItems->Clear();
417   return success;
418 }
419
420 bool CPluginDirectory::RunScriptWithParams(const CStdString& strPath)
421 {
422   CURL url(strPath);
423   if (url.GetHostName().empty()) // called with no script - should never happen
424     return false;
425
426   AddonPtr addon;
427   if (!CAddonMgr::Get().GetAddon(url.GetHostName(), addon, ADDON_PLUGIN) && !CAddonInstaller::Get().PromptForInstall(url.GetHostName(), addon))
428   {
429     CLog::Log(LOGERROR, "Unable to find plugin %s", url.GetHostName().c_str());
430     return false;
431   }
432
433   // options
434   CStdString options = url.GetOptions();
435   URIUtils::RemoveSlashAtEnd(options); // This MAY kill some scripts (eg though with a URL ending with a slash), but
436                                     // is needed for all others, as XBMC adds slashes to "folders"
437   url.SetOptions(""); // do this because we can then use the url to generate the basepath
438                       // which is passed to the plugin (and represents the share)
439
440   CStdString basePath(url.Get());
441
442   // setup our parameters to send the script
443   CStdString strHandle = StringUtils::Format("%i", -1);
444   vector<string> argv;
445   argv.push_back(basePath);
446   argv.push_back(strHandle);
447   argv.push_back(options);
448
449   // run the script
450   CLog::Log(LOGDEBUG, "%s - calling plugin %s('%s','%s','%s')", __FUNCTION__, addon->Name().c_str(), argv[0].c_str(), argv[1].c_str(), argv[2].c_str());
451   if (CScriptInvocationManager::Get().Execute(addon->LibPath(), addon, argv) >= 0)
452     return true;
453   else
454     CLog::Log(LOGERROR, "Unable to run plugin %s", addon->Name().c_str());
455
456   return false;
457 }
458
459 bool CPluginDirectory::WaitOnScriptResult(const CStdString &scriptPath, int scriptId, const CStdString &scriptName, bool retrievingDir)
460 {
461   const unsigned int timeBeforeProgressBar = 1500;
462   const unsigned int timeToKillScript = 1000;
463
464   unsigned int startTime = XbmcThreads::SystemClockMillis();
465   CGUIDialogProgress *progressBar = NULL;
466   bool cancelled = false;
467   bool inMainAppThread = g_application.IsCurrentThread();
468
469   CLog::Log(LOGDEBUG, "%s - waiting on the %s (id=%d) plugin...", __FUNCTION__, scriptName.c_str(), scriptId);
470   while (true)
471   {
472     {
473       CSingleExit ex(g_graphicsContext);
474       // check if the python script is finished
475       if (m_fetchComplete.WaitMSec(20))
476       { // python has returned
477         CLog::Log(LOGDEBUG, "%s- plugin returned %s", __FUNCTION__, m_success ? "successfully" : "failure");
478         break;
479       }
480     }
481     // check our script is still running
482     if (!CScriptInvocationManager::Get().IsRunning(scriptId))
483     { // check whether we exited normally
484       if (!m_fetchComplete.WaitMSec(0))
485       { // python didn't return correctly
486         CLog::Log(LOGDEBUG, " %s - plugin exited prematurely - terminating", __FUNCTION__);
487         m_success = false;
488       }
489       break;
490     }
491
492     // check whether we should pop up the progress dialog
493     if (!retrievingDir && !progressBar && XbmcThreads::SystemClockMillis() - startTime > timeBeforeProgressBar)
494     { // loading takes more then 1.5 secs, show a progress dialog
495       progressBar = (CGUIDialogProgress *)g_windowManager.GetWindow(WINDOW_DIALOG_PROGRESS);
496
497       // if script has shown progressbar don't override it
498       if (progressBar && progressBar->IsActive())
499       {
500         startTime = XbmcThreads::SystemClockMillis();
501         progressBar = NULL;
502       }
503
504       if (progressBar)
505       {
506         progressBar->SetHeading(scriptName);
507         progressBar->SetLine(0, retrievingDir ? 1040 : 10214);
508         progressBar->SetLine(1, "");
509         progressBar->SetLine(2, "");
510         progressBar->ShowProgressBar(retrievingDir);
511         progressBar->StartModal();
512       }
513     }
514
515     if (progressBar)
516     { // update the progress bar and check for user cancel
517       progressBar->Progress();
518       if (progressBar->IsCanceled())
519       { // user has cancelled our process - cancel our process
520         m_cancelled = true;
521       }
522     }
523     else // if the progressBar exists and we call StartModal or Progress we get the
524          //  ProcessRenderLoop call anyway.
525       if (inMainAppThread) 
526         g_windowManager.ProcessRenderLoop();
527
528     if (!cancelled && m_cancelled)
529     {
530       cancelled = true;
531       startTime = XbmcThreads::SystemClockMillis();
532     }
533     if ((cancelled && XbmcThreads::SystemClockMillis() - startTime > timeToKillScript) || g_application.m_bStop)
534     { // cancel our script
535       if (scriptId != -1 && CScriptInvocationManager::Get().IsRunning(scriptId))
536       {
537         CLog::Log(LOGDEBUG, "%s- cancelling plugin %s (id=%d)", __FUNCTION__, scriptName.c_str(), scriptId);
538         CScriptInvocationManager::Get().Stop(scriptId);
539         break;
540       }
541     }
542   }
543
544   if (progressBar)
545     CApplicationMessenger::Get().Close(progressBar, false, false);
546
547   return !cancelled && m_success;
548 }
549
550 void CPluginDirectory::SetResolvedUrl(int handle, bool success, const CFileItem *resultItem)
551 {
552   CSingleLock lock(m_handleLock);
553   CPluginDirectory *dir = dirFromHandle(handle);
554   if (!dir)
555     return;
556
557   dir->m_success = success;
558   *dir->m_fileResult = *resultItem;
559
560   // set the event to mark that we're done
561   dir->m_fetchComplete.Set();
562 }
563
564 CStdString CPluginDirectory::GetSetting(int handle, const CStdString &strID)
565 {
566   CSingleLock lock(m_handleLock);
567   CPluginDirectory *dir = dirFromHandle(handle);
568   if(dir && dir->m_addon)
569     return dir->m_addon->GetSetting(strID);
570   else
571     return "";
572 }
573
574 void CPluginDirectory::SetSetting(int handle, const CStdString &strID, const CStdString &value)
575 {
576   CSingleLock lock(m_handleLock);
577   CPluginDirectory *dir = dirFromHandle(handle);
578   if(dir && dir->m_addon)
579     dir->m_addon->UpdateSetting(strID, value);
580 }
581
582 void CPluginDirectory::SetContent(int handle, const CStdString &strContent)
583 {
584   CSingleLock lock(m_handleLock);
585   CPluginDirectory *dir = dirFromHandle(handle);
586   if (dir)
587     dir->m_listItems->SetContent(strContent);
588 }
589
590 void CPluginDirectory::SetProperty(int handle, const CStdString &strProperty, const CStdString &strValue)
591 {
592   CSingleLock lock(m_handleLock);
593   CPluginDirectory *dir = dirFromHandle(handle);
594   if (!dir)
595     return;
596   if (strProperty == "fanart_image")
597     dir->m_listItems->SetArt("fanart", strValue);
598   else
599     dir->m_listItems->SetProperty(strProperty, strValue);
600 }
601
602 void CPluginDirectory::CancelDirectory()
603 {
604   m_cancelled = true;
605 }
606
607 float CPluginDirectory::GetProgress() const
608 {
609   if (m_totalItems > 0)
610     return (m_listItems->Size() * 100.0f) / m_totalItems;
611   return 0.0f;
612 }