Root/src/gmenu2x.cpp

1/***************************************************************************
2 * Copyright (C) 2006 by Massimiliano Torromeo *
3 * massimiliano.torromeo@gmail.com *
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 of the License, or *
8 * (at your option) 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 this program; if not, write to the *
17 * Free Software Foundation, Inc., *
18 * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
19 ***************************************************************************/
20
21#include "gp2x.h"
22
23#include "background.h"
24#include "cpu.h"
25#include "debug.h"
26#include "filedialog.h"
27#include "filelister.h"
28#include "font.h"
29#include "gmenu2x.h"
30#include "helppopup.h"
31#include "iconbutton.h"
32#include "inputdialog.h"
33#include "linkapp.h"
34#include "mediamonitor.h"
35#include "menu.h"
36#include "menusettingbool.h"
37#include "menusettingdir.h"
38#include "menusettingfile.h"
39#include "menusettingimage.h"
40#include "menusettingint.h"
41#include "menusettingmultistring.h"
42#include "menusettingrgba.h"
43#include "menusettingstring.h"
44#include "messagebox.h"
45#include "powersaver.h"
46#include "settingsdialog.h"
47#include "textdialog.h"
48#include "wallpaperdialog.h"
49#include "utilities.h"
50
51#include <iostream>
52#include <sstream>
53#include <fstream>
54#include <algorithm>
55#include <stdlib.h>
56#include <unistd.h>
57#include <math.h>
58#include <SDL.h>
59#include <SDL_gfxPrimitives.h>
60#include <signal.h>
61
62#include <sys/statvfs.h>
63#include <errno.h>
64
65#include <sys/fcntl.h> //for battery
66
67#ifdef PLATFORM_PANDORA
68//#include <pnd_container.h>
69//#include <pnd_conf.h>
70//#include <pnd_discovery.h>
71#endif
72
73//for browsing the filesystem
74#include <sys/stat.h>
75#include <sys/types.h>
76#include <dirent.h>
77
78//for soundcard
79#include <sys/ioctl.h>
80#include <linux/soundcard.h>
81
82#include <sys/mman.h>
83
84using namespace std;
85
86#ifndef DEFAULT_WALLPAPER_PATH
87#define DEFAULT_WALLPAPER_PATH \
88  GMENU2X_SYSTEM_DIR "/skins/Default/wallpapers/default.png"
89#endif
90
91#ifdef _CARD_ROOT
92const char *CARD_ROOT = _CARD_ROOT;
93#elif defined(PLATFORM_A320) || defined(PLATFORM_GCW0)
94const char *CARD_ROOT = "/media";
95#else
96const char *CARD_ROOT = "/card";
97#endif
98
99static GMenu2X *app;
100static string gmenu2x_home;
101
102// Note: Keep this in sync with the enum!
103static const char *colorNames[NUM_COLORS] = {
104    "topBarBg",
105    "bottomBarBg",
106    "selectionBg",
107    "messageBoxBg",
108    "messageBoxBorder",
109    "messageBoxSelection",
110};
111
112static enum color stringToColor(const string &name)
113{
114    for (unsigned int i = 0; i < NUM_COLORS; i++) {
115        if (strcmp(colorNames[i], name.c_str()) == 0) {
116            return (enum color)i;
117        }
118    }
119    return (enum color)-1;
120}
121
122static const char *colorToString(enum color c)
123{
124    return colorNames[c];
125}
126
127static void quit_all(int err) {
128    delete app;
129    exit(err);
130}
131
132const string GMenu2X::getHome(void)
133{
134    return gmenu2x_home;
135}
136
137static void set_handler(int signal, void (*handler)(int))
138{
139    struct sigaction sig;
140    sigaction(signal, NULL, &sig);
141    sig.sa_handler = handler;
142    sigaction(signal, &sig, NULL);
143}
144
145int main(int /*argc*/, char * /*argv*/[]) {
146    INFO("---- GMenu2X starting ----\n");
147
148    set_handler(SIGINT, &quit_all);
149    set_handler(SIGSEGV, &quit_all);
150    set_handler(SIGTERM, &quit_all);
151
152    char *home = getenv("HOME");
153    if (home == NULL) {
154        ERROR("Unable to find gmenu2x home directory. The $HOME variable is not defined.\n");
155        return 1;
156    }
157
158    gmenu2x_home = (string)home + (string)"/.gmenu2x";
159    if (!fileExists(gmenu2x_home) && mkdir(gmenu2x_home.c_str(), 0770) < 0) {
160        ERROR("Unable to create gmenu2x home directory.\n");
161        return 1;
162    }
163
164    DEBUG("Home path: %s.\n", gmenu2x_home.c_str());
165
166    app = new GMenu2X();
167    DEBUG("Starting main()\n");
168    app->main();
169
170    return 0;
171}
172
173#ifdef ENABLE_CPUFREQ
174void GMenu2X::initCPULimits() {
175    // Note: These values are for the Dingoo.
176    // The NanoNote does not have cpufreq enabled in its kernel and
177    // other devices are not actively maintained.
178    // TODO: Read min and max from sysfs.
179    cpuFreqMin = 30;
180    cpuFreqMax = 500;
181    cpuFreqSafeMax = 420;
182    cpuFreqMenuDefault = 200;
183    cpuFreqAppDefault = 384;
184    cpuFreqMultiple = 24;
185
186    // Round min and max values to the specified multiple.
187    cpuFreqMin = ((cpuFreqMin + cpuFreqMultiple - 1) / cpuFreqMultiple)
188            * cpuFreqMultiple;
189    cpuFreqMax = (cpuFreqMax / cpuFreqMultiple) * cpuFreqMultiple;
190    cpuFreqSafeMax = (cpuFreqSafeMax / cpuFreqMultiple) * cpuFreqMultiple;
191    cpuFreqMenuDefault = (cpuFreqMenuDefault / cpuFreqMultiple) * cpuFreqMultiple;
192    cpuFreqAppDefault = (cpuFreqAppDefault / cpuFreqMultiple) * cpuFreqMultiple;
193}
194#endif
195
196GMenu2X::GMenu2X()
197    : appToLaunch(nullptr)
198{
199    usbnet = samba = inet = web = false;
200    useSelectionPng = false;
201
202#ifdef ENABLE_CPUFREQ
203    initCPULimits();
204#endif
205    //load config data
206    readConfig();
207
208    halfX = resX/2;
209    halfY = resY/2;
210    bottomBarIconY = resY-18;
211    bottomBarTextY = resY-10;
212
213    /* Do not clear the screen on exit.
214     * This may require an SDL patch available at
215     * https://github.com/mthuurne/opendingux-buildroot/blob
216     * /opendingux-2010.11/package/sdl/sdl-fbcon-clear-onexit.patch
217     */
218    setenv("SDL_FBCON_DONT_CLEAR", "1", 0);
219
220    //Screen
221    if( SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) < 0) {
222        ERROR("Could not initialize SDL: %s\n", SDL_GetError());
223        quit();
224    }
225
226    s = Surface::openOutputSurface(resX, resY, confInt["videoBpp"]);
227
228    bg = NULL;
229    font = NULL;
230    setSkin(confStr["skin"], !fileExists(confStr["wallpaper"]));
231    layers.insert(layers.begin(), make_shared<Background>(*this));
232    initMenu();
233
234#ifdef ENABLE_INOTIFY
235    monitor = new MediaMonitor(CARD_ROOT);
236#endif
237
238    if (!fileExists(confStr["wallpaper"])) {
239        DEBUG("No wallpaper defined; we will take the default one.\n");
240        confStr["wallpaper"] = DEFAULT_WALLPAPER_PATH;
241    }
242
243    initBG();
244
245    /* If a user-specified input.conf file exists, we load it;
246     * otherwise, we load the default one. */
247    string input_file = getHome() + "/input.conf";
248    if (fileExists(input_file.c_str())) {
249        DEBUG("Loading user-specific input.conf file: %s.\n", input_file.c_str());
250    } else {
251        input_file = GMENU2X_SYSTEM_DIR "/input.conf";
252        DEBUG("Loading system input.conf file: %s.\n", input_file.c_str());
253    }
254
255    input.init(input_file, menu.get());
256
257    if (confInt["backlightTimeout"] > 0)
258        PowerSaver::getInstance()->setScreenTimeout( confInt["backlightTimeout"] );
259
260    setInputSpeed();
261#ifdef ENABLE_CPUFREQ
262    setClock(confInt["menuClock"]);
263#endif
264}
265
266GMenu2X::~GMenu2X() {
267    if (PowerSaver::isRunning())
268        delete PowerSaver::getInstance();
269    quit();
270
271    delete font;
272#ifdef ENABLE_INOTIFY
273    delete monitor;
274#endif
275}
276
277void GMenu2X::quit() {
278    fflush(NULL);
279    sc.clear();
280    delete s;
281
282    SDL_Quit();
283    unsetenv("SDL_FBCON_DONT_CLEAR");
284}
285
286void GMenu2X::initBG() {
287    sc.del("bgmain");
288
289    // Load wallpaper.
290    delete bg;
291    bg = Surface::loadImage(confStr["wallpaper"]);
292    if (!bg) {
293        bg = Surface::emptySurface(resX, resY);
294    }
295
296    drawTopBar(bg);
297    drawBottomBar(bg);
298
299    Surface *bgmain = new Surface(bg);
300    sc.add(bgmain,"bgmain");
301
302    Surface *sd = Surface::loadImage("imgs/sd.png", confStr["skin"]);
303    if (sd) sd->blit(bgmain, 3, bottomBarIconY);
304
305#if defined(PLATFORM_A320) || defined(PLATFORM_GCW0)
306    string df = getDiskFree("/boot");
307#else
308    string df = getDiskFree(CARD_ROOT);
309#endif
310    bgmain->write(font, df, 22, bottomBarTextY, Font::HAlignLeft, Font::VAlignMiddle);
311    delete sd;
312
313    cpuX = font->getTextWidth(df)+32;
314#ifdef ENABLE_CPUFREQ
315    Surface *cpu = Surface::loadImage("imgs/cpu.png", confStr["skin"]);
316    if (cpu) cpu->blit(bgmain, cpuX, bottomBarIconY);
317    cpuX += 19;
318    manualX = cpuX+font->getTextWidth("300MHz")+5;
319    delete cpu;
320#else
321    manualX = cpuX;
322#endif
323
324    int serviceX = resX-38;
325    if (usbnet) {
326        if (web) {
327            Surface *webserver = Surface::loadImage(
328                "imgs/webserver.png", confStr["skin"]);
329            if (webserver) webserver->blit(bgmain, serviceX, bottomBarIconY);
330            serviceX -= 19;
331            delete webserver;
332        }
333        if (samba) {
334            Surface *sambaS = Surface::loadImage(
335                "imgs/samba.png", confStr["skin"]);
336            if (sambaS) sambaS->blit(bgmain, serviceX, bottomBarIconY);
337            serviceX -= 19;
338            delete sambaS;
339        }
340        if (inet) {
341            Surface *inetS = Surface::loadImage("imgs/inet.png", confStr["skin"]);
342            if (inetS) inetS->blit(bgmain, serviceX, bottomBarIconY);
343            serviceX -= 19;
344            delete inetS;
345        }
346    }
347
348    bgmain->convertToDisplayFormat();
349}
350
351void GMenu2X::initFont() {
352    delete font;
353    font = Font::defaultFont();
354    if (!font) {
355        ERROR("Cannot function without font; aborting...\n");
356        quit();
357        exit(-1);
358    }
359}
360
361void GMenu2X::initMenu() {
362    //Menu structure handler
363    menu.reset(new Menu(this, ts));
364    for (uint i=0; i<menu->getSections().size(); i++) {
365        //Add virtual links in the applications section
366        if (menu->getSections()[i]=="applications") {
367            menu->addActionLink(i,"Explorer", BIND(&GMenu2X::explorer),tr["Launch an application"],"skin:icons/explorer.png");
368        }
369
370        //Add virtual links in the setting section
371        else if (menu->getSections()[i]=="settings") {
372            menu->addActionLink(i,"GMenu2X",BIND(&GMenu2X::showSettings),tr["Configure GMenu2X's options"],"skin:icons/configure.png");
373            menu->addActionLink(i,tr["Skin"],BIND(&GMenu2X::skinMenu),tr["Configure skin"],"skin:icons/skin.png");
374            menu->addActionLink(i,tr["Wallpaper"],BIND(&GMenu2X::changeWallpaper),tr["Change GMenu2X wallpaper"],"skin:icons/wallpaper.png");
375            if (fileExists(LOG_FILE))
376                menu->addActionLink(i,tr["Log Viewer"],BIND(&GMenu2X::viewLog),tr["Displays last launched program's output"],"skin:icons/ebook.png");
377            menu->addActionLink(i,tr["About"],BIND(&GMenu2X::about),tr["Info about GMenu2X"],"skin:icons/about.png");
378        }
379    }
380
381    menu->skinUpdated();
382
383    menu->setSectionIndex(confInt["section"]);
384    menu->setLinkIndex(confInt["link"]);
385
386    layers.push_back(menu);
387}
388
389void GMenu2X::about() {
390    vector<string> text;
391    string line;
392    string fn(GMENU2X_SYSTEM_DIR);
393    string build_date("Build date: ");
394    fn.append("/about.txt");
395    build_date.append(__DATE__);
396
397    ifstream inf(fn.c_str(), ios_base::in);
398
399    while(getline(inf, line, '\n'))
400        text.push_back(line);
401    inf.close();
402
403    TextDialog td(this, "GMenu2X", build_date, "icons/about.png", &text);
404    td.exec();
405}
406
407void GMenu2X::viewLog() {
408    string logfile = LOG_FILE;
409    if (fileExists(logfile)) {
410        ifstream inf(logfile.c_str(), ios_base::in);
411        if (inf.is_open()) {
412            vector<string> log;
413
414            string line;
415            while (getline(inf, line, '\n'))
416                log.push_back(line);
417            inf.close();
418
419            TextDialog td(this, tr["Log Viewer"], tr["Displays last launched program's output"], "icons/ebook.png", &log);
420            td.exec();
421
422            MessageBox mb(this, tr["Do you want to delete the log file?"], "icons/ebook.png");
423            mb.setButton(InputManager::ACCEPT, tr["Yes"]);
424            mb.setButton(InputManager::CANCEL, tr["No"]);
425            if (mb.exec() == InputManager::ACCEPT) {
426                unlink(logfile.c_str());
427                menu->deleteSelectedLink();
428            }
429        }
430    }
431}
432
433void GMenu2X::readConfig() {
434    string conffile = GMENU2X_SYSTEM_DIR "/gmenu2x.conf";
435    readConfig(conffile);
436
437    conffile = getHome() + "/gmenu2x.conf";
438    readConfig(conffile);
439}
440
441void GMenu2X::readConfig(string conffile) {
442    if (fileExists(conffile)) {
443        ifstream inf(conffile.c_str(), ios_base::in);
444        if (inf.is_open()) {
445            string line;
446            while (getline(inf, line, '\n')) {
447                string::size_type pos = line.find("=");
448                string name = trim(line.substr(0,pos));
449                string value = trim(line.substr(pos+1,line.length()));
450
451                if (value.length()>1 && value.at(0)=='"' && value.at(value.length()-1)=='"')
452                    confStr[name] = value.substr(1,value.length()-2);
453                else
454                    confInt[name] = atoi(value.c_str());
455            }
456            inf.close();
457        }
458    }
459    if (!confStr["lang"].empty())
460        tr.setLang(confStr["lang"]);
461
462    if (!confStr["wallpaper"].empty() && !fileExists(confStr["wallpaper"]))
463        confStr["wallpaper"] = "";
464
465    if (confStr["skin"].empty() || SurfaceCollection::getSkinPath(confStr["skin"]).empty())
466        confStr["skin"] = "Default";
467
468    evalIntConf( &confInt["outputLogs"], 0, 0,1 );
469#ifdef ENABLE_CPUFREQ
470    evalIntConf( &confInt["maxClock"],
471                 cpuFreqSafeMax, cpuFreqMin, cpuFreqMax );
472    evalIntConf( &confInt["menuClock"],
473                 cpuFreqMenuDefault, cpuFreqMin, cpuFreqSafeMax );
474#endif
475    evalIntConf( &confInt["backlightTimeout"], 15, 0,120 );
476    evalIntConf( &confInt["videoBpp"], 32, 16, 32 );
477
478    if (confStr["tvoutEncoding"] != "PAL") confStr["tvoutEncoding"] = "NTSC";
479    resX = constrain( confInt["resolutionX"], 320,1920 );
480    resY = constrain( confInt["resolutionY"], 240,1200 );
481}
482
483void GMenu2X::saveSelection() {
484    if (confInt["saveSelection"] && (
485            confInt["section"] != menu->selSectionIndex()
486            || confInt["link"] != menu->selLinkIndex()
487    )) {
488        writeConfig();
489    }
490}
491
492void GMenu2X::writeConfig() {
493    string conffile = getHome() + "/gmenu2x.conf";
494    ofstream inf(conffile.c_str());
495    if (inf.is_open()) {
496        ConfStrHash::iterator endS = confStr.end();
497        for(ConfStrHash::iterator curr = confStr.begin(); curr != endS; curr++)
498            inf << curr->first << "=\"" << curr->second << "\"" << endl;
499
500        ConfIntHash::iterator endI = confInt.end();
501        for(ConfIntHash::iterator curr = confInt.begin(); curr != endI; curr++)
502            inf << curr->first << "=" << curr->second << endl;
503
504        inf.close();
505    }
506}
507
508void GMenu2X::writeSkinConfig() {
509    string conffile = getHome() + "/skins/";
510    if (!fileExists(conffile))
511      mkdir(conffile.c_str(), 0770);
512    conffile = conffile + confStr["skin"];
513    if (!fileExists(conffile))
514      mkdir(conffile.c_str(), 0770);
515    conffile = conffile + "/skin.conf";
516
517    ofstream inf(conffile.c_str());
518    if (inf.is_open()) {
519        ConfStrHash::iterator endS = skinConfStr.end();
520        for(ConfStrHash::iterator curr = skinConfStr.begin(); curr != endS; curr++)
521            inf << curr->first << "=\"" << curr->second << "\"" << endl;
522
523        ConfIntHash::iterator endI = skinConfInt.end();
524        for(ConfIntHash::iterator curr = skinConfInt.begin(); curr != endI; curr++)
525            inf << curr->first << "=" << curr->second << endl;
526
527        int i;
528        for (i = 0; i < NUM_COLORS; ++i) {
529            inf << colorToString((enum color)i) << "=#";
530            inf.width(2); inf.fill('0');
531            inf << right << hex << skinConfColors[i].r;
532            inf.width(2); inf.fill('0');
533            inf << right << hex << skinConfColors[i].g;
534            inf.width(2); inf.fill('0');
535            inf << right << hex << skinConfColors[i].b;
536            inf.width(2); inf.fill('0');
537            inf << right << hex << skinConfColors[i].a << endl;
538        }
539
540        inf.close();
541    }
542}
543
544void GMenu2X::readTmp() {
545    lastSelectorElement = -1;
546    if (fileExists("/tmp/gmenu2x.tmp")) {
547        ifstream inf("/tmp/gmenu2x.tmp", ios_base::in);
548        if (inf.is_open()) {
549            string line;
550            string section = "";
551            while (getline(inf, line, '\n')) {
552                string::size_type pos = line.find("=");
553                string name = trim(line.substr(0,pos));
554                string value = trim(line.substr(pos+1,line.length()));
555
556                if (name=="section")
557                    menu->setSectionIndex(atoi(value.c_str()));
558                else if (name=="link")
559                    menu->setLinkIndex(atoi(value.c_str()));
560                else if (name=="selectorelem")
561                    lastSelectorElement = atoi(value.c_str());
562                else if (name=="selectordir")
563                    lastSelectorDir = value;
564            }
565            inf.close();
566        }
567    }
568}
569
570void GMenu2X::writeTmp(int selelem, const string &selectordir) {
571    string conffile = "/tmp/gmenu2x.tmp";
572    ofstream inf(conffile.c_str());
573    if (inf.is_open()) {
574        inf << "section=" << menu->selSectionIndex() << endl;
575        inf << "link=" << menu->selLinkIndex() << endl;
576        if (selelem>-1)
577            inf << "selectorelem=" << selelem << endl;
578        if (!selectordir.empty())
579            inf << "selectordir=" << selectordir << endl;
580        inf.close();
581    }
582}
583
584void GMenu2X::main() {
585    if (!fileExists(CARD_ROOT))
586        CARD_ROOT = "";
587
588    appToLaunch = nullptr;
589
590    // Recover last session
591    readTmp();
592    if (lastSelectorElement > -1 && menu->selLinkApp() &&
593                (!menu->selLinkApp()->getSelectorDir().empty()
594                 || !lastSelectorDir.empty()))
595        menu->selLinkApp()->selector(lastSelectorElement, lastSelectorDir);
596
597    while (true) {
598        // Remove dismissed layers from the stack.
599        for (auto it = layers.begin(); it != layers.end(); ) {
600            if ((*it)->getStatus() == Layer::Status::DISMISSED) {
601                it = layers.erase(it);
602            } else {
603                ++it;
604            }
605        }
606
607        // Run animations.
608        bool animating = false;
609        for (auto layer : layers) {
610            animating |= layer->runAnimations();
611        }
612
613        // Paint layers.
614        for (auto layer : layers) {
615            layer->paint(*s);
616        }
617        if (appToLaunch) {
618            break;
619        }
620        s->flip();
621
622        // Handle touchscreen events.
623        if (ts.available()) {
624            ts.poll();
625            for (auto it = layers.rbegin(); it != layers.rend(); ++it) {
626                if ((*it)->handleTouchscreen(ts)) {
627                    break;
628                }
629            }
630        }
631
632        // Handle other input events.
633        InputManager::Button button;
634        bool gotEvent;
635        const bool wait = !animating;
636        do {
637            gotEvent = input.getButton(&button, wait);
638        } while (wait && !gotEvent);
639        if (gotEvent) {
640            for (auto it = layers.rbegin(); it != layers.rend(); ++it) {
641                if ((*it)->handleButtonPress(button)) {
642                    break;
643                }
644            }
645        }
646    }
647
648    if (appToLaunch) {
649        appToLaunch->drawRun();
650        appToLaunch->launch(fileToLaunch);
651    }
652}
653
654void GMenu2X::explorer() {
655    FileDialog fd(this, ts, tr["Select an application"], "dge,sh,bin,py,elf,");
656    if (fd.exec()) {
657        if (confInt["saveSelection"] && (confInt["section"]!=menu->selSectionIndex() || confInt["link"]!=menu->selLinkIndex()))
658            writeConfig();
659
660        string command = cmdclean(fd.getPath()+"/"+fd.getFile());
661        chdir(fd.getPath().c_str());
662        quit();
663#ifdef ENABLE_CPUFREQ
664        setClock(cpuFreqAppDefault);
665#endif
666        execlp("/bin/sh","/bin/sh","-c",command.c_str(),NULL);
667
668        //if execution continues then something went wrong and as we already called SDL_Quit we cannot continue
669        //try relaunching gmenu2x
670        ERROR("Error executing selected application, re-launching gmenu2x\n");
671        main();
672    }
673}
674
675void GMenu2X::queueLaunch(LinkApp *app, const std::string &file) {
676    appToLaunch = app;
677    fileToLaunch = file;
678}
679
680void GMenu2X::showHelpPopup() {
681    layers.push_back(make_shared<HelpPopup>(*this));
682}
683
684void GMenu2X::showSettings() {
685#ifdef ENABLE_CPUFREQ
686    int curMenuClock = confInt["menuClock"];
687#endif
688    bool showRootFolder = fileExists(CARD_ROOT);
689
690    FileLister fl_tr(GMENU2X_SYSTEM_DIR "/translations");
691    fl_tr.browse();
692
693    string tr_path = getHome() + "/translations";
694    if (fileExists(tr_path)) {
695        fl_tr.setPath(tr_path, false);
696        fl_tr.browse(false);
697    }
698
699    fl_tr.insertFile("English");
700    string lang = tr.lang();
701
702    vector<string> encodings;
703    encodings.push_back("NTSC");
704    encodings.push_back("PAL");
705
706    SettingsDialog sd(this, input, ts, tr["Settings"]);
707    sd.addSetting(new MenuSettingMultiString(this, ts, tr["Language"], tr["Set the language used by GMenu2X"], &lang, &fl_tr.getFiles()));
708    sd.addSetting(new MenuSettingBool(this, ts, tr["Save last selection"], tr["Save the last selected link and section on exit"], &confInt["saveSelection"]));
709#ifdef ENABLE_CPUFREQ
710    sd.addSetting(new MenuSettingInt(this, ts, tr["Clock for GMenu2X"], tr["Set the cpu working frequency when running GMenu2X"], &confInt["menuClock"], cpuFreqMin, cpuFreqSafeMax, cpuFreqMultiple));
711    sd.addSetting(new MenuSettingInt(this, ts, tr["Maximum overclock"], tr["Set the maximum overclock for launching links"], &confInt["maxClock"], cpuFreqMin, cpuFreqMax, cpuFreqMultiple));
712#endif
713    sd.addSetting(new MenuSettingBool(this, ts, tr["Output logs"], tr["Logs the output of the links. Use the Log Viewer to read them."], &confInt["outputLogs"]));
714    sd.addSetting(new MenuSettingInt(this, ts, tr["Screen Timeout"], tr["Set screen's backlight timeout in seconds"], &confInt["backlightTimeout"], 0, 120));
715// sd.addSetting(new MenuSettingMultiString(this, ts, tr["Tv-Out encoding"], tr["Encoding of the tv-out signal"], &confStr["tvoutEncoding"], &encodings));
716    sd.addSetting(new MenuSettingBool(this, ts, tr["Show root"], tr["Show root folder in the file selection dialogs"], &showRootFolder));
717
718    if (sd.exec() && sd.edited()) {
719#ifdef ENABLE_CPUFREQ
720        if (curMenuClock != confInt["menuClock"]) setClock(confInt["menuClock"]);
721#endif
722
723        if (confInt["backlightTimeout"] == 0) {
724            if (PowerSaver::isRunning())
725                delete PowerSaver::getInstance();
726        } else {
727            PowerSaver::getInstance()->setScreenTimeout( confInt["backlightTimeout"] );
728        }
729
730        if (lang == "English") lang = "";
731        if (lang != tr.lang()) {
732            tr.setLang(lang);
733            confStr["lang"] = lang;
734        }
735        /*if (fileExists(CARD_ROOT) && !showRootFolder)
736            unlink(CARD_ROOT);
737        else if (!fileExists(CARD_ROOT) && showRootFolder)
738            symlink("/", CARD_ROOT);*/
739        //WARNING: the above might be dangerous with CARD_ROOT set to /
740        writeConfig();
741    }
742}
743
744void GMenu2X::skinMenu() {
745    FileLister fl_sk(getHome() + "/skins", true, false);
746    fl_sk.addExclude("..");
747    fl_sk.browse();
748    fl_sk.setPath(GMENU2X_SYSTEM_DIR "/skins", false);
749    fl_sk.browse(false);
750
751    string curSkin = confStr["skin"];
752
753    SettingsDialog sd(this, input, ts, tr["Skin"]);
754    sd.addSetting(new MenuSettingMultiString(this, ts, tr["Skin"], tr["Set the skin used by GMenu2X"], &confStr["skin"], &fl_sk.getDirectories()));
755    sd.addSetting(new MenuSettingRGBA(this, ts, tr["Top Bar"], tr["Color of the top bar"], &skinConfColors[COLOR_TOP_BAR_BG]));
756    sd.addSetting(new MenuSettingRGBA(this, ts, tr["Bottom Bar"], tr["Color of the bottom bar"], &skinConfColors[COLOR_BOTTOM_BAR_BG]));
757    sd.addSetting(new MenuSettingRGBA(this, ts, tr["Selection"], tr["Color of the selection and other interface details"], &skinConfColors[COLOR_SELECTION_BG]));
758    sd.addSetting(new MenuSettingRGBA(this, ts, tr["Message Box"], tr["Background color of the message box"], &skinConfColors[COLOR_MESSAGE_BOX_BG]));
759    sd.addSetting(new MenuSettingRGBA(this, ts, tr["Message Box Border"], tr["Border color of the message box"], &skinConfColors[COLOR_MESSAGE_BOX_BORDER]));
760    sd.addSetting(new MenuSettingRGBA(this, ts, tr["Message Box Selection"], tr["Color of the selection of the message box"], &skinConfColors[COLOR_MESSAGE_BOX_SELECTION]));
761
762    if (sd.exec() && sd.edited()) {
763        if (curSkin != confStr["skin"]) {
764            setSkin(confStr["skin"]);
765            writeConfig();
766        }
767        writeSkinConfig();
768        initBG();
769    }
770}
771
772void GMenu2X::setSkin(const string &skin, bool setWallpaper) {
773    confStr["skin"] = skin;
774
775    //Clear previous skin settings
776    skinConfStr.clear();
777    skinConfInt.clear();
778
779    DEBUG("GMenu2X: setting new skin %s.\n", skin.c_str());
780
781    //clear collection and change the skin path
782    sc.clear();
783    sc.setSkin(skin);
784
785    //reset colors to the default values
786    skinConfColors[COLOR_TOP_BAR_BG] = (RGBAColor){255,255,255,130};
787    skinConfColors[COLOR_BOTTOM_BAR_BG] = (RGBAColor){255,255,255,130};
788    skinConfColors[COLOR_SELECTION_BG] = (RGBAColor){255,255,255,130};
789    skinConfColors[COLOR_MESSAGE_BOX_BG] = (RGBAColor){255,255,255,255};
790    skinConfColors[COLOR_MESSAGE_BOX_BORDER] = (RGBAColor){80,80,80,255};
791    skinConfColors[COLOR_MESSAGE_BOX_SELECTION] = (RGBAColor){160,160,160,255};
792
793    /* Load skin settings from user directory if present,
794     * or from the system directory. */
795    string skinconfname = getHome() + "/skins/" + skin + "/skin.conf";
796    if (!fileExists(skinconfname))
797      skinconfname = GMENU2X_SYSTEM_DIR "/skins/" + skin + "/skin.conf";
798
799    if (fileExists(skinconfname)) {
800        ifstream skinconf(skinconfname.c_str(), ios_base::in);
801        if (skinconf.is_open()) {
802            string line;
803            while (getline(skinconf, line, '\n')) {
804                line = trim(line);
805                DEBUG("skinconf: '%s'\n", line.c_str());
806                string::size_type pos = line.find("=");
807                string name = trim(line.substr(0,pos));
808                string value = trim(line.substr(pos+1,line.length()));
809
810                if (value.length()>0) {
811                    if (value.length()>1 && value.at(0)=='"' && value.at(value.length()-1)=='"')
812                        skinConfStr[name] = value.substr(1,value.length()-2);
813                    else if (value.at(0) == '#')
814                        skinConfColors[stringToColor(name)] = strtorgba( value.substr(1,value.length()) );
815                    else
816                        skinConfInt[name] = atoi(value.c_str());
817                }
818            }
819            skinconf.close();
820
821            if (setWallpaper && !skinConfStr["wallpaper"].empty()) {
822                string fp = sc.getSkinFilePath("wallpapers/" + skinConfStr["wallpaper"]);
823                if (!fp.empty())
824                    confStr["wallpaper"] = fp;
825                else
826                    WARNING("Unable to find wallpaper defined on skin %s\n", skin.c_str());
827            }
828        }
829    }
830
831    evalIntConf(&skinConfInt["topBarHeight"], 40, 32, 120);
832    evalIntConf(&skinConfInt["bottomBarHeight"], 20, 20, 120);
833    evalIntConf(&skinConfInt["linkHeight"], 40, 32, 120);
834    evalIntConf(&skinConfInt["linkWidth"], 60, 32, 120);
835
836    if (menu != NULL) menu->skinUpdated();
837
838    //Selection png
839    useSelectionPng = sc.addSkinRes("imgs/selection.png", false) != NULL;
840
841    //font
842    initFont();
843}
844
845void GMenu2X::showManual() {
846    menu->selLinkApp()->showManual();
847}
848
849void GMenu2X::showContextMenu() {
850    layers.push_back(make_shared<ContextMenu>(*this, *menu));
851}
852
853void GMenu2X::changeWallpaper() {
854    WallpaperDialog wp(this, ts);
855    if (wp.exec() && confStr["wallpaper"] != wp.wallpaper) {
856        confStr["wallpaper"] = wp.wallpaper;
857        initBG();
858        writeConfig();
859    }
860}
861
862void GMenu2X::addLink() {
863    FileDialog fd(this, ts, tr["Select an application"], "dge,sh,bin,py,elf,");
864    if (fd.exec())
865        menu->addLink(fd.getPath(), fd.getFile());
866}
867
868void GMenu2X::editLink() {
869    LinkApp *linkApp = menu->selLinkApp();
870    if (!linkApp) return;
871
872    vector<string> pathV;
873    split(pathV,linkApp->getFile(),"/");
874    string oldSection = "";
875    if (pathV.size()>1)
876        oldSection = pathV[pathV.size()-2];
877    string newSection = oldSection;
878
879    string linkTitle = linkApp->getTitle();
880    string linkDescription = linkApp->getDescription();
881    string linkIcon = linkApp->getIcon();
882    string linkManual = linkApp->getManual();
883    string linkSelFilter = linkApp->getSelectorFilter();
884    string linkSelDir = linkApp->getSelectorDir();
885    bool linkSelBrowser = linkApp->getSelectorBrowser();
886    string linkSelScreens = linkApp->getSelectorScreens();
887    string linkSelAliases = linkApp->getAliasFile();
888    int linkClock = linkApp->clock();
889
890    string diagTitle = tr.translate("Edit link: $1",linkTitle.c_str(),NULL);
891    string diagIcon = linkApp->getIconPath();
892
893    SettingsDialog sd(this, input, ts, diagTitle, diagIcon);
894    if (!linkApp->isOpk()) {
895        sd.addSetting(new MenuSettingString(this, ts, tr["Title"], tr["Link title"], &linkTitle, diagTitle, diagIcon));
896        sd.addSetting(new MenuSettingString(this, ts, tr["Description"], tr["Link description"], &linkDescription, diagTitle, diagIcon));
897        sd.addSetting(new MenuSettingMultiString(this, ts, tr["Section"], tr["The section this link belongs to"], &newSection, &menu->getSections()));
898        sd.addSetting(new MenuSettingImage(this, ts, tr["Icon"],
899                        tr.translate("Select an icon for the link: $1",
900                            linkTitle.c_str(), NULL), &linkIcon, "png"));
901        sd.addSetting(new MenuSettingFile(this, ts, tr["Manual"],
902                        tr["Select a graphic/textual manual or a readme"],
903                        &linkManual, "man.png,txt"));
904    }
905    if (!linkApp->isOpk() || !linkApp->getSelectorDir().empty()) {
906        sd.addSetting(new MenuSettingDir(this, ts, tr["Selector Directory"], tr["Directory to scan for the selector"], &linkSelDir));
907        sd.addSetting(new MenuSettingBool(this, ts, tr["Selector Browser"], tr["Allow the selector to change directory"], &linkSelBrowser));
908    }
909#ifdef ENABLE_CPUFREQ
910    sd.addSetting(new MenuSettingInt(this, ts, tr["Clock frequency"], tr["Cpu clock frequency to set when launching this link"], &linkClock, cpuFreqMin, confInt["maxClock"], cpuFreqMultiple));
911#endif
912    if (!linkApp->isOpk()) {
913        sd.addSetting(new MenuSettingString(this, ts, tr["Selector Filter"], tr["Selector filter (Separate values with a comma)"], &linkSelFilter, diagTitle, diagIcon));
914        sd.addSetting(new MenuSettingDir(this, ts, tr["Selector Screenshots"], tr["Directory of the screenshots for the selector"], &linkSelScreens));
915        sd.addSetting(new MenuSettingFile(this, ts, tr["Selector Aliases"], tr["File containing a list of aliases for the selector"], &linkSelAliases));
916#if defined(PLATFORM_A320) || defined(PLATFORM_GCW0)
917        sd.addSetting(new MenuSettingBool(this, ts, tr["Display Console"], tr["Must be enabled for console-based applications"], &linkApp->consoleApp));
918#endif
919    }
920
921    if (sd.exec() && sd.edited()) {
922        linkApp->setTitle(linkTitle);
923        linkApp->setDescription(linkDescription);
924        linkApp->setIcon(linkIcon);
925        linkApp->setManual(linkManual);
926        linkApp->setSelectorFilter(linkSelFilter);
927        linkApp->setSelectorDir(linkSelDir);
928        linkApp->setSelectorBrowser(linkSelBrowser);
929        linkApp->setSelectorScreens(linkSelScreens);
930        linkApp->setAliasFile(linkSelAliases);
931        linkApp->setClock(linkClock);
932
933        INFO("New Section: '%s'\n", newSection.c_str());
934
935        //if section changed move file and update link->file
936        if (oldSection!=newSection) {
937            vector<string>::const_iterator newSectionIndex = find(menu->getSections().begin(),menu->getSections().end(),newSection);
938            if (newSectionIndex==menu->getSections().end()) return;
939            string newFileName = "sections/"+newSection+"/"+linkTitle;
940            uint x=2;
941            while (fileExists(newFileName)) {
942                string id = "";
943                stringstream ss; ss << x; ss >> id;
944                newFileName = "sections/"+newSection+"/"+linkTitle+id;
945                x++;
946            }
947            rename(linkApp->getFile().c_str(),newFileName.c_str());
948            linkApp->renameFile(newFileName);
949
950            INFO("New section index: %i.\n", newSectionIndex - menu->getSections().begin());
951
952            menu->linkChangeSection(menu->selLinkIndex(), menu->selSectionIndex(), newSectionIndex - menu->getSections().begin());
953        }
954        linkApp->save();
955    }
956}
957
958void GMenu2X::deleteLink() {
959    if (menu->selLinkApp()!=NULL) {
960        MessageBox mb(this, tr.translate("Deleting $1",menu->selLink()->getTitle().c_str(),NULL)+"\n"+tr["Are you sure?"], menu->selLink()->getIconPath());
961        mb.setButton(InputManager::ACCEPT, tr["Yes"]);
962        mb.setButton(InputManager::CANCEL, tr["No"]);
963        if (mb.exec() == InputManager::ACCEPT)
964            menu->deleteSelectedLink();
965    }
966}
967
968void GMenu2X::addSection() {
969    InputDialog id(this, input, ts, tr["Insert a name for the new section"]);
970    if (id.exec()) {
971        //only if a section with the same name does not exist
972        if (find(menu->getSections().begin(), menu->getSections().end(), id.getInput())
973                == menu->getSections().end()) {
974            //section directory doesn't exists
975            if (menu->addSection(id.getInput()))
976                menu->setSectionIndex( menu->getSections().size()-1 ); //switch to the new section
977        }
978    }
979}
980
981void GMenu2X::renameSection() {
982    InputDialog id(this, input, ts, tr["Insert a new name for this section"],menu->selSection());
983    if (id.exec()) {
984        //only if a section with the same name does not exist & !samename
985        if (menu->selSection() != id.getInput()
986         && find(menu->getSections().begin(),menu->getSections().end(), id.getInput())
987                == menu->getSections().end()) {
988            //section directory doesn't exists
989            string newsectiondir = getHome() + "/sections/" + id.getInput();
990            string sectiondir = getHome() + "/sections/" + menu->selSection();
991
992            if (!rename(sectiondir.c_str(), newsectiondir.c_str())) {
993                string oldpng = menu->selSection() + ".png";
994                string newpng = id.getInput() + ".png";
995                string oldicon = sc.getSkinFilePath(oldpng);
996                string newicon = sc.getSkinFilePath(newpng);
997
998                if (!oldicon.empty() && newicon.empty()) {
999                    newicon = oldicon;
1000                    newicon.replace(newicon.find(oldpng), oldpng.length(), newpng);
1001
1002                    if (!fileExists(newicon)) {
1003                        rename(oldicon.c_str(), newicon.c_str());
1004                        sc.move("skin:"+oldpng, "skin:"+newpng);
1005                    }
1006                }
1007                menu->renameSection(menu->selSectionIndex(), id.getInput());
1008            }
1009        }
1010    }
1011}
1012
1013void GMenu2X::deleteSection() {
1014    MessageBox mb(this,tr["You will lose all the links in this section."]+"\n"+tr["Are you sure?"]);
1015    mb.setButton(InputManager::ACCEPT, tr["Yes"]);
1016    mb.setButton(InputManager::CANCEL, tr["No"]);
1017    if (mb.exec() == InputManager::ACCEPT) {
1018
1019        if (rmtree(getHome() + "/sections/" + menu->selSection()))
1020            menu->deleteSelectedSection();
1021    }
1022}
1023
1024void GMenu2X::scanner() {
1025    Surface scanbg(bg);
1026    drawButton(&scanbg, "cancel", tr["Exit"],
1027    drawButton(&scanbg, "accept", "", 5)-10);
1028    scanbg.write(font,tr["Link Scanner"],halfX,7,Font::HAlignCenter,Font::VAlignMiddle);
1029    scanbg.convertToDisplayFormat();
1030
1031    uint lineY = 42;
1032
1033#ifdef PLATFORM_PANDORA
1034    //char *configpath = pnd_conf_query_searchpath();
1035#else
1036#ifdef ENABLE_CPUFREQ
1037    setSafeMaxClock();
1038#endif
1039
1040    scanbg.write(font,tr["Scanning filesystem..."],5,lineY);
1041    scanbg.blit(s,0,0);
1042    s->flip();
1043    lineY += 26;
1044
1045    vector<string> files;
1046    scanPath(CARD_ROOT, &files);
1047
1048    stringstream ss;
1049    ss << files.size();
1050    string str = "";
1051    ss >> str;
1052    scanbg.write(font,tr.translate("$1 files found.",str.c_str(),NULL),5,lineY);
1053    lineY += 26;
1054    scanbg.write(font,tr["Creating links..."],5,lineY);
1055    scanbg.blit(s,0,0);
1056    s->flip();
1057    lineY += 26;
1058
1059    string path, file;
1060    string::size_type pos;
1061    uint linkCount = 0;
1062
1063    for (uint i = 0; i<files.size(); i++) {
1064        pos = files[i].rfind("/");
1065        if (pos!=string::npos && pos>0) {
1066            path = files[i].substr(0, pos+1);
1067            file = files[i].substr(pos+1, files[i].length());
1068            if (menu->addLink(path,file,"found "+file.substr(file.length()-3,3)))
1069                linkCount++;
1070        }
1071    }
1072
1073    ss.clear();
1074    ss << linkCount;
1075    ss >> str;
1076    scanbg.write(font,tr.translate("$1 links created.",str.c_str(),NULL),5,lineY);
1077    scanbg.blit(s,0,0);
1078    s->flip();
1079    lineY += 26;
1080
1081#ifdef ENABLE_CPUFREQ
1082    setMenuClock();
1083#endif
1084#endif
1085
1086    InputManager::Button button;
1087    do {
1088        button = input.waitForPressedButton();
1089    } while ((button != InputManager::SETTINGS)
1090                && (button != InputManager::ACCEPT)
1091                && (button != InputManager::CANCEL));
1092}
1093
1094void GMenu2X::scanPath(string path, vector<string> *files) {
1095    DIR *dirp;
1096    struct stat st;
1097    struct dirent *dptr;
1098    string filepath, ext;
1099
1100    if (path[path.length()-1]!='/') path += "/";
1101    if ((dirp = opendir(path.c_str())) == NULL) return;
1102
1103    while ((dptr = readdir(dirp))) {
1104        if (dptr->d_name[0]=='.')
1105            continue;
1106        filepath = path+dptr->d_name;
1107        int statRet = stat(filepath.c_str(), &st);
1108        if (S_ISDIR(st.st_mode))
1109            scanPath(filepath, files);
1110        if (statRet != -1) {
1111            ext = filepath.substr(filepath.length()-4,4);
1112#if defined(PLATFORM_A320) || defined(PLATFORM_GCW0) || defined(PLATFORM_NANONOTE)
1113            if (ext==".dge")
1114#else
1115            if (ext==".pxml")
1116#endif
1117                files->push_back(filepath);
1118        }
1119    }
1120
1121    closedir(dirp);
1122}
1123
1124typedef struct {
1125    unsigned short batt;
1126    unsigned short remocon;
1127} MMSP2ADC;
1128
1129void GMenu2X::setInputSpeed() {
1130    SDL_EnableKeyRepeat(250, 150);
1131}
1132
1133#ifdef ENABLE_CPUFREQ
1134void GMenu2X::setClock(unsigned mhz) {
1135    mhz = constrain(mhz, cpuFreqMin, confInt["maxClock"]);
1136#if defined(PLATFORM_A320) || defined(PLATFORM_GCW0) || defined(PLATFORM_NANONOTE)
1137    jz_cpuspeed(mhz);
1138#endif
1139}
1140#endif
1141
1142string GMenu2X::getDiskFree(const char *path) {
1143    string df = "";
1144    struct statvfs b;
1145
1146    int ret = statvfs(path, &b);
1147    if (ret == 0) {
1148        // Make sure that the multiplication happens in 64 bits.
1149        unsigned long freeMiB =
1150                ((unsigned long long)b.f_bfree * b.f_bsize) / (1024 * 1024);
1151        unsigned long totalMiB =
1152                ((unsigned long long)b.f_blocks * b.f_frsize) / (1024 * 1024);
1153        stringstream ss;
1154        if (totalMiB >= 10000) {
1155            ss << (freeMiB / 1024) << "." << ((freeMiB % 1024) * 10) / 1024 << "/"
1156               << (totalMiB / 1024) << "." << ((totalMiB % 1024) * 10) / 1024 << "GiB";
1157        } else {
1158            ss << freeMiB << "/" << totalMiB << "MiB";
1159        }
1160        ss >> df;
1161    } else WARNING("statvfs failed with error '%s'.\n", strerror(errno));
1162    return df;
1163}
1164
1165int GMenu2X::drawButton(IconButton *btn, int x, int y) {
1166    if (y<0) y = resY+y;
1167    btn->setPosition(x, y-7);
1168    btn->paint();
1169    return x+btn->getRect().w+6;
1170}
1171
1172int GMenu2X::drawButton(Surface *s, const string &btn, const string &text, int x, int y) {
1173    if (y<0) y = resY+y;
1174    SDL_Rect re = { static_cast<Sint16>(x), static_cast<Sint16>(y - 7), 0, 16 };
1175    if (sc.skinRes("imgs/buttons/"+btn+".png") != NULL) {
1176        sc["imgs/buttons/"+btn+".png"]->blit(s, x, y-7);
1177        re.w = sc["imgs/buttons/"+btn+".png"]->width() + 3;
1178        s->write(font, text, x+re.w, y, Font::HAlignLeft, Font::VAlignMiddle);
1179        re.w += font->getTextWidth(text);
1180    }
1181    return x+re.w+6;
1182}
1183
1184int GMenu2X::drawButtonRight(Surface *s, const string &btn, const string &text, int x, int y) {
1185    if (y<0) y = resY+y;
1186    if (sc.skinRes("imgs/buttons/"+btn+".png") != NULL) {
1187        x -= 16;
1188        sc["imgs/buttons/"+btn+".png"]->blit(s, x, y-7);
1189        x -= 3;
1190        s->write(font, text, x, y, Font::HAlignRight, Font::VAlignMiddle);
1191        return x-6-font->getTextWidth(text);
1192    }
1193    return x-6;
1194}
1195
1196void GMenu2X::drawScrollBar(uint pageSize, uint totalSize, uint pagePos) {
1197    if (totalSize <= pageSize) {
1198        // Everything fits on one screen, no scroll bar needed.
1199        return;
1200    }
1201
1202    unsigned int top, height;
1203    tie(top, height) = getContentArea();
1204    top += 1;
1205    height -= 2;
1206
1207    s->rectangle(resX - 8, top, 7, height, skinConfColors[COLOR_SELECTION_BG]);
1208    top += 2;
1209    height -= 4;
1210
1211    const uint barSize = height * pageSize / totalSize;
1212    const uint barPos = (height - barSize) * pagePos / (totalSize - pageSize);
1213
1214    s->box(resX - 6, top + barPos, 3, barSize,
1215            skinConfColors[COLOR_SELECTION_BG]);
1216}
1217
1218void GMenu2X::drawTopBar(Surface *s) {
1219    Surface *bar = sc.skinRes("imgs/topbar.png", false);
1220    if (bar) {
1221        bar->blit(s, 0, 0);
1222    } else {
1223        const int h = skinConfInt["topBarHeight"];
1224        s->box(0, 0, resX, h, skinConfColors[COLOR_TOP_BAR_BG]);
1225    }
1226}
1227
1228void GMenu2X::drawBottomBar(Surface *s) {
1229    Surface *bar = sc.skinRes("imgs/bottombar.png", false);
1230    if (bar) {
1231        bar->blit(s, 0, resY-bar->height());
1232    } else {
1233        const int h = skinConfInt["bottomBarHeight"];
1234        s->box(0, resY - h, resX, h, skinConfColors[COLOR_BOTTOM_BAR_BG]);
1235    }
1236}
1237

Archive Download this file



interactive