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(getHome()+"/log.txt"))
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 = getHome()+"/log.txt";
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                sync();
428                menu->deleteSelectedLink();
429            }
430        }
431    }
432}
433
434void GMenu2X::readConfig() {
435    string conffile = GMENU2X_SYSTEM_DIR "/gmenu2x.conf";
436    readConfig(conffile);
437
438    conffile = getHome() + "/gmenu2x.conf";
439    readConfig(conffile);
440}
441
442void GMenu2X::readConfig(string conffile) {
443    if (fileExists(conffile)) {
444        ifstream inf(conffile.c_str(), ios_base::in);
445        if (inf.is_open()) {
446            string line;
447            while (getline(inf, line, '\n')) {
448                string::size_type pos = line.find("=");
449                string name = trim(line.substr(0,pos));
450                string value = trim(line.substr(pos+1,line.length()));
451
452                if (value.length()>1 && value.at(0)=='"' && value.at(value.length()-1)=='"')
453                    confStr[name] = value.substr(1,value.length()-2);
454                else
455                    confInt[name] = atoi(value.c_str());
456            }
457            inf.close();
458        }
459    }
460    if (!confStr["lang"].empty())
461        tr.setLang(confStr["lang"]);
462
463    if (!confStr["wallpaper"].empty() && !fileExists(confStr["wallpaper"]))
464        confStr["wallpaper"] = "";
465
466    if (confStr["skin"].empty() || SurfaceCollection::getSkinPath(confStr["skin"]).empty())
467        confStr["skin"] = "Default";
468
469    evalIntConf( &confInt["outputLogs"], 0, 0,1 );
470#ifdef ENABLE_CPUFREQ
471    evalIntConf( &confInt["maxClock"],
472                 cpuFreqSafeMax, cpuFreqMin, cpuFreqMax );
473    evalIntConf( &confInt["menuClock"],
474                 cpuFreqMenuDefault, cpuFreqMin, cpuFreqSafeMax );
475#endif
476    evalIntConf( &confInt["backlightTimeout"], 15, 0,120 );
477    evalIntConf( &confInt["videoBpp"], 32, 16, 32 );
478
479    if (confStr["tvoutEncoding"] != "PAL") confStr["tvoutEncoding"] = "NTSC";
480    resX = constrain( confInt["resolutionX"], 320,1920 );
481    resY = constrain( confInt["resolutionY"], 240,1200 );
482}
483
484void GMenu2X::saveSelection() {
485    if (confInt["saveSelection"] && (
486            confInt["section"] != menu->selSectionIndex()
487            || confInt["link"] != menu->selLinkIndex()
488    )) {
489        writeConfig();
490    }
491}
492
493void GMenu2X::writeConfig() {
494    string conffile = getHome() + "/gmenu2x.conf";
495    ofstream inf(conffile.c_str());
496    if (inf.is_open()) {
497        ConfStrHash::iterator endS = confStr.end();
498        for(ConfStrHash::iterator curr = confStr.begin(); curr != endS; curr++)
499            inf << curr->first << "=\"" << curr->second << "\"" << endl;
500
501        ConfIntHash::iterator endI = confInt.end();
502        for(ConfIntHash::iterator curr = confInt.begin(); curr != endI; curr++)
503            inf << curr->first << "=" << curr->second << endl;
504
505        inf.close();
506        sync();
507    }
508}
509
510void GMenu2X::writeSkinConfig() {
511    string conffile = getHome() + "/skins/";
512    if (!fileExists(conffile))
513      mkdir(conffile.c_str(), 0770);
514    conffile = conffile + confStr["skin"];
515    if (!fileExists(conffile))
516      mkdir(conffile.c_str(), 0770);
517    conffile = conffile + "/skin.conf";
518
519    ofstream inf(conffile.c_str());
520    if (inf.is_open()) {
521        ConfStrHash::iterator endS = skinConfStr.end();
522        for(ConfStrHash::iterator curr = skinConfStr.begin(); curr != endS; curr++)
523            inf << curr->first << "=\"" << curr->second << "\"" << endl;
524
525        ConfIntHash::iterator endI = skinConfInt.end();
526        for(ConfIntHash::iterator curr = skinConfInt.begin(); curr != endI; curr++)
527            inf << curr->first << "=" << curr->second << endl;
528
529        int i;
530        for (i = 0; i < NUM_COLORS; ++i) {
531            inf << colorToString((enum color)i) << "=#";
532            inf.width(2); inf.fill('0');
533            inf << right << hex << skinConfColors[i].r;
534            inf.width(2); inf.fill('0');
535            inf << right << hex << skinConfColors[i].g;
536            inf.width(2); inf.fill('0');
537            inf << right << hex << skinConfColors[i].b;
538            inf.width(2); inf.fill('0');
539            inf << right << hex << skinConfColors[i].a << endl;
540        }
541
542        inf.close();
543        sync();
544    }
545}
546
547void GMenu2X::readTmp() {
548    lastSelectorElement = -1;
549    if (fileExists("/tmp/gmenu2x.tmp")) {
550        ifstream inf("/tmp/gmenu2x.tmp", ios_base::in);
551        if (inf.is_open()) {
552            string line;
553            string section = "";
554            while (getline(inf, line, '\n')) {
555                string::size_type pos = line.find("=");
556                string name = trim(line.substr(0,pos));
557                string value = trim(line.substr(pos+1,line.length()));
558
559                if (name=="section")
560                    menu->setSectionIndex(atoi(value.c_str()));
561                else if (name=="link")
562                    menu->setLinkIndex(atoi(value.c_str()));
563                else if (name=="selectorelem")
564                    lastSelectorElement = atoi(value.c_str());
565                else if (name=="selectordir")
566                    lastSelectorDir = value;
567            }
568            inf.close();
569        }
570    }
571}
572
573void GMenu2X::writeTmp(int selelem, const string &selectordir) {
574    string conffile = "/tmp/gmenu2x.tmp";
575    ofstream inf(conffile.c_str());
576    if (inf.is_open()) {
577        inf << "section=" << menu->selSectionIndex() << endl;
578        inf << "link=" << menu->selLinkIndex() << endl;
579        if (selelem>-1)
580            inf << "selectorelem=" << selelem << endl;
581        if (!selectordir.empty())
582            inf << "selectordir=" << selectordir << endl;
583        inf.close();
584        sync();
585    }
586}
587
588void GMenu2X::main() {
589    if (!fileExists(CARD_ROOT))
590        CARD_ROOT = "";
591
592    appToLaunch = nullptr;
593
594    // Recover last session
595    readTmp();
596    if (lastSelectorElement > -1 && menu->selLinkApp() &&
597                (!menu->selLinkApp()->getSelectorDir().empty()
598                 || !lastSelectorDir.empty()))
599        menu->selLinkApp()->selector(lastSelectorElement, lastSelectorDir);
600
601    while (true) {
602        // Remove dismissed layers from the stack.
603        for (auto it = layers.begin(); it != layers.end(); ) {
604            if ((*it)->getStatus() == Layer::Status::DISMISSED) {
605                it = layers.erase(it);
606            } else {
607                ++it;
608            }
609        }
610
611        // Run animations.
612        bool animating = false;
613        for (auto layer : layers) {
614            animating |= layer->runAnimations();
615        }
616
617        // Paint layers.
618        for (auto layer : layers) {
619            layer->paint(*s);
620        }
621        if (appToLaunch) {
622            break;
623        }
624        s->flip();
625
626        // Handle touchscreen events.
627        if (ts.available()) {
628            ts.poll();
629            for (auto it = layers.rbegin(); it != layers.rend(); ++it) {
630                if ((*it)->handleTouchscreen(ts)) {
631                    break;
632                }
633            }
634        }
635
636        // Handle other input events.
637        InputManager::Button button;
638        bool gotEvent;
639        const bool wait = !animating;
640        do {
641            gotEvent = input.getButton(&button, wait);
642        } while (wait && !gotEvent);
643        if (gotEvent) {
644            for (auto it = layers.rbegin(); it != layers.rend(); ++it) {
645                if ((*it)->handleButtonPress(button)) {
646                    break;
647                }
648            }
649        }
650    }
651
652    if (appToLaunch) {
653        appToLaunch->drawRun();
654        appToLaunch->launch(fileToLaunch);
655    }
656}
657
658void GMenu2X::explorer() {
659    FileDialog fd(this, ts, tr["Select an application"], "dge,sh,bin,py,elf,");
660    if (fd.exec()) {
661        if (confInt["saveSelection"] && (confInt["section"]!=menu->selSectionIndex() || confInt["link"]!=menu->selLinkIndex()))
662            writeConfig();
663
664        string command = cmdclean(fd.getPath()+"/"+fd.getFile());
665        chdir(fd.getPath().c_str());
666        quit();
667#ifdef ENABLE_CPUFREQ
668        setClock(cpuFreqAppDefault);
669#endif
670        execlp("/bin/sh","/bin/sh","-c",command.c_str(),NULL);
671
672        //if execution continues then something went wrong and as we already called SDL_Quit we cannot continue
673        //try relaunching gmenu2x
674        ERROR("Error executing selected application, re-launching gmenu2x\n");
675        main();
676    }
677}
678
679void GMenu2X::queueLaunch(LinkApp *app, const std::string &file) {
680    appToLaunch = app;
681    fileToLaunch = file;
682}
683
684void GMenu2X::showHelpPopup() {
685    layers.push_back(make_shared<HelpPopup>(*this));
686}
687
688void GMenu2X::showSettings() {
689#ifdef ENABLE_CPUFREQ
690    int curMenuClock = confInt["menuClock"];
691#endif
692    bool showRootFolder = fileExists(CARD_ROOT);
693
694    FileLister fl_tr(GMENU2X_SYSTEM_DIR "/translations");
695    fl_tr.browse();
696
697    string tr_path = getHome() + "/translations";
698    if (fileExists(tr_path)) {
699        fl_tr.setPath(tr_path, false);
700        fl_tr.browse(false);
701    }
702
703    fl_tr.insertFile("English");
704    string lang = tr.lang();
705
706    vector<string> encodings;
707    encodings.push_back("NTSC");
708    encodings.push_back("PAL");
709
710    SettingsDialog sd(this, input, ts, tr["Settings"]);
711    sd.addSetting(new MenuSettingMultiString(this, ts, tr["Language"], tr["Set the language used by GMenu2X"], &lang, &fl_tr.getFiles()));
712    sd.addSetting(new MenuSettingBool(this, ts, tr["Save last selection"], tr["Save the last selected link and section on exit"], &confInt["saveSelection"]));
713#ifdef ENABLE_CPUFREQ
714    sd.addSetting(new MenuSettingInt(this, ts, tr["Clock for GMenu2X"], tr["Set the cpu working frequency when running GMenu2X"], &confInt["menuClock"], cpuFreqMin, cpuFreqSafeMax, cpuFreqMultiple));
715    sd.addSetting(new MenuSettingInt(this, ts, tr["Maximum overclock"], tr["Set the maximum overclock for launching links"], &confInt["maxClock"], cpuFreqMin, cpuFreqMax, cpuFreqMultiple));
716#endif
717    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"]));
718    sd.addSetting(new MenuSettingInt(this, ts, tr["Screen Timeout"], tr["Set screen's backlight timeout in seconds"], &confInt["backlightTimeout"], 0, 120));
719// sd.addSetting(new MenuSettingMultiString(this, ts, tr["Tv-Out encoding"], tr["Encoding of the tv-out signal"], &confStr["tvoutEncoding"], &encodings));
720    sd.addSetting(new MenuSettingBool(this, ts, tr["Show root"], tr["Show root folder in the file selection dialogs"], &showRootFolder));
721
722    if (sd.exec() && sd.edited()) {
723#ifdef ENABLE_CPUFREQ
724        if (curMenuClock != confInt["menuClock"]) setClock(confInt["menuClock"]);
725#endif
726
727        if (confInt["backlightTimeout"] == 0) {
728            if (PowerSaver::isRunning())
729                delete PowerSaver::getInstance();
730        } else {
731            PowerSaver::getInstance()->setScreenTimeout( confInt["backlightTimeout"] );
732        }
733
734        if (lang == "English") lang = "";
735        if (lang != tr.lang()) {
736            tr.setLang(lang);
737            confStr["lang"] = lang;
738        }
739        /*if (fileExists(CARD_ROOT) && !showRootFolder)
740            unlink(CARD_ROOT);
741        else if (!fileExists(CARD_ROOT) && showRootFolder)
742            symlink("/", CARD_ROOT);*/
743        //WARNING: the above might be dangerous with CARD_ROOT set to /
744        writeConfig();
745    }
746}
747
748void GMenu2X::skinMenu() {
749    FileLister fl_sk(getHome() + "/skins", true, false);
750    fl_sk.addExclude("..");
751    fl_sk.browse();
752    fl_sk.setPath(GMENU2X_SYSTEM_DIR "/skins", false);
753    fl_sk.browse(false);
754
755    string curSkin = confStr["skin"];
756
757    SettingsDialog sd(this, input, ts, tr["Skin"]);
758    sd.addSetting(new MenuSettingMultiString(this, ts, tr["Skin"], tr["Set the skin used by GMenu2X"], &confStr["skin"], &fl_sk.getDirectories()));
759    sd.addSetting(new MenuSettingRGBA(this, ts, tr["Top Bar"], tr["Color of the top bar"], &skinConfColors[COLOR_TOP_BAR_BG]));
760    sd.addSetting(new MenuSettingRGBA(this, ts, tr["Bottom Bar"], tr["Color of the bottom bar"], &skinConfColors[COLOR_BOTTOM_BAR_BG]));
761    sd.addSetting(new MenuSettingRGBA(this, ts, tr["Selection"], tr["Color of the selection and other interface details"], &skinConfColors[COLOR_SELECTION_BG]));
762    sd.addSetting(new MenuSettingRGBA(this, ts, tr["Message Box"], tr["Background color of the message box"], &skinConfColors[COLOR_MESSAGE_BOX_BG]));
763    sd.addSetting(new MenuSettingRGBA(this, ts, tr["Message Box Border"], tr["Border color of the message box"], &skinConfColors[COLOR_MESSAGE_BOX_BORDER]));
764    sd.addSetting(new MenuSettingRGBA(this, ts, tr["Message Box Selection"], tr["Color of the selection of the message box"], &skinConfColors[COLOR_MESSAGE_BOX_SELECTION]));
765
766    if (sd.exec() && sd.edited()) {
767        if (curSkin != confStr["skin"]) {
768            setSkin(confStr["skin"]);
769            writeConfig();
770        }
771        writeSkinConfig();
772        initBG();
773    }
774}
775
776void GMenu2X::setSkin(const string &skin, bool setWallpaper) {
777    confStr["skin"] = skin;
778
779    //Clear previous skin settings
780    skinConfStr.clear();
781    skinConfInt.clear();
782
783    DEBUG("GMenu2X: setting new skin %s.\n", skin.c_str());
784
785    //clear collection and change the skin path
786    sc.clear();
787    sc.setSkin(skin);
788
789    //reset colors to the default values
790    skinConfColors[COLOR_TOP_BAR_BG] = (RGBAColor){255,255,255,130};
791    skinConfColors[COLOR_BOTTOM_BAR_BG] = (RGBAColor){255,255,255,130};
792    skinConfColors[COLOR_SELECTION_BG] = (RGBAColor){255,255,255,130};
793    skinConfColors[COLOR_MESSAGE_BOX_BG] = (RGBAColor){255,255,255,255};
794    skinConfColors[COLOR_MESSAGE_BOX_BORDER] = (RGBAColor){80,80,80,255};
795    skinConfColors[COLOR_MESSAGE_BOX_SELECTION] = (RGBAColor){160,160,160,255};
796
797    /* Load skin settings from user directory if present,
798     * or from the system directory. */
799    string skinconfname = getHome() + "/skins/" + skin + "/skin.conf";
800    if (!fileExists(skinconfname))
801      skinconfname = GMENU2X_SYSTEM_DIR "/skins/" + skin + "/skin.conf";
802
803    if (fileExists(skinconfname)) {
804        ifstream skinconf(skinconfname.c_str(), ios_base::in);
805        if (skinconf.is_open()) {
806            string line;
807            while (getline(skinconf, line, '\n')) {
808                line = trim(line);
809                DEBUG("skinconf: '%s'\n", line.c_str());
810                string::size_type pos = line.find("=");
811                string name = trim(line.substr(0,pos));
812                string value = trim(line.substr(pos+1,line.length()));
813
814                if (value.length()>0) {
815                    if (value.length()>1 && value.at(0)=='"' && value.at(value.length()-1)=='"')
816                        skinConfStr[name] = value.substr(1,value.length()-2);
817                    else if (value.at(0) == '#')
818                        skinConfColors[stringToColor(name)] = strtorgba( value.substr(1,value.length()) );
819                    else
820                        skinConfInt[name] = atoi(value.c_str());
821                }
822            }
823            skinconf.close();
824
825            if (setWallpaper && !skinConfStr["wallpaper"].empty()) {
826                string fp = sc.getSkinFilePath("wallpapers/" + skinConfStr["wallpaper"]);
827                if (!fp.empty())
828                    confStr["wallpaper"] = fp;
829                else
830                    WARNING("Unable to find wallpaper defined on skin %s\n", skin.c_str());
831            }
832        }
833    }
834
835    evalIntConf(&skinConfInt["topBarHeight"], 40, 32, 120);
836    evalIntConf(&skinConfInt["bottomBarHeight"], 20, 20, 120);
837    evalIntConf(&skinConfInt["linkHeight"], 40, 32, 120);
838    evalIntConf(&skinConfInt["linkWidth"], 60, 32, 120);
839
840    if (menu != NULL) menu->skinUpdated();
841
842    //Selection png
843    useSelectionPng = sc.addSkinRes("imgs/selection.png", false) != NULL;
844
845    //font
846    initFont();
847}
848
849void GMenu2X::showManual() {
850    menu->selLinkApp()->showManual();
851}
852
853void GMenu2X::showContextMenu() {
854    layers.push_back(make_shared<ContextMenu>(*this, *menu));
855}
856
857void GMenu2X::changeWallpaper() {
858    WallpaperDialog wp(this, ts);
859    if (wp.exec() && confStr["wallpaper"] != wp.wallpaper) {
860        confStr["wallpaper"] = wp.wallpaper;
861        initBG();
862        writeConfig();
863    }
864}
865
866void GMenu2X::addLink() {
867    FileDialog fd(this, ts, tr["Select an application"], "dge,sh,bin,py,elf,");
868    if (fd.exec()) {
869        menu->addLink(fd.getPath(), fd.getFile());
870        sync();
871    }
872}
873
874void GMenu2X::editLink() {
875    LinkApp *linkApp = menu->selLinkApp();
876    if (!linkApp) return;
877
878    vector<string> pathV;
879    split(pathV,linkApp->getFile(),"/");
880    string oldSection = "";
881    if (pathV.size()>1)
882        oldSection = pathV[pathV.size()-2];
883    string newSection = oldSection;
884
885    string linkTitle = linkApp->getTitle();
886    string linkDescription = linkApp->getDescription();
887    string linkIcon = linkApp->getIcon();
888    string linkManual = linkApp->getManual();
889    string linkSelFilter = linkApp->getSelectorFilter();
890    string linkSelDir = linkApp->getSelectorDir();
891    bool linkSelBrowser = linkApp->getSelectorBrowser();
892    string linkSelScreens = linkApp->getSelectorScreens();
893    string linkSelAliases = linkApp->getAliasFile();
894    int linkClock = linkApp->clock();
895
896    string diagTitle = tr.translate("Edit link: $1",linkTitle.c_str(),NULL);
897    string diagIcon = linkApp->getIconPath();
898
899    SettingsDialog sd(this, input, ts, diagTitle, diagIcon);
900    if (!linkApp->isOpk()) {
901        sd.addSetting(new MenuSettingString(this, ts, tr["Title"], tr["Link title"], &linkTitle, diagTitle, diagIcon));
902        sd.addSetting(new MenuSettingString(this, ts, tr["Description"], tr["Link description"], &linkDescription, diagTitle, diagIcon));
903        sd.addSetting(new MenuSettingMultiString(this, ts, tr["Section"], tr["The section this link belongs to"], &newSection, &menu->getSections()));
904        sd.addSetting(new MenuSettingImage(this, ts, tr["Icon"],
905                        tr.translate("Select an icon for the link: $1",
906                            linkTitle.c_str(), NULL), &linkIcon, "png"));
907        sd.addSetting(new MenuSettingFile(this, ts, tr["Manual"],
908                        tr["Select a graphic/textual manual or a readme"],
909                        &linkManual, "man.png,txt"));
910    }
911    if (!linkApp->isOpk() || !linkApp->getSelectorDir().empty()) {
912        sd.addSetting(new MenuSettingDir(this, ts, tr["Selector Directory"], tr["Directory to scan for the selector"], &linkSelDir));
913        sd.addSetting(new MenuSettingBool(this, ts, tr["Selector Browser"], tr["Allow the selector to change directory"], &linkSelBrowser));
914    }
915#ifdef ENABLE_CPUFREQ
916    sd.addSetting(new MenuSettingInt(this, ts, tr["Clock frequency"], tr["Cpu clock frequency to set when launching this link"], &linkClock, cpuFreqMin, confInt["maxClock"], cpuFreqMultiple));
917#endif
918    if (!linkApp->isOpk()) {
919        sd.addSetting(new MenuSettingString(this, ts, tr["Selector Filter"], tr["Selector filter (Separate values with a comma)"], &linkSelFilter, diagTitle, diagIcon));
920        sd.addSetting(new MenuSettingDir(this, ts, tr["Selector Screenshots"], tr["Directory of the screenshots for the selector"], &linkSelScreens));
921        sd.addSetting(new MenuSettingFile(this, ts, tr["Selector Aliases"], tr["File containing a list of aliases for the selector"], &linkSelAliases));
922#if defined(PLATFORM_A320) || defined(PLATFORM_GCW0)
923        sd.addSetting(new MenuSettingBool(this, ts, tr["Display Console"], tr["Must be enabled for console-based applications"], &linkApp->consoleApp));
924#endif
925    }
926
927    if (sd.exec() && sd.edited()) {
928        linkApp->setTitle(linkTitle);
929        linkApp->setDescription(linkDescription);
930        linkApp->setIcon(linkIcon);
931        linkApp->setManual(linkManual);
932        linkApp->setSelectorFilter(linkSelFilter);
933        linkApp->setSelectorDir(linkSelDir);
934        linkApp->setSelectorBrowser(linkSelBrowser);
935        linkApp->setSelectorScreens(linkSelScreens);
936        linkApp->setAliasFile(linkSelAliases);
937        linkApp->setClock(linkClock);
938
939        INFO("New Section: '%s'\n", newSection.c_str());
940
941        //if section changed move file and update link->file
942        if (oldSection!=newSection) {
943            vector<string>::const_iterator newSectionIndex = find(menu->getSections().begin(),menu->getSections().end(),newSection);
944            if (newSectionIndex==menu->getSections().end()) return;
945            string newFileName = "sections/"+newSection+"/"+linkTitle;
946            uint x=2;
947            while (fileExists(newFileName)) {
948                string id = "";
949                stringstream ss; ss << x; ss >> id;
950                newFileName = "sections/"+newSection+"/"+linkTitle+id;
951                x++;
952            }
953            rename(linkApp->getFile().c_str(),newFileName.c_str());
954            linkApp->renameFile(newFileName);
955
956            INFO("New section index: %i.\n", newSectionIndex - menu->getSections().begin());
957
958            menu->linkChangeSection(menu->selLinkIndex(), menu->selSectionIndex(), newSectionIndex - menu->getSections().begin());
959        }
960        linkApp->save();
961        sync();
962    }
963}
964
965void GMenu2X::deleteLink() {
966    if (menu->selLinkApp()!=NULL) {
967        MessageBox mb(this, tr.translate("Deleting $1",menu->selLink()->getTitle().c_str(),NULL)+"\n"+tr["Are you sure?"], menu->selLink()->getIconPath());
968        mb.setButton(InputManager::ACCEPT, tr["Yes"]);
969        mb.setButton(InputManager::CANCEL, tr["No"]);
970        if (mb.exec() == InputManager::ACCEPT) {
971            menu->deleteSelectedLink();
972            sync();
973        }
974    }
975}
976
977void GMenu2X::addSection() {
978    InputDialog id(this, input, ts, tr["Insert a name for the new section"]);
979    if (id.exec()) {
980        //only if a section with the same name does not exist
981        if (find(menu->getSections().begin(), menu->getSections().end(), id.getInput())
982                == menu->getSections().end()) {
983            //section directory doesn't exists
984            if (menu->addSection(id.getInput())) {
985                menu->setSectionIndex( menu->getSections().size()-1 ); //switch to the new section
986                sync();
987            }
988        }
989    }
990}
991
992void GMenu2X::renameSection() {
993    InputDialog id(this, input, ts, tr["Insert a new name for this section"],menu->selSection());
994    if (id.exec()) {
995        //only if a section with the same name does not exist & !samename
996        if (menu->selSection() != id.getInput()
997         && find(menu->getSections().begin(),menu->getSections().end(), id.getInput())
998                == menu->getSections().end()) {
999            //section directory doesn't exists
1000            string newsectiondir = getHome() + "/sections/" + id.getInput();
1001            string sectiondir = getHome() + "/sections/" + menu->selSection();
1002
1003            if (!rename(sectiondir.c_str(), newsectiondir.c_str())) {
1004                string oldpng = menu->selSection() + ".png";
1005                string newpng = id.getInput() + ".png";
1006                string oldicon = sc.getSkinFilePath(oldpng);
1007                string newicon = sc.getSkinFilePath(newpng);
1008
1009                if (!oldicon.empty() && newicon.empty()) {
1010                    newicon = oldicon;
1011                    newicon.replace(newicon.find(oldpng), oldpng.length(), newpng);
1012
1013                    if (!fileExists(newicon)) {
1014                        rename(oldicon.c_str(), newicon.c_str());
1015                        sc.move("skin:"+oldpng, "skin:"+newpng);
1016                    }
1017                }
1018                menu->renameSection(menu->selSectionIndex(), id.getInput());
1019                sync();
1020            }
1021        }
1022    }
1023}
1024
1025void GMenu2X::deleteSection() {
1026    MessageBox mb(this,tr["You will lose all the links in this section."]+"\n"+tr["Are you sure?"]);
1027    mb.setButton(InputManager::ACCEPT, tr["Yes"]);
1028    mb.setButton(InputManager::CANCEL, tr["No"]);
1029    if (mb.exec() == InputManager::ACCEPT) {
1030
1031        if (rmtree(getHome() + "/sections/" + menu->selSection())) {
1032            menu->deleteSelectedSection();
1033            sync();
1034        }
1035    }
1036}
1037
1038void GMenu2X::scanner() {
1039    Surface scanbg(bg);
1040    drawButton(&scanbg, "cancel", tr["Exit"],
1041    drawButton(&scanbg, "accept", "", 5)-10);
1042    scanbg.write(font,tr["Link Scanner"],halfX,7,Font::HAlignCenter,Font::VAlignMiddle);
1043    scanbg.convertToDisplayFormat();
1044
1045    uint lineY = 42;
1046
1047#ifdef PLATFORM_PANDORA
1048    //char *configpath = pnd_conf_query_searchpath();
1049#else
1050#ifdef ENABLE_CPUFREQ
1051    setSafeMaxClock();
1052#endif
1053
1054    scanbg.write(font,tr["Scanning filesystem..."],5,lineY);
1055    scanbg.blit(s,0,0);
1056    s->flip();
1057    lineY += 26;
1058
1059    vector<string> files;
1060    scanPath(CARD_ROOT, &files);
1061
1062    stringstream ss;
1063    ss << files.size();
1064    string str = "";
1065    ss >> str;
1066    scanbg.write(font,tr.translate("$1 files found.",str.c_str(),NULL),5,lineY);
1067    lineY += 26;
1068    scanbg.write(font,tr["Creating links..."],5,lineY);
1069    scanbg.blit(s,0,0);
1070    s->flip();
1071    lineY += 26;
1072
1073    string path, file;
1074    string::size_type pos;
1075    uint linkCount = 0;
1076
1077    for (uint i = 0; i<files.size(); i++) {
1078        pos = files[i].rfind("/");
1079        if (pos!=string::npos && pos>0) {
1080            path = files[i].substr(0, pos+1);
1081            file = files[i].substr(pos+1, files[i].length());
1082            if (menu->addLink(path,file,"found "+file.substr(file.length()-3,3)))
1083                linkCount++;
1084        }
1085    }
1086
1087    ss.clear();
1088    ss << linkCount;
1089    ss >> str;
1090    scanbg.write(font,tr.translate("$1 links created.",str.c_str(),NULL),5,lineY);
1091    scanbg.blit(s,0,0);
1092    s->flip();
1093    lineY += 26;
1094
1095#ifdef ENABLE_CPUFREQ
1096    setMenuClock();
1097#endif
1098    sync();
1099#endif
1100
1101    InputManager::Button button;
1102    do {
1103        button = input.waitForPressedButton();
1104    } while ((button != InputManager::SETTINGS)
1105                && (button != InputManager::ACCEPT)
1106                && (button != InputManager::CANCEL));
1107}
1108
1109void GMenu2X::scanPath(string path, vector<string> *files) {
1110    DIR *dirp;
1111    struct stat st;
1112    struct dirent *dptr;
1113    string filepath, ext;
1114
1115    if (path[path.length()-1]!='/') path += "/";
1116    if ((dirp = opendir(path.c_str())) == NULL) return;
1117
1118    while ((dptr = readdir(dirp))) {
1119        if (dptr->d_name[0]=='.')
1120            continue;
1121        filepath = path+dptr->d_name;
1122        int statRet = stat(filepath.c_str(), &st);
1123        if (S_ISDIR(st.st_mode))
1124            scanPath(filepath, files);
1125        if (statRet != -1) {
1126            ext = filepath.substr(filepath.length()-4,4);
1127#if defined(PLATFORM_A320) || defined(PLATFORM_GCW0) || defined(PLATFORM_NANONOTE)
1128            if (ext==".dge")
1129#else
1130            if (ext==".pxml")
1131#endif
1132                files->push_back(filepath);
1133        }
1134    }
1135
1136    closedir(dirp);
1137}
1138
1139typedef struct {
1140    unsigned short batt;
1141    unsigned short remocon;
1142} MMSP2ADC;
1143
1144void GMenu2X::setInputSpeed() {
1145    SDL_EnableKeyRepeat(250, 150);
1146}
1147
1148#ifdef ENABLE_CPUFREQ
1149void GMenu2X::setClock(unsigned mhz) {
1150    mhz = constrain(mhz, cpuFreqMin, confInt["maxClock"]);
1151#if defined(PLATFORM_A320) || defined(PLATFORM_GCW0) || defined(PLATFORM_NANONOTE)
1152    jz_cpuspeed(mhz);
1153#endif
1154}
1155#endif
1156
1157string GMenu2X::getDiskFree(const char *path) {
1158    string df = "";
1159    struct statvfs b;
1160
1161    int ret = statvfs(path, &b);
1162    if (ret == 0) {
1163        // Make sure that the multiplication happens in 64 bits.
1164        unsigned long freeMiB =
1165                ((unsigned long long)b.f_bfree * b.f_bsize) / (1024 * 1024);
1166        unsigned long totalMiB =
1167                ((unsigned long long)b.f_blocks * b.f_frsize) / (1024 * 1024);
1168        stringstream ss;
1169        if (totalMiB >= 10000) {
1170            ss << (freeMiB / 1024) << "." << ((freeMiB % 1024) * 10) / 1024 << "/"
1171               << (totalMiB / 1024) << "." << ((totalMiB % 1024) * 10) / 1024 << "GiB";
1172        } else {
1173            ss << freeMiB << "/" << totalMiB << "MiB";
1174        }
1175        ss >> df;
1176    } else WARNING("statvfs failed with error '%s'.\n", strerror(errno));
1177    return df;
1178}
1179
1180int GMenu2X::drawButton(IconButton *btn, int x, int y) {
1181    if (y<0) y = resY+y;
1182    btn->setPosition(x, y-7);
1183    btn->paint();
1184    return x+btn->getRect().w+6;
1185}
1186
1187int GMenu2X::drawButton(Surface *s, const string &btn, const string &text, int x, int y) {
1188    if (y<0) y = resY+y;
1189    SDL_Rect re = { static_cast<Sint16>(x), static_cast<Sint16>(y - 7), 0, 16 };
1190    if (sc.skinRes("imgs/buttons/"+btn+".png") != NULL) {
1191        sc["imgs/buttons/"+btn+".png"]->blit(s, x, y-7);
1192        re.w = sc["imgs/buttons/"+btn+".png"]->width() + 3;
1193        s->write(font, text, x+re.w, y, Font::HAlignLeft, Font::VAlignMiddle);
1194        re.w += font->getTextWidth(text);
1195    }
1196    return x+re.w+6;
1197}
1198
1199int GMenu2X::drawButtonRight(Surface *s, const string &btn, const string &text, int x, int y) {
1200    if (y<0) y = resY+y;
1201    if (sc.skinRes("imgs/buttons/"+btn+".png") != NULL) {
1202        x -= 16;
1203        sc["imgs/buttons/"+btn+".png"]->blit(s, x, y-7);
1204        x -= 3;
1205        s->write(font, text, x, y, Font::HAlignRight, Font::VAlignMiddle);
1206        return x-6-font->getTextWidth(text);
1207    }
1208    return x-6;
1209}
1210
1211void GMenu2X::drawScrollBar(uint pageSize, uint totalSize, uint pagePos) {
1212    if (totalSize <= pageSize) {
1213        // Everything fits on one screen, no scroll bar needed.
1214        return;
1215    }
1216
1217    unsigned int top, height;
1218    tie(top, height) = getContentArea();
1219    top += 1;
1220    height -= 2;
1221
1222    s->rectangle(resX - 8, top, 7, height, skinConfColors[COLOR_SELECTION_BG]);
1223    top += 2;
1224    height -= 4;
1225
1226    const uint barSize = height * pageSize / totalSize;
1227    const uint barPos = (height - barSize) * pagePos / (totalSize - pageSize);
1228
1229    s->box(resX - 6, top + barPos, 3, barSize,
1230            skinConfColors[COLOR_SELECTION_BG]);
1231}
1232
1233void GMenu2X::drawTopBar(Surface *s) {
1234    Surface *bar = sc.skinRes("imgs/topbar.png", false);
1235    if (bar) {
1236        bar->blit(s, 0, 0);
1237    } else {
1238        const int h = skinConfInt["topBarHeight"];
1239        s->box(0, 0, resX, h, skinConfColors[COLOR_TOP_BAR_BG]);
1240    }
1241}
1242
1243void GMenu2X::drawBottomBar(Surface *s) {
1244    Surface *bar = sc.skinRes("imgs/bottombar.png", false);
1245    if (bar) {
1246        bar->blit(s, 0, resY-bar->height());
1247    } else {
1248        const int h = skinConfInt["bottomBarHeight"];
1249        s->box(0, resY - h, resX, h, skinConfColors[COLOR_BOTTOM_BAR_BG]);
1250    }
1251}
1252

Archive Download this file



interactive