diff --git a/.gitignore b/.gitignore index 4055d624e6..ec05c29b64 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,8 @@ *.slo *.lo *.o - +*.mo + # Compiled Dynamic libraries *.so @@ -40,3 +41,4 @@ Makefile CPackConfig.cmake CPackSourceConfig.cmake *.cbp +/.vs diff --git a/CMake/Packages/FindSDL2MIXER.cmake b/CMake/Packages/FindSDL2MIXER.cmake new file mode 100644 index 0000000000..ce18011829 --- /dev/null +++ b/CMake/Packages/FindSDL2MIXER.cmake @@ -0,0 +1,49 @@ +# +# this module look for SDL2_Mixer (http://www.libsdl.org) support +# it will define the following values +# +# SDLMIXER_INCLUDE_DIR = where SDL_mixer.h can be found +# SDLMIXER_LIBRARY = the library to link against SDL2_mixer +# SDLMIXER_FOUND = set to 1 if SDL2_mixer is found +# + +IF(SDL2_Mixer_INCLUDE_DIRS) + + FIND_PATH(SDLMIXER_INCLUDE_DIR SDL2/SDL_mixer.h ${SDL2_Mixer_INCLUDE_DIRS}) + FIND_LIBRARY(SDLMIXER_LIBRARY SDL2_mixer ${SDL2_Mixer_LIBRARY_DIRS}) + +ELSE(SDL2_Mixer_INCLUDE_DIRS) + + SET(TRIAL_LIBRARY_PATHS + $ENV{SDL2_MIXER_HOME}/lib + /usr/lib + /usr/local/lib + /sw/lib + ) + SET(TRIAL_INCLUDE_PATHS + $ENV{SDL2_MIXER_HOME}/include/SDL2 + /usr/include/SDL2 + /usr/local/include/SDL2 + /sw/include/SDL2 + ) + + FIND_LIBRARY(SDLMIXER_LIBRARY SDL2_mixer ${TRIAL_LIBRARY_PATHS}) + FIND_PATH(SDLMIXER_INCLUDE_DIR SDL_mixer.h ${TRIAL_INCLUDE_PATHS}) + +ENDIF(SDL2_Mixer_INCLUDE_DIRS) + +IF(SDLMIXER_INCLUDE_DIR AND SDLMIXER_LIBRARY) + SET(SDLMIXER_FOUND 1 CACHE BOOL "Found SDL2_Mixer library") +ELSE(SDLMIXER_INCLUDE_DIR AND SDLMIXER_LIBRARY) + SET(SDLMIXER_FOUND 0 CACHE BOOL "Not fount SDL2_Mixer library") +ENDIF(SDLMIXER_INCLUDE_DIR AND SDLMIXER_LIBRARY) + +MARK_AS_ADVANCED( + SDLMIXER_INCLUDE_DIR + SDLMIXER_LIBRARY + SDLMIXER_FOUND +) + +INCLUDE(FindPackageHandleStandardArgs) + +FIND_PACKAGE_HANDLE_STANDARD_ARGS(SDLMIXER REQUIRED_VARS SDLMIXER_INCLUDE_DIR SDLMIXER_LIBRARY) diff --git a/CMakeLists.txt b/CMakeLists.txt index a167a808fb..96e9b418a4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -57,6 +57,7 @@ endif() find_package(Freetype REQUIRED) find_package(FreeImage REQUIRED) find_package(SDL2 REQUIRED) +find_package(SDL2MIXER REQUIRED) find_package(CURL REQUIRED) find_package(VLC REQUIRED) find_package(RapidJSON REQUIRED) @@ -66,6 +67,11 @@ if(CEC) find_package(libCEC REQUIRED) endif() +# i18n +# if(MSVC) +# find_package(Intl REQUIRED) +# endif() + #add ALSA for Linux if(${CMAKE_SYSTEM_NAME} MATCHES "Linux") find_package(ALSA REQUIRED) @@ -125,6 +131,7 @@ set(COMMON_INCLUDE_DIRS ${FREETYPE_INCLUDE_DIRS} ${FreeImage_INCLUDE_DIRS} ${SDL2_INCLUDE_DIR} + ${SDLMIXER_INCLUDE_DIR} ${CURL_INCLUDE_DIR} ${VLC_INCLUDE_DIR} ${RAPIDJSON_INCLUDE_DIRS} @@ -132,6 +139,12 @@ set(COMMON_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/es-core/src ) +# if(MSVC) +# LIST(APPEND COMMON_INCLUDE_DIRS +# ${Intl_INCLUDE_DIRS} +# ) +# endif() + #add libCEC_INCLUDE_DIR if(DEFINED libCEC_FOUND) LIST(APPEND COMMON_INCLUDE_DIRS @@ -184,14 +197,21 @@ endif() set(COMMON_LIBRARIES ${FREETYPE_LIBRARIES} - ${FreeImage_LIBRARIES} + ${FreeImage_LIBRARIES} ${SDL2_LIBRARY} + ${SDLMIXER_LIBRARY} ${CURL_LIBRARIES} ${VLC_LIBRARIES} pugixml nanosvg ) +# if(MSVC) +# LIST(APPEND COMMON_LIBRARIES +# ${Intl_LIBRARIES} +# ) +# endif() + #add libCEC_LIBRARIES if(DEFINED libCEC_FOUND) if(DEFINED BCMHOST) diff --git a/README.md b/README.md index 7543220f61..93b9438830 100644 --- a/README.md +++ b/README.md @@ -1,218 +1,350 @@ -EmulationStation -================ - -This is a fork of EmulationStation for RetroPie. -EmulationStation is a cross-platform graphical front-end for emulators with controller navigation. - -Building -======== - -EmulationStation uses some C++11 code, which means you'll need to use at least g++-4.7 on Linux, or VS2010 on Windows, to compile. - -EmulationStation has a few dependencies. For building, you'll need CMake, SDL2, FreeImage, FreeType, cURL and RapidJSON. You also should probably install the `fonts-droid` package which contains fallback fonts for Chinese/Japanese/Korean characters, but ES will still work fine without it (this package is only used at run-time). - -**On Debian/Ubuntu:** -All of this be easily installed with `apt-get`: -```bash -sudo apt-get install libsdl2-dev libfreeimage-dev libfreetype6-dev libcurl4-openssl-dev rapidjson-dev \ - libasound2-dev libgl1-mesa-dev build-essential cmake fonts-droid-fallback libvlc-dev \ - libvlccore-dev vlc-bin -``` -**On Fedora:** -All of this be easily installed with `dnf` (with rpmfusion activated) : -```bash -sudo dnf install SDL2-devel freeimage-devel freetype-devel curl-devel \ - alsa-lib-devel mesa-libGL-devel cmake \ - vlc-devel rapidjson-devel -``` - -Note this Repository uses a git submodule - to checkout the source and all submodules, use - -```bash -git clone --recursive https://github.com/RetroPie/EmulationStation.git -``` - -or - -```bash -git clone https://github.com/RetroPie/EmulationStation.git -cd EmulationStation -git submodule update --init -``` - -Then, generate and build the Makefile with CMake: -```bash -cd YourEmulationStationDirectory -cmake . -make -``` - -**On the Raspberry Pi:** - -Complete Raspberry Pi build instructions at [emulationstation.org](http://emulationstation.org/gettingstarted.html#install_rpi_standalone). - -**On Windows:** - -[FreeImage](http://downloads.sourceforge.net/freeimage/FreeImage3154Win32.zip) - -[FreeType2](http://download.savannah.gnu.org/releases/freetype/freetype-2.4.9.tar.bz2) (you'll need to compile) - -[SDL2](http://www.libsdl.org/release/SDL2-devel-2.0.8-VC.zip) - -[cURL](http://curl.haxx.se/download.html) (you'll need to compile or get the pre-compiled DLL version) - -[RapisJSON](https://github.com/tencent/rapidjson) (you'll need the `include/rapidsjon` added to the include path) - -(Remember to copy necessary .DLLs into the same folder as the executable: probably FreeImage.dll, freetype6.dll, SDL2.dll, libcurl.dll, and zlib1.dll. Exact list depends on if you built your libraries in "static" mode or not.) - -[CMake](http://www.cmake.org/cmake/resources/software.html) (this is used for generating the Visual Studio project) - -(If you don't know how to use CMake, here are some hints: run cmake-gui and point it at your EmulationStation folder. Point the "build" directory somewhere - I use EmulationStation/build. Click configure, choose "Visual Studio [year] Project", fill in red fields as they appear and keep clicking Configure (you may need to check "Advanced"), then click Generate.) - - -Configuring -=========== - -**~/.emulationstation/es_systems.cfg:** -When first run, an example systems configuration file will be created at `~/.emulationstation/es_systems.cfg`. `~` is `$HOME` on Linux, and `%HOMEPATH%` on Windows. This example has some comments explaining how to write the configuration file. See the "Writing an es_systems.cfg" section for more information. - -**Keep in mind you'll have to set up your emulator separately from EmulationStation!** - -**~/.emulationstation/es_input.cfg:** -When you first start EmulationStation, you will be prompted to configure an input device. The process is thus: - -1. Hold a button on the device you want to configure. This includes the keyboard. - -2. Press the buttons as they appear in the list. Some inputs can be skipped by holding any button down for a few seconds (e.g. page up/page down). - -3. You can review your mappings by pressing up and down, making any changes by pressing A. - -4. Choose "SAVE" to save this device and close the input configuration screen. - -The new configuration will be added to the `~/.emulationstation/es_input.cfg` file. - -**Both new and old devices can be (re)configured at any time by pressing the Start button and choosing "CONFIGURE INPUT".** From here, you may unplug the device you used to open the menu and plug in a new one, if necessary. New devices will be appended to the existing input configuration file, so your old devices will remain configured. - -**If your controller stops working, you can delete the `~/.emulationstation/es_input.cfg` file to make the input configuration screen re-appear on next run.** - - -You can use `--help` or `-h` to view a list of command-line options. Briefly outlined here: -``` ---resolution [width] [height] try and force a particular resolution ---gamelist-only skip automatic game search, only read from gamelist.xml ---ignore-gamelist ignore the gamelist (useful for troubleshooting) ---draw-framerate display the framerate ---no-exit don't show the exit option in the menu ---no-splash don't show the splash screen ---debug more logging, show console on Windows ---scrape scrape using command line interface ---windowed not fullscreen, should be used with --resolution ---vsync [1/on or 0/off] turn vsync on or off (default is on) ---max-vram [size] Max VRAM to use in Mb before swapping. 0 for unlimited ---force-kid Force the UI mode to be Kid ---force-kiosk Force the UI mode to be Kiosk ---force-disable-filters Force the UI to ignore applied filters in gamelist ---help, -h summon a sentient, angry tuba -``` - -As long as ES hasn't frozen, you can always press F4 to close the application. - - -Writing an es_systems.cfg -========================= - -Complete configuration instructions at [emulationstation.org](http://emulationstation.org/gettingstarted.html#config). - -The `es_systems.cfg` file contains the system configuration data for EmulationStation, written in XML. This tells EmulationStation what systems you have, what platform they correspond to (for scraping), and where the games are located. - -ES will check two places for an es_systems.cfg file, in the following order, stopping after it finds one that works: -* `~/.emulationstation/es_systems.cfg` -* `/etc/emulationstation/es_systems.cfg` - -The order EmulationStation displays systems reflects the order you define them in. - -**NOTE:** A system *must* have at least one game present in its "path" directory, or ES will ignore it! If no valid systems are found, ES will report an error and quit! - -Here's an example es_systems.cfg: - -```xml - - - - - - - snes - - - Super Nintendo Entertainment System - - - ~/roms/snes - - - .smc .sfc .SMC .SFC - - - snesemulator %ROM% - - - - snes - - - snes - - -``` - -The following "tags" are replaced by ES in launch commands: - -`%ROM%` - Replaced with absolute path to the selected ROM, with most Bash special characters escaped with a backslash. - -`%BASENAME%` - Replaced with the "base" name of the path to the selected ROM. For example, a path of "/foo/bar.rom", this tag would be "bar". This tag is useful for setting up AdvanceMAME. - -`%ROM_RAW%` - Replaced with the unescaped, absolute path to the selected ROM. If your emulator is picky about paths, you might want to use this instead of %ROM%, but enclosed in quotes. - -See [SYSTEMS.md](SYSTEMS.md) for some live examples in EmulationStation. - -gamelist.xml -============ - -The gamelist.xml file for a system defines metadata for games, such as a name, image (like a screenshot or box art), description, release date, and rating. - -If at least one game in a system has an image specified, ES will use the detailed view for that system (which displays metadata alongside the game list). - -*You can use ES's [scraping](http://en.wikipedia.org/wiki/Web_scraping) tools to avoid creating a gamelist.xml by hand.* There are two ways to run the scraper: - -* **If you want to scrape multiple games:** press start to open the menu and choose the "SCRAPER" option. Adjust your settings and press "SCRAPE NOW". -* **If you just want to scrape one game:** find the game on the game list in ES and press select. Choose "EDIT THIS GAME'S METADATA" and then press the "SCRAPE" button at the bottom of the metadata editor. - -You can also edit metadata within ES by using the metadata editor - just find the game you wish to edit on the gamelist, press Select, and choose "EDIT THIS GAME'S METADATA." - -A command-line version of the scraper is also provided - just run emulationstation with `--scrape` *(currently broken)*. - -The switch `--ignore-gamelist` can be used to ignore the gamelist and force ES to use the non-detailed view. - -If you're writing a tool to generate or parse gamelist.xml files, you should check out [GAMELISTS.md](GAMELISTS.md) for more detailed documentation. - - -Themes -====== - -By default, EmulationStation looks pretty ugly. You can fix that. If you want to know more about making your own themes (or editing existing ones), read [THEMES.md](THEMES.md)! - -I've put some themes up for download on my EmulationStation webpage: http://aloshi.com/emulationstation#themes - -If you're using RetroPie, you should already have a nice set of themes automatically installed! - - --Alec "Aloshi" Lofquist -http://www.aloshi.com -http://www.emulationstation.org +EmulationStation FCAMOD +======================= + +This is a fork of EmulationStation containing many additions. +This has been primary developped for Windows platform, but can be compiled for Linux & Raspberry Pi. + +Changes in my branch +==================== + +**System list :** +- Support for Multiple Emulators/Cores in es_systems.cfg, and setting Emulator/Core per game. + + ```xml + %HOME%\RetroArch\retroarch.exe -L %HOME%\RetroArch\cores\%CORE%_libretro.dll %ROM% + + + + mame2003_plus + mame2003 + + + + + fbalpha2012 + + + + ``` +**Grid view :** +- Animations when size changes and during scrolling. +- Supports having a label. + ```xml + + 969A9E + 1 0.18 + + + F6FAFF + + ``` +- Layout can be defined by number of columns and rows ( you had to calculate manually the size of tiles in previous versions ). Zooming the selected item can also be defined simply. + ```xml + + 4 3 + 1.04 + ``` +- Supports extended padding (top, left, bottom, right) : + ```xml + + 0.03 0.13 0.03 0.08 + ``` + +- Supports video in the selected item (delay can be defined in the theme) + ```xml + + 700 + ``` + +- Theme can define which image to use (image, thumbnail or marquee). + ```xml + + marquee + ``` + +- Theme can define the image sizing mode (minSize, maxSize or size). Gridtile items can define a padding. + ```xml + + 24 24 + minSize + ``` + +- Supports md_image, md_video, md_name items... just like detailed view. +- Ability to override grid size by system. + +**Detailed view :** +- Supports md_video, md_marquee items like video view did : Video view is no longer useful. + +**Custom views & Theming:** +- Allow creation of custom views, which inherits from one of the basic theme items ( basic, detailed, grid ). + ```xml + + + ``` +- Ability to select the view (or customview) to use globally or by system. +- The theme can force the default view to use ( attribute defaultView ) +- Fully supports Retropie & Recalbox Themes. +- Carousel supports element "logoPos" : this allows the logo not to be inevitably centered. +- Image loading : the image bytes where duplicated 3 times in memory. +- In previous versions, if a xml element was unknown in the theme, nothing was loaded. +- Support for glows around text +- Reflection for images ( table reflection effect ) +- Gradients for selected menu and list items. + +**Optimizations & Fixes:** +- Faster loading time, using multithreading. +- Optimized memory usage for files and gamelists. +- The loading sequence displays a progress bar. +- Reviewed SVG loading and size calculation mecanism. Previous versions unloaded/reloaded SVGs each time a new container needed to display it because of a size calculation problem. +- Ability to disable "Preload UI" mecanism. This mecanism is used to preload the UI of gamelists of every system. Disable it adds a small lags when opening +- Don't keep in memory the cache of image filenames when launching games -> It takes a lot of memory for nothing. +- Skip parsing 'downloaded_images' and 'media' folders ( better loading time ) +- Added option "Optimize images Vram Use" : Don't load an image in it source resolution if it needs to be displayed smaller -> Resize images in memory to save VRAM. Introduce longer image loading time, but less VRAM use. +- Fixed video starting : Videos started fading even if the video was not available yet ( but not really fading : there was no blending ). +- Software clipping : Avoid rendering clipped items -> They were previously clipped by OpenGl scissors. +- Carousel animation was corrupted if the carousel has to display only one item with 1 +- Font : Optimization when calculating text extend. +- If XML writer fails, the gamelist.xml file become empty and set to 0Kb -> Added a mecanism to secure that. Also, previous gamelist.xml version is saved as gamelist.xml.old. + +**Menus :** +- Cleaned menus + changed menu item order (by interest). +- Full support for menu Theming. +- Separated "Transition style" and "Game launch transition" +- Added option "Boot on gamelist" +- Added option "Hide system view" +- Added option "Display favorites first in gamelist" + +**General :** +- Localisation (French actually supported) +- OSK : On-screen Keyboard. +- Video elements can be added as extras. +- Fixed : Don't show Games what are marked Hidden in gamelist. +- Added a star icon before the name of the game when it is a favorite. +- Corrected favorites ( and custom lists ) management. +- Don't show Directories that contains only one Game : just Show the game. +- Case insensitive file extensions. +- Stop using "sortname" in gamelists. It is useful. + +**Windows specific :** +- Natively portable. If file ".emulationstation/es_systems.cfg" relative to the exe folder. +- Simplified "Quit" menu item ( no more popup asking to restart or turn off Windows ) +- Windows is now "Windowed No border" by default. On Windows, Exclusive fullscreen can be annoying... +- Stop using _wsystem for launching games. Run games with ShellExecuteEx instead ( avoids command window ) +- Add an option to leave ES open with a black screen "Loading..." when launching games ( avoids showing windows desktop ) +- Don't load all fields in Medadata Editor ( too tricky to use on windows, better use an external tool ). +- With some Nvidia GPUs when VSYNC is active, SDL_GL_SwapWindow takes a lot of CPU : Introduce a smart calculation based on display frequency to reduce the time SDL_GL_SwapWindow has to wait. This saves a lot of CPU load. + +Je crois que c'est ร  peu prรจs tout... + +Building +======== + +EmulationStation uses some C++11 code, which means you'll need to use at least g++-4.7 on Linux, or VS2010 on Windows, to compile. + +EmulationStation has a few dependencies. For building, you'll need CMake, SDL2, FreeImage, FreeType, cURL and RapidJSON. You also should probably install the `fonts-droid` package which contains fallback fonts for Chinese/Japanese/Korean characters, but ES will still work fine without it (this package is only used at run-time). + +**On Debian/Ubuntu:** +All of this be easily installed with `apt-get`: +```bash +sudo apt-get install libsdl2-dev libfreeimage-dev libfreetype6-dev libcurl4-openssl-dev rapidjson-dev \ + libasound2-dev libgl1-mesa-dev build-essential cmake fonts-droid-fallback libvlc-dev \ + libvlccore-dev vlc-bin +``` +**On Fedora:** +All of this be easily installed with `dnf` (with rpmfusion activated) : +```bash +sudo dnf install SDL2-devel freeimage-devel freetype-devel curl-devel \ + alsa-lib-devel mesa-libGL-devel cmake \ + vlc-devel rapidjson-devel +``` + +Note this Repository uses a git submodule - to checkout the source and all submodules, use + +```bash +git clone --recursive https://github.com/RetroPie/EmulationStation.git +``` + +or + +```bash +git clone https://github.com/RetroPie/EmulationStation.git +cd EmulationStation +git submodule update --init +``` + +Then, generate and build the Makefile with CMake: +```bash +cd YourEmulationStationDirectory +cmake . +make +``` + +**On the Raspberry Pi:** + +Complete Raspberry Pi build instructions at [emulationstation.org](http://emulationstation.org/gettingstarted.html#install_rpi_standalone). + +**On Windows:** + +[FreeImage](http://downloads.sourceforge.net/freeimage/FreeImage3154Win32.zip) + +[FreeType2](http://download.savannah.gnu.org/releases/freetype/freetype-2.4.9.tar.bz2) (you'll need to compile) + +[SDL2](http://www.libsdl.org/release/SDL2-devel-2.0.8-VC.zip) + +[cURL](http://curl.haxx.se/download.html) (you'll need to compile or get the pre-compiled DLL version) + +[RapisJSON](https://github.com/tencent/rapidjson) (you'll need the `include/rapidsjon` added to the include path) + +(Remember to copy necessary .DLLs into the same folder as the executable: probably FreeImage.dll, freetype6.dll, SDL2.dll, libcurl.dll, and zlib1.dll. Exact list depends on if you built your libraries in "static" mode or not.) + +[CMake](http://www.cmake.org/cmake/resources/software.html) (this is used for generating the Visual Studio project) + +(If you don't know how to use CMake, here are some hints: run cmake-gui and point it at your EmulationStation folder. Point the "build" directory somewhere - I use EmulationStation/build. Click configure, choose "Visual Studio [year] Project", fill in red fields as they appear and keep clicking Configure (you may need to check "Advanced"), then click Generate.) + + +Configuring +=========== + +**~/.emulationstation/es_systems.cfg:** +When first run, an example systems configuration file will be created at `~/.emulationstation/es_systems.cfg`. `~` is `$HOME` on Linux, and `%HOMEPATH%` on Windows. This example has some comments explaining how to write the configuration file. See the "Writing an es_systems.cfg" section for more information. + +**Keep in mind you'll have to set up your emulator separately from EmulationStation!** + +**~/.emulationstation/es_input.cfg:** +When you first start EmulationStation, you will be prompted to configure an input device. The process is thus: + +1. Hold a button on the device you want to configure. This includes the keyboard. + +2. Press the buttons as they appear in the list. Some inputs can be skipped by holding any button down for a few seconds (e.g. page up/page down). + +3. You can review your mappings by pressing up and down, making any changes by pressing A. + +4. Choose "SAVE" to save this device and close the input configuration screen. + +The new configuration will be added to the `~/.emulationstation/es_input.cfg` file. + +**Both new and old devices can be (re)configured at any time by pressing the Start button and choosing "CONFIGURE INPUT".** From here, you may unplug the device you used to open the menu and plug in a new one, if necessary. New devices will be appended to the existing input configuration file, so your old devices will remain configured. + +**If your controller stops working, you can delete the `~/.emulationstation/es_input.cfg` file to make the input configuration screen re-appear on next run.** + + +You can use `--help` or `-h` to view a list of command-line options. Briefly outlined here: +``` +--resolution [width] [height] try and force a particular resolution +--gamelist-only skip automatic game search, only read from gamelist.xml +--ignore-gamelist ignore the gamelist (useful for troubleshooting) +--draw-framerate display the framerate +--no-exit don't show the exit option in the menu +--no-splash don't show the splash screen +--debug more logging, show console on Windows +--scrape scrape using command line interface +--windowed not fullscreen, may be used with --resolution +--vsync [1/on or 0/off] turn vsync on or off (default is on) +--max-vram [size] Max VRAM to use in Mb before swapping. 0 for unlimited +--force-kid Force the UI mode to be Kid +--force-kiosk Force the UI mode to be Kiosk +--force-disable-filters Force the UI to ignore applied filters in gamelist +--help, -h summon a sentient, angry tuba +``` + +As long as ES hasn't frozen, you can always press F4 to close the application. + + +Writing an es_systems.cfg +========================= + +Complete configuration instructions at [emulationstation.org](http://emulationstation.org/gettingstarted.html#config). + +The `es_systems.cfg` file contains the system configuration data for EmulationStation, written in XML. This tells EmulationStation what systems you have, what platform they correspond to (for scraping), and where the games are located. + +ES will check two places for an es_systems.cfg file, in the following order, stopping after it finds one that works: +* `~/.emulationstation/es_systems.cfg` +* `/etc/emulationstation/es_systems.cfg` + +The order EmulationStation displays systems reflects the order you define them in. + +**NOTE:** A system *must* have at least one game present in its "path" directory, or ES will ignore it! If no valid systems are found, ES will report an error and quit! + +Here's an example es_systems.cfg: + +```xml + + + + + + + snes + + + Super Nintendo Entertainment System + + + ~/roms/snes + + + .smc .sfc .SMC .SFC + + + snesemulator %ROM% + + + + snes + + + snes + + +``` + +The following "tags" are replaced by ES in launch commands: + +`%ROM%` - Replaced with absolute path to the selected ROM, with most Bash special characters escaped with a backslash. + +`%BASENAME%` - Replaced with the "base" name of the path to the selected ROM. For example, a path of "/foo/bar.rom", this tag would be "bar". This tag is useful for setting up AdvanceMAME. + +`%ROM_RAW%` - Replaced with the unescaped, absolute path to the selected ROM. If your emulator is picky about paths, you might want to use this instead of %ROM%, but enclosed in quotes. + +See [SYSTEMS.md](SYSTEMS.md) for some live examples in EmulationStation. + +gamelist.xml +============ + +The gamelist.xml file for a system defines metadata for games, such as a name, image (like a screenshot or box art), description, release date, and rating. + +If at least one game in a system has an image specified, ES will use the detailed view for that system (which displays metadata alongside the game list). + +*You can use ES's [scraping](http://en.wikipedia.org/wiki/Web_scraping) tools to avoid creating a gamelist.xml by hand.* There are two ways to run the scraper: + +* **If you want to scrape multiple games:** press start to open the menu and choose the "SCRAPER" option. Adjust your settings and press "SCRAPE NOW". +* **If you just want to scrape one game:** find the game on the game list in ES and press select. Choose "EDIT THIS GAME'S METADATA" and then press the "SCRAPE" button at the bottom of the metadata editor. + +You can also edit metadata within ES by using the metadata editor - just find the game you wish to edit on the gamelist, press Select, and choose "EDIT THIS GAME'S METADATA." + +A command-line version of the scraper is also provided - just run emulationstation with `--scrape` *(currently broken)*. + +The switch `--ignore-gamelist` can be used to ignore the gamelist and force ES to use the non-detailed view. + +If you're writing a tool to generate or parse gamelist.xml files, you should check out [GAMELISTS.md](GAMELISTS.md) for more detailed documentation. + + +Themes +====== + +By default, EmulationStation looks pretty ugly. You can fix that. If you want to know more about making your own themes (or editing existing ones), read [THEMES.md](THEMES.md)! + +I've put some themes up for download on my EmulationStation webpage: http://aloshi.com/emulationstation#themes + +If you're using RetroPie, you should already have a nice set of themes automatically installed! + + +-Alec "Aloshi" Lofquist +http://www.aloshi.com +http://www.emulationstation.org diff --git a/THEMES.md b/THEMES.md index c10b2e57d0..f5e9fe6634 100644 --- a/THEMES.md +++ b/THEMES.md @@ -69,8 +69,8 @@ How it works Everything must be inside a `` tag. -**The `` tag *must* be specified**. This is the version of the theming system the theme was designed for. The current version is 3. - +**The `` tag *must* be specified**. This is the version of the theming system the theme was designed for. +The current version is 4. A *view* can be thought of as a particular "screen" within EmulationStation. Views are defined like this: @@ -109,6 +109,14 @@ Or, you can create your own elements by adding `extra="true"` (as is done in the ValueHere ``` +A *customView* can be thought of as a particular "screen" within EmulationStation. +Custom Views must inherit one of the standard views and are defined like this: + +```xml + + ... define elements here ... + +``` @@ -124,7 +132,7 @@ You can include theme files within theme files, similar to `#include` in C (thou `~/.emulationstation/all_themes.xml`: ```xml - 3 + 4 ./all_themes/myfont.ttf @@ -137,7 +145,7 @@ You can include theme files within theme files, similar to `#include` in C (thou `~/.emulationstation/snes/theme.xml`: ```xml - 3 + 4 ./../all_themes.xml @@ -150,7 +158,7 @@ You can include theme files within theme files, similar to `#include` in C (thou Is equivalent to this `snes/theme.xml`: ```xml - 3 + 4 ./all_themes/myfont.ttf @@ -170,7 +178,7 @@ Sometimes you want to apply the same properties to the same elements across mult ```xml - 3 + 4 ./snes_art/snes_header.png @@ -284,7 +292,7 @@ You can now change the order in which elements are rendered by setting `zIndex` * `imagegrid name="gamegrid"` - 20 * Media * `image name="md_image"` - 30 - * `video name="md_video"` - 30 + * `video name="md_video"` - 31 * `image name="md_marquee"` - 35 * Metadata - 40 * Labels @@ -392,6 +400,10 @@ Reference * `image name="md_image"` - POSITION | SIZE | Z_INDEX - Path is the "image" metadata for the currently selected game. + * `video name="md_video"` - POSITION | SIZE | Z_INDEX + - Path is the "video" metadata for the currently selected game. + * `image name="md_marquee"` - POSITION | SIZE | Z_INDEX + - Path is the "marquee" metadata for the currently selected game. * `rating name="md_rating"` - ALL - The "rating" metadata. * `datetime name="md_releasedate"` - ALL @@ -534,10 +546,33 @@ Reference - Displays details of the system currently selected in the carousel. * You can use extra elements (elements with `extra="true"`) to add your own backgrounds, etc. They will be displayed behind the carousel, and scroll relative to the carousel. +#### menu +* `helpsystem name="help"` - ALL + - The help system style for this view. If not defined, menus will have the same helpsystem as defined in system view. +* `menuBackground name="menubg"` - COLOR | PATH | FADEPATH + - The background behind menus. you can set an image and/or change color (alpha supported) + +* `menuSwitch name="menuswitch"` - PATHON | PATHOFF + - Images for the on/off switch in menus +* `menuSlider name="menuslider"` - PATH + - Image for the slider knob in menus +* `menuButton name="menubutton"` - PATH | FILLEDPATH + - Images for menu buttons +* `menuText name="menutext"` - FONTPATH | FONTSIZE | COLOR + - text for all menu entries +* `menuText name="menutitle"` - FONTPATH | FONTSIZE | COLOR + - text for menu titles +* `menuText name="menufooter"` - FONTPATH | FONTSIZE | COLOR + - text for menu footers or subtitles +* `menuTextSmall name="menutextsmall"` - FONTPATH | FONTSIZE | COLOR + - text for menu entries in smallerfont + +menu is used to theme helpsystem and ES menus. ## Types of properties: * NORMALIZED_PAIR - two decimals, in the range [0..1], delimited by a space. For example, `0.25 0.5`. Most commonly used for position (x and y coordinates) and size (width and height). +* NORMALIZED_RECT - four decimals, in the range [0..1], delimited by a space. For example, `0.25 0.5 0.10 0.30`. Most commonly used for padding to store top, left, bottom and right coordinates. * PATH - a path. If the first character is a `~`, it will be expanded into the environment variable for the home path (`$HOME` for Linux or `%HOMEPATH%` for Windows). If the first character is a `.`, it will be expanded to the theme file's directory, allowing you to specify resources relative to the theme file, like so: `./../general_art/myfont.ttf`. * BOOLEAN - `true`/`1` or `false`/`0`. * COLOR - a hexidecimal RGB or RGBA color (6 or 8 digits). If 6 digits, will assume the alpha channel is `FF` (not transparent). @@ -580,20 +615,35 @@ Can be created as an extra. - If true, component will be rendered, otherwise rendering will be skipped. Can be used to hide elements from a particular view. * `zIndex` - type: FLOAT. - z-index value for component. Components will be rendered in order of z-index value from low to high. +* `reflexion` - type: NORMALIZED_PAIR. + - NEW : table reflexion effect. First item is top position alpha, second is bottom alpha. + #### imagegrid * `pos` - type: NORMALIZED_PAIR. * `size` - type: NORMALIZED_PAIR. - The size of the grid. Take care the selected tile can go out of the grid size, so don't position the grid too close to another element or the screen border. -* `margin` - type: NORMALIZED_PAIR. +* `margin` - type: NORMALIZED_PAIR. Margin between tiles. +* `padding` - type: NORMALIZED_RECT. + - NEW : Padding for displaying tiles. +* `autoLayout` - type: NORMALIZED_PAIR. + - NEW : Number of column and rows in the grid (integer values). +* `autoLayoutSelectedZoom` - type: FLOAT. + - NEW : Zoom factor to apply when a tile is selected. +* `imageSource` - type: STRING. + - NEW : Selects the image to display. `thumbnail` by default, can also be set to `image` or `marquee`. +* `showVideoAtDelay` - type: FLOAT. + - NEW : delay in millseconds to display video, when the tile is selected. * `gameImage` - type: PATH. - The default image used for games which doesn't have an image. * `folderImage` - type: PATH. - The default image used for folders which doesn't have an image. * `scrollDirection` - type: STRING. - `vertical` by default, can also be set to `horizontal`. Not that in `horizontal` mod, the tiles are ordered from top to bottom, then from left to right. - +* `zIndex` - type: FLOAT. + - NEW : z-index value for component. Components will be rendered in order of z-index value from low to high. + #### gridtile * `size` - type: NORMALIZED_PAIR. @@ -612,7 +662,13 @@ Can be created as an extra. - Set the color of the center part of the ninepatch. The default tile background center color and selected tile background center color have no influence on each others. * `backgroundEdgeColor` - type: COLOR. - Set the color of the edge parts of the ninepatch. The default tile background edge color and selected tile background edge color have no influence on each others. - +* `selectionMode` - type: STRING. + - NEW : Selects if the background is over the full tile or only the image. `full` by default, can also be set to `image`. +* `imageSizeMode` - type: STRING. + - NEW : Selects the image sizing mode. `maxSize` by default, can also be set to `minSize` (outer zoom) or `size` (stretch). +* `reflexion` - type: NORMALIZED_PAIR. + - NEW : table reflexion effect. First item is top position alpha, second is bottom alpha. + #### video * `pos` - type: NORMALIZED_PAIR. @@ -638,7 +694,9 @@ Can be created as an extra. - If true, component will be rendered, otherwise rendering will be skipped. Can be used to hide elements from a particular view. * `zIndex` - type: FLOAT. - z-index value for component. Components will be rendered in order of z-index value from low to high. - +* `path` - type: PATH. + - NEW : Path to video file if video is an extra. + #### text Can be created as an extra. @@ -670,6 +728,10 @@ Can be created as an extra. - If true, component will be rendered, otherwise rendering will be skipped. Can be used to hide elements from a particular view. * `zIndex` - type: FLOAT. - z-index value for component. Components will be rendered in order of z-index value from low to high. +* `glowColor` - type: COLOR; + - NEW : Defines the color of the glow around the text. +* `glowSize` - type: FLOAT. + - NEW : Defines the size of the glow around the text. #### textlist @@ -679,6 +741,8 @@ Can be created as an extra. - Where on the component `pos` refers to. For example, an origin of `0.5 0.5` and a `pos` of `0.5 0.5` would place the component exactly in the middle of the screen. If the "POSITION" and "SIZE" attributes are themable, "ORIGIN" is implied. * `selectorColor` - type: COLOR. - Color of the "selector bar." +* `selectorColorEnd` - type: NORMALIZED_PAIR. + - NEW : Bottom color for the gradient of the "selector bar." * `selectorImagePath` - type: PATH. - Path to image to render in place of "selector bar." * `selectorImageTile` - type: BOOLEAN. @@ -804,6 +868,10 @@ EmulationStation borrows the concept of "nine patches" from Android (or "9-Slice * `color` - type: COLOR. - Controls the color of the carousel background. - Default is FFFFFFD8 +* `colorEnd` - type: COLOR. + - NEW : Color for the end of gradient +* `gradientType` - type: STRING. + - NEW : Sets the gradient direction. Accepted values are "horizontal" and "vertical". * `logoSize` - type: NORMALIZED_PAIR. Default is "0.25 0.155" * `logoScale` - type: FLOAT. - Selected logo is increased in size by this scale @@ -825,6 +893,26 @@ EmulationStation borrows the concept of "nine patches" from Android (or "9-Slice - Default is 3 * `zIndex` - type: FLOAT. - z-index value for component. Components will be rendered in order of z-index value from low to high. +* `logoPos` - type: NORMALIZED_PAIR. + - NEW : Set the logo position if it is not centered. + +#### menuText & menuTextSmall + +* `color` - type: COLOR. + - Default is 777777FF +* `fontPath` - type: PATH. + - Path to a truetype font (.ttf). +* `fontSize` - type: FLOAT. + - Size of the font as a percentage of screen height (e.g. for a value of `0.1`, the text's height would be 10% of the screen height). Default is 0.085 for menutitle, 0.045 for menutext and 0.035 for menufooter and menutextsmall. +* `separatorColor` - type: COLOR. + - Default is C6C7C6FF. Color of lines that separates menu entries. +* `selectedColor` - type: COLOR. + - Default is FFFFFFFF. Color of text for selected menu entry. +* `selectorColor` - type: COLOR. + - Default is 878787FF. Color of the selector bar. +* `selectorColorEnd` - type: NORMALIZED_PAIR. + - NEW : Bottom color for the gradient of the "selector bar." + The help system is a special element that displays a context-sensitive list of actions the user can take at any time. You should try and keep the position constant throughout every screen. Keep in mind the "default" settings (including position) are used whenever the user opens a menu. diff --git a/es-app/CMakeLists.txt b/es-app/CMakeLists.txt index 4ff4fdcce9..fece7e1532 100644 --- a/es-app/CMakeLists.txt +++ b/es-app/CMakeLists.txt @@ -13,6 +13,9 @@ set(ES_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/src/FileFilterIndex.h ${CMAKE_CURRENT_SOURCE_DIR}/src/SystemScreenSaver.h ${CMAKE_CURRENT_SOURCE_DIR}/src/CollectionSystemManager.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/ApiSystem.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/ContentInstaller.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/NetworkThread.h # GuiComponents ${CMAKE_CURRENT_SOURCE_DIR}/src/components/AsyncReqComponent.h @@ -34,14 +37,16 @@ set(ES_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperMulti.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperStart.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiGamelistFilter.h - ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiCollectionSystemsOptions.h - ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiInfoPopup.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiThemeInstall.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiCollectionSystemsOptions.h # Scrapers ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/Scraper.h ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/GamesDBJSONScraper.h ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/GamesDBJSONScraperResources.h ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/ScreenScraper.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/ThreadedScraper.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/md5.h # Views ${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/BasicGameListView.h @@ -72,6 +77,9 @@ set(ES_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/FileFilterIndex.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/SystemScreenSaver.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/CollectionSystemManager.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/ApiSystem.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/ContentInstaller.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/NetworkThread.cpp # GuiComponents ${CMAKE_CURRENT_SOURCE_DIR}/src/components/AsyncReqComponent.cpp @@ -92,14 +100,16 @@ set(ES_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperMulti.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperStart.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiGamelistFilter.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiCollectionSystemsOptions.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiInfoPopup.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiThemeInstall.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiCollectionSystemsOptions.cpp # Scrapers ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/Scraper.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/GamesDBJSONScraper.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/GamesDBJSONScraperResources.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/ScreenScraper.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/ThreadedScraper.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/md5.cpp # Views ${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/BasicGameListView.cpp @@ -158,8 +168,8 @@ SET(CPACK_RESOURCE_FILE README "${CMAKE_CURRENT_SOURCE_DIR}/README.md") SET(CPACK_DEBIAN_PACKAGE_MAINTAINER "Alec Lofquist ") SET(CPACK_DEBIAN_PACKAGE_SECTION "misc") SET(CPACK_DEBIAN_PACKAGE_PRIORITY "extra") -SET(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6, libsdl2-2.0-0, libfreeimage3, libfreetype6, libcurl3, libasound2") -SET(CPACK_DEBIAN_PACKAGE_BUILDS_DEPENDS "debhelper (>= 8.0.0), cmake, g++ (>= 4.8), libsdl2-dev, libfreeimage-dev, libfreetype6-dev, libcurl4-openssl-dev, libasound2-dev, libgl1-mesa-dev, rapidjson-dev") +SET(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6, libsdl2-2.0-0, libsdl2-mixer-2.0-0, libfreeimage3, libfreetype6, libcurl3, libasound2") +SET(CPACK_DEBIAN_PACKAGE_BUILDS_DEPENDS "debhelper (>= 8.0.0), cmake, g++ (>= 4.8), libsdl2-dev, libsdl2-mixer-dev, libfreeimage-dev, libfreetype6-dev, libcurl4-openssl-dev, libasound2-dev, libgl1-mesa-dev, rapidjson-dev") SET(CPACK_PACKAGE_VENDOR "emulationstation.org") SET(CPACK_PACKAGE_VERSION "2.0.0~rc1") diff --git a/es-app/src/ApiSystem.cpp b/es-app/src/ApiSystem.cpp new file mode 100644 index 0000000000..dd2d300f1d --- /dev/null +++ b/es-app/src/ApiSystem.cpp @@ -0,0 +1,440 @@ +#include "ApiSystem.h" +#include "HttpReq.h" +#include "utils/FileSystemUtil.h" +#include "utils/StringUtil.h" +#include +#include +#include +#include "Log.h" +#include "Window.h" +#include "components/AsyncNotificationComponent.h" + +UpdateState::State ApiSystem::state = UpdateState::State::NO_UPDATE; + +class ThreadedUpdater +{ +public: + ThreadedUpdater(Window* window) : mWindow(window) + { + ApiSystem::state = UpdateState::State::UPDATER_RUNNING; + + mWndNotification = new AsyncNotificationComponent(window, false); + mWndNotification->updateTitle(_U("\uF019 ") + _("EMULATIONSTATION")); + + mWindow->registerNotificationComponent(mWndNotification); + mHandle = new std::thread(&ThreadedUpdater::threadUpdate, this); + } + + ~ThreadedUpdater() + { + mWindow->unRegisterNotificationComponent(mWndNotification); + delete mWndNotification; + } + + void threadUpdate() + { + std::pair updateStatus = ApiSystem::updateSystem([this](const std::string info) + { + auto pos = info.find(">>>"); + if (pos != std::string::npos) + { + std::string percent(info.substr(pos)); + percent = Utils::String::replace(percent, ">", ""); + percent = Utils::String::replace(percent, "%", ""); + percent = Utils::String::replace(percent, " ", ""); + + int value = atoi(percent.c_str()); + + std::string text(info.substr(0, pos)); + text = Utils::String::trim(text); + + mWndNotification->updatePercent(value); + mWndNotification->updateText(text); + } + else + { + mWndNotification->updatePercent(-1); + mWndNotification->updateText(info); + } + }); + + if (updateStatus.second == 0) + { + ApiSystem::state = UpdateState::State::UPDATE_READY; + + mWndNotification->updateTitle(_U("\uF019 ") + _("UPDATE IS READY")); + mWndNotification->updateText(_("RESTART EMULATIONSTATION TO APPLY")); + + std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::hours(12)); + } + else + { + ApiSystem::state = UpdateState::State::NO_UPDATE; + + std::string error = _("AN ERROR OCCURED") + std::string(": ") + updateStatus.first; + mWindow->displayNotificationMessage(error); + } + + delete this; + } + +private: + std::thread* mHandle; + AsyncNotificationComponent* mWndNotification; + Window* mWindow; +}; + +void ApiSystem::startUpdate(Window* c) +{ +#if WIN32 + new ThreadedUpdater(c); +#endif +} + +std::string ApiSystem::checkUpdateVersion() +{ +#if WIN32 + std::string localVersion; + std::string localVersionFile = Utils::FileSystem::getExePath() + "/version.info"; + if (Utils::FileSystem::exists(localVersionFile)) + { + localVersion = Utils::FileSystem::readAllText(localVersionFile); + localVersion = Utils::String::replace(Utils::String::replace(localVersion, "\r", ""), "\n", ""); + } + + HttpReq httpreq("https://github.com/fabricecaruso/EmulationStation/releases/download/continuous-master/version.info"); + if (httpreq.wait()) + { + std::string serverVersion = httpreq.getContent(); + serverVersion = Utils::String::replace(Utils::String::replace(serverVersion, "\r", ""), "\n", ""); + if (!serverVersion.empty() && serverVersion != localVersion) + return serverVersion; + } +#endif + + return ""; +} + +#if WIN32 +#include +#include // #include for _bstr_t +#pragma comment(lib, "shell32.lib") +#pragma comment (lib, "comsuppw.lib" ) // link with "comsuppw.lib" (or debug version: "comsuppwd.lib") + +bool unzipFile(const std::string fileName, const std::string dest) +{ + bool ret = false; + + HRESULT hResult; + IShellDispatch* pISD; + Folder* pFromZip = nullptr; + VARIANT vDir, vFile, vOpt; + + OleInitialize(NULL); + CoInitialize(NULL); + + hResult = CoCreateInstance(CLSID_Shell, NULL, CLSCTX_INPROC_SERVER, IID_IShellDispatch, (void **)&pISD); + + if (SUCCEEDED(hResult)) + { + VariantInit(&vDir); + vDir.vt = VT_BSTR; + + int zipDirLen = (lstrlenA(fileName.c_str()) + 1) * sizeof(WCHAR); + BSTR bstrZip = SysAllocStringByteLen(NULL, zipDirLen); + MultiByteToWideChar(CP_ACP, 0, fileName.c_str(), -1, bstrZip, zipDirLen); + vDir.bstrVal = bstrZip; + + hResult = pISD->NameSpace(vDir, &pFromZip); + + if (hResult == S_OK && pFromZip != nullptr) + { + if (!Utils::FileSystem::exists(dest)) + Utils::FileSystem::createDirectory(dest); + + Folder *pToFolder = NULL; + + VariantInit(&vFile); + vFile.vt = VT_BSTR; + + int fnLen = (lstrlenA(dest.c_str()) + 1) * sizeof(WCHAR); + BSTR bstrFolder = SysAllocStringByteLen(NULL, fnLen); + MultiByteToWideChar(CP_ACP, 0, dest.c_str(), -1, bstrFolder, fnLen); + vFile.bstrVal = bstrFolder; + + hResult = pISD->NameSpace(vFile, &pToFolder); + if (hResult == S_OK && pToFolder) + { + FolderItems *fi = NULL; + pFromZip->Items(&fi); + + VariantInit(&vOpt); + vOpt.vt = VT_I4; + vOpt.lVal = FOF_NO_UI; //4; // Do not display a progress dialog box + + VARIANT newV; + VariantInit(&newV); + newV.vt = VT_DISPATCH; + newV.pdispVal = fi; + hResult = pToFolder->CopyHere(newV, vOpt); + if (hResult == S_OK) + ret = true; + + pFromZip->Release(); + pToFolder->Release(); + } + } + pISD->Release(); + } + + CoUninitialize(); + return ret; +} + +bool downloadFile(const std::string url, const std::string fileName, const std::string label, const std::function& func) +{ + if (func != nullptr) + func("Downloading " + label); + + HttpReq httpreq(url, fileName); + while (httpreq.status() == HttpReq::REQ_IN_PROGRESS) + { + if (func != nullptr) + func(std::string("Downloading " + label + " >>> " + std::to_string(httpreq.getPercent()) + " %")); + + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + } + + if (httpreq.status() != HttpReq::REQ_SUCCESS) + return false; + + return true; +} + + +void deleteDirectoryFiles(const std::string path) +{ + auto files = Utils::FileSystem::getDirContent(path, true, true); + std::reverse(std::begin(files), std::end(files)); + for (auto file : files) + { + if (Utils::FileSystem::isDirectory(file)) + ::RemoveDirectoryA(Utils::FileSystem::getPreferredPath(file).c_str()); + else + Utils::FileSystem::removeFile(file); + } +} +#endif + +std::pair ApiSystem::updateSystem(const std::function& func) +{ +#if WIN32 + std::string url = "https://github.com/fabricecaruso/EmulationStation/releases/download/continuous-master/EmulationStation-Win32-no-deps.zip"; + + std::string fileName = Utils::FileSystem::getFileName(url); + std::string path = Utils::FileSystem::getHomePath() + "/.emulationstation/update"; + + if (!Utils::FileSystem::exists(path)) + Utils::FileSystem::createDirectory(path); + else + deleteDirectoryFiles(path); + + std::string zipFile = path + "/" + fileName; + + if (downloadFile(url, zipFile, "update", func)) + { + if (func != nullptr) + func(std::string("Extracting update")); + + unzipFile(Utils::FileSystem::getPreferredPath(zipFile), Utils::FileSystem::getPreferredPath(path)); + Utils::FileSystem::removeFile(zipFile); + + auto files = Utils::FileSystem::getDirContent(path, true, true); + for (auto file : files) + { + + std::string relative = Utils::FileSystem::createRelativePath(file, path, false); + if (Utils::String::startsWith(relative, "./")) + relative = relative.substr(2); + + std::string localPath = Utils::FileSystem::getExePath() + "/" + relative; + + if (Utils::FileSystem::isDirectory(file)) + { + if (!Utils::FileSystem::exists(localPath)) + Utils::FileSystem::createDirectory(localPath); + } + else + { + if (Utils::FileSystem::exists(localPath)) + { + Utils::FileSystem::removeFile(localPath + ".old"); + rename(localPath.c_str(), (localPath + ".old").c_str()); + } + + if (Utils::FileSystem::copyFile(file, localPath)) + { + Utils::FileSystem::removeFile(localPath + ".old"); + Utils::FileSystem::removeFile(file); + } + } + } + + deleteDirectoryFiles(path); + + return std::pair("done.", 0); + } +#endif + + return std::pair("error.", 1); +} + +std::vector ApiSystem::getThemesList() +{ + LOG(LogDebug) << "ApiSystem::getThemesList"; + + std::vector res; + + HttpReq httpreq("https://batocera.org/upgrades/themes.txt"); + if (httpreq.wait()) + { + auto lines = Utils::String::split(httpreq.getContent(), '\n'); + for (auto line : lines) + { + auto parts = Utils::String::splitAny(line, " \t"); + if (parts.size() > 1) + { + auto themeName = parts[0]; + + std::string themeUrl = parts[1]; + std::string themeFolder = Utils::FileSystem::getFileName(themeUrl); + + bool themeExists = false; + + std::vector paths{ + Utils::FileSystem::getHomePath() + "/.emulationstation/themes", + "/etc/emulationstation/themes", + "/userdata/themes" + }; + + for (auto path : paths) + { + themeExists = Utils::FileSystem::isDirectory(path + "/" + themeName) || + Utils::FileSystem::isDirectory(path + "/" + themeFolder) || + Utils::FileSystem::isDirectory(path + "/" + themeFolder + "-master"); + + if (themeExists) + break; + } + + ThemeDownloadInfo info; + info.installed = themeExists; + info.name = themeName; + info.url = themeUrl; + + res.push_back(info); + } + } + } + + return res; +} + +bool downloadGitRepository(const std::string url, const std::string fileName, const std::string label, const std::function& func) +{ + if (func != nullptr) + func(_("Downloading") + " " + label); + + long downloadSize = 0; + + std::string statUrl = Utils::String::replace(url, "https://github.com/", "https://api.github.com/repos/"); + if (statUrl != url) + { + HttpReq statreq(statUrl); + if (statreq.wait()) + { + std::string content = statreq.getContent(); + auto pos = content.find("\"size\": "); + if (pos != std::string::npos) + { + auto end = content.find(",", pos); + if (end != std::string::npos) + downloadSize = atoi(content.substr(pos + 8, end - pos - 8).c_str()) * 1024; + } + } + } + + HttpReq httpreq(url + "/archive/master.zip", fileName); + + int curPos = -1; + while (httpreq.status() == HttpReq::REQ_IN_PROGRESS) + { + if (downloadSize > 0) + { + double pos = httpreq.getPosition(); + if (pos > 0 && curPos != pos) + { + if (func != nullptr) + { + std::string pc = std::to_string((int)(pos * 100.0 / downloadSize)); + func(std::string(_("Downloading") + " " + label + " >>> " + pc + " %")); + } + + curPos = pos; + } + } + + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + } + + if (httpreq.status() != HttpReq::REQ_SUCCESS) + return false; + + return true; +} + +std::pair ApiSystem::installTheme(std::string themeName, const std::function& func) +{ +#if WIN32 + for (auto theme : getThemesList()) + { + if (theme.name != themeName) + continue; + + std::string themeFileName = Utils::FileSystem::getFileName(theme.url); + std::string zipFile = Utils::FileSystem::getHomePath() + "/.emulationstation/themes/" + themeFileName + ".zip"; + zipFile = Utils::String::replace(zipFile, "/", "\\"); + + if (downloadGitRepository(theme.url, zipFile, themeName, func)) + { + if (func != nullptr) + func(_("Extracting") + " " + themeName); + + if (!unzipFile(zipFile, Utils::String::replace(Utils::FileSystem::getHomePath() + "/.emulationstation/themes", "/", "\\"))) + return std::pair(std::string("An error occured while extracting"), 1); + + std::string folderName = Utils::FileSystem::getHomePath() + "/.emulationstation/themes/" + themeFileName + "-master"; + if (Utils::FileSystem::isDirectory(folderName)) + { + std::string finalfolderName = Utils::FileSystem::getParent(folderName) + "/" + themeName; + + if (Utils::FileSystem::isDirectory(finalfolderName)) + deleteDirectoryFiles(finalfolderName); + + rename(folderName.c_str(), finalfolderName.c_str()); + + Utils::FileSystem::removeFile(zipFile); + + return std::pair(std::string("OK"), 0); + } + + return std::pair(std::string("Invalid extraction folder"), 1); + } + + return std::pair(std::string("An error occured while downloading"), 1); + } +#endif + + return std::pair(std::string("Theme not found"), 1); +} \ No newline at end of file diff --git a/es-app/src/ApiSystem.h b/es-app/src/ApiSystem.h new file mode 100644 index 0000000000..603197f512 --- /dev/null +++ b/es-app/src/ApiSystem.h @@ -0,0 +1,41 @@ +#ifndef API_SYSTEM +#define API_SYSTEM + +#include +#include +#include + +class Window; + +namespace UpdateState +{ + enum State + { + NO_UPDATE, + UPDATER_RUNNING, + UPDATE_READY + }; +} + +struct ThemeDownloadInfo +{ + bool installed; + std::string name; + std::string url; +}; + +class ApiSystem +{ +public: + static UpdateState::State state; + + static std::pair updateSystem(const std::function& func = nullptr); + static std::string checkUpdateVersion(); + static void startUpdate(Window* c); + + static std::vector getThemesList(); + static std::pair installTheme(std::string themeName, const std::function& func = nullptr); +}; + +#endif + diff --git a/es-app/src/CollectionSystemManager.cpp b/es-app/src/CollectionSystemManager.cpp index 6b665df78c..de71f138a9 100644 --- a/es-app/src/CollectionSystemManager.cpp +++ b/es-app/src/CollectionSystemManager.cpp @@ -1,1050 +1,1265 @@ -#include "CollectionSystemManager.h" - -#include "guis/GuiInfoPopup.h" -#include "utils/FileSystemUtil.h" -#include "utils/StringUtil.h" -#include "views/gamelist/IGameListView.h" -#include "views/ViewController.h" -#include "FileData.h" -#include "FileFilterIndex.h" -#include "Log.h" -#include "Settings.h" -#include "SystemData.h" -#include "ThemeData.h" -#include -#include - -std::string myCollectionsName = "collections"; - -#define LAST_PLAYED_MAX 50 - -/* Handling the getting, initialization, deinitialization, saving and deletion of - * a CollectionSystemManager Instance */ -CollectionSystemManager* CollectionSystemManager::sInstance = NULL; - -CollectionSystemManager::CollectionSystemManager(Window* window) : mWindow(window) -{ - CollectionSystemDecl systemDecls[] = { - //type name long name //default sort // theme folder // isCustom - { AUTO_ALL_GAMES, "all", "all games", "filename, ascending", "auto-allgames", false }, - { AUTO_LAST_PLAYED, "recent", "last played", "last played, descending", "auto-lastplayed", false }, - { AUTO_FAVORITES, "favorites", "favorites", "filename, ascending", "auto-favorites", false }, - { CUSTOM_COLLECTION, myCollectionsName, "collections", "filename, ascending", "custom-collections", true } - }; - - // create a map - std::vector tempSystemDecl = std::vector(systemDecls, systemDecls + sizeof(systemDecls) / sizeof(systemDecls[0])); - - for (std::vector::const_iterator it = tempSystemDecl.cbegin(); it != tempSystemDecl.cend(); ++it ) - { - mCollectionSystemDeclsIndex[(*it).name] = (*it); - } - - // creating standard environment data - mCollectionEnvData = new SystemEnvironmentData; - mCollectionEnvData->mStartPath = ""; - std::vector exts; - mCollectionEnvData->mSearchExtensions = exts; - mCollectionEnvData->mLaunchCommand = ""; - std::vector allPlatformIds; - allPlatformIds.push_back(PlatformIds::PLATFORM_IGNORE); - mCollectionEnvData->mPlatformIds = allPlatformIds; - - std::string path = getCollectionsFolder(); - if(!Utils::FileSystem::exists(path)) - Utils::FileSystem::createDirectory(path); - - mIsEditingCustom = false; - mEditingCollection = "Favorites"; - mEditingCollectionSystemData = NULL; - mCustomCollectionsBundle = NULL; -} - -CollectionSystemManager::~CollectionSystemManager() -{ - assert(sInstance == this); - removeCollectionsFromDisplayedSystems(); - - // iterate the map - for(std::map::const_iterator it = mCustomCollectionSystemsData.cbegin() ; it != mCustomCollectionSystemsData.cend() ; it++ ) - { - if (it->second.isPopulated) - { - saveCustomCollection(it->second.system); - } - delete it->second.system; - } - sInstance = NULL; -} - -CollectionSystemManager* CollectionSystemManager::get() -{ - assert(sInstance); - return sInstance; -} - -void CollectionSystemManager::init(Window* window) -{ - assert(!sInstance); - sInstance = new CollectionSystemManager(window); -} - -void CollectionSystemManager::deinit() -{ - if (sInstance) - { - delete sInstance; - } -} - -void CollectionSystemManager::saveCustomCollection(SystemData* sys) -{ - std::string name = sys->getName(); - std::unordered_map games = sys->getRootFolder()->getChildrenByFilename(); - bool found = mCustomCollectionSystemsData.find(name) != mCustomCollectionSystemsData.cend(); - if (found) { - CollectionSystemData sysData = mCustomCollectionSystemsData.at(name); - if (sysData.needsSave) - { - std::ofstream configFile; - configFile.open(getCustomCollectionConfigPath(name)); - for(std::unordered_map::const_iterator iter = games.cbegin(); iter != games.cend(); ++iter) - { - std::string path = iter->first; - configFile << path << std::endl; - } - configFile.close(); - } - } - else - { - LOG(LogError) << "Couldn't find collection to save! " << name; - } -} - -/* Methods to load all Collections into memory, and handle enabling the active ones */ -// loads all Collection Systems -void CollectionSystemManager::loadCollectionSystems() -{ - initAutoCollectionSystems(); - CollectionSystemDecl decl = mCollectionSystemDeclsIndex[myCollectionsName]; - mCustomCollectionsBundle = createNewCollectionEntry(decl.name, decl, false); - // we will also load custom systems here - initCustomCollectionSystems(); - if(Settings::getInstance()->getString("CollectionSystemsAuto") != "" || Settings::getInstance()->getString("CollectionSystemsCustom") != "") - { - // Now see which ones are enabled - loadEnabledListFromSettings(); - // add to the main System Vector, and create Views as needed - updateSystemsList(); - } -} - -// loads settings -void CollectionSystemManager::loadEnabledListFromSettings() -{ - // we parse the auto collection settings list - std::vector autoSelected = Utils::String::commaStringToVector(Settings::getInstance()->getString("CollectionSystemsAuto")); - - // iterate the map - for(std::map::iterator it = mAutoCollectionSystemsData.begin() ; it != mAutoCollectionSystemsData.end() ; it++ ) - { - it->second.isEnabled = (std::find(autoSelected.cbegin(), autoSelected.cend(), it->first) != autoSelected.cend()); - } - - // we parse the custom collection settings list - std::vector customSelected = Utils::String::commaStringToVector(Settings::getInstance()->getString("CollectionSystemsCustom")); - - // iterate the map - for(std::map::iterator it = mCustomCollectionSystemsData.begin() ; it != mCustomCollectionSystemsData.end() ; it++ ) - { - it->second.isEnabled = (std::find(customSelected.cbegin(), customSelected.cend(), it->first) != customSelected.cend()); - } -} - -// updates enabled system list in System View -void CollectionSystemManager::updateSystemsList() -{ - // remove all Collection Systems - removeCollectionsFromDisplayedSystems(); - // add custom enabled ones - addEnabledCollectionsToDisplayedSystems(&mCustomCollectionSystemsData); - - if(Settings::getInstance()->getBool("SortAllSystems")) - { - // sort custom individual systems with other systems - std::sort(SystemData::sSystemVector.begin(), SystemData::sSystemVector.end(), systemSort); - - // move RetroPie system to end, before auto collections - for(auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); ) - { - if ((*sysIt)->getName() == "retropie") - { - SystemData* retroPieSystem = (*sysIt); - sysIt = SystemData::sSystemVector.erase(sysIt); - SystemData::sSystemVector.push_back(retroPieSystem); - break; - } - else - { - sysIt++; - } - } - } - - if(mCustomCollectionsBundle->getRootFolder()->getChildren().size() > 0) - { - mCustomCollectionsBundle->getRootFolder()->sort(getSortTypeFromString(mCollectionSystemDeclsIndex[myCollectionsName].defaultSort)); - SystemData::sSystemVector.push_back(mCustomCollectionsBundle); - } - - // add auto enabled ones - addEnabledCollectionsToDisplayedSystems(&mAutoCollectionSystemsData); - - // create views for collections, before reload - for(auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); sysIt++) - { - if ((*sysIt)->isCollection()) - { - ViewController::get()->getGameListView((*sysIt)); - } - } - - // if we were editing a custom collection, and it's no longer enabled, exit edit mode - if(mIsEditingCustom && !mEditingCollectionSystemData->isEnabled) - { - exitEditMode(); - } -} - -/* Methods to manage collection files related to a source FileData */ -// updates all collection files related to the source file -void CollectionSystemManager::refreshCollectionSystems(FileData* file) -{ - if (!file->getSystem()->isGameSystem() || file->getType() != GAME) - return; - - std::map allCollections; - allCollections.insert(mAutoCollectionSystemsData.cbegin(), mAutoCollectionSystemsData.cend()); - allCollections.insert(mCustomCollectionSystemsData.cbegin(), mCustomCollectionSystemsData.cend()); - - for(auto sysDataIt = allCollections.cbegin(); sysDataIt != allCollections.cend(); sysDataIt++) - { - updateCollectionSystem(file, sysDataIt->second); - } -} - -void CollectionSystemManager::updateCollectionSystem(FileData* file, CollectionSystemData sysData) -{ - if (sysData.isPopulated) - { - // collection files use the full path as key, to avoid clashes - std::string key = file->getFullPath(); - - SystemData* curSys = sysData.system; - const std::unordered_map& children = curSys->getRootFolder()->getChildrenByFilename(); - bool found = children.find(key) != children.cend(); - FileData* rootFolder = curSys->getRootFolder(); - FileFilterIndex* fileIndex = curSys->getIndex(); - std::string name = curSys->getName(); - - if (found) { - // if we found it, we need to update it - FileData* collectionEntry = children.at(key); - // remove from index, so we can re-index metadata after refreshing - fileIndex->removeFromIndex(collectionEntry); - collectionEntry->refreshMetadata(); - // found and we are removing - if (name == "favorites" && file->metadata.get("favorite") == "false") { - // need to check if still marked as favorite, if not remove - ViewController::get()->getGameListView(curSys).get()->remove(collectionEntry, false); - } - else - { - // re-index with new metadata - fileIndex->addToIndex(collectionEntry); - ViewController::get()->onFileChanged(collectionEntry, FILE_METADATA_CHANGED); - } - } - else - { - // we didn't find it here - we need to check if we should add it - if (name == "recent" && file->metadata.get("playcount") > "0" && includeFileInAutoCollections(file) || - name == "favorites" && file->metadata.get("favorite") == "true") { - CollectionFileData* newGame = new CollectionFileData(file, curSys); - rootFolder->addChild(newGame); - fileIndex->addToIndex(newGame); - ViewController::get()->onFileChanged(file, FILE_METADATA_CHANGED); - ViewController::get()->getGameListView(curSys)->onFileChanged(newGame, FILE_METADATA_CHANGED); - } - } - rootFolder->sort(getSortTypeFromString(mCollectionSystemDeclsIndex[name].defaultSort)); - if (name == "recent") - { - trimCollectionCount(rootFolder, LAST_PLAYED_MAX); - ViewController::get()->onFileChanged(rootFolder, FILE_METADATA_CHANGED); - } - else - ViewController::get()->onFileChanged(rootFolder, FILE_SORTED); - } -} - -void CollectionSystemManager::trimCollectionCount(FileData* rootFolder, int limit) -{ - SystemData* curSys = rootFolder->getSystem(); - while ((int)rootFolder->getChildrenListToDisplay().size() > limit) - { - CollectionFileData* gameToRemove = (CollectionFileData*)rootFolder->getChildrenListToDisplay().back(); - ViewController::get()->getGameListView(curSys).get()->remove(gameToRemove, false); - } -} - -// deletes all collection files from collection systems related to the source file -void CollectionSystemManager::deleteCollectionFiles(FileData* file) -{ - // collection files use the full path as key, to avoid clashes - std::string key = file->getFullPath(); - // find games in collection systems - std::map allCollections; - allCollections.insert(mAutoCollectionSystemsData.cbegin(), mAutoCollectionSystemsData.cend()); - allCollections.insert(mCustomCollectionSystemsData.cbegin(), mCustomCollectionSystemsData.cend()); - - for(auto sysDataIt = allCollections.begin(); sysDataIt != allCollections.end(); sysDataIt++) - { - if (sysDataIt->second.isPopulated) - { - const std::unordered_map& children = (sysDataIt->second.system)->getRootFolder()->getChildrenByFilename(); - - bool found = children.find(key) != children.cend(); - if (found) { - sysDataIt->second.needsSave = true; - FileData* collectionEntry = children.at(key); - SystemData* systemViewToUpdate = getSystemToView(sysDataIt->second.system); - ViewController::get()->getGameListView(systemViewToUpdate).get()->remove(collectionEntry, false); - } - } - } -} - -// returns whether the current theme is compatible with Automatic or Custom Collections -bool CollectionSystemManager::isThemeGenericCollectionCompatible(bool genericCustomCollections) -{ - std::vector cfgSys = getCollectionThemeFolders(genericCustomCollections); - for(auto sysIt = cfgSys.cbegin(); sysIt != cfgSys.cend(); sysIt++) - { - if(!themeFolderExists(*sysIt)) - return false; - } - return true; -} - -bool CollectionSystemManager::isThemeCustomCollectionCompatible(std::vector stringVector) -{ - if (isThemeGenericCollectionCompatible(true)) - return true; - - // get theme path - auto themeSets = ThemeData::getThemeSets(); - auto set = themeSets.find(Settings::getInstance()->getString("ThemeSet")); - if(set != themeSets.cend()) - { - std::string defaultThemeFilePath = set->second.path + "/theme.xml"; - if (Utils::FileSystem::exists(defaultThemeFilePath)) - { - return true; - } - } - - for(auto sysIt = stringVector.cbegin(); sysIt != stringVector.cend(); sysIt++) - { - if(!themeFolderExists(*sysIt)) - return false; - } - return true; -} - -std::string CollectionSystemManager::getValidNewCollectionName(std::string inName, int index) -{ - std::string name = inName; - - if(index == 0) - { - size_t remove = std::string::npos; - - // get valid name - while((remove = name.find_first_not_of("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-[]() ")) != std::string::npos) - { - name.erase(remove, 1); - } - } - else - { - name += " (" + std::to_string(index) + ")"; - } - - if(name == "") - { - name = "New Collection"; - } - - if(name != inName) - { - LOG(LogInfo) << "Had to change name, from: " << inName << " to: " << name; - } - - // get used systems in es_systems.cfg - std::vector systemsInUse = getSystemsFromConfig(); - // get folders assigned to custom collections - std::vector autoSys = getCollectionThemeFolders(false); - // get folder assigned to custom collections - std::vector customSys = getCollectionThemeFolders(true); - // get folders assigned to user collections - std::vector userSys = getUserCollectionThemeFolders(); - // add them all to the list of systems in use - systemsInUse.insert(systemsInUse.cend(), autoSys.cbegin(), autoSys.cend()); - systemsInUse.insert(systemsInUse.cend(), customSys.cbegin(), customSys.cend()); - systemsInUse.insert(systemsInUse.cend(), userSys.cbegin(), userSys.cend()); - for(auto sysIt = systemsInUse.cbegin(); sysIt != systemsInUse.cend(); sysIt++) - { - if (*sysIt == name) - { - if(index > 0) { - name = name.substr(0, name.size()-4); - } - return getValidNewCollectionName(name, index+1); - } - } - // if it matches one of the custom collections reserved names - if (mCollectionSystemDeclsIndex.find(name) != mCollectionSystemDeclsIndex.cend()) - return getValidNewCollectionName(name, index+1); - return name; -} - -void CollectionSystemManager::setEditMode(std::string collectionName) -{ - if (mCustomCollectionSystemsData.find(collectionName) == mCustomCollectionSystemsData.cend()) - { - LOG(LogError) << "Tried to edit a non-existing collection: " << collectionName; - return; - } - mIsEditingCustom = true; - mEditingCollection = collectionName; - - CollectionSystemData* sysData = &(mCustomCollectionSystemsData.at(mEditingCollection)); - if (!sysData->isPopulated) - { - populateCustomCollection(sysData); - } - // if it's bundled, this needs to be the bundle system - mEditingCollectionSystemData = sysData; - - GuiInfoPopup* s = new GuiInfoPopup(mWindow, "Editing the '" + Utils::String::toUpper(collectionName) + "' Collection. Add/remove games with Y.", 10000); - mWindow->setInfoPopup(s); -} - -void CollectionSystemManager::exitEditMode() -{ - GuiInfoPopup* s = new GuiInfoPopup(mWindow, "Finished editing the '" + mEditingCollection + "' Collection.", 4000); - mWindow->setInfoPopup(s); - mIsEditingCustom = false; - mEditingCollection = "Favorites"; -} - -// adds or removes a game from a specific collection -bool CollectionSystemManager::toggleGameInCollection(FileData* file) -{ - if (file->getType() == GAME) - { - GuiInfoPopup* s; - bool adding = true; - std::string name = file->getName(); - std::string sysName = mEditingCollection; - if (mIsEditingCustom) - { - SystemData* sysData = mEditingCollectionSystemData->system; - mEditingCollectionSystemData->needsSave = true; - if (!mEditingCollectionSystemData->isPopulated) - { - populateCustomCollection(mEditingCollectionSystemData); - } - std::string key = file->getFullPath(); - FileData* rootFolder = sysData->getRootFolder(); - const std::unordered_map& children = rootFolder->getChildrenByFilename(); - bool found = children.find(key) != children.cend(); - FileFilterIndex* fileIndex = sysData->getIndex(); - std::string name = sysData->getName(); - - SystemData* systemViewToUpdate = getSystemToView(sysData); - - if (found) { - adding = false; - // if we found it, we need to remove it - FileData* collectionEntry = children.at(key); - // remove from index - fileIndex->removeFromIndex(collectionEntry); - // remove from bundle index as well, if needed - if(systemViewToUpdate != sysData) - { - systemViewToUpdate->getIndex()->removeFromIndex(collectionEntry); - } - ViewController::get()->getGameListView(systemViewToUpdate).get()->remove(collectionEntry, false); - } - else - { - // we didn't find it here, we should add it - CollectionFileData* newGame = new CollectionFileData(file, sysData); - rootFolder->addChild(newGame); - fileIndex->addToIndex(newGame); - ViewController::get()->getGameListView(systemViewToUpdate)->onFileChanged(newGame, FILE_METADATA_CHANGED); - rootFolder->sort(getSortTypeFromString(mEditingCollectionSystemData->decl.defaultSort)); - ViewController::get()->onFileChanged(systemViewToUpdate->getRootFolder(), FILE_SORTED); - // add to bundle index as well, if needed - if(systemViewToUpdate != sysData) - { - systemViewToUpdate->getIndex()->addToIndex(newGame); - } - } - updateCollectionFolderMetadata(sysData); - } - else - { - file->getSourceFileData()->getSystem()->getIndex()->removeFromIndex(file); - MetaDataList* md = &file->getSourceFileData()->metadata; - std::string value = md->get("favorite"); - if (value == "false") - { - md->set("favorite", "true"); - } - else - { - adding = false; - md->set("favorite", "false"); - } - file->getSourceFileData()->getSystem()->getIndex()->addToIndex(file); - refreshCollectionSystems(file->getSourceFileData()); - } - if (adding) - { - s = new GuiInfoPopup(mWindow, "Added '" + Utils::String::removeParenthesis(name) + "' to '" + Utils::String::toUpper(sysName) + "'", 4000); - } - else - { - s = new GuiInfoPopup(mWindow, "Removed '" + Utils::String::removeParenthesis(name) + "' from '" + Utils::String::toUpper(sysName) + "'", 4000); - } - mWindow->setInfoPopup(s); - return true; - } - return false; -} - -SystemData* CollectionSystemManager::getSystemToView(SystemData* sys) -{ - SystemData* systemToView = sys; - FileData* rootFolder = sys->getRootFolder(); - - FileData* bundleRootFolder = mCustomCollectionsBundle->getRootFolder(); - const std::unordered_map& bundleChildren = bundleRootFolder->getChildrenByFilename(); - - // is the rootFolder bundled in the "My Collections" system? - bool sysFoundInBundle = bundleChildren.find(rootFolder->getKey()) != bundleChildren.cend(); - - if (sysFoundInBundle && sys->isCollection()) - { - systemToView = mCustomCollectionsBundle; - } - return systemToView; -} - -/* Handles loading a collection system, creating an empty one, and populating on demand */ -// loads Automatic Collection systems (All, Favorites, Last Played) -void CollectionSystemManager::initAutoCollectionSystems() -{ - for(std::map::const_iterator it = mCollectionSystemDeclsIndex.cbegin() ; it != mCollectionSystemDeclsIndex.cend() ; it++ ) - { - CollectionSystemDecl sysDecl = it->second; - if (!sysDecl.isCustom) - { - createNewCollectionEntry(sysDecl.name, sysDecl); - } - } -} - -// this may come in handy if at any point in time in the future we want to -// automatically generate metadata for a folder -void CollectionSystemManager::updateCollectionFolderMetadata(SystemData* sys) -{ - FileData* rootFolder = sys->getRootFolder(); - - std::string desc = "This collection is empty."; - std::string rating = "0"; - std::string players = "1"; - std::string releasedate = "N/A"; - std::string developer = "None"; - std::string genre = "None"; - std::string video = ""; - std::string thumbnail = ""; - std::string image = ""; - - std::unordered_map games = rootFolder->getChildrenByFilename(); - - if(games.size() > 0) - { - std::string games_list = ""; - int games_counter = 0; - for(std::unordered_map::const_iterator iter = games.cbegin(); iter != games.cend(); ++iter) - { - games_counter++; - FileData* file = iter->second; - - std::string new_rating = file->metadata.get("rating"); - std::string new_releasedate = file->metadata.get("releasedate"); - std::string new_developer = file->metadata.get("developer"); - std::string new_genre = file->metadata.get("genre"); - std::string new_players = file->metadata.get("players"); - - rating = (new_rating > rating ? (new_rating != "" ? new_rating : rating) : rating); - players = (new_players > players ? (new_players != "" ? new_players : players) : players); - releasedate = (new_releasedate < releasedate ? (new_releasedate != "" ? new_releasedate : releasedate) : releasedate); - developer = (developer == "None" ? new_developer : (new_developer != developer ? "Various" : new_developer)); - genre = (genre == "None" ? new_genre : (new_genre != genre ? "Various" : new_genre)); - - switch(games_counter) - { - case 2: - case 3: - games_list += ", "; - case 1: - games_list += "'" + file->getName() + "'"; - break; - case 4: - games_list += " among other titles."; - } - } - - desc = "This collection contains " + std::to_string(games_counter) + " games, including " + games_list; - - FileData* randomGame = sys->getRandomGame(); - - video = randomGame->getVideoPath(); - thumbnail = randomGame->getThumbnailPath(); - image = randomGame->getImagePath(); - } - - - rootFolder->metadata.set("desc", desc); - rootFolder->metadata.set("rating", rating); - rootFolder->metadata.set("players", players); - rootFolder->metadata.set("genre", genre); - rootFolder->metadata.set("releasedate", releasedate); - rootFolder->metadata.set("developer", developer); - rootFolder->metadata.set("video", video); - rootFolder->metadata.set("thumbnail", thumbnail); - rootFolder->metadata.set("image", image); -} - -void CollectionSystemManager::initCustomCollectionSystems() -{ - std::vector systems = getCollectionsFromConfigFolder(); - for (auto nameIt = systems.cbegin(); nameIt != systems.cend(); nameIt++) - { - addNewCustomCollection(*nameIt); - } -} - -SystemData* CollectionSystemManager::getAllGamesCollection() -{ - CollectionSystemData* allSysData = &mAutoCollectionSystemsData["all"]; - if (!allSysData->isPopulated) - { - populateAutoCollection(allSysData); - } - return allSysData->system; -} - -SystemData* CollectionSystemManager::addNewCustomCollection(std::string name) -{ - CollectionSystemDecl decl = mCollectionSystemDeclsIndex[myCollectionsName]; - decl.themeFolder = name; - decl.name = name; - decl.longName = name; - return createNewCollectionEntry(name, decl); -} - -// creates a new, empty Collection system, based on the name and declaration -SystemData* CollectionSystemManager::createNewCollectionEntry(std::string name, CollectionSystemDecl sysDecl, bool index) -{ - SystemData* newSys = new SystemData(name, sysDecl.longName, mCollectionEnvData, sysDecl.themeFolder, true); - - CollectionSystemData newCollectionData; - newCollectionData.system = newSys; - newCollectionData.decl = sysDecl; - newCollectionData.isEnabled = false; - newCollectionData.isPopulated = false; - newCollectionData.needsSave = false; - - if (index) - { - if (!sysDecl.isCustom) - { - mAutoCollectionSystemsData[name] = newCollectionData; - } - else - { - mCustomCollectionSystemsData[name] = newCollectionData; - } - } - - return newSys; -} - -// populates an Automatic Collection System -void CollectionSystemManager::populateAutoCollection(CollectionSystemData* sysData) -{ - SystemData* newSys = sysData->system; - CollectionSystemDecl sysDecl = sysData->decl; - FileData* rootFolder = newSys->getRootFolder(); - FileFilterIndex* index = newSys->getIndex(); - for(auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); sysIt++) - { - // we won't iterate all collections - if ((*sysIt)->isGameSystem() && !(*sysIt)->isCollection()) { - std::vector files = (*sysIt)->getRootFolder()->getFilesRecursive(GAME); - for(auto gameIt = files.cbegin(); gameIt != files.cend(); gameIt++) - { - bool include = includeFileInAutoCollections((*gameIt)); - switch(sysDecl.type) { - case AUTO_LAST_PLAYED: - include = include && (*gameIt)->metadata.get("playcount") > "0"; - break; - case AUTO_FAVORITES: - // we may still want to add files we don't want in auto collections in "favorites" - include = (*gameIt)->metadata.get("favorite") == "true"; - break; - } - - if (include) { - CollectionFileData* newGame = new CollectionFileData(*gameIt, newSys); - rootFolder->addChild(newGame); - index->addToIndex(newGame); - } - } - } - } - rootFolder->sort(getSortTypeFromString(sysDecl.defaultSort)); - if (sysDecl.type == AUTO_LAST_PLAYED) - trimCollectionCount(rootFolder, LAST_PLAYED_MAX); - sysData->isPopulated = true; -} - -// populates a Custom Collection System -void CollectionSystemManager::populateCustomCollection(CollectionSystemData* sysData) -{ - SystemData* newSys = sysData->system; - sysData->isPopulated = true; - CollectionSystemDecl sysDecl = sysData->decl; - std::string path = getCustomCollectionConfigPath(newSys->getName()); - - if(!Utils::FileSystem::exists(path)) - { - LOG(LogInfo) << "Couldn't find custom collection config file at " << path; - return; - } - LOG(LogInfo) << "Loading custom collection config file at " << path; - - FileData* rootFolder = newSys->getRootFolder(); - FileFilterIndex* index = newSys->getIndex(); - - // get Configuration for this Custom System - std::ifstream input(path); - - // get all files map - std::unordered_map allFilesMap = getAllGamesCollection()->getRootFolder()->getChildrenByFilename(); - - // iterate list of files in config file - - for(std::string gameKey; getline(input, gameKey); ) - { - std::unordered_map::const_iterator it = allFilesMap.find(gameKey); - if (it != allFilesMap.cend()) { - CollectionFileData* newGame = new CollectionFileData(it->second, newSys); - rootFolder->addChild(newGame); - index->addToIndex(newGame); - } - else - { - LOG(LogInfo) << "Couldn't find game referenced at '" << gameKey << "' for system config '" << path << "'"; - } - } - rootFolder->sort(getSortTypeFromString(sysDecl.defaultSort)); - updateCollectionFolderMetadata(newSys); -} - -/* Handle System View removal and insertion of Collections */ -void CollectionSystemManager::removeCollectionsFromDisplayedSystems() -{ - // remove all Collection Systems - for(auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); ) - { - if ((*sysIt)->isCollection()) - { - sysIt = SystemData::sSystemVector.erase(sysIt); - } - else - { - sysIt++; - } - } - - // remove all custom collections in bundle - // this should not delete the objects from memory! - FileData* customRoot = mCustomCollectionsBundle->getRootFolder(); - std::vector mChildren = customRoot->getChildren(); - for(auto it = mChildren.cbegin(); it != mChildren.cend(); it++) - { - customRoot->removeChild(*it); - } - // clear index - mCustomCollectionsBundle->getIndex()->resetIndex(); - // remove view so it's re-created as needed - ViewController::get()->removeGameListView(mCustomCollectionsBundle); -} - -void CollectionSystemManager::addEnabledCollectionsToDisplayedSystems(std::map* colSystemData) -{ - // add auto enabled ones - for(std::map::iterator it = colSystemData->begin() ; it != colSystemData->end() ; it++ ) - { - if(it->second.isEnabled) - { - // check if populated, otherwise populate - if (!it->second.isPopulated) - { - if(it->second.decl.isCustom) - { - populateCustomCollection(&(it->second)); - } - else - { - populateAutoCollection(&(it->second)); - } - } - // check if it has its own view - if(!it->second.decl.isCustom || themeFolderExists(it->first) || !Settings::getInstance()->getBool("UseCustomCollectionsSystem")) - { - // exists theme folder, or we chose not to bundle it under the custom-collections system - // so we need to create a view - SystemData::sSystemVector.push_back(it->second.system); - } - else - { - FileData* newSysRootFolder = it->second.system->getRootFolder(); - mCustomCollectionsBundle->getRootFolder()->addChild(newSysRootFolder); - mCustomCollectionsBundle->getIndex()->importIndex(it->second.system->getIndex()); - } - } - } -} - -/* Auxiliary methods to get available custom collection possibilities */ -std::vector CollectionSystemManager::getSystemsFromConfig() -{ - std::vector systems; - std::string path = SystemData::getConfigPath(false); - - if(!Utils::FileSystem::exists(path)) - { - return systems; - } - - pugi::xml_document doc; - pugi::xml_parse_result res = doc.load_file(path.c_str()); - - if(!res) - { - return systems; - } - - //actually read the file - pugi::xml_node systemList = doc.child("systemList"); - - if(!systemList) - { - return systems; - } - - for(pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) - { - // theme folder - std::string themeFolder = system.child("theme").text().get(); - systems.push_back(themeFolder); - } - std::sort(systems.begin(), systems.end()); - return systems; -} - -// gets all folders from the current theme path -std::vector CollectionSystemManager::getSystemsFromTheme() -{ - std::vector systems; - - auto themeSets = ThemeData::getThemeSets(); - if(themeSets.empty()) - { - // no theme sets available - return systems; - } - - std::map::const_iterator set = themeSets.find(Settings::getInstance()->getString("ThemeSet")); - if(set == themeSets.cend()) - { - // currently selected theme set is missing, so just pick the first available set - set = themeSets.cbegin(); - Settings::getInstance()->setString("ThemeSet", set->first); - } - - std::string themePath = set->second.path; - - if (Utils::FileSystem::exists(themePath)) - { - Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(themePath); - - for (Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) - { - if (Utils::FileSystem::isDirectory(*it)) - { - //... here you have a directory - std::string folder = *it; - folder = folder.substr(themePath.size()+1); - - if(Utils::FileSystem::exists(set->second.getThemePath(folder))) - { - systems.push_back(folder); - } - } - } - } - std::sort(systems.begin(), systems.end()); - return systems; -} - -// returns the unused folders from current theme path -std::vector CollectionSystemManager::getUnusedSystemsFromTheme() -{ - // get used systems in es_systems.cfg - std::vector systemsInUse = getSystemsFromConfig(); - // get available folders in theme - std::vector themeSys = getSystemsFromTheme(); - // get folders assigned to custom collections - std::vector autoSys = getCollectionThemeFolders(false); - // get folder assigned to custom collections - std::vector customSys = getCollectionThemeFolders(true); - // get folders assigned to user collections - std::vector userSys = getUserCollectionThemeFolders(); - // add them all to the list of systems in use - systemsInUse.insert(systemsInUse.cend(), autoSys.cbegin(), autoSys.cend()); - systemsInUse.insert(systemsInUse.cend(), customSys.cbegin(), customSys.cend()); - systemsInUse.insert(systemsInUse.cend(), userSys.cbegin(), userSys.cend()); - - for(auto sysIt = themeSys.cbegin(); sysIt != themeSys.cend(); ) - { - if (std::find(systemsInUse.cbegin(), systemsInUse.cend(), *sysIt) != systemsInUse.cend()) - { - sysIt = themeSys.erase(sysIt); - } - else - { - sysIt++; - } - } - return themeSys; -} - -// returns which collection config files exist in the user folder -std::vector CollectionSystemManager::getCollectionsFromConfigFolder() -{ - std::vector systems; - std::string configPath = getCollectionsFolder(); - - if (Utils::FileSystem::exists(configPath)) - { - Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(configPath); - for (Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) - { - if (Utils::FileSystem::isRegularFile(*it)) - { - // it's a file - std::string filename = Utils::FileSystem::getFileName(*it); - - // need to confirm filename matches config format - if (filename != "custom-.cfg" && Utils::String::startsWith(filename, "custom-") && Utils::String::endsWith(filename, ".cfg")) - { - filename = filename.substr(7, filename.size()-11); - systems.push_back(filename); - } - else - { - LOG(LogInfo) << "Found non-collection config file in collections folder: " << filename; - } - } - } - } - return systems; -} - -// returns the theme folders for Automatic Collections (All, Favorites, Last Played) or generic Custom Collections folder -std::vector CollectionSystemManager::getCollectionThemeFolders(bool custom) -{ - std::vector systems; - for(std::map::const_iterator it = mCollectionSystemDeclsIndex.cbegin() ; it != mCollectionSystemDeclsIndex.cend() ; it++ ) - { - CollectionSystemDecl sysDecl = it->second; - if (sysDecl.isCustom == custom) - { - systems.push_back(sysDecl.themeFolder); - } - } - return systems; -} - -// returns the theme folders in use for the user-defined Custom Collections -std::vector CollectionSystemManager::getUserCollectionThemeFolders() -{ - std::vector systems; - for(std::map::const_iterator it = mCustomCollectionSystemsData.cbegin() ; it != mCustomCollectionSystemsData.cend() ; it++ ) - { - systems.push_back(it->second.decl.themeFolder); - } - return systems; -} - -// returns whether a specific folder exists in the theme -bool CollectionSystemManager::themeFolderExists(std::string folder) -{ - std::vector themeSys = getSystemsFromTheme(); - return std::find(themeSys.cbegin(), themeSys.cend(), folder) != themeSys.cend(); -} - -bool CollectionSystemManager::includeFileInAutoCollections(FileData* file) -{ - // we exclude non-game files from collections (i.e. "kodi", entries from non-game systems) - // if/when there are more in the future, maybe this can be a more complex method, with a proper list - // but for now a simple string comparison is more performant - return file->getName() != "kodi" && file->getSystem()->isGameSystem(); -} - -std::string getCustomCollectionConfigPath(std::string collectionName) -{ - return getCollectionsFolder() + "/custom-" + collectionName + ".cfg"; -} - -std::string getCollectionsFolder() -{ - return Utils::FileSystem::getGenericPath(Utils::FileSystem::getHomePath() + "/.emulationstation/collections"); -} - -bool systemSort(SystemData* sys1, SystemData* sys2) -{ - std::string name1 = Utils::String::toUpper(sys1->getName()); - std::string name2 = Utils::String::toUpper(sys2->getName()); - return name1.compare(name2) < 0; -} +#include "CollectionSystemManager.h" + +#include "guis/GuiInfoPopup.h" +#include "utils/FileSystemUtil.h" +#include "utils/StringUtil.h" +#include "views/gamelist/IGameListView.h" +#include "views/ViewController.h" +#include "FileData.h" +#include "FileFilterIndex.h" +#include "Log.h" +#include "Settings.h" +#include "SystemData.h" +#include "ThemeData.h" +#include +#include +#include "Gamelist.h" +#include "FileSorts.h" + +std::string myCollectionsName = "collections"; + +#define LAST_PLAYED_MAX 50 + +/* Handling the getting, initialization, deinitialization, saving and deletion of + * a CollectionSystemManager Instance */ +CollectionSystemManager* CollectionSystemManager::sInstance = NULL; + +std::vector CollectionSystemManager::getSystemDecls() +{ + CollectionSystemDecl systemDecls[] = { + //type name long name //default sort // theme folder // isCustom + { AUTO_ALL_GAMES, "all", "all games", "filename, ascending", "auto-allgames", false, true }, + { AUTO_LAST_PLAYED, "recent", "last played", "last played, descending", "auto-lastplayed", false, true }, + { AUTO_FAVORITES, "favorites", "favorites", "filename, ascending", "auto-favorites", false, true }, + { AUTO_AT2PLAYERS, "2players", "2 players", "filename, ascending", "auto-at2players", false, true }, + { AUTO_AT4PLAYERS, "4players", "4 players", "filename, ascending", "auto-at4players", false, true }, + { AUTO_NEVER_PLAYED, "neverplayed", "never played", "filename, ascending", "auto-neverplayed", false, true }, + + // Arcade meta + { AUTO_ARCADE, "arcade", "arcade", "filename, ascending", "arcade", false, true }, + + // Arcade systems + { CPS1_COLLECTION, "zcps1", "cps1", "filename, ascending", "cps1", false, false }, + { CPS2_COLLECTION, "zcps2", "cps2", "filename, ascending", "cps2", false, false }, + { CPS3_COLLECTION, "zcps3", "cps3", "filename, ascending", "cps3", false, false }, + { CAVE_COLLECTION, "zcave", "cave", "filename, ascending", "cave", false, false }, + { NEOGEO_COLLECTION, "zneogeo", "neogeo", "filename, ascending", "neogeo", false, false }, + { SEGA_COLLECTION, "zsega", "sega", "filename, ascending", "sega", false, false }, + { IREM_COLLECTION, "zirem", "irem", "filename, ascending", "irem", false, false }, + { MIDWAY_COLLECTION, "zmidway", "midway", "filename, ascending", "midway", false, false }, + { CAPCOM_COLLECTION, "zcapcom", "capcom", "filename, ascending", "capcom", false, false }, + { TECMO_COLLECTION, "ztecmo", "tecmo", "filename, ascending", "tecmo", false, false }, + { SNK_COLLECTION, "zsnk", "snk", "filename, ascending", "snk", false, false }, + { NAMCO_COLLECTION, "znamco", "namco", "filename, ascending", "namco", false, false }, + { TAITO_COLLECTION, "ztaito", "taito", "filename, ascending", "taito", false, false }, + { KONAMI_COLLECTION, "zkonami", "konami", "filename, ascending", "konami", false, false }, + { JALECO_COLLECTION, "zjaleco", "jaleco", "filename, ascending", "jaleco", false, false }, + { ATARI_COLLECTION, "zatari", "atari", "filename, ascending", "atari", false, false }, + { NINTENDO_COLLECTION, "znintendo", "nintendo", "filename, ascending", "nintendo", false, false }, + { SAMMY_COLLECTION, "zsammy", "sammy", "filename, ascending", "sammy", false, false }, + { ACCLAIM_COLLECTION, "zacclaim", "acclaim", "filename, ascending", "acclaim", false, false }, + { PSIKYO_COLLECTION, "zpsiko", "psiko", "filename, ascending", "psiko", false, false }, + { KANEKO_COLLECTION, "zkaneko", "kaneko", "filename, ascending", "kaneko", false, false }, + { COLECO_COLLECTION, "zcoleco", "coleco", "filename, ascending", "coleco", false, false }, + { ATLUS_COLLECTION, "zatlus", "atlus", "filename, ascending", "atlus", false, false }, + { BANPRESTO_COLLECTION, "zbanpresto", "banpresto", "filename, ascending", "banpresto", false, false }, + + { CUSTOM_COLLECTION, myCollectionsName, "collections", "filename, ascending", "custom-collections", true, true } + }; + + return std::vector(systemDecls, systemDecls + sizeof(systemDecls) / sizeof(systemDecls[0])); +} + +CollectionSystemManager::CollectionSystemManager(Window* window) : mWindow(window) +{ + // create a map + std::vector tempSystemDecl = getSystemDecls(); + + for (std::vector::const_iterator it = tempSystemDecl.cbegin(); it != tempSystemDecl.cend(); ++it ) + mCollectionSystemDeclsIndex[(*it).name] = (*it); + + // creating standard environment data + mCollectionEnvData = new SystemEnvironmentData; + mCollectionEnvData->mStartPath = ""; + mCollectionEnvData->mLaunchCommand = ""; + std::vector allPlatformIds; + allPlatformIds.push_back(PlatformIds::PLATFORM_IGNORE); + mCollectionEnvData->mPlatformIds = allPlatformIds; + + std::string path = getCollectionsFolder(); + if(!Utils::FileSystem::exists(path)) + Utils::FileSystem::createDirectory(path); + + mIsEditingCustom = false; + mEditingCollection = "Favorites"; + mEditingCollectionSystemData = NULL; + mCustomCollectionsBundle = NULL; +} + +CollectionSystemManager::~CollectionSystemManager() +{ + assert(sInstance == this); + removeCollectionsFromDisplayedSystems(); + + // iterate the map + for(std::map::const_iterator it = mCustomCollectionSystemsData.cbegin() ; it != mCustomCollectionSystemsData.cend() ; it++ ) + { + if (it->second.isPopulated) + { + saveCustomCollection(it->second.system); + } + delete it->second.system; + } + sInstance = NULL; +} + +CollectionSystemManager* CollectionSystemManager::get() +{ + assert(sInstance); + return sInstance; +} + +void CollectionSystemManager::init(Window* window) +{ + assert(!sInstance); + sInstance = new CollectionSystemManager(window); +} + +void CollectionSystemManager::deinit() +{ + if (sInstance) + { + delete sInstance; + } +} + +void CollectionSystemManager::saveCustomCollection(SystemData* sys) +{ + std::string name = sys->getName(); + auto games = sys->getRootFolder()->getChildren(); + + bool found = mCustomCollectionSystemsData.find(name) != mCustomCollectionSystemsData.cend(); + if (found) + { + CollectionSystemData sysData = mCustomCollectionSystemsData.at(name); + if (sysData.needsSave) + { + auto home = Utils::FileSystem::getHomePath(); + + std::ofstream configFile; + configFile.open(getCustomCollectionConfigPath(name)); + for(auto iter = games.cbegin(); iter != games.cend(); ++iter) + { + std::string path = (*iter)->getKey(); + + path = Utils::FileSystem::createRelativePath(path, "portnawak", true); + + configFile << path << std::endl; + } + configFile.close(); + } + } + else + { + LOG(LogError) << "Couldn't find collection to save! " << name; + } +} + +/* Methods to load all Collections into memory, and handle enabling the active ones */ +// loads all Collection Systems +void CollectionSystemManager::loadCollectionSystems(bool async) +{ + initAutoCollectionSystems(); + CollectionSystemDecl decl = mCollectionSystemDeclsIndex[myCollectionsName]; + mCustomCollectionsBundle = createNewCollectionEntry(decl.name, decl, false); + // we will also load custom systems here + initCustomCollectionSystems(); + if(Settings::getInstance()->getString("CollectionSystemsAuto") != "" || Settings::getInstance()->getString("CollectionSystemsCustom") != "") + { + // Now see which ones are enabled + loadEnabledListFromSettings(); + + + // add to the main System Vector, and create Views as needed + if (!async) + updateSystemsList(); + } +} + +// loads settings +void CollectionSystemManager::loadEnabledListFromSettings() +{ + // we parse the auto collection settings list + std::vector autoSelected = Utils::String::commaStringToVector(Settings::getInstance()->getString("CollectionSystemsAuto")); + + // iterate the map + for(std::map::iterator it = mAutoCollectionSystemsData.begin() ; it != mAutoCollectionSystemsData.end() ; it++ ) + { + it->second.isEnabled = (std::find(autoSelected.cbegin(), autoSelected.cend(), it->first) != autoSelected.cend()); + } + + // we parse the custom collection settings list + std::vector customSelected = Utils::String::commaStringToVector(Settings::getInstance()->getString("CollectionSystemsCustom")); + + // iterate the map + for(std::map::iterator it = mCustomCollectionSystemsData.begin() ; it != mCustomCollectionSystemsData.end() ; it++ ) + { + it->second.isEnabled = (std::find(customSelected.cbegin(), customSelected.cend(), it->first) != customSelected.cend()); + } +} + +// updates enabled system list in System View +void CollectionSystemManager::updateSystemsList() +{ + // remove all Collection Systems + removeCollectionsFromDisplayedSystems(); + + std::unordered_map map; + getAllGamesCollection()->getRootFolder()->createChildrenByFilenameMap(map); + + // add custom enabled ones + addEnabledCollectionsToDisplayedSystems(&mCustomCollectionSystemsData, &map); + + if (Settings::getInstance()->getBool("SortAllSystems")) + { + // sort custom individual systems with other systems + std::sort(SystemData::sSystemVector.begin(), SystemData::sSystemVector.end(), systemSort); + + // move RetroPie system to end, before auto collections + for(auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); ) + { + if ((*sysIt)->getName() == "retropie") + { + SystemData* retroPieSystem = (*sysIt); + sysIt = SystemData::sSystemVector.erase(sysIt); + SystemData::sSystemVector.push_back(retroPieSystem); + break; + } + else + { + sysIt++; + } + } + } + + if(mCustomCollectionsBundle->getRootFolder()->getChildren().size() > 0) + SystemData::sSystemVector.push_back(mCustomCollectionsBundle); + + // add auto enabled ones + addEnabledCollectionsToDisplayedSystems(&mAutoCollectionSystemsData, &map); + /* + // create views for collections, before reload + for(auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); sysIt++) + { + if ((*sysIt)->isCollection()) + { + ViewController::get()->getGameListView((*sysIt)); + } + }*/ + + // if we were editing a custom collection, and it's no longer enabled, exit edit mode + if(mIsEditingCustom && !mEditingCollectionSystemData->isEnabled) + { + exitEditMode(); + } +} + +/* Methods to manage collection files related to a source FileData */ +// updates all collection files related to the source file +void CollectionSystemManager::refreshCollectionSystems(FileData* file) +{ + if (!file->getSystem()->isGameSystem() || file->getType() != GAME) + return; + + std::map allCollections; + allCollections.insert(mAutoCollectionSystemsData.cbegin(), mAutoCollectionSystemsData.cend()); + allCollections.insert(mCustomCollectionSystemsData.cbegin(), mCustomCollectionSystemsData.cend()); + + for(auto sysDataIt = allCollections.cbegin(); sysDataIt != allCollections.cend(); sysDataIt++) + { + updateCollectionSystem(file, sysDataIt->second); + } +} + +void CollectionSystemManager::updateCollectionSystem(FileData* file, CollectionSystemData sysData) +{ + if (sysData.isPopulated) + { + // collection files use the full path as key, to avoid clashes + std::string key = file->getFullPath(); + + SystemData* curSys = sysData.system; + FileData* collectionEntry = curSys->getRootFolder()->FindByPath(key); + + FolderData* rootFolder = curSys->getRootFolder(); + + std::string name = curSys->getName(); + + if (collectionEntry != nullptr) + { + // if we found it, we need to update it + // remove from index, so we can re-index metadata after refreshing + curSys->removeFromIndex(collectionEntry); + collectionEntry->refreshMetadata(); + // found and we are removing + if (name == "favorites" && file->getMetadata().get("favorite") == "false") { + // need to check if still marked as favorite, if not remove + ViewController::get()->getGameListView(curSys).get()->remove(collectionEntry, false); + + ViewController::get()->onFileChanged(file, FILE_METADATA_CHANGED); + ViewController::get()->getGameListView(curSys)->onFileChanged(collectionEntry, FILE_METADATA_CHANGED); + } + else + { + // re-index with new metadata + curSys->addToIndex(collectionEntry); + ViewController::get()->onFileChanged(collectionEntry, FILE_METADATA_CHANGED); + } + } + else + { + // we didn't find it here - we need to check if we should add it + if (name == "recent" && file->getMetadata().get("playcount") > "0" && includeFileInAutoCollections(file) || + name == "favorites" && file->getMetadata().get("favorite") == "true") { + CollectionFileData* newGame = new CollectionFileData(file, curSys); + rootFolder->addChild(newGame); + curSys->addToIndex(newGame); + + ViewController::get()->onFileChanged(file, FILE_METADATA_CHANGED); + ViewController::get()->getGameListView(curSys)->onFileChanged(newGame, FILE_METADATA_CHANGED); + } + } + + curSys->updateDisplayedGameCount(); + + if (name == "recent") + { + sortLastPlayed(curSys); + trimCollectionCount(rootFolder, LAST_PLAYED_MAX); + ViewController::get()->onFileChanged(rootFolder, FILE_METADATA_CHANGED); + } + else + ViewController::get()->onFileChanged(rootFolder, FILE_SORTED); + } +} + +void CollectionSystemManager::sortLastPlayed(SystemData* system) +{ + if (system->getName() != "recent") + return; + + FolderData* rootFolder = system->getRootFolder(); + system->setSortId(FileSorts::LASTPLAYED_DESCENDING); + + const FileSorts::SortType& sort = FileSorts::getSortTypes().at(system->getSortId()); + + std::vector& childs = (std::vector&) rootFolder->getChildren(); + std::sort(childs.begin(), childs.end(), sort.comparisonFunction); + if (!sort.ascending) + std::reverse(childs.begin(), childs.end()); +} + +void CollectionSystemManager::trimCollectionCount(FolderData* rootFolder, int limit) +{ + SystemData* curSys = rootFolder->getSystem(); + std::shared_ptr listView = ViewController::get()->getGameListView(curSys, false); + + auto& childs = rootFolder->getChildren(); + while ((int)childs.size() > limit) + { + CollectionFileData* gameToRemove = (CollectionFileData*)childs.back(); + if (listView == nullptr) + delete gameToRemove; + else + listView.get()->remove(gameToRemove, false); + } +} + +// deletes all collection files from collection systems related to the source file +void CollectionSystemManager::deleteCollectionFiles(FileData* file) +{ + // collection files use the full path as key, to avoid clashes + std::string key = file->getFullPath(); + // find games in collection systems + std::map allCollections; + allCollections.insert(mAutoCollectionSystemsData.cbegin(), mAutoCollectionSystemsData.cend()); + allCollections.insert(mCustomCollectionSystemsData.cbegin(), mCustomCollectionSystemsData.cend()); + + for(auto sysDataIt = allCollections.begin(); sysDataIt != allCollections.end(); sysDataIt++) + { + if (sysDataIt->second.isPopulated) + { + FileData* collectionEntry = (sysDataIt->second.system)->getRootFolder()->FindByPath(key); + if (collectionEntry != nullptr) + { + sysDataIt->second.needsSave = true; + SystemData* systemViewToUpdate = getSystemToView(sysDataIt->second.system); + ViewController::get()->getGameListView(systemViewToUpdate).get()->remove(collectionEntry, false); + } + } + } +} + +// returns whether the current theme is compatible with Automatic or Custom Collections +bool CollectionSystemManager::isThemeGenericCollectionCompatible(bool genericCustomCollections) +{ + std::vector cfgSys = getCollectionThemeFolders(genericCustomCollections); + for(auto sysIt = cfgSys.cbegin(); sysIt != cfgSys.cend(); sysIt++) + { + if(!themeFolderExists(*sysIt)) + return false; + } + return true; +} + +bool CollectionSystemManager::isThemeCustomCollectionCompatible(std::vector stringVector) +{ + if (isThemeGenericCollectionCompatible(true)) + return true; + + // get theme path + auto themeSets = ThemeData::getThemeSets(); + auto set = themeSets.find(Settings::getInstance()->getString("ThemeSet")); + if(set != themeSets.cend()) + { + std::string defaultThemeFilePath = set->second.path + "/theme.xml"; + if (Utils::FileSystem::exists(defaultThemeFilePath)) + { + return true; + } + } + + for(auto sysIt = stringVector.cbegin(); sysIt != stringVector.cend(); sysIt++) + { + if(!themeFolderExists(*sysIt)) + return false; + } + return true; +} + +std::string CollectionSystemManager::getValidNewCollectionName(std::string inName, int index) +{ + std::string name = inName; + + if(index == 0) + { + size_t remove = std::string::npos; + + // get valid name + while((remove = name.find_first_not_of("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-[]() ")) != std::string::npos) + { + name.erase(remove, 1); + } + } + else + { + name += " (" + std::to_string(index) + ")"; + } + + if(name == "") + { + name = "New Collection"; + } + + if(name != inName) + { + LOG(LogInfo) << "Had to change name, from: " << inName << " to: " << name; + } + + // get used systems in es_systems.cfg + std::vector systemsInUse = getSystemsFromConfig(); + // get folders assigned to custom collections + std::vector autoSys = getCollectionThemeFolders(false); + // get folder assigned to custom collections + std::vector customSys = getCollectionThemeFolders(true); + // get folders assigned to user collections + std::vector userSys = getUserCollectionThemeFolders(); + // add them all to the list of systems in use + systemsInUse.insert(systemsInUse.cend(), autoSys.cbegin(), autoSys.cend()); + systemsInUse.insert(systemsInUse.cend(), customSys.cbegin(), customSys.cend()); + systemsInUse.insert(systemsInUse.cend(), userSys.cbegin(), userSys.cend()); + for(auto sysIt = systemsInUse.cbegin(); sysIt != systemsInUse.cend(); sysIt++) + { + if (*sysIt == name) + { + if(index > 0) { + name = name.substr(0, name.size()-4); + } + return getValidNewCollectionName(name, index+1); + } + } + // if it matches one of the custom collections reserved names + if (mCollectionSystemDeclsIndex.find(name) != mCollectionSystemDeclsIndex.cend()) + return getValidNewCollectionName(name, index+1); + return name; +} + +void CollectionSystemManager::setEditMode(std::string collectionName) +{ + if (mCustomCollectionSystemsData.find(collectionName) == mCustomCollectionSystemsData.cend()) + { + LOG(LogError) << "Tried to edit a non-existing collection: " << collectionName; + return; + } + mIsEditingCustom = true; + mEditingCollection = collectionName; + + CollectionSystemData* sysData = &(mCustomCollectionSystemsData.at(mEditingCollection)); + if (!sysData->isPopulated) + populateCustomCollection(sysData); + + // if it's bundled, this needs to be the bundle system + mEditingCollectionSystemData = sysData; + + char strbuf[512]; + snprintf(strbuf, 512, _("Editing the '%s' Collection. Add/remove games with Y.").c_str(), Utils::String::toUpper(collectionName).c_str()); + mWindow->displayNotificationMessage(strbuf, 10000); +} + +void CollectionSystemManager::exitEditMode() +{ + char strbuf[512]; + snprintf(strbuf, 512, _("Finished editing the '%s' Collection.").c_str(), mEditingCollection.c_str()); + mWindow->displayNotificationMessage(strbuf, 10000); + mIsEditingCustom = false; + mEditingCollection = "Favorites"; +} + +// adds or removes a game from a specific collection +bool CollectionSystemManager::toggleGameInCollection(FileData* file) +{ + if (file->getType() == GAME) + { + GuiInfoPopup* s; + bool adding = true; + std::string name = file->getName(); + std::string sysName = mEditingCollection; + if (mIsEditingCustom) + { + SystemData* sysData = mEditingCollectionSystemData->system; + mEditingCollectionSystemData->needsSave = true; + if (!mEditingCollectionSystemData->isPopulated) + populateCustomCollection(mEditingCollectionSystemData); + + std::string key = file->getFullPath(); + FolderData* rootFolder = sysData->getRootFolder(); + + FileData* collectionEntry = rootFolder->FindByPath(key); + + std::string name = sysData->getName(); + + SystemData* systemViewToUpdate = getSystemToView(sysData); + + if (collectionEntry != nullptr) { + adding = false; + // if we found it, we need to remove it + // remove from index + sysData->removeFromIndex(collectionEntry); + // remove from bundle index as well, if needed + if (systemViewToUpdate != sysData) + systemViewToUpdate->removeFromIndex(collectionEntry); + + ViewController::get()->getGameListView(systemViewToUpdate).get()->remove(collectionEntry, false); + } + else + { + // we didn't find it here, we should add it + CollectionFileData* newGame = new CollectionFileData(file, sysData); + rootFolder->addChild(newGame); + sysData->addToIndex(newGame); + ViewController::get()->getGameListView(systemViewToUpdate)->onFileChanged(newGame, FILE_METADATA_CHANGED); + ViewController::get()->onFileChanged(systemViewToUpdate->getRootFolder(), FILE_SORTED); + // add to bundle index as well, if needed + if(systemViewToUpdate != sysData) + { + systemViewToUpdate->addToIndex(newGame); + } + } + updateCollectionFolderMetadata(sysData); + } + else + { + SystemData* sysData = file->getSourceFileData()->getSystem(); + sysData->removeFromIndex(file); + + MetaDataList* md = &file->getSourceFileData()->getMetadata(); + + std::string value = md->get("favorite"); + if (value == "false") + md->set("favorite", "true"); + else + { + adding = false; + md->set("favorite", "false"); + } + sysData->addToIndex(file); + saveToGamelistRecovery(file); + + refreshCollectionSystems(file->getSourceFileData()); + + SystemData* systemViewToUpdate = getSystemToView(sysData); + if (systemViewToUpdate != NULL) + { + ViewController::get()->onFileChanged(file, FILE_METADATA_CHANGED); + ViewController::get()->getGameListView(systemViewToUpdate)->onFileChanged(file, FILE_METADATA_CHANGED); + } + + + } + + char trstring[512]; + + if (adding) + snprintf(trstring, 512, _("Added '%s' to '%s'").c_str(), Utils::String::removeParenthesis(name).c_str(), Utils::String::toUpper(sysName).c_str()); // batocera + else + snprintf(trstring, 512, _("Removed '%s' from '%s'").c_str(), Utils::String::removeParenthesis(name).c_str(), Utils::String::toUpper(sysName).c_str()); // batocera + + mWindow->displayNotificationMessage(trstring, 4000); + + return true; + } + return false; +} + +SystemData* CollectionSystemManager::getSystemToView(SystemData* sys) +{ + SystemData* systemToView = sys; + FileData* rootFolder = sys->getRootFolder(); + + FolderData* bundleRootFolder = mCustomCollectionsBundle->getRootFolder(); + + // is the rootFolder bundled in the "My Collections" system? + bool sysFoundInBundle = bundleRootFolder->FindByPath(rootFolder->getKey()) != nullptr; + if (sysFoundInBundle && sys->isCollection()) + { + systemToView = mCustomCollectionsBundle; + } + return systemToView; +} + +/* Handles loading a collection system, creating an empty one, and populating on demand */ +// loads Automatic Collection systems (All, Favorites, Last Played) +void CollectionSystemManager::initAutoCollectionSystems() +{ + for(std::map::const_iterator it = mCollectionSystemDeclsIndex.cbegin() ; it != mCollectionSystemDeclsIndex.cend() ; it++ ) + { + CollectionSystemDecl sysDecl = it->second; + if (!sysDecl.isCustom) + { + createNewCollectionEntry(sysDecl.name, sysDecl); + } + } +} + +// this may come in handy if at any point in time in the future we want to +// automatically generate metadata for a folder +void CollectionSystemManager::updateCollectionFolderMetadata(SystemData* sys) +{ + FolderData* rootFolder = sys->getRootFolder(); + + std::string desc = _("This collection is empty."); + std::string rating = "0"; + std::string players = "1"; + std::string releasedate = "N/A"; + std::string developer = _("None"); + std::string genre = _("None"); + std::string video = ""; + std::string thumbnail = ""; + std::string image = ""; + + auto games = rootFolder->getChildren(); + + if(games.size() > 0) + { + std::string games_list = ""; + int games_counter = 0; + for(auto iter = games.cbegin(); iter != games.cend(); ++iter) + { + games_counter++; + FileData* file = *iter; + + std::string new_rating = file->getMetadata().get("rating"); + std::string new_releasedate = file->getMetadata().get("releasedate"); + std::string new_developer = file->getMetadata().get("developer"); + std::string new_genre = file->getMetadata().get("genre"); + std::string new_players = file->getMetadata().get("players"); + + rating = (new_rating > rating ? (new_rating != "" ? new_rating : rating) : rating); + players = (new_players > players ? (new_players != "" ? new_players : players) : players); + releasedate = (new_releasedate < releasedate ? (new_releasedate != "" ? new_releasedate : releasedate) : releasedate); + developer = (developer == _("None") ? new_developer : (new_developer != developer ? _("Various") : new_developer)); + genre = (genre == _("None") ? new_genre : (new_genre != genre ? _("Various") : new_genre)); + + switch(games_counter) + { + case 2: + case 3: + games_list += ", "; + case 1: + games_list += "'" + file->getName() + "'"; + break; + case 4: + games_list += " " + _("among other titles."); + } + } + + desc = _("This collection contains") + " " + std::to_string(games_counter) + " " + _("games, including") + " " + games_list; + + FileData* randomGame = sys->getRandomGame(); + if (randomGame != nullptr) + { + video = randomGame->getVideoPath(); + thumbnail = randomGame->getThumbnailPath(); + image = randomGame->getImagePath(); + } + } + + + rootFolder->getMetadata().set("desc", desc); + rootFolder->getMetadata().set("rating", rating); + rootFolder->getMetadata().set("players", players); + rootFolder->getMetadata().set("genre", genre); + rootFolder->getMetadata().set("releasedate", releasedate); + rootFolder->getMetadata().set("developer", developer); + rootFolder->getMetadata().set("video", video); + rootFolder->getMetadata().set("thumbnail", thumbnail); + rootFolder->getMetadata().set("image", image); +} + +void CollectionSystemManager::initCustomCollectionSystems() +{ + std::vector systems = getCollectionsFromConfigFolder(); + for (auto nameIt = systems.cbegin(); nameIt != systems.cend(); nameIt++) + { + addNewCustomCollection(*nameIt); + } +} + +SystemData* CollectionSystemManager::getAllGamesCollection() +{ + CollectionSystemData* allSysData = &mAutoCollectionSystemsData["all"]; + if (!allSysData->isPopulated) + { + populateAutoCollection(allSysData); + } + return allSysData->system; +} + +SystemData* CollectionSystemManager::addNewCustomCollection(std::string name) +{ + CollectionSystemDecl decl = mCollectionSystemDeclsIndex[myCollectionsName]; + decl.themeFolder = name; + decl.name = name; + decl.longName = name; + return createNewCollectionEntry(name, decl); +} + +// creates a new, empty Collection system, based on the name and declaration +SystemData* CollectionSystemManager::createNewCollectionEntry(std::string name, CollectionSystemDecl sysDecl, bool index) +{ + SystemData* newSys = new SystemData(name, sysDecl.longName, mCollectionEnvData, sysDecl.themeFolder, true); + + CollectionSystemData newCollectionData; + newCollectionData.system = newSys; + newCollectionData.decl = sysDecl; + newCollectionData.isEnabled = false; + newCollectionData.isPopulated = false; + newCollectionData.needsSave = false; + + if (index) + { + if (!sysDecl.isCustom) + { + mAutoCollectionSystemsData[name] = newCollectionData; + } + else + { + mCustomCollectionSystemsData[name] = newCollectionData; + } + } + + return newSys; +} + +// populates an Automatic Collection System +void CollectionSystemManager::populateAutoCollection(CollectionSystemData* sysData) +{ + SystemData* newSys = sysData->system; + CollectionSystemDecl sysDecl = sysData->decl; + FolderData* rootFolder = newSys->getRootFolder(); + + for(auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); sysIt++) + { + std::vector platforms = (*sysIt)->getPlatformIds(); + bool isArcade = std::find(platforms.begin(), platforms.end(), PlatformIds::ARCADE) != platforms.end(); + + // we won't iterate all collections + if ((*sysIt)->isGameSystem() && !(*sysIt)->isCollection()) + { + std::vector files = (*sysIt)->getRootFolder()->getFilesRecursive(GAME); + for(auto gameIt = files.cbegin(); gameIt != files.cend(); gameIt++) + { + std::string systemarcadename; + + bool include = includeFileInAutoCollections((*gameIt)); + switch(sysDecl.type) + { + case AUTO_LAST_PLAYED: + include = include && (*gameIt)->getMetadata().get("playcount") > "0"; + break; + case AUTO_NEVER_PLAYED: + include = include && !((*gameIt)->getMetadata("playcount") > "0"); + break; + case AUTO_FAVORITES: + // we may still want to add files we don't want in auto collections in "favorites" + include = (*gameIt)->getMetadata().get("favorite") == "true"; + break; + case AUTO_ARCADE: + include = include && isArcade; + break; + case CPS1_COLLECTION: + systemarcadename = "cps1"; + break; + case CPS2_COLLECTION: + systemarcadename = "cps2"; + break; + case CPS3_COLLECTION: + systemarcadename = "cps3"; + break; + case CAVE_COLLECTION: + systemarcadename = "cave"; + break; + case NEOGEO_COLLECTION: + systemarcadename = "neogeo"; + break; + case SEGA_COLLECTION: + systemarcadename = "sega"; + break; + case IREM_COLLECTION: + systemarcadename = "irem"; + break; + case MIDWAY_COLLECTION: + systemarcadename = "midway"; + break; + case CAPCOM_COLLECTION: + systemarcadename = "capcom"; + break; + case TECMO_COLLECTION: + systemarcadename = "techmo"; + break; + case SNK_COLLECTION: + systemarcadename = "snk"; + break; + case NAMCO_COLLECTION: + systemarcadename = "namco"; + break; + case TAITO_COLLECTION: + systemarcadename = "taito"; + break; + case KONAMI_COLLECTION: + systemarcadename = "konami"; + break; + case JALECO_COLLECTION: + systemarcadename = "jaleco"; + break; + case ATARI_COLLECTION: + systemarcadename = "atari"; + break; + case NINTENDO_COLLECTION: + systemarcadename = "nintendo"; + break; + case SAMMY_COLLECTION: + systemarcadename = "sammy"; + break; + case ACCLAIM_COLLECTION: + systemarcadename = "acclaim"; + break; + case PSIKYO_COLLECTION: + systemarcadename = "psikyo"; + break; + case KANEKO_COLLECTION: + systemarcadename = "kaneko"; + break; + case COLECO_COLLECTION: + systemarcadename = "coleco"; + break; + case ATLUS_COLLECTION: + systemarcadename = "atlus"; + break; + case BANPRESTO_COLLECTION: + systemarcadename = "banpresto"; + break; + + case AUTO_AT2PLAYERS: + case AUTO_AT4PLAYERS: + { + std::string players = (*gameIt)->getMetadata("players"); + if (players.empty()) + include = false; + else + { + int min = -1; + + auto split = players.rfind("+"); + if (split != std::string::npos) + players = Utils::String::replace(players, "+", "-999"); + + split = players.rfind("-"); + if (split != std::string::npos) + { + min = atoi(players.substr(0, split).c_str()); + players = players.substr(split + 1); + } + + int max = atoi(players.c_str()); + int val = (sysDecl.type == AUTO_AT2PLAYERS ? 2 : 4); + include = min <= 0 ? (val == max) : (min <= val && val <= max); + } + } + break; + } + + if (!systemarcadename.empty()) + include = isArcade && (*gameIt)->getMetadata("arcadesystemname") == systemarcadename; + + if (include) + { + CollectionFileData* newGame = new CollectionFileData(*gameIt, newSys); + rootFolder->addChild(newGame); + newSys->addToIndex(newGame); + } + } + } + } + if (sysDecl.type == AUTO_LAST_PLAYED) + { + sortLastPlayed(newSys); + trimCollectionCount(rootFolder, LAST_PLAYED_MAX); + } + + sysData->isPopulated = true; +} + +// populates a Custom Collection System +void CollectionSystemManager::populateCustomCollection(CollectionSystemData* sysData, std::unordered_map* pMap) +{ + SystemData* newSys = sysData->system; + sysData->isPopulated = true; + CollectionSystemDecl sysDecl = sysData->decl; + std::string path = getCustomCollectionConfigPath(newSys->getName()); + + if(!Utils::FileSystem::exists(path)) + { + LOG(LogInfo) << "Couldn't find custom collection config file at " << path; + return; + } + LOG(LogInfo) << "Loading custom collection config file at " << path; + + FolderData* rootFolder = newSys->getRootFolder(); + + // get Configuration for this Custom System + std::ifstream input(path); + + FolderData* folder = getAllGamesCollection()->getRootFolder(); + + std::unordered_map map; + + if (pMap == nullptr) + { + folder->createChildrenByFilenameMap(map); + pMap = ↦ + } + + // iterate list of files in config file + for(std::string gameKey; getline(input, gameKey); ) + { + gameKey = Utils::FileSystem::resolveRelativePath(gameKey, "portnawak", true); + + std::unordered_map::const_iterator it = pMap->find(gameKey); + if (it != pMap->cend()) + { + CollectionFileData* newGame = new CollectionFileData(it->second, newSys); + rootFolder->addChild(newGame); + newSys->addToIndex(newGame); + } + else + { + LOG(LogInfo) << "Couldn't find game referenced at '" << gameKey << "' for system config '" << path << "'"; + } + } + updateCollectionFolderMetadata(newSys); +} + +/* Handle System View removal and insertion of Collections */ +void CollectionSystemManager::removeCollectionsFromDisplayedSystems() +{ + // remove all Collection Systems + for(auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); ) + { + if ((*sysIt)->isCollection()) + { + sysIt = SystemData::sSystemVector.erase(sysIt); + } + else + { + sysIt++; + } + } + + // remove all custom collections in bundle + // this should not delete the objects from memory! + FolderData* customRoot = mCustomCollectionsBundle->getRootFolder(); + std::vector mChildren = customRoot->getChildren(); + for(auto it = mChildren.cbegin(); it != mChildren.cend(); it++) + { + customRoot->removeChild(*it); + } + // clear index + mCustomCollectionsBundle->resetIndex(); + // remove view so it's re-created as needed + ViewController::get()->removeGameListView(mCustomCollectionsBundle); +} + +void CollectionSystemManager::addEnabledCollectionsToDisplayedSystems(std::map* colSystemData, std::unordered_map* pMap) +{ + // add auto enabled ones + for(std::map::iterator it = colSystemData->begin() ; it != colSystemData->end() ; it++ ) + { + if(it->second.isEnabled) + { + // check if populated, otherwise populate + if (!it->second.isPopulated) + { + if(it->second.decl.isCustom) + { + populateCustomCollection(&(it->second), pMap); + } + else + { + populateAutoCollection(&(it->second)); + } + } + // check if it has its own view + if(!it->second.decl.isCustom || themeFolderExists(it->first) || !Settings::getInstance()->getBool("UseCustomCollectionsSystem")) + { + if (it->second.decl.displayIfEmpty || it->second.system->getRootFolder()->getChildren().size() > 0) + { + // exists theme folder, or we chose not to bundle it under the custom-collections system + // so we need to create a view + if (it->second.isEnabled) + SystemData::sSystemVector.push_back(it->second.system); + } + } + else + { + FileData* newSysRootFolder = it->second.system->getRootFolder(); + mCustomCollectionsBundle->getRootFolder()->addChild(newSysRootFolder); + mCustomCollectionsBundle->getIndex(true)->importIndex(it->second.system->getIndex(true)); + } + } + } +} + +/* Auxiliary methods to get available custom collection possibilities */ +std::vector CollectionSystemManager::getSystemsFromConfig() +{ + std::vector systems; + std::string path = SystemData::getConfigPath(false); + + if(!Utils::FileSystem::exists(path)) + { + return systems; + } + + pugi::xml_document doc; + pugi::xml_parse_result res = doc.load_file(path.c_str()); + + if(!res) + { + return systems; + } + + //actually read the file + pugi::xml_node systemList = doc.child("systemList"); + + if(!systemList) + { + return systems; + } + + for(pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) + { + // theme folder + std::string themeFolder = system.child("theme").text().get(); + systems.push_back(themeFolder); + } + std::sort(systems.begin(), systems.end()); + return systems; +} + +// gets all folders from the current theme path +std::vector CollectionSystemManager::getSystemsFromTheme() +{ + std::vector systems; + + auto themeSets = ThemeData::getThemeSets(); + if(themeSets.empty()) + { + // no theme sets available + return systems; + } + + std::map::const_iterator set = themeSets.find(Settings::getInstance()->getString("ThemeSet")); + if(set == themeSets.cend()) + { + // currently selected theme set is missing, so just pick the first available set + set = themeSets.cbegin(); + Settings::getInstance()->setString("ThemeSet", set->first); + } + + std::string themePath = set->second.path; + + if (Utils::FileSystem::exists(themePath)) + { + Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(themePath); + + for (Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) + { + if (Utils::FileSystem::isDirectory(*it)) + { + //... here you have a directory + std::string folder = *it; + folder = folder.substr(themePath.size()+1); + + if(Utils::FileSystem::exists(set->second.getThemePath(folder))) + { + systems.push_back(folder); + } + } + } + } + std::sort(systems.begin(), systems.end()); + return systems; +} + +// returns the unused folders from current theme path +std::vector CollectionSystemManager::getUnusedSystemsFromTheme() +{ + // get used systems in es_systems.cfg + std::vector systemsInUse = getSystemsFromConfig(); + // get available folders in theme + std::vector themeSys = getSystemsFromTheme(); + // get folders assigned to custom collections + std::vector autoSys = getCollectionThemeFolders(false); + // get folder assigned to custom collections + std::vector customSys = getCollectionThemeFolders(true); + // get folders assigned to user collections + std::vector userSys = getUserCollectionThemeFolders(); + // add them all to the list of systems in use + systemsInUse.insert(systemsInUse.cend(), autoSys.cbegin(), autoSys.cend()); + systemsInUse.insert(systemsInUse.cend(), customSys.cbegin(), customSys.cend()); + systemsInUse.insert(systemsInUse.cend(), userSys.cbegin(), userSys.cend()); + + for(auto sysIt = themeSys.cbegin(); sysIt != themeSys.cend(); ) + { + if (std::find(systemsInUse.cbegin(), systemsInUse.cend(), *sysIt) != systemsInUse.cend()) + { + sysIt = themeSys.erase(sysIt); + } + else + { + sysIt++; + } + } + return themeSys; +} + +// returns which collection config files exist in the user folder +std::vector CollectionSystemManager::getCollectionsFromConfigFolder() +{ + std::vector systems; + std::string configPath = getCollectionsFolder(); + + if (Utils::FileSystem::exists(configPath)) + { + Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(configPath); + for (Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) + { + if (Utils::FileSystem::isRegularFile(*it)) + { + // it's a file + std::string filename = Utils::FileSystem::getFileName(*it); + + // need to confirm filename matches config format + if (filename != "custom-.cfg" && Utils::String::startsWith(filename, "custom-") && Utils::String::endsWith(filename, ".cfg")) + { + filename = filename.substr(7, filename.size()-11); + systems.push_back(filename); + } + else + { + LOG(LogInfo) << "Found non-collection config file in collections folder: " << filename; + } + } + } + } + return systems; +} + +// returns the theme folders for Automatic Collections (All, Favorites, Last Played) or generic Custom Collections folder +std::vector CollectionSystemManager::getCollectionThemeFolders(bool custom) +{ + std::vector systems; + for(std::map::const_iterator it = mCollectionSystemDeclsIndex.cbegin() ; it != mCollectionSystemDeclsIndex.cend() ; it++ ) + { + CollectionSystemDecl sysDecl = it->second; + if (sysDecl.isCustom == custom) + { + systems.push_back(sysDecl.themeFolder); + } + } + return systems; +} + +// returns the theme folders in use for the user-defined Custom Collections +std::vector CollectionSystemManager::getUserCollectionThemeFolders() +{ + std::vector systems; + for(std::map::const_iterator it = mCustomCollectionSystemsData.cbegin() ; it != mCustomCollectionSystemsData.cend() ; it++ ) + { + systems.push_back(it->second.decl.themeFolder); + } + return systems; +} + +// returns whether a specific folder exists in the theme +bool CollectionSystemManager::themeFolderExists(std::string folder) +{ + std::vector themeSys = getSystemsFromTheme(); + return std::find(themeSys.cbegin(), themeSys.cend(), folder) != themeSys.cend(); +} + +bool CollectionSystemManager::includeFileInAutoCollections(FileData* file) +{ + // we exclude non-game files from collections (i.e. "kodi", entries from non-game systems) + // if/when there are more in the future, maybe this can be a more complex method, with a proper list + // but for now a simple string comparison is more performant + return file->getName() != "kodi" && file->getSystem()->isGameSystem(); +} + +std::string getCustomCollectionConfigPath(std::string collectionName) +{ + return getCollectionsFolder() + "/custom-" + collectionName + ".cfg"; +} + +std::string getCollectionsFolder() +{ + return Utils::FileSystem::getGenericPath(Utils::FileSystem::getHomePath() + "/.emulationstation/collections"); +} + +bool systemSort(SystemData* sys1, SystemData* sys2) +{ + std::string name1 = Utils::String::toUpper(sys1->getName()); + std::string name2 = Utils::String::toUpper(sys2->getName()); + return name1.compare(name2) < 0; +} diff --git a/es-app/src/CollectionSystemManager.h b/es-app/src/CollectionSystemManager.h index 23afe19320..60cf43f045 100644 --- a/es-app/src/CollectionSystemManager.h +++ b/es-app/src/CollectionSystemManager.h @@ -5,8 +5,10 @@ #include #include #include +#include class FileData; +class FolderData; class SystemData; class Window; struct SystemEnvironmentData; @@ -15,8 +17,39 @@ enum CollectionSystemType { AUTO_ALL_GAMES, AUTO_LAST_PLAYED, + AUTO_AT2PLAYERS, + AUTO_AT4PLAYERS, + AUTO_NEVER_PLAYED, AUTO_FAVORITES, - CUSTOM_COLLECTION + + AUTO_ARCADE, + + CUSTOM_COLLECTION, + + CPS1_COLLECTION, + CPS2_COLLECTION, + CPS3_COLLECTION, + CAVE_COLLECTION, + NEOGEO_COLLECTION, + SEGA_COLLECTION, + IREM_COLLECTION, + MIDWAY_COLLECTION, + CAPCOM_COLLECTION, + TECMO_COLLECTION, + SNK_COLLECTION, + NAMCO_COLLECTION, + TAITO_COLLECTION, + KONAMI_COLLECTION, + JALECO_COLLECTION, + ATARI_COLLECTION, + NINTENDO_COLLECTION, + SAMMY_COLLECTION, + ACCLAIM_COLLECTION, + PSIKYO_COLLECTION, + KANEKO_COLLECTION, + COLECO_COLLECTION, + ATLUS_COLLECTION, + BANPRESTO_COLLECTION }; struct CollectionSystemDecl @@ -27,6 +60,7 @@ struct CollectionSystemDecl std::string defaultSort; std::string themeFolder; bool isCustom; + bool displayIfEmpty; }; struct CollectionSystemData @@ -41,6 +75,8 @@ struct CollectionSystemData class CollectionSystemManager { public: + static std::vector getSystemDecls(); + CollectionSystemManager(Window* window); ~CollectionSystemManager(); @@ -49,7 +85,7 @@ class CollectionSystemManager static void deinit(); void saveCustomCollection(SystemData* sys); - void loadCollectionSystems(); + void loadCollectionSystems(bool async=false); void loadEnabledListFromSettings(); void updateSystemsList(); @@ -57,7 +93,7 @@ class CollectionSystemManager void updateCollectionSystem(FileData* file, CollectionSystemData sysData); void deleteCollectionFiles(FileData* file); - inline std::map getAutoCollectionSystems() { return mAutoCollectionSystemsData; }; + inline std::map& getAutoCollectionSystems() { return mAutoCollectionSystemsData; }; inline std::map getCustomCollectionSystems() { return mCustomCollectionSystemsData; }; inline SystemData* getCustomCollectionsBundle() { return mCustomCollectionsBundle; }; std::vector getUnusedSystemsFromTheme(); @@ -75,6 +111,7 @@ class CollectionSystemManager SystemData* getSystemToView(SystemData* sys); void updateCollectionFolderMetadata(SystemData* sys); + void populateAutoCollection(CollectionSystemData* sysData); private: static CollectionSystemManager* sInstance; @@ -91,11 +128,11 @@ class CollectionSystemManager void initCustomCollectionSystems(); SystemData* getAllGamesCollection(); SystemData* createNewCollectionEntry(std::string name, CollectionSystemDecl sysDecl, bool index = true); - void populateAutoCollection(CollectionSystemData* sysData); - void populateCustomCollection(CollectionSystemData* sysData); + + void populateCustomCollection(CollectionSystemData* sysData, std::unordered_map* pMap = nullptr); void removeCollectionsFromDisplayedSystems(); - void addEnabledCollectionsToDisplayedSystems(std::map* colSystemData); + void addEnabledCollectionsToDisplayedSystems(std::map* colSystemData, std::unordered_map* pMap); std::vector getSystemsFromConfig(); std::vector getSystemsFromTheme(); @@ -103,7 +140,8 @@ class CollectionSystemManager std::vector getCollectionThemeFolders(bool custom); std::vector getUserCollectionThemeFolders(); - void trimCollectionCount(FileData* rootFolder, int limit); + void trimCollectionCount(FolderData* rootFolder, int limit); + void sortLastPlayed(SystemData* system); bool themeFolderExists(std::string folder); diff --git a/es-app/src/ContentInstaller.cpp b/es-app/src/ContentInstaller.cpp new file mode 100644 index 0000000000..7668e9b20e --- /dev/null +++ b/es-app/src/ContentInstaller.cpp @@ -0,0 +1,139 @@ +#include "ContentInstaller.h" +#include "Window.h" +#include "components/AsyncNotificationComponent.h" +#include "utils/StringUtil.h" +#include "ApiSystem.h" +#include "EsLocale.h" + +#define ICONINDEX _U("\uF019 ") + +ContentInstaller* ContentInstaller::mInstance = nullptr; +std::mutex ContentInstaller::mLock; +std::list> ContentInstaller::mQueue; +std::list> ContentInstaller::mProcessingQueue; + +void ContentInstaller::Enqueue(Window* window, ContentType type, const std::string contentName) +{ + std::unique_lock lock(mLock); + + for (auto item : mProcessingQueue) + if (item.first == type && item.second == contentName) + return; + + for (auto item : mQueue) + if (item.first == type && item.second == contentName) + return; + + mQueue.push_back(std::pair(type, contentName)); + + if (mInstance == nullptr) + mInstance = new ContentInstaller(window); + + mInstance->updateNotificationComponentTitle(true); +} + +ContentInstaller::ContentInstaller(Window* window) +{ + mInstance = this; + + mCurrent = 0; + mQueueSize = 0; + + mWindow = window; + + mWndNotification = new AsyncNotificationComponent(window, false); + mWindow->registerNotificationComponent(mWndNotification); + + mHandle = new std::thread(&ContentInstaller::threadUpdate, this); +} + +ContentInstaller::~ContentInstaller() +{ + mHandle = nullptr; + mWindow->unRegisterNotificationComponent(mWndNotification); + delete mWndNotification; +} + +void ContentInstaller::updateNotificationComponentTitle(bool incQueueSize) +{ + if (incQueueSize) + mQueueSize++; + + std::string cnt = " " + std::to_string(mCurrent) + "/" + std::to_string(mQueueSize); + mWndNotification->updateTitle(ICONINDEX + _("DOWNLOADING")+ cnt); +} + +void ContentInstaller::updateNotificationComponentContent(const std::string info) +{ + auto pos = info.find(">>>"); + if (pos != std::string::npos) + { + std::string percent(info.substr(pos)); + percent = Utils::String::replace(percent, ">", ""); + percent = Utils::String::replace(percent, "%", ""); + percent = Utils::String::replace(percent, " ", ""); + + int value = atoi(percent.c_str()); + + std::string text(info.substr(0, pos)); + text = Utils::String::trim(text); + + mWndNotification->updatePercent(value); + mWndNotification->updateText(text); + } + else + { + mWndNotification->updatePercent(-1); + mWndNotification->updateText(info); + } +} + +void ContentInstaller::threadUpdate() +{ + mCurrent = 0; + + while (true) + { + // Wait for an event to say there is something in the queue + std::unique_lock lock(mLock); + + if (mQueue.empty()) + break; + + mCurrent++; + updateNotificationComponentTitle(false); + + auto data = mQueue.front(); + mQueue.pop_front(); + mProcessingQueue.push_back(data); + + lock.unlock(); + + std::pair updateStatus; + + if (data.first == ContentType::CONTENT_THEME) + { + updateStatus = ApiSystem::installTheme(data.second, [this](const std::string info) + { + updateNotificationComponentContent(info); + }); + + if (updateStatus.second == 0) + mWindow->displayNotificationMessage(ICONINDEX + data.second + " : " + _("THEME INSTALLED SUCCESSFULLY")); + else + { + std::string error = _("AN ERROR OCCURED") + std::string(": ") + updateStatus.first; + mWindow->displayNotificationMessage(ICONINDEX + error); + } + + } + + lock.lock(); + mProcessingQueue.remove(data); + } + + std::unique_lock lock(mLock); + + delete this; + mInstance = nullptr; +} diff --git a/es-app/src/ContentInstaller.h b/es-app/src/ContentInstaller.h new file mode 100644 index 0000000000..724199f256 --- /dev/null +++ b/es-app/src/ContentInstaller.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include +#include +#include + +class Window; +class AsyncNotificationComponent; + + +class ContentInstaller +{ +public: + enum ContentType : int + { + CONTENT_THEME = 0 + }; + + static void Enqueue(Window* window, ContentType type, const std::string contentName); + +private: // Methods + ContentInstaller(Window* window); + ~ContentInstaller(); + + void updateNotificationComponentTitle(bool incQueueSize); + void updateNotificationComponentContent(const std::string info); + + void threadUpdate(); + +private: + AsyncNotificationComponent* mWndNotification; + Window* mWindow; + std::thread* mHandle; + + int mCurrent; + int mQueueSize; + +private: + static ContentInstaller* mInstance; + static std::mutex mLock; + static std::list> mQueue; + static std::list> mProcessingQueue; +}; diff --git a/es-app/src/EmulationStation.h b/es-app/src/EmulationStation.h index 2bd27925bc..ccd907d3f6 100644 --- a/es-app/src/EmulationStation.h +++ b/es-app/src/EmulationStation.h @@ -4,14 +4,17 @@ // These numbers and strings need to be manually updated for a new version. // Do this version number update as the very last commit for the new release version. -#define PROGRAM_VERSION_MAJOR 2 -#define PROGRAM_VERSION_MINOR 9 -#define PROGRAM_VERSION_MAINTENANCE 0 -#define PROGRAM_VERSION_STRING "2.9.0rp-dev" +#define PROGRAM_VERSION_MAJOR 2 +#define PROGRAM_VERSION_MINOR 10 +#define PROGRAM_VERSION_MAINTENANCE 2 +#define PROGRAM_VERSION_BUILD 2 + +#define PROGRAM_VERSION_STRING "2.10.2.2 FCAMOD" +#define SCRAPER_VERSION_STRING "2.10.2.2" #define PROGRAM_BUILT_STRING __DATE__ " - " __TIME__ -#define RESOURCE_VERSION_STRING "2,9,0\0" -#define RESOURCE_VERSION PROGRAM_VERSION_MAJOR,PROGRAM_VERSION_MINOR,PROGRAM_VERSION_MAINTENANCE +#define RESOURCE_VERSION_STRING "2,10,2,2\0" +#define RESOURCE_VERSION PROGRAM_VERSION_MAJOR,PROGRAM_VERSION_MINOR,PROGRAM_VERSION_MAINTENANCE,PROGRAM_VERSION_BUILD #endif // ES_APP_EMULATION_STATION_H diff --git a/es-app/src/FileData.cpp b/es-app/src/FileData.cpp index f4781d13d4..6fb8de5767 100644 --- a/es-app/src/FileData.cpp +++ b/es-app/src/FileData.cpp @@ -1,375 +1,606 @@ -#include "FileData.h" - -#include "utils/FileSystemUtil.h" -#include "utils/StringUtil.h" -#include "utils/TimeUtil.h" -#include "AudioManager.h" -#include "CollectionSystemManager.h" -#include "FileFilterIndex.h" -#include "FileSorts.h" -#include "Log.h" -#include "MameNames.h" -#include "platform.h" -#include "Scripting.h" -#include "SystemData.h" -#include "VolumeControl.h" -#include "Window.h" -#include - -FileData::FileData(FileType type, const std::string& path, SystemEnvironmentData* envData, SystemData* system) - : mType(type), mPath(path), mSystem(system), mEnvData(envData), mSourceFileData(NULL), mParent(NULL), metadata(type == GAME ? GAME_METADATA : FOLDER_METADATA) // metadata is REALLY set in the constructor! -{ - // metadata needs at least a name field (since that's what getName() will return) - if(metadata.get("name").empty()) - metadata.set("name", getDisplayName()); - mSystemName = system->getName(); -} - -FileData::~FileData() -{ - if(mParent) - mParent->removeChild(this); - - if(mType == GAME) - mSystem->getIndex()->removeFromIndex(this); - - mChildren.clear(); -} - -std::string FileData::getDisplayName() const -{ - std::string stem = Utils::FileSystem::getStem(mPath); - if(mSystem && mSystem->hasPlatformId(PlatformIds::ARCADE) || mSystem->hasPlatformId(PlatformIds::NEOGEO)) - stem = MameNames::getInstance()->getRealName(stem); - - return stem; -} - -std::string FileData::getCleanName() const -{ - return Utils::String::removeParenthesis(this->getDisplayName()); -} - -const std::string FileData::getThumbnailPath() const -{ - std::string thumbnail = metadata.get("thumbnail"); - - // no thumbnail, try image - if(thumbnail.empty()) - { - thumbnail = metadata.get("image"); - - // no image, try to use local image - if(thumbnail.empty() && Settings::getInstance()->getBool("LocalArt")) - { - const char* extList[2] = { ".png", ".jpg" }; - for(int i = 0; i < 2; i++) - { - if(thumbnail.empty()) - { - std::string path = mEnvData->mStartPath + "/images/" + getDisplayName() + "-image" + extList[i]; - if(Utils::FileSystem::exists(path)) - thumbnail = path; - } - } - } - } - - return thumbnail; -} - -const std::string& FileData::getName() -{ - return metadata.get("name"); -} - -const std::string& FileData::getSortName() -{ - if (metadata.get("sortname").empty()) - return metadata.get("name"); - else - return metadata.get("sortname"); -} - -const std::vector& FileData::getChildrenListToDisplay() { - - FileFilterIndex* idx = CollectionSystemManager::get()->getSystemToView(mSystem)->getIndex(); - if (idx->isFiltered()) { - mFilteredChildren.clear(); - for(auto it = mChildren.cbegin(); it != mChildren.cend(); it++) - { - if (idx->showFile((*it))) { - mFilteredChildren.push_back(*it); - } - } - - return mFilteredChildren; - } - else - { - return mChildren; - } -} - -const std::string FileData::getVideoPath() const -{ - std::string video = metadata.get("video"); - - // no video, try to use local video - if(video.empty() && Settings::getInstance()->getBool("LocalArt")) - { - std::string path = mEnvData->mStartPath + "/images/" + getDisplayName() + "-video.mp4"; - if(Utils::FileSystem::exists(path)) - video = path; - } - - return video; -} - -const std::string FileData::getMarqueePath() const -{ - std::string marquee = metadata.get("marquee"); - - // no marquee, try to use local marquee - if(marquee.empty() && Settings::getInstance()->getBool("LocalArt")) - { - const char* extList[2] = { ".png", ".jpg" }; - for(int i = 0; i < 2; i++) - { - if(marquee.empty()) - { - std::string path = mEnvData->mStartPath + "/images/" + getDisplayName() + "-marquee" + extList[i]; - if(Utils::FileSystem::exists(path)) - marquee = path; - } - } - } - - return marquee; -} - -const std::string FileData::getImagePath() const -{ - std::string image = metadata.get("image"); - - // no image, try to use local image - if(image.empty()) - { - const char* extList[2] = { ".png", ".jpg" }; - for(int i = 0; i < 2; i++) - { - if(image.empty()) - { - std::string path = mEnvData->mStartPath + "/images/" + getDisplayName() + "-image" + extList[i]; - if(Utils::FileSystem::exists(path)) - image = path; - } - } - } - - return image; -} - -std::vector FileData::getFilesRecursive(unsigned int typeMask, bool displayedOnly) const -{ - std::vector out; - FileFilterIndex* idx = mSystem->getIndex(); - - for(auto it = mChildren.cbegin(); it != mChildren.cend(); it++) - { - if((*it)->getType() & typeMask) - { - if (!displayedOnly || !idx->isFiltered() || idx->showFile(*it)) - out.push_back(*it); - } - - if((*it)->getChildren().size() > 0) - { - std::vector subchildren = (*it)->getFilesRecursive(typeMask, displayedOnly); - out.insert(out.cend(), subchildren.cbegin(), subchildren.cend()); - } - } - - return out; -} - -std::string FileData::getKey() { - return getFileName(); -} - -const bool FileData::isArcadeAsset() -{ - const std::string stem = Utils::FileSystem::getStem(mPath); - return ( - (mSystem && (mSystem->hasPlatformId(PlatformIds::ARCADE) || mSystem->hasPlatformId(PlatformIds::NEOGEO))) - && - (MameNames::getInstance()->isBios(stem) || MameNames::getInstance()->isDevice(stem)) - ); -} - -FileData* FileData::getSourceFileData() -{ - return this; -} - -void FileData::addChild(FileData* file) -{ - assert(mType == FOLDER); - assert(file->getParent() == NULL); - - const std::string key = file->getKey(); - if (mChildrenByFilename.find(key) == mChildrenByFilename.cend()) - { - mChildrenByFilename[key] = file; - mChildren.push_back(file); - file->mParent = this; - } -} - -void FileData::removeChild(FileData* file) -{ - assert(mType == FOLDER); - assert(file->getParent() == this); - mChildrenByFilename.erase(file->getKey()); - for(auto it = mChildren.cbegin(); it != mChildren.cend(); it++) - { - if(*it == file) - { - file->mParent = NULL; - mChildren.erase(it); - return; - } - } - - // File somehow wasn't in our children. - assert(false); - -} - -void FileData::sort(ComparisonFunction& comparator, bool ascending) -{ - std::stable_sort(mChildren.begin(), mChildren.end(), comparator); - - for(auto it = mChildren.cbegin(); it != mChildren.cend(); it++) - { - if((*it)->getChildren().size() > 0) - (*it)->sort(comparator, ascending); - } - - if(!ascending) - std::reverse(mChildren.begin(), mChildren.end()); -} - -void FileData::sort(const SortType& type) -{ - sort(*type.comparisonFunction, type.ascending); -} - -void FileData::launchGame(Window* window) -{ - LOG(LogInfo) << "Attempting to launch game..."; - - AudioManager::getInstance()->deinit(); - VolumeControl::getInstance()->deinit(); - window->deinit(); - - std::string command = mEnvData->mLaunchCommand; - - const std::string rom = Utils::FileSystem::getEscapedPath(getPath()); - const std::string basename = Utils::FileSystem::getStem(getPath()); - const std::string rom_raw = Utils::FileSystem::getPreferredPath(getPath()); - - command = Utils::String::replace(command, "%ROM%", rom); - command = Utils::String::replace(command, "%BASENAME%", basename); - command = Utils::String::replace(command, "%ROM_RAW%", rom_raw); - - Scripting::fireEvent("game-start", rom, basename); - - LOG(LogInfo) << " " << command; - int exitCode = runSystemCommand(command); - - if(exitCode != 0) - { - LOG(LogWarning) << "...launch terminated with nonzero exit code " << exitCode << "!"; - } - - Scripting::fireEvent("game-end"); - - window->init(); - VolumeControl::getInstance()->init(); - window->normalizeNextUpdate(); - - //update number of times the game has been launched - - FileData* gameToUpdate = getSourceFileData(); - - int timesPlayed = gameToUpdate->metadata.getInt("playcount") + 1; - gameToUpdate->metadata.set("playcount", std::to_string(static_cast(timesPlayed))); - - //update last played time - gameToUpdate->metadata.set("lastplayed", Utils::Time::DateTime(Utils::Time::now())); - CollectionSystemManager::get()->refreshCollectionSystems(gameToUpdate); -} - -CollectionFileData::CollectionFileData(FileData* file, SystemData* system) - : FileData(file->getSourceFileData()->getType(), file->getSourceFileData()->getPath(), file->getSourceFileData()->getSystemEnvData(), system) -{ - // we use this constructor to create a clone of the filedata, and change its system - mSourceFileData = file->getSourceFileData(); - refreshMetadata(); - mParent = NULL; - metadata = mSourceFileData->metadata; - mSystemName = mSourceFileData->getSystem()->getName(); -} - -CollectionFileData::~CollectionFileData() -{ - // need to remove collection file data at the collection object destructor - if(mParent) - mParent->removeChild(this); - mParent = NULL; -} - -std::string CollectionFileData::getKey() { - return getFullPath(); -} - -FileData* CollectionFileData::getSourceFileData() -{ - return mSourceFileData; -} - -void CollectionFileData::refreshMetadata() -{ - metadata = mSourceFileData->metadata; - mDirty = true; -} - -const std::string& CollectionFileData::getName() -{ - if (mDirty) { - mCollectionFileName = Utils::String::removeParenthesis(mSourceFileData->metadata.get("name")); - mCollectionFileName += " [" + Utils::String::toUpper(mSourceFileData->getSystem()->getName()) + "]"; - mDirty = false; - } - - if (Settings::getInstance()->getBool("CollectionShowSystemInfo")) - return mCollectionFileName; - return mSourceFileData->metadata.get("name"); -} - -// returns Sort Type based on a string description -FileData::SortType getSortTypeFromString(std::string desc) { - std::vector SortTypes = FileSorts::SortTypes; - // find it - for(unsigned int i = 0; i < FileSorts::SortTypes.size(); i++) - { - const FileData::SortType& sort = FileSorts::SortTypes.at(i); - if(sort.description == desc) - { - return sort; - } - } - // if not found default to name, ascending - return FileSorts::SortTypes.at(0); -} +#include "FileData.h" + +#include "utils/FileSystemUtil.h" +#include "utils/StringUtil.h" +#include "utils/TimeUtil.h" +#include "AudioManager.h" +#include "CollectionSystemManager.h" +#include "FileFilterIndex.h" +#include "FileSorts.h" +#include "Log.h" +#include "MameNames.h" +#include "platform.h" +#include "Scripting.h" +#include "SystemData.h" +#include "VolumeControl.h" +#include "Window.h" +#include "views/UIModeController.h" +#include +#include "Gamelist.h" + +FileData::FileData(FileType type, const std::string& path, SystemData* system) + : mType(type), mSystem(system), mParent(NULL), mMetadata(type == GAME ? GAME_METADATA : FOLDER_METADATA) // metadata is REALLY set in the constructor! +{ + mPath = Utils::FileSystem::createRelativePath(path, getSystemEnvData()->mStartPath, false); + +// TRACE("FileData : " << mPath); + + // metadata needs at least a name field (since that's what getName() will return) + if (mMetadata.get("name").empty()) + mMetadata.set("name", getDisplayName()); + + mMetadata.resetChangedFlag(); +} + +const std::string FileData::getPath() const +{ + if (mPath.empty()) + return getSystemEnvData()->mStartPath; + + return Utils::FileSystem::resolveRelativePath(mPath, getSystemEnvData()->mStartPath, true); +} + +inline SystemEnvironmentData* FileData::getSystemEnvData() const +{ + return mSystem->getSystemEnvData(); +} + +std::string FileData::getSystemName() const +{ + return mSystem->getName(); +} + +FileData::~FileData() +{ + if(mParent) + mParent->removeChild(this); + + if(mType == GAME) + mSystem->removeFromIndex(this); +} + +std::string FileData::getDisplayName() const +{ + std::string stem = Utils::FileSystem::getStem(getPath()); + if(mSystem && mSystem->hasPlatformId(PlatformIds::ARCADE) || mSystem->hasPlatformId(PlatformIds::NEOGEO)) + stem = MameNames::getInstance()->getRealName(stem); + + return stem; +} + +std::string FileData::getCleanName() const +{ + return Utils::String::removeParenthesis(this->getDisplayName()); +} + +const std::string FileData::getThumbnailPath() +{ + std::string thumbnail = getMetadata().get("thumbnail"); + + // no thumbnail, try image + if(thumbnail.empty()) + { + thumbnail = getMetadata().get("image"); + + // no image, try to use local image + if(thumbnail.empty() && Settings::getInstance()->getBool("LocalArt")) + { + const char* extList[2] = { ".png", ".jpg" }; + for(int i = 0; i < 2; i++) + { + if(thumbnail.empty()) + { + std::string path = getSystemEnvData()->mStartPath + "/images/" + getDisplayName() + "-thumb" + extList[i]; + if (Utils::FileSystem::exists(path)) + { + setMetadata("thumbnail", path); + thumbnail = path; + } + } + } + } + + if (thumbnail.empty()) + thumbnail = getMetadata().get("image"); + + // no image, try to use local image + if (thumbnail.empty() && Settings::getInstance()->getBool("LocalArt")) + { + const char* extList[2] = { ".png", ".jpg" }; + for (int i = 0; i < 2; i++) + { + if (thumbnail.empty()) + { + std::string path = getSystemEnvData()->mStartPath + "/images/" + getDisplayName() + "-image" + extList[i]; + if (!Utils::FileSystem::exists(path)) + path = getSystemEnvData()->mStartPath + "/images/" + getDisplayName() + extList[i]; + + if (Utils::FileSystem::exists(path)) + thumbnail = path; + } + } + } + } + + return thumbnail; +} + +const bool FileData::getFavorite() +{ + return getMetadata().get("favorite") == "true"; +} + +const bool FileData::getHidden() +{ + return getMetadata().get("hidden") == "true"; +} + +const bool FileData::getKidGame() +{ + return getMetadata().get("kidgame") != "false"; +} + +static std::shared_ptr showFilenames; + +void FileData::resetSettings() +{ + showFilenames = nullptr; +} + +const std::string FileData::getName() +{ + if (showFilenames == nullptr) + showFilenames = std::make_shared(Settings::getInstance()->getBool("ShowFilenames")); + + // Faster than accessing map each time + if (*showFilenames) + { + if (mSystem != nullptr && !mSystem->hasPlatformId(PlatformIds::ARCADE) && !mSystem->hasPlatformId(PlatformIds::NEOGEO)) + return Utils::FileSystem::getStem(getPath()); + else + return getDisplayName(); + } + + return getMetadata().getName(); +} + +const std::string FileData::getCore() const +{ + return getMetadata().get("core"); +} + +const std::string FileData::getEmulator() const +{ + return getMetadata().get("emulator"); +} + +const std::string FileData::getVideoPath() +{ + std::string video = getMetadata().get("video"); + + // no video, try to use local video + if(video.empty() && Settings::getInstance()->getBool("LocalArt")) + { + std::string path = getSystemEnvData()->mStartPath + "/images/" + getDisplayName() + "-video.mp4"; + if (Utils::FileSystem::exists(path)) + { + setMetadata("video", path); + video = path; + } + } + + return video; +} + +const std::string FileData::getMarqueePath() +{ + std::string marquee = getMetadata().get("marquee"); + + // no marquee, try to use local marquee + if (marquee.empty() && Settings::getInstance()->getBool("LocalArt")) + { + const char* extList[2] = { ".png", ".jpg" }; + for(int i = 0; i < 2; i++) + { + if(marquee.empty()) + { + std::string path = getSystemEnvData()->mStartPath + "/images/" + getDisplayName() + "-marquee" + extList[i]; + if(Utils::FileSystem::exists(path)) + { + setMetadata("marquee", path); + marquee = path; + } + } + } + } + + return marquee; +} + +const std::string FileData::getImagePath() +{ + std::string image = getMetadata().get("image"); + + // no image, try to use local image + if(image.empty()) + { + const char* extList[2] = { ".png", ".jpg" }; + for(int i = 0; i < 2; i++) + { + if(image.empty()) + { + std::string path = getSystemEnvData()->mStartPath + "/images/" + getDisplayName() + "-image" + extList[i]; + if(Utils::FileSystem::exists(path)) + { + setMetadata("image", path); + image = path; + } + } + } + } + + return image; +} + +std::string FileData::getKey() { + return getFileName(); +} + +const bool FileData::isArcadeAsset() +{ + if (mSystem && (mSystem->hasPlatformId(PlatformIds::ARCADE) || mSystem->hasPlatformId(PlatformIds::NEOGEO))) + { + const std::string stem = Utils::FileSystem::getStem(getPath()); + return MameNames::getInstance()->isBios(stem) || MameNames::getInstance()->isDevice(stem); + } + + return false; +} + +FileData* FileData::getSourceFileData() +{ + return this; +} + +void FileData::launchGame(Window* window) +{ + LOG(LogInfo) << "Attempting to launch game..."; + + AudioManager::getInstance()->deinit(); + VolumeControl::getInstance()->deinit(); + + bool hideWindow = Settings::getInstance()->getBool("HideWindow"); + window->deinit(hideWindow); + + std::string command = getSystemEnvData()->mLaunchCommand; + + const std::string rom = Utils::FileSystem::getEscapedPath(getPath()); + const std::string basename = Utils::FileSystem::getStem(getPath()); + const std::string rom_raw = Utils::FileSystem::getPreferredPath(getPath()); + + std::string emulator = getEmulator(); + if (emulator.length() == 0) + emulator = getSystemEnvData()->getDefaultEmulator(); + + std::string core = getCore(); + if (core.length() == 0) + core = getSystemEnvData()->getDefaultCore(emulator); + + std::string customCommandLine = getSystemEnvData()->getEmulatorCommandLine(emulator); + if (customCommandLine.length() > 0) + command = customCommandLine; + + + + command = Utils::String::replace(command, "%EMULATOR%", emulator); + command = Utils::String::replace(command, "%CORE%", core); + + command = Utils::String::replace(command, "%ROM%", rom); + command = Utils::String::replace(command, "%BASENAME%", basename); + command = Utils::String::replace(command, "%ROM_RAW%", rom_raw); + command = Utils::String::replace(command, "%SYSTEM%", getSystemName()); + command = Utils::String::replace(command, "%HOME%", Utils::FileSystem::getHomePath()); + + Scripting::fireEvent("game-start", rom, basename); + + LOG(LogInfo) << " " << command; + + int exitCode = runSystemCommand(command, getDisplayName(), hideWindow ? NULL : window); + if (exitCode != 0) + { + LOG(LogWarning) << "...launch terminated with nonzero exit code " << exitCode << "!"; + } + + Scripting::fireEvent("game-end"); + + window->init(hideWindow); + + VolumeControl::getInstance()->init(); + AudioManager::getInstance()->init(); + window->normalizeNextUpdate(); + + //update number of times the game has been launched + if (exitCode == 0) + { + FileData* gameToUpdate = getSourceFileData(); + + int timesPlayed = gameToUpdate->getMetadata().getInt("playcount") + 1; + gameToUpdate->getMetadata().set("playcount", std::to_string(static_cast(timesPlayed))); + + //update last played time + gameToUpdate->getMetadata().set("lastplayed", Utils::Time::DateTime(Utils::Time::now())); + CollectionSystemManager::get()->refreshCollectionSystems(gameToUpdate); + + saveToGamelistRecovery(gameToUpdate); + } + + // music + if (Settings::getInstance()->getBool("audio.bgmusic")) + AudioManager::getInstance()->playRandomMusic(); +} + +CollectionFileData::CollectionFileData(FileData* file, SystemData* system) + : FileData(file->getSourceFileData()->getType(), "", system) +{ + mSourceFileData = file->getSourceFileData(); + mParent = NULL; + // metadata = mSourceFileData->metadata; + mDirty = true; +} + +SystemEnvironmentData* CollectionFileData::getSystemEnvData() const +{ + return mSourceFileData->getSystemEnvData(); +} + +const std::string CollectionFileData::getPath() const +{ + return mSourceFileData->getPath(); +} + +std::string CollectionFileData::getSystemName() const +{ + return mSourceFileData->getSystem()->getName(); +} + +CollectionFileData::~CollectionFileData() +{ + // need to remove collection file data at the collection object destructor + if(mParent) + mParent->removeChild(this); + mParent = NULL; +} + +std::string CollectionFileData::getKey() { + return getFullPath(); +} + +FileData* CollectionFileData::getSourceFileData() +{ + return mSourceFileData; +} + +void CollectionFileData::refreshMetadata() +{ + // metadata = mSourceFileData->metadata; + mDirty = true; +} + +const std::string CollectionFileData::getName() +{ + if (mDirty) { + mCollectionFileName = Utils::String::removeParenthesis(mSourceFileData->getMetadata().get("name")); + mCollectionFileName += " [" + Utils::String::toUpper(mSourceFileData->getSystem()->getName()) + "]"; + mDirty = false; + } + + if (Settings::getInstance()->getBool("CollectionShowSystemInfo")) + return mCollectionFileName; + + return Utils::String::removeParenthesis(mSourceFileData->getMetadata().get("name")); +} + +const std::vector FolderData::getChildrenListToDisplay() +{ + std::vector ret; + + std::string showFoldersMode = Settings::getInstance()->getString("FolderViewMode"); + + bool showHiddenFiles = Settings::getInstance()->getBool("ShowHiddenFiles"); + bool filterKidGame = false; + + if (!Settings::getInstance()->getBool("ForceDisableFilters")) + { + if (UIModeController::getInstance()->isUIModeKiosk()) + showHiddenFiles = false; + + if (UIModeController::getInstance()->isUIModeKid()) + filterKidGame = true; + } + + auto sys = CollectionSystemManager::get()->getSystemToView(mSystem); + + FileFilterIndex* idx = sys->getIndex(false); + if (idx != nullptr && !idx->isFiltered()) + idx = nullptr; + + std::vector* items = &mChildren; + + std::vector flatGameList; + if (showFoldersMode == "never") + { + flatGameList = getFlatGameList(false, sys); + items = &flatGameList; + } + + bool refactorUniqueGameFolders = (showFoldersMode == "having multiple games"); + + for (auto it = items->cbegin(); it != items->cend(); it++) + { + if (idx != nullptr && !idx->showFile((*it))) + continue; + + if (!showHiddenFiles && (*it)->getHidden()) + continue; + + if (filterKidGame && !(*it)->getKidGame()) + continue; + + if ((*it)->getType() == FOLDER && refactorUniqueGameFolders) + { + FolderData* pFolder = (FolderData*)(*it); + auto fd = pFolder->findUniqueGameForFolder(); + if (fd != nullptr) + { + if (idx != nullptr && !idx->showFile(fd)) + continue; + + if (!showHiddenFiles && fd->getHidden()) + continue; + + if (filterKidGame && !fd->getKidGame()) + continue; + + ret.push_back(fd); + + continue; + } + } + + ret.push_back(*it); + } + + unsigned int currentSortId = sys->getSortId(); + if (currentSortId >= FileSorts::getSortTypes().size()) + currentSortId = 0; + + const FileSorts::SortType& sort = FileSorts::getSortTypes().at(currentSortId); + std::sort(ret.begin(), ret.end(), sort.comparisonFunction); + + if (!sort.ascending) + std::reverse(ret.begin(), ret.end()); + + return ret; +} + +FileData* FolderData::findUniqueGameForFolder() +{ + auto games = getFilesRecursive(GAME); + if (games.size() == 1) + { + auto it = games.cbegin(); + if ((*it)->getType() == GAME) + return (*it); + } + + return nullptr; +} + +std::vector FolderData::getFlatGameList(bool displayedOnly, SystemData* system) const +{ + std::vector ret = getFilesRecursive(GAME, displayedOnly, system); + + unsigned int currentSortId = system->getSortId(); + if (currentSortId < 0 || currentSortId >= FileSorts::getSortTypes().size()) + currentSortId = 0; + + auto sort = FileSorts::getSortTypes().at(currentSortId); + + std::stable_sort(ret.begin(), ret.end(), sort.comparisonFunction); + + if (!sort.ascending) + std::reverse(ret.begin(), ret.end()); + + return ret; +} + +std::vector FolderData::getFilesRecursive(unsigned int typeMask, bool displayedOnly, SystemData* system) const +{ + std::vector out; + + FileFilterIndex* idx = (system != nullptr ? system : mSystem)->getIndex(false); + + for (auto it = mChildren.cbegin(); it != mChildren.cend(); it++) + { + if ((*it)->getType() & typeMask) + { + if (!displayedOnly || idx == nullptr || !idx->isFiltered() || idx->showFile(*it)) + out.push_back(*it); + } + + if ((*it)->getType() != FOLDER) + continue; + + FolderData* folder = (FolderData*)(*it); + if (folder->getChildren().size() > 0) + { + std::vector subchildren = folder->getFilesRecursive(typeMask, displayedOnly, system); + out.insert(out.cend(), subchildren.cbegin(), subchildren.cend()); + } + } + + return out; +} + +void FolderData::addChild(FileData* file, bool assignParent) +{ + assert(file->getParent() == nullptr || !assignParent); + + mChildren.push_back(file); + + if (assignParent) + file->setParent(this); +} + +void FolderData::removeChild(FileData* file) +{ + assert(mType == FOLDER); + assert(file->getParent() == this); + + for (auto it = mChildren.cbegin(); it != mChildren.cend(); it++) + { + if (*it == file) + { + file->setParent(NULL); + mChildren.erase(it); + return; + } + } + + // File somehow wasn't in our children. + assert(false); + +} + +FileData* FolderData::FindByPath(const std::string& path) +{ + std::vector children = getChildren(); + + for (std::vector::const_iterator it = children.cbegin(); it != children.cend(); ++it) + { + if ((*it)->getPath() == path) + return (*it); + + if ((*it)->getType() != FOLDER) + continue; + + auto item = ((FolderData*)(*it))->FindByPath(path); + if (item != nullptr) + return item; + } + + return nullptr; +} + +void FolderData::createChildrenByFilenameMap(std::unordered_map& map) +{ + std::vector children = getChildren(); + + for (std::vector::const_iterator it = children.cbegin(); it != children.cend(); ++it) + { + if ((*it)->getType() == FOLDER) + ((FolderData*)(*it))->createChildrenByFilenameMap(map); + else + map[(*it)->getKey()] = (*it); + } +} diff --git a/es-app/src/FileData.h b/es-app/src/FileData.h index 63a9ce16e3..4bfaf64df6 100644 --- a/es-app/src/FileData.h +++ b/es-app/src/FileData.h @@ -25,36 +25,39 @@ enum FileChangeType FILE_SORTED }; -// Used for loading/saving gamelist.xml. -const char* fileTypeToString(FileType type); -FileType stringToFileType(const char* str); +class FolderData; // A tree node that holds information for a file. class FileData { public: - FileData(FileType type, const std::string& path, SystemEnvironmentData* envData, SystemData* system); + FileData(FileType type, const std::string& path, SystemData* system); virtual ~FileData(); - virtual const std::string& getName(); - virtual const std::string& getSortName(); + virtual const std::string getName(); + inline FileType getType() const { return mType; } - inline const std::string& getPath() const { return mPath; } - inline FileData* getParent() const { return mParent; } - inline const std::unordered_map& getChildrenByFilename() const { return mChildrenByFilename; } - inline const std::vector& getChildren() const { return mChildren; } + + inline FolderData* getParent() const { return mParent; } + void setParent(FolderData* parent) { mParent = parent; } + inline SystemData* getSystem() const { return mSystem; } - inline SystemEnvironmentData* getSystemEnvData() const { return mEnvData; } - virtual const std::string getThumbnailPath() const; - virtual const std::string getVideoPath() const; - virtual const std::string getMarqueePath() const; - virtual const std::string getImagePath() const; - const std::vector& getChildrenListToDisplay(); - std::vector getFilesRecursive(unsigned int typeMask, bool displayedOnly = false) const; + virtual const std::string getPath() const; - void addChild(FileData* file); // Error if mType != FOLDER - void removeChild(FileData* file); //Error if mType != FOLDER + virtual SystemEnvironmentData* getSystemEnvData() const; + + virtual const std::string getThumbnailPath(); + virtual const std::string getVideoPath(); + virtual const std::string getMarqueePath(); + virtual const std::string getImagePath(); + + virtual const std::string getCore() const; + virtual const std::string getEmulator() const; + + virtual const bool getHidden(); + virtual const bool getFavorite(); + virtual const bool getKidGame(); inline bool isPlaceHolder() { return mType == PLACEHOLDER; }; @@ -65,7 +68,7 @@ class FileData inline std::string getFullPath() { return getPath(); }; inline std::string getFileName() { return Utils::FileSystem::getFileName(getPath()); }; virtual FileData* getSourceFileData(); - inline std::string getSystemName() const { return mSystemName; }; + virtual std::string getSystemName() const; // Returns our best guess at the "real" name for this file (will attempt to perform MAME name translation) std::string getDisplayName() const; @@ -75,34 +78,24 @@ class FileData void launchGame(Window* window); - typedef bool ComparisonFunction(const FileData* a, const FileData* b); - struct SortType - { - ComparisonFunction* comparisonFunction; - bool ascending; - std::string description; - - SortType(ComparisonFunction* sortFunction, bool sortAscending, const std::string & sortDescription) - : comparisonFunction(sortFunction), ascending(sortAscending), description(sortDescription) {} - }; - - void sort(ComparisonFunction& comparator, bool ascending = true); - void sort(const SortType& type); - MetaDataList metadata; + static void resetSettings(); + + virtual const MetaDataList& getMetadata() const { return mMetadata; } + virtual MetaDataList& getMetadata() { return mMetadata; } -protected: - FileData* mSourceFileData; - FileData* mParent; - std::string mSystemName; + void setMetadata(MetaDataList value) { getMetadata() = value; } + + std::string getMetadata(const std::string& key) { return getMetadata().get(key); } + void setMetadata(const std::string& key, const std::string& value) { getMetadata().set(key, value); } private: - FileType mType; + MetaDataList mMetadata; + +protected: + FolderData* mParent; std::string mPath; - SystemEnvironmentData* mEnvData; + FileType mType; SystemData* mSystem; - std::unordered_map mChildrenByFilename; - std::vector mChildren; - std::vector mFilteredChildren; }; class CollectionFileData : public FileData @@ -110,16 +103,69 @@ class CollectionFileData : public FileData public: CollectionFileData(FileData* file, SystemData* system); ~CollectionFileData(); - const std::string& getName(); + const std::string getName(); void refreshMetadata(); FileData* getSourceFileData(); std::string getKey(); + virtual const std::string getPath() const; + + virtual std::string getSystemName() const; + virtual SystemEnvironmentData* getSystemEnvData() const; + + virtual const MetaDataList& getMetadata() const { return mSourceFileData->getMetadata(); } + virtual MetaDataList& getMetadata() { return mSourceFileData->getMetadata(); } + private: // needs to be updated when metadata changes std::string mCollectionFileName; + FileData* mSourceFileData; + bool mDirty; }; -FileData::SortType getSortTypeFromString(std::string desc); +class FolderData : public FileData +{ +public: + FolderData(const std::string& startpath, SystemData* system, bool ownsChildrens=true) : FileData(FOLDER, startpath, system) + { + mIsDisplayableAsVirtualFolder = false; + mOwnsChildrens = ownsChildrens; + } + + ~FolderData() + { + if (mOwnsChildrens) + { + for (int i = mChildren.size() - 1; i >= 0; i--) + delete mChildren.at(i); + } + + mChildren.clear(); + } + + inline bool isVirtualFolderDisplay() { return mIsDisplayableAsVirtualFolder && !mOwnsChildrens; } + void enableVirtualFolderDisplay(bool value) { mIsDisplayableAsVirtualFolder = value; }; + bool isVirtualFolderDisplayEnabled() { return mIsDisplayableAsVirtualFolder; }; + + FileData* FindByPath(const std::string& path); + + inline const std::vector& getChildren() const { return mChildren; } + const std::vector getChildrenListToDisplay(); + std::vector getFilesRecursive(unsigned int typeMask, bool displayedOnly = false, SystemData* system = nullptr) const; + + void addChild(FileData* file, bool assignParent = true); // Error if mType != FOLDER + void removeChild(FileData* file); //Error if mType != FOLDER + + void createChildrenByFilenameMap(std::unordered_map& map); + + FileData* findUniqueGameForFolder(); + +private: + std::vector getFlatGameList(bool displayedOnly, SystemData* system) const; + std::vector mChildren; + + bool mOwnsChildrens; + bool mIsDisplayableAsVirtualFolder; +}; #endif // ES_APP_FILE_DATA_H diff --git a/es-app/src/FileFilterIndex.cpp b/es-app/src/FileFilterIndex.cpp index 4ddf6b8302..23556d17ba 100644 --- a/es-app/src/FileFilterIndex.cpp +++ b/es-app/src/FileFilterIndex.cpp @@ -21,7 +21,7 @@ FileFilterIndex::FileFilterIndex() { PUBDEV_FILTER, &pubDevIndexAllKeys, &filterByPubDev, &pubDevIndexFilteredKeys, "developer", true, "publisher", "PUBLISHER / DEVELOPER" }, { RATINGS_FILTER, &ratingsIndexAllKeys, &filterByRatings, &ratingsIndexFilteredKeys, "rating", false, "", "RATING" }, { KIDGAME_FILTER, &kidGameIndexAllKeys, &filterByKidGame, &kidGameIndexFilteredKeys, "kidgame", false, "", "KIDGAME" }, - { HIDDEN_FILTER, &hiddenIndexAllKeys, &filterByHidden, &hiddenIndexFilteredKeys, "hidden", false, "", "HIDDEN" } + // { HIDDEN_FILTER, &hiddenIndexAllKeys, &filterByHidden, &hiddenIndexFilteredKeys, "hidden", false, "", "HIDDEN" } }; filterDataDecl = std::vector(filterDecls, filterDecls + sizeof(filterDecls) / sizeof(filterDecls[0])); @@ -51,7 +51,7 @@ void FileFilterIndex::importIndex(FileFilterIndex* indexToImport) { &pubDevIndexAllKeys, &(indexToImport->pubDevIndexAllKeys) }, { &ratingsIndexAllKeys, &(indexToImport->ratingsIndexAllKeys) }, { &favoritesIndexAllKeys, &(indexToImport->favoritesIndexAllKeys) }, - { &hiddenIndexAllKeys, &(indexToImport->hiddenIndexAllKeys) }, + // { &hiddenIndexAllKeys, &(indexToImport->hiddenIndexAllKeys) }, { &kidGameIndexAllKeys, &(indexToImport->kidGameIndexAllKeys) }, }; @@ -81,7 +81,7 @@ void FileFilterIndex::resetIndex() clearIndex(pubDevIndexAllKeys); clearIndex(ratingsIndexAllKeys); clearIndex(favoritesIndexAllKeys); - clearIndex(hiddenIndexAllKeys); + // clearIndex(hiddenIndexAllKeys); clearIndex(kidGameIndexAllKeys); } @@ -92,7 +92,7 @@ std::string FileFilterIndex::getIndexableKey(FileData* game, FilterIndexType typ { case GENRE_FILTER: { - key = Utils::String::toUpper(game->metadata.get("genre")); + key = Utils::String::toUpper(game->getMetadata().get("genre")); key = Utils::String::trim(key); if (getSecondary && !key.empty()) { std::istringstream f(key); @@ -114,18 +114,18 @@ std::string FileFilterIndex::getIndexableKey(FileData* game, FilterIndexType typ if (getSecondary) break; - key = game->metadata.get("players"); + key = game->getMetadata().get("players"); break; } case PUBDEV_FILTER: { - key = Utils::String::toUpper(game->metadata.get("publisher")); + key = Utils::String::toUpper(game->getMetadata().get("publisher")); key = Utils::String::trim(key); if ((getSecondary && !key.empty()) || (!getSecondary && key.empty())) - key = Utils::String::toUpper(game->metadata.get("developer")); + key = Utils::String::toUpper(game->getMetadata().get("developer")); else - key = Utils::String::toUpper(game->metadata.get("publisher")); + key = Utils::String::toUpper(game->getMetadata().get("publisher")); break; } case RATINGS_FILTER: @@ -133,7 +133,7 @@ std::string FileFilterIndex::getIndexableKey(FileData* game, FilterIndexType typ int ratingNumber = 0; if (!getSecondary) { - std::string ratingString = game->metadata.get("rating"); + std::string ratingString = game->getMetadata().get("rating"); if (!ratingString.empty()) { try { ratingNumber = (int)((std::stod(ratingString)*5)+0.5); @@ -154,21 +154,21 @@ std::string FileFilterIndex::getIndexableKey(FileData* game, FilterIndexType typ { if (game->getType() != GAME) return "FALSE"; - key = Utils::String::toUpper(game->metadata.get("favorite")); + key = Utils::String::toUpper(game->getMetadata().get("favorite")); break; } case HIDDEN_FILTER: { if (game->getType() != GAME) return "FALSE"; - key = Utils::String::toUpper(game->metadata.get("hidden")); + key = Utils::String::toUpper(game->getMetadata().get("hidden")); break; } case KIDGAME_FILTER: { if (game->getType() != GAME) return "FALSE"; - key = Utils::String::toUpper(game->metadata.get("kidgame")); + key = Utils::String::toUpper(game->getMetadata().get("kidgame")); break; } } @@ -186,7 +186,7 @@ void FileFilterIndex::addToIndex(FileData* game) managePubDevEntryInIndex(game); manageRatingsEntryInIndex(game); manageFavoritesEntryInIndex(game); - manageHiddenEntryInIndex(game); + //manageHiddenEntryInIndex(game); manageKidGameEntryInIndex(game); } @@ -197,7 +197,7 @@ void FileFilterIndex::removeFromIndex(FileData* game) managePubDevEntryInIndex(game, true); manageRatingsEntryInIndex(game, true); manageFavoritesEntryInIndex(game, true); - manageHiddenEntryInIndex(game, true); + //manageHiddenEntryInIndex(game, true); manageKidGameEntryInIndex(game, true); } @@ -281,14 +281,19 @@ void FileFilterIndex::debugPrintIndexes() for (auto x: favoritesIndexAllKeys) { LOG(LogInfo) << "Favorites Index: " << x.first << ": " << x.second; } - for (auto x : hiddenIndexAllKeys) { - LOG(LogInfo) << "Hidden Index: " << x.first << ": " << x.second; - } +// for (auto x : hiddenIndexAllKeys) { +// LOG(LogInfo) << "Hidden Index: " << x.first << ": " << x.second; +// } for (auto x : kidGameIndexAllKeys) { LOG(LogInfo) << "KidGames Index: " << x.first << ": " << x.second; } } +void FileFilterIndex::setTextFilter(const std::string text) +{ + mTextFilter = Utils::String::toUpper(text); +} + bool FileFilterIndex::showFile(FileData* game) { // this shouldn't happen, but just in case let's get it out of the way @@ -297,21 +302,23 @@ bool FileFilterIndex::showFile(FileData* game) // if folder, needs further inspection - i.e. see if folder contains at least one element // that should be shown - if (game->getType() == FOLDER) { - std::vector children = game->getChildren(); + if (game->getType() == FOLDER) + { + std::vector children = ((FolderData*) game)->getChildren(); // iterate through all of the children, until there's a match for (std::vector::const_iterator it = children.cbegin(); it != children.cend(); ++it ) { if (showFile(*it)) - { return true; - } } return false; } bool keepGoing = false; + if (!mTextFilter.empty() && Utils::String::toUpper(game->getName()).find(mTextFilter) != std::string::npos) + keepGoing = true; + for (std::vector::const_iterator it = filterDataDecl.cbegin(); it != filterDataDecl.cend(); ++it ) { FilterDataDecl filterData = (*it); if(*(filterData.filteredByRef)) @@ -346,10 +353,10 @@ bool FileFilterIndex::showFile(FileData* game) bool FileFilterIndex::isKeyBeingFilteredBy(std::string key, FilterIndexType type) { - const FilterIndexType filterTypes[7] = { FAVORITES_FILTER, GENRE_FILTER, PLAYER_FILTER, PUBDEV_FILTER, RATINGS_FILTER,HIDDEN_FILTER, KIDGAME_FILTER }; - std::vector filterKeysList[7] = { favoritesIndexFilteredKeys, genreIndexFilteredKeys, playersIndexFilteredKeys, pubDevIndexFilteredKeys, ratingsIndexFilteredKeys, hiddenIndexFilteredKeys, kidGameIndexFilteredKeys }; + const FilterIndexType filterTypes[6] = { FAVORITES_FILTER, GENRE_FILTER, PLAYER_FILTER, PUBDEV_FILTER, RATINGS_FILTER, KIDGAME_FILTER }; // ,HIDDEN_FILTER + std::vector filterKeysList[6] = { favoritesIndexFilteredKeys, genreIndexFilteredKeys, playersIndexFilteredKeys, pubDevIndexFilteredKeys, ratingsIndexFilteredKeys, kidGameIndexFilteredKeys }; // hiddenIndexFilteredKeys, - for (int i = 0; i < 7; i++) + for (int i = 0; i < 6; i++) { if (filterTypes[i] == type) { @@ -471,7 +478,7 @@ void FileFilterIndex::manageFavoritesEntryInIndex(FileData* game, bool remove) manageIndexEntry(&favoritesIndexAllKeys, key, remove); } - +/* void FileFilterIndex::manageHiddenEntryInIndex(FileData* game, bool remove) { // flag for including unknowns @@ -484,7 +491,7 @@ void FileFilterIndex::manageHiddenEntryInIndex(FileData* game, bool remove) manageIndexEntry(&hiddenIndexAllKeys, key, remove); } - +*/ void FileFilterIndex::manageKidGameEntryInIndex(FileData* game, bool remove) { // flag for including unknowns diff --git a/es-app/src/FileFilterIndex.h b/es-app/src/FileFilterIndex.h index 93b9c8d9ff..132a0a533b 100644 --- a/es-app/src/FileFilterIndex.h +++ b/es-app/src/FileFilterIndex.h @@ -42,7 +42,7 @@ class FileFilterIndex void clearAllFilters(); void debugPrintIndexes(); bool showFile(FileData* game); - bool isFiltered() { return (filterByGenre || filterByPlayers || filterByPubDev || filterByRatings || filterByFavorites || filterByHidden || filterByKidGame); }; + bool isFiltered() { return (!mTextFilter.empty() || filterByGenre || filterByPlayers || filterByPubDev || filterByRatings || filterByFavorites || filterByHidden || filterByKidGame); }; bool isKeyBeingFilteredBy(std::string key, FilterIndexType type); std::vector& getFilterDataDecls(); @@ -51,6 +51,9 @@ class FileFilterIndex void resetFilters(); void setUIModeFilters(); + void setTextFilter(const std::string text); + inline const std::string getTextFilter() { return mTextFilter; } + private: std::vector filterDataDecl; std::string getIndexableKey(FileData* game, FilterIndexType type, bool getSecondary); @@ -60,7 +63,7 @@ class FileFilterIndex void managePubDevEntryInIndex(FileData* game, bool remove = false); void manageRatingsEntryInIndex(FileData* game, bool remove = false); void manageFavoritesEntryInIndex(FileData* game, bool remove = false); - void manageHiddenEntryInIndex(FileData* game, bool remove = false); + //void manageHiddenEntryInIndex(FileData* game, bool remove = false); void manageKidGameEntryInIndex(FileData* game, bool remove = false); void manageIndexEntry(std::map* index, std::string key, bool remove); @@ -80,7 +83,7 @@ class FileFilterIndex std::map pubDevIndexAllKeys; std::map ratingsIndexAllKeys; std::map favoritesIndexAllKeys; - std::map hiddenIndexAllKeys; + //std::map hiddenIndexAllKeys; std::map kidGameIndexAllKeys; std::vector genreIndexFilteredKeys; @@ -88,11 +91,11 @@ class FileFilterIndex std::vector pubDevIndexFilteredKeys; std::vector ratingsIndexFilteredKeys; std::vector favoritesIndexFilteredKeys; - std::vector hiddenIndexFilteredKeys; + //std::vector hiddenIndexFilteredKeys; std::vector kidGameIndexFilteredKeys; FileData* mRootFolder; - + std::string mTextFilter; }; #endif // ES_APP_FILE_FILTER_INDEX_H diff --git a/es-app/src/FileSorts.cpp b/es-app/src/FileSorts.cpp index 58b4ba90a1..d82c33cfce 100644 --- a/es-app/src/FileSorts.cpp +++ b/es-app/src/FileSorts.cpp @@ -1,69 +1,103 @@ #include "FileSorts.h" - #include "utils/StringUtil.h" +#include "EsLocale.h" namespace FileSorts { - const FileData::SortType typesArr[] = { - FileData::SortType(&compareName, true, "filename, ascending"), - FileData::SortType(&compareName, false, "filename, descending"), - - FileData::SortType(&compareRating, true, "rating, ascending"), - FileData::SortType(&compareRating, false, "rating, descending"), - - FileData::SortType(&compareTimesPlayed, true, "times played, ascending"), - FileData::SortType(&compareTimesPlayed, false, "times played, descending"), + static Singleton* sInstance = nullptr; - FileData::SortType(&compareLastPlayed, true, "last played, ascending"), - FileData::SortType(&compareLastPlayed, false, "last played, descending"), + Singleton* getInstance() + { + if (sInstance == nullptr) + sInstance = new Singleton(); - FileData::SortType(&compareNumPlayers, true, "number players, ascending"), - FileData::SortType(&compareNumPlayers, false, "number players, descending"), + return sInstance; + } - FileData::SortType(&compareReleaseDate, true, "release date, ascending"), - FileData::SortType(&compareReleaseDate, false, "release date, descending"), + void reset() + { + if (sInstance != nullptr) + delete sInstance; - FileData::SortType(&compareGenre, true, "genre, ascending"), - FileData::SortType(&compareGenre, false, "genre, descending"), + sInstance = nullptr; + } - FileData::SortType(&compareDeveloper, true, "developer, ascending"), - FileData::SortType(&compareDeveloper, false, "developer, descending"), + const std::vector& getSortTypes() + { + return getInstance()->mSortTypes; + } - FileData::SortType(&comparePublisher, true, "publisher, ascending"), - FileData::SortType(&comparePublisher, false, "publisher, descending"), + SortType getSortType(int sortId) + { + for (auto sort : getSortTypes()) + if (sort.id == sortId) + return sort; - FileData::SortType(&compareSystem, true, "system, ascending"), - FileData::SortType(&compareSystem, false, "system, descending") - }; + return getSortTypes().at(0); + } - const std::vector SortTypes(typesArr, typesArr + sizeof(typesArr)/sizeof(typesArr[0])); + Singleton::Singleton() + { + mSortTypes.push_back(SortType(FILENAME_ASCENDING, &compareName, true, _("FILENAME, ASCENDING"), _U("\uF15d "))); + mSortTypes.push_back(SortType(FILENAME_DESCENDING, &compareName, false, _("FILENAME, DESCENDING"), _U("\uF15e "))); + mSortTypes.push_back(SortType(RATING_ASCENDING, &compareRating, true, _("RATING, ASCENDING"), _U("\uF165 "))); + mSortTypes.push_back(SortType(RATING_DESCENDING, &compareRating, false, _("RATING, DESCENDING"), _U("\uF164 "))); + mSortTypes.push_back(SortType(TIMESPLAYED_ASCENDING, &compareTimesPlayed, true, _("TIMES PLAYED, ASCENDING"), _U("\uF160 "))); + mSortTypes.push_back(SortType(TIMESPLAYED_DESCENDING, &compareTimesPlayed, false, _("TIMES PLAYED, DESCENDING"), _U("\uF161 "))); + mSortTypes.push_back(SortType(LASTPLAYED_ASCENDING, &compareLastPlayed, true, _("LAST PLAYED, ASCENDING"), _U("\uF160 "))); + mSortTypes.push_back(SortType(LASTPLAYED_DESCENDING, &compareLastPlayed, false, _("LAST PLAYED, DESCENDING"), _U("\uF161 "))); + mSortTypes.push_back(SortType(NUMBERPLAYERS_ASCENDING, &compareNumPlayers, true, _("NUMBER PLAYERS, ASCENDING"), _U("\uF162 "))); + mSortTypes.push_back(SortType(NUMBERPLAYERS_DESCENDING, &compareNumPlayers, false, _("NUMBER PLAYERS, DESCENDING"), _U("\uF163 "))); + mSortTypes.push_back(SortType(RELEASEDATE_ASCENDING, &compareReleaseDate, true, _("RELEASE DATE, ASCENDING"), _U("\uF160 "))); + mSortTypes.push_back(SortType(RELEASEDATE_DESCENDING, &compareReleaseDate, false, _("RELEASE DATE, DESCENDING"), _U("\uF161 "))); + mSortTypes.push_back(SortType(GENRE_ASCENDING, &compareGenre, true, _("GENRE, ASCENDING"), _U("\uF15d "))); + mSortTypes.push_back(SortType(GENRE_DESCENDING, &compareGenre, false, _("GENRE, DESCENDING"), _U("\uF15e "))); + mSortTypes.push_back(SortType(DEVELOPER_ASCENDING, &compareDeveloper, true, _("DEVELOPER, ASCENDING"), _U("\uF15d "))); + mSortTypes.push_back(SortType(DEVELOPER_DESCENDING, &compareDeveloper, false, _("DEVELOPER, DESCENDING"), _U("\uF15e "))); + mSortTypes.push_back(SortType(PUBLISHER_ASCENDING, &comparePublisher, true, _("PUBLISHER, ASCENDING"), _U("\uF15d "))); + mSortTypes.push_back(SortType(PUBLISHER_DESCENDING, &comparePublisher, false, _("PUBLISHER, DESCENDING"), _U("\uF15e "))); + mSortTypes.push_back(SortType(SYSTEM_ASCENDING, &compareSystem, true, _("SYSTEM, ASCENDING"), _U("\uF15d "))); + mSortTypes.push_back(SortType(SYSTEM_DESCENDING, &compareSystem, false, _("SYSTEM, DESCENDING"), _U("\uF15e "))); + } //returns if file1 should come before file2 bool compareName(const FileData* file1, const FileData* file2) { - // we compare the actual metadata name, as collection files have the system appended which messes up the order - std::string name1 = Utils::String::toUpper(file1->metadata.get("sortname")); - std::string name2 = Utils::String::toUpper(file2->metadata.get("sortname")); - if(name1.empty()){ - name1 = Utils::String::toUpper(file1->metadata.get("name")); - } - if(name2.empty()){ - name2 = Utils::String::toUpper(file2->metadata.get("name")); + if (file1->getType() != file2->getType()) + return file1->getType() == FOLDER; + + // we compare the actual metadata name, as collection files have the system appended which messes up the order + std::string name1 = ((FileData*)file1)->getName(); + std::string name2 = ((FileData*)file2)->getName(); + + for (auto ap = name1.c_str(), bp = name2.c_str(); ; ap++, bp++) + { + if (*ap == 0 & *bp != 0) + return true; + + if (*ap == 0 || *bp == 0) + return false; + + auto c1 = toupper(*ap); + auto c2 = toupper(*bp); + if (c1 != c2) + return c1 < c2; } - return name1.compare(name2) < 0; + + return false; } bool compareRating(const FileData* file1, const FileData* file2) { - return file1->metadata.getFloat("rating") < file2->metadata.getFloat("rating"); + return file1->getMetadata().getFloat("rating") < file2->getMetadata().getFloat("rating"); } bool compareTimesPlayed(const FileData* file1, const FileData* file2) { //only games have playcount metadata - if(file1->metadata.getType() == GAME_METADATA && file2->metadata.getType() == GAME_METADATA) + if (file1->getMetadata().getType() == GAME_METADATA && file2->getMetadata().getType() == GAME_METADATA) { - return (file1)->metadata.getInt("playcount") < (file2)->metadata.getInt("playcount"); + return (file1)->getMetadata().getInt("playcount") < (file2)->getMetadata().getInt("playcount"); } return false; @@ -73,39 +107,39 @@ namespace FileSorts { // since it's stored as an ISO string (YYYYMMDDTHHMMSS), we can compare as a string // as it's a lot faster than the time casts and then time comparisons - return (file1)->metadata.get("lastplayed") < (file2)->metadata.get("lastplayed"); + return (file1)->getMetadata().get("lastplayed") < (file2)->getMetadata().get("lastplayed"); } bool compareNumPlayers(const FileData* file1, const FileData* file2) { - return (file1)->metadata.getInt("players") < (file2)->metadata.getInt("players"); + return (file1)->getMetadata().getInt("players") < (file2)->getMetadata().getInt("players"); } bool compareReleaseDate(const FileData* file1, const FileData* file2) { // since it's stored as an ISO string (YYYYMMDDTHHMMSS), we can compare as a string // as it's a lot faster than the time casts and then time comparisons - return (file1)->metadata.get("releasedate") < (file2)->metadata.get("releasedate"); + return (file1)->getMetadata().get("releasedate") < (file2)->getMetadata().get("releasedate"); } bool compareGenre(const FileData* file1, const FileData* file2) { - std::string genre1 = Utils::String::toUpper(file1->metadata.get("genre")); - std::string genre2 = Utils::String::toUpper(file2->metadata.get("genre")); + std::string genre1 = Utils::String::toUpper(file1->getMetadata().get("genre")); + std::string genre2 = Utils::String::toUpper(file2->getMetadata().get("genre")); return genre1.compare(genre2) < 0; } bool compareDeveloper(const FileData* file1, const FileData* file2) { - std::string developer1 = Utils::String::toUpper(file1->metadata.get("developer")); - std::string developer2 = Utils::String::toUpper(file2->metadata.get("developer")); + std::string developer1 = Utils::String::toUpper(file1->getMetadata().get("developer")); + std::string developer2 = Utils::String::toUpper(file2->getMetadata().get("developer")); return developer1.compare(developer2) < 0; } bool comparePublisher(const FileData* file1, const FileData* file2) { - std::string publisher1 = Utils::String::toUpper(file1->metadata.get("publisher")); - std::string publisher2 = Utils::String::toUpper(file2->metadata.get("publisher")); + std::string publisher1 = Utils::String::toUpper(file1->getMetadata().get("publisher")); + std::string publisher2 = Utils::String::toUpper(file2->getMetadata().get("publisher")); return publisher1.compare(publisher2) < 0; } diff --git a/es-app/src/FileSorts.h b/es-app/src/FileSorts.h index d821ba0f9d..046c622fce 100644 --- a/es-app/src/FileSorts.h +++ b/es-app/src/FileSorts.h @@ -7,6 +7,56 @@ namespace FileSorts { + enum SortId : unsigned int + { + FILENAME_ASCENDING = 0, + FILENAME_DESCENDING = 1, + RATING_ASCENDING = 2, + RATING_DESCENDING = 3, + TIMESPLAYED_ASCENDING = 4, + TIMESPLAYED_DESCENDING = 5, + LASTPLAYED_ASCENDING = 6, + LASTPLAYED_DESCENDING = 7, + NUMBERPLAYERS_ASCENDING = 8, + NUMBERPLAYERS_DESCENDING = 9, + RELEASEDATE_ASCENDING = 10, + RELEASEDATE_DESCENDING = 11, + GENRE_ASCENDING = 12, + GENRE_DESCENDING = 13, + DEVELOPER_ASCENDING = 14, + DEVELOPER_DESCENDING = 15, + PUBLISHER_ASCENDING = 16, + PUBLISHER_DESCENDING = 17, + SYSTEM_ASCENDING = 18, + SYSTEM_DESCENDING = 19 + }; + + typedef bool ComparisonFunction(const FileData* a, const FileData* b); + + struct SortType + { + int id; + ComparisonFunction* comparisonFunction; + bool ascending; + std::string description; + std::string icon; + + SortType(int sortId, ComparisonFunction* sortFunction, bool sortAscending, const std::string & sortDescription, const std::string & iconId = "") + : id(sortId), comparisonFunction(sortFunction), ascending(sortAscending), description(sortDescription), icon(iconId) {} + }; + + class Singleton + { + public: + Singleton(); + + std::vector mSortTypes; + }; + + void reset(); + SortType getSortType(int sortId); + const std::vector& getSortTypes(); + bool compareName(const FileData* file1, const FileData* file2); bool compareRating(const FileData* file1, const FileData* file2); bool compareTimesPlayed(const FileData* file1, const FileData* fil2); @@ -17,8 +67,5 @@ namespace FileSorts bool compareDeveloper(const FileData* file1, const FileData* file2); bool comparePublisher(const FileData* file1, const FileData* file2); bool compareSystem(const FileData* file1, const FileData* file2); - - extern const std::vector SortTypes; }; - #endif // ES_APP_FILE_SORTS_H diff --git a/es-app/src/Gamelist.cpp b/es-app/src/Gamelist.cpp index a6975de2f8..3276aa7021 100644 --- a/es-app/src/Gamelist.cpp +++ b/es-app/src/Gamelist.cpp @@ -1,6 +1,7 @@ #include "Gamelist.h" #include "utils/FileSystemUtil.h" +#include "utils/StringUtil.h" #include "FileData.h" #include "FileFilterIndex.h" #include "Log.h" @@ -8,10 +9,22 @@ #include "SystemData.h" #include -FileData* findOrCreateFile(SystemData* system, const std::string& path, FileType type) +#ifdef WIN32 +#include +#include +#else +#include +#endif + +FileData* findOrCreateFile(SystemData* system, const std::string& path, FileType type, std::unordered_map& fileMap) { + auto pGame = fileMap.find(path); + if (pGame != fileMap.end()) + return pGame->second; + // first, verify that path is within the system's root folder - FileData* root = system->getRootFolder(); + FolderData* root = system->getRootFolder(); + bool contains = false; std::string relative = Utils::FileSystem::removeCommonPath(path, root->getPath(), contains); @@ -23,41 +36,52 @@ FileData* findOrCreateFile(SystemData* system, const std::string& path, FileType Utils::FileSystem::stringList pathList = Utils::FileSystem::getPathList(relative); auto path_it = pathList.begin(); - FileData* treeNode = root; - bool found = false; + FolderData* treeNode = root; + + // bool found = false; while(path_it != pathList.end()) { - const std::unordered_map& children = treeNode->getChildrenByFilename(); - - std::string key = *path_it; - found = children.find(key) != children.cend(); - if (found) { - treeNode = children.at(key); + std::string key = Utils::FileSystem::combine(treeNode->getPath(), *path_it); + FileData* item = (fileMap.find(key) != fileMap.end()) ? fileMap[key] : nullptr; + if (item != nullptr) + { + if (item->getType() == FOLDER) + treeNode = (FolderData*) item; + else + return item; } - + // this is the end if(path_it == --pathList.end()) { - if(found) - return treeNode; - - if(type == FOLDER) + if (type == FOLDER) { - LOG(LogWarning) << "gameList: folder doesn't already exist, won't create"; return NULL; + LOG(LogWarning) << "gameList: folder doesn't already exist, won't create"; } - FileData* file = new FileData(type, path, system->getSystemEnvData(), system); - - // skipping arcade assets from gamelist - if(!file->isArcadeAsset()) + if (type == GAME) // Final file { - treeNode->addChild(file); + // Skip if the extension in the gamelist is unknown + if (!system->getSystemEnvData()->isValidExtension(Utils::String::toLower(Utils::FileSystem::getExtension(path)))) + { + LOG(LogWarning) << "gameList: file extension is not known by systemlist"; + return NULL; + } + + // Add final game + item = new FileData(GAME, path, system); + if (!item->isArcadeAsset()) + { + fileMap[key] = item; + treeNode->addChild(item); + } + + return item; } - return file; } - - if(!found) + + if (item == nullptr) { // don't create folders unless it's leading up to a game // if type is a folder it's gonna be empty, so don't bother @@ -68,89 +92,145 @@ FileData* findOrCreateFile(SystemData* system, const std::string& path, FileType } // create missing folder - FileData* folder = new FileData(FOLDER, Utils::FileSystem::getStem(treeNode->getPath()) + "/" + *path_it, system->getSystemEnvData(), system); + FolderData* folder = new FolderData(Utils::FileSystem::getStem(treeNode->getPath()) + "/" + *path_it, system); + fileMap[key] = folder; treeNode->addChild(folder); treeNode = folder; } - + path_it++; } return NULL; } -void parseGamelist(SystemData* system) -{ +void loadGamelistFile (const std::string xmlpath, SystemData* system, std::unordered_map& fileMap, size_t checkSize = SIZE_MAX) +{ bool trustGamelist = Settings::getInstance()->getBool("ParseGamelistOnly"); - std::string xmlpath = system->getGamelistPath(false); - - if(!Utils::FileSystem::exists(xmlpath)) - return; LOG(LogInfo) << "Parsing XML file \"" << xmlpath << "\"..."; pugi::xml_document doc; pugi::xml_parse_result result = doc.load_file(xmlpath.c_str()); - if(!result) + if (!result) { LOG(LogError) << "Error parsing XML file \"" << xmlpath << "\"!\n " << result.description(); return; } pugi::xml_node root = doc.child("gameList"); - if(!root) + if (!root) { LOG(LogError) << "Could not find node in gamelist \"" << xmlpath << "\"!"; return; } + if (checkSize != SIZE_MAX) + { + auto parentSize = root.attribute("parentHash").as_uint(); + if (parentSize != checkSize) + { + LOG(LogWarning) << "gamelist size don't match !"; + return; + } + } + std::string relativeTo = system->getStartPath(); - const char* tagList[2] = { "game", "folder" }; - FileType typeList[2] = { GAME, FOLDER }; - for(int i = 0; i < 2; i++) + for (pugi::xml_node fileNode : root.children()) { - const char* tag = tagList[i]; - FileType type = typeList[i]; - for(pugi::xml_node fileNode = root.child(tag); fileNode; fileNode = fileNode.next_sibling(tag)) + FileType type = GAME; + + std::string tag = fileNode.name(); + + if (tag == "folder") + type = FOLDER; + else if (tag != "game") + continue; + + const std::string path = Utils::FileSystem::resolveRelativePath(fileNode.child("path").text().get(), relativeTo, false); + if (!trustGamelist && !Utils::FileSystem::exists(path)) { - const std::string path = Utils::FileSystem::resolveRelativePath(fileNode.child("path").text().get(), relativeTo, false); + LOG(LogWarning) << "File \"" << path << "\" does not exist! Ignoring."; + continue; + } - if(!trustGamelist && !Utils::FileSystem::exists(path)) - { - LOG(LogWarning) << "File \"" << path << "\" does not exist! Ignoring."; - continue; - } + FileData* file = findOrCreateFile(system, path, type, fileMap); + if (!file) + { + LOG(LogError) << "Error finding/creating FileData for \"" << path << "\", skipping."; + continue; + } + else if (!file->isArcadeAsset()) + { + std::string defaultName = file->getMetadata().get("name"); + file->setMetadata(MetaDataList::createFromXML(type == FOLDER ? FOLDER_METADATA : GAME_METADATA, fileNode, system)); - FileData* file = findOrCreateFile(system, path, type); - if(!file) - { - LOG(LogError) << "Error finding/creating FileData for \"" << path << "\", skipping."; - continue; - } - else if(!file->isArcadeAsset()) - { - std::string defaultName = file->metadata.get("name"); - file->metadata = MetaDataList::createFromXML(GAME_METADATA, fileNode, relativeTo); + //make sure name gets set if one didn't exist + if (file->getMetadata().get("name").empty()) + file->setMetadata("name", defaultName); - //make sure name gets set if one didn't exist - if(file->metadata.get("name").empty()) - file->metadata.set("name", defaultName); + if (!file->getHidden() && Utils::FileSystem::isHidden(path)) + file->getMetadata().set("hidden", "true"); - file->metadata.resetChangedFlag(); - } + if (checkSize != SIZE_MAX) + file->getMetadata().setDirty(); + else + file->getMetadata().resetChangedFlag(); } } } -void addFileDataNode(pugi::xml_node& parent, const FileData* file, const char* tag, SystemData* system) +std::string getTemporaryGamelistRecovery(SystemData* system) +{ + return Utils::FileSystem::getHomePath() + "/.emulationstation/recovery/" + system->getName(); +} + +void clearTemporaryGamelistRecovery(SystemData* system) +{ + auto path = getTemporaryGamelistRecovery(system); + + auto files = Utils::FileSystem::getDirContent(path, true, false); + if (files.size() > 0) + { + for (auto file : files) + if (!Utils::FileSystem::isDirectory(file)) + Utils::FileSystem::removeFile(file); + + std::reverse(std::begin(files), std::end(files)); + + for (auto file : files) + if (Utils::FileSystem::isDirectory(file)) + rmdir(file.c_str()); + } + + rmdir(path.c_str()); +} + +void parseGamelist(SystemData* system, std::unordered_map& fileMap) +{ + std::string xmlpath = system->getGamelistPath(false); + + auto size = Utils::FileSystem::getFileSize(xmlpath); + if (size != 0) + loadGamelistFile(xmlpath, system, fileMap); + + auto files = Utils::FileSystem::getDirContent(getTemporaryGamelistRecovery(system), true, false); + for (auto file : files) + loadGamelistFile(file, system, fileMap, size); + + if (size != SIZE_MAX) + system->setGamelistHash(size); +} + +bool addFileDataNode(pugi::xml_node& parent, const FileData* file, const char* tag, SystemData* system) { //create game and add to parent node pugi::xml_node newNode = parent.append_child(tag); //write metadata - file->metadata.appendToXML(newNode, true, system->getStartPath()); + file->getMetadata().appendToXML(newNode, true, system->getStartPath()); if(newNode.children().begin() == newNode.child("name") //first element is name && ++newNode.children().begin() == newNode.children().end() //theres only one element @@ -159,12 +239,68 @@ void addFileDataNode(pugi::xml_node& parent, const FileData* file, const char* t //if the only info is the default name, don't bother with this node //delete it and ultimately do nothing parent.remove_child(newNode); - }else{ - //there's something useful in there so we'll keep the node, add the path + return false; + } + + //there's something useful in there so we'll keep the node, add the path + // try and make the path relative if we can so things still work if we change the rom folder location in the future + newNode.prepend_child("path").text().set(Utils::FileSystem::createRelativePath(file->getPath(), system->getStartPath(), false).c_str()); + return true; +} + +bool saveToGamelistRecovery(FileData* file) +{ + if (!Settings::getInstance()->getBool("SaveGamelistsOnExit")) + return false; + + pugi::xml_document doc; + pugi::xml_node root = doc.append_child("gameList"); + + const char* tag = file->getType() == GAME ? "game" : "folder"; + + SystemData* system = file->getSourceFileData()->getSystem(); + root.append_attribute("parentHash").set_value(system->getGamelistHash()); + + if (addFileDataNode(root, file, tag, system)) + { + std::string fp = file->getFullPath(); + fp = Utils::FileSystem::createRelativePath(file->getFullPath(), system->getRootFolder()->getFullPath(), true); + fp = Utils::FileSystem::getParent(fp) + "/" + Utils::FileSystem::getStem(fp) + ".xml"; + + std::string path = Utils::FileSystem::getAbsolutePath(fp, getTemporaryGamelistRecovery(system)); + path = Utils::FileSystem::getCanonicalPath(path); + + std::string folder = Utils::FileSystem::getParent(path); + + if (!Utils::FileSystem::exists(folder)) + Utils::FileSystem::createDirectory(folder); + + if (!doc.save_file(path.c_str())) + { + LOG(LogError) << "Error saving gamelist.xml to \"" << path << "\" (for system " << system->getName() << ")!"; + return false; + } - // try and make the path relative if we can so things still work if we change the rom folder location in the future - newNode.prepend_child("path").text().set(Utils::FileSystem::createRelativePath(file->getPath(), system->getStartPath(), false).c_str()); + return true; } + + return false; +} + +bool hasDirtyFile(SystemData* system) +{ + if (system == nullptr || !system->isGameSystem() || system->getName() == "imageviewer") + return false; + + FolderData* rootFolder = system->getRootFolder(); + if (rootFolder == nullptr) + return false; + + for (auto file : rootFolder->getFilesRecursive(GAME | FOLDER)) + if (file->getMetadata().wasChanged()) + return true; + + return false; } void updateGamelist(SystemData* system) @@ -174,93 +310,140 @@ void updateGamelist(SystemData* system) //We have the complete information for every game though, so we can simply remove a game //we already have in the system from the XML, and then add it back from its GameData information... - if(Settings::getInstance()->getBool("IgnoreGamelist")) + if(system == nullptr || Settings::getInstance()->getBool("IgnoreGamelist")) + return; + + if (system->getName() == "imageviewer" || system->isCollection() || !system->isGameSystem()) return; + + FolderData* rootFolder = system->getRootFolder(); + if (rootFolder == nullptr) + { + LOG(LogError) << "Found no root folder for system \"" << system->getName() << "\"!"; + return; + } + + std::vector dirtyFiles; + std::vector files = rootFolder->getFilesRecursive(GAME | FOLDER); + for (auto file : files) + if (file->getMetadata().wasChanged()) + dirtyFiles.push_back(file); + + if (dirtyFiles.size() == 0) + { + clearTemporaryGamelistRecovery(system); + return; + } + + int numUpdated = 0; pugi::xml_document doc; pugi::xml_node root; std::string xmlReadPath = system->getGamelistPath(false); - if(Utils::FileSystem::exists(xmlReadPath)) + if (Utils::FileSystem::exists(xmlReadPath)) { //parse an existing file first pugi::xml_parse_result result = doc.load_file(xmlReadPath.c_str()); - if(!result) - { LOG(LogError) << "Error parsing XML file \"" << xmlReadPath << "\"!\n " << result.description(); - return; - } - root = doc.child("gameList"); + root = doc.child("gameList"); if(!root) { LOG(LogError) << "Could not find node in gamelist \"" << xmlReadPath << "\"!"; - return; + root = doc.append_child("gameList"); } }else{ //set up an empty gamelist to append to root = doc.append_child("gameList"); } + std::map xmlMap; - //now we have all the information from the XML. now iterate through all our games and add information from there - FileData* rootFolder = system->getRootFolder(); - if (rootFolder != nullptr) + for (pugi::xml_node fileNode : root.children()) + { + pugi::xml_node path = fileNode.child("path"); + if (path) + { + std::string nodePath = Utils::FileSystem::getCanonicalPath(Utils::FileSystem::resolveRelativePath(path.text().get(), system->getStartPath(), true)); + xmlMap[nodePath] = fileNode; + } + } + + // iterate through all files, checking if they're already in the XML + for(auto file : dirtyFiles) { - int numUpdated = 0; + bool removed = false; - //get only files, no folders - std::vector files = rootFolder->getFilesRecursive(GAME | FOLDER); - //iterate through all files, checking if they're already in the XML - for(std::vector::const_iterator fit = files.cbegin(); fit != files.cend(); ++fit) + // check if the file already exists in the XML + // if it does, remove it before adding + auto xmf = xmlMap.find(Utils::FileSystem::getCanonicalPath(file->getPath())); + if (xmf != xmlMap.cend()) { - const char* tag = ((*fit)->getType() == GAME) ? "game" : "folder"; + removed = true; + root.remove_child(xmf->second); + } + + const char* tag = (file->getType() == GAME) ? "game" : "folder"; + + // it was either removed or never existed to begin with; either way, we can add it now + if (addFileDataNode(root, file, tag, system)) + ++numUpdated; // Only if really added + else if (removed) + ++numUpdated; // Only if really removed + } - // do not touch if it wasn't changed anyway - if (!(*fit)->metadata.wasChanged()) - continue; + // Now write the file + if (numUpdated > 0) + { + //make sure the folders leading up to this path exist (or the write will fail) + std::string xmlWritePath(system->getGamelistPath(true)); + Utils::FileSystem::createDirectory(Utils::FileSystem::getParent(xmlWritePath)); - // check if the file already exists in the XML - // if it does, remove it before adding - for(pugi::xml_node fileNode = root.child(tag); fileNode; fileNode = fileNode.next_sibling(tag)) - { - pugi::xml_node pathNode = fileNode.child("path"); - if(!pathNode) - { - LOG(LogError) << "<" << tag << "> node contains no child!"; - continue; - } + LOG(LogInfo) << "Added/Updated " << numUpdated << " entities in '" << xmlReadPath << "'"; - std::string nodePath = Utils::FileSystem::getCanonicalPath(Utils::FileSystem::resolveRelativePath(pathNode.text().get(), system->getStartPath(), true)); - std::string gamePath = Utils::FileSystem::getCanonicalPath((*fit)->getPath()); - if(nodePath == gamePath) - { - // found it - root.remove_child(fileNode); - break; - } - } + // Secure XML writing -> Write to a temporary file first + std::string tmpFile = xmlWritePath + ".tmp"; + if (Utils::FileSystem::exists(tmpFile)) + Utils::FileSystem::removeFile(tmpFile); - // it was either removed or never existed to begin with; either way, we can add it now - addFileDataNode(root, *fit, tag, system); - ++numUpdated; + if (!doc.save_file(tmpFile.c_str())) { + LOG(LogError) << "Error saving gamelist.xml to \"" << xmlWritePath << "\" (for system " << system->getName() << ")!"; } + else if (Utils::FileSystem::exists(tmpFile)) + { + doc.reset(); - //now write the file +#ifdef WIN32 + ::Sleep(50); // Introduce a small sleep +#endif + + // Secure XML writing + if (Utils::FileSystem::getFileSize(tmpFile) != 0) + { + std::string savFile = xmlWritePath + ".old"; - if (numUpdated > 0) { - //make sure the folders leading up to this path exist (or the write will fail) - std::string xmlWritePath(system->getGamelistPath(true)); - Utils::FileSystem::createDirectory(Utils::FileSystem::getParent(xmlWritePath)); + // remove previous gamelist.xml.old file + if (Utils::FileSystem::exists(savFile)) + Utils::FileSystem::removeFile(savFile); - LOG(LogInfo) << "Added/Updated " << numUpdated << " entities in '" << xmlReadPath << "'"; + // rename gamelist.xml to gamelist.xml.old + if (Utils::FileSystem::exists(xmlWritePath)) + std::rename(xmlWritePath.c_str(), savFile.c_str()); + else + LOG(LogError) << "Unable to rename \"" << xmlWritePath << "to " << savFile << "\"!"; - if (!doc.save_file(xmlWritePath.c_str())) { - LOG(LogError) << "Error saving gamelist.xml to \"" << xmlWritePath << "\" (for system " << system->getName() << ")!"; + // rename gamelist.tmp.xml to gamelist.xml + if (std::rename(tmpFile.c_str(), xmlWritePath.c_str()) != 0) + LOG(LogError) << "Unable to rename \"" << tmpFile << "to " << xmlWritePath << "\"!"; + + clearTemporaryGamelistRecovery(system); } + else + Utils::FileSystem::removeFile(tmpFile); } - }else{ - LOG(LogError) << "Found no root folder for system \"" << system->getName() << "\"!"; } + else + clearTemporaryGamelistRecovery(system); } diff --git a/es-app/src/Gamelist.h b/es-app/src/Gamelist.h index d9502a196d..df7021ea68 100644 --- a/es-app/src/Gamelist.h +++ b/es-app/src/Gamelist.h @@ -2,12 +2,18 @@ #ifndef ES_APP_GAME_LIST_H #define ES_APP_GAME_LIST_H +#include + class SystemData; +class FileData; // Loads gamelist.xml data into a SystemData. -void parseGamelist(SystemData* system); +void parseGamelist(SystemData* system, std::unordered_map& fileMap); // Writes currently loaded metadata for a SystemData to gamelist.xml. void updateGamelist(SystemData* system); +bool saveToGamelistRecovery(FileData* file); +bool hasDirtyFile(SystemData* system); + #endif // ES_APP_GAME_LIST_H diff --git a/es-app/src/MetaData.cpp b/es-app/src/MetaData.cpp index f5d466b09e..54c87617a9 100644 --- a/es-app/src/MetaData.cpp +++ b/es-app/src/MetaData.cpp @@ -1,49 +1,117 @@ #include "MetaData.h" #include "utils/FileSystemUtil.h" +#include "utils/StringUtil.h" #include "Log.h" #include +#include "SystemData.h" +#include "Settings.h" MetaDataDecl gameDecls[] = { // key, type, default, statistic, name in GuiMetaDataEd, prompt in GuiMetaDataEd - {"name", MD_STRING, "", false, "name", "enter game name"}, - {"sortname", MD_STRING, "", false, "sortname", "enter game sort name"}, - {"desc", MD_MULTILINE_STRING, "", false, "description", "enter description"}, - {"image", MD_PATH, "", false, "image", "enter path to image"}, - {"video", MD_PATH , "", false, "video", "enter path to video"}, - {"marquee", MD_PATH, "", false, "marquee", "enter path to marquee"}, - {"thumbnail", MD_PATH, "", false, "thumbnail", "enter path to thumbnail"}, - {"rating", MD_RATING, "0.000000", false, "rating", "enter rating"}, - {"releasedate", MD_DATE, "not-a-date-time", false, "release date", "enter release date"}, - {"developer", MD_STRING, "unknown", false, "developer", "enter game developer"}, - {"publisher", MD_STRING, "unknown", false, "publisher", "enter game publisher"}, - {"genre", MD_STRING, "unknown", false, "genre", "enter game genre"}, - {"players", MD_INT, "1", false, "players", "enter number of players"}, - {"favorite", MD_BOOL, "false", false, "favorite", "enter favorite off/on"}, - {"hidden", MD_BOOL, "false", false, "hidden", "enter hidden off/on" }, - {"kidgame", MD_BOOL, "false", false, "kidgame", "enter kidgame off/on" }, - {"playcount", MD_INT, "0", true, "play count", "enter number of times played"}, - {"lastplayed", MD_TIME, "0", true, "last played", "enter last played date"} + { 0, "name", MD_STRING, "", false, "name", "enter game name"}, +// { 1, "sortname", MD_STRING, "", false, "sortname", "enter game sort name"}, + { 2, "desc", MD_MULTILINE_STRING, "", false, "description", "enter description"}, + { 3, "emulator", MD_PLIST, "", false, "emulator", "emulator" }, + { 4, "core", MD_PLIST, "", false, "core", "core" }, + { 5, "image", MD_PATH, "", false, "image", "enter path to image"}, + { 6, "video", MD_PATH , "", false, "video", "enter path to video"}, + { 7, "marquee", MD_PATH, "", false, "marquee", "enter path to marquee"}, + { 8, "thumbnail", MD_PATH, "", false, "thumbnail", "enter path to thumbnail"}, + { 9, "rating", MD_RATING, "0.000000", false, "rating", "enter rating"}, + { 10, "releasedate", MD_DATE, "not-a-date-time", false, "release date", "enter release date"}, + { 11, "developer", MD_STRING, "unknown", false, "developer", "enter game developer"}, + { 12, "publisher", MD_STRING, "unknown", false, "publisher", "enter game publisher"}, + { 13, "genre", MD_STRING, "unknown", false, "genre", "enter game genre"}, + { 14, "players", MD_INT, "1", false, "players", "enter number of players"}, + { 15, "favorite", MD_BOOL, "false", false, "favorite", "enter favorite off/on"}, + { 16, "hidden", MD_BOOL, "false", false, "hidden", "enter hidden off/on" }, + { 17, "kidgame", MD_BOOL, "false", false, "kidgame", "enter kidgame off/on" }, + { 18, "playcount", MD_INT, "0", true, "play count", "enter number of times played"}, + { 19, "lastplayed", MD_TIME, "0", true, "last played", "enter last played date"}, + { 20, "arcadesystemname", MD_STRING, "", false, "arcade system", "enter arcade system name"} }; + const std::vector gameMDD(gameDecls, gameDecls + sizeof(gameDecls) / sizeof(gameDecls[0])); MetaDataDecl folderDecls[] = { - {"name", MD_STRING, "", false, "name", "enter game name"}, - {"sortname", MD_STRING, "", false, "sortname", "enter game sort name"}, - {"desc", MD_MULTILINE_STRING, "", false, "description", "enter description"}, - {"image", MD_PATH, "", false, "image", "enter path to image"}, - {"thumbnail", MD_PATH, "", false, "thumbnail", "enter path to thumbnail"}, - {"video", MD_PATH, "", false, "video", "enter path to video"}, - {"marquee", MD_PATH, "", false, "marquee", "enter path to marquee"}, - {"rating", MD_RATING, "0.000000", false, "rating", "enter rating"}, - {"releasedate", MD_DATE, "not-a-date-time", false, "release date", "enter release date"}, - {"developer", MD_STRING, "unknown", false, "developer", "enter game developer"}, - {"publisher", MD_STRING, "unknown", false, "publisher", "enter game publisher"}, - {"genre", MD_STRING, "unknown", false, "genre", "enter game genre"}, - {"players", MD_INT, "1", false, "players", "enter number of players"} + { 0, "name", MD_STRING, "", false, "name", "enter game name"}, +// { 1, "sortname", MD_STRING, "", false, "sortname", "enter game sort name"}, + { 2, "desc", MD_MULTILINE_STRING, "", false, "description", "enter description"}, + { 3, "image", MD_PATH, "", false, "image", "enter path to image"}, + { 4, "thumbnail", MD_PATH, "", false, "thumbnail", "enter path to thumbnail"}, + { 5, "video", MD_PATH, "", false, "video", "enter path to video"}, + { 6, "marquee", MD_PATH, "", false, "marquee", "enter path to marquee"}, + { 7, "rating", MD_RATING, "0.000000", false, "rating", "enter rating"}, + { 8, "releasedate", MD_DATE, "not-a-date-time", false, "release date", "enter release date"}, + { 9, "developer", MD_STRING, "unknown", false, "developer", "enter game developer"}, + { 10, "publisher", MD_STRING, "unknown", false, "publisher", "enter game publisher"}, + { 11, "genre", MD_STRING, "unknown", false, "genre", "enter game genre"}, + { 12, "players", MD_INT, "1", false, "players", "enter number of players"}, + { 13, "favorite", MD_BOOL, "false", false, "favorite", "enter favorite off/on" }, + { 14, "hidden", MD_BOOL, "false", false, "hidden", "enter hidden off/on" }, }; + const std::vector folderMDD(folderDecls, folderDecls + sizeof(folderDecls) / sizeof(folderDecls[0])); +std::map MetaDataList::mGameIdMap = MetaDataList::BuildIdMap(GAME_METADATA); +std::map MetaDataList::mFolderIdMap = MetaDataList::BuildIdMap(FOLDER_METADATA); + +std::map MetaDataList::mGameTypeMap = MetaDataList::BuildTypeMap(GAME_METADATA); +std::map MetaDataList::mFolderTypeMap = MetaDataList::BuildTypeMap(FOLDER_METADATA); + +std::map MetaDataList::mDefaultGameMap = MetaDataList::BuildDefaultMap(GAME_METADATA); +std::map MetaDataList::mDefaultFolderMap = MetaDataList::BuildDefaultMap(FOLDER_METADATA); + +std::map MetaDataList::BuildTypeMap(MetaDataListType type) +{ + std::map ret; + + const std::vector& mdd = getMDDByType(type); + for (auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) + ret[iter->id] = iter->type; + + return ret; +} + +std::map MetaDataList::BuildIdMap(MetaDataListType type) +{ + std::map ret; + + const std::vector& mdd = getMDDByType(type); + for (auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) + ret[iter->key] = iter->id; + + return ret; +} + +std::map MetaDataList::BuildDefaultMap(MetaDataListType type) +{ + std::map ret; + + const std::vector& mdd = getMDDByType(type); + for (auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) + ret[iter->id] = iter->defaultValue; + + return ret; +} + +MetaDataType MetaDataList::getType(unsigned char id) const +{ + if (mType == GAME_METADATA) + return mGameTypeMap[id]; + + return mFolderTypeMap[id]; +} + +unsigned char MetaDataList::getId(const std::string& key) const +{ + if (mType == GAME_METADATA) + return mGameIdMap[key]; + + return mFolderIdMap[key]; +} + const std::vector& getMDDByType(MetaDataListType type) { switch(type) @@ -58,37 +126,44 @@ const std::vector& getMDDByType(MetaDataListType type) return gameMDD; } +MetaDataList::MetaDataList(MetaDataListType type) : mType(type), mWasChanged(false), mRelativeTo(nullptr) +{ - -MetaDataList::MetaDataList(MetaDataListType type) - : mType(type), mWasChanged(false) -{ - const std::vector& mdd = getMDD(); - for(auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) - set(iter->key, iter->defaultValue); } - -MetaDataList MetaDataList::createFromXML(MetaDataListType type, pugi::xml_node& node, const std::string& relativeTo) +MetaDataList MetaDataList::createFromXML(MetaDataListType type, pugi::xml_node& node, SystemData* system) { MetaDataList mdl(type); + mdl.mRelativeTo = system; + + auto sz = sizeof(MetaDataList); const std::vector& mdd = mdl.getMDD(); for(auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) { pugi::xml_node md = node.child(iter->key.c_str()); - if(md) - { - // if it's a path, resolve relative paths + if (md) + { std::string value = md.text().get(); - if (iter->type == MD_PATH) - { - value = Utils::FileSystem::resolveRelativePath(value, relativeTo, true); - } - mdl.set(iter->key, value); - }else{ - mdl.set(iter->key, iter->defaultValue); + + // if (iter->type == MD_PATH) // if it's a path, resolve relative paths + // value = Utils::FileSystem::resolveRelativePath(value, relativeTo, true); + + if (value == iter->defaultValue) + continue; + + if (iter->type == MD_BOOL) + value = Utils::String::toLower(value); + + // Players -> remove "1-" + if (type == GAME_METADATA && iter->id == 14 && iter->type == MD_INT && Utils::String::startsWith(value, "1-")) // "players" + value = Utils::String::replace(value, "1-", ""); + + if (iter->id == 0) + mdl.mName = value; + else + mdl.mMap[iter->id] = value; } } @@ -101,33 +176,85 @@ void MetaDataList::appendToXML(pugi::xml_node& parent, bool ignoreDefaults, cons for(auto mddIter = mdd.cbegin(); mddIter != mdd.cend(); mddIter++) { - auto mapIter = mMap.find(mddIter->key); - if(mapIter != mMap.cend()) + if (mddIter->id == 0) + { + parent.append_child("name").text().set(mName.c_str()); + continue; + } + + auto mapIter = mMap.find(mddIter->id); + if (mapIter != mMap.cend()) { // we have this value! // if it's just the default (and we ignore defaults), don't write it - if(ignoreDefaults && mapIter->second == mddIter->defaultValue) + if (ignoreDefaults && mapIter->second == mddIter->defaultValue) continue; - + // try and make paths relative if we can std::string value = mapIter->second; if (mddIter->type == MD_PATH) value = Utils::FileSystem::createRelativePath(value, relativeTo, true); - parent.append_child(mapIter->first.c_str()).text().set(value.c_str()); + parent.append_child(mddIter->key.c_str()).text().set(value.c_str()); // mapIter->first } } } +const std::string& MetaDataList::getName() const +{ + return mName; +} + void MetaDataList::set(const std::string& key, const std::string& value) { - mMap[key] = value; + if (key == "name") + { + if (mName == value) + return; + + mName = value; + } + else + { + auto id = getId(key); + + // Players -> remove "1-" + if (mType == GAME_METADATA && id == 14 && Utils::String::startsWith(value, "1-")) // "players" + { + mMap[id] = Utils::String::replace(value, "1-", ""); + return; + } + + auto prev = mMap.find(id); + if (prev != mMap.cend() && prev->second == value) + return; + + mMap[id] = value; + } + mWasChanged = true; } -const std::string& MetaDataList::get(const std::string& key) const +const std::string MetaDataList::get(const std::string& key) const { - return mMap.at(key); + if (key == "name") + return mName; + + auto id = getId(key); + + auto it = mMap.find(id); + if (it != mMap.end()) + { + if (getType(id) == MD_PATH && mRelativeTo != nullptr) // if it's a path, resolve relative paths + return Utils::FileSystem::resolveRelativePath(it->second, mRelativeTo->getStartPath(), true); + + return it->second; + } + + if (mType == GAME_METADATA) + return mDefaultGameMap.at(id); + + return mDefaultFolderMap.at(id); } int MetaDataList::getInt(const std::string& key) const @@ -149,3 +276,43 @@ void MetaDataList::resetChangedFlag() { mWasChanged = false; } + +void MetaDataList::importScrappedMetadata(const MetaDataList& source) +{ + int type = MetaDataImportType::Types::ALL; + + if (Settings::getInstance()->getString("Scraper") == "ScreenScraper") + { + if (Settings::getInstance()->getString("ScrapperImageSrc").empty()) + type &= ~MetaDataImportType::Types::IMAGE; + + if (Settings::getInstance()->getString("ScrapperThumbSrc").empty()) + type &= ~MetaDataImportType::Types::THUMB; + + if (Settings::getInstance()->getString("ScrapperLogoSrc").empty()) + type &= ~MetaDataImportType::Types::MARQUEE; + + if (!Settings::getInstance()->getBool("ScrapeVideos")) + type &= ~MetaDataImportType::Types::VIDEO; + } + + for (auto mdd : getMDD()) + { + if (mdd.key == "favorite" || mdd.key == "playcount" || mdd.key == "lastplayed") + continue; + + if (mdd.key == "image" && (type & MetaDataImportType::Types::IMAGE) != MetaDataImportType::Types::IMAGE) + continue; + + if (mdd.key == "thumbnail" && (type & MetaDataImportType::Types::THUMB) != MetaDataImportType::Types::THUMB) + continue; + + if (mdd.key == "marquee" && (type & MetaDataImportType::Types::MARQUEE) != MetaDataImportType::Types::MARQUEE) + continue; + + if (mdd.key == "video" && (type & MetaDataImportType::Types::VIDEO) != MetaDataImportType::Types::VIDEO) + continue; + + set(mdd.key, source.get(mdd.key)); + } +} \ No newline at end of file diff --git a/es-app/src/MetaData.h b/es-app/src/MetaData.h index 29154b4ee7..3c3026d83b 100644 --- a/es-app/src/MetaData.h +++ b/es-app/src/MetaData.h @@ -5,6 +5,8 @@ #include #include +class SystemData; + namespace pugi { class xml_node; } enum MetaDataType @@ -20,11 +22,26 @@ enum MetaDataType MD_PATH, MD_RATING, MD_DATE, - MD_TIME //used for lastplayed + MD_TIME, //used for lastplayed + MD_PLIST }; +namespace MetaDataImportType +{ + enum Types : int + { + IMAGE = 1, + THUMB = 2, + VIDEO = 4, + MARQUEE = 8, + ALL = IMAGE | THUMB | VIDEO | MARQUEE + }; +} + struct MetaDataDecl { + unsigned char id; + std::string key; MetaDataType type; std::string defaultValue; @@ -44,27 +61,54 @@ const std::vector& getMDDByType(MetaDataListType type); class MetaDataList { public: - static MetaDataList createFromXML(MetaDataListType type, pugi::xml_node& node, const std::string& relativeTo); + static MetaDataList createFromXML(MetaDataListType type, pugi::xml_node& node, SystemData* system); void appendToXML(pugi::xml_node& parent, bool ignoreDefaults, const std::string& relativeTo) const; MetaDataList(MetaDataListType type); void set(const std::string& key, const std::string& value); - const std::string& get(const std::string& key) const; + const std::string get(const std::string& key) const; int getInt(const std::string& key) const; float getFloat(const std::string& key) const; bool wasChanged() const; void resetChangedFlag(); + void setDirty() { mWasChanged = true; } - inline MetaDataListType getType() const { return mType; } + inline MetaDataListType getType() const { return (MetaDataListType) mType; } inline const std::vector& getMDD() const { return getMDDByType(getType()); } + const std::string& getName() const; + + void importScrappedMetadata(const MetaDataList& source); private: - MetaDataListType mType; - std::map mMap; - bool mWasChanged; + std::string mName; + unsigned char mType; + bool mWasChanged; + SystemData* mRelativeTo; + + std::map mMap; + + unsigned char getId(const std::string& key) const; + MetaDataType getType(unsigned char id) const; + + +private: // Static maps + + static std::map mDefaultGameMap; + static std::map mDefaultFolderMap; + + static std::map mGameIdMap; + static std::map mFolderIdMap; + + static std::map mGameTypeMap; + static std::map mFolderTypeMap; + + static std::map BuildDefaultMap(MetaDataListType type); + static std::map BuildIdMap(MetaDataListType type); + static std::map BuildTypeMap(MetaDataListType type); + }; #endif // ES_APP_META_DATA_H diff --git a/es-app/src/NetworkThread.cpp b/es-app/src/NetworkThread.cpp new file mode 100644 index 0000000000..2812ad54ba --- /dev/null +++ b/es-app/src/NetworkThread.cpp @@ -0,0 +1,55 @@ +#include "NetworkThread.h" +#include "ApiSystem.h" +#include "guis/GuiMsgBox.h" +#include "Log.h" +#include +#include + +NetworkThread::NetworkThread(Window* window) : mWindow(window) +{ + LOG(LogDebug) << "NetworkThread : Starting"; + + // creer le thread + mFirstRun = true; + mRunning = true; + mThread = new std::thread(&NetworkThread::run, this); +} + +NetworkThread::~NetworkThread() +{ + LOG(LogDebug) << "NetworkThread : Exit"; + + mRunning = false; + mThread->join(); + delete mThread; +} + +void NetworkThread::run() +{ + while (mRunning) + { + if (mFirstRun) + { + std::this_thread::sleep_for(std::chrono::seconds(5)); + mFirstRun = false; + } + else + std::this_thread::sleep_for(std::chrono::hours(1)); + + if (Settings::getInstance()->getBool("updates.enabled")) + { + LOG(LogDebug) << "NetworkThread : Checking for updates"; + + std::string version = ApiSystem::checkUpdateVersion(); + if (!version.empty()) + { + mWindow->displayNotificationMessage(_U("\uF019 ") + _("UPDATE AVAILABLE") + std::string(": ") + version); + mRunning = false; + } + else + { + LOG(LogDebug) << "NetworkThread : No update found"; + } + } + } +} \ No newline at end of file diff --git a/es-app/src/NetworkThread.h b/es-app/src/NetworkThread.h new file mode 100644 index 0000000000..35acc17edd --- /dev/null +++ b/es-app/src/NetworkThread.h @@ -0,0 +1,21 @@ +#pragma once + +#include "Window.h" +#include + +class NetworkThread +{ +public: + NetworkThread(Window * window); + virtual ~NetworkThread(); + +private: + Window* mWindow; + bool mRunning; + bool mFirstRun; + std::thread* mThread; + + void run(); +}; + + diff --git a/es-app/src/PlatformId.cpp b/es-app/src/PlatformId.cpp index 54a1a977e0..46fb1b9cfd 100644 --- a/es-app/src/PlatformId.cpp +++ b/es-app/src/PlatformId.cpp @@ -1,98 +1,145 @@ #include "PlatformId.h" +#include #include +#include "utils/StringUtil.h" namespace PlatformIds { - const char* PlatformNames[PLATFORM_COUNT + 1] = { - "unknown", // nothing set + static std::map Platforms = + { + { "unknown", PLATFORM_UNKNOWN }, + { "3do", THREEDO }, + { "amiga", AMIGA }, + { "amstradcpc", AMSTRAD_CPC }, + { "apple2", APPLE_II }, + { "arcade", ARCADE }, + { "atari800", ATARI_800 }, + { "atari2600", ATARI_2600 }, + { "atari5200", ATARI_5200 }, + { "atari7800", ATARI_7800 }, + { "atarilynx", ATARI_LYNX }, + { "atarist", ATARI_ST }, + { "atarijaguar", ATARI_JAGUAR }, + { "atarijaguarcd", ATARI_JAGUAR_CD }, + { "atarixe", ATARI_XE }, + { "colecovision", COLECOVISION }, + { "c64", COMMODORE_64 }, + { "intellivision", INTELLIVISION }, + { "macintosh", MAC_OS }, + { "xbox", XBOX }, + { "xbox360", XBOX_360 }, + { "msx", MSX }, + { "neogeo", NEOGEO }, + { "ngp", NEOGEO_POCKET }, + { "ngpc", NEOGEO_POCKET_COLOR }, + { "n3ds", NINTENDO_3DS }, + { "n64", NINTENDO_64 }, + { "nds", NINTENDO_DS }, + { "fds", FAMICOM_DISK_SYSTEM }, + { "nes", NINTENDO_ENTERTAINMENT_SYSTEM }, + { "gb", GAME_BOY }, + { "gba", GAME_BOY_ADVANCE }, + { "gbc", GAME_BOY_COLOR }, + { "gc", NINTENDO_GAMECUBE }, + { "wii", NINTENDO_WII }, + { "wiiu", NINTENDO_WII_U }, + { "virtualboy", NINTENDO_VIRTUAL_BOY }, + { "gameandwatch", NINTENDO_GAME_AND_WATCH }, + { "pc", PC }, + { "sega32x", SEGA_32X }, + { "segacd", SEGA_CD }, + { "dreamcast", SEGA_DREAMCAST }, + { "gamegear", SEGA_GAME_GEAR }, + { "genesis", SEGA_GENESIS }, + { "mastersystem", SEGA_MASTER_SYSTEM }, + { "megadrive", SEGA_MEGA_DRIVE }, + { "saturn", SEGA_SATURN }, + { "sg-1000", SEGA_SG1000 }, + { "psx", PLAYSTATION }, + { "ps2", PLAYSTATION_2 }, + { "ps3", PLAYSTATION_3 }, + { "ps4", PLAYSTATION_4 }, + { "psvita", PLAYSTATION_VITA }, + { "psp", PLAYSTATION_PORTABLE }, + { "snes", SUPER_NINTENDO }, + { "scummvm", SCUMMVM }, + { "x68000", SHARP_X6800 }, + { "pcengine", TURBOGRAFX_16 }, // (aka PC Engine) HuCards onlyy + { "pcenginecd", TURBOGRAFX_CD }, // (aka PC Engine) CD-ROMs onlynly + { "wonderswan", WONDERSWAN }, + { "wonderswancolor", WONDERSWAN_COLOR }, + { "zxspectrum", ZX_SPECTRUM }, + { "videopac", VIDEOPAC_ODYSSEY2 }, + { "vectrex", VECTREX }, + { "trs-80", TRS80_COLOR_COMPUTER }, + { "coco", TANDY }, + { "supergrafx", SUPERGRAFX }, + { "amigacd32", AMIGACD32 }, + { "amigacdtv", AMIGACDTV }, + { "atomiswave", ATOMISWAVE }, + { "cavestory", CAVESTORY }, + { "gx4000", GX4000 }, + { "lutro", LUTRO }, + { "moonlight", MOONLIGHT }, + { "naomi", NAOMI }, + { "neogeocd", NEOGEO_CD }, + { "pcfx", PCFX }, + { "pokemini", POKEMINI }, + { "prboom", PRBOOM }, + { "satellaview", SATELLAVIEW }, + { "sufami", SUFAMITURBO }, + { "zx81", ZX81 }, + + // batocera specific names + { "gb2players", GAME_BOY }, + { "gbc2players", GAME_BOY_COLOR }, + { "3ds", NINTENDO_3DS }, + { "sg1000", SEGA_SG1000 }, + { "odyssey2", VIDEOPAC_ODYSSEY2 }, + { "oricatmos", ORICATMOS }, - "3do", - "amiga", - "amstradcpc", - "apple2", - "arcade", - "atari800", - "atari2600", - "atari5200", - "atari7800", - "atarilynx", - "atarist", - "atarijaguar", - "atarijaguarcd", - "atarixe", - "colecovision", - "c64", // commodore 64 - "intellivision", - "macintosh", - "xbox", - "xbox360", - "msx", - "neogeo", - "ngp", // neo geo pocket - "ngpc", // neo geo pocket color - "n3ds", // nintendo 3DS - "n64", // nintendo 64 - "nds", // nintendo DS - "fds", // Famicom Disk System - "nes", // nintendo entertainment system - "gb", // game boy - "gba", // game boy advance - "gbc", // game boy color - "gc", // gamecube - "wii", - "wiiu", - "virtualboy", - "gameandwatch", - "pc", - "sega32x", - "segacd", - "dreamcast", - "gamegear", - "genesis", // sega genesis - "mastersystem", // sega master system - "megadrive", // sega megadrive - "saturn", // sega saturn - "sg-1000", - "psx", - "ps2", - "ps3", - "ps4", - "psvita", - "psp", // playstation portable - "snes", // super nintendo entertainment system - "scummvm", - "x6800", - "pcengine", // (aka turbografx-16) HuCards only - "pcenginecd", // (aka turbografx-16) CD-ROMs only - "wonderswan", - "wonderswancolor", - "zxspectrum", - "videopac", - "vectrex", - "trs-80", - "coco", + // windows specific systems & names + { "windows", MOONLIGHT }, + { "vpinball", VISUALPINBALL }, + { "fpinball", FUTUREPINBALL }, + { "o2em", VIDEOPAC_ODYSSEY2 }, - "ignore", // do not allow scraping for this system - "invalid" + // Misc systems + { "channelf", CHANNELF }, + { "oric", ORICATMOS }, + { "thomson", THOMSON_TO_MO }, + { "samcoupe", SAMCOUPE }, + { "openbor", OPENBOR }, + { "uzebox", UZEBOX }, + { "apple2gs", APPLE2GS }, + { "spectravideo", SPECTRAVIDEO }, + { "palm", PALMOS }, + { "daphne", DAPHNEE }, + { "solarus", SOLARUS }, + + { "ignore", PLATFORM_IGNORE }, + { "invalid", PLATFORM_COUNT } }; PlatformId getPlatformId(const char* str) { - if(str == NULL) + if (str == nullptr) return PLATFORM_UNKNOWN; - for(unsigned int i = 1; i < PLATFORM_COUNT; i++) - { - if(strcmp(PlatformNames[i], str) == 0) - return (PlatformId)i; - } + auto it = Platforms.find(Utils::String::toLower(str).c_str()); + if (it != Platforms.end()) + return (*it).second; return PLATFORM_UNKNOWN; } - const char* getPlatformName(PlatformId id) + std::string getPlatformName(PlatformId id) { - return PlatformNames[id]; + for (auto& it : Platforms) + if (it.second == id) + return it.first; + + return "unknown"; } } diff --git a/es-app/src/PlatformId.h b/es-app/src/PlatformId.h index a4a53f861b..59c2ad5864 100644 --- a/es-app/src/PlatformId.h +++ b/es-app/src/PlatformId.h @@ -2,6 +2,8 @@ #ifndef ES_APP_PLATFORM_ID_H #define ES_APP_PLATFORM_ID_H +#include + namespace PlatformIds { enum PlatformId : unsigned int @@ -72,14 +74,47 @@ namespace PlatformIds VIDEOPAC_ODYSSEY2, VECTREX, TRS80_COLOR_COMPUTER, - TANDY, + TANDY, + SUPERGRAFX, + AMIGACD32, + AMIGACDTV, + ATOMISWAVE, + CAVESTORY, + GX4000, + LUTRO, + MOONLIGHT, + NAOMI, + NEOGEO_CD, + PCFX, + POKEMINI, + PRBOOM, + SATELLAVIEW, + SUFAMITURBO, + ZX81, + + // Windows Specific + VISUALPINBALL, + FUTUREPINBALL, + + // Misc systems + CHANNELF, + ORICATMOS, + THOMSON_TO_MO, + SAMCOUPE, + OPENBOR, + UZEBOX, + APPLE2GS, + SPECTRAVIDEO, + PALMOS, + DAPHNEE, + SOLARUS, PLATFORM_IGNORE, // do not allow scraping for this system PLATFORM_COUNT }; - PlatformId getPlatformId(const char* str); - const char* getPlatformName(PlatformId id); + PlatformId getPlatformId(const char* str); + std::string getPlatformName(PlatformId id); } #endif // ES_APP_PLATFORM_ID_H diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index 65c4fc9757..da393f1b43 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -10,54 +10,93 @@ #include "Settings.h" #include "ThemeData.h" #include "views/UIModeController.h" -#include #include -#ifdef WIN32 -#include -#endif +#include "utils/StringUtil.h" +#include "utils/ThreadPool.h" +#include "GuiComponent.h" +#include "Window.h" +#include "views/ViewController.h" + +using namespace Utils; std::vector SystemData::sSystemVector; -SystemData::SystemData(const std::string& name, const std::string& fullName, SystemEnvironmentData* envData, const std::string& themeFolder, bool CollectionSystem) : +SystemData::SystemData(const std::string& name, const std::string& fullName, SystemEnvironmentData* envData, const std::string& themeFolder, bool CollectionSystem, bool groupedSystem) : mName(name), mFullName(fullName), mEnvData(envData), mThemeFolder(themeFolder), mIsCollectionSystem(CollectionSystem), mIsGameSystem(true) { - mFilterIndex = new FileFilterIndex(); + mIsGroupSystem = groupedSystem; + mGameListHash = 0; + mGameCount = -1; + mSortId = Settings::getInstance()->getInt(getName() + ".sort"), + + mGridSizeOverride = Vector2f(0, 0); + mViewModeChanged = false; + mFilterIndex = nullptr;// new FileFilterIndex(); // if it's an actual system, initialize it, if not, just create the data structure - if(!CollectionSystem) + if(!CollectionSystem && !mIsGroupSystem) { - mRootFolder = new FileData(FOLDER, mEnvData->mStartPath, mEnvData, this); - mRootFolder->metadata.set("name", mFullName); - - if(!Settings::getInstance()->getBool("ParseGamelistOnly")) - populateFolder(mRootFolder); - - if(!Settings::getInstance()->getBool("IgnoreGamelist")) - parseGamelist(this); - - mRootFolder->sort(FileSorts::SortTypes.at(0)); + mRootFolder = new FolderData(mEnvData->mStartPath, this); + mRootFolder->getMetadata().set("name", mFullName); + + std::unordered_map fileMap; + + if (!Settings::getInstance()->getBool("ParseGamelistOnly")) + { + populateFolder(mRootFolder, fileMap); + if (mRootFolder->getChildren().size() == 0) + return; + } - indexAllGameFilters(mRootFolder); + if (!Settings::getInstance()->getBool("IgnoreGamelist") && mName != "imageviewer") + parseGamelist(this, fileMap); } else { // virtual systems are updated afterwards, we're just creating the data structure - mRootFolder = new FileData(FOLDER, "" + name, mEnvData, this); + mRootFolder = new FolderData("" + name, this); } + + auto defaultView = Settings::getInstance()->getString(getName() + ".defaultView"); + auto gridSizeOverride = Vector2f::parseString(Settings::getInstance()->getString(getName() + ".gridSize")); + setSystemViewMode(defaultView, gridSizeOverride, false); + setIsGameSystemStatus(); loadTheme(); } -SystemData::~SystemData() +bool SystemData::setSystemViewMode(std::string newViewMode, Vector2f gridSizeOverride, bool setChanged) { - //save changed game data back to xml - if(!Settings::getInstance()->getBool("IgnoreGamelist") && Settings::getInstance()->getBool("SaveGamelistsOnExit") && !mIsCollectionSystem) + if (newViewMode == "automatic") + newViewMode = ""; + + if (mViewMode == newViewMode && gridSizeOverride == mGridSizeOverride) + return false; + + mGridSizeOverride = gridSizeOverride; + mViewMode = newViewMode; + mViewModeChanged = setChanged; + + if (setChanged) { - updateGamelist(this); + Settings::getInstance()->setString(getName() + ".defaultView", mViewMode); + Settings::getInstance()->setString(getName() + ".gridSize", Utils::String::replace(Utils::String::replace(mGridSizeOverride.toString(), ".000000", ""), "0 0", "")); } + return true; +} + +Vector2f SystemData::getGridSizeOverride() +{ + return mGridSizeOverride; +} + +SystemData::~SystemData() +{ delete mRootFolder; - delete mFilterIndex; + + if (mFilterIndex != nullptr) + delete mFilterIndex; } void SystemData::setIsGameSystemStatus() @@ -68,7 +107,7 @@ void SystemData::setIsGameSystemStatus() mIsGameSystem = (mName != "retropie"); } -void SystemData::populateFolder(FileData* folder) +void SystemData::populateFolder(FolderData* folder, std::unordered_map& fileMap) { const std::string& folderPath = folder->getPath(); if(!Utils::FileSystem::isDirectory(folderPath)) @@ -87,56 +126,88 @@ void SystemData::populateFolder(FileData* folder) return; } } - - std::string filePath; + +// std::string filePath; std::string extension; bool isGame; bool showHidden = Settings::getInstance()->getBool("ShowHiddenFiles"); - Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(folderPath); - for(Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) + + Utils::FileSystem::fileList dirContent = Utils::FileSystem::getDirInfo(folderPath); + + for(Utils::FileSystem::fileList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) { - filePath = *it; + auto fileInfo = *it; + //filePath = *it; // skip hidden files and folders - if(!showHidden && Utils::FileSystem::isHidden(filePath)) + if(!showHidden && fileInfo.hidden) continue; //this is a little complicated because we allow a list of extensions to be defined (delimited with a space) //we first get the extension of the file itself: - extension = Utils::FileSystem::getExtension(filePath); + extension = Utils::String::toLower(Utils::FileSystem::getExtension(fileInfo.path)); //fyi, folders *can* also match the extension and be added as games - this is mostly just to support higan //see issue #75: https://github.com/Aloshi/EmulationStation/issues/75 - + isGame = false; - if(std::find(mEnvData->mSearchExtensions.cbegin(), mEnvData->mSearchExtensions.cend(), extension) != mEnvData->mSearchExtensions.cend()) + if (mEnvData->isValidExtension(extension)) //std::find(mEnvData->mSearchExtensions.cbegin(), mEnvData->mSearchExtensions.cend(), extension) != mEnvData->mSearchExtensions.cend()) { - FileData* newGame = new FileData(GAME, filePath, mEnvData, this); - - // preventing new arcade assets to be added - if(!newGame->isArcadeAsset()) + if (fileMap.find(fileInfo.path) == fileMap.end()) { - folder->addChild(newGame); - isGame = true; + FileData* newGame = new FileData(GAME, fileInfo.path, this); + + // preventing new arcade assets to be added + if (extension != ".zip" || !newGame->isArcadeAsset()) + { + folder->addChild(newGame); + fileMap[fileInfo.path] = newGame; + isGame = true; + } } } - + //add directories that also do not match an extension as folders - if(!isGame && Utils::FileSystem::isDirectory(filePath)) + if (!isGame && fileInfo.directory) { - FileData* newFolder = new FileData(FOLDER, filePath, mEnvData, this); - populateFolder(newFolder); + // Don't loose time looking in downloaded_images, downloaded_videos & media folders + if (fileInfo.path.rfind("downloaded_") != std::string::npos || + fileInfo.path.rfind("media") != std::string::npos || + fileInfo.path.rfind("images") != std::string::npos || + fileInfo.path.rfind("videos") != std::string::npos) + continue; - //ignore folders that do not contain games - if(newFolder->getChildrenByFilename().size() == 0) + FolderData* newFolder = new FolderData(fileInfo.path, this); + populateFolder(newFolder, fileMap); + + if (newFolder->getChildren().size() == 0) delete newFolder; else - folder->addChild(newFolder); + { + const std::string& key = newFolder->getPath(); + if (fileMap.find(key) == fileMap.end()) + { + folder->addChild(newFolder); + fileMap[key] = newFolder; + } + } } } } -void SystemData::indexAllGameFilters(const FileData* folder) +FileFilterIndex* SystemData::getIndex(bool createIndex) +{ + if (mFilterIndex == nullptr && createIndex) + { + mFilterIndex = new FileFilterIndex(); + indexAllGameFilters(mRootFolder); + mFilterIndex->setUIModeFilters(); + } + + return mFilterIndex; +} + +void SystemData::indexAllGameFilters(const FolderData* folder) { const std::vector& children = folder->getChildren(); @@ -145,7 +216,7 @@ void SystemData::indexAllGameFilters(const FileData* folder) switch((*it)->getType()) { case GAME: { mFilterIndex->addToIndex(*it); } break; - case FOLDER: { indexAllGameFilters(*it); } break; + case FOLDER: { indexAllGameFilters((FolderData*)*it); } break; } } } @@ -167,10 +238,198 @@ std::vector readList(const std::string& str, const char* delims = " return ret; } +SystemData* SystemData::loadSystem(pugi::xml_node system) +{ + std::vector emulatorList; + + std::string name, fullname, path, cmd, themeFolder, defaultCore; + + name = system.child("name").text().get(); + fullname = system.child("fullname").text().get(); + path = system.child("path").text().get(); + defaultCore = system.child("defaultCore").text().get(); + + pugi::xml_node emulators = system.child("emulators"); + if (emulators != NULL) + { + for (pugi::xml_node emulator : emulators.children()) + { + EmulatorData emulatorData; + emulatorData.mName = emulator.attribute("name").value(); + emulatorData.mCommandLine = emulator.attribute("command").value(); + + pugi::xml_node cores = emulator.child("cores"); + if (cores != NULL) + { + for (pugi::xml_node core : cores.children()) + { + const std::string& corename = core.text().get(); + + if (defaultCore.length() == 0) + defaultCore = corename; + + emulatorData.mCores.push_back(corename); + } + } + + emulatorList.push_back(emulatorData); + } + } + /* + if (window != NULL) + window->renderLoadingScreen(fullname, systemCount == 0 ? 0 : currentSystem / systemCount); + + currentSystem++; + */ + // convert extensions list from a string into a vector of strings + + std::vector list = readList(system.child("extension").text().get()); + std::unordered_set extensions; + + for (auto extension = list.cbegin(); extension != list.cend(); extension++) + { + std::string xt = Utils::String::toLower(*extension); + if (std::find(extensions.begin(), extensions.end(), xt) == extensions.end()) + extensions.insert(xt); + } + + cmd = system.child("command").text().get(); + + // platform id list + const char* platformList = system.child("platform").text().get(); + std::vector platformStrs = readList(platformList); + std::vector platformIds; + for (auto it = platformStrs.cbegin(); it != platformStrs.cend(); it++) + { + const char* str = it->c_str(); + PlatformIds::PlatformId platformId = PlatformIds::getPlatformId(str); + + if (platformId == PlatformIds::PLATFORM_IGNORE) + { + // when platform is ignore, do not allow other platforms + platformIds.clear(); + platformIds.push_back(platformId); + break; + } + + // if there appears to be an actual platform ID supplied but it didn't match the list, warn + if (platformId != PlatformIds::PLATFORM_UNKNOWN) + platformIds.push_back(platformId); + else if (str != NULL && str[0] != '\0' && platformId == PlatformIds::PLATFORM_UNKNOWN) + LOG(LogWarning) << " Unknown platform for system \"" << name << "\" (platform \"" << str << "\" from list \"" << platformList << "\")"; + } + + // theme folder + themeFolder = system.child("theme").text().as_string(name.c_str()); + + //validate + if (name.empty() || path.empty() || extensions.empty() || cmd.empty()) + { + LOG(LogError) << "System \"" << name << "\" is missing name, path, extension, or command!"; + return nullptr; + } + + //convert path to generic directory seperators + path = Utils::FileSystem::getGenericPath(path); + + //expand home symbol if the startpath contains ~ + if (path[0] == '~') + { + path.erase(0, 1); + path.insert(0, Utils::FileSystem::getHomePath()); + path = Utils::FileSystem::getCanonicalPath(path); + } + + //create the system runtime environment data + SystemEnvironmentData* envData = new SystemEnvironmentData; + envData->mSystemName = name; + envData->mStartPath = path; + envData->mSearchExtensions = extensions; + envData->mLaunchCommand = cmd; + envData->mPlatformIds = platformIds; + envData->mEmulators = emulatorList; + envData->mGroup = system.child("group").text().get(); + + SystemData* newSys = new SystemData(name, fullname, envData, themeFolder); + if (newSys->getRootFolder()->getChildren().size() == 0) + { + LOG(LogWarning) << "System \"" << name << "\" has no games! Ignoring it."; + delete newSys; + + return nullptr; + } + + return newSys; +} + +void SystemData::createGroupedSystems() +{ + std::map> map; + + for (auto it = sSystemVector.cbegin(); it != sSystemVector.cend(); it++) + { + SystemData* sys = *it; + if (!sys->isCollection() && !sys->getSystemEnvData()->mGroup.empty()) + { + if (Settings::getInstance()->getBool(sys->getSystemEnvData()->mGroup + ".ungroup")) + continue; + + map[sys->getSystemEnvData()->mGroup].push_back(sys); + } + } + + for (auto item : map) + { + SystemEnvironmentData* envData = new SystemEnvironmentData; + envData->mStartPath = ""; + envData->mLaunchCommand = ""; + + SystemData* system = new SystemData(item.first, item.first, envData, item.first, false, true); + system->mIsGroupSystem = true; + system->mIsGameSystem = false; + + FolderData* root = system->getRootFolder(); + + for (auto childSystem : item.second) + { + auto children = childSystem->getRootFolder()->getChildren(); + if (children.size() > 0) + { + auto folder = new FolderData(childSystem->getRootFolder()->getPath(), childSystem, false); + root->addChild(folder); + + auto theme = childSystem->getTheme(); + if (theme) + { + const ThemeData::ThemeElement* logoElem = theme->getElement("system", "logo", "image"); + if (logoElem && logoElem->has("path")) + { + std::string path = logoElem->get("path"); + folder->setMetadata("image", path); + folder->setMetadata("thumbnail", path); + + folder->enableVirtualFolderDisplay(true); + } + } + + for (auto child : children) + folder->addChild(child, false); + } + } + + if (root->getChildren().size() > 0) + { + system->loadTheme(); + sSystemVector.push_back(system); + } + } +} + //creates systems from information located in a config file -bool SystemData::loadConfig() +bool SystemData::loadConfig(Window* window) { deleteSystems(); + ThemeData::setDefaultTheme(nullptr); std::string path = getConfigPath(false); @@ -202,80 +461,109 @@ bool SystemData::loadConfig() return false; } - for(pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) + std::vector systemsNames; + + int systemCount = 0; + for (pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) { - std::string name, fullname, path, cmd, themeFolder; + systemsNames.push_back(system.child("fullname").text().get()); + systemCount++; + } - name = system.child("name").text().get(); - fullname = system.child("fullname").text().get(); - path = system.child("path").text().get(); + Utils::FileSystem::FileSystemCacheActivator fsc; - // convert extensions list from a string into a vector of strings - std::vector extensions = readList(system.child("extension").text().get()); + int currentSystem = 0; - cmd = system.child("command").text().get(); + typedef SystemData* SystemDataPtr; - // platform id list - const char* platformList = system.child("platform").text().get(); - std::vector platformStrs = readList(platformList); - std::vector platformIds; - for(auto it = platformStrs.cbegin(); it != platformStrs.cend(); it++) - { - const char* str = it->c_str(); - PlatformIds::PlatformId platformId = PlatformIds::getPlatformId(str); + ThreadPool* pThreadPool = NULL; + SystemDataPtr* systems = NULL; + + if (std::thread::hardware_concurrency() > 2 && Settings::getInstance()->getBool("ThreadedLoading")) + { + pThreadPool = new ThreadPool(); - if(platformId == PlatformIds::PLATFORM_IGNORE) - { - // when platform is ignore, do not allow other platforms - platformIds.clear(); - platformIds.push_back(platformId); - break; - } + systems = new SystemDataPtr[systemCount]; + for (int i = 0; i < systemCount; i++) + systems[i] = nullptr; + + pThreadPool->queueWorkItem([] { CollectionSystemManager::get()->loadCollectionSystems(true); }); + } - // if there appears to be an actual platform ID supplied but it didn't match the list, warn - if(str != NULL && str[0] != '\0' && platformId == PlatformIds::PLATFORM_UNKNOWN) - LOG(LogWarning) << " Unknown platform for system \"" << name << "\" (platform \"" << str << "\" from list \"" << platformList << "\")"; - else if(platformId != PlatformIds::PLATFORM_UNKNOWN) - platformIds.push_back(platformId); + int processedSystem = 0; + + for (pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) + { + if (pThreadPool != NULL) + { + pThreadPool->queueWorkItem([system, currentSystem, systems, &processedSystem] + { + systems[currentSystem] = loadSystem(system); + processedSystem++; + }); } + else + { + std::string fullname = system.child("fullname").text().get(); - // theme folder - themeFolder = system.child("theme").text().as_string(name.c_str()); + if (window != NULL) + window->renderLoadingScreen(fullname, systemCount == 0 ? 0 : (float)currentSystem / (float)(systemCount + 1)); - //validate - if(name.empty() || path.empty() || extensions.empty() || cmd.empty()) - { - LOG(LogError) << "System \"" << name << "\" is missing name, path, extension, or command!"; - continue; + std::string nm = system.child("name").text().get(); + StopWatch watch("SystemData " + nm); + + SystemData* pSystem = loadSystem(system); + if (pSystem != nullptr) + sSystemVector.push_back(pSystem); } - //convert path to generic directory seperators - path = Utils::FileSystem::getGenericPath(path); + currentSystem++; + } - //expand home symbol if the startpath contains ~ - if(path[0] == '~') + if (pThreadPool != NULL) + { + if (window != NULL) { - path.erase(0, 1); - path.insert(0, Utils::FileSystem::getHomePath()); + pThreadPool->wait([window, &processedSystem, systemCount, &systemsNames] + { + int px = processedSystem - 1; + if (px >= 0 && px < systemsNames.size()) + window->renderLoadingScreen(systemsNames.at(px), (float)px / (float)(systemCount + 1)); + }, 10); } + else + pThreadPool->wait(); - //create the system runtime environment data - SystemEnvironmentData* envData = new SystemEnvironmentData; - envData->mStartPath = path; - envData->mSearchExtensions = extensions; - envData->mLaunchCommand = cmd; - envData->mPlatformIds = platformIds; - - SystemData* newSys = new SystemData(name, fullname, envData, themeFolder); - if(newSys->getRootFolder()->getChildrenByFilename().size() == 0) + for (int i = 0; i < systemCount; i++) { - LOG(LogWarning) << "System \"" << name << "\" has no games! Ignoring it."; - delete newSys; - }else{ - sSystemVector.push_back(newSys); + SystemData* pSystem = systems[i]; + if (pSystem != nullptr) + sSystemVector.push_back(pSystem); } + + delete[] systems; + delete pThreadPool; + + if (window != NULL) + window->renderLoadingScreen(_("Favorites"), systemCount == 0 ? 0 : currentSystem / systemCount); + + createGroupedSystems(); + CollectionSystemManager::get()->updateSystemsList(); + } + else + { + if (window != NULL) + window->renderLoadingScreen(_("Favorites"), systemCount == 0 ? 0 : currentSystem / systemCount); + + createGroupedSystems(); + CollectionSystemManager::get()->loadCollectionSystems(); + } + + if (SystemData::sSystemVector.size() > 0) + { + auto theme = SystemData::sSystemVector.at(0)->getTheme(); + ViewController::get()->onThemeChanged(theme); } - CollectionSystemManager::get()->loadCollectionSystems(); return true; } @@ -326,12 +614,40 @@ void SystemData::writeExampleConfig(const std::string& path) LOG(LogError) << "Example config written! Go read it at \"" << path << "\"!"; } +bool SystemData::hasDirtySystems() +{ + bool saveOnExit = !Settings::getInstance()->getBool("IgnoreGamelist") && Settings::getInstance()->getBool("SaveGamelistsOnExit"); + if (!saveOnExit) + return false; + + for (unsigned int i = 0; i < sSystemVector.size(); i++) + { + SystemData* pData = sSystemVector.at(i); + if (pData->mIsCollectionSystem) + continue; + + + if (hasDirtyFile(pData)) + return true; + } + + return false; +} + void SystemData::deleteSystems() { + bool saveOnExit = !Settings::getInstance()->getBool("IgnoreGamelist") && Settings::getInstance()->getBool("SaveGamelistsOnExit"); + for(unsigned int i = 0; i < sSystemVector.size(); i++) { - delete sSystemVector.at(i); + SystemData* pData = sSystemVector.at(i); + + if (saveOnExit && !pData->mIsCollectionSystem) + updateGamelist(pData); + + delete pData; } + sSystemVector.clear(); } @@ -346,9 +662,23 @@ std::string SystemData::getConfigPath(bool forWrite) bool SystemData::isVisible() { - return (getDisplayedGameCount() > 0 || - (UIModeController::getInstance()->isUIModeFull() && mIsCollectionSystem) || - (mIsCollectionSystem && mName == "favorites")); + if (isGroupChildSystem()) + return false; + + if ((getDisplayedGameCount() > 0 || + (UIModeController::getInstance()->isUIModeFull() && mIsCollectionSystem) || + (mIsCollectionSystem && mName == "favorites"))) + { + if (!mIsCollectionSystem) + { + auto hiddenSystems = Utils::String::split(Settings::getInstance()->getString("HiddenSystems"), ';'); + return std::find(hiddenSystems.cbegin(), hiddenSystems.cend(), getName()) == hiddenSystems.cend(); + } + + return true; + } + + return false; } SystemData* SystemData::getNext() const @@ -381,16 +711,20 @@ SystemData* SystemData::getPrev() const std::string SystemData::getGamelistPath(bool forWrite) const { - std::string filePath; + std::string fileRomPath = mRootFolder->getPath() + "/gamelist.xml"; + if(Utils::FileSystem::exists(fileRomPath)) + return fileRomPath; - filePath = mRootFolder->getPath() + "/gamelist.xml"; - if(Utils::FileSystem::exists(filePath)) - return filePath; + std::string filePath = Utils::FileSystem::getHomePath() + "/.emulationstation/gamelists/" + mName + "/gamelist.xml"; + + // Default to system rom folder + if (forWrite && !Utils::FileSystem::exists(filePath) && Utils::FileSystem::isDirectory(mRootFolder->getPath())) + return fileRomPath; - filePath = Utils::FileSystem::getHomePath() + "/.emulationstation/gamelists/" + mName + "/gamelist.xml"; if(forWrite) // make sure the directory exists if we're going to write to it, or crashes will happen Utils::FileSystem::createDirectory(Utils::FileSystem::getParent(filePath)); - if(forWrite || Utils::FileSystem::exists(filePath)) + + if (forWrite || Utils::FileSystem::exists(filePath)) return filePath; return "/etc/emulationstation/gamelists/" + mName + "/gamelist.xml"; @@ -473,18 +807,27 @@ FileData* SystemData::getRandomGame() return list.at(target); } -unsigned int SystemData::getDisplayedGameCount() const +int SystemData::getDisplayedGameCount() { - return (unsigned int)mRootFolder->getFilesRecursive(GAME, true).size(); + if (mGameCount < 0) + mGameCount = mRootFolder->getFilesRecursive(GAME, true).size(); + + return mGameCount; +} + +void SystemData::updateDisplayedGameCount() +{ + mGameCount =-1; } void SystemData::loadTheme() { + //StopWatch watch("SystemData::loadTheme " + getName()); + mTheme = std::make_shared(); std::string path = getThemePath(); - - if(!Utils::FileSystem::exists(path)) // no theme available for this platform + if (!Utils::FileSystem::exists(path)) // no theme available for this platform return; try @@ -494,11 +837,73 @@ void SystemData::loadTheme() sysData.insert(std::pair("system.name", getName())); sysData.insert(std::pair("system.theme", getThemeFolder())); sysData.insert(std::pair("system.fullName", getFullName())); - - mTheme->loadFile(sysData, path); - } catch(ThemeException& e) + + mTheme->loadFile(getThemeFolder(), sysData, path); + } + catch(ThemeException& e) { LOG(LogError) << e.what(); mTheme = std::make_shared(); // reset to empty } } + +void SystemData::setSortId(const unsigned int sortId) +{ + mSortId = sortId; + Settings::getInstance()->setInt(getName() + ".sort", mSortId); +} + +void SystemData::deleteIndex() +{ + if (mFilterIndex != nullptr) + { + delete mFilterIndex; + mFilterIndex = nullptr; + } +} + +bool SystemData::isGroupChildSystem() +{ + if (mEnvData != nullptr && !mEnvData->mGroup.empty()) + return !Settings::getInstance()->getBool(mEnvData->mGroup + ".ungroup"); + + return false; +} + +std::unordered_set SystemData::getAllGroupNames() +{ + std::unordered_set names; + + for (auto sys : SystemData::sSystemVector) + { + if (sys->isGroupSystem()) + names.insert(sys->getName()); + else if (sys->mEnvData != nullptr && !sys->mEnvData->mGroup.empty()) + names.insert(sys->mEnvData->mGroup); + } + + return names; +} + +std::unordered_set SystemData::getGroupChildSystemNames(const std::string groupName) +{ + std::unordered_set names; + + for (auto sys : SystemData::sSystemVector) + if (sys->mEnvData != nullptr && sys->mEnvData->mGroup == groupName) + names.insert(sys->getFullName()); + + return names; +} + +SystemData* SystemData::getParentGroupSystem() +{ + if (!isGroupChildSystem() || isGroupSystem()) + return this; + + for (auto sys : SystemData::sSystemVector) + if (sys->isGroupSystem() && sys->getName() == mEnvData->mGroup) + return sys; + + return this; +} \ No newline at end of file diff --git a/es-app/src/SystemData.h b/es-app/src/SystemData.h index 63bf667fec..e783e41100 100644 --- a/es-app/src/SystemData.h +++ b/es-app/src/SystemData.h @@ -1,90 +1,226 @@ -#pragma once -#ifndef ES_APP_SYSTEM_DATA_H -#define ES_APP_SYSTEM_DATA_H - -#include "PlatformId.h" -#include -#include -#include -#include - -class FileData; -class FileFilterIndex; -class ThemeData; - -struct SystemEnvironmentData -{ - std::string mStartPath; - std::vector mSearchExtensions; - std::string mLaunchCommand; - std::vector mPlatformIds; -}; - -class SystemData -{ -public: - SystemData(const std::string& name, const std::string& fullName, SystemEnvironmentData* envData, const std::string& themeFolder, bool CollectionSystem = false); - ~SystemData(); - - inline FileData* getRootFolder() const { return mRootFolder; }; - inline const std::string& getName() const { return mName; } - inline const std::string& getFullName() const { return mFullName; } - inline const std::string& getStartPath() const { return mEnvData->mStartPath; } - inline const std::vector& getExtensions() const { return mEnvData->mSearchExtensions; } - inline const std::string& getThemeFolder() const { return mThemeFolder; } - inline SystemEnvironmentData* getSystemEnvData() const { return mEnvData; } - inline const std::vector& getPlatformIds() const { return mEnvData->mPlatformIds; } - inline bool hasPlatformId(PlatformIds::PlatformId id) { if (!mEnvData) return false; return std::find(mEnvData->mPlatformIds.cbegin(), mEnvData->mPlatformIds.cend(), id) != mEnvData->mPlatformIds.cend(); } - - inline const std::shared_ptr& getTheme() const { return mTheme; } - - std::string getGamelistPath(bool forWrite) const; - bool hasGamelist() const; - std::string getThemePath() const; - - unsigned int getGameCount() const; - unsigned int getDisplayedGameCount() const; - - static void deleteSystems(); - static bool loadConfig(); //Load the system config file at getConfigPath(). Returns true if no errors were encountered. An example will be written if the file doesn't exist. - static void writeExampleConfig(const std::string& path); - static std::string getConfigPath(bool forWrite); // if forWrite, will only return ~/.emulationstation/es_systems.cfg, never /etc/emulationstation/es_systems.cfg - - static std::vector sSystemVector; - - inline std::vector::const_iterator getIterator() const { return std::find(sSystemVector.cbegin(), sSystemVector.cend(), this); }; - inline std::vector::const_reverse_iterator getRevIterator() const { return std::find(sSystemVector.crbegin(), sSystemVector.crend(), this); }; - inline bool isCollection() { return mIsCollectionSystem; }; - inline bool isGameSystem() { return mIsGameSystem; }; - - bool isVisible(); - - SystemData* getNext() const; - SystemData* getPrev() const; - static SystemData* getRandomSystem(); - FileData* getRandomGame(); - - // Load or re-load theme. - void loadTheme(); - - FileFilterIndex* getIndex() { return mFilterIndex; }; - -private: - bool mIsCollectionSystem; - bool mIsGameSystem; - std::string mName; - std::string mFullName; - SystemEnvironmentData* mEnvData; - std::string mThemeFolder; - std::shared_ptr mTheme; - - void populateFolder(FileData* folder); - void indexAllGameFilters(const FileData* folder); - void setIsGameSystemStatus(); - - FileFilterIndex* mFilterIndex; - - FileData* mRootFolder; -}; - -#endif // ES_APP_SYSTEM_DATA_H +#pragma once +#ifndef ES_APP_SYSTEM_DATA_H +#define ES_APP_SYSTEM_DATA_H + +#include "PlatformId.h" +#include +#include +#include +#include + +#include +#include "math/Vector2f.h" +#include +#include + +#include "FileFilterIndex.h" +#include "Settings.h" + +class FileData; +class FolderData; +class ThemeData; +class Window; + +struct EmulatorData +{ + std::string mName; + std::string mCommandLine; + std::vector mCores; +}; + +struct SystemEnvironmentData +{ + std::string mSystemName; + + std::string mStartPath; + std::unordered_set mSearchExtensions; + std::string mLaunchCommand; + std::vector mPlatformIds; + std::vector mEmulators; + std::string mGroup; + + bool isValidExtension(const std::string extension) + { + return mSearchExtensions.find(extension) != mSearchExtensions.cend(); + } + + std::vector getCores(std::string emulatorName) + { + std::vector list; + + for (auto& emulator : mEmulators) + if (emulatorName == emulator.mName) + return emulator.mCores; + + return list; + } + + std::string getDefaultEmulator() + { + std::string currentEmul = Settings::getInstance()->getString(mSystemName + ".emulator"); + + for (auto& emulator : mEmulators) + if (currentEmul == emulator.mName) + return emulator.mName; + + for (auto& emulator : mEmulators) + return emulator.mName; + + return ""; + } + + std::string getDefaultCore(std::string emulatorName) + { + std::string currentCore = Settings::getInstance()->getString(mSystemName + ".core"); + + for (auto& emulator : mEmulators) + { + if (emulatorName == emulator.mName) + { + for (auto core : emulator.mCores) + if (core == currentCore) + return core; + + for (auto core : emulator.mCores) + return core; + } + } + + return ""; + } + + std::string getEmulatorCommandLine(std::string emulatorName) + { + for (auto& emulator : mEmulators) + if (emulatorName == emulator.mName) + return emulator.mCommandLine; + + return ""; + } +}; + +class SystemData +{ +public: + SystemData(const std::string& name, const std::string& fullName, SystemEnvironmentData* envData, const std::string& themeFolder, bool CollectionSystem = false, bool groupedSystem = false); + ~SystemData(); + + inline FolderData* getRootFolder() const { return mRootFolder; }; + inline const std::string& getName() const { return mName; } + inline const std::string& getFullName() const { return mFullName; } + inline const std::string& getStartPath() const { return mEnvData->mStartPath; } + //inline const std::vector& getExtensions() const { return mEnvData->mSearchExtensions; } + inline const std::string& getThemeFolder() const { return mThemeFolder; } + inline SystemEnvironmentData* getSystemEnvData() const { return mEnvData; } + inline const std::vector& getPlatformIds() const { return mEnvData->mPlatformIds; } + inline bool hasPlatformId(PlatformIds::PlatformId id) { if (!mEnvData) return false; return std::find(mEnvData->mPlatformIds.cbegin(), mEnvData->mPlatformIds.cend(), id) != mEnvData->mPlatformIds.cend(); } + + inline const std::shared_ptr& getTheme() const { return mTheme; } + + std::string getSystemViewMode() const { if (mViewMode == "automatic") return ""; else return mViewMode; }; + bool setSystemViewMode(std::string newViewMode, Vector2f gridSizeOverride = Vector2f(0,0), bool setChanged = true); + + Vector2f getGridSizeOverride(); + + std::string getGamelistPath(bool forWrite) const; + bool hasGamelist() const; + std::string getThemePath() const; + + unsigned int getGameCount() const; + + int getDisplayedGameCount(); + void updateDisplayedGameCount(); + + static bool hasDirtySystems(); + static void deleteSystems(); + static bool loadConfig(Window* window); //Load the system config file at getConfigPath(). Returns true if no errors were encountered. An example will be written if the file doesn't exist. + static void writeExampleConfig(const std::string& path); + static std::string getConfigPath(bool forWrite); // if forWrite, will only return ~/.emulationstation/es_systems.cfg, never /etc/emulationstation/es_systems.cfg + + static std::vector sSystemVector; + + inline std::vector::const_iterator getIterator() const { return std::find(sSystemVector.cbegin(), sSystemVector.cend(), this); }; + inline std::vector::const_reverse_iterator getRevIterator() const { return std::find(sSystemVector.crbegin(), sSystemVector.crend(), this); }; + inline bool isCollection() { return mIsCollectionSystem; }; + inline bool isGameSystem() { return mIsGameSystem; }; + + inline bool isGroupSystem() { return mIsGroupSystem; }; + bool isGroupChildSystem(); + + bool isVisible(); + + SystemData* getNext() const; + SystemData* getPrev() const; + static SystemData* getRandomSystem(); + FileData* getRandomGame(); + + // Load or re-load theme. + void loadTheme(); + + FileFilterIndex* getIndex(bool createIndex = false); + + void removeFromIndex(FileData* game) { + if (mFilterIndex != nullptr) mFilterIndex->removeFromIndex(game); + }; + + void addToIndex(FileData* game) { + if (mFilterIndex != nullptr) mFilterIndex->addToIndex(game); + }; + + void resetFilters() { + if (mFilterIndex != nullptr) mFilterIndex->resetFilters(); + }; + + void resetIndex() { + if (mFilterIndex != nullptr) mFilterIndex->resetIndex(); + }; + + void setUIModeFilters() { + if (mFilterIndex != nullptr) mFilterIndex->setUIModeFilters(); + } + + void deleteIndex(); + + unsigned int getSortId() const { return mSortId; }; + void setSortId(const unsigned int sortId = 0); + + void setGamelistHash(size_t size) { mGameListHash = size; } + size_t getGamelistHash() { return mGameListHash; } + + SystemData* getParentGroupSystem(); + + static std::unordered_set getAllGroupNames(); + static std::unordered_set getGroupChildSystemNames(const std::string groupName); + +private: + static SystemData* loadSystem(pugi::xml_node system); + static void createGroupedSystems(); + + size_t mGameListHash; + bool mIsCollectionSystem; + bool mIsGameSystem; + bool mIsGroupSystem; + + std::string mName; + std::string mFullName; + SystemEnvironmentData* mEnvData; + std::string mThemeFolder; + std::shared_ptr mTheme; + + std::string mViewMode; + Vector2f mGridSizeOverride; + bool mViewModeChanged; + + unsigned int mSortId; + + void populateFolder(FolderData* folder, std::unordered_map& fileMap); + void indexAllGameFilters(const FolderData* folder); + void setIsGameSystemStatus(); + + FileFilterIndex* mFilterIndex; + + FolderData* mRootFolder; + int mGameCount; +}; + +#endif // ES_APP_SYSTEM_DATA_H diff --git a/es-app/src/SystemScreenSaver.cpp b/es-app/src/SystemScreenSaver.cpp index 942a8a7426..03f0f87d4b 100644 --- a/es-app/src/SystemScreenSaver.cpp +++ b/es-app/src/SystemScreenSaver.cpp @@ -13,9 +13,14 @@ #include "PowerSaver.h" #include "Sound.h" #include "SystemData.h" +#include "components/ImageComponent.h" +#include "components/TextComponent.h" #include #include -#define FADE_TIME 300 +#include "AudioManager.h" +#include "math/Vector2i.h" + +#define FADE_TIME 500 SystemScreenSaver::SystemScreenSaver(Window* window) : mVideoScreensaver(NULL), @@ -31,8 +36,9 @@ SystemScreenSaver::SystemScreenSaver(Window* window) : mSystemName(""), mGameName(""), mCurrentGame(NULL), - mStopBackgroundAudio(true) + mLoadingNext(false) { + mWindow->setScreenSaver(this); std::string path = getTitleFolder(); if(!Utils::FileSystem::exists(path)) @@ -46,14 +52,11 @@ SystemScreenSaver::~SystemScreenSaver() // Delete subtitle file, if existing remove(getTitlePath().c_str()); mCurrentGame = NULL; - delete mVideoScreensaver; - delete mImageScreensaver; } bool SystemScreenSaver::allowSleep() { - //return false; - return ((mVideoScreensaver == NULL) && (mImageScreensaver == NULL)); + return (mVideoScreensaver == nullptr && mImageScreensaver == nullptr); } bool SystemScreenSaver::isScreenSaverActive() @@ -63,54 +66,49 @@ bool SystemScreenSaver::isScreenSaverActive() void SystemScreenSaver::startScreenSaver() { + bool loadingNext = mLoadingNext; + + stopScreenSaver(); + std::string screensaver_behavior = Settings::getInstance()->getString("ScreenSaverBehavior"); - if (!mVideoScreensaver && (screensaver_behavior == "random video")) + if (screensaver_behavior == "random video") { + if (!loadingNext && Settings::getInstance()->getBool("VideoAudio")) + AudioManager::getInstance()->deinit(); + + mVideoChangeTime = Settings::getInstance()->getInt("ScreenSaverSwapVideoTimeout"); + // Configure to fade out the windows, Skip Fading if Instant mode mState = PowerSaver::getMode() == PowerSaver::INSTANT ? STATE_SCREENSAVER_ACTIVE : STATE_FADE_OUT_WINDOW; - mVideoChangeTime = Settings::getInstance()->getInt("ScreenSaverSwapVideoTimeout"); - mOpacity = 0.0f; - + + if (mState == STATE_FADE_OUT_WINDOW) + { + mState = STATE_FADE_IN_VIDEO; + mOpacity = 1.0f; + } + else + mOpacity = 0.0f; + // Load a random video - std::string path = ""; - pickRandomVideo(path); + std::string path = pickRandomVideo(); - int retry = 200; - while(retry > 0 && ((path.empty() || !Utils::FileSystem::exists(path)) || mCurrentGame == NULL)) + int retry = 10; + while (retry > 0 && !Utils::FileSystem::exists(path)) { retry--; - pickRandomVideo(path); + path = pickRandomVideo(); } if (!path.empty() && Utils::FileSystem::exists(path)) { -#ifdef _RPI_ - // Create the correct type of video component - if (Settings::getInstance()->getBool("ScreenSaverOmxPlayer")) - mVideoScreensaver = new VideoPlayerComponent(mWindow, getTitlePath()); - else - mVideoScreensaver = new VideoVlcComponent(mWindow, getTitlePath()); -#else - mVideoScreensaver = new VideoVlcComponent(mWindow, getTitlePath()); -#endif + LOG(LogDebug) << "VideoScreenSaver::startScreenSaver " << path.c_str(); - mVideoScreensaver->topWindow(true); - mVideoScreensaver->setOrigin(0.5f, 0.5f); - mVideoScreensaver->setPosition(Renderer::getScreenWidth() / 2.0f, Renderer::getScreenHeight() / 2.0f); - - if (Settings::getInstance()->getBool("StretchVideoOnScreenSaver")) - { - mVideoScreensaver->setResize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); - } - else - { - mVideoScreensaver->setMaxSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); - } + mVideoScreensaver = std::make_shared(mWindow); + mVideoScreensaver->setGame(mCurrentGame); mVideoScreensaver->setVideo(path); - mVideoScreensaver->setScreensaverMode(true); - mVideoScreensaver->onShow(); + PowerSaver::runningScreenSaver(true); mTimer = 0; return; @@ -118,62 +116,46 @@ void SystemScreenSaver::startScreenSaver() } else if (screensaver_behavior == "slideshow") { - // Configure to fade out the windows, Skip Fading if Instant mode - mState = PowerSaver::getMode() == PowerSaver::INSTANT - ? STATE_SCREENSAVER_ACTIVE - : STATE_FADE_OUT_WINDOW; mVideoChangeTime = Settings::getInstance()->getInt("ScreenSaverSwapImageTimeout"); - mOpacity = 0.0f; + + // Configure to fade out the windows, Skip Fading if Instant mode + mState = PowerSaver::getMode() == PowerSaver::INSTANT + ? STATE_SCREENSAVER_ACTIVE + : STATE_FADE_OUT_WINDOW; + + if (mState == STATE_FADE_OUT_WINDOW) + { + mState = STATE_FADE_IN_VIDEO; + mOpacity = 1.0f; + } + else + mOpacity = 0.0f; // Load a random image - std::string path = ""; + std::string path; if (Settings::getInstance()->getBool("SlideshowScreenSaverCustomImageSource")) { - pickRandomCustomImage(path); + path = pickRandomCustomImage(); // Custom images are not tied to the game list mCurrentGame = NULL; } else - { - pickRandomGameListImage(path); - } - - if (!mImageScreensaver) - { - mImageScreensaver = new ImageComponent(mWindow, false, false); - } - - mTimer = 0; - - mImageScreensaver->setImage(path); - mImageScreensaver->setOrigin(0.5f, 0.5f); - mImageScreensaver->setPosition(Renderer::getScreenWidth() / 2.0f, Renderer::getScreenHeight() / 2.0f); + path = pickRandomGameListImage(); - if (Settings::getInstance()->getBool("SlideshowScreenSaverStretch")) - { - mImageScreensaver->setResize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); - } - else + if (!path.empty() && Utils::FileSystem::exists(path)) { - mImageScreensaver->setMaxSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); - } + LOG(LogDebug) << "ImageScreenSaver::startScreenSaver " << path.c_str(); - std::string bg_audio_file = Settings::getInstance()->getString("SlideshowScreenSaverBackgroundAudioFile"); - if ((!mBackgroundAudio) && (bg_audio_file != "")) - { - if (Utils::FileSystem::exists(bg_audio_file)) - { - // paused PS so that the background audio keeps playing - PowerSaver::pause(); - mBackgroundAudio = Sound::get(bg_audio_file); - mBackgroundAudio->play(); - } - } + mImageScreensaver = std::make_shared(mWindow); + mImageScreensaver->setGame(mCurrentGame); + mImageScreensaver->setImage(path); - PowerSaver::runningScreenSaver(true); - mTimer = 0; - return; + PowerSaver::runningScreenSaver(true); + mTimer = 0; + return; + } } + // No videos. Just use a standard screensaver mState = STATE_SCREENSAVER_ACTIVE; mCurrentGame = NULL; @@ -181,31 +163,36 @@ void SystemScreenSaver::startScreenSaver() void SystemScreenSaver::stopScreenSaver() { - if ((mBackgroundAudio) && (mStopBackgroundAudio)) - { - mBackgroundAudio->stop(); - mBackgroundAudio.reset(); - // if we were playing audio, we paused PS - PowerSaver::resume(); - } + bool isExitingScreenSaver = !mLoadingNext; + + if (mLoadingNext) + mFadingImageScreensaver = mImageScreensaver; + else + mFadingImageScreensaver = nullptr; // so that we stop the background audio next time, unless we're restarting the screensaver - mStopBackgroundAudio = true; + mLoadingNext = false; - delete mVideoScreensaver; - mVideoScreensaver = NULL; - delete mImageScreensaver; - mImageScreensaver = NULL; + mVideoScreensaver = nullptr; + mImageScreensaver = nullptr; // we need this to loop through different videos mState = STATE_INACTIVE; PowerSaver::runningScreenSaver(false); + + // Exiting video screen saver -> Restore sound + if (isExitingScreenSaver && Settings::getInstance()->getBool("VideoAudio") && mVideoScreensaver) + { + AudioManager::getInstance()->init(); + AudioManager::getInstance()->playRandomMusic(); + } } void SystemScreenSaver::renderScreenSaver() { - std::string screensaver_behavior = Settings::getInstance()->getString("ScreenSaverBehavior"); - if (mVideoScreensaver && screensaver_behavior == "random video") + Transform4x4f transform = Transform4x4f::Identity(); + + if (mVideoScreensaver) { // Render black background Renderer::setMatrix(Transform4x4f::Identity()); @@ -214,39 +201,39 @@ void SystemScreenSaver::renderScreenSaver() // Only render the video if the state requires it if ((int)mState >= STATE_FADE_IN_VIDEO) { - Transform4x4f transform = Transform4x4f::Identity(); + unsigned int opacity = 255 - (unsigned char)(mOpacity * 255); + + mVideoScreensaver->setOpacity(opacity); mVideoScreensaver->render(transform); } } - else if (mImageScreensaver && screensaver_behavior == "slideshow") + else if (mImageScreensaver) { // Render black background - Renderer::setMatrix(Transform4x4f::Identity()); - Renderer::drawRect(0.0f, 0.0f, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x000000FF, 0x000000FF); + Renderer::setMatrix(transform); + Renderer::drawRect(0.0f, 0.0f, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x000000FF); + + if (mFadingImageScreensaver != nullptr) + mFadingImageScreensaver->render(transform); // Only render the video if the state requires it if ((int)mState >= STATE_FADE_IN_VIDEO) - { + { if (mImageScreensaver->hasImage()) { - mImageScreensaver->setOpacity(255- (unsigned char) (mOpacity * 255)); - - Transform4x4f transform = Transform4x4f::Identity(); + unsigned int opacity = 255 - (unsigned char)(mOpacity * 255); + + Renderer::setMatrix(transform); + Renderer::drawRect(0.0f, 0.0f, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x00000000 | opacity); + mImageScreensaver->setOpacity(opacity); mImageScreensaver->render(transform); } } - - // Check if we need to restart the background audio - if ((mBackgroundAudio) && (Settings::getInstance()->getString("SlideshowScreenSaverBackgroundAudioFile") != "")) - { - if (!mBackgroundAudio->isPlaying()) - { - mBackgroundAudio->play(); - } - } } else if (mState != STATE_INACTIVE) { + std::string screensaver_behavior = Settings::getInstance()->getString("ScreenSaverBehavior"); + Renderer::setMatrix(Transform4x4f::Identity()); unsigned char color = screensaver_behavior == "dim" ? 0x000000A0 : 0x000000FF; Renderer::drawRect(0.0f, 0.0f, Renderer::getScreenWidth(), Renderer::getScreenHeight(), color, color); @@ -263,13 +250,13 @@ unsigned long SystemScreenSaver::countGameListNodes(const char *nodeName) if (!(*it)->isGameSystem() || (*it)->isCollection()) continue; - FileData* rootFileData = (*it)->getRootFolder(); + FolderData* rootFileData = (*it)->getRootFolder(); - FileType type = GAME; - std::vector allFiles = rootFileData->getFilesRecursive(type, true); + std::vector allFiles = rootFileData->getFilesRecursive(GAME, true); std::vector::const_iterator itf; // declare an iterator to a vector of strings - for(itf=allFiles.cbegin() ; itf < allFiles.cend(); itf++) { + for (itf=allFiles.cbegin() ; itf < allFiles.cend(); itf++) + { if ((strcmp(nodeName, "video") == 0 && (*itf)->getVideoPath() != "") || (strcmp(nodeName, "image") == 0 && (*itf)->getImagePath() != "")) { @@ -298,8 +285,10 @@ void SystemScreenSaver::countImages() } } -void SystemScreenSaver::pickGameListNode(unsigned long index, const char *nodeName, std::string& path) +std::string SystemScreenSaver::pickGameListNode(unsigned long index, const char *nodeName) { + std::string path; + std::vector::const_iterator it; for (it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); ++it) { @@ -307,65 +296,73 @@ void SystemScreenSaver::pickGameListNode(unsigned long index, const char *nodeNa if (!(*it)->isGameSystem() || (*it)->isCollection()) continue; - FileData* rootFileData = (*it)->getRootFolder(); + FolderData* rootFileData = (*it)->getRootFolder(); FileType type = GAME; std::vector allFiles = rootFileData->getFilesRecursive(type, true); std::vector::const_iterator itf; // declare an iterator to a vector of strings - for(itf=allFiles.cbegin() ; itf < allFiles.cend(); itf++) { + for(itf=allFiles.cbegin() ; itf < allFiles.cend(); itf++) + { if ((strcmp(nodeName, "video") == 0 && (*itf)->getVideoPath() != "") || (strcmp(nodeName, "image") == 0 && (*itf)->getImagePath() != "")) { - if (index-- == 0) + if (index-- <= 0) { // We have it - path = ""; if (strcmp(nodeName, "video") == 0) path = (*itf)->getVideoPath(); else if (strcmp(nodeName, "image") == 0) path = (*itf)->getImagePath(); + + if (!Utils::FileSystem::exists(path)) + continue; + mSystemName = (*it)->getFullName(); mGameName = (*itf)->getName(); mCurrentGame = (*itf); - // end of getting FileData - if (Settings::getInstance()->getString("ScreenSaverGameInfo") != "never") - writeSubtitle(mGameName.c_str(), mSystemName.c_str(), - (Settings::getInstance()->getString("ScreenSaverGameInfo") == "always")); - return; +#ifdef _RPI_ + if (Settings::getInstance()->getBool("ScreenSaverOmxPlayer")) + if (Settings::getInstance()->getString("ScreenSaverGameInfo") != "never" && strcmp(nodeName, "video") == 0) + writeSubtitle(mGameName.c_str(), mSystemName.c_str(), (Settings::getInstance()->getString("ScreenSaverGameInfo") == "always")); +#endif + + return path; } } } } + + return ""; } -void SystemScreenSaver::pickRandomVideo(std::string& path) +std::string SystemScreenSaver::pickRandomVideo() { countVideos(); mCurrentGame = NULL; - if (mVideoCount > 0) - { - int video = (int)(((float)rand() / float(RAND_MAX)) * (float)mVideoCount); - - pickGameListNode(video, "video", path); - } + if (mVideoCount == 0) + return ""; + + int video = (int)(((float)rand() / float(RAND_MAX)) * (float)mVideoCount); + return pickGameListNode(video, "video"); } -void SystemScreenSaver::pickRandomGameListImage(std::string& path) +std::string SystemScreenSaver::pickRandomGameListImage() { countImages(); mCurrentGame = NULL; - if (mImageCount > 0) - { - int image = (int)(((float)rand() / float(RAND_MAX)) * (float)mImageCount); - - pickGameListNode(image, "image", path); - } + if (mImageCount == 0) + return ""; + + int image = (int)(((float)rand() / float(RAND_MAX)) * (float)mImageCount); + return pickGameListNode(image, "image"); } -void SystemScreenSaver::pickRandomCustomImage(std::string& path) +std::string SystemScreenSaver::pickRandomCustomImage() { + std::string path; + std::string imageDir = Settings::getInstance()->getString("SlideshowScreenSaverImageDir"); if ((imageDir != "") && (Utils::FileSystem::exists(imageDir))) { @@ -403,6 +400,8 @@ void SystemScreenSaver::pickRandomCustomImage(std::string& path) { LOG(LogError) << "Slideshow Screensaver - Image directory does not exist: " << imageDir << "\n"; } + + return path; } void SystemScreenSaver::update(int deltaTime) @@ -416,7 +415,7 @@ void SystemScreenSaver::update(int deltaTime) mOpacity = 1.0f; // Update to the next state - mState = STATE_FADE_IN_VIDEO; + mState = STATE_FADE_IN_VIDEO; } } else if (mState == STATE_FADE_IN_VIDEO) @@ -427,6 +426,7 @@ void SystemScreenSaver::update(int deltaTime) mOpacity = 0.0f; // Update to the next state mState = STATE_SCREENSAVER_ACTIVE; + mFadingImageScreensaver = nullptr; } } else if (mState == STATE_SCREENSAVER_ACTIVE) @@ -434,23 +434,21 @@ void SystemScreenSaver::update(int deltaTime) // Update the timer that swaps the videos mTimer += deltaTime; if (mTimer > mVideoChangeTime) - { nextVideo(); - } } // If we have a loaded video then update it if (mVideoScreensaver) mVideoScreensaver->update(deltaTime); + if (mImageScreensaver) mImageScreensaver->update(deltaTime); } -void SystemScreenSaver::nextVideo() { - mStopBackgroundAudio = false; - stopScreenSaver(); +void SystemScreenSaver::nextVideo() +{ + mLoadingNext = true; startScreenSaver(); - mState = STATE_SCREENSAVER_ACTIVE; } FileData* SystemScreenSaver::getCurrentGame() @@ -463,12 +461,398 @@ void SystemScreenSaver::launchGame() if (mCurrentGame != NULL) { // launching Game - ViewController::get()->goToGameList(mCurrentGame->getSystem()); - IGameListView* view = ViewController::get()->getGameListView(mCurrentGame->getSystem()).get(); - view->setCursor(mCurrentGame); + auto view = ViewController::get()->getGameListView(mCurrentGame->getSystem(), false); + if (view != nullptr) + view->setCursor(mCurrentGame); + if (Settings::getInstance()->getBool("ScreenSaverControls")) + mCurrentGame->launchGame(mWindow); + else + ViewController::get()->goToGameList(mCurrentGame->getSystem()); + } +} + + +// ------------------------------------------------------------------------------------------------------------------------ +// GAME SCREEN SAVER BASE CLASS +// ------------------------------------------------------------------------------------------------------------------------ + +GameScreenSaverBase::GameScreenSaverBase(Window* window) : GuiComponent(window), + mViewport(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight()) +{ + mDecoration = nullptr; + mMarquee = nullptr; + mLabelGame = nullptr; + mLabelSystem = nullptr; +} + +GameScreenSaverBase::~GameScreenSaverBase() +{ + if (mMarquee != nullptr) + { + delete mMarquee; + mMarquee = nullptr; + } + + if (mDecoration != nullptr) + { + delete mDecoration; + mDecoration = nullptr; + } + + if (mLabelGame != nullptr) + { + delete mLabelGame; + mLabelGame = nullptr; + } + + if (mLabelSystem != nullptr) + { + delete mLabelSystem; + mLabelSystem = nullptr; + } +} + +#include "guis/GuiMenu.h" +#include +#include +#include + +void GameScreenSaverBase::setGame(FileData* game) +{ + if (mLabelGame != nullptr) + { + delete mLabelGame; + mLabelGame = nullptr; + } + + if (mLabelSystem != nullptr) + { + delete mLabelSystem; + mLabelSystem = nullptr; + } + + if (mMarquee != nullptr) + { + delete mMarquee; + mMarquee = nullptr; + } + + if (mDecoration != nullptr) + { + delete mDecoration; + mDecoration = nullptr; + } + + if (game == nullptr) + return; + + mViewport = Renderer::Rect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight()); + + /* +#ifdef _RPI_ + if (!Settings::getInstance()->getBool("ScreenSaverOmxPlayer")) +#endif + if (Settings::getInstance()->getBool("ScreenSaverDecoration")) + { + auto sets = GuiMenu::getDecorationsSets(game->getSystem()); + int setId = (int)(((float)rand() / float(RAND_MAX)) * (float)sets.size()); + + if (setId >= 0 && setId < sets.size() && Utils::FileSystem::exists(sets[setId].imageUrl)) + { + std::string infoFile = Utils::String::replace(sets[setId].imageUrl, ".png", ".info"); + if (Utils::FileSystem::exists(infoFile)) + { + FILE* fp = fopen(infoFile.c_str(), "r"); // non-Windows use "r" + if (fp) + { + char readBuffer[65536]; + rapidjson::FileReadStream is(fp, readBuffer, sizeof(readBuffer)); + rapidjson::Document doc; + doc.ParseStream(is); + + if (!doc.HasParseError()) + { + if (doc.HasMember("top") && doc.HasMember("left") && doc.HasMember("bottom") && doc.HasMember("right") && doc.HasMember("width") && doc.HasMember("height")) + { + auto width = doc["width"].GetInt(); + auto height = doc["height"].GetInt(); + if (width > 0 && height > 0) + { + float px = Renderer::getScreenWidth() / (float)width; + float py = Renderer::getScreenHeight() / (float)height; + + auto top = doc["top"].GetInt(); + auto left = doc["left"].GetInt(); + auto bottom = doc["bottom"].GetInt(); + auto right = doc["right"].GetInt(); + + mViewport = Renderer::Rect(left * px, top * py, (width - right) * px, (height - bottom) * py); + } + } + } + + fclose(fp); + } + } + + mDecoration = new ImageComponent(mWindow, true); + mDecoration->setImage(sets[setId].imageUrl); + mDecoration->setOrigin(0.5f, 0.5f); + mDecoration->setPosition(Renderer::getScreenWidth() / 2.0f, (float)Renderer::getScreenHeight() / 2.0f); + mDecoration->setMaxSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); + } + } + */ + if (!Settings::getInstance()->getBool("SlideshowScreenSaverGameName")) + return; + + if (Settings::getInstance()->getBool("ScreenSaverMarquee") && Utils::FileSystem::exists(game->getMarqueePath())) + { + mMarquee = new ImageComponent(mWindow, true); + mMarquee->setImage(game->getMarqueePath()); + mMarquee->setOrigin(0.5f, 0.5f); + mMarquee->setPosition(mViewport.x / 2.0f + mViewport.w * 0.50f, mViewport.y / 2.0f + mViewport.h * 0.18f); + mMarquee->setMaxSize((float)mViewport.w * 0.40f, (float)mViewport.h * 0.22f); + } + + auto ph = ThemeData::getMenuTheme()->Text.font->getPath(); + auto sz = mViewport.h / 16.f; + auto font = Font::get(sz, ph); + + int h = mViewport.h / 4.0f; + int fh = font->getLetterHeight(); + + mLabelGame = new TextComponent(mWindow); + mLabelGame->setPosition(mViewport.x / 2.0f, mViewport.y / 2.0f + mViewport.h - h - fh / 2); + mLabelGame->setSize(mViewport.w, h - fh / 2); + mLabelGame->setHorizontalAlignment(ALIGN_CENTER); + mLabelGame->setVerticalAlignment(ALIGN_CENTER); + mLabelGame->setColor(0xFFFFFFFF); + mLabelGame->setGlowColor(0x00000040); + mLabelGame->setGlowSize(3); + mLabelGame->setFont(font); + mLabelGame->setText(game->getName()); + + mLabelSystem = new TextComponent(mWindow); + mLabelSystem->setPosition(mViewport.x / 2.0f, mViewport.y / 2.0f + mViewport.h - h + fh / 2); + mLabelSystem->setSize(mViewport.w, h + fh / 2); + mLabelSystem->setHorizontalAlignment(ALIGN_CENTER); + mLabelSystem->setVerticalAlignment(ALIGN_CENTER); + mLabelSystem->setColor(0xD0D0D0FF); + mLabelSystem->setGlowColor(0x00000060); + mLabelSystem->setGlowSize(2); + mLabelSystem->setFont(ph, sz * 0.66); + mLabelSystem->setText(game->getSystem()->getFullName()); +} + +void GameScreenSaverBase::render(const Transform4x4f& transform) +{ + if (mMarquee) + { + mMarquee->setOpacity(mOpacity); + mMarquee->render(transform); + } + else if (mLabelGame) + { + mLabelGame->setOpacity(mOpacity); + mLabelGame->render(transform); + } + + if (mLabelSystem) + { + mLabelSystem->setOpacity(mOpacity); + mLabelSystem->render(transform); + } + + if (mDecoration) + { + mDecoration->setOpacity(mOpacity); + mDecoration->render(transform); + } +} + +void GameScreenSaverBase::setOpacity(unsigned char opacity) +{ + mOpacity = opacity; +} + + +// ------------------------------------------------------------------------------------------------------------------------ +// IMAGE SCREEN SAVER CLASS +// ------------------------------------------------------------------------------------------------------------------------ + +ImageScreenSaver::ImageScreenSaver(Window* window) : GameScreenSaverBase(window) +{ + mImage = nullptr; +} + +ImageScreenSaver::~ImageScreenSaver() +{ + if (mImage != nullptr) + delete mImage; +} + +void ImageScreenSaver::setImage(const std::string path) +{ + if (mImage == nullptr) + { + mImage = new ImageComponent(mWindow, true); + mImage->setOrigin(0.5f, 0.5f); + mImage->setPosition((mViewport.x + mViewport.w) / 2.0f, (mViewport.y + mViewport.h) / 2.0f); + + if (Settings::getInstance()->getBool("SlideshowScreenSaverStretch")) + mImage->setMinSize((float)mViewport.w, (float)mViewport.h); + else + mImage->setMaxSize((float)mViewport.w, (float)mViewport.h); + } + + mImage->setImage(path); +} + +bool ImageScreenSaver::hasImage() +{ + return mImage != nullptr && mImage->hasImage(); +} + +void ImageScreenSaver::render(const Transform4x4f& transform) +{ + if (mImage) + { + mImage->setOpacity(mOpacity); + + Renderer::pushClipRect(Vector2i(mViewport.x, mViewport.y), Vector2i(mViewport.w, mViewport.h)); + mImage->render(transform); + Renderer::popClipRect(); + } + + GameScreenSaverBase::render(transform); +} + + +// ------------------------------------------------------------------------------------------------------------------------ +// VIDEO SCREEN SAVER CLASS +// ------------------------------------------------------------------------------------------------------------------------ + +VideoScreenSaver::VideoScreenSaver(Window* window) : GameScreenSaverBase(window) +{ + mVideo = nullptr; + mTime = 0; + mFade = 1.0; +} + +VideoScreenSaver::~VideoScreenSaver() +{ + if (mVideo != nullptr) + delete mVideo; +} + +void VideoScreenSaver::setVideo(const std::string path) +{ + if (mVideo == nullptr) + { +#ifdef _RPI_ + // Create the correct type of video component + if (Settings::getInstance()->getBool("ScreenSaverOmxPlayer")) + mVideo = new VideoPlayerComponent(mWindow, getTitlePath()); + else +#endif + mVideo = new VideoVlcComponent(mWindow); + + mVideo->topWindow(true); + mVideo->setOrigin(0.5f, 0.5f); + + mVideo->setPosition((mViewport.x + mViewport.w) / 2.0f, (mViewport.y + mViewport.h) / 2.0f); + + if (Settings::getInstance()->getBool("StretchVideoOnScreenSaver")) + mVideo->setMinSize((float)mViewport.w, (float)mViewport.h); + else + mVideo->setMaxSize((float)mViewport.w, (float)mViewport.h); + + mVideo->setVideo(path); + mVideo->setScreensaverMode(true); + mVideo->onShow(); + } + + mFade = 1.0; + mTime = 0; + mVideo->setVideo(path); +} + +#define SUBTITLE_DURATION 4000 +#define SUBTITLE_FADE 150 + +void VideoScreenSaver::render(const Transform4x4f& transform) +{ + if (mVideo) + { + mVideo->setOpacity(mOpacity); + + Renderer::pushClipRect(Vector2i(mViewport.x, mViewport.y), Vector2i(mViewport.w, mViewport.h)); + mVideo->render(transform); + Renderer::popClipRect(); + } + +#ifdef _RPI_ + if (Settings::getInstance()->getBool("ScreenSaverOmxPlayer")) + return; +#endif + + if (Settings::getInstance()->getString("ScreenSaverGameInfo") != "never") + { + if (mMarquee && mFade != 0) + { + mMarquee->setOpacity(mOpacity * mFade); + mMarquee->render(transform); + } + else if (mLabelGame && mFade != 0) + { + mLabelGame->setOpacity(mOpacity * mFade); + mLabelGame->render(transform); + } + + if (mLabelSystem && mFade != 0) { - view->launch(mCurrentGame); + mLabelSystem->setOpacity(mOpacity * mFade); + mLabelSystem->render(transform); } } + + if (mDecoration) + { + mDecoration->setOpacity(mOpacity); + mDecoration->render(transform); + } } + +void VideoScreenSaver::update(int deltaTime) +{ + GameScreenSaverBase::update(deltaTime); + + if (mVideo) + { + if (Settings::getInstance()->getString("ScreenSaverGameInfo") == "start & end") + { + int duration = SUBTITLE_DURATION; + int end = Settings::getInstance()->getInt("ScreenSaverSwapVideoTimeout") - duration; + + if (mTime >= duration - SUBTITLE_FADE && mTime < duration) + { + mFade -= (float)deltaTime / SUBTITLE_FADE; + if (mFade < 0) + mFade = 0; + } + else if (mTime >= end - SUBTITLE_FADE && mTime < end) + { + mFade += (float)deltaTime / SUBTITLE_FADE; + if (mFade > 1) + mFade = 1; + } + else if (mTime > duration && mTime < end - SUBTITLE_FADE) + mFade = 0; + } + + mTime += deltaTime; + mVideo->update(deltaTime); + } +} \ No newline at end of file diff --git a/es-app/src/SystemScreenSaver.h b/es-app/src/SystemScreenSaver.h index a26f4407e9..54ecac7743 100644 --- a/es-app/src/SystemScreenSaver.h +++ b/es-app/src/SystemScreenSaver.h @@ -3,10 +3,67 @@ #define ES_APP_SYSTEM_SCREEN_SAVER_H #include "Window.h" +#include "GuiComponent.h" +#include "renderers/Renderer.h" class ImageComponent; class Sound; class VideoComponent; +class TextComponent; + +class GameScreenSaverBase : public GuiComponent +{ +public: + GameScreenSaverBase(Window* window); + ~GameScreenSaverBase(); + + virtual void setGame(FileData* mCurrentGame); + + void render(const Transform4x4f& transform) override; + + void setOpacity(unsigned char opacity) override; + +protected: + ImageComponent* mMarquee; + TextComponent* mLabelGame; + TextComponent* mLabelSystem; + + ImageComponent* mDecoration; + + Renderer::Rect mViewport; +}; + +class ImageScreenSaver : public GameScreenSaverBase +{ +public: + ImageScreenSaver(Window* window); + ~ImageScreenSaver(); + + void setImage(const std::string path); + bool hasImage(); + + void render(const Transform4x4f& transform) override; + +private: + ImageComponent* mImage; +}; + +class VideoScreenSaver : public GameScreenSaverBase +{ +public: + VideoScreenSaver(Window* window); + ~VideoScreenSaver(); + + void setVideo(const std::string path); + void render(const Transform4x4f& transform) override; + void update(int deltaTime) override; + +private: + VideoComponent* mVideo; + + int mTime; + float mFade; +}; // Screensaver implementation for main window class SystemScreenSaver : public Window::ScreenSaver @@ -31,12 +88,11 @@ class SystemScreenSaver : public Window::ScreenSaver unsigned long countGameListNodes(const char *nodeName); void countVideos(); void countImages(); - void pickGameListNode(unsigned long index, const char *nodeName, std::string& path); - void pickRandomVideo(std::string& path); - void pickRandomGameListImage(std::string& path); - void pickRandomCustomImage(std::string& path); - void input(InputConfig* config, Input input); + std::string pickGameListNode(unsigned long index, const char *nodeName); + std::string pickRandomVideo(); + std::string pickRandomGameListImage(); + std::string pickRandomCustomImage(); enum STATE { STATE_INACTIVE, @@ -47,11 +103,16 @@ class SystemScreenSaver : public Window::ScreenSaver private: bool mVideosCounted; - unsigned long mVideoCount; - VideoComponent* mVideoScreensaver; + unsigned long mVideoCount; bool mImagesCounted; unsigned long mImageCount; - ImageComponent* mImageScreensaver; + + //VideoComponent* mVideoScreensaver; + std::shared_ptr mVideoScreensaver; + + std::shared_ptr mFadingImageScreensaver; + std::shared_ptr mImageScreensaver; + Window* mWindow; STATE mState; float mOpacity; @@ -60,8 +121,10 @@ class SystemScreenSaver : public Window::ScreenSaver std::string mGameName; std::string mSystemName; int mVideoChangeTime; - std::shared_ptr mBackgroundAudio; - bool mStopBackgroundAudio; + + //std::shared_ptr mBackgroundAudio; + bool mLoadingNext; + }; #endif // ES_APP_SYSTEM_SCREEN_SAVER_H diff --git a/es-app/src/VolumeControl.cpp b/es-app/src/VolumeControl.cpp index 9153523ed8..c02c21d02f 100644 --- a/es-app/src/VolumeControl.cpp +++ b/es-app/src/VolumeControl.cpp @@ -8,12 +8,12 @@ #endif #if defined(__linux__) - #if defined(_RPI_) || defined(_VERO4K_) - const char * VolumeControl::mixerName = "PCM"; - #else - const char * VolumeControl::mixerName = "Master"; - #endif - const char * VolumeControl::mixerCard = "default"; +#if defined(_RPI_) || defined(_VERO4K_) +const char * VolumeControl::mixerName = "PCM"; +#else +const char * VolumeControl::mixerName = "Master"; +#endif +const char * VolumeControl::mixerCard = "default"; #endif std::weak_ptr VolumeControl::sInstance; @@ -22,7 +22,7 @@ std::weak_ptr VolumeControl::sInstance; VolumeControl::VolumeControl() : originalVolume(0), internalVolume(0) #if defined (__APPLE__) - #error TODO: Not implemented for MacOS yet!!! +#error TODO: Not implemented for MacOS yet!!! #elif defined(__linux__) , mixerIndex(0), mixerHandle(nullptr), mixerElem(nullptr), mixerSelemId(nullptr) #elif defined(WIN32) || defined(_WIN32) @@ -35,10 +35,10 @@ VolumeControl::VolumeControl() originalVolume = getVolume(); } -VolumeControl::VolumeControl(const VolumeControl & right): +VolumeControl::VolumeControl(const VolumeControl & right) : originalVolume(0), internalVolume(0) #if defined (__APPLE__) - #error TODO: Not implemented for MacOS yet!!! +#error TODO: Not implemented for MacOS yet!!! #elif defined(__linux__) , mixerIndex(0), mixerHandle(nullptr), mixerElem(nullptr), mixerSelemId(nullptr) #elif defined(WIN32) || defined(_WIN32) @@ -81,7 +81,7 @@ void VolumeControl::init() { //initialize audio mixer interface #if defined (__APPLE__) - #error TODO: Not implemented for MacOS yet!!! +#error TODO: Not implemented for MacOS yet!!! #elif defined(__linux__) //try to open mixer device if (mixerHandle == nullptr) @@ -152,10 +152,10 @@ void VolumeControl::init() } #elif defined(WIN32) || defined(_WIN32) //get windows version information - OSVERSIONINFOEXA osVer = {sizeof(OSVERSIONINFO)}; + OSVERSIONINFOEXA osVer = { sizeof(OSVERSIONINFO) }; ::GetVersionExA(reinterpret_cast(&osVer)); //check windows version - if(osVer.dwMajorVersion < 6) + if (osVer.dwMajorVersion < 6) { //Windows older than Vista. use mixer API. open default mixer if (mixerHandle == nullptr) @@ -229,7 +229,7 @@ void VolumeControl::deinit() { //deinitialize audio mixer interface #if defined (__APPLE__) - #error TODO: Not implemented for MacOS yet!!! +#error TODO: Not implemented for MacOS yet!!! #elif defined(__linux__) if (mixerHandle != nullptr) { snd_mixer_detach(mixerHandle, mixerCard); @@ -256,10 +256,21 @@ int VolumeControl::getVolume() const int volume = 0; #if defined (__APPLE__) - #error TODO: Not implemented for MacOS yet!!! +#error TODO: Not implemented for MacOS yet!!! #elif defined(__linux__) if (mixerElem != nullptr) { + if (mixerHandle != nullptr) + snd_mixer_handle_events(mixerHandle); + + int mute_state; + if (snd_mixer_selem_has_playback_switch(mixerElem)) + { + snd_mixer_selem_get_playback_switch(mixerElem, SND_MIXER_SCHN_UNKNOWN, &mute_state); + if (!mute_state) // system Muted + return 0; + } + //get volume range long minVolume; long maxVolume; @@ -312,6 +323,14 @@ int VolumeControl::getVolume() const { //Windows Vista or above. use EndpointVolume API float floatVolume = 0.0f; //0-1 + + BOOL mute = FALSE; + if (endpointVolume->GetMute(&mute) == S_OK) + { + if (mute) + return 0; + } + if (endpointVolume->GetMasterVolumeLevelScalar(&floatVolume) == S_OK) { volume = (int)Math::round(floatVolume * 100.0f); @@ -350,7 +369,7 @@ void VolumeControl::setVolume(int volume) //store values in internal variables internalVolume = volume; #if defined (__APPLE__) - #error TODO: Not implemented for MacOS yet!!! +#error TODO: Not implemented for MacOS yet!!! #elif defined(__linux__) if (mixerElem != nullptr) { diff --git a/es-app/src/VolumeControl.h b/es-app/src/VolumeControl.h index a2e420e7e2..e4135f59f2 100644 --- a/es-app/src/VolumeControl.h +++ b/es-app/src/VolumeControl.h @@ -5,15 +5,15 @@ #include #if defined (__APPLE__) - #error TODO: Not implemented for MacOS yet!!! +#error TODO: Not implemented for MacOS yet!!! #elif defined(__linux__) - #include - #include - #include +#include +#include +#include #elif defined(WIN32) || defined(_WIN32) - #include - #include - #include +#include +#include +#include #endif /*! @@ -22,14 +22,14 @@ Singleton pattern. Call getInstance() to get an object. class VolumeControl { #if defined (__APPLE__) - #error TODO: Not implemented for MacOS yet!!! +#error TODO: Not implemented for MacOS yet!!! #elif defined(__linux__) - static const char * mixerName; - static const char * mixerCard; - int mixerIndex; - snd_mixer_t* mixerHandle; - snd_mixer_elem_t* mixerElem; - snd_mixer_selem_id_t* mixerSelemId; + static const char * mixerName; + static const char * mixerCard; + int mixerIndex; + snd_mixer_t* mixerHandle; + snd_mixer_elem_t* mixerElem; + snd_mixer_selem_id_t* mixerSelemId; #elif defined(WIN32) || defined(_WIN32) HMIXER mixerHandle; MIXERCONTROL mixerControl; @@ -43,7 +43,7 @@ class VolumeControl VolumeControl(); VolumeControl(const VolumeControl & right); - VolumeControl & operator=(const VolumeControl & right); + VolumeControl & operator=(const VolumeControl & right); public: static std::shared_ptr & getInstance(); diff --git a/es-app/src/components/RatingComponent.cpp b/es-app/src/components/RatingComponent.cpp index 4a06cf142f..357c36bb5d 100644 --- a/es-app/src/components/RatingComponent.cpp +++ b/es-app/src/components/RatingComponent.cpp @@ -3,7 +3,7 @@ #include "resources/TextureResource.h" #include "ThemeData.h" -RatingComponent::RatingComponent(Window* window) : GuiComponent(window), mColorShift(0xFFFFFFFF) +RatingComponent::RatingComponent(Window* window) : GuiComponent(window), mColorShift(0xFFFFFFFF), mUnfilledColor(0xFFFFFFFF) { mFilledTexture = TextureResource::get(":/star_filled.svg", true); mUnfilledTexture = TextureResource::get(":/star_unfilled.svg", true); @@ -106,18 +106,40 @@ void RatingComponent::updateColors() void RatingComponent::render(const Transform4x4f& parentTrans) { - if (!isVisible()) + if (!isVisible() || mFilledTexture == nullptr || mUnfilledTexture == nullptr) return; Transform4x4f trans = parentTrans * getTransform(); - Renderer::setMatrix(trans); + + + Vector2f clipPos(trans.translation().x(), trans.translation().y()); + if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) + return; - mFilledTexture->bind(); - Renderer::drawTriangleStrips(&mVertices[0], 4); + Renderer::setMatrix(trans); - mUnfilledTexture->bind(); - Renderer::drawTriangleStrips(&mVertices[4], 4); + if (mUnfilledTexture->bind()) + { + if (mUnfilledColor != mColorShift) + { + const unsigned int color = Renderer::convertColor(mUnfilledColor); + for (int i = 0; i < 8; ++i) + mVertices[i].col = color; + } + + Renderer::drawTriangleStrips(&mVertices[4], 4); + Renderer::bindTexture(0); + + if (mUnfilledColor != mColorShift) + updateColors(); + } + if (mFilledTexture->bind()) + { + Renderer::drawTriangleStrips(&mVertices[0], 4); + Renderer::bindTexture(0); + } + renderChildren(trans); } @@ -158,8 +180,16 @@ void RatingComponent::applyTheme(const std::shared_ptr& theme, const } - if(properties & COLOR && elem->has("color")) - setColorShift(elem->get("color")); + if (properties & COLOR) + { + if (elem->has("color")) + setColorShift(elem->get("color")); + + if (elem->has("unfilledColor")) + mUnfilledColor = elem->get("unfilledColor"); + else + mUnfilledColor = mColorShift; + } if(imgChanged) onSizeChanged(); diff --git a/es-app/src/components/RatingComponent.h b/es-app/src/components/RatingComponent.h index ae2c3ae7e2..efc6cac9f9 100644 --- a/es-app/src/components/RatingComponent.h +++ b/es-app/src/components/RatingComponent.h @@ -45,6 +45,7 @@ class RatingComponent : public GuiComponent Renderer::Vertex mVertices[8]; unsigned int mColorShift; + unsigned int mUnfilledColor; std::shared_ptr mFilledTexture; std::shared_ptr mUnfilledTexture; diff --git a/es-app/src/components/ScraperSearchComponent.cpp b/es-app/src/components/ScraperSearchComponent.cpp index 752c4a8e69..6a73a910cd 100644 --- a/es-app/src/components/ScraperSearchComponent.cpp +++ b/es-app/src/components/ScraperSearchComponent.cpp @@ -8,6 +8,7 @@ #include "components/TextComponent.h" #include "guis/GuiMsgBox.h" #include "guis/GuiTextEditPopup.h" +#include "guis/GuiTextEditPopupKeyboard.h" #include "resources/Font.h" #include "utils/StringUtil.h" #include "FileData.h" @@ -15,9 +16,15 @@ #include "Window.h" ScraperSearchComponent::ScraperSearchComponent(Window* window, SearchType type) : GuiComponent(window), - mGrid(window, Vector2i(4, 3)), mBusyAnim(window), + mGrid(window, Vector2i(4, 3)), mBusyAnim(window), mSearchType(type) { + auto theme = ThemeData::getMenuTheme(); + + auto font = theme->TextSmall.font; // Font::get(FONT_SIZE_SMALL); // this gets replaced in onSizeChanged() so its just a placeholder + const unsigned int mdColor = theme->Text.color; // 0x777777FF; + const unsigned int mdLblColor = theme->Text.color; // 0x666666FF; + addChild(&mGrid); mBlockAccept = false; @@ -26,7 +33,7 @@ ScraperSearchComponent::ScraperSearchComponent(Window* window, SearchType type) mGrid.setEntry(std::make_shared(mWindow), Vector2i(0, 0), false, false, Vector2i(1, 3), GridFlags::BORDER_TOP | GridFlags::BORDER_BOTTOM); // selected result name - mResultName = std::make_shared(mWindow, "Result name", Font::get(FONT_SIZE_MEDIUM), 0x777777FF); + mResultName = std::make_shared(mWindow, "Result name", ThemeData::getMenuTheme()->Text.font, mdColor); // selected result thumbnail mResultThumbnail = std::make_shared(mWindow); @@ -34,14 +41,12 @@ ScraperSearchComponent::ScraperSearchComponent(Window* window, SearchType type) // selected result desc + container mDescContainer = std::make_shared(mWindow); - mResultDesc = std::make_shared(mWindow, "Result desc", Font::get(FONT_SIZE_SMALL), 0x777777FF); + mResultDesc = std::make_shared(mWindow, "Result desc", font, mdColor); mDescContainer->addChild(mResultDesc.get()); mDescContainer->setAutoScroll(true); + + - // metadata - auto font = Font::get(FONT_SIZE_SMALL); // this gets replaced in onSizeChanged() so its just a placeholder - const unsigned int mdColor = 0x777777FF; - const unsigned int mdLblColor = 0x666666FF; mMD_Rating = std::make_shared(mWindow); mMD_ReleaseDate = std::make_shared(mWindow); mMD_ReleaseDate->setColor(mdColor); @@ -50,12 +55,12 @@ ScraperSearchComponent::ScraperSearchComponent(Window* window, SearchType type) mMD_Genre = std::make_shared(mWindow, "", font, mdColor); mMD_Players = std::make_shared(mWindow, "", font, mdColor); - mMD_Pairs.push_back(MetaDataPair(std::make_shared(mWindow, "RATING:", font, mdLblColor), mMD_Rating, false)); - mMD_Pairs.push_back(MetaDataPair(std::make_shared(mWindow, "RELEASED:", font, mdLblColor), mMD_ReleaseDate)); - mMD_Pairs.push_back(MetaDataPair(std::make_shared(mWindow, "DEVELOPER:", font, mdLblColor), mMD_Developer)); - mMD_Pairs.push_back(MetaDataPair(std::make_shared(mWindow, "PUBLISHER:", font, mdLblColor), mMD_Publisher)); - mMD_Pairs.push_back(MetaDataPair(std::make_shared(mWindow, "GENRE:", font, mdLblColor), mMD_Genre)); - mMD_Pairs.push_back(MetaDataPair(std::make_shared(mWindow, "PLAYERS:", font, mdLblColor), mMD_Players)); + mMD_Pairs.push_back(MetaDataPair(std::make_shared(mWindow, _("Rating") + ":", font, mdLblColor), mMD_Rating, false)); + mMD_Pairs.push_back(MetaDataPair(std::make_shared(mWindow, _("Released") + ":", font, mdLblColor), mMD_ReleaseDate)); + mMD_Pairs.push_back(MetaDataPair(std::make_shared(mWindow, _("Developer") + ":", font, mdLblColor), mMD_Developer)); + mMD_Pairs.push_back(MetaDataPair(std::make_shared(mWindow, _("Publisher") + ":", font, mdLblColor), mMD_Publisher)); + mMD_Pairs.push_back(MetaDataPair(std::make_shared(mWindow, _("Genre") + ":", font, mdLblColor), mMD_Genre)); + mMD_Pairs.push_back(MetaDataPair(std::make_shared(mWindow, _("Players") + ":", font, mdLblColor), mMD_Players)); mMD_Grid = std::make_shared(mWindow, Vector2i(2, (int)mMD_Pairs.size()*2 - 1)); unsigned int i = 0; @@ -78,7 +83,7 @@ ScraperSearchComponent::ScraperSearchComponent(Window* window, SearchType type) void ScraperSearchComponent::onSizeChanged() { mGrid.setSize(mSize); - + if(mSize.x() == 0 || mSize.y() == 0) return; @@ -87,10 +92,10 @@ void ScraperSearchComponent::onSizeChanged() mGrid.setColWidthPerc(0, 0.02f); // looks better when this is higher in auto mode else mGrid.setColWidthPerc(0, 0.01f); - + mGrid.setColWidthPerc(1, 0.25f); mGrid.setColWidthPerc(2, 0.25f); - + // row heights if(mSearchType == ALWAYS_ACCEPT_FIRST_RESULT) // show name mGrid.setRowHeightPerc(0, (mResultName->getFont()->getHeight() * 1.6f) / mGrid.getSize().y()); // result name @@ -112,12 +117,12 @@ void ScraperSearchComponent::onSizeChanged() // metadata resizeMetadata(); - + if(mSearchType != ALWAYS_ACCEPT_FIRST_RESULT) mDescContainer->setSize(mGrid.getColWidth(1)*boxartCellScale + mGrid.getColWidth(2), mResultDesc->getFont()->getHeight() * 3); else mDescContainer->setSize(mGrid.getColWidth(3)*boxartCellScale, mResultDesc->getFont()->getHeight() * 8); - + mResultDesc->setSize(mDescContainer->getSize().x(), 0); // make desc text wrap at edge of container mGrid.onSizeChanged(); @@ -235,12 +240,12 @@ void ScraperSearchComponent::onSearchDone(const std::vector if (!isValidConfiguredScraper()) { mWindow->pushGui(new GuiMsgBox(mWindow, Utils::String::toUpper("Configured scraper is no longer available.\nPlease change the scraping source in the settings."), - "FINISH", mSkipCallback)); + _("FINISH"), mSkipCallback)); } else { ComponentListRow row; - row.addElement(std::make_shared(mWindow, "NO GAMES FOUND - SKIP", font, color), true); + row.addElement(std::make_shared(mWindow, _("NO GAMES FOUND - SKIP"), font, color), true); if(mSkipCallback) row.makeAcceptInputHandler(mSkipCallback); @@ -278,10 +283,10 @@ void ScraperSearchComponent::onSearchDone(const std::vector void ScraperSearchComponent::onSearchError(const std::string& error) { LOG(LogInfo) << "ScraperSearchComponent search error: " << error; - mWindow->pushGui(new GuiMsgBox(mWindow, Utils::String::toUpper(error), - "RETRY", std::bind(&ScraperSearchComponent::search, this, mLastSearch), - "SKIP", mSkipCallback, - "CANCEL", mCancelCallback)); + mWindow->pushGui(new GuiMsgBox(mWindow, _("AN ERROR HAS OCCURED") + " :\n" + Utils::String::toUpper(error), + _("RETRY"), std::bind(&ScraperSearchComponent::search, this, mLastSearch), // batocera + _("SKIP"), mSkipCallback, // batocera + _("CANCEL"), mCancelCallback, ICON_ERROR)); // batocera } int ScraperSearchComponent::getSelectedIndex() @@ -299,7 +304,7 @@ void ScraperSearchComponent::updateInfoPane() { i = 0; } - + if(i != -1 && (int)mScraperResults.size() > i) { ScraperSearchResult& res = mScraperResults.at(i); @@ -370,7 +375,7 @@ void ScraperSearchComponent::returnResult(ScraperSearchResult result) mBlockAccept = true; // resolve metadata image before returning - if(!result.imageUrl.empty()) + if(!result.imageUrl.empty() || !result.thumbnailUrl.empty()) { mMDResolveHandle = resolveMetaDataAssets(result, mLastSearch); return; @@ -383,8 +388,25 @@ void ScraperSearchComponent::update(int deltaTime) { GuiComponent::update(deltaTime); - if(mBlockAccept) + if (mBlockAccept) { + if (mMDResolveHandle && mMDResolveHandle->status() == ASYNC_IN_PROGRESS) + { + if (mSearchType == ALWAYS_ACCEPT_FIRST_RESULT && !mResultThumbnail->hasImage()) + { + ScraperSearchResult result = mMDResolveHandle->getResult(); + + if (!result.mdl.get("thumbnail").empty()) + mResultThumbnail->setImage(result.mdl.get("thumbnail")); + else if (!result.mdl.get("image").empty()) + mResultThumbnail->setImage(result.mdl.get("image")); + } + + mBusyAnim.setText(_("DOWNLOADING") + " " + Utils::String::toUpper(mMDResolveHandle->getCurrentItem())); + } + else if (mSearchHandle && mSearchHandle->status() == ASYNC_IN_PROGRESS) + mBusyAnim.setText(_("SEARCHING")); + mBusyAnim.update(deltaTime); } @@ -398,17 +420,28 @@ void ScraperSearchComponent::update(int deltaTime) auto status = mSearchHandle->status(); auto results = mSearchHandle->getResults(); auto statusString = mSearchHandle->getStatusString(); - - // we reset here because onSearchDone in auto mode can call mSkipCallback() which can call - // another search() which will set our mSearchHandle to something important - mSearchHandle.reset(); - - if(status == ASYNC_DONE) + + if (status == ASYNC_DONE && results.size() == 0 && mSearchType == NEVER_AUTO_ACCEPT && + mLastSearch.nameOverride.empty() && Settings::getInstance()->getString("Scraper") == "ScreenScraper") { - onSearchDone(results); - }else if(status == ASYNC_ERROR) + // ScreenScraper in UI mode -> jeuInfo has no result, try with jeuRecherche + mLastSearch.nameOverride = mLastSearch.game->getName(); + mSearchHandle = startScraperSearch(mLastSearch); + } + else { - onSearchError(statusString); + // we reset here because onSearchDone in auto mode can call mSkipCallback() which can call + // another search() which will set our mSearchHandle to something important + mSearchHandle.reset(); + + if (status == ASYNC_DONE) + { + onSearchDone(results); + } + else if (status == ASYNC_ERROR) + { + onSearchError(statusString); + } } } @@ -453,7 +486,8 @@ void ScraperSearchComponent::openInputScreen(ScraperSearchParams& params) }; stop(); - mWindow->pushGui(new GuiTextEditPopup(mWindow, "SEARCH FOR", + + mWindow->pushGui(new GuiTextEditPopupKeyboard(mWindow, "SEARCH FOR", // initial value is last search if there was one, otherwise the clean path name params.nameOverride.empty() ? params.game->getCleanName() : params.nameOverride, searchForFunc, false, "SEARCH")); @@ -463,8 +497,8 @@ std::vector ScraperSearchComponent::getHelpPrompts() { std::vector prompts = mGrid.getHelpPrompts(); if(getSelectedIndex() != -1) - prompts.push_back(HelpPrompt("a", "accept result")); - + prompts.push_back(HelpPrompt("a", _("accept result"))); + return prompts; } diff --git a/es-app/src/components/TextListComponent.h b/es-app/src/components/TextListComponent.h index b9c0f3670f..b034b8ffc7 100644 --- a/es-app/src/components/TextListComponent.h +++ b/es-app/src/components/TextListComponent.h @@ -7,6 +7,7 @@ #include "utils/StringUtil.h" #include "Log.h" #include "Sound.h" +#include "Settings.h" #include class TextCache; @@ -37,14 +38,14 @@ class TextListComponent : public IList using IList::stopScrolling; TextListComponent(Window* window); - + bool input(InputConfig* config, Input input) override; void update(int deltaTime) override; void render(const Transform4x4f& parentTrans) override; void applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) override; void add(const std::string& name, const T& obj, unsigned int colorId); - + enum Alignment { ALIGN_LEFT, @@ -63,7 +64,7 @@ class TextListComponent : public IList it->data.textCache.reset(); } - inline void setUppercase(bool /*uppercase*/) + inline void setUppercase(bool /*uppercase*/) { mUppercase = true; for(auto it = mEntries.begin(); it != mEntries.end(); it++) @@ -110,7 +111,7 @@ class TextListComponent : public IList }; template -TextListComponent::TextListComponent(Window* window) : +TextListComponent::TextListComponent(Window* window) : IList(window), mSelectorImage(window) { mMarqueeOffset = 0; @@ -137,19 +138,31 @@ template void TextListComponent::render(const Transform4x4f& parentTrans) { Transform4x4f trans = parentTrans * getTransform(); - + std::shared_ptr& font = mFont; if(size() == 0) return; + Vector2f clipPos(trans.translation().x(), trans.translation().y()); + if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) + return; + + if (Settings::getInstance()->getBool("DebugGrid")) + { + Renderer::setMatrix(trans); + Renderer::drawRect(0.0f, 0.0f, mSize.x(), mSize.y(), 0xFF000033); + Renderer::setMatrix(parentTrans); + } + + const float entrySize = Math::max(font->getHeight(1.0), (float)font->getSize()) * mLineSpacing; int startEntry = 0; //number of entries that can fit on the screen simultaniously - int screenCount = (int)(mSize.y() / entrySize + 0.5f); - + int screenCount = Math::round(mSize.y() / entrySize); // (int)(mSize.y() / entrySize); // + 0.5f -> avoid partial items + if(size() >= screenCount) { startEntry = mCursor - screenCount/2; @@ -181,7 +194,7 @@ void TextListComponent::render(const Transform4x4f& parentTrans) // clip to inside margins Vector3f dim(mSize.x(), mSize.y(), 0); dim = trans * dim - trans.translation(); - Renderer::pushClipRect(Vector2i((int)(trans.translation().x() + mHorizontalMargin), (int)trans.translation().y()), + Renderer::pushClipRect(Vector2i((int)(trans.translation().x() + mHorizontalMargin), (int)trans.translation().y()), Vector2i((int)(dim.x() - mHorizontalMargin*2), (int)dim.y())); for(int i = startEntry; i < listCutoff; i++) @@ -281,7 +294,7 @@ bool TextListComponent::input(InputConfig* config, Input input) return true; } }else{ - if(config->isMappedLike("down", input) || config->isMappedLike("up", input) || + if(config->isMappedLike("down", input) || config->isMappedLike("up", input) || config->isMappedTo("pagedown", input) || config->isMappedTo("pageup", input)) { stopScrolling(); @@ -377,7 +390,7 @@ void TextListComponent::applyTheme(const std::shared_ptr& theme, c if (elem->has("selectorColorEnd")) setSelectorColorEnd(elem->get("selectorColorEnd")); if (elem->has("selectorGradientType")) - setSelectorColorGradientHorizontal(!(elem->get("selectorGradientType").compare("horizontal"))); + setSelectorColorGradientHorizontal(elem->get("selectorGradientType").compare("horizontal")); if(elem->has("selectedColor")) setSelectedColor(elem->get("selectedColor")); if(elem->has("primaryColor")) diff --git a/es-app/src/guis/GuiCollectionSystemsOptions.cpp b/es-app/src/guis/GuiCollectionSystemsOptions.cpp index ede6f3eb6b..a24a2b6b38 100644 --- a/es-app/src/guis/GuiCollectionSystemsOptions.cpp +++ b/es-app/src/guis/GuiCollectionSystemsOptions.cpp @@ -1,39 +1,70 @@ +#include #include "guis/GuiCollectionSystemsOptions.h" #include "components/OptionListComponent.h" #include "components/SwitchComponent.h" #include "guis/GuiSettings.h" +#include "guis/GuiTextEditPopupKeyboard.h" #include "guis/GuiTextEditPopup.h" #include "utils/StringUtil.h" #include "views/ViewController.h" #include "CollectionSystemManager.h" #include "Window.h" -GuiCollectionSystemsOptions::GuiCollectionSystemsOptions(Window* window) : GuiComponent(window), mMenu(window, "GAME COLLECTION SETTINGS") +GuiCollectionSystemsOptions::GuiCollectionSystemsOptions(Window* window) + : GuiSettings(window, _("GAME COLLECTION SETTINGS").c_str()) { initializeMenu(); } void GuiCollectionSystemsOptions::initializeMenu() { - addChild(&mMenu); + auto groupNames = SystemData::getAllGroupNames(); + if (groupNames.size() > 0) + { + auto ungroupedSystems = std::make_shared>(mWindow, _("GROUPED SYSTEMS"), true); + for (auto groupName : groupNames) + { + std::string description; + for (auto zz : SystemData::getGroupChildSystemNames(groupName)) + { + if (!description.empty()) + description += ", "; - // get collections + description += zz; + } + ungroupedSystems->addEx(groupName, description, groupName, !Settings::getInstance()->getBool(groupName + ".ungroup")); + } + + addWithLabel(_("GROUPED SYSTEMS"), ungroupedSystems); + + addSaveFunc([this, ungroupedSystems, groupNames] + { + std::vector checkedItems = ungroupedSystems->getSelectedObjects(); + for (auto groupName : groupNames) + { + bool isGroupActive = std::find(checkedItems.cbegin(), checkedItems.cend(), groupName) != checkedItems.cend(); + if (Settings::getInstance()->setBool(groupName + ".ungroup", !isGroupActive)) + setVariable("reloadSystems", true); + } + }); + } + + // get collections addSystemsToMenu(); // add "Create New Custom Collection from Theme" - std::vector unusedFolders = CollectionSystemManager::get()->getUnusedSystemsFromTheme(); if (unusedFolders.size() > 0) { - addEntry("CREATE NEW CUSTOM COLLECTION FROM THEME", 0x777777FF, true, - [this, unusedFolders] { - auto s = new GuiSettings(mWindow, "SELECT THEME FOLDER"); - std::shared_ptr< OptionListComponent > folderThemes = std::make_shared< OptionListComponent >(mWindow, "SELECT THEME FOLDER", true); + addEntry(_("CREATE NEW CUSTOM COLLECTION FROM THEME").c_str(), true, + [this, unusedFolders] { + auto s = new GuiSettings(mWindow, _("SELECT THEME FOLDER").c_str()); + std::shared_ptr< OptionListComponent > folderThemes = std::make_shared< OptionListComponent >(mWindow, _("SELECT THEME FOLDER"), true); // add Custom Systems - for(auto it = unusedFolders.cbegin() ; it != unusedFolders.cend() ; it++ ) + for (auto it = unusedFolders.cbegin(); it != unusedFolders.cend(); it++) { ComponentListRow row; std::string name = *it; @@ -43,7 +74,7 @@ void GuiCollectionSystemsOptions::initializeMenu() }; row.makeAcceptInputHandler(createCollectionCall); - auto themeFolder = std::make_shared(mWindow, Utils::String::toUpper(name), Font::get(FONT_SIZE_SMALL), 0x777777FF); + auto themeFolder = std::make_shared(mWindow, Utils::String::toUpper(name), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color); row.addElement(themeFolder, true); s->addRow(row); } @@ -51,8 +82,6 @@ void GuiCollectionSystemsOptions::initializeMenu() }); } - ComponentListRow row; - row.addElement(std::make_shared(mWindow, "CREATE NEW CUSTOM COLLECTION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); auto createCustomCollection = [this](const std::string& newVal) { std::string name = newVal; // we need to store the first Gui and remove it, as it'll be deleted by the actual Gui @@ -61,76 +90,123 @@ void GuiCollectionSystemsOptions::initializeMenu() window->removeGui(topGui); createCollection(name); }; - row.makeAcceptInputHandler([this, createCustomCollection] { - mWindow->pushGui(new GuiTextEditPopup(mWindow, "New Collection Name", "", createCustomCollection, false)); + addEntry(_("CREATE NEW CUSTOM COLLECTION").c_str(), true, [this, createCustomCollection] { + if (Settings::getInstance()->getBool("UseOSK")) { + mWindow->pushGui(new GuiTextEditPopupKeyboard(mWindow, _("New Collection Name"), "", createCustomCollection, false)); + } + else { + mWindow->pushGui(new GuiTextEditPopup(mWindow, _("New Collection Name"), "", createCustomCollection, false)); + } }); - mMenu.addRow(row); - - bundleCustomCollections = std::make_shared(mWindow); + std::shared_ptr bundleCustomCollections = std::make_shared(mWindow); bundleCustomCollections->setState(Settings::getInstance()->getBool("UseCustomCollectionsSystem")); - mMenu.addWithLabel("GROUP UNTHEMED CUSTOM COLLECTIONS", bundleCustomCollections); + addWithLabel(_("GROUP UNTHEMED CUSTOM COLLECTIONS"), bundleCustomCollections); + addSaveFunc([this, bundleCustomCollections] + { + if (Settings::getInstance()->setBool("UseCustomCollectionsSystem", bundleCustomCollections->getState())) + setVariable("reloadAll", true); + }); - sortAllSystemsSwitch = std::make_shared(mWindow); + std::shared_ptr sortAllSystemsSwitch = std::make_shared(mWindow); sortAllSystemsSwitch->setState(Settings::getInstance()->getBool("SortAllSystems")); - mMenu.addWithLabel("SORT CUSTOM COLLECTIONS AND SYSTEMS", sortAllSystemsSwitch); + addWithLabel(_("SORT CUSTOM COLLECTIONS AND SYSTEMS"), sortAllSystemsSwitch); + addSaveFunc([this, sortAllSystemsSwitch] + { + if (Settings::getInstance()->setBool("SortAllSystems", sortAllSystemsSwitch->getState())) + setVariable("reloadAll", true); + }); - toggleSystemNameInCollections = std::make_shared(mWindow); + std::shared_ptr toggleSystemNameInCollections = std::make_shared(mWindow); toggleSystemNameInCollections->setState(Settings::getInstance()->getBool("CollectionShowSystemInfo")); - mMenu.addWithLabel("SHOW SYSTEM NAME IN COLLECTIONS", toggleSystemNameInCollections); - - if(CollectionSystemManager::get()->isEditing()) + addWithLabel(_("SHOW SYSTEM NAME IN COLLECTIONS"), toggleSystemNameInCollections); + addSaveFunc([this, toggleSystemNameInCollections] { - row.elements.clear(); - row.addElement(std::make_shared(mWindow, "FINISH EDITING '" + Utils::String::toUpper(CollectionSystemManager::get()->getEditingCollection()) + "' COLLECTION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - row.makeAcceptInputHandler(std::bind(&GuiCollectionSystemsOptions::exitEditMode, this)); - mMenu.addRow(row); - } + if (Settings::getInstance()->setBool("CollectionShowSystemInfo", toggleSystemNameInCollections->getState())) + setVariable("reloadAll", true); + }); - mMenu.addButton("BACK", "back", std::bind(&GuiCollectionSystemsOptions::applySettings, this)); - mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); -} -void GuiCollectionSystemsOptions::addEntry(const char* name, unsigned int color, bool add_arrow, const std::function& func) -{ - std::shared_ptr font = Font::get(FONT_SIZE_MEDIUM); - // populate the list - ComponentListRow row; - row.addElement(std::make_shared(mWindow, name, font, color), true); + if (CollectionSystemManager::get()->isEditing()) + addEntry((_("FINISH EDITING COLLECTION") + " : " + Utils::String::toUpper(CollectionSystemManager::get()->getEditingCollection())).c_str(), false, std::bind(&GuiCollectionSystemsOptions::exitEditMode, this)); - if(add_arrow) + addSaveFunc([this] { - std::shared_ptr bracket = makeArrow(mWindow); - row.addElement(bracket, false); - } + std::string newAutoSettings = Utils::String::vectorToCommaString(autoOptionList->getSelectedObjects()); + std::string newCustomSettings = Utils::String::vectorToCommaString(customOptionList->getSelectedObjects()); - row.makeAcceptInputHandler(func); + bool dirty = Settings::getInstance()->setString("CollectionSystemsAuto", newAutoSettings); + dirty |= Settings::getInstance()->setString("CollectionSystemsCustom", newCustomSettings); + + if (dirty) + setVariable("reloadAll", true); + }); - mMenu.addRow(row); + onFinalize([this] + { + if (getVariable("reloadSystems")) + { + Window* window = mWindow; + window->renderLoadingScreen(_("Loading...")); + + ViewController::get()->goToStart(); + delete ViewController::get(); + ViewController::init(window); + CollectionSystemManager::deinit(); + CollectionSystemManager::init(window); + SystemData::loadConfig(window); + + GuiComponent* gui; + while ((gui = window->peekGui()) != NULL) + { + window->removeGui(gui); + if (gui != this) + delete gui; + } + ViewController::get()->reloadAll(nullptr); // Avoid reloading themes a second time + window->endRenderLoadingScreen(); + + window->pushGui(ViewController::get()); + } + else if (getVariable("reloadAll")) + { + Settings::getInstance()->saveFile(); + + CollectionSystemManager::get()->loadEnabledListFromSettings(); + CollectionSystemManager::get()->updateSystemsList(); + ViewController::get()->goToStart(); + ViewController::get()->reloadAll(mWindow); + mWindow->endRenderLoadingScreen(); + } + }); } -void GuiCollectionSystemsOptions::createCollection(std::string inName) { +void GuiCollectionSystemsOptions::createCollection(std::string inName) +{ std::string name = CollectionSystemManager::get()->getValidNewCollectionName(inName); SystemData* newSys = CollectionSystemManager::get()->addNewCustomCollection(name); customOptionList->add(name, name, true); + std::string outAuto = Utils::String::vectorToCommaString(autoOptionList->getSelectedObjects()); std::string outCustom = Utils::String::vectorToCommaString(customOptionList->getSelectedObjects()); updateSettings(outAuto, outCustom); + ViewController::get()->goToSystemView(newSys); Window* window = mWindow; CollectionSystemManager::get()->setEditMode(name); - while(window->peekGui() && window->peekGui() != ViewController::get()) + while (window->peekGui() && window->peekGui() != ViewController::get()) delete window->peekGui(); + return; } void GuiCollectionSystemsOptions::exitEditMode() { CollectionSystemManager::get()->exitEditMode(); - applySettings(); + close(); } GuiCollectionSystemsOptions::~GuiCollectionSystemsOptions() @@ -141,82 +217,63 @@ GuiCollectionSystemsOptions::~GuiCollectionSystemsOptions() void GuiCollectionSystemsOptions::addSystemsToMenu() { - std::map autoSystems = CollectionSystemManager::get()->getAutoCollectionSystems(); + std::map &autoSystems = CollectionSystemManager::get()->getAutoCollectionSystems(); + + autoOptionList = std::make_shared< OptionListComponent >(mWindow, _("SELECT COLLECTIONS"), true); - autoOptionList = std::make_shared< OptionListComponent >(mWindow, "SELECT COLLECTIONS", true); + bool hasGroup = false; - // add Auto Systems - for(std::map::const_iterator it = autoSystems.cbegin() ; it != autoSystems.cend() ; it++ ) + // add Auto Systems && preserve order + for (auto systemDecl : CollectionSystemManager::getSystemDecls()) { - autoOptionList->add(it->second.decl.longName, it->second.decl.name, it->second.isEnabled); + auto it = autoSystems.find(systemDecl.name); + if (it == autoSystems.cend()) + continue; + + if (it->second.decl.displayIfEmpty) + autoOptionList->add(it->second.decl.longName, it->second.decl.name, it->second.isEnabled); + else + { + if (!it->second.isPopulated) + CollectionSystemManager::get()->populateAutoCollection(&(it->second)); + + if (it->second.system->getRootFolder()->getChildren().size() == 0) + continue; + + if (!hasGroup) + { + autoOptionList->addGroup(_("ARCADE SYSTEMS")); + hasGroup = true; + } + + autoOptionList->add(it->second.decl.longName, it->second.decl.name, it->second.isEnabled); + } } - mMenu.addWithLabel("AUTOMATIC GAME COLLECTIONS", autoOptionList); + addWithLabel(_("AUTOMATIC GAME COLLECTIONS"), autoOptionList); std::map customSystems = CollectionSystemManager::get()->getCustomCollectionSystems(); - customOptionList = std::make_shared< OptionListComponent >(mWindow, "SELECT COLLECTIONS", true); + customOptionList = std::make_shared< OptionListComponent >(mWindow, _("SELECT COLLECTIONS"), true); // add Custom Systems - for(std::map::const_iterator it = customSystems.cbegin() ; it != customSystems.cend() ; it++ ) + for (std::map::const_iterator it = customSystems.cbegin(); it != customSystems.cend(); it++) { customOptionList->add(it->second.decl.longName, it->second.decl.name, it->second.isEnabled); } - mMenu.addWithLabel("CUSTOM GAME COLLECTIONS", customOptionList); -} - -void GuiCollectionSystemsOptions::applySettings() -{ - std::string outAuto = Utils::String::vectorToCommaString(autoOptionList->getSelectedObjects()); - std::string prevAuto = Settings::getInstance()->getString("CollectionSystemsAuto"); - std::string outCustom = Utils::String::vectorToCommaString(customOptionList->getSelectedObjects()); - std::string prevCustom = Settings::getInstance()->getString("CollectionSystemsCustom"); - bool outSort = sortAllSystemsSwitch->getState(); - bool prevSort = Settings::getInstance()->getBool("SortAllSystems"); - bool outBundle = bundleCustomCollections->getState(); - bool prevBundle = Settings::getInstance()->getBool("UseCustomCollectionsSystem"); - bool prevShow = Settings::getInstance()->getBool("CollectionShowSystemInfo"); - bool outShow = toggleSystemNameInCollections->getState(); - bool needUpdateSettings = prevAuto != outAuto || prevCustom != outCustom || outSort != prevSort || outBundle != prevBundle || prevShow != outShow ; - if (needUpdateSettings) - { - updateSettings(outAuto, outCustom); - } - - delete this; + addWithLabel(_("CUSTOM GAME COLLECTIONS"), customOptionList); } void GuiCollectionSystemsOptions::updateSettings(std::string newAutoSettings, std::string newCustomSettings) { - Settings::getInstance()->setString("CollectionSystemsAuto", newAutoSettings); - Settings::getInstance()->setString("CollectionSystemsCustom", newCustomSettings); - Settings::getInstance()->setBool("SortAllSystems", sortAllSystemsSwitch->getState()); - Settings::getInstance()->setBool("UseCustomCollectionsSystem", bundleCustomCollections->getState()); - Settings::getInstance()->setBool("CollectionShowSystemInfo", toggleSystemNameInCollections->getState()); - Settings::getInstance()->saveFile(); - CollectionSystemManager::get()->loadEnabledListFromSettings(); - CollectionSystemManager::get()->updateSystemsList(); - ViewController::get()->goToStart(); - ViewController::get()->reloadAll(); -} - -bool GuiCollectionSystemsOptions::input(InputConfig* config, Input input) -{ - bool consumed = GuiComponent::input(config, input); - if(consumed) - return true; + bool dirty = Settings::getInstance()->setString("CollectionSystemsAuto", newAutoSettings); + dirty |= Settings::getInstance()->setString("CollectionSystemsCustom", newCustomSettings); - if(config->isMappedTo("b", input) && input.value != 0) + if (dirty) { - applySettings(); + Settings::getInstance()->saveFile(); + CollectionSystemManager::get()->loadEnabledListFromSettings(); + CollectionSystemManager::get()->updateSystemsList(); + ViewController::get()->goToStart(); + ViewController::get()->reloadAll(); } - - - return false; -} - -std::vector GuiCollectionSystemsOptions::getHelpPrompts() -{ - std::vector prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", "back")); - return prompts; -} +} \ No newline at end of file diff --git a/es-app/src/guis/GuiCollectionSystemsOptions.h b/es-app/src/guis/GuiCollectionSystemsOptions.h index dd19bf9626..42a8b23ecf 100644 --- a/es-app/src/guis/GuiCollectionSystemsOptions.h +++ b/es-app/src/guis/GuiCollectionSystemsOptions.h @@ -3,35 +3,29 @@ #define ES_APP_GUIS_GUI_COLLECTION_SYSTEM_OPTIONS_H #include "components/MenuComponent.h" +#include "GuiSettings.h" template class OptionListComponent; class SwitchComponent; class SystemData; -class GuiCollectionSystemsOptions : public GuiComponent +class GuiCollectionSystemsOptions : public GuiSettings { public: GuiCollectionSystemsOptions(Window* window); ~GuiCollectionSystemsOptions(); - bool input(InputConfig* config, Input input) override; - - virtual std::vector getHelpPrompts() override; private: void initializeMenu(); - void applySettings(); void addSystemsToMenu(); - void addEntry(const char* name, unsigned int color, bool add_arrow, const std::function& func); + void updateSettings(std::string newAutoSettings, std::string newCustomSettings); void createCollection(std::string inName); void exitEditMode(); std::shared_ptr< OptionListComponent > autoOptionList; std::shared_ptr< OptionListComponent > customOptionList; - std::shared_ptr sortAllSystemsSwitch; - std::shared_ptr bundleCustomCollections; - std::shared_ptr toggleSystemNameInCollections; - MenuComponent mMenu; + SystemData* mSystem; }; diff --git a/es-app/src/guis/GuiFastSelect.cpp b/es-app/src/guis/GuiFastSelect.cpp index b6d16d456e..4b71eb5b99 100644 --- a/es-app/src/guis/GuiFastSelect.cpp +++ b/es-app/src/guis/GuiFastSelect.cpp @@ -73,14 +73,14 @@ bool GuiFastSelect::input(InputConfig* config, Input input) return true; }else if(config->isMappedLike("left", input) && input.value != 0) { - mSortId = (mSortId + 1) % FileSorts::SortTypes.size(); + mSortId = (mSortId + 1) % FileSorts::getSortTypes().size(); updateSortText(); return true; }else if(config->isMappedLike("right", input) && input.value != 0) { mSortId--; if(mSortId < 0) - mSortId += (int)FileSorts::SortTypes.size(); + mSortId += (int)FileSorts::getSortTypes().size(); updateSortText(); return true; @@ -125,18 +125,13 @@ void GuiFastSelect::scroll() void GuiFastSelect::updateSortText() { std::stringstream ss; - ss << "<- " << FileSorts::SortTypes.at(mSortId).description << " ->"; + ss << "<- " << FileSorts::getSortTypes().at(mSortId).description << " ->"; mSortText.setText(ss.str()); } void GuiFastSelect::updateGameListSort() { - const FileData::SortType& sort = FileSorts::SortTypes.at(mSortId); - - FileData* root = mGameList->getCursor()->getSystem()->getRootFolder(); - root->sort(sort); // will also recursively sort children - - // notify that the root folder was sorted + FolderData* root = mGameList->getCursor()->getSystem()->getRootFolder(); mGameList->onFileChanged(root, FILE_SORTED); } @@ -145,7 +140,7 @@ void GuiFastSelect::updateGameListCursor() const std::vector& list = mGameList->getCursor()->getParent()->getChildren(); // only skip by letter when the sort mode is alphabetical - const FileData::SortType& sort = FileSorts::SortTypes.at(mSortId); + const FileSorts::SortType& sort = FileSorts::getSortTypes().at(mSortId); if(sort.comparisonFunction != &FileSorts::compareName) return; diff --git a/es-app/src/guis/GuiGameScraper.cpp b/es-app/src/guis/GuiGameScraper.cpp index d8ebcb7160..7bcaceb587 100644 --- a/es-app/src/guis/GuiGameScraper.cpp +++ b/es-app/src/guis/GuiGameScraper.cpp @@ -13,6 +13,12 @@ GuiGameScraper::GuiGameScraper(Window* window, ScraperSearchParams params, std:: mSearchParams(params), mClose(false) { + auto theme = ThemeData::getMenuTheme(); + mBox.setImagePath(theme->Background.path); + mBox.setEdgeColor(theme->Background.color); + mBox.setCenterColor(theme->Background.centerColor); + mBox.setCornerSize(theme->Background.cornerSize); + PowerSaver::pause(); addChild(&mBox); addChild(&mGrid); @@ -20,13 +26,13 @@ GuiGameScraper::GuiGameScraper(Window* window, ScraperSearchParams params, std:: // row 0 is a spacer mGameName = std::make_shared(mWindow, Utils::String::toUpper(Utils::FileSystem::getFileName(mSearchParams.game->getPath())), - Font::get(FONT_SIZE_MEDIUM), 0x777777FF, ALIGN_CENTER); + theme->Text.font, theme->Text.color, ALIGN_CENTER); mGrid.setEntry(mGameName, Vector2i(0, 1), false, true); // row 2 is a spacer - mSystemName = std::make_shared(mWindow, Utils::String::toUpper(mSearchParams.system->getFullName()), Font::get(FONT_SIZE_SMALL), - 0x888888FF, ALIGN_CENTER); + mSystemName = std::make_shared(mWindow, Utils::String::toUpper(mSearchParams.system->getFullName()), + theme->TextSmall.font, theme->TextSmall.color , ALIGN_CENTER); mGrid.setEntry(mSystemName, Vector2i(0, 3), false, true); // row 4 is a spacer @@ -38,11 +44,11 @@ GuiGameScraper::GuiGameScraper(Window* window, ScraperSearchParams params, std:: // buttons std::vector< std::shared_ptr > buttons; - buttons.push_back(std::make_shared(mWindow, "INPUT", "search", [&] { + buttons.push_back(std::make_shared(mWindow, _("INPUT"), _("search"), [&] { mSearch->openInputScreen(mSearchParams); mGrid.resetCursor(); })); - buttons.push_back(std::make_shared(mWindow, "CANCEL", "cancel", [&] { delete this; })); + buttons.push_back(std::make_shared(mWindow, _("CANCEL"), _("CANCEL"), [&] { delete this; })); mButtonGrid = makeButtonGrid(mWindow, buttons); mGrid.setEntry(mButtonGrid, Vector2i(0, 6), true, false); diff --git a/es-app/src/guis/GuiGamelistFilter.cpp b/es-app/src/guis/GuiGamelistFilter.cpp index f7a68b67ca..e9691130af 100644 --- a/es-app/src/guis/GuiGamelistFilter.cpp +++ b/es-app/src/guis/GuiGamelistFilter.cpp @@ -15,20 +15,20 @@ void GuiGamelistFilter::initializeMenu() // get filters from system - mFilterIndex = mSystem->getIndex(); + mFilterIndex = mSystem->getIndex(true); ComponentListRow row; - + // show filtered menu row.elements.clear(); - row.addElement(std::make_shared(mWindow, "RESET ALL FILTERS", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, _("RESET ALL FILTERS"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); row.makeAcceptInputHandler(std::bind(&GuiGamelistFilter::resetAllFilters, this)); mMenu.addRow(row); row.elements.clear(); addFiltersToMenu(); - mMenu.addButton("BACK", "back", std::bind(&GuiGamelistFilter::applyFilters, this)); + mMenu.addButton(_("BACK"), _("BACK"), std::bind(&GuiGamelistFilter::applyFilters, this)); mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); } @@ -61,10 +61,9 @@ void GuiGamelistFilter::addFiltersToMenu() FilterIndexType type = (*it).type; // type of filter std::map* allKeys = (*it).allIndexKeys; // all possible filters for this type - std::string menuLabel = (*it).menuLabel; // text to show in menu + std::string menuLabel = _((*it).menuLabel); // text to show in menu std::shared_ptr< OptionListComponent > optionList; - - + // add filters (with first one selected) ComponentListRow row; @@ -112,6 +111,6 @@ bool GuiGamelistFilter::input(InputConfig* config, Input input) std::vector GuiGamelistFilter::getHelpPrompts() { std::vector prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", "back")); + prompts.push_back(HelpPrompt("b", _("BACK"))); return prompts; } diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index 764ef0b3fe..d35b64bede 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -1,267 +1,549 @@ -#include "GuiGamelistOptions.h" - -#include "guis/GuiGamelistFilter.h" -#include "scrapers/Scraper.h" -#include "views/gamelist/IGameListView.h" -#include "views/UIModeController.h" -#include "views/ViewController.h" -#include "CollectionSystemManager.h" -#include "FileFilterIndex.h" -#include "FileSorts.h" -#include "GuiMetaDataEd.h" -#include "SystemData.h" - -GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : GuiComponent(window), - mSystem(system), mMenu(window, "OPTIONS"), fromPlaceholder(false), mFiltersChanged(false) -{ - addChild(&mMenu); - - // check it's not a placeholder folder - if it is, only show "Filter Options" - FileData* file = getGamelist()->getCursor(); - fromPlaceholder = file->isPlaceHolder(); - ComponentListRow row; - - if (!fromPlaceholder) { - // jump to letter - row.elements.clear(); - - // define supported character range - // this range includes all numbers, capital letters, and most reasonable symbols - char startChar = '!'; - char endChar = '_'; - - char curChar = (char)toupper(getGamelist()->getCursor()->getSortName()[0]); - if(curChar < startChar || curChar > endChar) - curChar = startChar; - - mJumpToLetterList = std::make_shared(mWindow, "JUMP TO...", false); - for (char c = startChar; c <= endChar; c++) - { - // check if c is a valid first letter in current list - const std::vector& files = getGamelist()->getCursor()->getParent()->getChildrenListToDisplay(); - for (auto file : files) - { - char candidate = (char)toupper(file->getSortName()[0]); - if (c == candidate) - { - mJumpToLetterList->add(std::string(1, c), c, c == curChar); - break; - } - } - } - - row.addElement(std::make_shared(mWindow, "JUMP TO...", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - row.addElement(mJumpToLetterList, false); - row.input_handler = [&](InputConfig* config, Input input) { - if(config->isMappedTo("a", input) && input.value) - { - jumpToLetter(); - return true; - } - else if(mJumpToLetterList->input(config, input)) - { - return true; - } - return false; - }; - mMenu.addRow(row); - - // sort list by - mListSort = std::make_shared(mWindow, "SORT GAMES BY", false); - for(unsigned int i = 0; i < FileSorts::SortTypes.size(); i++) - { - const FileData::SortType& sort = FileSorts::SortTypes.at(i); - mListSort->add(sort.description, &sort, i == 0); // TODO - actually make the sort type persistent - } - - mMenu.addWithLabel("SORT GAMES BY", mListSort); - } - // show filtered menu - if(!Settings::getInstance()->getBool("ForceDisableFilters")) - { - row.elements.clear(); - row.addElement(std::make_shared(mWindow, "FILTER GAMELIST", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - row.addElement(makeArrow(mWindow), false); - row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::openGamelistFilter, this)); - mMenu.addRow(row); - } - - std::map customCollections = CollectionSystemManager::get()->getCustomCollectionSystems(); - - if(UIModeController::getInstance()->isUIModeFull() && - ((customCollections.find(system->getName()) != customCollections.cend() && CollectionSystemManager::get()->getEditingCollection() != system->getName()) || - CollectionSystemManager::get()->getCustomCollectionsBundle()->getName() == system->getName())) - { - row.elements.clear(); - row.addElement(std::make_shared(mWindow, "ADD/REMOVE GAMES TO THIS GAME COLLECTION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::startEditMode, this)); - mMenu.addRow(row); - } - - if(UIModeController::getInstance()->isUIModeFull() && CollectionSystemManager::get()->isEditing()) - { - row.elements.clear(); - row.addElement(std::make_shared(mWindow, "FINISH EDITING '" + Utils::String::toUpper(CollectionSystemManager::get()->getEditingCollection()) + "' COLLECTION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::exitEditMode, this)); - mMenu.addRow(row); - } - - if (UIModeController::getInstance()->isUIModeFull() && !fromPlaceholder && !(mSystem->isCollection() && file->getType() == FOLDER)) - { - row.elements.clear(); - row.addElement(std::make_shared(mWindow, "EDIT THIS GAME'S METADATA", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - row.addElement(makeArrow(mWindow), false); - row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::openMetaDataEd, this)); - mMenu.addRow(row); - } - - // center the menu - setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); - mMenu.setPosition((mSize.x() - mMenu.getSize().x()) / 2, (mSize.y() - mMenu.getSize().y()) / 2); -} - -GuiGamelistOptions::~GuiGamelistOptions() -{ - // apply sort - if (!fromPlaceholder) { - FileData* root = mSystem->getRootFolder(); - root->sort(*mListSort->getSelected()); // will also recursively sort children - - // notify that the root folder was sorted - getGamelist()->onFileChanged(root, FILE_SORTED); - } - if (mFiltersChanged) - { - // only reload full view if we came from a placeholder - // as we need to re-display the remaining elements for whatever new - // game is selected - ViewController::get()->reloadGameListView(mSystem); - } -} - -void GuiGamelistOptions::openGamelistFilter() -{ - mFiltersChanged = true; - GuiGamelistFilter* ggf = new GuiGamelistFilter(mWindow, mSystem); - mWindow->pushGui(ggf); -} - -void GuiGamelistOptions::startEditMode() -{ - std::string editingSystem = mSystem->getName(); - // need to check if we're editing the collections bundle, as we will want to edit the selected collection within - if(editingSystem == CollectionSystemManager::get()->getCustomCollectionsBundle()->getName()) - { - FileData* file = getGamelist()->getCursor(); - // do we have the cursor on a specific collection? - if (file->getType() == FOLDER) - { - editingSystem = file->getName(); - } - else - { - // we are inside a specific collection. We want to edit that one. - editingSystem = file->getSystem()->getName(); - } - } - CollectionSystemManager::get()->setEditMode(editingSystem); - delete this; -} - -void GuiGamelistOptions::exitEditMode() -{ - CollectionSystemManager::get()->exitEditMode(); - delete this; -} - -void GuiGamelistOptions::openMetaDataEd() -{ - // open metadata editor - // get the FileData that hosts the original metadata - FileData* file = getGamelist()->getCursor()->getSourceFileData(); - ScraperSearchParams p; - p.game = file; - p.system = file->getSystem(); - - std::function deleteBtnFunc; - - if (file->getType() == FOLDER) - { - deleteBtnFunc = NULL; - } - else - { - deleteBtnFunc = [this, file] { - CollectionSystemManager::get()->deleteCollectionFiles(file); - ViewController::get()->getGameListView(file->getSystem()).get()->remove(file, true); - }; - } - - mWindow->pushGui(new GuiMetaDataEd(mWindow, &file->metadata, file->metadata.getMDD(), p, Utils::FileSystem::getFileName(file->getPath()), - std::bind(&IGameListView::onFileChanged, ViewController::get()->getGameListView(file->getSystem()).get(), file, FILE_METADATA_CHANGED), deleteBtnFunc)); -} - -void GuiGamelistOptions::jumpToLetter() -{ - char letter = mJumpToLetterList->getSelected(); - IGameListView* gamelist = getGamelist(); - - // this is a really shitty way to get a list of files - const std::vector& files = gamelist->getCursor()->getParent()->getChildrenListToDisplay(); - - long min = 0; - long max = (long)files.size() - 1; - long mid = 0; - - while(max >= min) - { - mid = ((max - min) / 2) + min; - - // game somehow has no first character to check - if(files.at(mid)->getName().empty()) - continue; - - char checkLetter = (char)toupper(files.at(mid)->getSortName()[0]); - - if(checkLetter < letter) - min = mid + 1; - else if(checkLetter > letter || (mid > 0 && (letter == toupper(files.at(mid - 1)->getSortName()[0])))) - max = mid - 1; - else - break; //exact match found - } - - gamelist->setCursor(files.at(mid)); - - delete this; -} - -bool GuiGamelistOptions::input(InputConfig* config, Input input) -{ - if((config->isMappedTo("b", input) || config->isMappedTo("select", input)) && input.value) - { - delete this; - return true; - } - - return mMenu.input(config, input); -} - -HelpStyle GuiGamelistOptions::getHelpStyle() -{ - HelpStyle style = HelpStyle(); - style.applyTheme(mSystem->getTheme(), "system"); - return style; -} - -std::vector GuiGamelistOptions::getHelpPrompts() -{ - auto prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", "close")); - return prompts; -} - -IGameListView* GuiGamelistOptions::getGamelist() -{ - return ViewController::get()->getGameListView(mSystem).get(); -} +#include "GuiGamelistOptions.h" + +#include "guis/GuiGamelistFilter.h" +#include "scrapers/Scraper.h" +#include "views/gamelist/IGameListView.h" +#include "views/UIModeController.h" +#include "views/ViewController.h" +#include "CollectionSystemManager.h" +#include "FileFilterIndex.h" +#include "FileSorts.h" +#include "GuiMetaDataEd.h" +#include "SystemData.h" +#include "components/SwitchComponent.h" +#include "animations/LambdaAnimation.h" +#include "guis/GuiTextEditPopup.h" +#include "guis/GuiTextEditPopupKeyboard.h" +#include "guis/GuiMsgBox.h" +#include "scrapers/ThreadedScraper.h" +#include "guis/GuiMenu.h" + +std::vector GuiGamelistOptions::gridSizes { + "automatic", + + "1x1", + + "2x1", + "2x2", + "2x3", + "2x4", + "2x5", + "2x6", + "2x7", + + "3x1", + "3x2", + "3x3", + "3x4", + "3x5", + "3x6", + "3x7", + + "4x1", + "4x2", + "4x3", + "4x4", + "4x5", + "4x6", + "4x7", + + "5x1", + "5x2", + "5x3", + "5x4", + "5x5", + "5x6", + "5x7", + + "6x1", + "6x2", + "6x3", + "6x4", + "6x5", + "6x6", + "6x7", + + "7x1", + "7x2", + "7x3", + "7x4", + "7x5", + "7x6", + "7x7" +}; + +GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system, bool showGridFeatures) : GuiComponent(window), + mSystem(system), mMenu(window, "OPTIONS"), fromPlaceholder(false), mFiltersChanged(false), mReloadAll(false) +{ + auto theme = ThemeData::getMenuTheme(); + + mGridSize = nullptr; + addChild(&mMenu); + + if (!Settings::getInstance()->getBool("ForceDisableFilters")) + addTextFilterToMenu(); + + // check it's not a placeholder folder - if it is, only show "Filter Options" + FileData* file = getGamelist()->getCursor(); + fromPlaceholder = file->isPlaceHolder(); + ComponentListRow row; + + if (!fromPlaceholder) + { + // jump to letter + row.elements.clear(); + + std::vector letters = getGamelist()->getEntriesLetters(); + if (!letters.empty()) + { + mJumpToLetterList = std::make_shared(mWindow, _("JUMP TO..."), false); // batocera + + char curChar = (char)toupper(getGamelist()->getCursor()->getName()[0]); + + if (std::find(letters.begin(), letters.end(), std::string(1, curChar)) == letters.end()) + curChar = letters.at(0)[0]; + + for (auto letter : letters) + mJumpToLetterList->add(letter, letter[0], letter[0] == curChar); + + row.addElement(std::make_shared(mWindow, _("JUMP TO..."), theme->Text.font, theme->Text.color), true); // batocera + row.addElement(mJumpToLetterList, false); + row.input_handler = [&](InputConfig* config, Input input) + { + if (config->isMappedTo("a", input) && input.value) + { + jumpToLetter(); + return true; + } + else if (mJumpToLetterList->input(config, input)) + { + return true; + } + return false; + }; + mMenu.addRow(row); + } + } + + // sort list by + unsigned int currentSortId = mSystem->getSortId(); + if (currentSortId > FileSorts::getSortTypes().size()) { + currentSortId = 0; + } + + mListSort = std::make_shared(mWindow, _("SORT GAMES BY"), false); + for(unsigned int i = 0; i < FileSorts::getSortTypes().size(); i++) + { + const FileSorts::SortType& sort = FileSorts::getSortTypes().at(i); + mListSort->add(sort.icon + sort.description, sort.id, sort.id == currentSortId); // TODO - actually make the sort type persistent + } + + if (!mListSort->hasSelection()) + mListSort->selectFirstItem(); + + mMenu.addWithLabel(_("SORT GAMES BY"), mListSort); + + + + auto glv = ViewController::get()->getGameListView(system); + + std::string viewName = glv->getName(); + + + + //else + { + // GameList view style + mViewMode = std::make_shared< OptionListComponent >(mWindow, _("GAMELIST VIEW STYLE"), false); + std::vector> styles; + styles.push_back(std::pair("automatic", _("automatic"))); + + auto mViews = system->getTheme()->getViewsOfTheme(); + for (auto it = mViews.cbegin(); it != mViews.cend(); ++it) + { + if (it->first == "basic" || it->first == "detailed" || it->first == "grid") + styles.push_back(std::pair(it->first, _(it->first.c_str()))); + else + styles.push_back(*it); + } + + std::string viewMode = system->getSystemViewMode(); + + bool found = false; + for (auto it = styles.cbegin(); it != styles.cend(); it++) + { + bool sel = (viewMode.empty() && it->first == "automatic") || viewMode == it->first; + if (sel) + found = true; + + mViewMode->add(it->second, it->first, sel); + } + + if (!found) + mViewMode->selectFirstItem(); + + mMenu.addWithLabel(_("GAMELIST VIEW STYLE"), mViewMode); + + auto subsetNames = system->getTheme()->getSubSetNames(viewName); + if (subsetNames.size() > 0) + { + mMenu.addEntry(_("VIEW CUSTOMISATION"), true, [this, system]() { GuiMenu::openThemeConfiguration(mWindow, this, nullptr, system->getThemeFolder()); }); + } + else if (showGridFeatures) // Grid size override + { + auto gridOverride = system->getGridSizeOverride(); + auto ovv = std::to_string((int)gridOverride.x()) + "x" + std::to_string((int)gridOverride.y()); + + mGridSize = std::make_shared>(mWindow, _("GRID SIZE"), false); + + found = false; + for (auto it = gridSizes.cbegin(); it != gridSizes.cend(); it++) + { + bool sel = (gridOverride == Vector2f(0, 0) && *it == "automatic") || ovv == *it; + if (sel) + found = true; + + mGridSize->add(_(*it), *it, sel); + } + + if (!found) + mGridSize->selectFirstItem(); + + mMenu.addWithLabel(_("GRID SIZE"), mGridSize); + } + } + + // show filtered menu + if(!Settings::getInstance()->getBool("ForceDisableFilters")) + { + row.elements.clear(); + row.addElement(std::make_shared(mWindow, _("APPLY FILTER"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); + row.addElement(makeArrow(mWindow), false); + row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::openGamelistFilter, this)); + mMenu.addRow(row); + } + + // Show favorites first in gamelists + auto favoritesFirstSwitch = std::make_shared(mWindow); + favoritesFirstSwitch->setState(Settings::getInstance()->getBool("FavoritesFirst")); + mMenu.addWithLabel(_("SHOW FAVORITES ON TOP"), favoritesFirstSwitch); + addSaveFunc([favoritesFirstSwitch, this] + { + if (Settings::getInstance()->setBool("FavoritesFirst", favoritesFirstSwitch->getState())) + mReloadAll = true; + }); + + // hidden files + auto hidden_files = std::make_shared(mWindow); + hidden_files->setState(Settings::getInstance()->getBool("ShowHiddenFiles")); + mMenu.addWithLabel(_("SHOW HIDDEN FILES"), hidden_files); + addSaveFunc([hidden_files, this] + { + if (Settings::getInstance()->setBool("ShowHiddenFiles", hidden_files->getState())) + mReloadAll = true; + }); + + // Folder View Mode + auto foldersBehavior = std::make_shared< OptionListComponent >(mWindow, _("SHOW FOLDERS"), false); + std::vector folders; + folders.push_back("always"); + folders.push_back("never"); + folders.push_back("having multiple games"); + + for (auto it = folders.cbegin(); it != folders.cend(); it++) + foldersBehavior->add(_(it->c_str()), *it, Settings::getInstance()->getString("FolderViewMode") == *it); + + mMenu.addWithLabel(_("SHOW FOLDERS"), foldersBehavior); + addSaveFunc([this, foldersBehavior] + { + if (Settings::getInstance()->setString("FolderViewMode", foldersBehavior->getSelected())) + mReloadAll = true; + }); + + std::map customCollections = CollectionSystemManager::get()->getCustomCollectionSystems(); + + if(UIModeController::getInstance()->isUIModeFull() && + ((customCollections.find(system->getName()) != customCollections.cend() && CollectionSystemManager::get()->getEditingCollection() != system->getName()) || + CollectionSystemManager::get()->getCustomCollectionsBundle()->getName() == system->getName())) + { + row.elements.clear(); + row.addElement(std::make_shared(mWindow, _("ADD/REMOVE GAMES TO THIS GAME COLLECTION"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); + row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::startEditMode, this)); + mMenu.addRow(row); + } + + if(UIModeController::getInstance()->isUIModeFull() && CollectionSystemManager::get()->isEditing()) + { + row.elements.clear(); + row.addElement(std::make_shared(mWindow, _("FINISH EDITING")+" '" + Utils::String::toUpper(CollectionSystemManager::get()->getEditingCollection()) + "' "+_("COLLECTION"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); + row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::exitEditMode, this)); + mMenu.addRow(row); + } + + if (UIModeController::getInstance()->isUIModeFull() && !fromPlaceholder && !(mSystem->isCollection() && file->getType() == FOLDER)) + { + row.elements.clear(); + row.addElement(std::make_shared(mWindow, _("EDIT THIS GAME'S METADATA"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); + row.addElement(makeArrow(mWindow), false); + row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::openMetaDataEd, this)); + mMenu.addRow(row); + } + + // center the menu + setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); + mMenu.animateTo(Vector2f((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, (Renderer::getScreenHeight() - mMenu.getSize().y()) / 2)); +} + +GuiGamelistOptions::~GuiGamelistOptions() +{ + if (mSystem == nullptr) + return; + + for (auto it = mSaveFuncs.cbegin(); it != mSaveFuncs.cend(); it++) + (*it)(); + + // apply sort + if (!fromPlaceholder && mListSort->getSelected() != mSystem->getSortId()) + { + mSystem->setSortId(mListSort->getSelected()); + + FolderData* root = mSystem->getRootFolder(); + getGamelist()->onFileChanged(root, FILE_SORTED); + } + + Vector2f gridSizeOverride(0, 0); + + if (mGridSize != NULL) + { + auto str = mGridSize->getSelected(); + + size_t divider = str.find('x'); + if (divider != std::string::npos) + { + std::string first = str.substr(0, divider); + std::string second = str.substr(divider + 1, std::string::npos); + + gridSizeOverride = Vector2f((float)atof(first.c_str()), (float)atof(second.c_str())); + } + } + else + gridSizeOverride = mSystem->getGridSizeOverride(); + + std::string viewMode = mViewMode->getSelected(); + + if (mSystem->getSystemViewMode() != (viewMode == "automatic" ? "" : viewMode)) + { + for (auto sm : Settings::getInstance()->getStringMap()) + if (Utils::String::startsWith(sm.first, "subset." + mSystem->getThemeFolder() + ".")) + Settings::getInstance()->setString(sm.first, ""); + } + + bool viewModeChanged = mSystem->setSystemViewMode(viewMode, gridSizeOverride); + + Settings::getInstance()->saveFile(); + + if (mReloadAll) + { + mWindow->renderLoadingScreen(_("Loading...")); + ViewController::get()->reloadAll(mWindow); + mWindow->endRenderLoadingScreen(); + } + else if (mFiltersChanged || viewModeChanged) + { + // only reload full view if we came from a placeholder + // as we need to re-display the remaining elements for whatever new + // game is selected + mSystem->loadTheme(); + ViewController::get()->reloadGameListView(mSystem); + } +} + +void GuiGamelistOptions::addTextFilterToMenu() +{ + auto theme = ThemeData::getMenuTheme(); + std::shared_ptr font = theme->Text.font; + unsigned int color = theme->Text.color; + + ComponentListRow row; + + auto lbl = std::make_shared(mWindow, _("FILTER GAMES BY TEXT"), font, color); + row.addElement(lbl, true); // label + + std::string searchText; + + auto idx = mSystem->getIndex(false); + if (idx != nullptr) + searchText = idx->getTextFilter(); + + mTextFilter = std::make_shared(mWindow, searchText, font, color, ALIGN_RIGHT); + row.addElement(mTextFilter, true); + + auto spacer = std::make_shared(mWindow); + spacer->setSize(Renderer::getScreenWidth() * 0.005f, 0); + row.addElement(spacer, false); + + auto bracket = std::make_shared(mWindow); + + auto searchIcon = theme->getMenuIcon("searchIcon"); + bracket->setImage(searchIcon.empty() ? ":/search.svg" : searchIcon); + + bracket->setResize(Vector2f(0, lbl->getFont()->getLetterHeight())); + row.addElement(bracket, false); + + auto updateVal = [this](const std::string& newVal) + { + mTextFilter->setValue(Utils::String::toUpper(newVal)); + + auto index = mSystem->getIndex(!newVal.empty()); + if (index != nullptr) + { + mFiltersChanged = true; + + index->setTextFilter(newVal); + if (!index->isFiltered()) + mSystem->deleteIndex(); + + delete this; + } + }; + + row.makeAcceptInputHandler([this, updateVal] + { + mWindow->pushGui(new GuiTextEditPopupKeyboard(mWindow, _("FILTER GAMES BY TEXT"), mTextFilter->getValue(), updateVal, false)); + }); + + mMenu.addRow(row); +} + +void GuiGamelistOptions::openGamelistFilter() +{ + mFiltersChanged = true; + GuiGamelistFilter* ggf = new GuiGamelistFilter(mWindow, mSystem); + mWindow->pushGui(ggf); +} + +void GuiGamelistOptions::startEditMode() +{ + std::string editingSystem = mSystem->getName(); + // need to check if we're editing the collections bundle, as we will want to edit the selected collection within + if(editingSystem == CollectionSystemManager::get()->getCustomCollectionsBundle()->getName()) + { + FileData* file = getGamelist()->getCursor(); + // do we have the cursor on a specific collection? + if (file->getType() == FOLDER) + { + editingSystem = file->getName(); + } + else + { + // we are inside a specific collection. We want to edit that one. + editingSystem = file->getSystem()->getName(); + } + } + CollectionSystemManager::get()->setEditMode(editingSystem); + delete this; +} + +void GuiGamelistOptions::exitEditMode() +{ + CollectionSystemManager::get()->exitEditMode(); + delete this; +} + +void GuiGamelistOptions::openMetaDataEd() +{ + if (ThreadedScraper::isRunning()) + { + mWindow->pushGui(new GuiMsgBox(mWindow, _("THIS FUNCTION IS DISABLED WHEN SCRAPING IS RUNNING"))); + return; + } + + // open metadata editor + // get the FileData that hosts the original metadata + FileData* file = getGamelist()->getCursor()->getSourceFileData(); + ScraperSearchParams p; + p.game = file; + p.system = file->getSystem(); + + std::function deleteBtnFunc; + + if (file->getType() == FOLDER) + { + deleteBtnFunc = NULL; + } + else + { + deleteBtnFunc = [this, file] { + CollectionSystemManager::get()->deleteCollectionFiles(file); + ViewController::get()->getGameListView(file->getSystem()).get()->remove(file, true); + }; + } + + mWindow->pushGui(new GuiMetaDataEd(mWindow, &file->getMetadata(), file->getMetadata().getMDD(), p, Utils::FileSystem::getFileName(file->getPath()), + std::bind(&IGameListView::onFileChanged, ViewController::get()->getGameListView(file->getSystem()).get(), file, FILE_METADATA_CHANGED), deleteBtnFunc, file)); +} + +void GuiGamelistOptions::jumpToLetter() +{ + char letter = mJumpToLetterList->getSelected(); + IGameListView* gamelist = getGamelist(); + + // this is a really shitty way to get a list of files + const std::vector& files = gamelist->getCursor()->getParent()->getChildrenListToDisplay(); + + long min = 0; + long max = (long)files.size() - 1; + long mid = 0; + + while(max >= min) + { + mid = ((max - min) / 2) + min; + + // game somehow has no first character to check + if(files.at(mid)->getName().empty()) + continue; + + char checkLetter = (char)toupper(files.at(mid)->getName()[0]); + + if(checkLetter < letter) + min = mid + 1; + else if(checkLetter > letter || (mid > 0 && (letter == toupper(files.at(mid - 1)->getName()[0])))) + max = mid - 1; + else + break; //exact match found + } + + gamelist->setCursor(files.at(mid)); + + delete this; +} + +bool GuiGamelistOptions::input(InputConfig* config, Input input) +{ + if((config->isMappedTo("b", input) || config->isMappedTo("select", input)) && input.value) + { + delete this; + return true; + } + + return mMenu.input(config, input); +} + +HelpStyle GuiGamelistOptions::getHelpStyle() +{ + HelpStyle style = HelpStyle(); + style.applyTheme(mSystem->getTheme(), "system"); + return style; +} + +std::vector GuiGamelistOptions::getHelpPrompts() +{ + auto prompts = mMenu.getHelpPrompts(); + prompts.push_back(HelpPrompt("b", _("CLOSE"))); + return prompts; +} + +IGameListView* GuiGamelistOptions::getGamelist() +{ + return ViewController::get()->getGameListView(mSystem).get(); +} diff --git a/es-app/src/guis/GuiGamelistOptions.h b/es-app/src/guis/GuiGamelistOptions.h index acf861669a..379ecc1dfc 100644 --- a/es-app/src/guis/GuiGamelistOptions.h +++ b/es-app/src/guis/GuiGamelistOptions.h @@ -4,6 +4,7 @@ #include "components/MenuComponent.h" #include "components/OptionListComponent.h" +#include "components/SliderComponent.h" #include "FileData.h" #include "GuiComponent.h" @@ -13,14 +14,19 @@ class SystemData; class GuiGamelistOptions : public GuiComponent { public: - GuiGamelistOptions(Window* window, SystemData* system); + GuiGamelistOptions(Window* window, SystemData* system, bool showGridFeatures = false); virtual ~GuiGamelistOptions(); virtual bool input(InputConfig* config, Input input) override; virtual std::vector getHelpPrompts() override; virtual HelpStyle getHelpStyle() override; + static std::vector gridSizes; + private: + inline void addSaveFunc(const std::function& func) { mSaveFuncs.push_back(func); }; + + void addTextFilterToMenu(); void openGamelistFilter(); void openMetaDataEd(); void startEditMode(); @@ -32,13 +38,20 @@ class GuiGamelistOptions : public GuiComponent typedef OptionListComponent LetterList; std::shared_ptr mJumpToLetterList; - typedef OptionListComponent SortList; + typedef OptionListComponent SortList; std::shared_ptr mListSort; + std::shared_ptr mTextFilter; + std::shared_ptr> mViewMode; + std::shared_ptr> mGridSize; + SystemData* mSystem; IGameListView* getGamelist(); bool fromPlaceholder; bool mFiltersChanged; + + std::vector< std::function > mSaveFuncs; + bool mReloadAll; }; #endif // ES_APP_GUIS_GUI_GAME_LIST_OPTIONS_H diff --git a/es-app/src/guis/GuiGeneralScreensaverOptions.cpp b/es-app/src/guis/GuiGeneralScreensaverOptions.cpp index 69817db9ac..d42eca1134 100644 --- a/es-app/src/guis/GuiGeneralScreensaverOptions.cpp +++ b/es-app/src/guis/GuiGeneralScreensaverOptions.cpp @@ -8,39 +8,36 @@ #include "guis/GuiVideoScreensaverOptions.h" #include "Settings.h" -GuiGeneralScreensaverOptions::GuiGeneralScreensaverOptions(Window* window, const char* title) : GuiScreensaverOptions(window, title) +GuiGeneralScreensaverOptions::GuiGeneralScreensaverOptions(Window* window, std::string title) : GuiScreensaverOptions(window, title) { + auto theme = ThemeData::getMenuTheme(); + // screensaver time - auto screensaver_time = std::make_shared(mWindow, 0.f, 30.f, 1.f, "m"); + auto screensaver_time = std::make_shared(mWindow, 0.f, 120.0f, 1.f, "m"); screensaver_time->setValue((float)(Settings::getInstance()->getInt("ScreenSaverTime") / (1000 * 60))); - addWithLabel("SCREENSAVER AFTER", screensaver_time); + addWithLabel(_("SCREENSAVER AFTER"), screensaver_time); addSaveFunc([screensaver_time] { Settings::getInstance()->setInt("ScreenSaverTime", (int)Math::round(screensaver_time->getValue()) * (1000 * 60)); PowerSaver::updateTimeouts(); }); - // Allow ScreenSaver Controls - ScreenSaverControls - auto ss_controls = std::make_shared(mWindow); - ss_controls->setState(Settings::getInstance()->getBool("ScreenSaverControls")); - addWithLabel("SCREENSAVER CONTROLS", ss_controls); - addSaveFunc([ss_controls] { Settings::getInstance()->setBool("ScreenSaverControls", ss_controls->getState()); }); - // screensaver behavior - auto screensaver_behavior = std::make_shared< OptionListComponent >(mWindow, "SCREENSAVER BEHAVIOR", false); + auto screensaver_behavior = std::make_shared< OptionListComponent >(mWindow, _("SCREENSAVER BEHAVIOR"), false); std::vector screensavers; screensavers.push_back("dim"); screensavers.push_back("black"); screensavers.push_back("random video"); screensavers.push_back("slideshow"); for(auto it = screensavers.cbegin(); it != screensavers.cend(); it++) - screensaver_behavior->add(*it, *it, Settings::getInstance()->getString("ScreenSaverBehavior") == *it); - addWithLabel("SCREENSAVER BEHAVIOR", screensaver_behavior); + screensaver_behavior->add(_(it->c_str()), *it, Settings::getInstance()->getString("ScreenSaverBehavior") == *it); + addWithLabel(_("SCREENSAVER BEHAVIOR"), screensaver_behavior); addSaveFunc([this, screensaver_behavior] { - if (Settings::getInstance()->getString("ScreenSaverBehavior") != "random video" && screensaver_behavior->getSelected() == "random video") { + if (Settings::getInstance()->getString("ScreenSaverBehavior") != "random video" + && screensaver_behavior->getSelected() == "random video") { // if before it wasn't risky but now there's a risk of problems, show warning mWindow->pushGui(new GuiMsgBox(mWindow, - "The \"Random Video\" screensaver shows videos from your gamelist.\n\nIf you do not have videos, or if in several consecutive attempts the games it selects don't have videos it will default to black.\n\nMore options in the \"UI Settings\" > \"Video Screensaver\" menu.", - "OK", [] { return; })); + _("THE \"RANDOM VIDEO\" SCREENSAVER SHOWS VIDEOS FROM YOUR GAMELIST.\nIF YOU DON'T HAVE VIDEOS, OR IF NONE OF THEM CAN BE PLAYED AFTER A FEW ATTEMPTS, IT WILL DEFAULT TO \"BLACK\".\nMORE OPTIONS IN THE \"UI SETTINGS\" -> \"RANDOM VIDEO SCREENSAVER SETTINGS\" MENU."), + _("OK"), [] { return; })); } Settings::getInstance()->setString("ScreenSaverBehavior", screensaver_behavior->getSelected()); PowerSaver::updateTimeouts(); @@ -50,16 +47,22 @@ GuiGeneralScreensaverOptions::GuiGeneralScreensaverOptions(Window* window, const // show filtered menu row.elements.clear(); - row.addElement(std::make_shared(mWindow, "VIDEO SCREENSAVER SETTINGS", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, _("VIDEO SCREENSAVER SETTINGS"), theme->Text.font, theme->Text.color), true); row.addElement(makeArrow(mWindow), false); row.makeAcceptInputHandler(std::bind(&GuiGeneralScreensaverOptions::openVideoScreensaverOptions, this)); addRow(row); row.elements.clear(); - row.addElement(std::make_shared(mWindow, "SLIDESHOW SCREENSAVER SETTINGS", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, _("SLIDESHOW SCREENSAVER SETTINGS"), theme->Text.font, theme->Text.color), true); row.addElement(makeArrow(mWindow), false); row.makeAcceptInputHandler(std::bind(&GuiGeneralScreensaverOptions::openSlideshowScreensaverOptions, this)); addRow(row); + + // Allow ScreenSaver Controls - ScreenSaverControls + auto ss_controls = std::make_shared(mWindow); + ss_controls->setState(Settings::getInstance()->getBool("ScreenSaverControls")); + addWithLabel(_("SCREENSAVER CONTROLS"), ss_controls); + addSaveFunc([ss_controls] { Settings::getInstance()->setBool("ScreenSaverControls", ss_controls->getState()); }); } GuiGeneralScreensaverOptions::~GuiGeneralScreensaverOptions() @@ -67,10 +70,10 @@ GuiGeneralScreensaverOptions::~GuiGeneralScreensaverOptions() } void GuiGeneralScreensaverOptions::openVideoScreensaverOptions() { - mWindow->pushGui(new GuiVideoScreensaverOptions(mWindow, "VIDEO SCREENSAVER")); + mWindow->pushGui(new GuiVideoScreensaverOptions(mWindow, _("VIDEO SCREENSAVER").c_str())); } void GuiGeneralScreensaverOptions::openSlideshowScreensaverOptions() { - mWindow->pushGui(new GuiSlideshowScreensaverOptions(mWindow, "SLIDESHOW SCREENSAVER")); + mWindow->pushGui(new GuiSlideshowScreensaverOptions(mWindow, _("SLIDESHOW SCREENSAVER").c_str())); } diff --git a/es-app/src/guis/GuiGeneralScreensaverOptions.h b/es-app/src/guis/GuiGeneralScreensaverOptions.h index 1992d9ad15..1a850e3871 100644 --- a/es-app/src/guis/GuiGeneralScreensaverOptions.h +++ b/es-app/src/guis/GuiGeneralScreensaverOptions.h @@ -7,7 +7,7 @@ class GuiGeneralScreensaverOptions : public GuiScreensaverOptions { public: - GuiGeneralScreensaverOptions(Window* window, const char* title); + GuiGeneralScreensaverOptions(Window* window, std::string title); virtual ~GuiGeneralScreensaverOptions(); private: diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index 8e588989fb..3b238cf6e4 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -18,84 +18,219 @@ #include "VolumeControl.h" #include #include +#include "AudioManager.h" +#include "resources/TextureData.h" +#include "animations/LambdaAnimation.h" +#include "guis/GuiThemeInstall.h" +#include "GuiGamelistOptions.h" // grid sizes #include "platform.h" +#include "renderers/Renderer.h" // setSwapInterval() +#include "guis/GuiTextEditPopupKeyboard.h" +#include "scrapers/ThreadedScraper.h" +#include "ApiSystem.h" +#include "platform.h" +#include "views/gamelist/IGameListView.h" -GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, "MAIN MENU"), mVersion(window) +GuiMenu::GuiMenu(Window* window, bool animate) : GuiComponent(window), mMenu(window, _("MAIN MENU")), mVersion(window) { - bool isFullUI = UIModeController::getInstance()->isUIModeFull(); + auto theme = ThemeData::getMenuTheme(); + bool isFullUI = UIModeController::getInstance()->isUIModeFull(); + if (isFullUI) - addEntry("SCRAPER", 0x777777FF, true, [this] { openScraperSettings(); }); - - addEntry("SOUND SETTINGS", 0x777777FF, true, [this] { openSoundSettings(); }); + { + addEntry(_("UI SETTINGS"), true, [this] { openUISettings(); }, "iconUI"); + addEntry(_("CONFIGURE INPUT"), true, [this] { openConfigInput(); }, "iconControllers"); + } + addEntry(_("SOUND SETTINGS"), true, [this] { openSoundSettings(); }, "iconSound"); if (isFullUI) - addEntry("UI SETTINGS", 0x777777FF, true, [this] { openUISettings(); }); + { + addEntry(_("GAME COLLECTION SETTINGS"), true, [this] { openCollectionSystemSettings(); }, "iconGames"); - if (isFullUI) - addEntry("GAME COLLECTION SETTINGS", 0x777777FF, true, [this] { openCollectionSystemSettings(); }); + // Emulator settings + for (auto system : SystemData::sSystemVector) + { + if (system->isCollection() || system->getSystemEnvData()->mEmulators.size() == 0 || (system->getSystemEnvData()->mEmulators.size() == 1 && system->getSystemEnvData()->mEmulators[0].mCores.size() <= 1)) + continue; - if (isFullUI) - addEntry("OTHER SETTINGS", 0x777777FF, true, [this] { openOtherSettings(); }); + addEntry(_("EMULATOR SETTINGS"), true, [this] { openEmulatorSettings(); }, "iconSystem"); + break; + } + + addEntry(_("SCRAPER"), true, [this] { openScraperSettings(); }, "iconScraper"); - if (isFullUI) - addEntry("CONFIGURE INPUT", 0x777777FF, true, [this] { openConfigInput(); }); +#if WIN32 + addEntry(_("DOWNLOADS AND UPDATES"), true, [this] { openUpdateSettings(); }, "iconUpdates"); +#endif - addEntry("QUIT", 0x777777FF, true, [this] {openQuitMenu(); }); + addEntry(_("ADVANCED SETTINGS"), true, [this] { openOtherSettings(); }, "iconAdvanced"); + } + + addEntry(_("QUIT"), !Settings::getInstance()->getBool("ShowOnlyExit"), [this] {openQuitMenu(); }, "iconQuit"); addChild(&mMenu); addVersionInfo(); + setSize(mMenu.getSize()); - setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, Renderer::getScreenHeight() * 0.15f); + + if (animate) + animateTo( + Vector2f((Renderer::getScreenWidth() - mSize.x()) / 2, Renderer::getScreenHeight() * 0.9), + Vector2f((Renderer::getScreenWidth() - mSize.x()) / 2, (Renderer::getScreenHeight() - mSize.y()) / 2)); + else + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, (Renderer::getScreenHeight() - mSize.y()) / 2); } void GuiMenu::openScraperSettings() { - auto s = new GuiSettings(mWindow, "SCRAPER"); + auto s = new GuiSettings(mWindow, _("SCRAPER")); + + std::string scraper = Settings::getInstance()->getString("Scraper"); // scrape from - auto scraper_list = std::make_shared< OptionListComponent< std::string > >(mWindow, "SCRAPE FROM", false); + auto scraper_list = std::make_shared< OptionListComponent< std::string > >(mWindow, _("SCRAPE FROM"), false); std::vector scrapers = getScraperList(); // Select either the first entry of the one read from the settings, just in case the scraper from settings has vanished. - for(auto it = scrapers.cbegin(); it != scrapers.cend(); it++) - scraper_list->add(*it, *it, *it == Settings::getInstance()->getString("Scraper")); + for (auto it = scrapers.cbegin(); it != scrapers.cend(); it++) + scraper_list->add(*it, *it, *it == scraper); - s->addWithLabel("SCRAPE FROM", scraper_list); + s->addWithLabel(_("SCRAPE FROM"), scraper_list); s->addSaveFunc([scraper_list] { Settings::getInstance()->setString("Scraper", scraper_list->getSelected()); }); - // scrape ratings - auto scrape_ratings = std::make_shared(mWindow); - scrape_ratings->setState(Settings::getInstance()->getBool("ScrapeRatings")); - s->addWithLabel("SCRAPE RATINGS", scrape_ratings); - s->addSaveFunc([scrape_ratings] { Settings::getInstance()->setBool("ScrapeRatings", scrape_ratings->getState()); }); + + if (scraper == "ScreenScraper") + { + // Image source : tag + std::string imageSourceName = Settings::getInstance()->getString("ScrapperImageSrc"); + auto imageSource = std::make_shared< OptionListComponent >(mWindow, _("IMAGE SOURCE"), false); + //imageSource->add(_("NONE"), "", imageSourceName.empty()); + imageSource->add(_("SCREENSHOT"), "ss", imageSourceName == "ss"); + imageSource->add(_("TITLE SCREENSHOT"), "sstitle", imageSourceName == "sstitle"); + imageSource->add(_("MIX V1"), "mixrbv1", imageSourceName == "mixrbv1"); + imageSource->add(_("MIX V2"), "mixrbv2", imageSourceName == "mixrbv2"); + imageSource->add(_("BOX 2D"), "box-2D", imageSourceName == "box-2D"); + imageSource->add(_("BOX 3D"), "box-3D", imageSourceName == "box-3D"); + + if (!imageSource->hasSelection()) + imageSource->selectFirstItem(); + + s->addWithLabel(_("IMAGE SOURCE"), imageSource); + s->addSaveFunc([imageSource] { Settings::getInstance()->setString("ScrapperImageSrc", imageSource->getSelected()); }); + + // Box source : tag + std::string thumbSourceName = Settings::getInstance()->getString("ScrapperThumbSrc"); + auto thumbSource = std::make_shared< OptionListComponent >(mWindow, _("BOX SOURCE"), false); + thumbSource->add(_("NONE"), "", thumbSourceName.empty()); + thumbSource->add(_("BOX 2D"), "box-2D", thumbSourceName == "box-2D"); + thumbSource->add(_("BOX 3D"), "box-3D", thumbSourceName == "box-3D"); + + if (!thumbSource->hasSelection()) + thumbSource->selectFirstItem(); + + s->addWithLabel(_("BOX SOURCE"), thumbSource); + s->addSaveFunc([thumbSource] { Settings::getInstance()->setString("ScrapperThumbSrc", thumbSource->getSelected()); }); + + imageSource->setSelectedChangedCallback([this, thumbSource](std::string value) + { + if (value == "box-2D") + thumbSource->remove(_("BOX 2D")); + else + thumbSource->add(_("BOX 2D"), "box-2D", false); + + if (value == "box-3D") + thumbSource->remove(_("BOX 3D")); + else + thumbSource->add(_("BOX 3D"), "box-3D", false); + }); + + // Logo source : tag + std::string logoSourceName = Settings::getInstance()->getString("ScrapperLogoSrc"); + auto logoSource = std::make_shared< OptionListComponent >(mWindow, _("LOGO SOURCE"), false); + logoSource->add(_("NONE"), "", logoSourceName.empty()); + logoSource->add(_("WHEEL"), "wheel", logoSourceName == "wheel"); + logoSource->add(_("MARQUEE"), "marquee", logoSourceName == "marquee"); + + if (!logoSource->hasSelection()) + logoSource->selectFirstItem(); + + s->addWithLabel(_("LOGO SOURCE"), logoSource); + s->addSaveFunc([logoSource] { Settings::getInstance()->setString("ScrapperLogoSrc", logoSource->getSelected()); }); + + // scrape ratings + auto scrape_ratings = std::make_shared(mWindow); + scrape_ratings->setState(Settings::getInstance()->getBool("ScrapeRatings")); + s->addWithLabel(_("SCRAPE RATINGS"), scrape_ratings); + s->addSaveFunc([scrape_ratings] { Settings::getInstance()->setBool("ScrapeRatings", scrape_ratings->getState()); }); + + // scrape video + auto scrape_video = std::make_shared(mWindow); + scrape_video->setState(Settings::getInstance()->getBool("ScrapeVideos")); + s->addWithLabel(_("SCRAPE VIDEOS"), scrape_video); + s->addSaveFunc([scrape_video] { Settings::getInstance()->setBool("ScrapeVideos", scrape_video->getState()); }); + + // Account + createInputTextRow(s, _("USERNAME"), "ScreenScraperUser", false); + createInputTextRow(s, _("PASSWORD"), "ScreenScraperPass", true); + } + else + { + // scrape ratings + auto scrape_ratings = std::make_shared(mWindow); + scrape_ratings->setState(Settings::getInstance()->getBool("ScrapeRatings")); + s->addWithLabel(_("SCRAPE RATINGS"), scrape_ratings); // batocera + s->addSaveFunc([scrape_ratings] { Settings::getInstance()->setBool("ScrapeRatings", scrape_ratings->getState()); }); + } // scrape now ComponentListRow row; - auto openScrapeNow = [this] { mWindow->pushGui(new GuiScraperStart(mWindow)); }; + auto openScrapeNow = [this] + { + if (ThreadedScraper::isRunning()) + { + Window* window = mWindow; + + mWindow->pushGui(new GuiMsgBox(mWindow, _("SCRAPING IS RUNNING. DO YOU WANT TO STOP IT ?"), _("YES"), [this, window] + { + ThreadedScraper::stop(); + }, _("NO"), nullptr)); + + return; + } + + mWindow->pushGui(new GuiScraperStart(mWindow)); + }; std::function openAndSave = openScrapeNow; openAndSave = [s, openAndSave] { s->save(); openAndSave(); }; - row.makeAcceptInputHandler(openAndSave); + s->addEntry(_("SCRAPE NOW"), true, openAndSave, "iconScraper"); - auto scrape_now = std::make_shared(mWindow, "SCRAPE NOW", Font::get(FONT_SIZE_MEDIUM), 0x777777FF); - auto bracket = makeArrow(mWindow); - row.addElement(scrape_now, true); - row.addElement(bracket, false); - s->addRow(row); + s->updatePosition(); + + scraper_list->setSelectedChangedCallback([this, s, scraper, scraper_list](std::string value) + { + if (value != scraper && (scraper == "ScreenScraper" || value == "ScreenScraper")) + { + Settings::getInstance()->setString("Scraper", value); + delete s; + openScraperSettings(); + } + }); mWindow->pushGui(s); } void GuiMenu::openSoundSettings() { - auto s = new GuiSettings(mWindow, "SOUND SETTINGS"); - + auto s = new GuiSettings(mWindow, _("SOUND SETTINGS")); + // volume auto volume = std::make_shared(mWindow, 0.f, 100.f, 1.f, "%"); volume->setValue((float)VolumeControl::getInstance()->getVolume()); - s->addWithLabel("SYSTEM VOLUME", volume); - s->addSaveFunc([volume] { VolumeControl::getInstance()->setVolume((int)Math::round(volume->getValue())); }); + volume->setOnValueChanged([](const float &newVal) { VolumeControl::getInstance()->setVolume((int)Math::round(newVal)); }); + s->addWithLabel(_("SYSTEM VOLUME"), volume); + //s->addSaveFunc([volume] { VolumeControl::getInstance()->setVolume((int)Math::round(volume->getValue())); }); if (UIModeController::getInstance()->isUIModeFull()) { @@ -151,11 +286,47 @@ void GuiMenu::openSoundSettings() VolumeControl::getInstance()->init(); }); #endif + // disable sounds + auto music_enabled = std::make_shared(mWindow); + music_enabled->setState(Settings::getInstance()->getBool("audio.bgmusic")); + s->addWithLabel(_("FRONTEND MUSIC"), music_enabled); + s->addSaveFunc([music_enabled] { + Settings::getInstance()->setBool("audio.bgmusic", music_enabled->getState()); + if (music_enabled->getState()) + AudioManager::getInstance()->playRandomMusic(); + else + AudioManager::getInstance()->stopMusic(); + }); + + //display music titles + auto display_titles = std::make_shared(mWindow); + display_titles->setState(Settings::getInstance()->getBool("MusicTitles")); + s->addWithLabel(_("DISPLAY SONG TITLES"), display_titles); + s->addSaveFunc([display_titles] { + Settings::getInstance()->setBool("MusicTitles", display_titles->getState()); + }); + + // music per system + auto music_per_system = std::make_shared(mWindow); + music_per_system->setState(Settings::getInstance()->getBool("audio.persystem")); + s->addWithLabel(_("ONLY PLAY SYSTEM-SPECIFIC MUSIC FOLDER"), music_per_system); + s->addSaveFunc([music_per_system] { + Settings::getInstance()->setBool("audio.persystem", music_per_system->getState()); + }); + + // batocera - music per system + auto enableThemeMusics = std::make_shared(mWindow); + enableThemeMusics->setState(Settings::getInstance()->getBool("audio.thememusics")); + s->addWithLabel(_("PLAY THEME MUSICS"), enableThemeMusics); + s->addSaveFunc([enableThemeMusics] { + if (Settings::getInstance()->setBool("audio.thememusics", enableThemeMusics->getState())) + AudioManager::getInstance()->themeChanged(ViewController::get()->getState().getSystem()->getTheme(), true); + }); // disable sounds auto sounds_enabled = std::make_shared(mWindow); sounds_enabled->setState(Settings::getInstance()->getBool("EnableSounds")); - s->addWithLabel("ENABLE NAVIGATION SOUNDS", sounds_enabled); + s->addWithLabel(_("ENABLE NAVIGATION SOUNDS"), sounds_enabled); s->addSaveFunc([sounds_enabled] { if (sounds_enabled->getState() && !Settings::getInstance()->getBool("EnableSounds") @@ -169,9 +340,14 @@ void GuiMenu::openSoundSettings() auto video_audio = std::make_shared(mWindow); video_audio->setState(Settings::getInstance()->getBool("VideoAudio")); - s->addWithLabel("ENABLE VIDEO AUDIO", video_audio); + s->addWithLabel(_("ENABLE VIDEO AUDIO"), video_audio); s->addSaveFunc([video_audio] { Settings::getInstance()->setBool("VideoAudio", video_audio->getState()); }); + auto videolowermusic = std::make_shared(mWindow); + videolowermusic->setState(Settings::getInstance()->getBool("VideoLowersMusic")); + s->addWithLabel(_("LOWER MUSIC WHEN PLAYING VIDEO"), videolowermusic); + s->addSaveFunc([videolowermusic] { Settings::getInstance()->setBool("VideoLowersMusic", videolowermusic->getState()); }); + #ifdef _RPI_ // OMX player Audio Device auto omx_audio_dev = std::make_shared< OptionListComponent >(mWindow, "OMX PLAYER AUDIO DEVICE", false); @@ -197,22 +373,435 @@ void GuiMenu::openSoundSettings() #endif } + s->updatePosition(); mWindow->pushGui(s); } -void GuiMenu::openUISettings() +struct ThemeConfigOption +{ + std::string defaultSettingName; + std::string subset; + std::shared_ptr> component; +}; + +void GuiMenu::openThemeConfiguration(Window* mWindow, GuiComponent* s, std::shared_ptr> theme_set,const std::string systemTheme) { - auto s = new GuiSettings(mWindow, "UI SETTINGS"); + if (theme_set != nullptr && Settings::getInstance()->getString("ThemeSet") != theme_set->getSelected()) + { + mWindow->pushGui(new GuiMsgBox(mWindow, _("YOU MUST APPLY THE THEME BEFORE EDIT CONFIGURATION"), _("OK"))); + return; + } + + Window* window = mWindow; + + auto system = ViewController::get()->getState().getSystem(); + auto theme = system->getTheme(); + + auto themeconfig = new GuiSettings(mWindow, (systemTheme.empty() ? _("THEME CONFIGURATION") : _("VIEW CUSTOMISATION")).c_str()); + + auto themeSubSets = theme->getSubSets(); + + std::string viewName; + bool showGridFeatures = true; + if (!systemTheme.empty()) + { + auto glv = ViewController::get()->getGameListView(system); + viewName = glv->getName(); + std::string baseType = theme->getCustomViewBaseType(viewName); + + showGridFeatures = (viewName == "grid" || baseType == "grid"); + } + + + // gamelist_style + std::shared_ptr> gamelist_style = nullptr; + + if (systemTheme.empty()) + { + gamelist_style = std::make_shared< OptionListComponent >(mWindow, _("GAMELIST VIEW STYLE"), false); + + std::vector> styles; + styles.push_back(std::pair("automatic", _("automatic"))); + + if (system != NULL) + { + auto mViews = theme->getViewsOfTheme(); + for (auto it = mViews.cbegin(); it != mViews.cend(); ++it) + { + if (it->first == "basic" || it->first == "detailed" || it->first == "grid") + styles.push_back(std::pair(it->first, _(it->first.c_str()))); + else + styles.push_back(*it); + } + } + else + { + styles.push_back(std::pair("basic", _("basic"))); + styles.push_back(std::pair("detailed", _("detailed"))); + } + + auto viewPreference = systemTheme.empty() ? Settings::getInstance()->getString("GamelistViewStyle") : system->getSystemViewMode(); + if (!theme->hasView(viewPreference)) + viewPreference = "automatic"; + + for (auto it = styles.cbegin(); it != styles.cend(); it++) + gamelist_style->add(it->second, it->first, viewPreference == it->first); + + if (!gamelist_style->hasSelection()) + gamelist_style->selectFirstItem(); + + themeconfig->addWithLabel(_("GAMELIST VIEW STYLE"), gamelist_style); + } + + // Default grid size + std::shared_ptr> mGridSize = nullptr; + if (showGridFeatures && system != NULL && theme->hasView("grid")) + { + Vector2f gridOverride = + systemTheme.empty() ? Vector2f::parseString(Settings::getInstance()->getString("DefaultGridSize")) : + system->getGridSizeOverride(); + + auto ovv = std::to_string((int)gridOverride.x()) + "x" + std::to_string((int)gridOverride.y()); + + mGridSize = std::make_shared>(mWindow, _("DEFAULT GRID SIZE"), false); + + bool found = false; + for (auto it = GuiGamelistOptions::gridSizes.cbegin(); it != GuiGamelistOptions::gridSizes.cend(); it++) + { + bool sel = (gridOverride == Vector2f(0, 0) && *it == "automatic") || ovv == *it; + if (sel) + found = true; + + mGridSize->add(_(it->c_str()), *it, sel); + } + + if (!found) + mGridSize->selectFirstItem(); + + themeconfig->addWithLabel(_("DEFAULT GRID SIZE"), mGridSize); + } + + std::map options; + + for (std::string subset : theme->getSubSetNames(viewName)) + { + std::string settingName = "subset." + subset; + std::string perSystemSettingName = systemTheme.empty() ? "" : "subset." + systemTheme + "." + subset; + + if (subset == "colorset") settingName = "ThemeColorSet"; + else if (subset == "iconset") settingName = "ThemeIconSet"; + else if (subset == "menu") settingName = "ThemeMenu"; + else if (subset == "systemview") settingName = "ThemeSystemView"; + else if (subset == "gamelistview") settingName = "ThemeGamelistView"; + else if (subset == "region") settingName = "ThemeRegionName"; + + auto themeColorSets = ThemeData::getSubSet(themeSubSets, subset); + + if (themeColorSets.size() > 0) + { + auto selectedColorSet = themeColorSets.end(); + auto selectedName = !perSystemSettingName.empty() ? Settings::getInstance()->getString(perSystemSettingName) : Settings::getInstance()->getString(settingName); + + if (!perSystemSettingName.empty() && selectedName.empty()) + selectedName = Settings::getInstance()->getString(settingName); + + for (auto it = themeColorSets.begin(); it != themeColorSets.end() && selectedColorSet == themeColorSets.end(); it++) + if (it->name == selectedName) + selectedColorSet = it; + + std::shared_ptr> item = std::make_shared >(mWindow, _(("THEME " + Utils::String::toUpper(subset)).c_str()), false); + item->setTag(!perSystemSettingName.empty()? perSystemSettingName : settingName); + + for (auto it = themeColorSets.begin(); it != themeColorSets.end(); it++) + { + std::string displayName = it->displayName; + + if (!systemTheme.empty()) + { + std::string defaultValue = Settings::getInstance()->getString(settingName); + if (defaultValue.empty()) + defaultValue = system->getTheme()->getDefaultSubSetValue(subset); + + if (it->name == defaultValue) + displayName = displayName + " (" + _("DEFAULT") +")"; + } + + item->add(displayName, it->name, it == selectedColorSet); + } + + if (selectedColorSet == themeColorSets.end()) + item->selectFirstItem(); + + if (!themeColorSets.empty()) + { + std::string displayName = themeColorSets.cbegin()->subSetDisplayName; + if (!displayName.empty()) + { + std::string prefix; + + if (systemTheme.empty()) + { + for (auto subsetName : themeColorSets.cbegin()->appliesTo) + { + std::string pfx = theme->getViewDisplayName(subsetName); + if (!pfx.empty()) + { + if (prefix.empty()) + prefix = pfx; + else + prefix = prefix + ", " + pfx; + } + } + + if (!prefix.empty()) + prefix = " ("+ prefix+")"; + + } + + themeconfig->addWithLabel(displayName + prefix, item); + } + else + themeconfig->addWithLabel(_(("THEME " + Utils::String::toUpper(subset)).c_str()), item); + } + + ThemeConfigOption opt; + opt.component = item; + opt.subset = subset; + opt.defaultSettingName = settingName; + options[!perSystemSettingName.empty() ? perSystemSettingName : settingName] = opt; + } + else + { + ThemeConfigOption opt; + opt.component = nullptr; + options[!perSystemSettingName.empty() ? perSystemSettingName : settingName] = opt; + } + } + + + if (systemTheme.empty()) + { + themeconfig->addEntry(_("RESET GAMELIST CUSTOMISATIONS"), false, [s, themeconfig, window] + { + Settings::getInstance()->setString("GamelistViewStyle", ""); + Settings::getInstance()->setString("DefaultGridSize", ""); + + for (auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); sysIt++) + (*sysIt)->setSystemViewMode("automatic", Vector2f(0, 0)); + + themeconfig->setVariable("reloadAll", true); + themeconfig->close(); + }); + } + + // theme_colorset, theme_iconset, theme_menu, theme_systemview, theme_gamelistview, theme_region, + themeconfig->addSaveFunc([systemTheme, system, themeconfig, theme_set, options, gamelist_style, mGridSize, window] + { + bool reloadAll = systemTheme.empty() ? Settings::getInstance()->setString("ThemeSet", theme_set == nullptr ? "" : theme_set->getSelected()) : false; + + for (auto option : options) + { + ThemeConfigOption& opt = option.second; + + std::string value; + + if (opt.component != nullptr) + { + value = opt.component->getSelected(); + + if (!systemTheme.empty() && !value.empty()) + { + std::string defaultValue = Settings::getInstance()->getString(opt.defaultSettingName); + if (defaultValue.empty()) + defaultValue = system->getTheme()->getDefaultSubSetValue(opt.subset); + + if (value == defaultValue) + value = ""; + } + else if (systemTheme.empty() && value == system->getTheme()->getDefaultSubSetValue(opt.subset)) + value = ""; + } + + if (value != Settings::getInstance()->getString(option.first)) + reloadAll |= Settings::getInstance()->setString(option.first, value); + } + + Vector2f gridSizeOverride(0, 0); + + if (mGridSize != nullptr) + { + std::string str = mGridSize->getSelected(); + std::string value = ""; + + size_t divider = str.find('x'); + if (divider != std::string::npos) + { + std::string first = str.substr(0, divider); + std::string second = str.substr(divider + 1, std::string::npos); + + gridSizeOverride = Vector2f((float)atof(first.c_str()), (float)atof(second.c_str())); + value = Utils::String::replace(Utils::String::replace(gridSizeOverride.toString(), ".000000", ""), "0 0", ""); + } + + if (systemTheme.empty()) + reloadAll |= Settings::getInstance()->setString("DefaultGridSize", value); + } + else if (systemTheme.empty()) + reloadAll |= Settings::getInstance()->setString("DefaultGridSize", ""); + + if (systemTheme.empty()) + reloadAll |= Settings::getInstance()->setString("GamelistViewStyle", gamelist_style == nullptr ? "" : gamelist_style->getSelected()); + else + { + std::string viewMode = gamelist_style == nullptr ? system->getSystemViewMode() : gamelist_style->getSelected(); + reloadAll |= system->setSystemViewMode(viewMode, gridSizeOverride); + } + + if (reloadAll || themeconfig->getVariable("reloadAll")) + { + if (systemTheme.empty()) + { + CollectionSystemManager::get()->updateSystemsList(); + ViewController::get()->reloadAll(window); + window->endRenderLoadingScreen(); + + if (theme_set != nullptr) + { + std::string oldTheme = Settings::getInstance()->getString("ThemeSet"); + Scripting::fireEvent("theme-changed", theme_set->getSelected(), oldTheme); + } + } + else + { + system->loadTheme(); + system->resetFilters(); + ViewController::get()->reloadGameListView(system); + } + } + }); + + mWindow->pushGui(themeconfig); +} + +void GuiMenu::openUISettings() +{ + auto pthis = this; + Window* window = mWindow; + + auto s = new GuiSettings(mWindow, _("UI SETTINGS")); + + // theme set + auto theme = ThemeData::getMenuTheme(); + auto themeSets = ThemeData::getThemeSets(); + auto system = ViewController::get()->getState().getSystem(); - //UI mode - auto UImodeSelection = std::make_shared< OptionListComponent >(mWindow, "UI MODE", false); + if (!themeSets.empty()) + { + std::map::const_iterator selectedSet = themeSets.find(Settings::getInstance()->getString("ThemeSet")); + if (selectedSet == themeSets.cend()) + selectedSet = themeSets.cbegin(); + + auto theme_set = std::make_shared< OptionListComponent >(mWindow, "THEME", false); + for (auto it = themeSets.cbegin(); it != themeSets.cend(); it++) + theme_set->add(it->first, it->first, it == selectedSet); + + s->addWithLabel(_("THEME"), theme_set); + s->addSaveFunc([s, theme_set, window] + { + std::string oldTheme = Settings::getInstance()->getString("ThemeSet"); + if (oldTheme != theme_set->getSelected()) + { + Settings::getInstance()->setString("ThemeSet", theme_set->getSelected()); + + // theme changed without setting options, forcing options to avoid crash/blank theme + Settings::getInstance()->setString("ThemeRegionName", ""); + Settings::getInstance()->setString("ThemeColorSet", ""); + Settings::getInstance()->setString("ThemeIconSet", ""); + Settings::getInstance()->setString("ThemeMenu", ""); + Settings::getInstance()->setString("ThemeSystemView", ""); + Settings::getInstance()->setString("ThemeGamelistView", ""); + Settings::getInstance()->setString("GamelistViewStyle", ""); + Settings::getInstance()->setString("DefaultGridSize", ""); + + for(auto sm : Settings::getInstance()->getStringMap()) + if (Utils::String::startsWith(sm.first, "subset.")) + Settings::getInstance()->setString(sm.first, ""); + + for (auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); sysIt++) + (*sysIt)->setSystemViewMode("automatic", Vector2f(0, 0)); + + s->setVariable("reloadCollections", true); + s->setVariable("reloadAll", true); + s->setVariable("reloadGuiMenu", true); + + Scripting::fireEvent("theme-changed", theme_set->getSelected(), oldTheme); + } + }); + + bool showThemeConfiguration = system->getTheme()->hasSubsets() || system->getTheme()->hasView("grid"); + if (showThemeConfiguration) + { + s->addSubMenu(_("THEME CONFIGURATION"), [this, s, theme_set]() { openThemeConfiguration(mWindow, s, theme_set); }); + } + else // GameList view style only, acts like Retropie for simple themes + { + auto gamelist_style = std::make_shared< OptionListComponent >(mWindow, _("GAMELIST VIEW STYLE"), false); + std::vector> styles; + styles.push_back(std::pair("automatic", _("automatic"))); + + auto system = ViewController::get()->getState().getSystem(); + if (system != NULL) + { + auto mViews = system->getTheme()->getViewsOfTheme(); + for (auto it = mViews.cbegin(); it != mViews.cend(); ++it) + styles.push_back(*it); + } + else + { + styles.push_back(std::pair("basic", _("basic"))); + styles.push_back(std::pair("detailed", _("detailed"))); + styles.push_back(std::pair("video", _("video"))); + styles.push_back(std::pair("grid", _("grid"))); + } + + auto viewPreference = Settings::getInstance()->getString("GamelistViewStyle"); + if (!system->getTheme()->hasView(viewPreference)) + viewPreference = "automatic"; + + for (auto it = styles.cbegin(); it != styles.cend(); it++) + gamelist_style->add(it->second, it->first, viewPreference == it->first); + + s->addWithLabel(_("GAMELIST VIEW STYLE"), gamelist_style); + s->addSaveFunc([s, gamelist_style, window] + { + if (Settings::getInstance()->setString("GamelistViewStyle", gamelist_style->getSelected())) + { + s->setVariable("reloadAll", true); + s->setVariable("reloadGuiMenu", true); + } + }); + } + } + + // screensaver + ComponentListRow screensaver_row; + screensaver_row.elements.clear(); + screensaver_row.addElement(std::make_shared(mWindow, _("SCREENSAVER SETTINGS"), theme->Text.font, theme->Text.color), true); + screensaver_row.addElement(makeArrow(mWindow), false); + screensaver_row.makeAcceptInputHandler(std::bind(&GuiMenu::openScreensaverOptions, this)); + s->addRow(screensaver_row); + + + //#ifndef WIN32 + //UI mode + auto UImodeSelection = std::make_shared< OptionListComponent >(mWindow, _("UI MODE"), false); std::vector UImodes = UIModeController::getInstance()->getUIModes(); for (auto it = UImodes.cbegin(); it != UImodes.cend(); it++) - UImodeSelection->add(*it, *it, Settings::getInstance()->getString("UIMode") == *it); - s->addWithLabel("UI MODE", UImodeSelection); - Window* window = mWindow; - s->addSaveFunc([ UImodeSelection, window] + UImodeSelection->add(_(*it), *it, Settings::getInstance()->getString("UIMode") == *it); + s->addWithLabel(_("UI MODE"), UImodeSelection); + + s->addSaveFunc([UImodeSelection, window] { std::string selectedMode = UImodeSelection->getSelected(); if (selectedMode != "Full") @@ -224,31 +813,203 @@ void GuiMenu::openUISettings() msg += "Do you want to proceed?"; window->pushGui(new GuiMsgBox(window, msg, "YES", [selectedMode] { - LOG(LogDebug) << "Setting UI mode to " << selectedMode; - Settings::getInstance()->setString("UIMode", selectedMode); - Settings::getInstance()->saveFile(); - }, "NO",nullptr)); + LOG(LogDebug) << "Setting UI mode to " << selectedMode; + Settings::getInstance()->setString("UIMode", selectedMode); + Settings::getInstance()->saveFile(); + }, "NO", nullptr)); } }); + //#endif - // screensaver - ComponentListRow screensaver_row; - screensaver_row.elements.clear(); - screensaver_row.addElement(std::make_shared(mWindow, "SCREENSAVER SETTINGS", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - screensaver_row.addElement(makeArrow(mWindow), false); - screensaver_row.makeAcceptInputHandler(std::bind(&GuiMenu::openScreensaverOptions, this)); - s->addRow(screensaver_row); + // LANGUAGE + /* + std::vector langues; + langues.push_back("en"); + + std::string xmlpath = ResourceManager::getInstance()->getResourcePath(":/splash.svg"); + if (xmlpath.length() > 0) + { + xmlpath = Utils::FileSystem::getParent(xmlpath) + "/locale/"; + + Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(xmlpath, true); + for (Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) + { + if (Utils::FileSystem::isDirectory(*it)) + continue; + + std::string name = *it; + + if (name.rfind("emulationstation2.po") == std::string::npos) + continue; + + name = Utils::FileSystem::getParent(name); + name = Utils::FileSystem::getFileName(name); + + if (name != "en") + langues.push_back(name); + } + + if (langues.size() > 1) + { + auto language = std::make_shared< OptionListComponent >(mWindow, _("LANGUAGE"), false); + + for (auto it = langues.cbegin(); it != langues.cend(); it++) + language->add(*it, *it, Settings::getInstance()->getString("Language") == *it); + + s->addWithLabel(_("LANGUAGE"), language); + s->addSaveFunc([language, window, pthis, s] { + + if (language->getSelected() != Settings::getInstance()->getString("Language")) + { + if (Settings::getInstance()->setString("Language", language->getSelected())) + s->setVariable("reloadGuiMenu", true); + } + }); + } + } + */ + // transition style + auto transition_style = std::make_shared< OptionListComponent >(mWindow, _("TRANSITION STYLE"), false); + std::vector transitions; + transitions.push_back("auto"); + transitions.push_back("fade"); + transitions.push_back("slide"); + transitions.push_back("instant"); + + for (auto it = transitions.cbegin(); it != transitions.cend(); it++) + transition_style->add(_(*it), *it, Settings::getInstance()->getString("TransitionStyle") == *it); + + if (!transition_style->hasSelection()) + transition_style->selectFirstItem(); + + s->addWithLabel(_("TRANSITION STYLE"), transition_style); + s->addSaveFunc([transition_style] { + if (Settings::getInstance()->getString("TransitionStyle") == "instant" + && transition_style->getSelected() != "instant" + && PowerSaver::getMode() == PowerSaver::INSTANT) + { + Settings::getInstance()->setString("PowerSaverMode", "default"); + PowerSaver::init(); + } + Settings::getInstance()->setString("TransitionStyle", transition_style->getSelected()); + GuiComponent::ALLOWANIMATIONS = Settings::getInstance()->getString("TransitionStyle") != "instant"; + }); + + auto transitionOfGames_style = std::make_shared< OptionListComponent >(mWindow, _("GAME LAUNCH TRANSITION"), false); + std::vector gameTransitions; + gameTransitions.push_back("fade"); + gameTransitions.push_back("slide"); + gameTransitions.push_back("instant"); + for (auto it = gameTransitions.cbegin(); it != gameTransitions.cend(); it++) + transitionOfGames_style->add(_(*it), *it, Settings::getInstance()->getString("GameTransitionStyle") == *it); + + s->addWithLabel(_("GAME LAUNCH TRANSITION"), transitionOfGames_style); + s->addSaveFunc([transitionOfGames_style] { + if (Settings::getInstance()->getString("GameTransitionStyle") == "instant" + && transitionOfGames_style->getSelected() != "instant" + && PowerSaver::getMode() == PowerSaver::INSTANT) + { + Settings::getInstance()->setString("PowerSaverMode", "default"); + PowerSaver::init(); + } + Settings::getInstance()->setString("GameTransitionStyle", transitionOfGames_style->getSelected()); + }); + + + // Optionally start in selected system + auto systemfocus_list = std::make_shared< OptionListComponent >(mWindow, _("START ON SYSTEM"), false); + systemfocus_list->add(_("NONE"), "", Settings::getInstance()->getString("StartupSystem") == ""); + + for (auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) + if ("retropie" != (*it)->getName() && (*it)->isVisible()) + systemfocus_list->add((*it)->getName(), (*it)->getName(), Settings::getInstance()->getString("StartupSystem") == (*it)->getName()); + + if (!systemfocus_list->hasSelection()) + systemfocus_list->selectFirstItem(); + + s->addWithLabel(_("START ON SYSTEM"), systemfocus_list); + s->addSaveFunc([systemfocus_list] { + Settings::getInstance()->setString("StartupSystem", systemfocus_list->getSelected()); + }); + + + + // Select systems to hide + auto hiddenSystems = Utils::String::split(Settings::getInstance()->getString("HiddenSystems"), ';'); + + auto displayedSystems = std::make_shared>(mWindow, _("VISIBLE SYSTEMS"), true); + + for (auto system : SystemData::sSystemVector) + if(!system->isCollection() && !system->isGroupChildSystem()) + displayedSystems->add(system->getFullName(), system, std::find(hiddenSystems.cbegin(), hiddenSystems.cend(), system->getName()) == hiddenSystems.cend()); + + s->addWithLabel(_("VISIBLE SYSTEMS"), displayedSystems); + s->addSaveFunc([s, displayedSystems] + { + std::string hiddenSystems; + + std::vector sys = displayedSystems->getSelectedObjects(); + + for (auto system : SystemData::sSystemVector) + { + if (system->isCollection() || system->isGroupChildSystem()) + continue; + + if (std::find(sys.cbegin(), sys.cend(), system) == sys.cend()) + { + if (hiddenSystems.empty()) + hiddenSystems = system->getName(); + else + hiddenSystems = hiddenSystems + ";" + system->getName(); + } + } + + if (Settings::getInstance()->setString("HiddenSystems", hiddenSystems)) + { + Settings::getInstance()->saveFile(); + s->setVariable("reloadAll", true); + } + }); + + + // Open gamelist at start + auto bootOnGamelist = std::make_shared(mWindow); + bootOnGamelist->setState(Settings::getInstance()->getBool("StartupOnGameList")); + s->addWithLabel(_("BOOT ON GAMELIST"), bootOnGamelist); + s->addSaveFunc([bootOnGamelist] { Settings::getInstance()->setBool("StartupOnGameList", bootOnGamelist->getState()); }); + + // Hide system view + auto hideSystemView = std::make_shared(mWindow); + hideSystemView->setState(Settings::getInstance()->getBool("HideSystemView")); + s->addWithLabel(_("HIDE SYSTEM VIEW"), hideSystemView); + s->addSaveFunc([hideSystemView] + { + bool hideSysView = Settings::getInstance()->getBool("HideSystemView"); + Settings::getInstance()->setBool("HideSystemView", hideSystemView->getState()); + + if (!hideSysView && hideSystemView->getState()) + ViewController::get()->goToStart(true); + }); + + +#if defined(_WIN32) + // quick system select (left/right in game list view) + auto hideWindowScreen = std::make_shared(mWindow); + hideWindowScreen->setState(Settings::getInstance()->getBool("HideWindow")); + s->addWithLabel(_("HIDE WHEN RUNNING GAME"), hideWindowScreen); + s->addSaveFunc([hideWindowScreen] { Settings::getInstance()->setBool("HideWindow", hideWindowScreen->getState()); }); +#endif // quick system select (left/right in game list view) auto quick_sys_select = std::make_shared(mWindow); quick_sys_select->setState(Settings::getInstance()->getBool("QuickSystemSelect")); - s->addWithLabel("QUICK SYSTEM SELECT", quick_sys_select); + s->addWithLabel(_("QUICK SYSTEM SELECT"), quick_sys_select); s->addSaveFunc([quick_sys_select] { Settings::getInstance()->setBool("QuickSystemSelect", quick_sys_select->getState()); }); // carousel transition option auto move_carousel = std::make_shared(mWindow); move_carousel->setState(Settings::getInstance()->getBool("MoveCarousel")); - s->addWithLabel("CAROUSEL TRANSITIONS", move_carousel); + s->addWithLabel(_("CAROUSEL TRANSITIONS"), move_carousel); s->addSaveFunc([move_carousel] { if (move_carousel->getState() && !Settings::getInstance()->getBool("MoveCarousel") @@ -260,167 +1021,410 @@ void GuiMenu::openUISettings() Settings::getInstance()->setBool("MoveCarousel", move_carousel->getState()); }); - // transition style - auto transition_style = std::make_shared< OptionListComponent >(mWindow, "TRANSITION STYLE", false); - std::vector transitions; - transitions.push_back("fade"); - transitions.push_back("slide"); - transitions.push_back("instant"); - for(auto it = transitions.cbegin(); it != transitions.cend(); it++) - transition_style->add(*it, *it, Settings::getInstance()->getString("TransitionStyle") == *it); - s->addWithLabel("TRANSITION STYLE", transition_style); - s->addSaveFunc([transition_style] { - if (Settings::getInstance()->getString("TransitionStyle") == "instant" - && transition_style->getSelected() != "instant" - && PowerSaver::getMode() == PowerSaver::INSTANT) + // clock + auto clock = std::make_shared(mWindow); + clock->setState(Settings::getInstance()->getBool("DrawClock")); + s->addWithLabel(_("SHOW CLOCK"), clock); + s->addSaveFunc( + [clock] { Settings::getInstance()->setBool("DrawClock", clock->getState()); }); + + // show help + auto show_help = std::make_shared(mWindow); + show_help->setState(Settings::getInstance()->getBool("ShowHelpPrompts")); + s->addWithLabel(_("ON-SCREEN HELP"), show_help); + s->addSaveFunc([s, show_help] + { + if (Settings::getInstance()->setBool("ShowHelpPrompts", show_help->getState())) + s->setVariable("reloadAll", true); + }); + + // filenames + auto hidden_files = std::make_shared(mWindow); + hidden_files->setState(Settings::getInstance()->getBool("ShowFilenames")); + s->addWithLabel(_("SHOW FILENAMES IN LISTS"), hidden_files); + s->addSaveFunc([hidden_files, s] + { + if (Settings::getInstance()->setBool("ShowFilenames", hidden_files->getState())) { - Settings::getInstance()->setString("PowerSaverMode", "default"); - PowerSaver::init(); + FileData::resetSettings(); + s->setVariable("reloadCollections", true); + s->setVariable("reloadAll", true); } - Settings::getInstance()->setString("TransitionStyle", transition_style->getSelected()); }); - // theme set - auto themeSets = ThemeData::getThemeSets(); + // enable filters (ForceDisableFilters) + auto enable_filter = std::make_shared(mWindow); + enable_filter->setState(!Settings::getInstance()->getBool("ForceDisableFilters")); + s->addWithLabel(_("ENABLE FILTERS"), enable_filter); + s->addSaveFunc([enable_filter, s] { + bool filter_is_enabled = !Settings::getInstance()->getBool("ForceDisableFilters"); + if (Settings::getInstance()->setBool("ForceDisableFilters", !enable_filter->getState())) + s->setVariable("reloadAll", true); + }); + - if(!themeSets.empty()) + s->onFinalize([s, pthis, window] { - std::map::const_iterator selectedSet = themeSets.find(Settings::getInstance()->getString("ThemeSet")); - if(selectedSet == themeSets.cend()) - selectedSet = themeSets.cbegin(); + if (s->getVariable("reloadCollections")) + CollectionSystemManager::get()->updateSystemsList(); - auto theme_set = std::make_shared< OptionListComponent >(mWindow, "THEME SET", false); - for(auto it = themeSets.cbegin(); it != themeSets.cend(); it++) - theme_set->add(it->first, it->first, it == selectedSet); - s->addWithLabel("THEME SET", theme_set); + if (s->getVariable("reloadAll")) + { + ViewController::get()->reloadAll(window); + window->endRenderLoadingScreen(); + } - Window* window = mWindow; - s->addSaveFunc([window, theme_set] + if (s->getVariable("reloadGuiMenu")) { - bool needReload = false; - std::string oldTheme = Settings::getInstance()->getString("ThemeSet"); - if(oldTheme != theme_set->getSelected()) - needReload = true; + delete pthis; + window->pushGui(new GuiMenu(window, false)); + } + }); + + s->updatePosition(); + mWindow->pushGui(s); +} + +void GuiMenu::openSystemEmulatorSettings(SystemData* system) +{ + auto theme = ThemeData::getMenuTheme(); + + GuiSettings* s = new GuiSettings(mWindow, system->getFullName().c_str()); + + auto emul_choice = std::make_shared>(mWindow, _("EMULATOR"), false); + auto core_choice = std::make_shared>(mWindow, _("CORE"), false); + + std::string currentEmul = Settings::getInstance()->getString(system->getName() + ".emulator"); + std::string defaultEmul = (system->getSystemEnvData()->mEmulators.size() == 0 ? "" : system->getSystemEnvData()->mEmulators[0].mName); - Settings::getInstance()->setString("ThemeSet", theme_set->getSelected()); +// if (defaultEmul.length() == 0) + emul_choice->add(_("AUTO"), "", false); +// else +// emul_choice->add(_("AUTO") + " (" + defaultEmul + ")", "", currentEmul.length() == 0); - if(needReload) + bool found = false; + for (auto core : system->getSystemEnvData()->mEmulators) + { + if (core.mName == currentEmul) + found = true; + + emul_choice->add(core.mName, core.mName, core.mName == currentEmul); + } + + if (!found) + emul_choice->selectFirstItem(); + + ComponentListRow row; + row.addElement(std::make_shared(mWindow, _("EMULATOR"), theme->Text.font, theme->Text.color), true); + row.addElement(emul_choice, false); + + s->addRow(row); + + emul_choice->setSelectedChangedCallback([this, system, core_choice](std::string emulatorName) + { + std::string currentCore = Settings::getInstance()->getString(system->getName() + ".core"); + std::string defaultCore; + + for (auto& emulator : system->getSystemEnvData()->mEmulators) + { + if (emulatorName == emulator.mName) { - Scripting::fireEvent("theme-changed", theme_set->getSelected(), oldTheme); - CollectionSystemManager::get()->updateSystemsList(); - ViewController::get()->goToStart(); - ViewController::get()->reloadAll(); // TODO - replace this with some sort of signal-based implementation + for (auto core : emulator.mCores) + { + defaultCore = core; + break; + } } - }); + } + + core_choice->clear(); + + // if (defaultCore.length() == 0) + core_choice->add(_("AUTO"), "", false); + // else + // core_choice->add(_("AUTO") + " (" + defaultCore + ")", "", false); + + std::vector cores = system->getSystemEnvData()->getCores(emulatorName); + + bool found = false; + + for (auto it = cores.begin(); it != cores.end(); it++) + { + std::string core = *it; + core_choice->add(core, core, currentCore == core); + if (currentCore == core) + found = true; + } + + if (!found) + core_choice->selectFirstItem(); + else + core_choice->invalidate(); + }); + + row.elements.clear(); + row.addElement(std::make_shared(mWindow, "CORE", theme->Text.font, theme->Text.color), true); + row.addElement(core_choice, false); + s->addRow(row); + + // force change event to load core list + emul_choice->invalidate(); + + + s->addSaveFunc([system, emul_choice, core_choice] + { + Settings::getInstance()->setString(system->getName() + ".emulator", emul_choice->getSelected()); + Settings::getInstance()->setString(system->getName() + ".core", core_choice->getSelected()); + }); + + mWindow->pushGui(s); +} + +void GuiMenu::openEmulatorSettings() +{ + GuiSettings* configuration = new GuiSettings(mWindow, _("EMULATOR SETTINGS").c_str()); + + Window* window = mWindow; + + // For each activated system + for (auto system : SystemData::sSystemVector) + { + if (system->isCollection()) + continue; + + if (system->getSystemEnvData()->mEmulators.size() == 0) + continue; + + if (system->getSystemEnvData()->mEmulators.size() == 1 && system->getSystemEnvData()->mEmulators[0].mCores.size() <= 1) + continue; + + configuration->addEntry(system->getFullName(), true, [this, system] { openSystemEmulatorSettings(system); }); } - // GameList view style - auto gamelist_style = std::make_shared< OptionListComponent >(mWindow, "GAMELIST VIEW STYLE", false); - std::vector styles; - styles.push_back("automatic"); - styles.push_back("basic"); - styles.push_back("detailed"); - styles.push_back("video"); - styles.push_back("grid"); - - for (auto it = styles.cbegin(); it != styles.cend(); it++) - gamelist_style->add(*it, *it, Settings::getInstance()->getString("GamelistViewStyle") == *it); - s->addWithLabel("GAMELIST VIEW STYLE", gamelist_style); - s->addSaveFunc([gamelist_style] { - bool needReload = false; - if (Settings::getInstance()->getString("GamelistViewStyle") != gamelist_style->getSelected()) - needReload = true; - Settings::getInstance()->setString("GamelistViewStyle", gamelist_style->getSelected()); - if (needReload) - ViewController::get()->reloadAll(); + window->pushGui(configuration); +} + +void GuiMenu::openUpdateSettings() +{ + Window* window = mWindow; + auto s = new GuiSettings(mWindow, _("DOWNLOADS AND UPDATES")); + + // themes installer/browser + s->addEntry(_("THEME INSTALLER"), true, [this] + { + mWindow->pushGui(new GuiThemeInstall(mWindow)); }); - // Optionally start in selected system - auto systemfocus_list = std::make_shared< OptionListComponent >(mWindow, "START ON SYSTEM", false); - systemfocus_list->add("NONE", "", Settings::getInstance()->getString("StartupSystem") == ""); - for (auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) + // Enable updates + auto updates_enabled = std::make_shared(mWindow); + updates_enabled->setState(Settings::getInstance()->getBool("updates.enabled")); + s->addWithLabel(_("AUTO UPDATES"), updates_enabled); + s->addSaveFunc([updates_enabled] + { + Settings::getInstance()->setBool("updates.enabled", updates_enabled->getState()); + }); + + // Start update + s->addEntry(ApiSystem::state == UpdateState::State::UPDATE_READY ? _("APPLY UPDATE") : _("START UPDATE"), true, [this, s] { - if ("retropie" != (*it)->getName()) + if (ApiSystem::checkUpdateVersion().empty()) { - systemfocus_list->add((*it)->getName(), (*it)->getName(), Settings::getInstance()->getString("StartupSystem") == (*it)->getName()); + mWindow->pushGui(new GuiMsgBox(mWindow, _("NO UPDATE AVAILABLE"))); + return; + } + + if (ApiSystem::state == UpdateState::State::UPDATE_READY) + { + if (quitES(QuitMode::QUIT)) + LOG(LogWarning) << "Reboot terminated with non-zero result!"; + } + else if (ApiSystem::state == UpdateState::State::UPDATER_RUNNING) + mWindow->pushGui(new GuiMsgBox(mWindow, _("UPDATE IS ALREADY RUNNING"))); + else + { + ApiSystem::startUpdate(mWindow); + + s->setVariable("closeGuiMenu", true); + s->close(); } - } - s->addWithLabel("START ON SYSTEM", systemfocus_list); - s->addSaveFunc([systemfocus_list] { - Settings::getInstance()->setString("StartupSystem", systemfocus_list->getSelected()); }); - // show help - auto show_help = std::make_shared(mWindow); - show_help->setState(Settings::getInstance()->getBool("ShowHelpPrompts")); - s->addWithLabel("ON-SCREEN HELP", show_help); - s->addSaveFunc([show_help] { Settings::getInstance()->setBool("ShowHelpPrompts", show_help->getState()); }); + s->updatePosition(); - // enable filters (ForceDisableFilters) - auto enable_filter = std::make_shared(mWindow); - enable_filter->setState(!Settings::getInstance()->getBool("ForceDisableFilters")); - s->addWithLabel("ENABLE FILTERS", enable_filter); - s->addSaveFunc([enable_filter] { - bool filter_is_enabled = !Settings::getInstance()->getBool("ForceDisableFilters"); - Settings::getInstance()->setBool("ForceDisableFilters", !enable_filter->getState()); - if (enable_filter->getState() != filter_is_enabled) ViewController::get()->ReloadAndGoToStart(); + auto pthis = this; + + s->onFinalize([s, pthis, window] + { + if (s->getVariable("closeGuiMenu")) + delete pthis; }); mWindow->pushGui(s); } + void GuiMenu::openOtherSettings() { - auto s = new GuiSettings(mWindow, "OTHER SETTINGS"); + Window* window = mWindow; + auto s = new GuiSettings(mWindow, _("ADVANCED SETTINGS")); - // maximum vram - auto max_vram = std::make_shared(mWindow, 0.f, 1000.f, 10.f, "Mb"); - max_vram->setValue((float)(Settings::getInstance()->getInt("MaxVRAM"))); - s->addWithLabel("VRAM LIMIT", max_vram); - s->addSaveFunc([max_vram] { Settings::getInstance()->setInt("MaxVRAM", (int)Math::round(max_vram->getValue())); }); + /* + // Emulator settings + for (auto system : SystemData::sSystemVector) + { + if (system->isCollection() || system->getSystemEnvData()->mEmulators.size() == 0 || (system->getSystemEnvData()->mEmulators.size() == 1 && system->getSystemEnvData()->mEmulators[0].mCores.size() <= 1)) + continue; + s->addEntry(_("EMULATOR SETTINGS"), true, [this] { openEmulatorSettings(); }, "iconGames"); + break; + } + */ // power saver - auto power_saver = std::make_shared< OptionListComponent >(mWindow, "POWER SAVER MODES", false); + auto power_saver = std::make_shared< OptionListComponent >(mWindow, _("POWER SAVER MODES"), false); std::vector modes; modes.push_back("disabled"); modes.push_back("default"); modes.push_back("enhanced"); modes.push_back("instant"); for (auto it = modes.cbegin(); it != modes.cend(); it++) - power_saver->add(*it, *it, Settings::getInstance()->getString("PowerSaverMode") == *it); - s->addWithLabel("POWER SAVER MODES", power_saver); + power_saver->add(_(it->c_str()), *it, Settings::getInstance()->getString("PowerSaverMode") == *it); + + s->addWithLabel(_("POWER SAVER MODES"), power_saver); s->addSaveFunc([this, power_saver] { if (Settings::getInstance()->getString("PowerSaverMode") != "instant" && power_saver->getSelected() == "instant") { Settings::getInstance()->setString("TransitionStyle", "instant"); + Settings::getInstance()->setString("GameTransitionStyle", "instant"); Settings::getInstance()->setBool("MoveCarousel", false); Settings::getInstance()->setBool("EnableSounds", false); } + + GuiComponent::ALLOWANIMATIONS = Settings::getInstance()->getString("TransitionStyle") != "instant"; + Settings::getInstance()->setString("PowerSaverMode", power_saver->getSelected()); PowerSaver::init(); }); + + // LANGUAGE + + std::vector langues; + langues.push_back("en"); + + std::string xmlpath = ResourceManager::getInstance()->getResourcePath(":/splash.svg"); + if (xmlpath.length() > 0) + { + xmlpath = Utils::FileSystem::getParent(xmlpath) + "/locale/"; + + Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(xmlpath, true); + for (Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) + { + if (Utils::FileSystem::isDirectory(*it)) + continue; + + std::string name = *it; + + if (name.rfind("emulationstation2.po") == std::string::npos) + continue; + + name = Utils::FileSystem::createRelativePath(name, xmlpath, false); + if (Utils::String::startsWith(name, "./")) + { + name = name.substr(2); + + while (name.find("/") != std::string::npos) + name = Utils::FileSystem::getParent(name); + } + else + name = Utils::FileSystem::getParent(name); + + name = Utils::FileSystem::getFileName(name); + + if (name != "en") + langues.push_back(name); + } + + if (langues.size() > 1) + { + auto language = std::make_shared< OptionListComponent >(mWindow, _("LANGUAGE"), false); + + for (auto it = langues.cbegin(); it != langues.cend(); it++) + language->add(*it, *it, Settings::getInstance()->getString("Language") == *it); + + s->addWithLabel(_("LANGUAGE"), language); + s->addSaveFunc([language, window, s] { + + if (language->getSelected() != Settings::getInstance()->getString("Language")) + { + if (Settings::getInstance()->setString("Language", language->getSelected())) + s->setVariable("reloadGuiMenu", true); + } + }); + } + } + + + // maximum vram + auto max_vram = std::make_shared(mWindow, 40.f, 1000.f, 10.f, "Mb"); + max_vram->setValue((float)(Settings::getInstance()->getInt("MaxVRAM"))); + s->addWithLabel(_("VRAM LIMIT"), max_vram); + s->addSaveFunc([max_vram] { Settings::getInstance()->setInt("MaxVRAM", (int)Math::round(max_vram->getValue())); }); + + + /* +#if WIN32 + + // Enable updates + auto updates_enabled = std::make_shared(mWindow); + updates_enabled->setState(Settings::getInstance()->getBool("updates.enabled")); + s->addWithLabel(_("AUTO UPDATES"), updates_enabled); + s->addSaveFunc([updates_enabled] + { + Settings::getInstance()->setBool("updates.enabled", updates_enabled->getState()); + }); + + // Start update + s->addEntry(ApiSystem::state == UpdateState::State::UPDATE_READY ? _("APPLY UPDATE") : _("START UPDATE"), true, [this] + { + if (ApiSystem::checkUpdateVersion().empty()) + { + mWindow->pushGui(new GuiMsgBox(mWindow, _("NO UPDATE AVAILABLE"))); + return; + } + + if (ApiSystem::state == UpdateState::State::UPDATE_READY) + { + if (quitES(QuitMode::QUIT)) + LOG(LogWarning) << "Reboot terminated with non-zero result!"; + } + else if (ApiSystem::state == UpdateState::State::UPDATER_RUNNING) + mWindow->pushGui(new GuiMsgBox(mWindow, _("UPDATE IS ALREADY RUNNING"))); + else + ApiSystem::startUpdate(mWindow); + }); +#endif +*/ + + + + // gamelists auto save_gamelists = std::make_shared(mWindow); save_gamelists->setState(Settings::getInstance()->getBool("SaveGamelistsOnExit")); - s->addWithLabel("SAVE METADATA ON EXIT", save_gamelists); + s->addWithLabel(_("SAVE METADATA ON EXIT"), save_gamelists); s->addSaveFunc([save_gamelists] { Settings::getInstance()->setBool("SaveGamelistsOnExit", save_gamelists->getState()); }); auto parse_gamelists = std::make_shared(mWindow); parse_gamelists->setState(Settings::getInstance()->getBool("ParseGamelistOnly")); - s->addWithLabel("PARSE GAMESLISTS ONLY", parse_gamelists); + s->addWithLabel(_("PARSE GAMESLISTS ONLY"), parse_gamelists); s->addSaveFunc([parse_gamelists] { Settings::getInstance()->setBool("ParseGamelistOnly", parse_gamelists->getState()); }); - + +#ifndef WIN32 auto local_art = std::make_shared(mWindow); local_art->setState(Settings::getInstance()->getBool("LocalArt")); - s->addWithLabel("SEARCH FOR LOCAL ART", local_art); + s->addWithLabel(_("SEARCH FOR LOCAL ART"), local_art); s->addSaveFunc([local_art] { Settings::getInstance()->setBool("LocalArt", local_art->getState()); }); - - // hidden files - auto hidden_files = std::make_shared(mWindow); - hidden_files->setState(Settings::getInstance()->getBool("ShowHiddenFiles")); - s->addWithLabel("SHOW HIDDEN FILES", hidden_files); - s->addSaveFunc([hidden_files] { Settings::getInstance()->setBool("ShowHiddenFiles", hidden_files->getState()); }); +#endif #ifdef _RPI_ // Video Player - VideoOmxPlayer @@ -442,12 +1446,95 @@ void GuiMenu::openOtherSettings() #endif - // framerate + // preload UI + auto preloadUI = std::make_shared(mWindow); + preloadUI->setState(Settings::getInstance()->getBool("PreloadUI")); + s->addWithLabel(_("PRELOAD UI"), preloadUI); + s->addSaveFunc([preloadUI] { Settings::getInstance()->setBool("PreloadUI", preloadUI->getState()); }); + + // optimizeVram + auto optimizeVram = std::make_shared(mWindow); + optimizeVram->setState(Settings::getInstance()->getBool("OptimizeVRAM")); + s->addWithLabel(_("OPTIMIZE IMAGES VRAM USE"), optimizeVram); + s->addSaveFunc([optimizeVram] + { + TextureData::OPTIMIZEVRAM = optimizeVram->getState(); + Settings::getInstance()->setBool("OptimizeVRAM", optimizeVram->getState()); + }); + +#ifdef WIN32 + // vsync + auto vsync = std::make_shared(mWindow); + vsync->setState(Settings::getInstance()->getBool("VSync")); + s->addWithLabel(_("VSYNC"), vsync); + s->addSaveFunc([vsync] + { + Settings::getInstance()->setBool("VSync", vsync->getState()); + Renderer::setSwapInterval(); + }); +#endif + + // framerate auto framerate = std::make_shared(mWindow); framerate->setState(Settings::getInstance()->getBool("DrawFramerate")); - s->addWithLabel("SHOW FRAMERATE", framerate); + s->addWithLabel(_("SHOW FRAMERATE"), framerate); s->addSaveFunc([framerate] { Settings::getInstance()->setBool("DrawFramerate", framerate->getState()); }); + // threaded loading + auto threadedLoading = std::make_shared(mWindow); + threadedLoading->setState(Settings::getInstance()->getBool("ThreadedLoading")); + s->addWithLabel(_("THREADED LOADING"), threadedLoading); + s->addSaveFunc([threadedLoading] { Settings::getInstance()->setBool("ThreadedLoading", threadedLoading->getState()); }); + +#ifndef _RPI_ + // full exit + auto fullExitMenu = std::make_shared(mWindow); + fullExitMenu->setState(!Settings::getInstance()->getBool("ShowOnlyExit")); + s->addWithLabel(_("COMPLETE QUIT MENU"), fullExitMenu); + s->addSaveFunc([fullExitMenu] { Settings::getInstance()->setBool("ShowOnlyExit", !fullExitMenu->getState()); }); +#endif + + // log level + auto logLevel = std::make_shared< OptionListComponent >(mWindow, _("LOG LEVEL"), false); + std::vector levels; + levels.push_back("default"); + levels.push_back("disabled"); + levels.push_back("warning"); + levels.push_back("error"); + levels.push_back("debug"); + + auto level = Settings::getInstance()->getString("LogLevel"); + if (level.empty()) + level = "default"; + + for (auto it = levels.cbegin(); it != levels.cend(); it++) + logLevel->add(_(it->c_str()), *it, level == *it); + + s->addWithLabel(_("LOG LEVEL"), logLevel); + s->addSaveFunc([this, logLevel] + { + if (Settings::getInstance()->setString("LogLevel", logLevel->getSelected() == "default" ? "" : logLevel->getSelected())) + { + Log::setupReportingLevel(); + Log::init(); + } + }); + + + + + s->updatePosition(); + + auto pthis = this; + + s->onFinalize([s, pthis, window] + { + if (s->getVariable("reloadGuiMenu")) + { + delete pthis; + window->pushGui(new GuiMenu(window, false)); + } + }); mWindow->pushGui(s); @@ -456,120 +1543,222 @@ void GuiMenu::openOtherSettings() void GuiMenu::openConfigInput() { Window* window = mWindow; - window->pushGui(new GuiMsgBox(window, "ARE YOU SURE YOU WANT TO CONFIGURE INPUT?", "YES", +// window->pushGui(new GuiDetectDevice(window, false, nullptr)); + + window->pushGui(new GuiMsgBox(window, _("ARE YOU SURE YOU WANT TO CONFIGURE INPUT?"), _("YES"), [window] { window->pushGui(new GuiDetectDevice(window, false, nullptr)); - }, "NO", nullptr) + }, _("NO"), nullptr) ); } void GuiMenu::openQuitMenu() { - auto s = new GuiSettings(mWindow, "QUIT"); + if (Settings::getInstance()->getBool("ShowOnlyExit")) + { + Scripting::fireEvent("quit"); + quitES(); + return; + } + + auto s = new GuiSettings(mWindow, _("QUIT")); Window* window = mWindow; ComponentListRow row; if (UIModeController::getInstance()->isUIModeFull()) { +#ifndef WIN32 + // Restart does not work on Windows row.makeAcceptInputHandler([window] { - window->pushGui(new GuiMsgBox(window, "REALLY RESTART?", "YES", + window->pushGui(new GuiMsgBox(window, _("REALLY RESTART?"), _("YES"), [] { Scripting::fireEvent("quit"); if(quitES(QuitMode::RESTART) != 0) LOG(LogWarning) << "Restart terminated with non-zero result!"; - }, "NO", nullptr)); + }, _("NO"), nullptr)); }); - row.addElement(std::make_shared(window, "RESTART EMULATIONSTATION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(window, _("RESTART EMULATIONSTATION"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); s->addRow(row); - - +#endif if(Settings::getInstance()->getBool("ShowExit")) { row.elements.clear(); row.makeAcceptInputHandler([window] { - window->pushGui(new GuiMsgBox(window, "REALLY QUIT?", "YES", + window->pushGui(new GuiMsgBox(window, _("REALLY QUIT?"), _("YES"), [] { Scripting::fireEvent("quit"); quitES(); - }, "NO", nullptr)); + }, _("NO"), nullptr)); }); - row.addElement(std::make_shared(window, "QUIT EMULATIONSTATION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(window, _("QUIT EMULATIONSTATION"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); s->addRow(row); } } row.elements.clear(); row.makeAcceptInputHandler([window] { - window->pushGui(new GuiMsgBox(window, "REALLY RESTART?", "YES", + window->pushGui(new GuiMsgBox(window, _("REALLY RESTART?"), _("YES"), [] { Scripting::fireEvent("quit", "reboot"); Scripting::fireEvent("reboot"); if (quitES(QuitMode::REBOOT) != 0) LOG(LogWarning) << "Restart terminated with non-zero result!"; - }, "NO", nullptr)); + }, _("NO"), nullptr)); }); - row.addElement(std::make_shared(window, "RESTART SYSTEM", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(window, _("RESTART SYSTEM"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); s->addRow(row); row.elements.clear(); row.makeAcceptInputHandler([window] { - window->pushGui(new GuiMsgBox(window, "REALLY SHUTDOWN?", "YES", + window->pushGui(new GuiMsgBox(window, _("REALLY SHUTDOWN?"), _("YES"), [] { Scripting::fireEvent("quit", "shutdown"); Scripting::fireEvent("shutdown"); if (quitES(QuitMode::SHUTDOWN) != 0) LOG(LogWarning) << "Shutdown terminated with non-zero result!"; - }, "NO", nullptr)); + }, _("NO"), nullptr)); }); - row.addElement(std::make_shared(window, "SHUTDOWN SYSTEM", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(window, _("SHUTDOWN SYSTEM"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); s->addRow(row); + s->updatePosition(); mWindow->pushGui(s); } +std::string getBuildTime() +{ + std::string datestr = __DATE__; + std::string timestr = __TIME__; + + std::istringstream iss_date(datestr); + std::string str_month; + int day; + int year; + iss_date >> str_month >> day >> year; + + int month; + if (str_month == "Jan") month = 1; + else if (str_month == "Feb") month = 2; + else if (str_month == "Mar") month = 3; + else if (str_month == "Apr") month = 4; + else if (str_month == "May") month = 5; + else if (str_month == "Jun") month = 6; + else if (str_month == "Jul") month = 7; + else if (str_month == "Aug") month = 8; + else if (str_month == "Sep") month = 9; + else if (str_month == "Oct") month = 10; + else if (str_month == "Nov") month = 11; + else if (str_month == "Dec") month = 12; + else exit(-1); + + for (std::string::size_type pos = timestr.find(':'); pos != std::string::npos; pos = timestr.find(':', pos)) + timestr[pos] = ' '; + + std::istringstream iss_time(timestr); + int hour, min, sec; + iss_time >> hour >> min >> sec; + + char buffer[100]; + + sprintf(buffer, "%4d%.2d%.2d%.2d%.2d%.2d\n", year, month, day, hour, min, sec); + + return buffer; +} + void GuiMenu::addVersionInfo() { - std::string buildDate = (Settings::getInstance()->getBool("Debug") ? std::string( " (" + Utils::String::toUpper(PROGRAM_BUILT_STRING) + ")") : ("")); + std::string buildDate = getBuildTime(); + // (Settings::getInstance()->getBool("Debug") ? std::string( " (" + Utils::String::toUpper(PROGRAM_BUILT_STRING) + ")") : ("")); + + auto theme = ThemeData::getMenuTheme(); +// mVersion.setFont(Font::get(FONT_SIZE_SMALL)); +// mVersion.setColor(0x5E5E5EFF); - mVersion.setFont(Font::get(FONT_SIZE_SMALL)); - mVersion.setColor(0x5E5E5EFF); - mVersion.setText("EMULATIONSTATION V" + Utils::String::toUpper(PROGRAM_VERSION_STRING) + buildDate); - mVersion.setHorizontalAlignment(ALIGN_CENTER); + mVersion.setFont(theme->Footer.font); + mVersion.setColor(theme->Footer.color); + + mVersion.setLineSpacing(0); + +#if WIN32 + std::string localVersion; + std::string localVersionFile = Utils::FileSystem::getExePath() + "/version.info"; + if (Utils::FileSystem::exists(localVersionFile)) + { + localVersion = Utils::FileSystem::readAllText(localVersionFile); + localVersion = Utils::String::replace(Utils::String::replace(localVersion, "\r", ""), "\n", ""); + mVersion.setText("EMULATIONSTATION V" + localVersion+" FCAMOD"); + } + else +#endif + mVersion.setText("EMULATIONSTATION V" + Utils::String::toUpper(PROGRAM_VERSION_STRING) + " BUILD " + buildDate); + + mVersion.setHorizontalAlignment(ALIGN_CENTER); + mVersion.setVerticalAlignment(ALIGN_CENTER); addChild(&mVersion); } void GuiMenu::openScreensaverOptions() { - mWindow->pushGui(new GuiGeneralScreensaverOptions(mWindow, "SCREENSAVER SETTINGS")); + mWindow->pushGui(new GuiGeneralScreensaverOptions(mWindow, _("SCREENSAVER SETTINGS"))); } -void GuiMenu::openCollectionSystemSettings() { +void GuiMenu::openCollectionSystemSettings() +{ + if (ThreadedScraper::isRunning()) + { + mWindow->pushGui(new GuiMsgBox(mWindow, _("THIS FUNCTION IS DISABLED WHEN SCRAPING IS RUNNING"))); + return; + } + mWindow->pushGui(new GuiCollectionSystemsOptions(mWindow)); } void GuiMenu::onSizeChanged() { - mVersion.setSize(mSize.x(), 0); - mVersion.setPosition(0, mSize.y() - mVersion.getSize().y()); + float h = mMenu.getButtonGridHeight(); + + mVersion.setSize(mSize.x(), h); + mVersion.setPosition(0, mSize.y() - h); // mVersion.getSize().y() } -void GuiMenu::addEntry(const char* name, unsigned int color, bool add_arrow, const std::function& func) +void GuiMenu::addEntry(std::string name, bool add_arrow, const std::function& func, const std::string iconName) { - std::shared_ptr font = Font::get(FONT_SIZE_MEDIUM); + auto theme = ThemeData::getMenuTheme(); + std::shared_ptr font = theme->Text.font; + unsigned int color = theme->Text.color; // populate the list ComponentListRow row; + + if (!iconName.empty()) + { + std::string iconPath = theme->getMenuIcon(iconName); + if (!iconPath.empty()) + { + // icon + auto icon = std::make_shared(mWindow); + icon->setImage(iconPath); + icon->setColorShift(theme->Text.color); + icon->setResize(0, theme->Text.font->getLetterHeight() * 1.25f); + row.addElement(icon, false); + + // spacer between icon and text + auto spacer = std::make_shared(mWindow); + spacer->setSize(10, 0); + row.addElement(spacer, false); + } + } + row.addElement(std::make_shared(mWindow, name, font, color), true); - if(add_arrow) + if (add_arrow) { std::shared_ptr bracket = makeArrow(mWindow); row.addElement(bracket, false); } row.makeAcceptInputHandler(func); - mMenu.addRow(row); } @@ -590,15 +1779,71 @@ bool GuiMenu::input(InputConfig* config, Input input) HelpStyle GuiMenu::getHelpStyle() { HelpStyle style = HelpStyle(); - style.applyTheme(ViewController::get()->getState().getSystem()->getTheme(), "system"); + + if (ThemeData::getDefaultTheme() != nullptr) + { + std::shared_ptr theme = std::shared_ptr(ThemeData::getDefaultTheme(), [](ThemeData*) {}); + style.applyTheme(theme, "system"); + } + else + style.applyTheme(ViewController::get()->getState().getSystem()->getTheme(), "system"); + return style; } std::vector GuiMenu::getHelpPrompts() { std::vector prompts; - prompts.push_back(HelpPrompt("up/down", "choose")); - prompts.push_back(HelpPrompt("a", "select")); - prompts.push_back(HelpPrompt("start", "close")); + prompts.push_back(HelpPrompt("up/down", _("CHOOSE"))); + prompts.push_back(HelpPrompt("a", _("SELECT"))); + prompts.push_back(HelpPrompt("start", _("CLOSE"))); return prompts; } + +void GuiMenu::createInputTextRow(GuiSettings *gui, std::string title, const char *settingsID, bool password) +{ + auto theme = ThemeData::getMenuTheme(); + std::shared_ptr font = theme->Text.font; + unsigned int color = theme->Text.color; + + // LABEL + Window *window = mWindow; + ComponentListRow row; + + auto lbl = std::make_shared(window, title, font, color); + row.addElement(lbl, true); // label + + std::shared_ptr ed; + + std::string value = Settings::getInstance()->getString(settingsID); + + ed = std::make_shared(window, ((password && value != "") ? "*********" : value), font, color, ALIGN_RIGHT); // Font::get(FONT_SIZE_MEDIUM, FONT_PATH_LIGHT) + row.addElement(ed, true); + + auto spacer = std::make_shared(mWindow); + spacer->setSize(Renderer::getScreenWidth() * 0.005f, 0); + row.addElement(spacer, false); + + auto bracket = std::make_shared(mWindow); + bracket->setImage(theme->Icons.arrow); + bracket->setResize(Vector2f(0, lbl->getFont()->getLetterHeight())); + row.addElement(bracket, false); + + auto updateVal = [ed, settingsID, password](const std::string &newVal) { + if (!password) + ed->setValue(newVal); + else { + ed->setValue("*********"); + } + + Settings::getInstance()->setString(settingsID, newVal); + }; // ok callback (apply new value to ed) + + row.makeAcceptInputHandler([this, title, updateVal, settingsID] + { + std::string data = Settings::getInstance()->getString(settingsID); + mWindow->pushGui(new GuiTextEditPopupKeyboard(mWindow, title, data, updateVal, false)); + }); + + gui->addRow(row); +} \ No newline at end of file diff --git a/es-app/src/guis/GuiMenu.h b/es-app/src/guis/GuiMenu.h index da8406bb79..0ad48948d1 100644 --- a/es-app/src/guis/GuiMenu.h +++ b/es-app/src/guis/GuiMenu.h @@ -3,20 +3,27 @@ #define ES_APP_GUIS_GUI_MENU_H #include "components/MenuComponent.h" +#include "components/OptionListComponent.h" #include "GuiComponent.h" +class GuiSettings; +class SystemData; + class GuiMenu : public GuiComponent { public: - GuiMenu(Window* window); + GuiMenu(Window* window, bool animate = true); bool input(InputConfig* config, Input input) override; void onSizeChanged() override; std::vector getHelpPrompts() override; HelpStyle getHelpStyle() override; + static void openThemeConfiguration(Window* mWindow, GuiComponent* s, std::shared_ptr> theme_set, const std::string systemTheme = ""); + private: - void addEntry(const char* name, unsigned int color, bool add_arrow, const std::function& func); + void addEntry(std::string name, bool add_arrow, const std::function& func, const std::string iconName = ""); + void addVersionInfo(); void openCollectionSystemSettings(); void openConfigInput(); @@ -26,9 +33,15 @@ class GuiMenu : public GuiComponent void openScreensaverOptions(); void openSoundSettings(); void openUISettings(); + void openUpdateSettings(); + void openEmulatorSettings(); + void openSystemEmulatorSettings(SystemData* system); + + void createInputTextRow(GuiSettings *gui, std::string title, const char *settingsID, bool password); MenuComponent mMenu; TextComponent mVersion; + }; #endif // ES_APP_GUIS_GUI_MENU_H diff --git a/es-app/src/guis/GuiMetaDataEd.cpp b/es-app/src/guis/GuiMetaDataEd.cpp index fe7d29a968..394e34ee80 100644 --- a/es-app/src/guis/GuiMetaDataEd.cpp +++ b/es-app/src/guis/GuiMetaDataEd.cpp @@ -18,9 +18,10 @@ #include "FileFilterIndex.h" #include "SystemData.h" #include "Window.h" +#include "guis/GuiTextEditPopupKeyboard.h" GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector& mdd, ScraperSearchParams scraperParams, - const std::string& /*header*/, std::function saveCallback, std::function deleteFunc) : GuiComponent(window), + const std::string& /*header*/, std::function saveCallback, std::function deleteFunc, FileData* file) : GuiComponent(window), mScraperParams(scraperParams), mBackground(window, ":/frame.png"), @@ -30,14 +31,21 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector mMetaData(md), mSavedCallback(saveCallback), mDeleteFunc(deleteFunc) { + auto theme = ThemeData::getMenuTheme(); + mBackground.setImagePath(theme->Background.path); // ":/frame.png" + mBackground.setEdgeColor(theme->Background.color); + mBackground.setCenterColor(theme->Background.centerColor); + mBackground.setCornerSize(theme->Background.cornerSize); + addChild(&mBackground); addChild(&mGrid); mHeaderGrid = std::make_shared(mWindow, Vector2i(1, 5)); - mTitle = std::make_shared(mWindow, "EDIT METADATA", Font::get(FONT_SIZE_LARGE), 0x555555FF, ALIGN_CENTER); + mTitle = std::make_shared(mWindow, _("EDIT METADATA"), theme->Title.font, theme->Title.color, ALIGN_CENTER); mSubtitle = std::make_shared(mWindow, Utils::String::toUpper(Utils::FileSystem::getFileName(scraperParams.game->getPath())), - Font::get(FONT_SIZE_SMALL), 0x777777FF, ALIGN_CENTER); + theme->TextSmall.font, theme->TextSmall.color, ALIGN_CENTER); + mHeaderGrid->setEntry(mTitle, Vector2i(0, 1), false, true); mHeaderGrid->setEntry(mSubtitle, Vector2i(0, 3), false, true); @@ -46,6 +54,12 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector mList = std::make_shared(mWindow); mGrid.setEntry(mList, Vector2i(0, 1), true, true); + + SystemData* system = file->getSystem(); + + auto emul_choice = std::make_shared>(mWindow, _("EMULATOR"), false); + auto core_choice = std::make_shared>(mWindow, _("CORE"), false); + // populate list for(auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) { @@ -54,22 +68,103 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector // don't add statistics if(iter->isStatistic) continue; - + /* +#if defined(_WIN32) + if (iter->displayName == "sortname" || iter->displayName == "image" || iter->displayName == "video" || iter->displayName == "marquee" || + iter->displayName == "thumbnail" || iter->displayName == "kidgame" || iter->displayName == "description" || iter->displayName == "release date" || + iter->displayName == "genre" || iter->displayName == "publisher" || iter->displayName == "developer" || iter->displayName == "players") + continue; +#endif +*/ // create ed and add it (and any related components) to mMenu // ed's value will be set below ComponentListRow row; - auto lbl = std::make_shared(mWindow, Utils::String::toUpper(iter->displayName), Font::get(FONT_SIZE_SMALL), 0x777777FF); + + if (iter->displayName == "emulator") + { + std::string defaultEmul = system->getSystemEnvData()->getDefaultEmulator(); + std::string currentEmul = file->getEmulator(); + + if (defaultEmul.length() == 0) + emul_choice->add(_("DEFAULT"), "", true); + else + emul_choice->add(_("DEFAULT") + " (" + defaultEmul + ")", "", currentEmul.length() == 0); + + for (auto core : file->getSystem()->getSystemEnvData()->mEmulators) + emul_choice->add(core.mName, core.mName, core.mName == currentEmul); + + row.addElement(std::make_shared(mWindow, _("EMULATOR"), theme->Text.font, theme->Text.color), true); + row.addElement(emul_choice, false); + + mList->addRow(row); + emul_choice->setTag(iter->key); + mEditors.push_back(emul_choice); + + emul_choice->setSelectedChangedCallback([this, system, core_choice, file](std::string emulatorName) + { + std::string currentCore = file->getCore(); + + std::string defaultCore = system->getSystemEnvData()->getDefaultCore(emulatorName); + if (emulatorName.length() == 0) + defaultCore = system->getSystemEnvData()->getDefaultCore(system->getSystemEnvData()->getDefaultEmulator()); + + core_choice->clear(); + if (defaultCore.length() == 0) + core_choice->add(_("DEFAULT"), "", false); + else + core_choice->add(_("DEFAULT")+" ("+ defaultCore+")", "", false); + + std::vector cores = system->getSystemEnvData()->getCores(emulatorName); + + bool found = false; + + for (auto it = cores.begin(); it != cores.end(); it++) + { + std::string core = *it; + core_choice->add(core, core, currentCore == core); + if (currentCore == core) + found = true; + } + + if (!found) + core_choice->selectFirstItem(); + else + core_choice->invalidate(); + }); + + continue; + } + + if (iter->displayName == "core") + { + // core_choice->add(_("DEFAULT"), "", true); + core_choice->setTag(iter->key); + + row.addElement(std::make_shared(mWindow, "CORE", theme->Text.font, theme->Text.color), true); + row.addElement(core_choice, false); + + mList->addRow(row); + ed = core_choice; + + mEditors.push_back(core_choice); + + // force change event to load core list + emul_choice->invalidate(); + continue; + } + + auto lbl = std::make_shared(mWindow, _(Utils::String::toUpper(iter->displayName)), theme->Text.font, theme->Text.color); row.addElement(lbl, true); // label - switch(iter->type) + switch (iter->type) { - case MD_BOOL: + case MD_BOOL: { ed = std::make_shared(window); row.addElement(ed, false, true); break; } - case MD_RATING: + case MD_RATING: { ed = std::make_shared(window); const float height = lbl->getSize().y() * 0.71f; @@ -85,7 +180,7 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector break; } - case MD_DATE: + case MD_DATE: { ed = std::make_shared(window); row.addElement(ed, false); @@ -99,17 +194,17 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector break; } - case MD_TIME: + case MD_TIME: { ed = std::make_shared(window, DateTimeEditComponent::DISP_RELATIVE_TO_NOW); row.addElement(ed, false); break; } - case MD_MULTILINE_STRING: - default: + case MD_MULTILINE_STRING: + default: { // MD_STRING - ed = std::make_shared(window, "", Font::get(FONT_SIZE_SMALL, FONT_PATH_LIGHT), 0x777777FF, ALIGN_RIGHT); + ed = std::make_shared(window, "", theme->Text.font, theme->Text.color, ALIGN_RIGHT); row.addElement(ed, true); auto spacer = std::make_shared(mWindow); @@ -117,45 +212,67 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector row.addElement(spacer, false); auto bracket = std::make_shared(mWindow); - bracket->setImage(":/arrow.svg"); + bracket->setImage(ThemeData::getMenuTheme()->Icons.arrow);// ":/arrow.svg"); bracket->setResize(Vector2f(0, lbl->getFont()->getLetterHeight())); row.addElement(bracket, false); bool multiLine = iter->type == MD_MULTILINE_STRING; const std::string title = iter->displayPrompt; auto updateVal = [ed](const std::string& newVal) { ed->setValue(newVal); }; // ok callback (apply new value to ed) - row.makeAcceptInputHandler([this, title, ed, updateVal, multiLine] { - mWindow->pushGui(new GuiTextEditPopup(mWindow, title, ed->getValue(), updateVal, multiLine)); + row.makeAcceptInputHandler([this, title, ed, updateVal, multiLine] + { + if (multiLine) + mWindow->pushGui(new GuiTextEditPopup(mWindow, title, ed->getValue(), updateVal, multiLine)); + else + mWindow->pushGui(new GuiTextEditPopupKeyboard(mWindow, title, ed->getValue(), updateVal, multiLine)); }); break; } - } + } assert(ed); mList->addRow(row); + + ed->setTag(iter->key); ed->setValue(mMetaData->get(iter->key)); + mEditors.push_back(ed); } std::vector< std::shared_ptr > buttons; - if(!scraperParams.system->hasPlatformId(PlatformIds::PLATFORM_IGNORE)) - buttons.push_back(std::make_shared(mWindow, "SCRAPE", "scrape", std::bind(&GuiMetaDataEd::fetch, this))); + if (!scraperParams.system->hasPlatformId(PlatformIds::PLATFORM_IGNORE)) + buttons.push_back(std::make_shared(mWindow, _("SCRAPE"), _("SCRAPE"), std::bind(&GuiMetaDataEd::fetch, this))); - buttons.push_back(std::make_shared(mWindow, "SAVE", "save", [&] { save(); delete this; })); - buttons.push_back(std::make_shared(mWindow, "CANCEL", "cancel", [&] { delete this; })); + buttons.push_back(std::make_shared(mWindow, _("SAVE"), _("SAVE"), [&] { save(); delete this; })); + buttons.push_back(std::make_shared(mWindow, _("CANCEL"), _("CANCEL"), [&] { delete this; })); if(mDeleteFunc) { auto deleteFileAndSelf = [&] { mDeleteFunc(); delete this; }; auto deleteBtnFunc = [this, deleteFileAndSelf] { mWindow->pushGui(new GuiMsgBox(mWindow, "THIS WILL DELETE THE ACTUAL GAME FILE(S)!\nARE YOU SURE?", "YES", deleteFileAndSelf, "NO", nullptr)); }; - buttons.push_back(std::make_shared(mWindow, "DELETE", "delete", deleteBtnFunc)); + buttons.push_back(std::make_shared(mWindow, _("DELETE"), _("DELETE"), deleteBtnFunc)); } mButtons = makeButtonGrid(mWindow, buttons); mGrid.setEntry(mButtons, Vector2i(0, 2), true, false); - // resize + center + mGrid.setUnhandledInputCallback([this](InputConfig* config, Input input) -> bool { + if (config->isMappedLike("down", input)) { + mGrid.setCursorTo(mList); + mList->setCursorIndex(0); + return true; + } + if (config->isMappedLike("up", input)) { + mList->setCursorIndex(mList->size() - 1); + mGrid.moveCursor(Vector2i(0, 1)); + return true; + } + return false; + }); + + + // resize + center float width = (float)Math::min(Renderer::getScreenHeight(), (int)(Renderer::getScreenWidth() * 0.90f)); setSize(width, Renderer::getScreenHeight() * 0.82f); setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, (Renderer::getScreenHeight() - mSize.y()) / 2); @@ -179,30 +296,46 @@ void GuiMetaDataEd::onSizeChanged() mHeaderGrid->setRowHeightPerc(3, subtitleHeight / mHeaderGrid->getSize().y()); } + +#include "Gamelist.h" + void GuiMetaDataEd::save() { // remove game from index - mScraperParams.system->getIndex()->removeFromIndex(mScraperParams.game); + mScraperParams.system->removeFromIndex(mScraperParams.game); - for(unsigned int i = 0; i < mEditors.size(); i++) - { - if(mMetaDataDecl.at(i).isStatistic) - continue; - mMetaData->set(mMetaDataDecl.at(i).key, mEditors.at(i)->getValue()); + for (unsigned int i = 0; i < mEditors.size(); i++) + { + std::shared_ptr ed = mEditors.at(i); + + auto val = ed->getValue(); + auto key = ed->getTag(); + + if (key == "core" || key == "emulator") + { + std::shared_ptr> list = std::static_pointer_cast>(ed); + val = list->getSelected(); + } + + mMetaData->set(key, val); } // enter game in index - mScraperParams.system->getIndex()->addToIndex(mScraperParams.game); + mScraperParams.system->addToIndex(mScraperParams.game); - if(mSavedCallback) + if (mSavedCallback) mSavedCallback(); + saveToGamelistRecovery(mScraperParams.game); + // update respective Collection Entries CollectionSystemManager::get()->refreshCollectionSystems(mScraperParams.game); } void GuiMetaDataEd::fetch() { +// mScraperParams.nameOverride = mScraperParams.game->getName(); + GuiGameScraper* scr = new GuiGameScraper(mWindow, mScraperParams, std::bind(&GuiMetaDataEd::fetchDone, this, std::placeholders::_1)); mWindow->pushGui(scr); } @@ -211,10 +344,13 @@ void GuiMetaDataEd::fetchDone(const ScraperSearchResult& result) { for(unsigned int i = 0; i < mEditors.size(); i++) { - if(mMetaDataDecl.at(i).isStatistic) + auto val = mEditors.at(i)->getValue(); + auto key = mEditors.at(i)->getTag(); + + // Don't override favorite & hidden values, as they are not statistics + if (key == "favorite" || key == "hidden") continue; - const std::string& key = mMetaDataDecl.at(i).key; mEditors.at(i)->setValue(result.mdl.get(key)); } } @@ -225,7 +361,7 @@ void GuiMetaDataEd::close(bool closeAllWindows) bool dirty = false; for(unsigned int i = 0; i < mEditors.size(); i++) { - const std::string& key = mMetaDataDecl.at(i).key; + auto key = mEditors.at(i)->getTag(); if(mMetaData->get(key) != mEditors.at(i)->getValue()) { dirty = true; @@ -250,9 +386,9 @@ void GuiMetaDataEd::close(bool closeAllWindows) { // changes were made, ask if the user wants to save them mWindow->pushGui(new GuiMsgBox(mWindow, - "SAVE CHANGES?", - "YES", [this, closeFunc] { save(); closeFunc(); }, - "NO", closeFunc + _("SAVE CHANGES ?"), + _("YES"), [this, closeFunc] { save(); closeFunc(); }, + _("NO"), closeFunc )); }else{ closeFunc(); @@ -277,7 +413,7 @@ bool GuiMetaDataEd::input(InputConfig* config, Input input) std::vector GuiMetaDataEd::getHelpPrompts() { std::vector prompts = mGrid.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", "back")); - prompts.push_back(HelpPrompt("start", "close")); + prompts.push_back(HelpPrompt("b", _("BACK"))); + prompts.push_back(HelpPrompt("start", _("CLOSE"))); return prompts; } diff --git a/es-app/src/guis/GuiMetaDataEd.h b/es-app/src/guis/GuiMetaDataEd.h index b7b7665be7..8bdf3531d1 100644 --- a/es-app/src/guis/GuiMetaDataEd.h +++ b/es-app/src/guis/GuiMetaDataEd.h @@ -5,18 +5,21 @@ #include "components/ComponentGrid.h" #include "components/NinePatchComponent.h" #include "scrapers/Scraper.h" +#include "components/MenuComponent.h" +#include "components/OptionListComponent.h" #include "GuiComponent.h" #include "MetaData.h" class ComponentList; class TextComponent; +class FileData; class GuiMetaDataEd : public GuiComponent { public: - GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector& mdd, ScraperSearchParams params, - const std::string& header, std::function savedCallback, std::function deleteFunc); - + GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector& mdd, ScraperSearchParams params, + const std::string& header, std::function savedCallback, std::function deleteFunc, FileData* file); + bool input(InputConfig* config, Input input) override; void onSizeChanged() override; virtual std::vector getHelpPrompts() override; @@ -29,7 +32,7 @@ class GuiMetaDataEd : public GuiComponent NinePatchComponent mBackground; ComponentGrid mGrid; - + std::shared_ptr mTitle; std::shared_ptr mSubtitle; std::shared_ptr mHeaderGrid; @@ -38,6 +41,9 @@ class GuiMetaDataEd : public GuiComponent ScraperSearchParams mScraperParams; + // typedef OptionListComponent CoreList; + //std::shared_ptr mCoreList; + std::vector< std::shared_ptr > mEditors; std::vector mMetaDataDecl; diff --git a/es-app/src/guis/GuiScraperMulti.cpp b/es-app/src/guis/GuiScraperMulti.cpp index 28b87db40f..7150d48cba 100644 --- a/es-app/src/guis/GuiScraperMulti.cpp +++ b/es-app/src/guis/GuiScraperMulti.cpp @@ -15,6 +15,12 @@ GuiScraperMulti::GuiScraperMulti(Window* window, const std::queueBackground.path); // ":/frame.png" + mBackground.setEdgeColor(theme->Background.color); + mBackground.setCenterColor(theme->Background.centerColor); + mBackground.setCornerSize(theme->Background.cornerSize); + assert(mSearchQueue.size()); addChild(&mBackground); @@ -29,13 +35,13 @@ GuiScraperMulti::GuiScraperMulti(Window* window, const std::queue(mWindow, "SCRAPING IN PROGRESS", Font::get(FONT_SIZE_LARGE), 0x555555FF, ALIGN_CENTER); + mTitle = std::make_shared(mWindow, "SCRAPING IN PROGRESS", ThemeData::getMenuTheme()->Title.font, ThemeData::getMenuTheme()->Title.color, ALIGN_CENTER); mGrid.setEntry(mTitle, Vector2i(0, 0), false, true); - mSystem = std::make_shared(mWindow, "SYSTEM", Font::get(FONT_SIZE_MEDIUM), 0x777777FF, ALIGN_CENTER); + mSystem = std::make_shared(mWindow, _("SYSTEM"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color, ALIGN_CENTER); mGrid.setEntry(mSystem, Vector2i(0, 1), false, true); - mSubtitle = std::make_shared(mWindow, "subtitle text", Font::get(FONT_SIZE_SMALL), 0x888888FF, ALIGN_CENTER); + mSubtitle = std::make_shared(mWindow, "subtitle text", ThemeData::getMenuTheme()->TextSmall.font, ThemeData::getMenuTheme()->TextSmall.color, ALIGN_CENTER); mGrid.setEntry(mSubtitle, Vector2i(0, 2), false, true); mSearchComp = std::make_shared(mWindow, @@ -113,8 +119,9 @@ void GuiScraperMulti::acceptResult(const ScraperSearchResult& result) { ScraperSearchParams& search = mSearchQueue.front(); - search.game->metadata = result.mdl; - updateGamelist(search.system); + search.game->getMetadata().importScrappedMetadata(result.mdl); + saveToGamelistRecovery(search.game); + // updateGamelist(search.system); mSearchQueue.pop(); mCurrentGame++; diff --git a/es-app/src/guis/GuiScraperStart.cpp b/es-app/src/guis/GuiScraperStart.cpp index 2242046901..ee7c67a2d7 100644 --- a/es-app/src/guis/GuiScraperStart.cpp +++ b/es-app/src/guis/GuiScraperStart.cpp @@ -7,35 +7,73 @@ #include "views/ViewController.h" #include "FileData.h" #include "SystemData.h" +#include "scrapers/ThreadedScraper.h" GuiScraperStart::GuiScraperStart(Window* window) : GuiComponent(window), - mMenu(window, "SCRAPE NOW") + mMenu(window, _("SCRAPE NOW")) { + mOverwriteMedias = true; + addChild(&mMenu); // add filters (with first one selected) - mFilters = std::make_shared< OptionListComponent >(mWindow, "SCRAPE THESE GAMES", false); - mFilters->add("All Games", + mFilters = std::make_shared< OptionListComponent >(mWindow, _("SCRAPE THESE GAMES"), false); + mFilters->add(_("All Games"), [](SystemData*, FileData*) -> bool { return true; }, false); - mFilters->add("Only missing image", - [](SystemData*, FileData* g) -> bool { return g->metadata.get("image").empty(); }, true); - mMenu.addWithLabel("Filter", mFilters); + + mFilters->add(_("Only missing medias"), [this](SystemData*, FileData* g) -> bool + { + mOverwriteMedias = false; + + if (Settings::getInstance()->getString("Scraper") == "ScreenScraper") + { + if (!Settings::getInstance()->getString("ScrapperImageSrc").empty() && !Utils::FileSystem::exists(g->getMetadata().get("image"))) + return true; + + if (!Settings::getInstance()->getString("ScrapperThumbSrc").empty() && !Utils::FileSystem::exists(g->getMetadata().get("thumbnail"))) + return true; + + if (!Settings::getInstance()->getString("ScrapperLogoSrc").empty() && !Utils::FileSystem::exists(g->getMetadata().get("marquee"))) + return true; + + if (Settings::getInstance()->getBool("ScrapeVideos") && !Utils::FileSystem::exists(g->getMetadata().get("video"))) + return true; + + return false; + } + else + return !Utils::FileSystem::exists(g->getMetadata().get("image")); + + }, true); + + mMenu.addWithLabel(_("FILTER"), mFilters); + + std::string currentSystem; + + if (ViewController::get()->getState().viewing == ViewController::GAME_LIST) + currentSystem = ViewController::get()->getState().getSystem()->getName(); //add systems (all with a platformid specified selected) - mSystems = std::make_shared< OptionListComponent >(mWindow, "SCRAPE THESE SYSTEMS", true); + mSystems = std::make_shared< OptionListComponent >(mWindow, _("SCRAPE THESE SYSTEMS"), true); for(auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) { - if(!(*it)->hasPlatformId(PlatformIds::PLATFORM_IGNORE)) - mSystems->add((*it)->getFullName(), *it, !(*it)->getPlatformIds().empty()); + if ((*it)->isGroupSystem()) + continue; + + if (!(*it)->hasPlatformId(PlatformIds::PLATFORM_IGNORE)) + mSystems->add((*it)->getFullName(), *it, + currentSystem.empty() ? + !(*it)->getPlatformIds().empty() : + (*it)->getName() == currentSystem && !(*it)->getPlatformIds().empty()); } - mMenu.addWithLabel("Systems", mSystems); + mMenu.addWithLabel(_("SYSTEMS"), mSystems); mApproveResults = std::make_shared(mWindow); - mApproveResults->setState(true); - mMenu.addWithLabel("User decides on conflicts", mApproveResults); + mApproveResults->setState(false); + mMenu.addWithLabel(_("USER DECIDES ON CONFLICTS"), mApproveResults); - mMenu.addButton("START", "start", std::bind(&GuiScraperStart::pressedStart, this)); - mMenu.addButton("BACK", "back", [&] { delete this; }); + mMenu.addButton(_("START"), _("START"), std::bind(&GuiScraperStart::pressedStart, this)); + mMenu.addButton(_("BACK"), _("BACK"), [&] { delete this; }); mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); } @@ -47,10 +85,10 @@ void GuiScraperStart::pressedStart() { if((*it)->getPlatformIds().empty()) { - mWindow->pushGui(new GuiMsgBox(mWindow, - Utils::String::toUpper("Warning: some of your selected systems do not have a platform set. Results may be even more inaccurate than usual!\nContinue anyway?"), - "YES", std::bind(&GuiScraperStart::start, this), - "NO", nullptr)); + mWindow->pushGui(new GuiMsgBox(mWindow, + _(Utils::String::toUpper("Warning: some of your selected systems do not have a platform set. Results may be even more inaccurate than usual!\nContinue anyway?")), + _("YES"), std::bind(&GuiScraperStart::start, this), + _("NO"), nullptr)); return; } } @@ -64,11 +102,30 @@ void GuiScraperStart::start() if(searches.empty()) { - mWindow->pushGui(new GuiMsgBox(mWindow, - "NO GAMES FIT THAT CRITERIA.")); - }else{ - GuiScraperMulti* gsm = new GuiScraperMulti(mWindow, searches, mApproveResults->getState()); - mWindow->pushGui(gsm); + mWindow->pushGui(new GuiMsgBox(mWindow, _("NO GAMES FIT THAT CRITERIA."))); + } + else + { + if (ThreadedScraper::isRunning()) + { + Window* window = mWindow; + + mWindow->pushGui(new GuiMsgBox(mWindow, _("SCRAPING IS RUNNING. DO YOU WANT TO STOP IT ?"), _("YES"), [this, window] + { + ThreadedScraper::stop(); + }, _("NO"), nullptr)); + + return; + } + + if (mApproveResults->getState()) + { + GuiScraperMulti* gsm = new GuiScraperMulti(mWindow, searches, mApproveResults->getState()); + mWindow->pushGui(gsm); + } + else + ThreadedScraper::start(mWindow, searches); + delete this; } } @@ -86,6 +143,7 @@ std::queue GuiScraperStart::getSearches(std::vectorisMappedTo("b", input)) { delete this; @@ -122,7 +180,7 @@ bool GuiScraperStart::input(InputConfig* config, Input input) std::vector GuiScraperStart::getHelpPrompts() { std::vector prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", "back")); - prompts.push_back(HelpPrompt("start", "close")); + prompts.push_back(HelpPrompt("b", _("BACK"))); + prompts.push_back(HelpPrompt("start", _("CLOSE"))); return prompts; } diff --git a/es-app/src/guis/GuiScraperStart.h b/es-app/src/guis/GuiScraperStart.h index 6c442b9fe0..81a1bdb550 100644 --- a/es-app/src/guis/GuiScraperStart.h +++ b/es-app/src/guis/GuiScraperStart.h @@ -35,6 +35,7 @@ class GuiScraperStart : public GuiComponent std::shared_ptr mApproveResults; MenuComponent mMenu; + bool mOverwriteMedias; }; #endif // ES_APP_GUIS_GUI_SCRAPER_START_H diff --git a/es-app/src/guis/GuiScreensaverOptions.cpp b/es-app/src/guis/GuiScreensaverOptions.cpp index 6537694e5f..460241cffe 100644 --- a/es-app/src/guis/GuiScreensaverOptions.cpp +++ b/es-app/src/guis/GuiScreensaverOptions.cpp @@ -1,16 +1,16 @@ #include "guis/GuiScreensaverOptions.h" -#include "guis/GuiTextEditPopup.h" +#include "guis/GuiTextEditPopupKeyboard.h" #include "views/ViewController.h" #include "Settings.h" #include "SystemData.h" #include "Window.h" -GuiScreensaverOptions::GuiScreensaverOptions(Window* window, const char* title) : GuiComponent(window), mMenu(window, title) +GuiScreensaverOptions::GuiScreensaverOptions(Window* window, std::string title) : GuiComponent(window), mMenu(window, title) { addChild(&mMenu); - mMenu.addButton("BACK", "go back", [this] { delete this; }); + mMenu.addButton(_("BACK"), _("BACK"), [this] { delete this; }); setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); mMenu.setPosition((mSize.x() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); @@ -63,8 +63,8 @@ std::vector GuiScreensaverOptions::getHelpPrompts() { std::vector prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", "back")); - prompts.push_back(HelpPrompt("start", "close")); + prompts.push_back(HelpPrompt("b", _("BACK"))); + prompts.push_back(HelpPrompt("start", _("CLOSE"))); return prompts; } @@ -73,7 +73,7 @@ void GuiScreensaverOptions::addEditableTextComponent(ComponentListRow row, const { row.elements.clear(); - auto lbl = std::make_shared(mWindow, Utils::String::toUpper(label), Font::get(FONT_SIZE_MEDIUM), 0x777777FF); + auto lbl = std::make_shared(mWindow, Utils::String::toUpper(label), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color); row.addElement(lbl, true); // label row.addElement(ed, true); @@ -89,7 +89,7 @@ void GuiScreensaverOptions::addEditableTextComponent(ComponentListRow row, const auto updateVal = [ed](const std::string& newVal) { ed->setValue(newVal); }; // ok callback (apply new value to ed) row.makeAcceptInputHandler([this, label, ed, updateVal] { - mWindow->pushGui(new GuiTextEditPopup(mWindow, label, ed->getValue(), updateVal, false)); + mWindow->pushGui(new GuiTextEditPopupKeyboard(mWindow, label, ed->getValue(), updateVal, false)); }); assert(ed); addRow(row); diff --git a/es-app/src/guis/GuiScreensaverOptions.h b/es-app/src/guis/GuiScreensaverOptions.h index cabcff3c59..7eec66c660 100644 --- a/es-app/src/guis/GuiScreensaverOptions.h +++ b/es-app/src/guis/GuiScreensaverOptions.h @@ -8,7 +8,7 @@ class GuiScreensaverOptions : public GuiComponent { public: - GuiScreensaverOptions(Window* window, const char* title); + GuiScreensaverOptions(Window* window, std::string title); virtual ~GuiScreensaverOptions(); // just calls save(); virtual void save(); diff --git a/es-app/src/guis/GuiSettings.cpp b/es-app/src/guis/GuiSettings.cpp index 98e4c0fd58..fc81a23580 100644 --- a/es-app/src/guis/GuiSettings.cpp +++ b/es-app/src/guis/GuiSettings.cpp @@ -4,28 +4,50 @@ #include "Settings.h" #include "SystemData.h" #include "Window.h" +#include "EsLocale.h" -GuiSettings::GuiSettings(Window* window, const char* title) : GuiComponent(window), mMenu(window, title) +GuiSettings::GuiSettings(Window* window, const std::string title) : GuiComponent(window), mMenu(window, title) { addChild(&mMenu); - mMenu.addButton("BACK", "go back", [this] { delete this; }); + mCloseButton = "start"; + mMenu.addButton(_("BACK"), _("go back"), [this] { close(); }); setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); - mMenu.setPosition((mSize.x() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); + + if (Renderer::isSmallScreen()) + mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, (Renderer::getScreenHeight() - mMenu.getSize().y()) / 2); + else + mMenu.setPosition((mSize.x() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); } GuiSettings::~GuiSettings() +{ + +} + +void GuiSettings::updatePosition() +{ + setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); + mMenu.setPosition((mSize.x() - mMenu.getSize().x()) / 2, (mSize.y() - mMenu.getSize().y()) / 2); +} + +void GuiSettings::close() { save(); + + if (mOnFinalizeFunc != nullptr) + mOnFinalizeFunc(); + + delete this; } void GuiSettings::save() { - if(!mSaveFuncs.size()) + if (!mSaveFuncs.size()) return; - for(auto it = mSaveFuncs.cbegin(); it != mSaveFuncs.cend(); it++) + for (auto it = mSaveFuncs.cbegin(); it != mSaveFuncs.cend(); it++) (*it)(); Settings::getInstance()->saveFile(); @@ -33,17 +55,17 @@ void GuiSettings::save() bool GuiSettings::input(InputConfig* config, Input input) { - if(config->isMappedTo("b", input) && input.value != 0) + if (config->isMappedTo("b", input) && input.value != 0) { - delete this; + close(); return true; } - if(config->isMappedTo("start", input) && input.value != 0) + if (config->isMappedTo(mCloseButton, input) && input.value != 0) { // close everything Window* window = mWindow; - while(window->peekGui() && window->peekGui() != ViewController::get()) + while (window->peekGui() && window->peekGui() != ViewController::get()) delete window->peekGui(); return true; } @@ -62,8 +84,21 @@ std::vector GuiSettings::getHelpPrompts() { std::vector prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", "back")); - prompts.push_back(HelpPrompt("start", "close")); + prompts.push_back(HelpPrompt("b", _("BACK"))); + prompts.push_back(HelpPrompt(mCloseButton, _("CLOSE"))); return prompts; } + +void GuiSettings::addSubMenu(const std::string& label, const std::function& func) +{ + ComponentListRow row; + row.makeAcceptInputHandler(func); + + auto theme = ThemeData::getMenuTheme(); + + auto entryMenu = std::make_shared(mWindow, label, theme->Text.font, theme->Text.color); + row.addElement(entryMenu, true); + row.addElement(makeArrow(mWindow), false); + mMenu.addRow(row); +}; \ No newline at end of file diff --git a/es-app/src/guis/GuiSettings.h b/es-app/src/guis/GuiSettings.h index d5b0d97998..473edb99b4 100644 --- a/es-app/src/guis/GuiSettings.h +++ b/es-app/src/guis/GuiSettings.h @@ -8,21 +8,56 @@ class GuiSettings : public GuiComponent { public: - GuiSettings(Window* window, const char* title); + GuiSettings(Window* window, const std::string title); virtual ~GuiSettings(); // just calls save(); + void close(); void save(); + inline void addRow(const ComponentListRow& row) { mMenu.addRow(row); }; - inline void addWithLabel(const std::string& label, const std::shared_ptr& comp) { mMenu.addWithLabel(label, comp); }; + inline void addWithLabel(const std::string& label, const std::shared_ptr& comp, bool setCursorHere = false) { mMenu.addWithLabel(label, comp, "", setCursorHere); }; inline void addSaveFunc(const std::function& func) { mSaveFuncs.push_back(func); }; + inline void addEntry(const std::string name, bool add_arrow = false, const std::function& func = nullptr, const std::string iconName = "") { mMenu.addEntry(name, add_arrow, func, iconName); }; + + void addSubMenu(const std::string& label, const std::function& func); + + inline void setSave(bool sav) { mDoSave = sav; }; // batocera bool input(InputConfig* config, Input input) override; std::vector getHelpPrompts() override; HelpStyle getHelpStyle() override; -private: + MenuComponent& getMenu() { return mMenu; } + + inline void onFinalize(const std::function& func) { mOnFinalizeFunc = func; }; + + bool getVariable(const std::string name) + { + if (mVariableMap.find(name) == mVariableMap.cend()) + return false; + + return mVariableMap[name]; + } + + void setCloseButton(const std::string name) { mCloseButton = name; } + void setVariable(const std::string name, bool value) { mVariableMap[name] = value; } + void updatePosition(); + + void setTitle(const std::string title) { mMenu.setTitle(title); } + void setSubTitle(const std::string text) { mMenu.setSubTitle(text); } + +protected: MenuComponent mMenu; + +private: + bool mDoSave = true; // batocera + std::vector< std::function > mSaveFuncs; + std::function mOnFinalizeFunc; + + std::map mVariableMap; + + std::string mCloseButton; }; #endif // ES_APP_GUIS_GUI_SETTINGS_H diff --git a/es-app/src/guis/GuiSlideshowScreensaverOptions.cpp b/es-app/src/guis/GuiSlideshowScreensaverOptions.cpp index 9f3c5b9db9..6d7dd28d37 100644 --- a/es-app/src/guis/GuiSlideshowScreensaverOptions.cpp +++ b/es-app/src/guis/GuiSlideshowScreensaverOptions.cpp @@ -2,7 +2,7 @@ #include "components/SliderComponent.h" #include "components/SwitchComponent.h" -#include "guis/GuiTextEditPopup.h" +#include "guis/GuiTextEditPopupKeyboard.h" #include "utils/StringUtil.h" #include "Settings.h" #include "Window.h" @@ -14,37 +14,54 @@ GuiSlideshowScreensaverOptions::GuiSlideshowScreensaverOptions(Window* window, c // image duration (seconds) auto sss_image_sec = std::make_shared(mWindow, 1.f, 60.f, 1.f, "s"); sss_image_sec->setValue((float)(Settings::getInstance()->getInt("ScreenSaverSwapImageTimeout") / (1000))); - addWithLabel(row, "SWAP IMAGE AFTER (SECS)", sss_image_sec); + addWithLabel(row, _("SWAP IMAGE AFTER (SECS)"), sss_image_sec); addSaveFunc([sss_image_sec] { int playNextTimeout = (int)Math::round(sss_image_sec->getValue()) * (1000); Settings::getInstance()->setInt("ScreenSaverSwapImageTimeout", playNextTimeout); PowerSaver::updateTimeouts(); }); + + // SHOW GAME NAME + auto ss_controls = std::make_shared(mWindow); + ss_controls->setState(Settings::getInstance()->getBool("SlideshowScreenSaverGameName")); + addWithLabel(row, _("SHOW GAME INFO"), ss_controls); + addSaveFunc([ss_controls] { Settings::getInstance()->setBool("SlideshowScreenSaverGameName", ss_controls->getState()); }); + auto marquee_screensaver = std::make_shared(mWindow); + marquee_screensaver->setState(Settings::getInstance()->getBool("ScreenSaverMarquee")); + addWithLabel(row, _("USE MARQUEE AS GAME INFO"), marquee_screensaver); + addSaveFunc([marquee_screensaver] { Settings::getInstance()->setBool("ScreenSaverMarquee", marquee_screensaver->getState()); }); + /* + auto decoration_screensaver = std::make_shared(mWindow); + decoration_screensaver->setState(Settings::getInstance()->getBool("ScreenSaverDecoration")); + addWithLabel(row, _("USE RANDOM DECORATION"), decoration_screensaver); + addSaveFunc([decoration_screensaver] { Settings::getInstance()->setBool("ScreenSaverDecoration", decoration_screensaver->getState()); }); + */ // stretch auto sss_stretch = std::make_shared(mWindow); sss_stretch->setState(Settings::getInstance()->getBool("SlideshowScreenSaverStretch")); - addWithLabel(row, "STRETCH IMAGES", sss_stretch); + addWithLabel(row, _("STRETCH IMAGES"), sss_stretch); addSaveFunc([sss_stretch] { Settings::getInstance()->setBool("SlideshowScreenSaverStretch", sss_stretch->getState()); }); + /* // background audio file - auto sss_bg_audio_file = std::make_shared(mWindow, "", Font::get(FONT_SIZE_SMALL), 0x777777FF); + auto sss_bg_audio_file = std::make_shared(mWindow, "", ThemeData::getMenuTheme()->TextSmall.font, ThemeData::getMenuTheme()->TextSmall.color); addEditableTextComponent(row, "BACKGROUND AUDIO", sss_bg_audio_file, Settings::getInstance()->getString("SlideshowScreenSaverBackgroundAudioFile")); addSaveFunc([sss_bg_audio_file] { Settings::getInstance()->setString("SlideshowScreenSaverBackgroundAudioFile", sss_bg_audio_file->getValue()); }); - + */ // image source auto sss_custom_source = std::make_shared(mWindow); sss_custom_source->setState(Settings::getInstance()->getBool("SlideshowScreenSaverCustomImageSource")); - addWithLabel(row, "USE CUSTOM IMAGES", sss_custom_source); + addWithLabel(row, _("USE CUSTOM IMAGES"), sss_custom_source); addSaveFunc([sss_custom_source] { Settings::getInstance()->setBool("SlideshowScreenSaverCustomImageSource", sss_custom_source->getState()); }); // custom image directory - auto sss_image_dir = std::make_shared(mWindow, "", Font::get(FONT_SIZE_SMALL), 0x777777FF); - addEditableTextComponent(row, "CUSTOM IMAGE DIR", sss_image_dir, Settings::getInstance()->getString("SlideshowScreenSaverImageDir")); + auto sss_image_dir = std::make_shared(mWindow, "", ThemeData::getMenuTheme()->TextSmall.font, ThemeData::getMenuTheme()->TextSmall.color); + addEditableTextComponent(row, _("CUSTOM IMAGE DIR"), sss_image_dir, Settings::getInstance()->getString("SlideshowScreenSaverImageDir")); addSaveFunc([sss_image_dir] { Settings::getInstance()->setString("SlideshowScreenSaverImageDir", sss_image_dir->getValue()); }); @@ -52,17 +69,18 @@ GuiSlideshowScreensaverOptions::GuiSlideshowScreensaverOptions(Window* window, c // recurse custom image directory auto sss_recurse = std::make_shared(mWindow); sss_recurse->setState(Settings::getInstance()->getBool("SlideshowScreenSaverRecurse")); - addWithLabel(row, "CUSTOM IMAGE DIR RECURSIVE", sss_recurse); + addWithLabel(row, _("CUSTOM IMAGE DIR RECURSIVE"), sss_recurse); addSaveFunc([sss_recurse] { Settings::getInstance()->setBool("SlideshowScreenSaverRecurse", sss_recurse->getState()); }); // custom image filter - auto sss_image_filter = std::make_shared(mWindow, "", Font::get(FONT_SIZE_SMALL), 0x777777FF); - addEditableTextComponent(row, "CUSTOM IMAGE FILTER", sss_image_filter, Settings::getInstance()->getString("SlideshowScreenSaverImageFilter")); + auto sss_image_filter = std::make_shared(mWindow, "", ThemeData::getMenuTheme()->TextSmall.font, ThemeData::getMenuTheme()->TextSmall.color); + addEditableTextComponent(row, _("CUSTOM IMAGE FILTER"), sss_image_filter, Settings::getInstance()->getString("SlideshowScreenSaverImageFilter")); addSaveFunc([sss_image_filter] { Settings::getInstance()->setString("SlideshowScreenSaverImageFilter", sss_image_filter->getValue()); }); + } GuiSlideshowScreensaverOptions::~GuiSlideshowScreensaverOptions() @@ -73,10 +91,38 @@ void GuiSlideshowScreensaverOptions::addWithLabel(ComponentListRow row, const st { row.elements.clear(); - auto lbl = std::make_shared(mWindow, Utils::String::toUpper(label), Font::get(FONT_SIZE_MEDIUM), 0x777777FF); + auto lbl = std::make_shared(mWindow, Utils::String::toUpper(label), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color); row.addElement(lbl, true); // label row.addElement(component, false, true); addRow(row); } + +void GuiSlideshowScreensaverOptions::addEditableTextComponent(ComponentListRow row, const std::string label, std::shared_ptr ed, std::string value) +{ + row.elements.clear(); + + auto lbl = std::make_shared(mWindow, Utils::String::toUpper(label), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color); + row.addElement(lbl, true); // label + + row.addElement(ed, true); + + auto spacer = std::make_shared(mWindow); + spacer->setSize(Renderer::getScreenWidth() * 0.005f, 0); + row.addElement(spacer, false); + + auto bracket = std::make_shared(mWindow); + bracket->setImage(ThemeData::getMenuTheme()->Icons.arrow); // ":/arrow.svg"); + bracket->setResize(Vector2f(0, lbl->getFont()->getLetterHeight())); + row.addElement(bracket, false); + + auto updateVal = [ed](const std::string& newVal) { ed->setValue(newVal); }; // ok callback (apply new value to ed) + row.makeAcceptInputHandler([this, label, ed, updateVal] { + mWindow->pushGui(new GuiTextEditPopupKeyboard(mWindow, label, ed->getValue(), updateVal, false)); + }); + + assert(ed); + addRow(row); + ed->setValue(value); +} diff --git a/es-app/src/guis/GuiSlideshowScreensaverOptions.h b/es-app/src/guis/GuiSlideshowScreensaverOptions.h index 4232a69bad..ec46ee4f0c 100644 --- a/es-app/src/guis/GuiSlideshowScreensaverOptions.h +++ b/es-app/src/guis/GuiSlideshowScreensaverOptions.h @@ -11,6 +11,7 @@ class GuiSlideshowScreensaverOptions : public GuiScreensaverOptions virtual ~GuiSlideshowScreensaverOptions(); private: + void addEditableTextComponent(ComponentListRow row, const std::string label, std::shared_ptr ed, std::string value); void addWithLabel(ComponentListRow row, const std::string label, std::shared_ptr component); }; diff --git a/es-app/src/guis/GuiThemeInstall.cpp b/es-app/src/guis/GuiThemeInstall.cpp new file mode 100644 index 0000000000..1b2933c2ea --- /dev/null +++ b/es-app/src/guis/GuiThemeInstall.cpp @@ -0,0 +1,109 @@ +#include "guis/GuiThemeInstall.h" + +#include "ApiSystem.h" +#include "components/OptionListComponent.h" +#include "guis/GuiSettings.h" +#include "views/ViewController.h" +#include "utils/StringUtil.h" +#include "components/ComponentGrid.h" +#include "components/MultiLineMenuEntry.h" +#include "EsLocale.h" +#include "ContentInstaller.h" + +GuiThemeInstall::GuiThemeInstall(Window* window) + : GuiComponent(window), mMenu(window, _("SELECT THEME TO INSTALL").c_str()) +{ + auto theme = ThemeData::getMenuTheme(); + + addChild(&mMenu); + + auto themes = ApiSystem::getThemesList(); + for(auto utheme : themes) + { + std::string themeName = utheme.name; + + ComponentListRow row; + + // icon + /* + auto icon = std::make_shared(mWindow); + icon->setImage(utheme.installed ? ":/star_filled.svg" : ":/star_unfilled.svg"); + icon->setColorShift(theme->Text.color); + icon->setResize(0, theme->Text.font->getLetterHeight() * 1.25f); + row.addElement(icon, false); + */ + + auto icon = std::make_shared(mWindow); + icon->setColor(theme->Text.color); + + if (utheme.installed) + icon->setOpacity(192); + + icon->setFont(theme->Text.font); + icon->setText(utheme.installed ? _U("\uF021") : _U("\uF019")); + icon->setSize(theme->Text.font->getLetterHeight() * 1.5f, 0); + //icon->setResize(0, theme->Text.font->getLetterHeight() * 1.25f); + row.addElement(icon, false); + + + // spacer between icon and text + auto spacer = std::make_shared(mWindow); + spacer->setSize(10, 0); + row.addElement(spacer, false); + + auto grid = std::make_shared(window, utheme.name, utheme.url); + row.addElement(grid, true); + row.makeAcceptInputHandler([this, themeName] { start(themeName); }); + + mMenu.addRow(row); + } + + mMenu.addButton(_("BACK"), "back", [&] { delete this; }); + + if (Renderer::isSmallScreen()) + mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, (Renderer::getScreenHeight() - mMenu.getSize().y()) / 2); + else + mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); +} + +void GuiThemeInstall::start(std::string themeName) +{ + if (themeName.empty()) + return; + + char trstring[256]; + snprintf(trstring, 256, _("'%s' ADDED TO DOWNLOAD QUEUE").c_str(), themeName.c_str()); // batocera + mWindow->displayNotificationMessage(_U("\uF019 ") + std::string(trstring)); + + ContentInstaller::Enqueue(mWindow, ContentInstaller::CONTENT_THEME, themeName); + delete this; +} + +bool GuiThemeInstall::input(InputConfig* config, Input input) +{ + if(GuiComponent::input(config, input)) + return true; + + if(input.value != 0 && config->isMappedTo("b", input)) + { + delete this; + return true; + } + + if(config->isMappedTo("start", input) && input.value != 0) + { + // close everything + Window* window = mWindow; + while(window->peekGui() && window->peekGui() != ViewController::get()) + delete window->peekGui(); + } + return false; +} + +std::vector GuiThemeInstall::getHelpPrompts() +{ + std::vector prompts = mMenu.getHelpPrompts(); + prompts.push_back(HelpPrompt("b", _("BACK"))); + prompts.push_back(HelpPrompt("start", _("CLOSE"))); + return prompts; +} diff --git a/es-app/src/guis/GuiThemeInstall.h b/es-app/src/guis/GuiThemeInstall.h new file mode 100644 index 0000000000..0f4041d0b0 --- /dev/null +++ b/es-app/src/guis/GuiThemeInstall.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include "GuiComponent.h" +#include "components/MenuComponent.h" +#include "components/ComponentGrid.h" +#include "components/TextComponent.h" + +template +class OptionListComponent; + +// Batocera +class GuiThemeInstall : public GuiComponent +{ +public: + GuiThemeInstall(Window* window); + bool input(InputConfig* config, Input input) override; + + virtual std::vector getHelpPrompts() override; + +private: + void start(std::string themeName); + + MenuComponent mMenu; +}; diff --git a/es-app/src/guis/GuiVideoScreensaverOptions.cpp b/es-app/src/guis/GuiVideoScreensaverOptions.cpp index b491c2ff1f..734eb72692 100644 --- a/es-app/src/guis/GuiVideoScreensaverOptions.cpp +++ b/es-app/src/guis/GuiVideoScreensaverOptions.cpp @@ -11,79 +11,67 @@ GuiVideoScreensaverOptions::GuiVideoScreensaverOptions(Window* window, const cha // timeout to swap videos auto swap = std::make_shared(mWindow, 10.f, 1000.f, 1.f, "s"); swap->setValue((float)(Settings::getInstance()->getInt("ScreenSaverSwapVideoTimeout") / (1000))); - addWithLabel("SWAP VIDEO AFTER (SECS)", swap); + addWithLabel(_("SWAP VIDEO AFTER (SECS)"), swap); addSaveFunc([swap] { int playNextTimeout = (int)Math::round(swap->getValue()) * (1000); Settings::getInstance()->setInt("ScreenSaverSwapVideoTimeout", playNextTimeout); PowerSaver::updateTimeouts(); }); - auto stretch_screensaver = std::make_shared(mWindow); - stretch_screensaver->setState(Settings::getInstance()->getBool("StretchVideoOnScreenSaver")); - addWithLabel("STRETCH VIDEO ON SCREENSAVER", stretch_screensaver); - addSaveFunc([stretch_screensaver] { Settings::getInstance()->setBool("StretchVideoOnScreenSaver", stretch_screensaver->getState()); }); #ifdef _RPI_ auto ss_omx = std::make_shared(mWindow); ss_omx->setState(Settings::getInstance()->getBool("ScreenSaverOmxPlayer")); - addWithLabel("USE OMX PLAYER FOR SCREENSAVER", ss_omx); + addWithLabel(_("USE OMX PLAYER FOR SCREENSAVER"), ss_omx); addSaveFunc([ss_omx, this] { Settings::getInstance()->setBool("ScreenSaverOmxPlayer", ss_omx->getState()); }); + + ss_omx->setOnChangedCallback([this, ss_omx, window]() + { + if (Settings::getInstance()->setBool("ScreenSaverOmxPlayer", ss_omx->getState())) + { + Window* pw = mWindow; + delete this; + pw->pushGui(new GuiVideoScreensaverOptions(pw, _("VIDEO SCREENSAVER").c_str())); + } + }); #endif // Render Video Game Name as subtitles - auto ss_info = std::make_shared< OptionListComponent >(mWindow, "SHOW GAME INFO", false); + auto ss_info = std::make_shared< OptionListComponent >(mWindow, _("SHOW GAME INFO"), false); std::vector info_type; info_type.push_back("always"); info_type.push_back("start & end"); info_type.push_back("never"); for(auto it = info_type.cbegin(); it != info_type.cend(); it++) - ss_info->add(*it, *it, Settings::getInstance()->getString("ScreenSaverGameInfo") == *it); - addWithLabel("SHOW GAME INFO ON SCREENSAVER", ss_info); + ss_info->add(_(it->c_str()), *it, Settings::getInstance()->getString("ScreenSaverGameInfo") == *it); + addWithLabel(_("SHOW GAME INFO ON SCREENSAVER"), ss_info); addSaveFunc([ss_info, this] { Settings::getInstance()->setString("ScreenSaverGameInfo", ss_info->getSelected()); }); -#ifdef _RPI_ - ComponentListRow row; - - // Set subtitle position - auto ss_omx_subs_align = std::make_shared< OptionListComponent >(mWindow, "GAME INFO ALIGNMENT", false); - std::vector align_mode; - align_mode.push_back("left"); - align_mode.push_back("center"); - for(auto it = align_mode.cbegin(); it != align_mode.cend(); it++) - ss_omx_subs_align->add(*it, *it, Settings::getInstance()->getString("SubtitleAlignment") == *it); - addWithLabel("GAME INFO ALIGNMENT", ss_omx_subs_align); - addSaveFunc([ss_omx_subs_align, this] { Settings::getInstance()->setString("SubtitleAlignment", ss_omx_subs_align->getSelected()); }); - // Set font size - auto ss_omx_font_size = std::make_shared(mWindow, 1.f, 64.f, 1.f, "h"); - ss_omx_font_size->setValue((float)(Settings::getInstance()->getInt("SubtitleSize"))); - addWithLabel("GAME INFO FONT SIZE", ss_omx_font_size); - addSaveFunc([ss_omx_font_size] { - int subSize = (int)Math::round(ss_omx_font_size->getValue()); - Settings::getInstance()->setInt("SubtitleSize", subSize); - }); + bool advancedOptions = true; - // Define subtitle font - auto ss_omx_font_file = std::make_shared(mWindow, "", Font::get(FONT_SIZE_SMALL), 0x777777FF); - addEditableTextComponent(row, "PATH TO FONT FILE", ss_omx_font_file, Settings::getInstance()->getString("SubtitleFont")); - addSaveFunc([ss_omx_font_file] { - Settings::getInstance()->setString("SubtitleFont", ss_omx_font_file->getValue()); - }); +//#ifdef _RPI_ + advancedOptions = !Settings::getInstance()->getBool("ScreenSaverOmxPlayer"); +//#endif - // Define subtitle italic font - auto ss_omx_italic_font_file = std::make_shared(mWindow, "", Font::get(FONT_SIZE_SMALL), 0x777777FF); - addEditableTextComponent(row, "PATH TO ITALIC FONT FILE", ss_omx_italic_font_file, Settings::getInstance()->getString("SubtitleItalicFont")); - addSaveFunc([ss_omx_italic_font_file] { - Settings::getInstance()->setString("SubtitleItalicFont", ss_omx_italic_font_file->getValue()); - }); -#endif + if (advancedOptions) + { + auto marquee_screensaver = std::make_shared(mWindow); + marquee_screensaver->setState(Settings::getInstance()->getBool("ScreenSaverMarquee")); + addWithLabel(_("USE MARQUEE AS GAME INFO"), marquee_screensaver); + addSaveFunc([marquee_screensaver] { Settings::getInstance()->setBool("ScreenSaverMarquee", marquee_screensaver->getState()); }); +/* + auto decoration_screensaver = std::make_shared(mWindow); + decoration_screensaver->setState(Settings::getInstance()->getBool("ScreenSaverDecoration")); + addWithLabel(_("USE RANDOM DECORATION"), decoration_screensaver); + addSaveFunc([decoration_screensaver] { Settings::getInstance()->setBool("ScreenSaverDecoration", decoration_screensaver->getState()); }); +*/ + } -#ifndef _RPI_ - auto captions_compatibility = std::make_shared(mWindow); - captions_compatibility->setState(Settings::getInstance()->getBool("CaptionsCompatibility")); - addWithLabel("USE COMPATIBLE LOW RESOLUTION FOR CAPTIONS", captions_compatibility); - addSaveFunc([captions_compatibility] { Settings::getInstance()->setBool("CaptionsCompatibility", captions_compatibility->getState()); }); -#endif + auto stretch_screensaver = std::make_shared(mWindow); + stretch_screensaver->setState(Settings::getInstance()->getBool("StretchVideoOnScreenSaver")); + addWithLabel(_("STRETCH VIDEO ON SCREENSAVER"), stretch_screensaver); + addSaveFunc([stretch_screensaver] { Settings::getInstance()->setBool("StretchVideoOnScreenSaver", stretch_screensaver->getState()); }); } GuiVideoScreensaverOptions::~GuiVideoScreensaverOptions() diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index dd6538b8fa..9592c14644 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -21,24 +21,105 @@ #include #include #include -#ifdef WIN32 + +#if defined(_WIN32) #include +#elif defined(__linux__) +#include #endif +#include "resources/TextureData.h" #include +#include "AudioManager.h" +#include "NetworkThread.h" +#include "scrapers/ThreadedScraper.h" +#include "ImageIO.h" bool scrape_cmdline = false; +#include "components/VideoVlcComponent.h" + +static std::string gPlayVideo; +static int gPlayVideoDuration = 0; + +void playVideo() +{ + Settings::getInstance()->setBool("AlwaysOnTop", true); + + Window window; + if (!window.init(true)) + { + LOG(LogError) << "Window failed to initialize!"; + return; + } + + Settings::getInstance()->setBool("VideoAudio", true); + + bool exitLoop = false; + + VideoVlcComponent vid(&window); + vid.setVideo(gPlayVideo); + vid.setOrigin(0.5f, 0.5f); + vid.setPosition(Renderer::getScreenWidth() / 2.0f, Renderer::getScreenHeight() / 2.0f); + vid.setMaxSize(Renderer::getScreenWidth(), Renderer::getScreenHeight()); + + vid.setOnVideoEnded([&exitLoop]() + { + exitLoop = true; + return false; + }); + + window.pushGui(&vid); + + vid.onShow(); + vid.topWindow(true); + + int lastTime = SDL_GetTicks(); + int totalTime = 0; + + while (!exitLoop) + { + SDL_Event event; + + if (SDL_PollEvent(&event)) + { + do + { + if (event.type == SDL_QUIT) + return; + } while (SDL_PollEvent(&event)); + } + + int curTime = SDL_GetTicks(); + int deltaTime = curTime - lastTime; + + if (vid.isPlaying()) + { + totalTime += deltaTime; + + if (gPlayVideoDuration > 0 && totalTime > gPlayVideoDuration * 100) + break; + } + + Transform4x4f transform = Transform4x4f::Identity(); + vid.update(deltaTime); + vid.render(transform); + + Renderer::swapBuffers(); + } + + window.deinit(true); +} + bool parseArgs(int argc, char* argv[]) { Utils::FileSystem::setExePath(argv[0]); - // We need to process --home before any call to Settings::getInstance(), because settings are loaded from homepath - for(int i = 1; i < argc; i++) + for (int i = 1; i < argc; i++) { - if(strcmp(argv[i], "--home") == 0) + if (strcmp(argv[i], "--home") == 0) { - if(i >= argc - 1) + if (i >= argc - 1) { std::cerr << "Invalid home path supplied."; return false; @@ -48,10 +129,34 @@ bool parseArgs(int argc, char* argv[]) break; } } - + for(int i = 1; i < argc; i++) { - if(strcmp(argv[i], "--resolution") == 0) + if (strcmp(argv[i], "--videoduration") == 0) + { + gPlayVideoDuration = atoi(argv[i + 1]); + i++; // skip the argument value + } + else + if (strcmp(argv[i], "--video") == 0) + { + gPlayVideo = argv[i + 1]; + i++; // skip the argument value + } + else + if (strcmp(argv[i], "--monitor") == 0) + { + if (i >= argc - 1) + { + std::cerr << "Invalid monitor supplied."; + return false; + } + + int monitorId = atoi(argv[i + 1]); + i++; // skip the argument value + Settings::getInstance()->setInt("MonitorID", monitorId); + } + else if (strcmp(argv[i], "--resolution") == 0) { if(i >= argc - 2) { @@ -64,9 +169,11 @@ bool parseArgs(int argc, char* argv[]) i += 2; // skip the argument value Settings::getInstance()->setInt("WindowWidth", width); Settings::getInstance()->setInt("WindowHeight", height); - }else if(strcmp(argv[i], "--screensize") == 0) + Settings::getInstance()->setBool("FullscreenBorderless", false); + } + else if (strcmp(argv[i], "--screensize") == 0) { - if(i >= argc - 2) + if (i >= argc - 2) { std::cerr << "Invalid screensize supplied."; return false; @@ -77,7 +184,8 @@ bool parseArgs(int argc, char* argv[]) i += 2; // skip the argument value Settings::getInstance()->setInt("ScreenWidth", width); Settings::getInstance()->setInt("ScreenHeight", height); - }else if(strcmp(argv[i], "--screenoffset") == 0) + } + else if (strcmp(argv[i], "--screenoffset") == 0) { if(i >= argc - 2) { @@ -90,7 +198,8 @@ bool parseArgs(int argc, char* argv[]) i += 2; // skip the argument value Settings::getInstance()->setInt("ScreenOffsetX", x); Settings::getInstance()->setInt("ScreenOffsetY", y); - }else if (strcmp(argv[i], "--screenrotate") == 0) + } + else if (strcmp(argv[i], "--screenrotate") == 0) { if (i >= argc - 1) { @@ -101,44 +210,74 @@ bool parseArgs(int argc, char* argv[]) int rotate = atoi(argv[i + 1]); ++i; // skip the argument value Settings::getInstance()->setInt("ScreenRotate", rotate); - }else if(strcmp(argv[i], "--gamelist-only") == 0) + } + else if (strcmp(argv[i], "--gamelist-only") == 0) { Settings::getInstance()->setBool("ParseGamelistOnly", true); - }else if(strcmp(argv[i], "--ignore-gamelist") == 0) + } + else if (strcmp(argv[i], "--ignore-gamelist") == 0) { Settings::getInstance()->setBool("IgnoreGamelist", true); - }else if(strcmp(argv[i], "--show-hidden-files") == 0) + } + else if (strcmp(argv[i], "--show-hidden-files") == 0) { Settings::getInstance()->setBool("ShowHiddenFiles", true); - }else if(strcmp(argv[i], "--draw-framerate") == 0) + } + else if (strcmp(argv[i], "--draw-framerate") == 0) { Settings::getInstance()->setBool("DrawFramerate", true); - }else if(strcmp(argv[i], "--no-exit") == 0) + } + else if (strcmp(argv[i], "--no-exit") == 0) { Settings::getInstance()->setBool("ShowExit", false); - }else if(strcmp(argv[i], "--no-splash") == 0) + } + else if (strcmp(argv[i], "--no-splash") == 0) { Settings::getInstance()->setBool("SplashScreen", false); - }else if(strcmp(argv[i], "--debug") == 0) + } + else if (strcmp(argv[i], "--debug") == 0) { Settings::getInstance()->setBool("Debug", true); Settings::getInstance()->setBool("HideConsole", false); Log::setReportingLevel(LogDebug); - }else if(strcmp(argv[i], "--fullscreen-borderless") == 0) + } + else if (strcmp(argv[i], "--fullscreen-borderless") == 0) { Settings::getInstance()->setBool("FullscreenBorderless", true); - }else if(strcmp(argv[i], "--windowed") == 0) + } + else if (strcmp(argv[i], "--fullscreen") == 0) { - Settings::getInstance()->setBool("Windowed", true); - }else if(strcmp(argv[i], "--vsync") == 0) + Settings::getInstance()->setBool("FullscreenBorderless", false); + } + else if (strcmp(argv[i], "--windowed") == 0 || strcmp(argv[i], "-windowed") == 0) { - bool vsync = (strcmp(argv[i + 1], "on") == 0 || strcmp(argv[i + 1], "1") == 0) ? true : false; + Settings::getInstance()->setBool("Windowed", true); + } + else if (strcmp(argv[i], "--vsync") == 0 || strcmp(argv[i], "-vsync") == 0) + { + bool vsync = false; + if (i == argc - 1) + vsync = true; + else + { + std::string arg = argv[i + 1]; + if (arg.find("-") == 0) + vsync = true; + else + { + vsync = (strcmp(argv[i + 1], "on") == 0 || strcmp(argv[i + 1], "1") == 0) ? true : false; + i++; // skip vsync value + } + } + Settings::getInstance()->setBool("VSync", vsync); - i++; // skip vsync value - }else if(strcmp(argv[i], "--scrape") == 0) + + } + else if (strcmp(argv[i], "--scrape") == 0) { scrape_cmdline = true; - }else if(strcmp(argv[i], "--max-vram") == 0) + } + else if (strcmp(argv[i], "--max-vram") == 0) { int maxVRAM = atoi(argv[i + 1]); Settings::getInstance()->setInt("MaxVRAM", maxVRAM); @@ -184,7 +323,6 @@ bool parseArgs(int argc, char* argv[]) "--force-kid Force the UI mode to be Kid\n" "--force-kiosk Force the UI mode to be Kiosk\n" "--force-disable-filters Force the UI to ignore applied filters in gamelist\n" - "--home [path] Directory to use as home path\n" "--help, -h summon a sentient, angry tuba\n\n" "More information available in README.md.\n"; return false; //exit after printing help @@ -214,25 +352,31 @@ bool verifyHomeFolderExists() } // Returns true if everything is OK, -bool loadSystemConfigFile(const char** errorString) +bool loadSystemConfigFile(Window* window, const char** errorString) { *errorString = NULL; + + ImageIO::loadImageCache(); - if(!SystemData::loadConfig()) + if (!SystemData::loadConfig(window)) { LOG(LogError) << "Error while parsing systems configuration file!"; + *errorString = "IT LOOKS LIKE YOUR SYSTEMS CONFIGURATION FILE HAS NOT BEEN SET UP OR IS INVALID. YOU'LL NEED TO DO THIS BY HAND, UNFORTUNATELY.\n\n" "VISIT EMULATIONSTATION.ORG FOR MORE INFORMATION."; + return false; } if(SystemData::sSystemVector.size() == 0) { LOG(LogError) << "No systems found! Does at least one system have a game present? (check that extensions match!)\n(Also, make sure you've updated your es_systems.cfg for XML!)"; + *errorString = "WE CAN'T FIND ANY SYSTEMS!\n" "CHECK THAT YOUR PATHS ARE CORRECT IN THE SYSTEMS CONFIGURATION FILE, " "AND YOUR GAME DIRECTORY HAS AT LEAST ONE GAME WITH THE CORRECT EXTENSION.\n\n" "VISIT EMULATIONSTATION.ORG FOR MORE INFORMATION."; + return false; } @@ -245,15 +389,38 @@ void onExit() Log::close(); } +void processAudioTitles(Window* window) +{ + if (Settings::getInstance()->getBool("MusicTitles")) + { + std::string songName = AudioManager::getInstance()->popSongName(); + if (!songName.empty()) + window->displayNotificationMessage(_U("\uF028 ") + songName); + } +} + +#include "ApiSystem.h" + int main(int argc, char* argv[]) { srand((unsigned int)time(NULL)); +#if WIN32 + std::locale::global(std::locale("en-US")); + setlocale(LC_NUMERIC, "C"); + setlocale(LC_TIME, "C"); + setlocale(LC_MONETARY, "C"); +#else std::locale::global(std::locale("C")); +#endif if(!parseArgs(argc, argv)) return 0; - +/* + ApiSystem::checkUpdateVersion(); + ApiSystem::updateSystem(nullptr); + return 0; + */ // only show the console on Windows if HideConsole is false #ifdef WIN32 // MSVC has a "SubSystem" option, with two primary options: "WINDOWS" and "CONSOLE". @@ -292,13 +459,19 @@ int main(int argc, char* argv[]) FreeImage_Initialise(); #endif + if (!gPlayVideo.empty()) + { + playVideo(); + return 0; + } + //if ~/.emulationstation doesn't exist and cannot be created, bail if(!verifyHomeFolderExists()) return 1; //start the logger - Log::init(); - Log::open(); + Log::setupReportingLevel(); + Log::init(); LOG(LogInfo) << "EmulationStation - v" << PROGRAM_VERSION_STRING << ", built " << PROGRAM_BUILT_STRING; //always close the log on exit @@ -312,42 +485,42 @@ int main(int argc, char* argv[]) MameNames::init(); window.pushGui(ViewController::get()); + TextureData::OPTIMIZEVRAM = Settings::getInstance()->getBool("OptimizeVRAM"); + GuiComponent::ALLOWANIMATIONS = Settings::getInstance()->getString("TransitionStyle") != "instant"; + bool splashScreen = Settings::getInstance()->getBool("SplashScreen"); bool splashScreenProgress = Settings::getInstance()->getBool("SplashScreenProgress"); - if(!scrape_cmdline) + if (!scrape_cmdline) { - if(!window.init()) + if(!window.init(true)) { LOG(LogError) << "Window failed to initialize!"; return 1; } - if(splashScreen) - { - std::string progressText = "Loading..."; - if (splashScreenProgress) - progressText = "Loading system config..."; - window.renderLoadingScreen(progressText); - } + if (splashScreen) + window.renderLoadingScreen(_("Loading...")); } const char* errorMsg = NULL; - if(!loadSystemConfigFile(&errorMsg)) + if(!loadSystemConfigFile(&window, &errorMsg)) { // something went terribly wrong - if(errorMsg == NULL) + if (errorMsg == NULL) { LOG(LogError) << "Unknown error occured while parsing system config file."; - if(!scrape_cmdline) + + if (!scrape_cmdline) Renderer::deinit(); + return 1; } // we can't handle es_systems.cfg file problems inside ES itself, so display the error message then quit window.pushGui(new GuiMsgBox(&window, errorMsg, - "QUIT", [] { + _("QUIT"), [] { SDL_Event* quit = new SDL_Event(); quit->type = SDL_QUIT; SDL_PushEvent(quit); @@ -355,54 +528,82 @@ int main(int argc, char* argv[]) } //run the command line scraper then quit - if(scrape_cmdline) - { + if (scrape_cmdline) return run_scraper_cmdline(); - } + +#if WIN32 + if (Settings::getInstance()->getBool("updates.enabled")) + NetworkThread* nthread = new NetworkThread(&window); +#endif //dont generate joystick events while we're loading (hopefully fixes "automatically started emulator" bug) SDL_JoystickEventState(SDL_DISABLE); // preload what we can right away instead of waiting for the user to select it // this makes for no delays when accessing content, but a longer startup time - ViewController::get()->preload(); - if(splashScreen && splashScreenProgress) - window.renderLoadingScreen("Done."); + if (Settings::getInstance()->getBool("PreloadUI")) + ViewController::get()->preload(); + + if (splashScreen && splashScreenProgress) + window.renderLoadingScreen(_("Starting UI")); //choose which GUI to open depending on if an input configuration already exists - if(errorMsg == NULL) + if (errorMsg == NULL) { - if(Utils::FileSystem::exists(InputManager::getConfigPath()) && InputManager::getInstance()->getNumConfiguredDevices() > 0) - { - ViewController::get()->goToStart(); - }else{ - window.pushGui(new GuiDetectDevice(&window, true, [] { ViewController::get()->goToStart(); })); - } + if (Utils::FileSystem::exists(InputManager::getConfigPath()) && InputManager::getInstance()->getNumConfiguredDevices() > 0) + ViewController::get()->goToStart(true); + else + window.pushGui(new GuiDetectDevice(&window, true, [] { ViewController::get()->goToStart(true); })); } //generate joystick events since we're done loading SDL_JoystickEventState(SDL_ENABLE); + window.endRenderLoadingScreen(); + + if (Settings::getInstance()->getBool("audio.bgmusic")) + AudioManager::getInstance()->playRandomMusic(); + +#ifdef WIN32 + DWORD displayFrequency = 60; + + DEVMODE lpDevMode; + memset(&lpDevMode, 0, sizeof(DEVMODE)); + lpDevMode.dmSize = sizeof(DEVMODE); + lpDevMode.dmFields = DM_BITSPERPEL | DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFLAGS | DM_DISPLAYFREQUENCY; + lpDevMode.dmDriverExtra = 0; + + if (EnumDisplaySettings(NULL, ENUM_CURRENT_SETTINGS, &lpDevMode) != 0) { + displayFrequency = lpDevMode.dmDisplayFrequency; // default value if cannot retrieve from user settings. + } + + int timeLimit = (1000 / displayFrequency) - 6; // Margin for vsync +#endif + int lastTime = SDL_GetTicks(); int ps_time = SDL_GetTicks(); + int exitMode = 0; bool running = true; while(running) { + int processStart = SDL_GetTicks(); + SDL_Event event; bool ps_standby = PowerSaver::getState() && (int) SDL_GetTicks() - ps_time > PowerSaver::getMode(); - if(ps_standby ? SDL_WaitEventTimeout(&event, PowerSaver::getTimeout()) : SDL_PollEvent(&event)) + if (ps_standby ? SDL_WaitEventTimeout(&event, PowerSaver::getTimeout()) : SDL_PollEvent(&event)) { do { InputManager::getInstance()->parseEvent(event, &window); - if(event.type == SDL_QUIT) + if (event.type == SDL_QUIT) running = false; - } while(SDL_PollEvent(&event)); + } + while(SDL_PollEvent(&event)); // triggered if exiting from SDL_WaitEvent due to event if (ps_standby) @@ -416,13 +617,13 @@ int main(int argc, char* argv[]) { // If exitting SDL_WaitEventTimeout due to timeout. Trail considering // timeout as an event - ps_time = SDL_GetTicks(); + ps_time = SDL_GetTicks(); } - if(window.isSleeping()) + if (window.isSleeping()) { lastTime = SDL_GetTicks(); - SDL_Delay(1); // this doesn't need to be accurate, we're just giving up our CPU time until something wakes us up + SDL_Delay(10); // this doesn't need to be accurate, we're just giving up our CPU time until something wakes us up continue; } @@ -431,20 +632,50 @@ int main(int argc, char* argv[]) lastTime = curTime; // cap deltaTime if it ever goes negative - if(deltaTime < 0) + if (deltaTime < 0) deltaTime = 1000; + processAudioTitles(&window); + window.update(deltaTime); window.render(); - Renderer::swapBuffers(); - + Log::flush(); + + int processDuration = SDL_GetTicks() - processStart; + +#ifdef WIN32 + if (processDuration < timeLimit) + { + int timeToWait = timeLimit - processDuration; + if (timeToWait > 0 && timeToWait < 100) + Sleep(timeToWait); + } + + int swapStart = SDL_GetTicks(); +#endif + + Renderer::swapBuffers(); +/* +#ifdef WIN32 + int swapDuration = SDL_GetTicks() - swapStart; + + char buffer[100]; + sprintf_s(buffer, "px=%d swap=%d, sleep=%d\n", processDuration, swapDuration, timeLimit - processDuration); + OutputDebugStringA(buffer); +#endif +*/ } + ThreadedScraper::stop(); + while(window.peekGui() != ViewController::get()) delete window.peekGui(); - window.deinit(); + if (SystemData::hasDirtySystems()) + window.renderLoadingScreen(_("SAVING DATA. PLEASE WAIT...")); + + ImageIO::saveImageCache(); MameNames::deinit(); CollectionSystemManager::deinit(); SystemData::deleteSystems(); @@ -454,6 +685,8 @@ int main(int argc, char* argv[]) FreeImage_DeInitialise(); #endif + window.deinit(true); + processQuitMode(); LOG(LogInfo) << "EmulationStation cleanly shutting down."; diff --git a/es-app/src/scrapers/GamesDBJSONScraper.cpp b/es-app/src/scrapers/GamesDBJSONScraper.cpp index ad4ddbf606..e44fbf4088 100644 --- a/es-app/src/scrapers/GamesDBJSONScraper.cpp +++ b/es-app/src/scrapers/GamesDBJSONScraper.cpp @@ -97,13 +97,30 @@ const std::map gamesdb_new_platformid_map{ { VECTREX, "4939" }, { TRS80_COLOR_COMPUTER, "4941" }, { TANDY, "4941" }, + { SUPERGRAFX, "34" }, // The code is TurboGrafx 16, but they manage SUPERGRAFX into this one.... + + { AMIGACD32, "4947" }, + // { AMIGACDTV, ?? }, + // { ATOMISWAVE, ?? }, + { CAVESTORY, "1" }, + // { GX4000, ?? }, + // { LUTRO, ?? }, + // { NAOMI, ?? }, + { NEOGEO_CD, "24" }, + { PCFX, "4930" }, + { POKEMINI, "4957" }, + { PRBOOM, "1" }, + { SATELLAVIEW, "6" }, + { SUFAMITURBO, "6" }, + // { ZX81, ?? }, + { MOONLIGHT, "1" }, // "PC" }; void thegamesdb_generate_json_scraper_requests(const ScraperSearchParams& params, std::queue>& requests, std::vector& results) { resources.prepare(); - std::string path = "https://api.thegamesdb.net"; + std::string path = "https://api.thegamesdb.net/v1"; bool usingGameID = false; const std::string apiKey = std::string("apikey=") + resources.getApiKey(); std::string cleanName = params.nameOverride; @@ -338,12 +355,13 @@ void processGame(const Value& game, const Value& boxart, std::vector& req, std::vector& results) + // Process should return false only when we reached a maximum scrap by minute, to retry +bool TheGamesDBJSONRequest::process(HttpReq* request, std::vector& results) { - assert(req->status() == HttpReq::REQ_SUCCESS); + assert(request->status() == HttpReq::REQ_SUCCESS); Document doc; - doc.Parse(req->getContent().c_str()); + doc.Parse(request->getContent().c_str()); if (doc.HasParseError()) { @@ -351,14 +369,14 @@ void TheGamesDBJSONRequest::process(const std::unique_ptr& req, std::ve std::string("TheGamesDBJSONRequest - Error parsing JSON. \n\t") + GetParseError_En(doc.GetParseError()); setError(err); LOG(LogError) << err; - return; + return true; } if (!doc.HasMember("data") || !doc["data"].HasMember("games") || !doc["data"]["games"].IsArray()) { std::string warn = "TheGamesDBJSONRequest - Response had no game data.\n"; LOG(LogWarning) << warn; - return; + return true; } const Value& games = doc["data"]["games"]; @@ -366,7 +384,7 @@ void TheGamesDBJSONRequest::process(const std::unique_ptr& req, std::ve { std::string warn = "TheGamesDBJSONRequest - Response had no include boxart data.\n"; LOG(LogWarning) << warn; - return; + return true; } const Value& boxart = doc["include"]["boxart"]; @@ -375,12 +393,11 @@ void TheGamesDBJSONRequest::process(const std::unique_ptr& req, std::ve { std::string warn = "TheGamesDBJSONRequest - Response include had no usable boxart data.\n"; LOG(LogWarning) << warn; - return; + return true; } resources.ensureResources(); - - + for (int i = 0; i < (int)games.Size(); ++i) { auto& v = games[i]; @@ -393,4 +410,6 @@ void TheGamesDBJSONRequest::process(const std::unique_ptr& req, std::ve LOG(LogError) << "Error while processing game: " << e.what(); } } + + return true; } diff --git a/es-app/src/scrapers/GamesDBJSONScraper.h b/es-app/src/scrapers/GamesDBJSONScraper.h index 505dbcf2be..672b7eddf0 100644 --- a/es-app/src/scrapers/GamesDBJSONScraper.h +++ b/es-app/src/scrapers/GamesDBJSONScraper.h @@ -28,7 +28,7 @@ class TheGamesDBJSONRequest : public ScraperHttpRequest } protected: - void process(const std::unique_ptr& req, std::vector& results) override; + bool process(HttpReq* request, std::vector& results) override; bool isGameRequest() { return !mRequestQueue; } std::queue>* mRequestQueue; diff --git a/es-app/src/scrapers/GamesDBJSONScraperResources.cpp b/es-app/src/scrapers/GamesDBJSONScraperResources.cpp index 7d59bef46b..3246079603 100644 --- a/es-app/src/scrapers/GamesDBJSONScraperResources.cpp +++ b/es-app/src/scrapers/GamesDBJSONScraperResources.cpp @@ -17,7 +17,7 @@ using namespace rapidjson; namespace { -constexpr char GamesDBAPIKey[] = "445fcbc3f32bb2474bc27016b99eb963d318ee3a608212c543b9a79de1041600"; + constexpr char GamesDBAPIKey[] = "445fcbc3f32bb2474bc27016b99eb963d318ee3a608212c543b9a79de1041600"; constexpr int MAX_WAIT_MS = 90000; @@ -153,7 +153,7 @@ bool TheGamesDBJSONRequestResources::saveResource(HttpReq* req, std::unordered_m std::unique_ptr TheGamesDBJSONRequestResources::fetchResource(const std::string& endpoint) { - std::string path = "https://api.thegamesdb.net"; + std::string path = "https://api.thegamesdb.net/v1"; path += endpoint; path += "?apikey=" + getApiKey(); diff --git a/es-app/src/scrapers/Scraper.cpp b/es-app/src/scrapers/Scraper.cpp index bc3e6b00dd..c71924ab3a 100644 --- a/es-app/src/scrapers/Scraper.cpp +++ b/es-app/src/scrapers/Scraper.cpp @@ -8,26 +8,28 @@ #include "SystemData.h" #include #include +#include "utils/FileSystemUtil.h" +#include "utils/StringUtil.h" +#include +// batocera const std::map scraper_request_funcs { - { "TheGamesDB", &thegamesdb_generate_json_scraper_requests }, - { "ScreenScraper", &screenscraper_generate_scraper_requests } + { "ScreenScraper", &screenscraper_generate_scraper_requests }, + { "TheGamesDB", &thegamesdb_generate_json_scraper_requests } }; std::unique_ptr startScraperSearch(const ScraperSearchParams& params) { const std::string& name = Settings::getInstance()->getString("Scraper"); + std::unique_ptr handle(new ScraperSearchHandle()); // Check if the Scraper in the settings still exists as a registered scraping source. - if (scraper_request_funcs.find(name) == scraper_request_funcs.end()) - { - LOG(LogWarning) << "Configured scraper (" << name << ") unavailable, scraping aborted."; - } + auto it = scraper_request_funcs.find(name); + if (it != scraper_request_funcs.end()) + it->second(params, handle->mRequestQueue, handle->mResults); else - { - scraper_request_funcs.at(name)(params, handle->mRequestQueue, handle->mResults); - } + LOG(LogWarning) << "Configured scraper (" << name << ") unavailable, scraping aborted."; return handle; } @@ -70,7 +72,7 @@ void ScraperSearchHandle::update() if(status == ASYNC_ERROR) { // propegate error - setError(req.getStatusString()); + setError(req.getErrorCode(), req.getStatusString()); // empty our queue while(!mRequestQueue.empty()) @@ -89,7 +91,7 @@ void ScraperSearchHandle::update() } // we finished without any errors! - if(mRequestQueue.empty()) + if(mRequestQueue.empty() && mStatus != ASYNC_ERROR) { setStatus(ASYNC_DONE); return; @@ -105,30 +107,75 @@ ScraperRequest::ScraperRequest(std::vector& resultsWrite) : // ScraperHttpRequest -ScraperHttpRequest::ScraperHttpRequest(std::vector& resultsWrite, const std::string& url) +ScraperHttpRequest::ScraperHttpRequest(std::vector& resultsWrite, const std::string& url) : ScraperRequest(resultsWrite) { setStatus(ASYNC_IN_PROGRESS); - mReq = std::unique_ptr(new HttpReq(url)); + mRequest = new HttpReq(url); + mRetryCount = 0; +} + +ScraperHttpRequest::~ScraperHttpRequest() +{ + delete mRequest; } void ScraperHttpRequest::update() { - HttpReq::Status status = mReq->status(); + HttpReq::Status status = mRequest->status(); + + // not ready yet + if (status == HttpReq::REQ_IN_PROGRESS) + return; + if(status == HttpReq::REQ_SUCCESS) { setStatus(ASYNC_DONE); // if process() has an error, status will be changed to ASYNC_ERROR - process(mReq, mResults); + process(mRequest, mResults); return; } - // not ready yet - if(status == HttpReq::REQ_IN_PROGRESS) + if (status == HttpReq::REQ_429_TOOMANYREQUESTS) + { + mRetryCount++; + if (mRetryCount > 4) + { + setStatus(ASYNC_DONE); // Ignore error + return; + } + + setStatus(ASYNC_IN_PROGRESS); + + LOG(LogDebug) << "REQ_429_TOOMANYREQUESTS : Wait before Retrying"; + + std::string url = mRequest->getUrl(); + std::this_thread::sleep_for(std::chrono::seconds(mRetryCount < 3 ? 5 : 10)); + + delete mRequest; + mRequest = new HttpReq(url); + + LOG(LogDebug) << "REQ_429_TOOMANYREQUESTS : Retrying"; + return; + } + // Ignored errors + if (status == HttpReq::REQ_404_NOTFOUND || status == HttpReq::REQ_IO_ERROR) + { + setStatus(ASYNC_DONE); + return; + } + + // Blocking errors + if (status != HttpReq::REQ_SUCCESS) + { + setError(status, mRequest->getErrorMsg()); + return; + } + // everything else is some sort of error - LOG(LogError) << "ScraperHttpRequest network error (status: " << status << ") - " << mReq->getErrorMsg(); - setError(mReq->getErrorMsg()); + LOG(LogError) << "ScraperHttpRequest network error (status: " << status << ") - " << mRequest->getErrorMsg(); + setError(mRequest->getErrorMsg()); } @@ -141,30 +188,145 @@ std::unique_ptr resolveMetaDataAssets(const ScraperSearchResult MDResolveHandle::MDResolveHandle(const ScraperSearchResult& result, const ScraperSearchParams& search) : mResult(result) { - if(!result.imageUrl.empty()) + mPercent = -1; + + std::string ext; + + // If we have a file extension returned by the scraper, then use it. + // Otherwise, try to guess it by the name of the URL, which point to an image. + if (!result.imageType.empty()) + { + ext = result.imageType; + } + else { + size_t dot = result.imageUrl.find_last_of('.'); - std::string ext; + if (dot != std::string::npos) + ext = result.imageUrl.substr(dot, std::string::npos); + } - // If we have a file extension returned by the scraper, then use it. - // Otherwise, try to guess it by the name of the URL, which point to an image. - if (!result.imageType.empty()) - { - ext = result.imageType; - }else{ - size_t dot = result.imageUrl.find_last_of('.'); + bool ss = Settings::getInstance()->getString("Scraper") == "ScreenScraper"; - if (dot != std::string::npos) - ext = result.imageUrl.substr(dot, std::string::npos); - } + auto tmp = Settings::getInstance()->getString("ScrapperImageSrc"); + auto md = search.game->getMetadata().get("image"); + if (!search.overWriteMedias && ss && !Settings::getInstance()->getString("ScrapperImageSrc").empty() && Utils::FileSystem::exists(search.game->getMetadata().get("image"))) + mResult.mdl.set("image", search.game->getMetadata().get("image")); + else if (!result.imageUrl.empty()) + { std::string imgPath = getSaveAsPath(search, "image", ext); - mFuncs.push_back(ResolvePair(downloadImageAsync(result.imageUrl, imgPath), [this, imgPath] + if (!search.overWriteMedias && Utils::FileSystem::exists(imgPath)) { mResult.mdl.set("image", imgPath); + + if (mResult.thumbnailUrl.find(mResult.imageUrl) == 0) + mResult.thumbnailUrl = ""; + mResult.imageUrl = ""; - })); + } + else + + mFuncs.push_back(new ResolvePair( + [this, result, imgPath] + { + return downloadImageAsync(result.imageUrl, imgPath); + }, + [this, imgPath] + { + mResult.mdl.set("image", imgPath); + + if (mResult.thumbnailUrl.find(mResult.imageUrl) == 0) + mResult.thumbnailUrl = ""; + + mResult.imageUrl = ""; + }, "image", result.mdl.getName())); + } + + if (!search.overWriteMedias && ss && !Settings::getInstance()->getString("ScrapperThumbSrc").empty() && Utils::FileSystem::exists(search.game->getMetadata().get("thumbnail"))) + mResult.mdl.set("thumbnail", search.game->getMetadata().get("thumbnail")); + else if (!result.thumbnailUrl.empty() && (result.imageUrl.empty() || result.thumbnailUrl.find(result.imageUrl) != 0)) + { + std::string thumbPath = getSaveAsPath(search, "thumb", ext); + + if (!search.overWriteMedias && Utils::FileSystem::exists(thumbPath)) + { + mResult.mdl.set("thumbnail", thumbPath); + mResult.thumbnailUrl = ""; + } + else + + mFuncs.push_back(new ResolvePair( + [this, result, thumbPath] + { + return downloadImageAsync(result.thumbnailUrl, thumbPath); + }, + [this, thumbPath] + { + mResult.mdl.set("thumbnail", thumbPath); + mResult.thumbnailUrl = ""; + }, "thumbnail", result.mdl.getName())); + } + + if (!search.overWriteMedias && ss && !Settings::getInstance()->getString("ScrapperLogoSrc").empty() && Utils::FileSystem::exists(search.game->getMetadata().get("marquee"))) + mResult.mdl.set("marquee", search.game->getMetadata().get("marquee")); + else if (!result.marqueeUrl.empty()) + { + std::string marqueePath = getSaveAsPath(search, "marquee", ext); + + if (!search.overWriteMedias && Utils::FileSystem::exists(marqueePath)) + { + mResult.mdl.set("marquee", marqueePath); + mResult.marqueeUrl = ""; + } + else + + mFuncs.push_back(new ResolvePair( + [this, result, marqueePath] + { + return downloadImageAsync(result.marqueeUrl, marqueePath); + }, + [this, marqueePath] + { + mResult.mdl.set("marquee", marqueePath); + mResult.marqueeUrl = ""; + }, "marquee", result.mdl.getName())); + } + + if (!search.overWriteMedias && Settings::getInstance()->getBool("ScrapeVideos") && Utils::FileSystem::exists(search.game->getMetadata().get("video"))) + mResult.mdl.set("video", search.game->getMetadata().get("video")); + else if (!result.videoUrl.empty()) + { + std::string videoPath = getSaveAsPath(search, "video", ".mp4"); + + if (!search.overWriteMedias && Utils::FileSystem::exists(videoPath)) + { + mResult.mdl.set("video", videoPath); + mResult.videoUrl = ""; + } + else + + mFuncs.push_back(new ResolvePair( + [this, result, videoPath] + { + return downloadImageAsync(result.videoUrl, videoPath); + }, + [this, videoPath] + { + mResult.mdl.set("video", videoPath); + mResult.videoUrl = ""; + }, "video", result.mdl.getName())); + } + + auto it = mFuncs.cbegin(); + if (it == mFuncs.cend()) + setStatus(ASYNC_DONE); + else + { + mSource = (*it)->source; + mCurrentItem = (*it)->name; + (*it)->Run(); } } @@ -172,73 +334,125 @@ void MDResolveHandle::update() { if(mStatus == ASYNC_DONE || mStatus == ASYNC_ERROR) return; - + auto it = mFuncs.cbegin(); - while(it != mFuncs.cend()) + if (it == mFuncs.cend()) { - if(it->first->status() == ASYNC_ERROR) - { - setError(it->first->getStatusString()); - return; - }else if(it->first->status() == ASYNC_DONE) + setStatus(ASYNC_DONE); + return; + } + + ResolvePair* pPair = (*it); + + if (pPair->handle->status() == ASYNC_IN_PROGRESS) + mPercent = pPair->handle->getPercent(); + + if (pPair->handle->status() == ASYNC_ERROR) + { + setError(pPair->handle->getErrorCode(), pPair->handle->getStatusString()); + for (auto fc : mFuncs) + delete fc; + + return; + } + else if (pPair->handle->status() == ASYNC_DONE) + { + pPair->onFinished(); + mFuncs.erase(it); + delete pPair; + + auto next = mFuncs.cbegin(); + if (next != mFuncs.cend()) { - it->second(); - it = mFuncs.erase(it); - continue; + mSource = (*next)->source; + mCurrentItem = (*next)->name; + (*next)->Run(); } - it++; } - + if(mFuncs.empty()) setStatus(ASYNC_DONE); } std::unique_ptr downloadImageAsync(const std::string& url, const std::string& saveAs) { - return std::unique_ptr(new ImageDownloadHandle(url, saveAs, + return std::unique_ptr(new ImageDownloadHandle(url, saveAs, Settings::getInstance()->getInt("ScraperResizeWidth"), Settings::getInstance()->getInt("ScraperResizeHeight"))); } -ImageDownloadHandle::ImageDownloadHandle(const std::string& url, const std::string& path, int maxWidth, int maxHeight) : - mSavePath(path), mMaxWidth(maxWidth), mMaxHeight(maxHeight), mReq(new HttpReq(url)) +ImageDownloadHandle::ImageDownloadHandle(const std::string& url, const std::string& path, int maxWidth, int maxHeight) : + mSavePath(path), mMaxWidth(maxWidth), mMaxHeight(maxHeight) +{ + mRequest = new HttpReq(url, path); +} + +ImageDownloadHandle::~ImageDownloadHandle() { + delete mRequest; +} + +int ImageDownloadHandle::getPercent() +{ + if (mRequest->status() == HttpReq::REQ_IN_PROGRESS) + return mRequest->getPercent(); + + return -1; } void ImageDownloadHandle::update() { - if(mReq->status() == HttpReq::REQ_IN_PROGRESS) - return; + HttpReq::Status status = mRequest->status(); - if(mReq->status() != HttpReq::REQ_SUCCESS) + if (status == HttpReq::REQ_IN_PROGRESS) + return; + + if (status == HttpReq::REQ_429_TOOMANYREQUESTS) { - std::stringstream ss; - ss << "Network error: " << mReq->getErrorMsg(); - setError(ss.str()); + mRetryCount++; + if (mRetryCount > 4) + { + setStatus(ASYNC_DONE); // Ignore error + return; + } + + setStatus(ASYNC_IN_PROGRESS); + + LOG(LogDebug) << "REQ_429_TOOMANYREQUESTS : Wait before Retrying"; + + std::string url = mRequest->getUrl(); + std::this_thread::sleep_for(std::chrono::seconds(mRetryCount < 3 ? 5 : 10)); + + delete mRequest; + mRequest = new HttpReq(url, mSavePath); + + LOG(LogDebug) << "REQ_429_TOOMANYREQUESTS : Retrying"; + return; } - // download is done, save it to disk - std::ofstream stream(mSavePath, std::ios_base::out | std::ios_base::binary); - if(stream.bad()) + // Ignored errors + if (status == HttpReq::REQ_404_NOTFOUND || status == HttpReq::REQ_IO_ERROR) { - setError("Failed to open image path to write. Permission error? Disk full?"); + setStatus(ASYNC_DONE); return; } - const std::string& content = mReq->getContent(); - stream.write(content.data(), content.length()); - stream.close(); - if(stream.bad()) + // Blocking errors + if (status != HttpReq::REQ_SUCCESS) { - setError("Failed to save image. Disk full?"); + setError(status, mRequest->getErrorMsg()); return; } - // resize it - if(!resizeImage(mSavePath, mMaxWidth, mMaxHeight)) + if (status == HttpReq::REQ_SUCCESS && mStatus == ASYNC_IN_PROGRESS) { - setError("Error saving resized image. Out of memory? Disk full?"); - return; + // It's an image ? + std::string ext = Utils::String::toLower(Utils::FileSystem::getExtension(mSavePath)); + if (ext == ".jpg" || ext == ".jpeg" || ext == ".png" || ext == ".bmp" || ext == ".gif") + { + try { resizeImage(mSavePath, mMaxWidth, mMaxHeight); } + catch(...) { } + } } setStatus(ASYNC_DONE); @@ -253,7 +467,7 @@ bool resizeImage(const std::string& path, int maxWidth, int maxHeight) FREE_IMAGE_FORMAT format = FIF_UNKNOWN; FIBITMAP* image = NULL; - + //detect the filetype format = FreeImage_GetFileType(path.c_str(), 0); if(format == FIF_UNKNOWN) @@ -276,14 +490,23 @@ bool resizeImage(const std::string& path, int maxWidth, int maxHeight) float width = (float)FreeImage_GetWidth(image); float height = (float)FreeImage_GetHeight(image); - if(maxWidth == 0) + if (width == 0 || height == 0) { + FreeImage_Unload(image); + return true; + } + + if(maxWidth == 0) maxWidth = (int)((maxHeight / height) * width); - }else if(maxHeight == 0) - { + else if(maxHeight == 0) maxHeight = (int)((maxWidth / width) * height); + + if (width <= maxWidth && height <= maxHeight) + { + FreeImage_Unload(image); + return true; } - + FIBITMAP* imageRescaled = FreeImage_Rescale(image, maxWidth, maxHeight, FILTER_BILINEAR); FreeImage_Unload(image); @@ -293,7 +516,14 @@ bool resizeImage(const std::string& path, int maxWidth, int maxHeight) return false; } - bool saved = (FreeImage_Save(format, imageRescaled, path.c_str()) != 0); + bool saved = false; + + try + { + saved = (FreeImage_Save(format, imageRescaled, path.c_str()) != 0); + } + catch(...) { } + FreeImage_Unload(imageRescaled); if(!saved) @@ -307,16 +537,20 @@ std::string getSaveAsPath(const ScraperSearchParams& params, const std::string& const std::string subdirectory = params.system->getName(); const std::string name = Utils::FileSystem::getStem(params.game->getPath()) + "-" + suffix; - std::string path = Utils::FileSystem::getHomePath() + "/.emulationstation/downloaded_images/"; - - if(!Utils::FileSystem::exists(path)) - Utils::FileSystem::createDirectory(path); + std::string subFolder = "images"; + if (suffix == "video") + subFolder = "videos"; - path += subdirectory + "/"; + std::string path = params.system->getRootFolder()->getPath() + "/" + subFolder + "/"; // batocera if(!Utils::FileSystem::exists(path)) Utils::FileSystem::createDirectory(path); + // batocera + //path += subdirectory + "/"; + // + //if(!Utils::FileSystem::exists(path)) + // Utils::FileSystem::createDirectory(path); path += name + extension; return path; diff --git a/es-app/src/scrapers/Scraper.h b/es-app/src/scrapers/Scraper.h index b86fc90803..671d9823e6 100644 --- a/es-app/src/scrapers/Scraper.h +++ b/es-app/src/scrapers/Scraper.h @@ -18,9 +18,12 @@ class SystemData; struct ScraperSearchParams { + ScraperSearchParams() { overWriteMedias = true; } + SystemData* system; FileData* game; + bool overWriteMedias; std::string nameOverride; }; @@ -31,6 +34,13 @@ struct ScraperSearchResult MetaDataList mdl; std::string imageUrl; std::string thumbnailUrl; + std::string videoUrl; + std::string marqueeUrl; + + bool hadMedia() + { + return !imageUrl.empty() || !thumbnailUrl.empty() || !videoUrl.empty() || !marqueeUrl.empty(); + } // Needed to pre-set the image type std::string imageType; @@ -53,8 +63,8 @@ struct ScraperSearchResult // We could do this if we used threads. Right now ES doesn't because I'm pretty sure I'll fuck it up, // and I'm not sure of the performance of threads on the Pi (single-core ARM). -// We could also do this if we used coroutines. -// I can't find a really good cross-platform coroutine library (x86/64/ARM Linux + Windows), +// We could also do this if we used coroutines. +// I can't find a really good cross-platform coroutine library (x86/64/ARM Linux + Windows), // and I don't want to spend more time chasing libraries than just writing it the long way once. // So, I did it the "long" way. @@ -64,6 +74,7 @@ struct ScraperSearchResult // a scraper search gathers results from (potentially multiple) ScraperRequests + class ScraperRequest : public AsyncHandle { public: @@ -71,24 +82,26 @@ class ScraperRequest : public AsyncHandle // returns "true" once we're done virtual void update() = 0; - + protected: std::vector& mResults; }; - // a single HTTP request that needs to be processed to get the results class ScraperHttpRequest : public ScraperRequest { public: ScraperHttpRequest(std::vector& resultsWrite, const std::string& url); + ~ScraperHttpRequest(); + virtual void update() override; protected: - virtual void process(const std::unique_ptr& req, std::vector& results) = 0; + virtual bool process(HttpReq* request, std::vector& results) = 0; private: - std::unique_ptr mReq; + HttpReq* mRequest; + int mRetryCount; }; // a request to get a list of results @@ -129,24 +142,69 @@ class MDResolveHandle : public AsyncHandle MDResolveHandle(const ScraperSearchResult& result, const ScraperSearchParams& search); void update() override; - inline const ScraperSearchResult& getResult() const { assert(mStatus == ASYNC_DONE); return mResult; } + inline const ScraperSearchResult& getResult() const { return mResult; } // assert(mStatus == ASYNC_DONE); -> FCA : Why ??? + + std::string getCurrentItem() { + return mCurrentItem; + } + + std::string getCurrentSource() { + return mSource; + } + + int getPercent() { + return mPercent; + } private: ScraperSearchResult mResult; - typedef std::pair< std::unique_ptr, std::function > ResolvePair; - std::vector mFuncs; + class ResolvePair + { + public: + ResolvePair(std::function()> _invoker, std::function _function, std::string _name, std::string _source) + { + func = _invoker; + onFinished = _function; + name = _name; + source = _source; + } + + void Run() + { + handle = func(); + } + + std::function onFinished; + std::string name; + std::string source; + + std::unique_ptr handle; + + private: + std::function()> func; + }; + + std::vector mFuncs; + std::string mCurrentItem; + std::string mSource; + int mPercent; }; class ImageDownloadHandle : public AsyncHandle { public: ImageDownloadHandle(const std::string& url, const std::string& path, int maxWidth, int maxHeight); + ~ImageDownloadHandle(); void update() override; + virtual int getPercent(); + private: - std::unique_ptr mReq; + HttpReq* mRequest; + int mRetryCount; + std::string mSavePath; int mMaxWidth; int mMaxHeight; diff --git a/es-app/src/scrapers/ScreenScraper.cpp b/es-app/src/scrapers/ScreenScraper.cpp index 7be4ebbe47..64981a99bd 100644 --- a/es-app/src/scrapers/ScreenScraper.cpp +++ b/es-app/src/scrapers/ScreenScraper.cpp @@ -9,9 +9,20 @@ #include "SystemData.h" #include #include +#include "EsLocale.h" +#include "md5.h" +#include using namespace PlatformIds; +std::string ScreenScraperRequest::ensureUrl(const std::string url) +{ + return Utils::String::replace( + Utils::String::replace(url, " ", "%20") , + "#screenscraperserveur#", "https://www.screenscraper.fr/"); +} + + /** List of systems and their IDs from https://www.screenscraper.fr/api/systemesListe.php?devid=xxx&devpassword=yyy&softname=zzz&output=XML @@ -22,7 +33,7 @@ const std::map screenscraper_platformid_map{ { AMSTRAD_CPC, 65 }, { APPLE_II, 86 }, { ARCADE, 75 }, - { ATARI_800, 26 }, // Use ATARI_2600 as an alias for atari 800 + { ATARI_800, 43 }, { ATARI_2600, 26 }, { ATARI_5200, 40 }, { ATARI_7800, 41 }, @@ -81,9 +92,73 @@ const std::map screenscraper_platformid_map{ { VIDEOPAC_ODYSSEY2, 104 }, { VECTREX, 102 }, { TRS80_COLOR_COMPUTER, 144 }, - { TANDY, 144 } + { TANDY, 144 }, + { SUPERGRAFX, 105 }, + + { AMIGACD32, 130 }, + { AMIGACDTV, 129 }, + { ATOMISWAVE, 53 }, + { CAVESTORY, 135 }, + { GX4000, 87 }, + { LUTRO, 206 }, + { NAOMI, 56 }, + { NEOGEO_CD, 142 }, + { PCFX, 72 }, + { POKEMINI, 211 }, + { PRBOOM, 135 }, + { SATELLAVIEW, 107 }, + { SUFAMITURBO, 108 }, + { ZX81, 77 }, + { MOONLIGHT, 138 }, // "PC Windows" + + // Windows + { VISUALPINBALL, 198 }, + { FUTUREPINBALL, 199 }, + + // Misc + { ORICATMOS, 131 }, + { CHANNELF, 80 }, + { THOMSON_TO_MO, 141 }, + { SAMCOUPE, 213 }, + { OPENBOR, 214 }, + { UZEBOX, 216 }, + { APPLE2GS, 217 }, + { SPECTRAVIDEO, 218 }, + { PALMOS, 219 }, + { DAPHNEE, 49 }, + { SOLARUS, 223 } + }; +const std::map screenscraper_arcadesystemid_map{ + { 6, "cps1" }, + { 7, "cps2" }, + { 8, "cps3" }, + { 47, "cave" }, + { 68, "neogeo" }, + { 142, "neogeo" }, + { 147, "sega" }, + { 148, "irem" }, + { 150, "midway" }, + { 151, "capcom" }, + { 153, "tecmo" }, + { 154, "snk" }, + { 155, "namco" }, + { 156, "namco" }, + { 157, "taito" }, + { 158, "konami" }, + { 159, "jaleco" }, + { 160, "atari" }, + { 161, "nintendo" }, + { 162, "dataeast" }, + { 164, "sammy" }, + { 166, "acclaim" }, + { 167, "psikyo" }, + { 174, "kaneko" }, + { 183, "coleco" }, + { 185, "atlus" }, + { 186, "banpresto" } +}; // Helper XML parsing method, finding a node-by-name recursively. pugi::xml_node find_node_by_name_re(const pugi::xml_node& node, const std::vector node_names) { @@ -125,7 +200,53 @@ void screenscraper_generate_scraper_requests(const ScraperSearchParams& params, ScreenScraperRequest::ScreenScraperConfig ssConfig; - path = ssConfig.getGameSearchUrl(params.game->getFileName()); + // FCA Fix for names override not working on Retropie + if (params.nameOverride.length() == 0) + { + path = ssConfig.getGameSearchUrl(params.game->getFileName()); + path = Utils::String::replace(path, "%20-%20", "%20"); + path += "&romtype=rom"; + + // Use md5 to search scrapped game + int length = Utils::FileSystem::getFileSize(params.game->getFullPath()); + if (length <= 131072 * 1024) // 128 Mb max + { + try + { + // 64 Kb blocks + #define MD5BUFFERSIZE 64 * 1024 + + char* buffer = new char[MD5BUFFERSIZE]; + if (buffer) + { + size_t size; + + FILE* file = fopen(params.game->getFullPath().c_str(), "rb"); + if (file) + { + MD5 md5 = MD5(); + + while (size = fread(buffer, 1, MD5BUFFERSIZE, file)) + md5.update(buffer, size); + + md5.finalize(); + + std::string val = md5.hexdigest(); + if (!val.empty()) + path += "&md5=" + val; + + fclose(file); + } + + delete buffer; + } + } + catch (std::bad_alloc& ex) {} + } + } + else + path = ssConfig.getGameSearchUrl(params.nameOverride, true); + auto& platforms = params.system->getPlatformIds(); std::vector p_ids; @@ -155,47 +276,134 @@ void screenscraper_generate_scraper_requests(const ScraperSearchParams& params, path += HttpReq::urlEncode(std::to_string(*platform)); requests.push(std::unique_ptr(new ScreenScraperRequest(requests, results, path))); } - } -void ScreenScraperRequest::process(const std::unique_ptr& req, std::vector& results) +// Process should return false only when we reached a maximum scrap by minute, to retry +bool ScreenScraperRequest::process(HttpReq* request, std::vector& results) { - assert(req->status() == HttpReq::REQ_SUCCESS); + assert(request->status() == HttpReq::REQ_SUCCESS); + + auto content = request->getContent(); pugi::xml_document doc; - pugi::xml_parse_result parseResult = doc.load(req->getContent().c_str()); + pugi::xml_parse_result parseResult = doc.load(content.c_str()); if (!parseResult) { std::stringstream ss; ss << "ScreenScraperRequest - Error parsing XML." << std::endl << parseResult.description() << ""; - std::string err = ss.str(); - setError(err); - LOG(LogError) << err; - - return; + //setError(err); Don't consider it an error -> Request is a success. Simply : Game is not found + LOG(LogWarning) << err; + + if (Utils::String::toLower(content).find("maximum threads per minute reached") != std::string::npos) + return false; + + return true; } processGame(doc, results); + return true; +} + +pugi::xml_node ScreenScraperRequest::findMedia(pugi::xml_node media_list, std::vector mediaNames, std::string region) +{ + for (std::string media : mediaNames) + { + pugi::xml_node art = findMedia(media_list, media, region); + if (art) + return art; + } + return pugi::xml_node(NULL); } +pugi::xml_node ScreenScraperRequest::findMedia(pugi::xml_node media_list, std::string mediaName, std::string region) +{ + pugi::xml_node art = pugi::xml_node(NULL); + + // Do an XPath query for media[type='$media_type'], then filter by region + // We need to do this because any child of 'medias' has the form + // + // and we need to find the right media for the region. + + pugi::xpath_node_set results = media_list.select_nodes((static_cast("media[@type='") + mediaName + "']").c_str()); + + if (!results.size()) + return art; + + // Region fallback: WOR(LD), US, CUS(TOM?), JP, EU + for (auto _region : std::vector{ region, "wor", "us", "cus", "jp", "eu", "" }) + { + if (art) + break; + + for (auto node : results) + { + if (node.node().attribute("region").value() == _region) + { + art = node.node(); + break; + } + } + } + + return art; +} + +std::vector ScreenScraperRequest::getRipList(std::string imageSource) +{ + std::vector ripList; + + if (imageSource == "ss") + ripList = { "ss", "sstitle", "mixrbv1", "mixrbv2", "box-2D", "box-3D" }; + else if (imageSource == "sstitle") + ripList = { "sstitle", "ss", "mixrbv1", "mixrbv2", "box-2D", "box-3D" }; + else if (imageSource == "mixrbv1" || imageSource == "mixrbv2" || imageSource == "mixrbv") + ripList = { "mixrbv1", "mixrbv2", "ss", "box-3D", "box-2D" }; + else if (imageSource == "box-2D") + ripList = { "box-2D", "box-3D" }; + else if (imageSource == "box-3D") + ripList = { "box-3D", "box-2D" }; + else if (imageSource == "wheel") + ripList = { "wheel", "wheel-hd", "wheel-steel", "wheel-carbon", "screenmarqueesmall", "screenmarquee" }; + else if (imageSource == "marquee") + ripList = { "screenmarqueesmall", "screenmarquee", "wheel", "wheel-hd", "wheel-steel", "wheel-carbon" }; + else if (imageSource == "video") + ripList = { "video-normalized", "video" }; + + return ripList; +} void ScreenScraperRequest::processGame(const pugi::xml_document& xmldoc, std::vector& out_results) { + LOG(LogDebug) << "ScreenScraperRequest::processGame >>"; + pugi::xml_node data = xmldoc.child("Data"); - pugi::xml_node game = data.child("jeu"); + if (data.child("jeux")) + data = data.child("jeux"); - if (game) + for (pugi::xml_node game = data.child("jeu"); game; game = game.next_sibling("jeu")) { ScraperSearchResult result; ScreenScraperRequest::ScreenScraperConfig ssConfig; - std::string region = Utils::String::toLower(ssConfig.region).c_str(); - std::string language = Utils::String::toLower(ssConfig.language).c_str(); + std::string region = Utils::String::toLower(ssConfig.region); - // Name fallback: US, WOR(LD). ( Xpath: Data/jeu[0]/noms/nom[*] ). + std::string language = Utils::String::toLower(EsLocale::getLanguage()); + if (language.empty()) + language = "en"; + else + { + auto shortNameDivider = language.find("_"); + if (shortNameDivider != std::string::npos) + { + region = Utils::String::toLower(language.substr(shortNameDivider + 1)); + language = Utils::String::toLower(language.substr(0, shortNameDivider)); + } + } + + // Name fallback: US, WOR(LD). ( Xpath: Data/jeu[0]/noms/nom[*] ). result.mdl.set("name", find_child_by_attribute_list(game.child("noms"), "nom", "region", { region, "wor", "us" , "ss", "eu", "jp" }).text().get()); // Description fallback language: EN, WOR(LD) @@ -207,12 +415,12 @@ void ScreenScraperRequest::processGame(const pugi::xml_document& xmldoc, std::ve // Genre fallback language: EN. ( Xpath: Data/jeu[0]/genres/genre[*] ) result.mdl.set("genre", find_child_by_attribute_list(game.child("genres"), "genre", "langue", { language, "en" }).text().get()); - LOG(LogDebug) << "Genre: " << result.mdl.get("genre"); + //LOG(LogDebug) << "Genre: " << result.mdl.get("genre"); // Get the date proper. The API returns multiple 'date' children nodes to the 'dates' main child of 'jeu'. // Date fallback: WOR(LD), US, SS, JP, EU std::string _date = find_child_by_attribute_list(game.child("dates"), "date", "region", { region, "wor", "us", "ss", "jp", "eu" }).text().get(); - LOG(LogDebug) << "Release Date (unparsed): " << _date; + //LOG(LogDebug) << "Release Date (unparsed): " << _date; // Date can be YYYY-MM-DD or just YYYY. if (_date.length() > 4) @@ -223,7 +431,7 @@ void ScreenScraperRequest::processGame(const pugi::xml_document& xmldoc, std::ve result.mdl.set("releasedate", Utils::Time::DateTime(Utils::Time::stringToTime(_date, "%Y"))); } - LOG(LogDebug) << "Release Date (parsed): " << result.mdl.get("releasedate"); + //LOG(LogDebug) << "Release Date (parsed): " << result.mdl.get("releasedate"); /// Developer for the game( Xpath: Data/jeu[0]/developpeur ) std::string developer = game.child("developpeur").text().get(); @@ -238,7 +446,20 @@ void ScreenScraperRequest::processGame(const pugi::xml_document& xmldoc, std::ve // Players result.mdl.set("players", game.child("joueurs").text().get()); - // TODO: Validate rating + if(game.child("systeme").attribute("id")) + { + int systemId = game.child("systeme").attribute("id").as_int(); + + if(screenscraper_arcadesystemid_map.find(systemId) != screenscraper_arcadesystemid_map.cend()) + { + std::string systemName = screenscraper_arcadesystemid_map.at(game.child("systeme").attribute("id").as_int(0)); + result.mdl.set("arcadesystemname", systemName); + } + //else + // LOG(LogDebug) << "System " << systemId << " not found"; + } + + // TODO: Validate rating if (Settings::getInstance()->getBool("ScrapeRatings") && game.child("note")) { float ratingVal = (game.child("note").text().as_int() / 20.0f); @@ -252,54 +473,76 @@ void ScreenScraperRequest::processGame(const pugi::xml_document& xmldoc, std::ve if (media_list) { - pugi::xml_node art = pugi::xml_node(NULL); + std::vector ripList = getRipList(Settings::getInstance()->getString("ScrapperImageSrc")); + if (!ripList.empty()) + { + pugi::xml_node art = findMedia(media_list, ripList, region); + if (art) + { + // Sending a 'softname' containing space will make the image URLs returned by the API also contain the space. + // Escape any spaces in the URL here + result.imageUrl = ensureUrl(art.text().get()); - // Do an XPath query for media[type='$media_type'], then filter by region - // We need to do this because any child of 'medias' has the form - // - // and we need to find the right media for the region. - pugi::xpath_node_set results = media_list.select_nodes((static_cast("media[@type='") + ssConfig.media_name + "']").c_str()); + // Get the media type returned by ScreenScraper + std::string media_type = art.attribute("format").value(); + if (!media_type.empty()) + result.imageType = "." + media_type; - if (results.size()) + // Ask for the same image, but with a smaller size, for the thumbnail displayed during scraping + result.thumbnailUrl = result.imageUrl + "&maxheight=250"; + } + else + LOG(LogDebug) << "Failed to find media XML node for image"; + } + + if (!Settings::getInstance()->getString("ScrapperThumbSrc").empty() && + Settings::getInstance()->getString("ScrapperThumbSrc") != Settings::getInstance()->getString("ScrapperImageSrc")) { - // Region fallback: WOR(LD), US, CUS(TOM?), JP, EU - for (auto _region : std::vector{ region, "wor", "us", "cus", "jp", "eu" }) + ripList = getRipList(Settings::getInstance()->getString("ScrapperThumbSrc")); + if (!ripList.empty()) { + pugi::xml_node art = findMedia(media_list, ripList, region); if (art) - break; - - for (auto node : results) { - if (node.node().attribute("region").value() == _region) - { - art = node.node(); - break; - } + // Ask for the same image, but with a smaller size, for the thumbnail displayed during scraping + result.thumbnailUrl = ensureUrl(art.text().get()); } + else + LOG(LogDebug) << "Failed to find media XML node for thumbnail"; } - } // results + } - if (art) + if (!Settings::getInstance()->getString("ScrapperLogoSrc").empty()) { - // Sending a 'softname' containing space will make the image URLs returned by the API also contain the space. - // Escape any spaces in the URL here - result.imageUrl = Utils::String::replace(art.text().get(), " ", "%20"); - - // Get the media type returned by ScreenScraper - std::string media_type = art.attribute("format").value(); - if (!media_type.empty()) - result.imageType = "." + media_type; - - // Ask for the same image, but with a smaller size, for the thumbnail displayed during scraping - result.thumbnailUrl = result.imageUrl + "&maxheight=250"; - }else{ - LOG(LogDebug) << "Failed to find media XML node with name=" << ssConfig.media_name; + ripList = getRipList(Settings::getInstance()->getString("ScrapperLogoSrc")); + if (!ripList.empty()) + { + pugi::xml_node art = findMedia(media_list, ripList, region); + if (art) + result.marqueeUrl = ensureUrl(art.text().get()); + else + LOG(LogDebug) << "Failed to find media XML node for video"; + } } + if (Settings::getInstance()->getBool("ScrapeVideos")) + { + ripList = getRipList("video"); + if (!ripList.empty()) + { + pugi::xml_node art = findMedia(media_list, ripList, region); + if (art) + result.videoUrl = ensureUrl(art.text().get()); + else + LOG(LogDebug) << "Failed to find media XML node for video"; + } + } } out_results.push_back(result); } // game + + LOG(LogDebug) << "ScreenScraperRequest::processGame <<"; } // Currently not used in this module @@ -332,17 +575,34 @@ void ScreenScraperRequest::processList(const pugi::xml_document& xmldoc, std::ve game = game.next_sibling("jeu"); } - - } -std::string ScreenScraperRequest::ScreenScraperConfig::getGameSearchUrl(const std::string gameName) const +std::string ScreenScraperRequest::ScreenScraperConfig::getGameSearchUrl(const std::string gameName, bool jeuRecherche) const { - return API_URL_BASE + + + std::string ret = API_URL_BASE + "/jeuInfos.php?devid=" + Utils::String::scramble(API_DEV_U, API_DEV_KEY) + "&devpassword=" + Utils::String::scramble(API_DEV_P, API_DEV_KEY) + "&softname=" + HttpReq::urlEncode(API_SOFT_NAME) + "&output=xml" + "&romnom=" + HttpReq::urlEncode(gameName); + if (jeuRecherche) + { + ret = API_URL_BASE + + "/jeuRecherche.php?devid=" + Utils::String::scramble(API_DEV_U, API_DEV_KEY) + + "&devpassword=" + Utils::String::scramble(API_DEV_P, API_DEV_KEY) + + "&softname=" + HttpReq::urlEncode(API_SOFT_NAME) + + "&output=xml" + + "&recherche=" + HttpReq::urlEncode(gameName); + } + + std::string user = Settings::getInstance()->getString("ScreenScraperUser"); + std::string pass = Settings::getInstance()->getString("ScreenScraperPass"); + + if (!user.empty() && !pass.empty()) + ret = ret + "&ssid=" + HttpReq::urlEncode(user) + "&sspassword=" + HttpReq::urlEncode(pass); + + return ret; } diff --git a/es-app/src/scrapers/ScreenScraper.h b/es-app/src/scrapers/ScreenScraper.h index 2999a4649e..fa0bb4ebb4 100644 --- a/es-app/src/scrapers/ScreenScraper.h +++ b/es-app/src/scrapers/ScreenScraper.h @@ -21,12 +21,13 @@ class ScreenScraperRequest : public ScraperHttpRequest // Settings for the scraper static const struct ScreenScraperConfig { - std::string getGameSearchUrl(const std::string gameName) const; + std::string getGameSearchUrl(const std::string gameName, bool jeuRecherche=false) const; // Access to the API - const std::string API_DEV_U = { 91, 32, 7, 17 }; - const std::string API_DEV_P = { 108, 28, 54, 55, 83, 43, 91, 44, 30, 22, 41, 12, 0, 108, 38, 29 }; - const std::string API_DEV_KEY = { 54, 73, 115, 100, 101, 67, 111, 107, 79, 66, 68, 66, 67, 56, 118, 77, 54, 88, 101, 54 }; + const std::string API_DEV_U = { 60, 10, 23, 6, 22, 69, 25, 23, 80, 21 }; + const std::string API_DEV_P = { 104, 86, 49, 16, 32, 98, 49, 60, 47, 85, 87, 47, 81, 67, 10, 58 }; + const std::string API_DEV_KEY = { 80, 101, 97, 99, 101, 32, 97, 110, 100, 32, 98, 101, 32, 119, 105, 108, 100 }; + const std::string API_URL_BASE = "https://www.screenscraper.fr/api2"; const std::string API_SOFT_NAME = "Emulationstation " + static_cast(PROGRAM_VERSION_STRING); @@ -43,21 +44,22 @@ class ScreenScraperRequest : public ScraperHttpRequest Note that no all games contain values for these, so we default to "box-2D" since it's the most common. **/ - std::string media_name = "box-2D"; + //std::string media_name = "box-2D"; // Which Region to use when selecting the artwork - // Applies to: artwork, name of the game, date of release + // Applies to: artwork, name of the game, date of release std::string region = "US"; // Which Language to use when selecting the textual information // Applies to: description, genre - std::string language = "EN"; + //std::string language = "EN"; ScreenScraperConfig() {}; } configuration; protected: - void process(const std::unique_ptr& req, std::vector& results) override; + bool process(HttpReq* request, std::vector& results) override; + std::string ensureUrl(const std::string url); void processList(const pugi::xml_document& xmldoc, std::vector& results); void processGame(const pugi::xml_document& xmldoc, std::vector& results); @@ -65,7 +67,10 @@ class ScreenScraperRequest : public ScraperHttpRequest std::queue< std::unique_ptr >* mRequestQueue; - +private: + std::vector getRipList(std::string imageSource); + pugi::xml_node findMedia(pugi::xml_node media_list, std::vector mediaNames, std::string region); + pugi::xml_node findMedia(pugi::xml_node media_list, std::string mediaName, std::string region); }; diff --git a/es-app/src/scrapers/ThreadedScraper.cpp b/es-app/src/scrapers/ThreadedScraper.cpp new file mode 100644 index 0000000000..134472bf92 --- /dev/null +++ b/es-app/src/scrapers/ThreadedScraper.cpp @@ -0,0 +1,216 @@ +#include "ThreadedScraper.h" +#include "Window.h" +#include "FileData.h" +#include "components/AsyncNotificationComponent.h" +#include "EsLocale.h" +#include "guis/GuiMsgBox.h" +#include "Gamelist.h" +#include "Log.h" + +#define GUIICON _U("\uF03E ") + +ThreadedScraper* ThreadedScraper::mInstance = nullptr; +bool ThreadedScraper::mPaused = false; + +ThreadedScraper::ThreadedScraper(Window* window, const std::queue& searches) + : mSearchQueue(searches), mWindow(window) +{ + mExit = false; + mTotal = (int) mSearchQueue.size(); + + mWndNotification = new AsyncNotificationComponent(window); + + mWindow->registerNotificationComponent(mWndNotification); + search(mSearchQueue.front()); + mHandle = new std::thread(&ThreadedScraper::run, this); +} + +ThreadedScraper::~ThreadedScraper() +{ + mWindow->unRegisterNotificationComponent(mWndNotification); + delete mWndNotification; + + ThreadedScraper::mInstance = nullptr; +} + +std::string ThreadedScraper::formatGameName(FileData* game) +{ + return "["+game->getSystemName()+"] " + game->getName(); +} + +void ThreadedScraper::search(const ScraperSearchParams& params) +{ + LOG(LogDebug) << "ThreadedScraper::formatGameName"; + + std::string gameName = formatGameName(params.game); + + LOG(LogInfo) << "ThreadedScraper::search >> " << gameName; + + mCurrentAction = ""; + mLastSearch = params; + mSearchHandle = startScraperSearch(params); + + std::string idx = std::to_string(mTotal + 1 - mSearchQueue.size()) + "/" + std::to_string(mTotal); + + mWndNotification->updateTitle(GUIICON + _("SCRAPING") + "... " + idx); + mWndNotification->updateText(gameName, _("Searching")+"..."); + mWndNotification->updatePercent(-1); + + LOG(LogDebug) << "ThreadedScraper::search <<"; +} + +void ThreadedScraper::processError(int status, const std::string statusString) +{ + if (status == HttpReq::REQ_430_TOOMANYSCRAPS || status == HttpReq::REQ_430_TOOMANYFAILURES || + status == HttpReq::REQ_426_BLACKLISTED || status == HttpReq::REQ_FILESTREAM_ERROR || status == HttpReq::REQ_426_SERVERMAINTENANCE || + status == HttpReq::REQ_403_BADLOGIN || status == HttpReq::REQ_401_FORBIDDEN) + { + mExit = true; + mWindow->postToUiThread([statusString](Window* w) { w->pushGui(new GuiMsgBox(w, _("SCRAPE FAILED") + " : " + statusString)); }); + } + else + mErrors.push_back(statusString); +} + +void ThreadedScraper::run() +{ + while (!mExit && !mSearchQueue.empty()) + { + if (mPaused) + { + while (!mExit && mPaused) + { + std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + } + } + + if (mSearchHandle && mSearchHandle->status() != ASYNC_IN_PROGRESS) + { + auto status = mSearchHandle->status(); + auto results = mSearchHandle->getResults(); + auto statusString = mSearchHandle->getStatusString(); + auto httpCode = mSearchHandle->getErrorCode(); + + LOG(LogDebug) << "ThreadedScraper::SearchResponse : " << httpCode << " " << statusString; + + mSearchHandle.reset(); + + if (status == ASYNC_DONE) + { + if (results.size() > 0) + { + if (results[0].hadMedia()) + processMedias(results[0]); + else + acceptResult(results[0]); + } + } + else if (status == ASYNC_ERROR) + processError(httpCode, statusString); + } + + if (mMDResolveHandle && mMDResolveHandle->status() != ASYNC_IN_PROGRESS) + { + auto status = mMDResolveHandle->status(); + auto result = mMDResolveHandle->getResult(); + auto statusString = mMDResolveHandle->getStatusString(); + auto httpCode = mMDResolveHandle->getErrorCode(); + + LOG(LogDebug) << "ThreadedScraper::ResolveResponse : " << statusString; + + mCurrentAction = ""; + mMDResolveHandle.reset(); + + if (status == ASYNC_DONE) + acceptResult(result); + else if (status == ASYNC_ERROR) + processError(httpCode, statusString); + } + + if (mMDResolveHandle && mMDResolveHandle->status() == ASYNC_IN_PROGRESS) + { + std::string action = mMDResolveHandle->getCurrentItem(); + if (action != mCurrentAction) + { + mCurrentAction = action; + mWndNotification->updateText(formatGameName(mLastSearch.game), _("Downloading") + " " + mCurrentAction); + } + + mWndNotification->updatePercent(mMDResolveHandle->getPercent()); + } + + if (mSearchHandle == nullptr && mMDResolveHandle == nullptr) + { + mSearchQueue.pop(); + + if (mSearchQueue.empty()) + { + LOG(LogDebug) << "ThreadedScraper::finished"; + break; + } + + search(mSearchQueue.front()); + } + else + { + std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + } + + if (!mExit) + mWindow->displayNotificationMessage(GUIICON + _("SCRAPING FINISHED. REFRESH UPDATE GAMES LISTS TO APPLY CHANGES.")); + + delete this; + ThreadedScraper::mInstance = nullptr; +} + +void ThreadedScraper::processMedias(ScraperSearchResult result) +{ + LOG(LogDebug) << "ThreadedScraper::processMedias >>"; + mMDResolveHandle = resolveMetaDataAssets(result, mLastSearch); + LOG(LogDebug) << "ThreadedScraper::processMedias <<"; +} + +void ThreadedScraper::acceptResult(const ScraperSearchResult& result) +{ + LOG(LogDebug) << "ThreadedScraper::acceptResult >>"; + + ScraperSearchParams& search = mSearchQueue.front(); + + auto game = search.game; + + mWindow->postToUiThread([game, result](Window* w) + { + LOG(LogDebug) << "ThreadedScraper::importScrappedMetadata"; + game->getMetadata().importScrappedMetadata(result.mdl); + + LOG(LogDebug) << "ThreadedScraper::saveToGamelistRecovery"; + saveToGamelistRecovery(game); + }); + + LOG(LogDebug) << "ThreadedScraper::acceptResult <<"; +} + +void ThreadedScraper::start(Window* window, const std::queue& searches) +{ + if (ThreadedScraper::mInstance != nullptr) + return; + + ThreadedScraper::mInstance = new ThreadedScraper(window, searches); +} + +void ThreadedScraper::stop() +{ + auto thread = ThreadedScraper::mInstance; + if (thread == nullptr) + return; + + try + { + thread->mExit = true; + } + catch (...) {} +} + diff --git a/es-app/src/scrapers/ThreadedScraper.h b/es-app/src/scrapers/ThreadedScraper.h new file mode 100644 index 0000000000..ade11ee411 --- /dev/null +++ b/es-app/src/scrapers/ThreadedScraper.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include "Scraper.h" +#include "components/AsyncNotificationComponent.h" + +class ThreadedScraper +{ +public: + static void start(Window* window, const std::queue& searches); + static void stop(); + static bool isRunning() { return mInstance != nullptr; } + + static void pause() { mPaused = true; } + static void resume() { mPaused = false; } + +private: + ThreadedScraper(Window* window, const std::queue& searches); + ~ThreadedScraper(); + + Window* mWindow; + AsyncNotificationComponent* mWndNotification; + std::string mCurrentAction; + + std::vector mErrors; + + void run(); + + std::thread* mHandle; + std::queue mSearchQueue; + + ScraperSearchParams mLastSearch; + std::unique_ptr mSearchHandle; + std::unique_ptr mMDResolveHandle; + + void search(const ScraperSearchParams& params); + void processMedias(ScraperSearchResult result); + void acceptResult(const ScraperSearchResult& result); + void processError(int status, const std::string statusString); + + std::string formatGameName(FileData* game); + + int mTotal; + bool mExit; + + static bool mPaused; + static ThreadedScraper* mInstance; +}; + diff --git a/es-app/src/scrapers/md5.cpp b/es-app/src/scrapers/md5.cpp new file mode 100644 index 0000000000..631e5b1973 --- /dev/null +++ b/es-app/src/scrapers/md5.cpp @@ -0,0 +1,362 @@ +/* MD5 +converted to C++ class by Frank Thilo (thilo@unix-ag.org) +for bzflag (http://www.bzflag.org) + +based on: + +md5.h and md5.c +reference implemantion of RFC 1321 + +Copyright (C) 1991-2, RSA Data Security, Inc. Created 1991. All +rights reserved. + +License to copy and use this software is granted provided that it +is identified as the "RSA Data Security, Inc. MD5 Message-Digest +Algorithm" in all material mentioning or referencing this software +or this function. + +License is also granted to make and use derivative works provided +that such works are identified as "derived from the RSA Data +Security, Inc. MD5 Message-Digest Algorithm" in all material +mentioning or referencing the derived work. + +RSA Data Security, Inc. makes no representations concerning either +the merchantability of this software or the suitability of this +software for any particular purpose. It is provided "as is" +without express or implied warranty of any kind. + +These notices must be retained in any copies of any part of this +documentation and/or software. + +*/ + +/* interface header */ +#include "md5.h" + +/* system implementation headers */ +#include + + +// Constants for MD5Transform routine. +#define S11 7 +#define S12 12 +#define S13 17 +#define S14 22 +#define S21 5 +#define S22 9 +#define S23 14 +#define S24 20 +#define S31 4 +#define S32 11 +#define S33 16 +#define S34 23 +#define S41 6 +#define S42 10 +#define S43 15 +#define S44 21 + +/////////////////////////////////////////////// + +// F, G, H and I are basic MD5 functions. +inline MD5::uint4 MD5::F(uint4 x, uint4 y, uint4 z) { + return x&y | ~x&z; +} + +inline MD5::uint4 MD5::G(uint4 x, uint4 y, uint4 z) { + return x&z | y&~z; +} + +inline MD5::uint4 MD5::H(uint4 x, uint4 y, uint4 z) { + return x^y^z; +} + +inline MD5::uint4 MD5::I(uint4 x, uint4 y, uint4 z) { + return y ^ (x | ~z); +} + +// rotate_left rotates x left n bits. +inline MD5::uint4 MD5::rotate_left(uint4 x, int n) { + return (x << n) | (x >> (32 - n)); +} + +// FF, GG, HH, and II transformations for rounds 1, 2, 3, and 4. +// Rotation is separate from addition to prevent recomputation. +inline void MD5::FF(uint4 &a, uint4 b, uint4 c, uint4 d, uint4 x, uint4 s, uint4 ac) { + a = rotate_left(a + F(b, c, d) + x + ac, s) + b; +} + +inline void MD5::GG(uint4 &a, uint4 b, uint4 c, uint4 d, uint4 x, uint4 s, uint4 ac) { + a = rotate_left(a + G(b, c, d) + x + ac, s) + b; +} + +inline void MD5::HH(uint4 &a, uint4 b, uint4 c, uint4 d, uint4 x, uint4 s, uint4 ac) { + a = rotate_left(a + H(b, c, d) + x + ac, s) + b; +} + +inline void MD5::II(uint4 &a, uint4 b, uint4 c, uint4 d, uint4 x, uint4 s, uint4 ac) { + a = rotate_left(a + I(b, c, d) + x + ac, s) + b; +} + +////////////////////////////////////////////// + +// default ctor, just initailize +MD5::MD5() +{ + init(); +} + +////////////////////////////////////////////// + +// nifty shortcut ctor, compute MD5 for string and finalize it right away +MD5::MD5(const std::string &text) +{ + init(); + update(text.c_str(), text.length()); + finalize(); +} + +////////////////////////////// + +void MD5::init() +{ + finalized = false; + + count[0] = 0; + count[1] = 0; + + // load magic initialization constants. + state[0] = 0x67452301; + state[1] = 0xefcdab89; + state[2] = 0x98badcfe; + state[3] = 0x10325476; +} + +////////////////////////////// + +// decodes input (unsigned char) into output (uint4). Assumes len is a multiple of 4. +void MD5::decode(uint4 output[], const uint1 input[], size_type len) +{ + for (unsigned int i = 0, j = 0; j < len; i++, j += 4) + output[i] = ((uint4)input[j]) | (((uint4)input[j + 1]) << 8) | + (((uint4)input[j + 2]) << 16) | (((uint4)input[j + 3]) << 24); +} + +////////////////////////////// + +// encodes input (uint4) into output (unsigned char). Assumes len is +// a multiple of 4. +void MD5::encode(uint1 output[], const uint4 input[], size_type len) +{ + for (size_type i = 0, j = 0; j < len; i++, j += 4) { + output[j] = input[i] & 0xff; + output[j + 1] = (input[i] >> 8) & 0xff; + output[j + 2] = (input[i] >> 16) & 0xff; + output[j + 3] = (input[i] >> 24) & 0xff; + } +} + +////////////////////////////// + +// apply MD5 algo on a block +void MD5::transform(const uint1 block[blocksize]) +{ + uint4 a = state[0], b = state[1], c = state[2], d = state[3], x[16]; + decode(x, block, blocksize); + + /* Round 1 */ + FF(a, b, c, d, x[0], S11, 0xd76aa478); /* 1 */ + FF(d, a, b, c, x[1], S12, 0xe8c7b756); /* 2 */ + FF(c, d, a, b, x[2], S13, 0x242070db); /* 3 */ + FF(b, c, d, a, x[3], S14, 0xc1bdceee); /* 4 */ + FF(a, b, c, d, x[4], S11, 0xf57c0faf); /* 5 */ + FF(d, a, b, c, x[5], S12, 0x4787c62a); /* 6 */ + FF(c, d, a, b, x[6], S13, 0xa8304613); /* 7 */ + FF(b, c, d, a, x[7], S14, 0xfd469501); /* 8 */ + FF(a, b, c, d, x[8], S11, 0x698098d8); /* 9 */ + FF(d, a, b, c, x[9], S12, 0x8b44f7af); /* 10 */ + FF(c, d, a, b, x[10], S13, 0xffff5bb1); /* 11 */ + FF(b, c, d, a, x[11], S14, 0x895cd7be); /* 12 */ + FF(a, b, c, d, x[12], S11, 0x6b901122); /* 13 */ + FF(d, a, b, c, x[13], S12, 0xfd987193); /* 14 */ + FF(c, d, a, b, x[14], S13, 0xa679438e); /* 15 */ + FF(b, c, d, a, x[15], S14, 0x49b40821); /* 16 */ + + /* Round 2 */ + GG(a, b, c, d, x[1], S21, 0xf61e2562); /* 17 */ + GG(d, a, b, c, x[6], S22, 0xc040b340); /* 18 */ + GG(c, d, a, b, x[11], S23, 0x265e5a51); /* 19 */ + GG(b, c, d, a, x[0], S24, 0xe9b6c7aa); /* 20 */ + GG(a, b, c, d, x[5], S21, 0xd62f105d); /* 21 */ + GG(d, a, b, c, x[10], S22, 0x2441453); /* 22 */ + GG(c, d, a, b, x[15], S23, 0xd8a1e681); /* 23 */ + GG(b, c, d, a, x[4], S24, 0xe7d3fbc8); /* 24 */ + GG(a, b, c, d, x[9], S21, 0x21e1cde6); /* 25 */ + GG(d, a, b, c, x[14], S22, 0xc33707d6); /* 26 */ + GG(c, d, a, b, x[3], S23, 0xf4d50d87); /* 27 */ + GG(b, c, d, a, x[8], S24, 0x455a14ed); /* 28 */ + GG(a, b, c, d, x[13], S21, 0xa9e3e905); /* 29 */ + GG(d, a, b, c, x[2], S22, 0xfcefa3f8); /* 30 */ + GG(c, d, a, b, x[7], S23, 0x676f02d9); /* 31 */ + GG(b, c, d, a, x[12], S24, 0x8d2a4c8a); /* 32 */ + + /* Round 3 */ + HH(a, b, c, d, x[5], S31, 0xfffa3942); /* 33 */ + HH(d, a, b, c, x[8], S32, 0x8771f681); /* 34 */ + HH(c, d, a, b, x[11], S33, 0x6d9d6122); /* 35 */ + HH(b, c, d, a, x[14], S34, 0xfde5380c); /* 36 */ + HH(a, b, c, d, x[1], S31, 0xa4beea44); /* 37 */ + HH(d, a, b, c, x[4], S32, 0x4bdecfa9); /* 38 */ + HH(c, d, a, b, x[7], S33, 0xf6bb4b60); /* 39 */ + HH(b, c, d, a, x[10], S34, 0xbebfbc70); /* 40 */ + HH(a, b, c, d, x[13], S31, 0x289b7ec6); /* 41 */ + HH(d, a, b, c, x[0], S32, 0xeaa127fa); /* 42 */ + HH(c, d, a, b, x[3], S33, 0xd4ef3085); /* 43 */ + HH(b, c, d, a, x[6], S34, 0x4881d05); /* 44 */ + HH(a, b, c, d, x[9], S31, 0xd9d4d039); /* 45 */ + HH(d, a, b, c, x[12], S32, 0xe6db99e5); /* 46 */ + HH(c, d, a, b, x[15], S33, 0x1fa27cf8); /* 47 */ + HH(b, c, d, a, x[2], S34, 0xc4ac5665); /* 48 */ + + /* Round 4 */ + II(a, b, c, d, x[0], S41, 0xf4292244); /* 49 */ + II(d, a, b, c, x[7], S42, 0x432aff97); /* 50 */ + II(c, d, a, b, x[14], S43, 0xab9423a7); /* 51 */ + II(b, c, d, a, x[5], S44, 0xfc93a039); /* 52 */ + II(a, b, c, d, x[12], S41, 0x655b59c3); /* 53 */ + II(d, a, b, c, x[3], S42, 0x8f0ccc92); /* 54 */ + II(c, d, a, b, x[10], S43, 0xffeff47d); /* 55 */ + II(b, c, d, a, x[1], S44, 0x85845dd1); /* 56 */ + II(a, b, c, d, x[8], S41, 0x6fa87e4f); /* 57 */ + II(d, a, b, c, x[15], S42, 0xfe2ce6e0); /* 58 */ + II(c, d, a, b, x[6], S43, 0xa3014314); /* 59 */ + II(b, c, d, a, x[13], S44, 0x4e0811a1); /* 60 */ + II(a, b, c, d, x[4], S41, 0xf7537e82); /* 61 */ + II(d, a, b, c, x[11], S42, 0xbd3af235); /* 62 */ + II(c, d, a, b, x[2], S43, 0x2ad7d2bb); /* 63 */ + II(b, c, d, a, x[9], S44, 0xeb86d391); /* 64 */ + + state[0] += a; + state[1] += b; + state[2] += c; + state[3] += d; + + // Zeroize sensitive information. + memset(x, 0, sizeof x); +} + +////////////////////////////// + +// MD5 block update operation. Continues an MD5 message-digest +// operation, processing another message block +void MD5::update(const unsigned char input[], size_type length) +{ + // compute number of bytes mod 64 + size_type index = count[0] / 8 % blocksize; + + // Update number of bits + if ((count[0] += (length << 3)) < (length << 3)) + count[1]++; + count[1] += (length >> 29); + + // number of bytes we need to fill in buffer + size_type firstpart = 64 - index; + + size_type i; + + // transform as many times as possible. + if (length >= firstpart) + { + // fill buffer first, transform + memcpy(&buffer[index], input, firstpart); + transform(buffer); + + // transform chunks of blocksize (64 bytes) + for (i = firstpart; i + blocksize <= length; i += blocksize) + transform(&input[i]); + + index = 0; + } + else + i = 0; + + // buffer remaining input + memcpy(&buffer[index], &input[i], length - i); +} + +////////////////////////////// + +// for convenience provide a verson with signed char +void MD5::update(const char input[], size_type length) +{ + update((const unsigned char*)input, length); +} + +////////////////////////////// + +// MD5 finalization. Ends an MD5 message-digest operation, writing the +// the message digest and zeroizing the context. +MD5& MD5::finalize() +{ + static unsigned char padding[64] = { + 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + }; + + if (!finalized) { + // Save number of bits + unsigned char bits[8]; + encode(bits, count, 8); + + // pad out to 56 mod 64. + size_type index = count[0] / 8 % 64; + size_type padLen = (index < 56) ? (56 - index) : (120 - index); + update(padding, padLen); + + // Append length (before padding) + update(bits, 8); + + // Store state in digest + encode(digest, state, 16); + + // Zeroize sensitive information. + memset(buffer, 0, sizeof buffer); + memset(count, 0, sizeof count); + + finalized = true; + } + + return *this; +} + +////////////////////////////// + +// return hex representation of digest as string +std::string MD5::hexdigest() const +{ + if (!finalized) + return ""; + + char buf[33]; + for (int i = 0; i<16; i++) + sprintf(buf + i * 2, "%02x", digest[i]); + buf[32] = 0; + + return std::string(buf); +} + +////////////////////////////// + +std::ostream& operator<<(std::ostream& out, MD5 md5) +{ + return out << md5.hexdigest(); +} + +////////////////////////////// + +std::string md5(const std::string str) +{ + MD5 md5 = MD5(str); + + return md5.hexdigest(); +} \ No newline at end of file diff --git a/es-app/src/scrapers/md5.h b/es-app/src/scrapers/md5.h new file mode 100644 index 0000000000..5f1dcad422 --- /dev/null +++ b/es-app/src/scrapers/md5.h @@ -0,0 +1,94 @@ +/* MD5 +converted to C++ class by Frank Thilo (thilo@unix-ag.org) +for bzflag (http://www.bzflag.org) + +based on: + +md5.h and md5.c +reference implementation of RFC 1321 + +Copyright (C) 1991-2, RSA Data Security, Inc. Created 1991. All +rights reserved. + +License to copy and use this software is granted provided that it +is identified as the "RSA Data Security, Inc. MD5 Message-Digest +Algorithm" in all material mentioning or referencing this software +or this function. + +License is also granted to make and use derivative works provided +that such works are identified as "derived from the RSA Data +Security, Inc. MD5 Message-Digest Algorithm" in all material +mentioning or referencing the derived work. + +RSA Data Security, Inc. makes no representations concerning either +the merchantability of this software or the suitability of this +software for any particular purpose. It is provided "as is" +without express or implied warranty of any kind. + +These notices must be retained in any copies of any part of this +documentation and/or software. + +*/ + +#ifndef BZF_MD5_H +#define BZF_MD5_H + +#include +#include + + +// a small class for calculating MD5 hashes of strings or byte arrays +// it is not meant to be fast or secure +// +// usage: 1) feed it blocks of uchars with update() +// 2) finalize() +// 3) get hexdigest() string +// or +// MD5(std::string).hexdigest() +// +// assumes that char is 8 bit and int is 32 bit +class MD5 +{ +public: + typedef unsigned int size_type; // must be 32bit + + MD5(); + MD5(const std::string& text); + + void update(const unsigned char *buf, size_type length); + void update(const char *buf, size_type length); + MD5& finalize(); + std::string hexdigest() const; + friend std::ostream& operator<<(std::ostream&, MD5 md5); + +private: + void init(); + typedef unsigned char uint1; // 8bit + typedef unsigned int uint4; // 32bit + enum { blocksize = 64 }; // VC6 won't eat a const static int here + + void transform(const uint1 block[blocksize]); + static void decode(uint4 output[], const uint1 input[], size_type len); + static void encode(uint1 output[], const uint4 input[], size_type len); + + bool finalized; + uint1 buffer[blocksize]; // bytes that didn't fit in last 64 byte chunk + uint4 count[2]; // 64bit counter for number of bits (lo, hi) + uint4 state[4]; // digest so far + uint1 digest[16]; // the result + + // low level logic operations + static inline uint4 F(uint4 x, uint4 y, uint4 z); + static inline uint4 G(uint4 x, uint4 y, uint4 z); + static inline uint4 H(uint4 x, uint4 y, uint4 z); + static inline uint4 I(uint4 x, uint4 y, uint4 z); + static inline uint4 rotate_left(uint4 x, int n); + static inline void FF(uint4 &a, uint4 b, uint4 c, uint4 d, uint4 x, uint4 s, uint4 ac); + static inline void GG(uint4 &a, uint4 b, uint4 c, uint4 d, uint4 x, uint4 s, uint4 ac); + static inline void HH(uint4 &a, uint4 b, uint4 c, uint4 d, uint4 x, uint4 s, uint4 ac); + static inline void II(uint4 &a, uint4 b, uint4 c, uint4 d, uint4 x, uint4 s, uint4 ac); +}; + +std::string md5(const std::string str); + +#endif \ No newline at end of file diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 44e299c499..c55c73651e 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -8,6 +8,10 @@ #include "Settings.h" #include "SystemData.h" #include "Window.h" +#include "AudioManager.h" +#include "components/VideoComponent.h" +#include "components/VideoVlcComponent.h" +#include // buffer values for scrolling velocity (left, stopped, right) const int logoBuffersLeft[] = { -5, -2, -1 }; @@ -20,101 +24,281 @@ SystemView::SystemView(Window* window) : IList(wind mCamOffset = 0; mExtrasCamOffset = 0; mExtrasFadeOpacity = 0.0f; - + mLastSystem = nullptr; + mScreensaverActive = false; + mDisable = false; + mShowing = false; + mLastCursor = 0; + mStaticBackground = nullptr; + mStaticVideoBackground = nullptr; + mExtrasFadeOldCursor = -1; + setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); populate(); } -void SystemView::populate() +SystemView::~SystemView() { - mEntries.clear(); + if (mStaticVideoBackground != nullptr) + { + delete mStaticVideoBackground; + mStaticVideoBackground = nullptr; + } - for(auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) + if (mStaticBackground != nullptr) { - const std::shared_ptr& theme = (*it)->getTheme(); + delete mStaticBackground; + mStaticBackground = nullptr; + } - if(mViewNeedsReload) - getViewElements(theme); + clearEntries(); +} + +void SystemView::clearEntries() +{ + for (int i = 0; i < mEntries.size(); i++) + { + for (auto extra : mEntries[i].data.backgroundExtras) + delete extra; - if((*it)->isVisible()) + mEntries[i].data.backgroundExtras.clear(); + } + + mEntries.clear(); +} + +class SystemRandomPlaylist : public IPlaylist +{ +public: + enum PlaylistType + { + IMAGE, + THUMBNAIL, + MARQUEE, + VIDEO + }; + + SystemRandomPlaylist(SystemData* system, PlaylistType type) : mMt19937(mRandomDevice()) + { + mFirstRun = true; + mSystem = system; + mType = type; + } + + std::string getNextItem() + { + if (mFirstRun) { - Entry e; - e.name = (*it)->getName(); - e.object = *it; + std::vector files = mSystem->getRootFolder()->getFilesRecursive(GAME, false); - // make logo - const ThemeData::ThemeElement* logoElem = theme->getElement("system", "logo", "image"); - if(logoElem) + for (auto file : files) { - std::string path = logoElem->get("path"); - std::string defaultPath = logoElem->has("default") ? logoElem->get("default") : ""; - if((!path.empty() && ResourceManager::getInstance()->fileExists(path)) - || (!defaultPath.empty() && ResourceManager::getInstance()->fileExists(defaultPath))) + switch (mType) { - ImageComponent* logo = new ImageComponent(mWindow, false, false); - logo->setMaxSize(mCarousel.logoSize * mCarousel.logoScale); - logo->applyTheme(theme, "system", "logo", ThemeFlags::PATH | ThemeFlags::COLOR); - logo->setRotateByTargetSize(true); - e.data.logo = std::shared_ptr(logo); + case IMAGE: + if (!file->getImagePath().empty()) + mPaths.push_back(file->getImagePath()); + break; + + case THUMBNAIL: + if (!file->getThumbnailPath().empty()) + mPaths.push_back(file->getThumbnailPath()); + break; + + case MARQUEE: + if (!file->getMarqueePath().empty()) + mPaths.push_back(file->getMarqueePath()); + break; + + case VIDEO: + if (!file->getVideoPath().empty()) + mPaths.push_back(file->getVideoPath()); + break; } } - if (!e.data.logo) - { - // no logo in theme; use text - TextComponent* text = new TextComponent(mWindow, - (*it)->getName(), - Font::get(FONT_SIZE_LARGE), - 0x000000FF, - ALIGN_CENTER); - text->setSize(mCarousel.logoSize * mCarousel.logoScale); - text->applyTheme((*it)->getTheme(), "system", "logoText", ThemeFlags::FONT_PATH | ThemeFlags::FONT_SIZE | ThemeFlags::COLOR | ThemeFlags::FORCE_UPPERCASE | ThemeFlags::LINE_SPACING | ThemeFlags::TEXT); - e.data.logo = std::shared_ptr(text); - - if (mCarousel.type == VERTICAL || mCarousel.type == VERTICAL_WHEEL) + + if (mPaths.size() > 0) + mUniformDistribution = std::uniform_int_distribution(0, mPaths.size() - 1); + + mFirstRun = false; + } + + if (mPaths.size() > 0) + { + int idx = mUniformDistribution(mMt19937); + if (idx >= 0 && idx < mPaths.size() && Utils::FileSystem::exists(mPaths[idx])) + return mPaths[idx]; + + // File not found ? Try the next file... + int stopidx = idx; + + idx++; + if (idx >= mPaths.size()) + idx = 0; + + while (idx != stopidx && idx < mPaths.size() && !Utils::FileSystem::exists(mPaths[idx])) + { + idx++; + if (idx >= mPaths.size()) + idx = 0; + } + + if (idx >= 0 && idx < mPaths.size() && Utils::FileSystem::exists(mPaths[idx])) + return mPaths[idx]; + } + + return ""; + } + +private: + SystemData* mSystem; + bool mFirstRun; + PlaylistType mType; + + std::vector mPaths; + + std::random_device mRandomDevice; + std::mt19937 mMt19937; + std::uniform_int_distribution mUniformDistribution; +}; + +void SystemView::populate() +{ + clearEntries(); + + for(auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) + { + const std::shared_ptr& theme = (*it)->getTheme(); + + if(mViewNeedsReload) + getViewElements(theme); + + if (!(*it)->isVisible()) + continue; + + Entry e; + e.name = (*it)->getName(); + e.object = *it; + + // make logo + const ThemeData::ThemeElement* logoElem = theme->getElement("system", "logo", "image"); + if (logoElem && logoElem->has("path")) + { + std::string path = logoElem->get("path"); + std::string defaultPath = logoElem->has("default") ? logoElem->get("default") : ""; + + if ((!path.empty() && ResourceManager::getInstance()->fileExists(path)) + || (!defaultPath.empty() && ResourceManager::getInstance()->fileExists(defaultPath))) + { + // Remove dynamic flags for png & jpg files : themes can contain oversized images that can't be unloaded by the TextureResource manager + ImageComponent* logo = new ImageComponent(mWindow, false, Utils::String::toLower(Utils::FileSystem::getExtension(path)) != ".svg"); + logo->setMaxSize(mCarousel.logoSize * mCarousel.logoScale); + logo->applyTheme(theme, "system", "logo", ThemeFlags::COLOR | ThemeFlags::ALIGNMENT | ThemeFlags::VISIBLE); // ThemeFlags::PATH | + + // Process here to be enable to set max picture size + auto elem = theme->getElement("system", "logo", "image"); + if (elem && elem->has("path")) { - text->setHorizontalAlignment(mCarousel.logoAlignment); - text->setVerticalAlignment(ALIGN_CENTER); - } else { - text->setHorizontalAlignment(ALIGN_CENTER); - text->setVerticalAlignment(mCarousel.logoAlignment); + auto path = elem->get("path"); + if (Utils::FileSystem::exists(path)) + logo->setImage(path, (elem->has("tile") && elem->get("tile")), MaxSizeInfo(mCarousel.logoSize * mCarousel.logoScale)); } + + if (mCarousel.size.x() != mCarousel.logoSize.x() & mCarousel.size.y() != mCarousel.logoSize.y()) + logo->setRotateByTargetSize(true); + + e.data.logo = std::shared_ptr(logo); } + } + + if (!e.data.logo) + { + // no logo in theme; use text + TextComponent* text = new TextComponent(mWindow, + (*it)->getFullName(), + Font::get(FONT_SIZE_LARGE), + 0x000000FF, + ALIGN_CENTER); + text->setSize(mCarousel.logoSize * mCarousel.logoScale); + text->applyTheme((*it)->getTheme(), "system", "logoText", ThemeFlags::FONT_PATH | ThemeFlags::FONT_SIZE | ThemeFlags::COLOR | ThemeFlags::FORCE_UPPERCASE | ThemeFlags::LINE_SPACING | ThemeFlags::TEXT); + e.data.logo = std::shared_ptr(text); if (mCarousel.type == VERTICAL || mCarousel.type == VERTICAL_WHEEL) { - if (mCarousel.logoAlignment == ALIGN_LEFT) - e.data.logo->setOrigin(0, 0.5); - else if (mCarousel.logoAlignment == ALIGN_RIGHT) - e.data.logo->setOrigin(1.0, 0.5); - else - e.data.logo->setOrigin(0.5, 0.5); - } else { - if (mCarousel.logoAlignment == ALIGN_TOP) - e.data.logo->setOrigin(0.5, 0); - else if (mCarousel.logoAlignment == ALIGN_BOTTOM) - e.data.logo->setOrigin(0.5, 1); - else - e.data.logo->setOrigin(0.5, 0.5); + text->setHorizontalAlignment(mCarousel.logoAlignment); + text->setVerticalAlignment(ALIGN_CENTER); } + else + { + text->setHorizontalAlignment(ALIGN_CENTER); + text->setVerticalAlignment(mCarousel.logoAlignment); + } + } + + if (mCarousel.type == VERTICAL || mCarousel.type == VERTICAL_WHEEL) + { + if (mCarousel.logoAlignment == ALIGN_LEFT) + e.data.logo->setOrigin(0, 0.5); + else if (mCarousel.logoAlignment == ALIGN_RIGHT) + e.data.logo->setOrigin(1.0, 0.5); + else + e.data.logo->setOrigin(0.5, 0.5); + } + else { + if (mCarousel.logoAlignment == ALIGN_TOP) + e.data.logo->setOrigin(0.5, 0); + else if (mCarousel.logoAlignment == ALIGN_BOTTOM) + e.data.logo->setOrigin(0.5, 1); + else + e.data.logo->setOrigin(0.5, 0.5); + } - Vector2f denormalized = mCarousel.logoSize * e.data.logo->getOrigin(); - e.data.logo->setPosition(denormalized.x(), denormalized.y(), 0.0); - // delete any existing extras - for (auto extra : e.data.backgroundExtras) - delete extra; - e.data.backgroundExtras.clear(); + + Vector2f denormalized = mCarousel.logoSize * e.data.logo->getOrigin(); + e.data.logo->setPosition(denormalized.x(), denormalized.y(), 0.0); + // delete any existing extras + for (auto extra : e.data.backgroundExtras) + delete extra; + e.data.backgroundExtras.clear(); + + // make background extras + e.data.backgroundExtras = ThemeData::makeExtras((*it)->getTheme(), "system", mWindow); + + for (auto extra : e.data.backgroundExtras) + { + if (extra->isKindOf()) + { + auto elem = (*it)->getTheme()->getElement("system", extra->getTag(), "video"); + if (elem != nullptr && elem->has("path") && Utils::String::startsWith(elem->get("path"), "{random")) + ((VideoComponent*)extra)->setPlaylist(std::make_shared(*it, SystemRandomPlaylist::VIDEO)); + } + else if (extra->isKindOf()) + { + auto elem = (*it)->getTheme()->getElement("system", extra->getTag(), "image"); + if (elem != nullptr && elem->has("path") && Utils::String::startsWith(elem->get("path"), "{random")) + { + std::string src = elem->get("path"); - // make background extras - e.data.backgroundExtras = ThemeData::makeExtras((*it)->getTheme(), "system", mWindow); + SystemRandomPlaylist::PlaylistType type = SystemRandomPlaylist::IMAGE; - // sort the extras by z-index - std::stable_sort(e.data.backgroundExtras.begin(), e.data.backgroundExtras.end(), [](GuiComponent* a, GuiComponent* b) { - return b->getZIndex() > a->getZIndex(); - }); + if (src == "{random:thumbnail}") + type = SystemRandomPlaylist::THUMBNAIL; + else if (src == "{random:marquee}") + type = SystemRandomPlaylist::MARQUEE; - this->add(e); + ((ImageComponent*)extra)->setPlaylist(std::make_shared(*it, type)); + } + } } + + // sort the extras by z-index + std::stable_sort(e.data.backgroundExtras.begin(), e.data.backgroundExtras.end(), [](GuiComponent* a, GuiComponent* b) { + return b->getZIndex() > a->getZIndex(); + }); + + this->add(e); } + if (mEntries.size() == 0) { // Something is wrong, there is not a single system to show, check if UI mode is not full @@ -123,6 +307,15 @@ void SystemView::populate() Settings::getInstance()->setString("UIMode", "Full"); mWindow->pushGui(new GuiMsgBox(mWindow, "The selected UI mode has nothing to show,\n returning to UI mode: FULL", "OK", nullptr)); } + + if (Settings::getInstance()->setString("HiddenSystems", "")) + { + Settings::getInstance()->saveFile(); + + // refresh GUI + populate(); + mWindow->pushGui(new GuiMsgBox(mWindow, "ERROR: EVERY SYSTEM IS HIDDEN, RE-DISPLAYING ALL OF THEM NOW", "OK", nullptr)); + } } } @@ -159,6 +352,34 @@ bool SystemView::input(InputConfig* config, Input input) listInput(1); return true; } + if (config->isMappedTo("pagedown", input)) + { + int cursor = mCursor + 10; + if (cursor < 0) + cursor += (int)mEntries.size(); + + if (cursor >= (int)mEntries.size()) + cursor -= (int)mEntries.size(); + + auto sd = mEntries.at(cursor).object; + ViewController::get()->goToSystemView(sd, true); + //listInput(10); + return true; + } + if (config->isMappedTo("pageup", input)) + { + int cursor = mCursor - 10; + if (cursor < 0) + cursor += (int)mEntries.size(); + if (cursor >= (int)mEntries.size()) + cursor -= (int)mEntries.size(); + + auto sd = mEntries.at(cursor).object; + ViewController::get()->goToSystemView(sd, true); + //listInput(-10); + return true; + } + break; case HORIZONTAL: case HORIZONTAL_WHEEL: @@ -173,6 +394,34 @@ bool SystemView::input(InputConfig* config, Input input) listInput(1); return true; } + if (config->isMappedTo("pagedown", input) && mEntries.size() > 10) + { + int cursor = mCursor + 10; + if (cursor < 0) + cursor += (int)mEntries.size(); + + if (cursor >= (int)mEntries.size()) + cursor -= (int)mEntries.size(); + + auto sd = mEntries.at(cursor).object; + ViewController::get()->goToSystemView(sd, true); + //listInput(10); + return true; + } + if (config->isMappedTo("pageup", input) && mEntries.size() > 10) + { + int cursor = mCursor - 10; + if (cursor < 0) + cursor += (int)mEntries.size(); + if (cursor >= (int)mEntries.size()) + cursor -= (int)mEntries.size(); + + auto sd = mEntries.at(cursor).object; + ViewController::get()->goToSystemView(sd, true); + //listInput(-10); + return true; + } + break; } @@ -193,7 +442,9 @@ bool SystemView::input(InputConfig* config, Input input) if(config->isMappedLike("left", input) || config->isMappedLike("right", input) || config->isMappedLike("up", input) || - config->isMappedLike("down", input)) + config->isMappedLike("down", input) || + config->isMappedLike("pagedown", input) || + config->isMappedLike("pageup", input)) listInput(0); if(!UIModeController::getInstance()->isUIModeKid() && config->isMappedTo("select", input) && Settings::getInstance()->getBool("ScreenSaverControls")) { @@ -209,11 +460,17 @@ bool SystemView::input(InputConfig* config, Input input) void SystemView::update(int deltaTime) { listUpdate(deltaTime); + updateExtras([this, deltaTime](GuiComponent* p) { p->update(deltaTime); }); GuiComponent::update(deltaTime); } void SystemView::onCursorChanged(const CursorState& /*state*/) { + if (mLastSystem != getSelected()) { + mLastSystem = getSelected(); + AudioManager::getInstance()->themeChanged(getSelected()->getTheme()); + } + // update help style updateHelpPrompts(); @@ -233,14 +490,23 @@ void SystemView::onCursorChanged(const CursorState& /*state*/) if(abs(target - posMax - startPos) < dist) endPos = target - posMax; // loop around the start (max - 1 -> -1) - // animate mSystemInfo's opacity (fade out, wait, fade back in) cancelAnimation(1); cancelAnimation(2); std::string transition_style = Settings::getInstance()->getString("TransitionStyle"); - bool goFast = transition_style == "instant"; + if (transition_style == "auto") + { + if (mCarousel.defaultTransition == "instant" || mCarousel.defaultTransition == "fade" || mCarousel.defaultTransition == "slide") + transition_style = mCarousel.defaultTransition; + else + transition_style = "slide"; + } + + int systemInfoDelay = mCarousel.systemInfoDelay; + + bool goFast = transition_style == "instant" || systemInfoDelay == 0; const float infoStartOpacity = mSystemInfo.getOpacity() / 255.f; Animation* infoFadeOut = new LambdaAnimation( @@ -255,11 +521,15 @@ void SystemView::onCursorChanged(const CursorState& /*state*/) setAnimation(infoFadeOut, 0, [this, gameCount] { std::stringstream ss; - if (!getSelected()->isGameSystem()) + if (!getSelected()->isGameSystem() && !getSelected()->isGroupSystem()) ss << "CONFIGURATION"; else - ss << gameCount << " GAMES AVAILABLE"; - + { + char strbuf[256]; + snprintf(strbuf, 256, EsLocale::nGetText("%i GAME AVAILABLE", "%i GAMES AVAILABLE", gameCount).c_str(), gameCount); + ss << strbuf; + } + mSystemInfo.setText(ss.str()); }, false, 1); @@ -270,20 +540,33 @@ void SystemView::onCursorChanged(const CursorState& /*state*/) }, goFast ? 10 : 300); // wait 600ms to fade in - setAnimation(infoFadeIn, goFast ? 0 : 2000, nullptr, false, 2); + setAnimation(infoFadeIn, goFast ? 0 : systemInfoDelay, nullptr, false, 2); + // fake preload + //setAnimation(infoFadeIn, goFast ? 0 : systemInfoDelay, [this] { ViewController::get()->getGameListView(mEntries.at(mCursor).object); }, false, 2); // no need to animate transition, we're not going anywhere (probably mEntries.size() == 1) if(endPos == mCamOffset && endPos == mExtrasCamOffset) return; + if (mLastCursor == mCursor) + return; + + if (!mCarousel.scrollSound.empty()) + Sound::get(mCarousel.scrollSound)->play(); + + int oldCursor = mLastCursor; + mLastCursor = mCursor; + Animation* anim; bool move_carousel = Settings::getInstance()->getBool("MoveCarousel"); if(transition_style == "fade") { float startExtrasFade = mExtrasFadeOpacity; anim = new LambdaAnimation( - [this, startExtrasFade, startPos, endPos, posMax, move_carousel](float t) + [this, startExtrasFade, startPos, endPos, posMax, move_carousel, oldCursor](float t) { + mExtrasFadeOldCursor = oldCursor; + t -= 1; float f = Math::lerp(startPos, endPos, t*t*t + 1); if(f < 0) @@ -294,15 +577,23 @@ void SystemView::onCursorChanged(const CursorState& /*state*/) this->mCamOffset = move_carousel ? f : endPos; t += 1; + /* if(t < 0.3f) this->mExtrasFadeOpacity = Math::lerp(0.0f, 1.0f, t / 0.3f + startExtrasFade); else if(t < 0.7f) this->mExtrasFadeOpacity = 1.0f; else this->mExtrasFadeOpacity = Math::lerp(1.0f, 0.0f, (t - 0.7f) / 0.3f); + */ + + if (t < 0.3f) + this->mExtrasFadeOpacity = 1.0f; + else if (t >= 0.7f) + this->mExtrasFadeOpacity = 0.0f; + else + this->mExtrasFadeOpacity = Math::lerp(1.0f, 0.0f, (t - 0.3f) / 0.4f); - if(t > 0.5f) - this->mExtrasCamOffset = endPos; + this->mExtrasCamOffset = endPos; }, 500); } else if (transition_style == "slide") { @@ -337,22 +628,44 @@ void SystemView::onCursorChanged(const CursorState& /*state*/) }, move_carousel ? 500 : 1); } + for (int i = 0; i < mEntries.size(); i++) + if (i != oldCursor && i != mCursor) + activateExtras(i, false); + + activateExtras(mCursor); + + setAnimation(anim, 0, [this] + { + mExtrasFadeOldCursor = -1; + + for (int i = 0; i < mEntries.size(); i++) + if (i != mCursor) + activateExtras(i, false); - setAnimation(anim, 0, nullptr, false, 0); + }, false, 0); } void SystemView::render(const Transform4x4f& parentTrans) { - if(size() == 0) + if (size() == 0 || !mVisible) return; // nothing to render Transform4x4f trans = getTransform() * parentTrans; + if (!Renderer::isVisibleOnScreen(trans.translation().x(), trans.translation().y(), mSize.x(), mSize.y())) + return; + auto systemInfoZIndex = mSystemInfo.getZIndex(); auto minMax = std::minmax(mCarousel.zIndex, systemInfoZIndex); renderExtras(trans, INT16_MIN, minMax.first); - renderFade(trans); + // renderFade(trans); + + if (mStaticBackground != nullptr) + mStaticBackground->render(trans); + + if (mStaticVideoBackground != nullptr) + mStaticVideoBackground->render(trans); if (mCarousel.zIndex > mSystemInfo.getZIndex()) { renderInfoBar(trans); @@ -375,14 +688,15 @@ std::vector SystemView::getHelpPrompts() { std::vector prompts; if (mCarousel.type == VERTICAL || mCarousel.type == VERTICAL_WHEEL) - prompts.push_back(HelpPrompt("up/down", "choose")); + prompts.push_back(HelpPrompt("up/down", _("CHOOSE"))); else - prompts.push_back(HelpPrompt("left/right", "choose")); - prompts.push_back(HelpPrompt("a", "select")); - prompts.push_back(HelpPrompt("x", "random")); + prompts.push_back(HelpPrompt("left/right", _("CHOOSE"))); + + prompts.push_back(HelpPrompt("a", _("SELECT"))); + prompts.push_back(HelpPrompt("x", _("RANDOM"))); if (!UIModeController::getInstance()->isUIModeKid() && Settings::getInstance()->getBool("ScreenSaverControls")) - prompts.push_back(HelpPrompt("select", "launch screensaver")); + prompts.push_back(HelpPrompt("select", _("LAUNCH SCREENSAVER"))); return prompts; } @@ -417,7 +731,39 @@ void SystemView::getViewElements(const std::shared_ptr& theme) const ThemeData::ThemeElement* sysInfoElem = theme->getElement("system", "systemInfo", "text"); if (sysInfoElem) + { mSystemInfo.applyTheme(theme, "system", "systemInfo", ThemeFlags::ALL); + mSystemInfo.setOpacity(0); + } + + const ThemeData::ThemeElement* fixedBackgroundElem = theme->getElement("system", "staticBackground", "image"); + if (fixedBackgroundElem) + { + if (mStaticBackground == nullptr) + mStaticBackground = new ImageComponent(mWindow, false); + + mStaticBackground->applyTheme(theme, "system", "staticBackground", ThemeFlags::ALL); + } + else if (mStaticBackground != nullptr) + { + delete mStaticBackground; + mStaticBackground = nullptr; + } + + const ThemeData::ThemeElement* fixedVideoBackgroundElem = theme->getElement("system", "staticBackgroundVideo", "video"); + if (fixedVideoBackgroundElem && (!fixedVideoBackgroundElem->has("visible") || fixedVideoBackgroundElem->get("visible"))) + { + if (mStaticVideoBackground == nullptr) + mStaticVideoBackground = new VideoVlcComponent(mWindow); + + mStaticVideoBackground->applyTheme(theme, "system", "staticBackgroundVideo", ThemeFlags::ALL); + } + else if (mStaticBackground != nullptr) + { + delete mStaticVideoBackground; + mStaticVideoBackground = nullptr; + } + mViewNeedsReload = false; } @@ -486,6 +832,12 @@ void SystemView::renderCarousel(const Transform4x4f& trans) break; } + if (mCarousel.logoPos.x() >= 0) + xOff = mCarousel.logoPos.x() - (mCarousel.type == HORIZONTAL ? (mCamOffset * logoSpacing[0]) : 0); + + if (mCarousel.logoPos.y() >= 0) + yOff = mCarousel.logoPos.y() - (mCarousel.type == VERTICAL ? (mCamOffset * logoSpacing[1]) : 0); + int center = (int)(mCamOffset); int logoCount = Math::min(mCarousel.maxLogoCount, (int)mEntries.size()); @@ -493,7 +845,7 @@ void SystemView::renderCarousel(const Transform4x4f& trans) int bufferIndex = getScrollingVelocity() + 1; int bufferLeft = logoBuffersLeft[bufferIndex]; int bufferRight = logoBuffersRight[bufferIndex]; - if (logoCount == 1) + if (logoCount == 1 && mCamOffset == 0) { bufferLeft = 0; bufferRight = 0; @@ -537,6 +889,8 @@ void SystemView::renderInfoBar(const Transform4x4f& trans) mSystemInfo.render(trans); } +#include + // Draw background extras void SystemView::renderExtras(const Transform4x4f& trans, float lower, float upper) { @@ -546,6 +900,77 @@ void SystemView::renderExtras(const Transform4x4f& trans, float lower, float upp int bufferIndex = getScrollingVelocity() + 1; Renderer::pushClipRect(Vector2i::Zero(), Vector2i((int)mSize.x(), (int)mSize.y())); + + std::unordered_set allPaths; + std::unordered_set paths; + std::unordered_set allValues; + std::unordered_set values; + + if (mExtrasFadeOpacity && mExtrasFadeOldCursor >= 0 && mExtrasFadeOldCursor < mEntries.size() && mExtrasFadeOldCursor != mCursor) + { + // ExtrasFadeOpacity : Collect images paths & text values + // paths & values must have only the elements that are not common + if (mCursor >= 0 && mCursor < mEntries.size()) + { + for (GuiComponent* extra : mEntries.at(mCursor).data.backgroundExtras) + { + if (extra->getZIndex() < lower || extra->getZIndex() >= upper) + continue; + + if (extra->isStaticExtra()) + continue; + + std::string value = extra->getValue(); + if (extra->isKindOf()) + paths.insert(value); + else if (extra->isKindOf()) + values.insert(value); + } + + allValues = values; + allPaths = paths; + + for (GuiComponent* extra : mEntries.at(mExtrasFadeOldCursor).data.backgroundExtras) + { + if (extra->getZIndex() < lower || extra->getZIndex() >= upper) + continue; + + if (extra->isStaticExtra()) + continue; + + std::string value = extra->getValue(); + if (extra->isKindOf()) + paths.erase(value); + else if (extra->isKindOf()) + values.erase(value); + } + } + + Renderer::pushClipRect(Vector2i((int)trans.translation()[0], (int)trans.translation()[1]), Vector2i((int)mSize.x(), (int)mSize.y())); + + // ExtrasFadeOpacity : Render only items with different paths or values + for (GuiComponent* extra : mEntries.at(mExtrasFadeOldCursor).data.backgroundExtras) + { + if (extra->getZIndex() < lower || extra->getZIndex() >= upper) + continue; + + if (extra->isStaticExtra()) + continue; + + std::string value = extra->getValue(); + if (extra->isKindOf()) + { + if (allPaths.find(value) == allPaths.cend()) + extra->render(trans); + else if (((ImageComponent*)extra)->isTiled() && extra->getPosition() == Vector3f::Zero() && extra->getSize() == Vector2f(Renderer::getScreenWidth(), Renderer::getScreenHeight())) + extra->render(trans); + } + else if (extra->isKindOf() && allValues.find(value) == allValues.cend()) + extra->render(trans); + } + + Renderer::popClipRect(); + } for (int i = extrasCenter + logoBuffersLeft[bufferIndex]; i <= extrasCenter + logoBuffersRight[bufferIndex]; i++) { @@ -555,27 +980,118 @@ void SystemView::renderExtras(const Transform4x4f& trans, float lower, float upp while (index >= (int)mEntries.size()) index -= (int)mEntries.size(); + if (mExtrasFadeOpacity && (index == mExtrasFadeOldCursor || index != mCursor)) + continue; + //Only render selected system when not showing - if (mShowing || index == mCursor) + if (!mShowing && index != mCursor) + continue; + + Entry& entry = mEntries.at(index); + + Vector2i size = Vector2i(Math::round(mSize.x()), Math::round(mSize.y())); + + Transform4x4f extrasTrans = trans; + if (mCarousel.type == HORIZONTAL || mCarousel.type == HORIZONTAL_WHEEL) { - Transform4x4f extrasTrans = trans; - if (mCarousel.type == HORIZONTAL || mCarousel.type == HORIZONTAL_WHEEL) - extrasTrans.translate(Vector3f((i - mExtrasCamOffset) * mSize.x(), 0, 0)); - else - extrasTrans.translate(Vector3f(0, (i - mExtrasCamOffset) * mSize.y(), 0)); - - Renderer::pushClipRect(Vector2i((int)extrasTrans.translation()[0], (int)extrasTrans.translation()[1]), - Vector2i((int)mSize.x(), (int)mSize.y())); - SystemViewData data = mEntries.at(index).data; - for (unsigned int j = 0; j < data.backgroundExtras.size(); j++) { - GuiComponent *extra = data.backgroundExtras[j]; - if (extra->getZIndex() >= lower && extra->getZIndex() < upper) { - extra->render(extrasTrans); + extrasTrans.translate(Vector3f((i - mExtrasCamOffset) * mSize.x(), 0, 0)); + + if (extrasTrans.translation()[0] >= 0 && extrasTrans.translation()[0] <= Renderer::getScreenWidth() && extrasTrans.translation()[0] + mSize.x() > Renderer::getScreenWidth()) + size.x() = Renderer::getScreenWidth() - extrasTrans.translation()[0]; + } + else + { + extrasTrans.translate(Vector3f(0, (i - mExtrasCamOffset) * mSize.y(), 0)); + + if (extrasTrans.translation()[1] >= 0 && extrasTrans.translation()[1] <= Renderer::getScreenHeight() && extrasTrans.translation()[1] + mSize.y() > Renderer::getScreenHeight()) + size.y() = Renderer::getScreenHeight() - extrasTrans.translation()[1]; + } + + if (!Renderer::isVisibleOnScreen(extrasTrans.translation()[0], extrasTrans.translation()[1], mSize.x(), mSize.y())) + continue; + + if (mExtrasFadeOpacity && mExtrasFadeOldCursor == index) + extrasTrans = trans; + + Renderer::pushClipRect(Vector2i(Math::round(extrasTrans.translation()[0]), Math::round(extrasTrans.translation()[1])), Vector2i(Math::round(size.x()), Math::round(size.y()))); + + for (GuiComponent* extra : mEntries.at(index).data.backgroundExtras) + { + if (extra->getZIndex() < lower || extra->getZIndex() >= upper) + continue; + + // ExtrasFadeOpacity : Apply opacity only on elements that are not common with the original view + if (mExtrasFadeOpacity && !extra->isStaticExtra()) + { + std::string value = extra->getValue(); + if (extra->isKindOf()) + { + if (paths.find(value) != paths.cend()) + { + auto opa = extra->getOpacity(); + extra->setOpacity((1.0f - mExtrasFadeOpacity) * opa); + extra->render(extra->isStaticExtra() ? trans : extrasTrans); + extra->setOpacity(opa); + continue; + } + else if (((ImageComponent*)extra)->isTiled() && extra->getPosition() == Vector3f::Zero() && extra->getSize() == Vector2f(Renderer::getScreenWidth(), Renderer::getScreenHeight())) + { + auto opa = extra->getOpacity(); + extra->setOpacity((1.0f - mExtrasFadeOpacity) * opa); + extra->render(extra->isStaticExtra() ? trans : extrasTrans); + extra->setOpacity(opa); + continue; + } + } + else if (extra->isKindOf() && values.find(value) != values.cend()) + { + auto opa = extra->getOpacity(); + extra->setOpacity((1.0f - mExtrasFadeOpacity) * opa); + extra->render(extra->isStaticExtra() ? trans : extrasTrans); + extra->setOpacity(opa); + continue; + } + } + + if (extra->isStaticExtra()) + { + bool popClip = false; + + if (extrasTrans.translation()[0] > Renderer::getScreenWidth()) + continue; + else if (extrasTrans.translation()[1] > Renderer::getScreenHeight()) + continue; + else if (extrasTrans.translation()[0] < 0) + { + int x = Math::round(size.x() + extrasTrans.translation()[0]); + if (x == 0) + continue; + + Renderer::pushClipRect(Vector2i(0, Math::round(extrasTrans.translation()[1])), Vector2i(x, Math::round(size.y()))); + popClip = true; + } + else if (extrasTrans.translation()[1] < 0) + { + int y = Math::round(size.y() + extrasTrans.translation()[1]); + if (y == 0) + continue; + + Renderer::pushClipRect(Vector2i(Math::round(extrasTrans.translation()[0]), 0), Vector2i(Math::round(size.x()), y)); + popClip = true; } + + extra->render(trans); + + if (popClip) + Renderer::popClipRect(); } - Renderer::popClipRect(); + else + extra->render(extrasTrans); } + + Renderer::popClipRect(); } + Renderer::popClipRect(); } @@ -586,7 +1102,7 @@ void SystemView::renderFade(const Transform4x4f& trans) { unsigned int fadeColor = 0x00000000 | (unsigned char)(mExtrasFadeOpacity * 255); Renderer::setMatrix(trans); - Renderer::drawRect(0.0f, 0.0f, mSize.x(), mSize.y(), fadeColor, fadeColor); + //Renderer::drawRect(0.0f, 0.0f, mSize.x(), mSize.y(), fadeColor, fadeColor); } } @@ -611,8 +1127,12 @@ void SystemView::getDefaultElements(void) mCarousel.logoRotationOrigin.y() = 0.5; mCarousel.logoSize.x() = 0.25f * mSize.x(); mCarousel.logoSize.y() = 0.155f * mSize.y(); + mCarousel.logoPos = Vector2f(-1, -1); mCarousel.maxLogoCount = 3; mCarousel.zIndex = 40; + mCarousel.systemInfoDelay = 2000; + mCarousel.scrollSound = ""; + mCarousel.defaultTransition = ""; // System Info Bar mSystemInfo.setSize(mSize.x(), mSystemInfo.getFont()->getLetterHeight()*2.2f); @@ -623,6 +1143,18 @@ void SystemView::getDefaultElements(void) mSystemInfo.setColor(0x000000FF); mSystemInfo.setZIndex(50); mSystemInfo.setDefaultZIndex(50); + + if (mStaticBackground != nullptr) + { + delete mStaticBackground; + mStaticBackground = nullptr; + } + + if (mStaticVideoBackground != nullptr) + { + delete mStaticVideoBackground; + mStaticVideoBackground = nullptr; + } } void SystemView::getCarouselFromTheme(const ThemeData::ThemeElement* elem) @@ -652,11 +1184,13 @@ void SystemView::getCarouselFromTheme(const ThemeData::ThemeElement* elem) if (elem->has("colorEnd")) mCarousel.colorEnd = elem->get("colorEnd"); if (elem->has("gradientType")) - mCarousel.colorGradientHorizontal = !(elem->get("gradientType").compare("horizontal")); + mCarousel.colorGradientHorizontal = elem->get("gradientType").compare("horizontal"); if (elem->has("logoScale")) mCarousel.logoScale = elem->get("logoScale"); if (elem->has("logoSize")) mCarousel.logoSize = elem->get("logoSize") * mSize; + if (elem->has("logoPos")) + mCarousel.logoPos = elem->get("logoPos") * mSize; if (elem->has("maxLogoCount")) mCarousel.maxLogoCount = (int)Math::round(elem->get("maxLogoCount")); if (elem->has("zIndex")) @@ -678,14 +1212,91 @@ void SystemView::getCarouselFromTheme(const ThemeData::ThemeElement* elem) else mCarousel.logoAlignment = ALIGN_CENTER; } + + if (elem->has("systemInfoDelay")) + mCarousel.systemInfoDelay = elem->get("systemInfoDelay"); + + if (elem->has("scrollSound")) + mCarousel.scrollSound = elem->get("scrollSound"); + + if (elem->has("defaultTransition")) + mCarousel.defaultTransition = elem->get("defaultTransition"); } void SystemView::onShow() { + GuiComponent::onShow(); mShowing = true; + activateExtras(mCursor); + + if (mStaticVideoBackground) + mStaticVideoBackground->onShow(); } void SystemView::onHide() { + GuiComponent::onHide(); mShowing = false; + updateExtras([this](GuiComponent* p) { p->onHide(); }); + + if (mStaticVideoBackground) + mStaticVideoBackground->onHide(); +} + +void SystemView::onScreenSaverActivate() +{ + mScreensaverActive = true; + updateExtras([this](GuiComponent* p) { p->onScreenSaverActivate(); }); + + if (mStaticVideoBackground) + mStaticVideoBackground->onScreenSaverActivate(); +} + +void SystemView::onScreenSaverDeactivate() +{ + mScreensaverActive = false; + updateExtras([this](GuiComponent* p) { p->onScreenSaverDeactivate(); }); + + if (mStaticVideoBackground) + mStaticVideoBackground->onScreenSaverDeactivate(); +} + +void SystemView::topWindow(bool isTop) +{ + mDisable = !isTop; + updateExtras([this, isTop](GuiComponent* p) { p->topWindow(isTop); }); + + if (mStaticVideoBackground) + mStaticVideoBackground->topWindow(isTop); +} + +void SystemView::updateExtras(const std::function& func) +{ + for (int i = 0; i < mEntries.size(); i++) + { + SystemViewData data = mEntries.at(i).data; + for (unsigned int j = 0; j < data.backgroundExtras.size(); j++) + { + GuiComponent* extra = data.backgroundExtras[j]; + func(extra); + } + } +} + +void SystemView::activateExtras(int cursor, bool activate) +{ + if (cursor < 0 || cursor >= mEntries.size()) + return; + + bool show = activate && mShowing && !mScreensaverActive && !mDisable; + + SystemViewData data = mEntries.at(cursor).data; + for (unsigned int j = 0; j < data.backgroundExtras.size(); j++) + { + GuiComponent *extra = data.backgroundExtras[j]; + if (show && activate) + extra->onShow(); + else + extra->onHide(); + } } diff --git a/es-app/src/views/SystemView.h b/es-app/src/views/SystemView.h index 85b0aca44f..2b431ea4b2 100644 --- a/es-app/src/views/SystemView.h +++ b/es-app/src/views/SystemView.h @@ -7,9 +7,11 @@ #include "resources/Font.h" #include "GuiComponent.h" #include +#include class AnimatedImageComponent; class SystemData; +class VideoVlcComponent; enum CarouselType : unsigned int { @@ -20,7 +22,7 @@ enum CarouselType : unsigned int }; struct SystemViewData -{ +{ std::shared_ptr logo; std::vector backgroundExtras; }; @@ -40,13 +42,19 @@ struct SystemViewCarousel bool colorGradientHorizontal; int maxLogoCount; // number of logos shown on the carousel Vector2f logoSize; + Vector2f logoPos; float zIndex; + float systemInfoDelay; + + std::string defaultTransition; + std::string scrollSound; }; class SystemView : public IList { public: SystemView(Window* window); + ~SystemView(); virtual void onShow() override; virtual void onHide() override; @@ -66,6 +74,14 @@ class SystemView : public IList void onCursorChanged(const CursorState& state) override; private: + void activateExtras(int cursor, bool activate = true); + void updateExtras(const std::function& func); + void clearEntries(); + + virtual void onScreenSaverActivate() override; + virtual void onScreenSaverDeactivate() override; + virtual void topWindow(bool isTop) override; + void populate(); void getViewElements(const std::shared_ptr& theme); void getDefaultElements(void); @@ -77,16 +93,24 @@ class SystemView : public IList void renderFade(const Transform4x4f& trans); - SystemViewCarousel mCarousel; - TextComponent mSystemInfo; + SystemViewCarousel mCarousel; + TextComponent mSystemInfo; + SystemData* mLastSystem; + ImageComponent* mStaticBackground; + VideoVlcComponent* mStaticVideoBackground; // unit is list index float mCamOffset; float mExtrasCamOffset; float mExtrasFadeOpacity; + int mExtrasFadeOldCursor; bool mViewNeedsReload; bool mShowing; + bool mDisable; + bool mScreensaverActive; + + int mLastCursor; }; #endif // ES_APP_VIEWS_SYSTEM_VIEW_H diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index 3489628f6c..ed317203ac 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -16,6 +16,9 @@ #include "Settings.h" #include "SystemData.h" #include "Window.h" +#include "AudioManager.h" +#include "utils/ThreadPool.h" +#include ViewController* ViewController::sInstance = NULL; @@ -43,16 +46,23 @@ ViewController::~ViewController() sInstance = NULL; } -void ViewController::goToStart() +void ViewController::goToStart(bool forceImmediate) { + bool hideSystemView = Settings::getInstance()->getBool("HideSystemView"); + bool startOnGamelist = Settings::getInstance()->getBool("StartupOnGameList"); + // If specific system is requested, go directly to the game list auto requestedSystem = Settings::getInstance()->getString("StartupSystem"); if("" != requestedSystem && "retropie" != requestedSystem) { for(auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++){ - if ((*it)->getName() == requestedSystem) + if ((*it)->getName() == requestedSystem && !(*it)->isGroupChildSystem()) { - goToGameList(*it); + if (hideSystemView || startOnGamelist) + goToGameList(*it, forceImmediate); + else + goToSystemView(*it, forceImmediate); + return; } } @@ -60,14 +70,17 @@ void ViewController::goToStart() // Requested system doesn't exist Settings::getInstance()->setString("StartupSystem", ""); } - goToSystemView(SystemData::sSystemVector.at(0)); + + if (hideSystemView || startOnGamelist) + goToGameList(SystemData::sSystemVector.at(0), forceImmediate); + else + goToSystemView(SystemData::sSystemVector.at(0), forceImmediate); } void ViewController::ReloadAndGoToStart() { - mWindow->renderLoadingScreen("Loading..."); ViewController::get()->reloadAll(); - ViewController::get()->goToStart(); + ViewController::get()->goToStart(false); } int ViewController::getSystemId(SystemData* system) @@ -76,7 +89,7 @@ int ViewController::getSystemId(SystemData* system) return (int)(std::find(sysVec.cbegin(), sysVec.cend(), system) - sysVec.cbegin()); } -void ViewController::goToSystemView(SystemData* system) +void ViewController::goToSystemView(SystemData* system, bool forceImmediate) { // Tell any current view it's about to be hidden if (mCurrentView) @@ -95,7 +108,7 @@ void ViewController::goToSystemView(SystemData* system) mCurrentView->onShow(); PowerSaver::setState(true); - playViewTransition(); + playViewTransition(forceImmediate); } void ViewController::goToNextGameList() @@ -103,7 +116,11 @@ void ViewController::goToNextGameList() assert(mState.viewing == GAME_LIST); SystemData* system = getState().getSystem(); assert(system); - goToGameList(system->getNext()); + + system = system->getNext(); + goToGameList(system); + + AudioManager::getInstance()->themeChanged(system->getTheme()); } void ViewController::goToPrevGameList() @@ -111,10 +128,14 @@ void ViewController::goToPrevGameList() assert(mState.viewing == GAME_LIST); SystemData* system = getState().getSystem(); assert(system); - goToGameList(system->getPrev()); + + system = system->getPrev(); + goToGameList(system); + + AudioManager::getInstance()->themeChanged(system->getTheme()); } -void ViewController::goToGameList(SystemData* system) +void ViewController::goToGameList(SystemData* system, bool forceImmediate) { if(mState.viewing == SYSTEM_SELECT) { @@ -131,18 +152,16 @@ void ViewController::goToGameList(SystemData* system) mState.system = system; if (mCurrentView) - { mCurrentView->onHide(); - } + mCurrentView = getGameListView(system); if (mCurrentView) - { mCurrentView->onShow(); - } - playViewTransition(); + + playViewTransition(forceImmediate); } -void ViewController::playViewTransition() +void ViewController::playViewTransition(bool forceImmediate) { Vector3f target(Vector3f::Zero()); if(mCurrentView) @@ -153,7 +172,7 @@ void ViewController::playViewTransition() return; std::string transition_style = Settings::getInstance()->getString("TransitionStyle"); - if(transition_style == "fade") + if (!forceImmediate && transition_style == "fade") { // fade // stop whatever's currently playing, leaving mFadeOpacity wherever it is @@ -172,20 +191,24 @@ void ViewController::playViewTransition() }); // fast-forward animation if we're partway faded - if(target == -mCamera.translation()) + if (target == -mCamera.translation()) { // not changing screens, so cancel the first half entirely advanceAnimation(0, FADE_DURATION); advanceAnimation(0, FADE_WAIT); advanceAnimation(0, FADE_DURATION - (int)(mFadeOpacity * FADE_DURATION)); - }else{ - advanceAnimation(0, (int)(mFadeOpacity * FADE_DURATION)); } - } else if (transition_style == "slide"){ + else + advanceAnimation(0, (int)(mFadeOpacity * FADE_DURATION)); + } + else if (!forceImmediate && (transition_style == "slide" || transition_style == "auto")) + { // slide or simple slide setAnimation(new MoveCameraAnimation(mCamera, target)); updateHelpPrompts(); // update help prompts immediately - } else { + } + else + { // instant setAnimation(new LambdaAnimation( [this, target](float /*t*/) @@ -212,8 +235,8 @@ void ViewController::launch(FileData* game, Vector3f center) } // Hide the current view - if (mCurrentView) - mCurrentView->onHide(); + //if (mCurrentView) + //mCurrentView->onHide(); Transform4x4f origCamera = mCamera; origCamera.translation() = -mCurrentView->getPosition(); @@ -223,7 +246,9 @@ void ViewController::launch(FileData* game, Vector3f center) mWindow->stopInfoPopup(); // make sure we disable any existing info popup mLockInput = true; - std::string transition_style = Settings::getInstance()->getString("TransitionStyle"); + mWindow->loadCustomImageLoadingScreen(game->getImagePath(), game->getName()); + + std::string transition_style = Settings::getInstance()->getString("GameTransitionStyle"); if(transition_style == "fade") { // fade out, launch game, fade back in @@ -233,16 +258,16 @@ void ViewController::launch(FileData* game, Vector3f center) setAnimation(new LambdaAnimation(fadeFunc, 800), 0, [this, game, fadeFunc] { game->launchGame(mWindow); - setAnimation(new LambdaAnimation(fadeFunc, 800), 0, [this] { mLockInput = false; }, true); - this->onFileChanged(game, FILE_METADATA_CHANGED); + setAnimation(new LambdaAnimation(fadeFunc, 800), 0, [this] { mLockInput = false; mWindow->endRenderLoadingScreen(); }, true); + this->onFileChanged(game, FILE_METADATA_CHANGED); }); } else if (transition_style == "slide"){ // move camera to zoom in on center + fade out, launch game, come back in setAnimation(new LaunchAnimation(mCamera, mFadeOpacity, center, 1500), 0, [this, origCamera, center, game] { - game->launchGame(mWindow); + game->launchGame(mWindow); mCamera = origCamera; - setAnimation(new LaunchAnimation(mCamera, mFadeOpacity, center, 600), 0, [this] { mLockInput = false; }, true); + setAnimation(new LaunchAnimation(mCamera, mFadeOpacity, center, 600), 0, [this] { mLockInput = false; mWindow->endRenderLoadingScreen(); }, true); this->onFileChanged(game, FILE_METADATA_CHANGED); }); } else { // instant @@ -250,7 +275,7 @@ void ViewController::launch(FileData* game, Vector3f center) { game->launchGame(mWindow); mCamera = origCamera; - setAnimation(new LaunchAnimation(mCamera, mFadeOpacity, center, 10), 0, [this] { mLockInput = false; }, true); + setAnimation(new LaunchAnimation(mCamera, mFadeOpacity, center, 10), 0, [this] { mLockInput = false; mWindow->endRenderLoadingScreen(); }, true); this->onFileChanged(game, FILE_METADATA_CHANGED); }); } @@ -267,48 +292,115 @@ void ViewController::removeGameListView(SystemData* system) } } -std::shared_ptr ViewController::getGameListView(SystemData* system) +std::shared_ptr ViewController::getGameListView(SystemData* system, bool loadIfnull) { //if we already made one, return that one auto exists = mGameListViews.find(system); if(exists != mGameListViews.cend()) return exists->second; - system->getIndex()->setUIModeFilters(); + if (!loadIfnull) + return nullptr; + + system->setUIModeFilters(); + system->updateDisplayedGameCount(); + //if we didn't, make it, remember it, and return it std::shared_ptr view; bool themeHasVideoView = system->getTheme()->hasView("video"); + bool themeHasGridView = system->getTheme()->hasView("grid"); //decide type GameListViewType selectedViewType = AUTOMATIC; + bool allowDetailedDowngrade = false; + + bool forceView = false; std::string viewPreference = Settings::getInstance()->getString("GamelistViewStyle"); + if (!system->getTheme()->hasView(viewPreference)) + viewPreference = "automatic"; + + std::string customThemeName; + Vector2f gridSizeOverride = Vector2f::parseString(Settings::getInstance()->getString("DefaultGridSize")); + + + if (viewPreference != "automatic" && !system->getSystemViewMode().empty() && system->getTheme()->hasView(system->getSystemViewMode()) && system->getSystemViewMode() != viewPreference) + gridSizeOverride = Vector2f(0, 0); + + Vector2f bySystemGridOverride = system->getGridSizeOverride(); //Vector2f(0,0); + if (bySystemGridOverride != Vector2f(0, 0)) + gridSizeOverride = bySystemGridOverride; + + if (!system->getSystemViewMode().empty() && system->getTheme()->hasView(system->getSystemViewMode())) + { + viewPreference = system->getSystemViewMode(); + forceView = true; + } + + if (viewPreference == "automatic") + { + auto defaultView = system->getTheme()->getDefaultView(); + if (!defaultView.empty() && system->getTheme()->hasView(defaultView)) + viewPreference = defaultView; + } + + if (system->getTheme()->isCustomView(viewPreference)) + { + auto baseClass = system->getTheme()->getCustomViewBaseType(viewPreference); + if (!baseClass.empty()) // this is a customView + { + customThemeName = viewPreference; + viewPreference = baseClass; + } + } + if (viewPreference.compare("basic") == 0) selectedViewType = BASIC; - if (viewPreference.compare("detailed") == 0) + else if (viewPreference.compare("detailed") == 0) + { + allowDetailedDowngrade = true; selectedViewType = DETAILED; - if (viewPreference.compare("grid") == 0) + } + else if (themeHasGridView && viewPreference.compare("grid") == 0) selectedViewType = GRID; - if (viewPreference.compare("video") == 0) + else if (viewPreference.compare("video") == 0) selectedViewType = VIDEO; - if (selectedViewType == AUTOMATIC) - { - std::vector files = system->getRootFolder()->getFilesRecursive(GAME | FOLDER); - for (auto it = files.cbegin(); it != files.cend(); it++) + if (!forceView && (selectedViewType == AUTOMATIC || allowDetailedDowngrade)) + { + selectedViewType = BASIC; + + if (system->getTheme()->getDefaultView() != "basic") { - if (themeHasVideoView && !(*it)->getVideoPath().empty()) + std::vector files = system->getRootFolder()->getFilesRecursive(GAME | FOLDER); + for (auto it = files.cbegin(); it != files.cend(); it++) { - selectedViewType = VIDEO; - break; + if (themeHasVideoView && !(*it)->getVideoPath().empty() && viewPreference.compare("detailed") != 0) + { + selectedViewType = VIDEO; + break; + } + else if (!(*it)->getThumbnailPath().empty()) + { + /* + if (!allowDetailedDowngrade && (*it)->metadata.get("thumbnail").length() > 0) + { + if (themeHasGridView) + selectedViewType = GRID; + else + selectedViewType = DETAILED; + } + else*/ + selectedViewType = DETAILED; + + if (!themeHasVideoView) + break; + + // Don't break out in case any subsequent files have video + } } - else if (!(*it)->getThumbnailPath().empty()) - { - selectedViewType = DETAILED; - // Don't break out in case any subsequent files have video - } - } + } } // Create the view @@ -320,17 +412,28 @@ std::shared_ptr ViewController::getGameListView(SystemData* syste case DETAILED: view = std::shared_ptr(new DetailedGameListView(mWindow, system->getRootFolder())); break; - case GRID: - view = std::shared_ptr(new GridGameListView(mWindow, system->getRootFolder())); + case GRID: + { + view = std::shared_ptr(new GridGameListView(mWindow, system->getRootFolder(), system->getTheme(), customThemeName, gridSizeOverride)); + } + break; case BASIC: default: view = std::shared_ptr(new BasicGameListView(mWindow, system->getRootFolder())); break; } + + if (selectedViewType != GRID) + { + // GridGameListView theme needs to be loaded before populating. - view->setTheme(system->getTheme()); + if (!customThemeName.empty()) + view->setThemeName(customThemeName); + view->setTheme(system->getTheme()); + } + std::vector& sysVec = SystemData::sSystemVector; int id = (int)(std::find(sysVec.cbegin(), sysVec.cend(), system) - sysVec.cbegin()); view->setPosition(id * (float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight() * 2); @@ -356,9 +459,26 @@ std::shared_ptr ViewController::getSystemListView() bool ViewController::input(InputConfig* config, Input input) { - if(mLockInput) + if (mLockInput) return true; + + if (config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_F5) + { + mWindow->render(); + + ResourceManager::getInstance()->unloadAll(); + ResourceManager::getInstance()->reloadAll(); + +#if WIN32 + EsLocale::reset(); +#endif + + ViewController::get()->reloadAll(mWindow); + mWindow->endRenderLoadingScreen(); + return true; + } + // open menu if(!UIModeController::getInstance()->isUIModeKid() && config->isMappedTo("start", input) && input.value != 0) { @@ -372,7 +492,7 @@ bool ViewController::input(InputConfig* config, Input input) return true; } - if(mCurrentView) + if (mCurrentView) return mCurrentView->input(config, input); return false; @@ -381,9 +501,7 @@ bool ViewController::input(InputConfig* config, Input input) void ViewController::update(int deltaTime) { if(mCurrentView) - { mCurrentView->update(deltaTime); - } updateSelf(deltaTime); } @@ -401,9 +519,14 @@ void ViewController::render(const Transform4x4f& parentTrans) // Keep track of UI mode changes. UIModeController::getInstance()->monitorUIMode(); - // draw systemview - getSystemListView()->render(trans); + // clipping + Vector3f sysStart = getSystemListView()->getPosition(); + Vector3f sysEnd = getSystemListView()->getPosition() + Vector3f(getSystemListView()->getSize().x(), getSystemListView()->getSize().y(), 0); + // draw systemview + if (!Settings::getInstance()->getBool("HideSystemView")) + getSystemListView()->render(trans); + // draw gamelists for(auto it = mGameListViews.cbegin(); it != mGameListViews.cend(); it++) { @@ -411,8 +534,7 @@ void ViewController::render(const Transform4x4f& parentTrans) Vector3f guiStart = it->second->getPosition(); Vector3f guiEnd = it->second->getPosition() + Vector3f(it->second->getSize().x(), it->second->getSize().y(), 0); - if(guiEnd.x() >= viewStart.x() && guiEnd.y() >= viewStart.y() && - guiStart.x() <= viewEnd.x() && guiStart.y() <= viewEnd.y()) + if (guiEnd.x() > viewStart.x() && guiEnd.y() >= viewStart.y() && guiStart.x() < viewEnd.x() && guiStart.y() <= viewEnd.y()) it->second->render(trans); } @@ -420,85 +542,161 @@ void ViewController::render(const Transform4x4f& parentTrans) mWindow->renderHelpPromptsEarly(); // fade out - if(mFadeOpacity) + if (mFadeOpacity) { - unsigned int fadeColor = 0x00000000 | (unsigned char)(mFadeOpacity * 255); - Renderer::setMatrix(parentTrans); - Renderer::drawRect(0.0f, 0.0f, Renderer::getScreenWidth(), Renderer::getScreenHeight(), fadeColor, fadeColor); + if (Settings::getInstance()->getBool("HideWindow")) + { + Renderer::setMatrix(parentTrans); + Renderer::drawRect(0.0f, 0.0f, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x00000000 | (unsigned char)(mFadeOpacity * 255)); + } + else + mWindow->renderGameLoadingScreen(mFadeOpacity, false); } } void ViewController::preload() { - uint32_t i = 0; + int i = 1; + int max = SystemData::sSystemVector.size() + 1; + + bool splash = Settings::getInstance()->getBool("SplashScreen") && Settings::getInstance()->getBool("SplashScreenProgress"); + if (splash) + mWindow->renderLoadingScreen(_("Preloading UI"), (float)i / (float)max); + for(auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) - { - if(Settings::getInstance()->getBool("SplashScreen") && - Settings::getInstance()->getBool("SplashScreenProgress")) + { + if ((*it)->isGroupChildSystem()) + continue; + + if (splash) { i++; - char buffer[100]; - sprintf (buffer, "Loading '%s' (%d/%d)", - (*it)->getFullName().c_str(), i, (int)SystemData::sSystemVector.size()); - mWindow->renderLoadingScreen(std::string(buffer)); + mWindow->renderLoadingScreen(_("Preloading UI"), (float) i / (float)max); } - (*it)->getIndex()->resetFilters(); + (*it)->resetFilters(); getGameListView(*it); } + + // First load the system list + getSystemListView(); } void ViewController::reloadGameListView(IGameListView* view, bool reloadTheme) { + if (reloadTheme) + ThemeData::setDefaultTheme(nullptr); + for(auto it = mGameListViews.cbegin(); it != mGameListViews.cend(); it++) { if(it->second.get() == view) { + + // addPlaceholder() + bool isCurrent = (mCurrentView == it->second); + SystemData* system = it->first; + FileData* cursor = view->getCursor(); + mGameListViews.erase(it); - if(reloadTheme) + if (reloadTheme) system->loadTheme(); - system->getIndex()->setUIModeFilters(); - std::shared_ptr newView = getGameListView(system); + system->setUIModeFilters(); + system->updateDisplayedGameCount(); + + std::shared_ptr newView = getGameListView(system); + // to counter having come from a placeholder - if (!cursor->isPlaceHolder()) { + if (cursor != nullptr && !cursor->isPlaceHolder() && system->getName() != "recent") newView->setCursor(cursor); - } + if(isCurrent) mCurrentView = newView; break; } } + + if (SystemData::sSystemVector.size() > 0 && reloadTheme) + ViewController::get()->onThemeChanged(SystemData::sSystemVector.at(0)->getTheme()); + // Redisplay the current view if (mCurrentView) mCurrentView->onShow(); - } -void ViewController::reloadAll() +void ViewController::reloadAll(Window* window) { + Utils::FileSystem::FileSystemCacheActivator fsc; + + ThemeData::setDefaultTheme(nullptr); + + SystemData* system = nullptr; + + if (mState.viewing == SYSTEM_SELECT) + { + int idx = mSystemListView->getCursorIndex(); + if (idx >= 0 && idx < SystemData::sSystemVector.size()) + system = SystemData::sSystemVector[mSystemListView->getCursorIndex()]; + else + system = mState.getSystem(); + } + // clear all gamelistviews std::map cursorMap; + for(auto it = mGameListViews.cbegin(); it != mGameListViews.cend(); it++) - { cursorMap[it->first] = it->second->getCursor(); - } + mGameListViews.clear(); + for (auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) + { + if (cursorMap.find((*it)) == cursorMap.end()) + cursorMap[(*it)] = NULL; + } + + float idx = 0; + + if (window) + window->renderLoadingScreen(_("Loading theme...")); + + Utils::ThreadPool pool; + + for (auto it = cursorMap.cbegin(); it != cursorMap.cend(); it++) + { + auto system = it->first; + pool.queueWorkItem([system] + { + system->loadTheme(); + system->resetFilters(); + }); + } + + pool.wait(); // load themes, create gamelistviews and reset filters for(auto it = cursorMap.cbegin(); it != cursorMap.cend(); it++) { - it->first->loadTheme(); - it->first->getIndex()->resetFilters(); - getGameListView(it->first)->setCursor(it->second); + // it->first->loadTheme(); + // it->first->resetFilters(); + + if (it->second != NULL) + getGameListView(it->first)->setCursor(it->second); + + idx++; + + if (window) + window->renderLoadingScreen(_("Loading..."), (float)idx / (float)cursorMap.size()); } + if (SystemData::sSystemVector.size() > 0) + ViewController::get()->onThemeChanged(SystemData::sSystemVector.at(0)->getTheme()); + // Rebuild SystemListView mSystemListView.reset(); getSystemListView(); @@ -507,15 +705,15 @@ void ViewController::reloadAll() if(mState.viewing == GAME_LIST) { mCurrentView = getGameListView(mState.getSystem()); - }else if(mState.viewing == SYSTEM_SELECT) - { - SystemData* system = mState.getSystem(); - goToSystemView(SystemData::sSystemVector.front()); + } + else if(mState.viewing == SYSTEM_SELECT && system != nullptr) + { + goToSystemView(SystemData::sSystemVector.front(), false); mSystemListView->goToSystem(system, false); mCurrentView = mSystemListView; - }else{ - goToSystemView(SystemData::sSystemVector.front()); } + else + goToSystemView(SystemData::sSystemVector.front(), false); updateHelpPrompts(); } @@ -540,3 +738,32 @@ HelpStyle ViewController::getHelpStyle() return mCurrentView->getHelpStyle(); } + + +void ViewController::onThemeChanged(const std::shared_ptr& theme) +{ + ThemeData::setDefaultTheme(theme.get()); + mWindow->onThemeChanged(theme); +} + +void ViewController::onShow() +{ + if (mCurrentView) + mCurrentView->onShow(); +} + +void ViewController::onScreenSaverActivate() +{ + GuiComponent::onScreenSaverActivate(); + + if (mCurrentView) + mCurrentView->onScreenSaverActivate(); +} + +void ViewController::onScreenSaverDeactivate() +{ + GuiComponent::onScreenSaverDeactivate(); + + if (mCurrentView) + mCurrentView->onScreenSaverDeactivate(); +} diff --git a/es-app/src/views/ViewController.h b/es-app/src/views/ViewController.h index c5a4d3acaf..925036cd0c 100644 --- a/es-app/src/views/ViewController.h +++ b/es-app/src/views/ViewController.h @@ -10,6 +10,7 @@ class IGameListView; class SystemData; class SystemView; +class Window; // Used to smoothly transition the camera between multiple views (e.g. from system to system, from gamelist to gamelist). class ViewController : public GuiComponent @@ -28,14 +29,14 @@ class ViewController : public GuiComponent // the current gamelist view (as it may change to be detailed). void reloadGameListView(IGameListView* gamelist, bool reloadTheme = false); inline void reloadGameListView(SystemData* system, bool reloadTheme = false) { reloadGameListView(getGameListView(system).get(), reloadTheme); } - void reloadAll(); // Reload everything with a theme. Used when the "ThemeSet" setting changes. + void reloadAll(Window* window = nullptr); // Reload everything with a theme. Used when the "ThemeSet" setting changes. // Navigation. void goToNextGameList(); void goToPrevGameList(); - void goToGameList(SystemData* system); - void goToSystemView(SystemData* system); - void goToStart(); + void goToGameList(SystemData* system, bool forceImmediate = false); + void goToSystemView(SystemData* system, bool forceImmediate = false); + void goToStart(bool forceImmediate = false); void ReloadAndGoToStart(); void onFileChanged(FileData* file, FileChangeType change); @@ -62,7 +63,8 @@ class ViewController : public GuiComponent BASIC, DETAILED, GRID, - VIDEO + VIDEO + //,GRIDEX }; struct State @@ -81,15 +83,21 @@ class ViewController : public GuiComponent virtual std::vector getHelpPrompts() override; virtual HelpStyle getHelpStyle() override; - std::shared_ptr getGameListView(SystemData* system); + std::shared_ptr getGameListView(SystemData* system, bool loadIfnull = true); std::shared_ptr getSystemListView(); void removeGameListView(SystemData* system); + void onThemeChanged(const std::shared_ptr& theme); + + virtual void onShow() override; + virtual void onScreenSaverActivate(); + virtual void onScreenSaverDeactivate(); + private: ViewController(Window* window); static ViewController* sInstance; - void playViewTransition(); + void playViewTransition(bool forceImmediate); int getSystemId(SystemData* system); std::shared_ptr mCurrentView; diff --git a/es-app/src/views/gamelist/BasicGameListView.cpp b/es-app/src/views/gamelist/BasicGameListView.cpp index 899f12578c..d4db0b54dc 100644 --- a/es-app/src/views/gamelist/BasicGameListView.cpp +++ b/es-app/src/views/gamelist/BasicGameListView.cpp @@ -7,15 +7,35 @@ #include "Settings.h" #include "SystemData.h" -BasicGameListView::BasicGameListView(Window* window, FileData* root) +BasicGameListView::BasicGameListView(Window* window, FolderData* root) : ISimpleGameListView(window, root), mList(window) { + mLoaded = false; + mList.setSize(mSize.x(), mSize.y() * 0.8f); mList.setPosition(0, mSize.y() * 0.2f); mList.setDefaultZIndex(20); - addChild(&mList); + addChild(&mList); + + populateList(mRoot->getChildrenListToDisplay()); +} + +void BasicGameListView::onShow() +{ + /* + if (!mLoaded) + { + populateList(mRoot->getChildrenListToDisplay()); + mLoaded = true; + } + */ + ISimpleGameListView::onShow(); +} - populateList(root->getChildrenListToDisplay()); +void BasicGameListView::setThemeName(std::string name) +{ + ISimpleGameListView::setThemeName(name); + // mGrid.setThemeName(getName()); } void BasicGameListView::onThemeChanged(const std::shared_ptr& theme) @@ -29,7 +49,7 @@ void BasicGameListView::onThemeChanged(const std::shared_ptr& theme) void BasicGameListView::onFileChanged(FileData* file, FileChangeType change) { - if(change == FILE_METADATA_CHANGED) + if (change == FILE_METADATA_CHANGED) { // might switch to a detailed view ViewController::get()->reloadGameListView(this); @@ -42,12 +62,59 @@ void BasicGameListView::onFileChanged(FileData* file, FileChangeType change) void BasicGameListView::populateList(const std::vector& files) { mList.clear(); - mHeaderText.setText(mRoot->getSystem()->getFullName()); + + std::string systemName = mRoot->getSystem()->getFullName(); + mHeaderText.setText(systemName); + + std::string systemShortName = mRoot->getSystem()->getName(); + + bool favoritesFirst = Settings::getInstance()->getBool("FavoritesFirst"); + bool showFavoriteIcon = (systemName != "favorites" && systemShortName != "recent"); + if (!showFavoriteIcon) + favoritesFirst = false; + if (files.size() > 0) { - for(auto it = files.cbegin(); it != files.cend(); it++) + if (mCursorStack.size()) { - mList.add((*it)->getName(), *it, ((*it)->getType() == FOLDER)); + FileData* placeholder = new FileData(PLACEHOLDER, "..", this->mRoot->getSystem()); + mList.add(". .", placeholder, (placeholder->getType() == PLACEHOLDER)); + } + + if (favoritesFirst) + { + for (auto file : files) + { + if (!file->getFavorite()) + continue; + + if (showFavoriteIcon) + mList.add(_U("\uF006 ") + file->getName(), file, file->getType() == FOLDER); + else if (file->getType() == FOLDER) + mList.add(_U("\uF114 ") + file->getName(), file, true); + else + mList.add(file->getName(), file, false); + } + } + + for (auto file : files) + { + if (file->getFavorite()) + { + if (favoritesFirst) + continue; + + if (showFavoriteIcon) + { + mList.add(_U("\uF006 ") + file->getName(), file, file->getType() == FOLDER); + continue; + } + } + + if (file->getType() == FOLDER) + mList.add(_U("\uF114 ") + file->getName(), file, true); + else + mList.add(file->getName(), file, false); } } else @@ -58,6 +125,9 @@ void BasicGameListView::populateList(const std::vector& files) FileData* BasicGameListView::getCursor() { + if (mList.size() == 0) + return nullptr; + return mList.getSelected(); } @@ -65,36 +135,44 @@ void BasicGameListView::setCursor(FileData* cursor) { if(!mList.setCursor(cursor) && (!cursor->isPlaceHolder())) { - populateList(cursor->getParent()->getChildrenListToDisplay()); - mList.setCursor(cursor); + auto children = mRoot->getChildrenListToDisplay(); - // update our cursor stack in case our cursor just got set to some folder we weren't in before - if(mCursorStack.empty() || mCursorStack.top() != cursor->getParent()) + auto gameIter = std::find(children.cbegin(), children.cend(), cursor); + if (gameIter == children.cend()) { - std::stack tmp; - FileData* ptr = cursor->getParent(); - while(ptr && ptr != mRoot) - { - tmp.push(ptr); - ptr = ptr->getParent(); - } + children = cursor->getParent()->getChildrenListToDisplay(); - // flip the stack and put it in mCursorStack - mCursorStack = std::stack(); - while(!tmp.empty()) + // update our cursor stack in case our cursor just got set to some folder we weren't in before + if (mCursorStack.empty() || mCursorStack.top() != cursor->getParent()) { - mCursorStack.push(tmp.top()); - tmp.pop(); + std::stack tmp; + FileData* ptr = cursor->getParent(); + while (ptr && ptr != mRoot) + { + tmp.push(ptr); + ptr = ptr->getParent(); + } + + // flip the stack and put it in mCursorStack + mCursorStack = std::stack(); + while (!tmp.empty()) + { + mCursorStack.push(tmp.top()); + tmp.pop(); + } } } + + populateList(children); + mList.setCursor(cursor); } } void BasicGameListView::addPlaceholder() { // empty list - add a placeholder - FileData* placeholder = new FileData(PLACEHOLDER, "", this->mRoot->getSystem()->getSystemEnvData(), this->mRoot->getSystem()); - mList.add(placeholder->getName(), placeholder, (placeholder->getType() == PLACEHOLDER)); + FileData* placeholder = new FileData(PLACEHOLDER, "<" + _("No Entries Found") + ">", mRoot->getSystem()); + mList.add(placeholder->getName(), placeholder, true); } std::string BasicGameListView::getQuickSystemSelectRightButton() @@ -116,7 +194,8 @@ void BasicGameListView::remove(FileData *game, bool deleteFile) { if (deleteFile) Utils::FileSystem::removeFile(game->getPath()); // actually delete the file on the filesystem - FileData* parent = game->getParent(); + + FolderData* parent = game->getParent(); if (getCursor() == game) // Select next element in list, or prev if none { std::vector siblings = parent->getChildrenListToDisplay(); @@ -146,18 +225,30 @@ std::vector BasicGameListView::getHelpPrompts() std::vector prompts; if(Settings::getInstance()->getBool("QuickSystemSelect")) - prompts.push_back(HelpPrompt("left/right", "system")); - prompts.push_back(HelpPrompt("up/down", "choose")); - prompts.push_back(HelpPrompt("a", "launch")); - prompts.push_back(HelpPrompt("b", "back")); + prompts.push_back(HelpPrompt("left/right", _("SYSTEM"))); + + prompts.push_back(HelpPrompt("up/down", _("CHOOSE"))); + prompts.push_back(HelpPrompt("a", _("LAUNCH"))); + prompts.push_back(HelpPrompt("b", _("BACK"))); + if(!UIModeController::getInstance()->isUIModeKid()) - prompts.push_back(HelpPrompt("select", "options")); + prompts.push_back(HelpPrompt("select", _("options"))); + if(mRoot->getSystem()->isGameSystem()) - prompts.push_back(HelpPrompt("x", "random")); + prompts.push_back(HelpPrompt("x", _("RANDOM"))); + if(mRoot->getSystem()->isGameSystem() && !UIModeController::getInstance()->isUIModeKid()) { std::string prompt = CollectionSystemManager::get()->getEditingCollection(); + if (prompt == "Favorites") + prompt = "FAVORIS"; + prompts.push_back(HelpPrompt("y", prompt)); } return prompts; } + +std::vector BasicGameListView::getFileDataEntries() +{ + return mList.getObjects(); +} diff --git a/es-app/src/views/gamelist/BasicGameListView.h b/es-app/src/views/gamelist/BasicGameListView.h index f1a216b0f5..663aeeca68 100644 --- a/es-app/src/views/gamelist/BasicGameListView.h +++ b/es-app/src/views/gamelist/BasicGameListView.h @@ -8,7 +8,7 @@ class BasicGameListView : public ISimpleGameListView { public: - BasicGameListView(Window* window, FileData* root); + BasicGameListView(Window* window, FolderData* root); // Called when a FileData* is added, has its metadata changed, or is removed virtual void onFileChanged(FileData* file, FileChangeType change); @@ -18,12 +18,23 @@ class BasicGameListView : public ISimpleGameListView virtual FileData* getCursor() override; virtual void setCursor(FileData* file) override; - virtual const char* getName() const override { return "basic"; } + virtual const char* getName() const override + { + if (!mCustomThemeName.empty()) + return mCustomThemeName.c_str(); + + return "basic"; + } virtual std::vector getHelpPrompts() override; virtual void launch(FileData* game) override; + virtual void setThemeName(std::string name); + virtual void onShow() override; + protected: + virtual std::vector getFileDataEntries() override; + virtual std::string getQuickSystemSelectRightButton() override; virtual std::string getQuickSystemSelectLeftButton() override; virtual void populateList(const std::vector& files) override; @@ -31,6 +42,7 @@ class BasicGameListView : public ISimpleGameListView virtual void addPlaceholder(); TextListComponent mList; + bool mLoaded; }; #endif // ES_APP_VIEWS_GAME_LIST_BASIC_GAME_LIST_VIEW_H diff --git a/es-app/src/views/gamelist/DetailedGameListView.cpp b/es-app/src/views/gamelist/DetailedGameListView.cpp index 37827bcb86..d9053f458d 100644 --- a/es-app/src/views/gamelist/DetailedGameListView.cpp +++ b/es-app/src/views/gamelist/DetailedGameListView.cpp @@ -1,22 +1,29 @@ #include "views/gamelist/DetailedGameListView.h" #include "animations/LambdaAnimation.h" + +#ifdef _RPI_ +#include "components/VideoPlayerComponent.h" +#endif +#include "components/VideoVlcComponent.h" +#include "utils/FileSystemUtil.h" #include "views/ViewController.h" +#ifdef _RPI_ +#include "Settings.h" +#endif -DetailedGameListView::DetailedGameListView(Window* window, FileData* root) : - BasicGameListView(window, root), - mDescContainer(window), mDescription(window), - mImage(window), +DetailedGameListView::DetailedGameListView(Window* window, FolderData* root) : + BasicGameListView(window, root), + mDescContainer(window), mDescription(window), + mImage(nullptr), mMarquee(nullptr), mVideo(nullptr), mThumbnail(nullptr), - mLblRating(window), mLblReleaseDate(window), mLblDeveloper(window), mLblPublisher(window), + mLblRating(window), mLblReleaseDate(window), mLblDeveloper(window), mLblPublisher(window), mLblGenre(window), mLblPlayers(window), mLblLastPlayed(window), mLblPlayCount(window), - mRating(window), mReleaseDate(window), mDeveloper(window), mPublisher(window), + mRating(window), mReleaseDate(window), mDeveloper(window), mPublisher(window), mGenre(window), mPlayers(window), mLastPlayed(window), mPlayCount(window), mName(window) { - //mHeaderImage.setPosition(mSize.x() * 0.25f, 0); - const float padding = 0.01f; mList.setPosition(mSize.x() * (0.50f + padding), mList.getPosition().y()); @@ -24,37 +31,32 @@ DetailedGameListView::DetailedGameListView(Window* window, FileData* root) : mList.setAlignment(TextListComponent::ALIGN_LEFT); mList.setCursorChangedCallback([&](const CursorState& /*state*/) { updateInfoPanel(); }); - // image - mImage.setOrigin(0.5f, 0.5f); - mImage.setPosition(mSize.x() * 0.25f, mList.getPosition().y() + mSize.y() * 0.2125f); - mImage.setMaxSize(mSize.x() * (0.50f - 2*padding), mSize.y() * 0.4f); - mImage.setDefaultZIndex(30); - addChild(&mImage); + createImage(); // metadata labels + values - mLblRating.setText("Rating: "); + mLblRating.setText(_("Rating") + ": "); addChild(&mLblRating); addChild(&mRating); - mLblReleaseDate.setText("Released: "); + mLblReleaseDate.setText(_("Released") + ": "); addChild(&mLblReleaseDate); addChild(&mReleaseDate); - mLblDeveloper.setText("Developer: "); + mLblDeveloper.setText(_("Developer") + ": "); addChild(&mLblDeveloper); addChild(&mDeveloper); - mLblPublisher.setText("Publisher: "); + mLblPublisher.setText(_("Publisher")+": "); addChild(&mLblPublisher); addChild(&mPublisher); - mLblGenre.setText("Genre: "); + mLblGenre.setText(_("Genre") + ": "); addChild(&mLblGenre); addChild(&mGenre); - mLblPlayers.setText("Players: "); + mLblPlayers.setText(_("Players") + ": "); addChild(&mLblPlayers); addChild(&mPlayers); - mLblLastPlayed.setText("Last played: "); + mLblLastPlayed.setText(_("Last played") + ": "); addChild(&mLblLastPlayed); mLastPlayed.setDisplayRelative(true); addChild(&mLastPlayed); - mLblPlayCount.setText("Times played: "); + mLblPlayCount.setText(_("Times played")); addChild(&mLblPlayCount); addChild(&mPlayCount); @@ -75,10 +77,97 @@ DetailedGameListView::DetailedGameListView(Window* window, FileData* root) : mDescription.setSize(mDescContainer.getSize().x(), 0); mDescContainer.addChild(&mDescription); - initMDLabels(); initMDValues(); - updateInfoPanel(); +} + +DetailedGameListView::~DetailedGameListView() +{ + if (mThumbnail != nullptr) + delete mThumbnail; + + if (mImage != nullptr) + delete mImage; + + if (mMarquee != nullptr) + delete mMarquee; + + if (mVideo != nullptr) + delete mVideo; +} + +void DetailedGameListView::createImage() +{ + if (mImage != nullptr) + return; + + const float padding = 0.01f; + + // Image + mImage = new ImageComponent(mWindow, true); + mImage->setOrigin(0.5f, 0.5f); + mImage->setPosition(mSize.x() * 0.25f, mList.getPosition().y() + mSize.y() * 0.2125f); + mImage->setMaxSize(mSize.x() * (0.50f - 2 * padding), mSize.y() * 0.4f); + mImage->setDefaultZIndex(30); + addChild(mImage); +} + +void DetailedGameListView::createThumbnail() +{ + if (mThumbnail != nullptr) + return; + + const float padding = 0.01f; + + // Image + mThumbnail = new ImageComponent(mWindow); + mThumbnail->setAllowFading(false); + mThumbnail->setOrigin(0.5f, 0.5f); + mThumbnail->setPosition(mSize.x() * 0.25f, mList.getPosition().y() + mSize.y() * 0.2125f); + mThumbnail->setMaxSize(mSize.x() * (0.50f - 2 * padding), mSize.y() * 0.4f); + mThumbnail->setDefaultZIndex(30); + addChild(mThumbnail); +} + +void DetailedGameListView::createVideo() +{ + if (mVideo != nullptr) + return; + + const float padding = 0.01f; + + // video +// Create the correct type of video window +#ifdef _RPI_ + if (Settings::getInstance()->getBool("VideoOmxPlayer")) + mVideo = new VideoPlayerComponent(mWindow, ""); + else +#endif + mVideo = new VideoVlcComponent(mWindow, ""); + + // Default is IMAGE in Recalbox themes -> video view does not exist + mVideo->setSnapshotSource(IMAGE); + + mVideo->setOrigin(0.5f, 0.5f); + mVideo->setPosition(mSize.x() * 0.25f, mSize.y() * 0.4f); + mVideo->setSize(mSize.x() * (0.5f - 2 * padding), mSize.y() * 0.4f); + mVideo->setStartDelay(2000); + mVideo->setDefaultZIndex(31); + addChild(mVideo); +} + +void DetailedGameListView::createMarquee() +{ + const float padding = 0.01f; + + // Marquee + mMarquee = new ImageComponent(mWindow); + mMarquee->setOrigin(0.5f, 0.5f); + mMarquee->setPosition(mSize.x() * 0.25f, mSize.y() * 0.10f); + mMarquee->setMaxSize(mSize.x() * (0.5f - 2 * padding), mSize.y() * 0.18f); + mMarquee->setDefaultZIndex(35); + addChild(mMarquee); + } void DetailedGameListView::onThemeChanged(const std::shared_ptr& theme) @@ -86,28 +175,76 @@ void DetailedGameListView::onThemeChanged(const std::shared_ptr& them BasicGameListView::onThemeChanged(theme); using namespace ThemeFlags; - mImage.applyTheme(theme, getName(), "md_image", POSITION | ThemeFlags::SIZE | Z_INDEX | ROTATION | VISIBLE); + mName.applyTheme(theme, getName(), "md_name", ALL); + if (theme->getElement(getName(), "md_video", "video")) + { + createVideo(); + mVideo->applyTheme(theme, getName(), "md_video", ALL ^ (PATH)); + } + else if (mVideo != nullptr) + { + removeChild(mVideo); + delete mVideo; + mVideo = nullptr; + } + + if (mVideo == nullptr || theme->getElement(getName(), "md_image", "image")) + { + createImage(); + mImage->applyTheme(theme, getName(), "md_image", ALL ^ (PATH)); + } + else if (mImage != nullptr) + { + removeChild(mImage); + delete mImage; + mImage = nullptr; + } + + if (theme->getElement(getName(), "md_thumbnail", "image")) + { + createThumbnail(); + mThumbnail->applyTheme(theme, getName(), "md_thumbnail", ALL ^ (PATH)); + } + else if (mThumbnail != nullptr) + { + removeChild(mThumbnail); + delete mThumbnail; + mThumbnail = nullptr; + } + + if (theme->getElement(getName(), "md_marquee", "image")) + { + createMarquee(); + mMarquee->applyTheme(theme, getName(), "md_marquee", ALL ^ (PATH)); + } + else if (mMarquee != nullptr) + { + removeChild(mMarquee); + delete mMarquee; + mMarquee = nullptr; + } + initMDLabels(); std::vector labels = getMDLabels(); assert(labels.size() == 8); + const char* lblElements[8] = { - "md_lbl_rating", "md_lbl_releasedate", "md_lbl_developer", "md_lbl_publisher", + "md_lbl_rating", "md_lbl_releasedate", "md_lbl_developer", "md_lbl_publisher", "md_lbl_genre", "md_lbl_players", "md_lbl_lastplayed", "md_lbl_playcount" }; - - for(unsigned int i = 0; i < labels.size(); i++) + + for (unsigned int i = 0; i < labels.size(); i++) { labels[i]->applyTheme(theme, getName(), lblElements[i], ALL); } - initMDValues(); std::vector values = getMDValues(); assert(values.size() == 8); const char* valElements[8] = { - "md_rating", "md_releasedate", "md_developer", "md_publisher", + "md_rating", "md_releasedate", "md_developer", "md_publisher", "md_genre", "md_players", "md_lastplayed", "md_playcount" }; @@ -121,6 +258,7 @@ void DetailedGameListView::onThemeChanged(const std::shared_ptr& them mDescription.applyTheme(theme, getName(), "md_description", ALL ^ (POSITION | ThemeFlags::SIZE | ThemeFlags::ORIGIN | TEXT | ROTATION)); sortChildren(); + updateInfoPanel(); } void DetailedGameListView::initMDLabels() @@ -131,7 +269,7 @@ void DetailedGameListView::initMDLabels() const unsigned int rowCount = (int)(components.size() / 2); Vector3f start(mSize.x() * 0.01f, mSize.y() * 0.625f, 0.0f); - + const float colSize = (mSize.x() * 0.48f) / colCount; const float rowPadding = 0.01f * mSize.y(); @@ -188,40 +326,98 @@ void DetailedGameListView::initMDValues() mDescContainer.setSize(mDescContainer.getSize().x(), mSize.y() - mDescContainer.getPosition().y()); } +std::string DetailedGameListView::getMetadata(FileData* file, std::string name) +{ + std::string ret = file->getMetadata().get(name); + if (ret == "unknown") + return _(ret); + + return ret; +} + +#include "platform.h" + void DetailedGameListView::updateInfoPanel() { FileData* file = (mList.size() == 0 || mList.isScrolling()) ? NULL : mList.getSelected(); bool fadingOut; - if(file == NULL) - { + if (file == NULL) + { + if (mVideo != nullptr) + mVideo->setVideo(""); + //mImage.setImage(""); //mDescription.setText(""); fadingOut = true; - }else{ - mImage.setImage(file->getImagePath()); - mDescription.setText(file->metadata.get("desc")); + } + else + { + std::string imagePath = file->getImagePath().empty() ? file->getThumbnailPath() : file->getImagePath(); + + if (mVideo != nullptr) + { + if (!mVideo->setVideo(file->getVideoPath())) + mVideo->setDefaultVideo(); + + std::string snapShot = imagePath; + + auto src = mVideo->getSnapshotSource(); + if (src == MARQUEE && !file->getMarqueePath().empty()) + snapShot = file->getMarqueePath(); + if (src == THUMBNAIL && !file->getThumbnailPath().empty()) + snapShot = file->getThumbnailPath(); + + mVideo->setImage(snapShot); + } + + if (mThumbnail != nullptr) + mThumbnail->setImage(file->getThumbnailPath()); + + if (mImage != nullptr) + mImage->setImage(imagePath); + + if (mMarquee != nullptr) + mMarquee->setImage(file->getMarqueePath(), false, mMarquee->getMaxSizeInfo()); + + mDescription.setText(getMetadata(file, "desc")); mDescContainer.reset(); - mRating.setValue(file->metadata.get("rating")); - mReleaseDate.setValue(file->metadata.get("releasedate")); - mDeveloper.setValue(file->metadata.get("developer")); - mPublisher.setValue(file->metadata.get("publisher")); - mGenre.setValue(file->metadata.get("genre")); - mPlayers.setValue(file->metadata.get("players")); - mName.setValue(file->metadata.get("name")); + mRating.setValue(getMetadata(file, "rating")); + mReleaseDate.setValue(getMetadata(file, "releasedate")); + mDeveloper.setValue(getMetadata(file, "developer")); + mPublisher.setValue(getMetadata(file, "publisher")); + mGenre.setValue(getMetadata(file, "genre")); + mPlayers.setValue(getMetadata(file, "players")); + mName.setValue(getMetadata(file, "name")); if(file->getType() == GAME) { - mLastPlayed.setValue(file->metadata.get("lastplayed")); - mPlayCount.setValue(file->metadata.get("playcount")); + mLastPlayed.setValue(getMetadata(file, "lastplayed")); + mPlayCount.setValue(getMetadata(file, "playcount")); } - + fadingOut = false; } + + // We're clearing / populating : don't setup fade animations + if (file == nullptr && mList.getObjects().size() == 0 && mList.getCursorIndex() == 0 && mList.getScrollingVelocity() == 0) + return; std::vector comps = getMDValues(); - comps.push_back(&mImage); + + if (mVideo != nullptr) + comps.push_back(mVideo); + + if (mImage != nullptr) + comps.push_back(mImage); + + if (mThumbnail != nullptr) + comps.push_back(mThumbnail); + + if (mMarquee != nullptr) + comps.push_back(mMarquee); + comps.push_back(&mDescription); comps.push_back(&mName); std::vector labels = getMDLabels(); @@ -234,14 +430,25 @@ void DetailedGameListView::updateInfoPanel() // then animate if reverse != fadingOut // an animation is not playing // then animate if opacity != our target opacity - if((comp->isAnimationPlaying(0) && comp->isAnimationReversed(0) != fadingOut) || + if((comp->isAnimationPlaying(0) && comp->isAnimationReversed(0) != fadingOut) || (!comp->isAnimationPlaying(0) && comp->getOpacity() != (fadingOut ? 0 : 255))) { auto func = [comp](float t) { comp->setOpacity((unsigned char)(Math::lerp(0.0f, 1.0f, t)*255)); }; - comp->setAnimation(new LambdaAnimation(func, 150), 0, nullptr, fadingOut); + + bool isFadeOut = fadingOut; + comp->setAnimation(new LambdaAnimation(func, 150), 0, [this, isFadeOut, file] + { + if (isFadeOut) + { + if (mVideo != nullptr) mVideo->setImage(""); + if (mImage != nullptr) mImage->setImage(""); + if (mThumbnail != nullptr) mThumbnail->setImage(""); + if (mMarquee != nullptr) mMarquee->setImage(""); + } + }, fadingOut); } } } @@ -249,8 +456,13 @@ void DetailedGameListView::updateInfoPanel() void DetailedGameListView::launch(FileData* game) { Vector3f target(Renderer::getScreenWidth() / 2.0f, Renderer::getScreenHeight() / 2.0f, 0); - if(mImage.hasImage()) - target = Vector3f(mImage.getCenter().x(), mImage.getCenter().y(), 0); + + if (mVideo != nullptr) + target = Vector3f(mVideo->getCenter().x(), mVideo->getCenter().y(), 0); + else if (mImage != nullptr && mImage->hasImage()) + target = Vector3f(mImage->getCenter().x(), mImage->getCenter().y(), 0); + else if (mThumbnail != nullptr && mThumbnail->hasImage()) + target = Vector3f(mThumbnail->getCenter().x(), mThumbnail->getCenter().y(), 0); ViewController::get()->launch(game, target); } @@ -282,3 +494,9 @@ std::vector DetailedGameListView::getMDValues() ret.push_back(&mPlayCount); return ret; } + +void DetailedGameListView::onShow() +{ + BasicGameListView::onShow(); + updateInfoPanel(); +} diff --git a/es-app/src/views/gamelist/DetailedGameListView.h b/es-app/src/views/gamelist/DetailedGameListView.h index 76c8a93537..dbda8b39c7 100644 --- a/es-app/src/views/gamelist/DetailedGameListView.h +++ b/es-app/src/views/gamelist/DetailedGameListView.h @@ -7,24 +7,43 @@ #include "components/ScrollableContainer.h" #include "views/gamelist/BasicGameListView.h" +class VideoComponent; + class DetailedGameListView : public BasicGameListView { public: - DetailedGameListView(Window* window, FileData* root); + DetailedGameListView(Window* window, FolderData* root); + ~DetailedGameListView(); virtual void onThemeChanged(const std::shared_ptr& theme) override; + virtual void onShow() override; + + virtual const char* getName() const override + { + if (!mCustomThemeName.empty()) + return mCustomThemeName.c_str(); - virtual const char* getName() const override { return "detailed"; } + return "detailed"; + } virtual void launch(FileData* game) override; private: void updateInfoPanel(); + + void createVideo(); + void createMarquee(); + void createImage(); + void createThumbnail(); void initMDLabels(); void initMDValues(); + std::string getMetadata(FileData* file, std::string name); - ImageComponent mImage; + ImageComponent* mImage; + ImageComponent* mThumbnail; + ImageComponent* mMarquee; + VideoComponent* mVideo; TextComponent mLblRating, mLblReleaseDate, mLblDeveloper, mLblPublisher, mLblGenre, mLblPlayers, mLblLastPlayed, mLblPlayCount; @@ -37,12 +56,14 @@ class DetailedGameListView : public BasicGameListView DateTimeComponent mLastPlayed; TextComponent mPlayCount; TextComponent mName; - + std::vector getMDLabels(); std::vector getMDValues(); ScrollableContainer mDescContainer; TextComponent mDescription; + + }; #endif // ES_APP_VIEWS_GAME_LIST_DETAILED_GAME_LIST_VIEW_H diff --git a/es-app/src/views/gamelist/GridGameListView.cpp b/es-app/src/views/gamelist/GridGameListView.cpp index 8a0fbe7c8e..92b58af143 100644 --- a/es-app/src/views/gamelist/GridGameListView.cpp +++ b/es-app/src/views/gamelist/GridGameListView.cpp @@ -6,12 +6,19 @@ #include "CollectionSystemManager.h" #include "Settings.h" #include "SystemData.h" +#include "Window.h" +#include "guis/GuiGamelistOptions.h" -GridGameListView::GridGameListView(Window* window, FileData* root) : +#ifdef _RPI_ +#include "components/VideoPlayerComponent.h" +#endif +#include "components/VideoVlcComponent.h" + +GridGameListView::GridGameListView(Window* window, FolderData* root, const std::shared_ptr& theme, std::string themeName, Vector2f gridSize) : ISimpleGameListView(window, root), mGrid(window), mDescContainer(window), mDescription(window), - + mImage(nullptr), mVideo(nullptr), mMarquee(nullptr), mThumbnail(nullptr), mLblRating(window), mLblReleaseDate(window), mLblDeveloper(window), mLblPublisher(window), mLblGenre(window), mLblPlayers(window), mLblLastPlayed(window), mLblPlayCount(window), @@ -21,13 +28,14 @@ GridGameListView::GridGameListView(Window* window, FileData* root) : { const float padding = 0.01f; + mLoaded = false; + + mGrid.setGridSizeOverride(gridSize); mGrid.setPosition(mSize.x() * 0.1f, mSize.y() * 0.1f); mGrid.setDefaultZIndex(20); mGrid.setCursorChangedCallback([&](const CursorState& /*state*/) { updateInfoPanel(); }); addChild(&mGrid); - populateList(root->getChildrenListToDisplay()); - // metadata labels + values mLblRating.setText("Rating: "); addChild(&mLblRating); @@ -71,24 +79,115 @@ GridGameListView::GridGameListView(Window* window, FileData* root) : mDescription.setFont(Font::get(FONT_SIZE_SMALL)); mDescription.setSize(mDescContainer.getSize().x(), 0); mDescContainer.addChild(&mDescription); - - + initMDLabels(); initMDValues(); + + if (!themeName.empty()) + setThemeName(themeName); + + setTheme(theme); + + populateList(mRoot->getChildrenListToDisplay()); updateInfoPanel(); } +void GridGameListView::createVideo() +{ + if (mVideo != nullptr) + return; + + const float padding = 0.01f; + + // video +// Create the correct type of video window +#ifdef _RPI_ + if (Settings::getInstance()->getBool("VideoOmxPlayer")) + mVideo = new VideoPlayerComponent(mWindow, ""); + else +#endif + mVideo = new VideoVlcComponent(mWindow,""); + + mVideo->setSnapshotSource(IMAGE); + mVideo->setOrigin(0.5f, 0.5f); + mVideo->setPosition(mSize.x() * 0.25f, mSize.y() * 0.4f); + mVideo->setSize(mSize.x() * (0.5f - 2 * padding), mSize.y() * 0.4f); + mVideo->setStartDelay(2000); + mVideo->setDefaultZIndex(31); + addChild(mVideo); +} + +void GridGameListView::onShow() +{ + ISimpleGameListView::onShow(); + updateInfoPanel(); +} + + +GridGameListView::~GridGameListView() +{ + if (mImage != nullptr) + delete mImage; + + if (mThumbnail != nullptr) + delete mThumbnail; + + if (mMarquee != nullptr) + delete mMarquee; + + if (mVideo != nullptr) + delete mVideo; +} + +void GridGameListView::setThemeName(std::string name) +{ + ISimpleGameListView::setThemeName(name); + mGrid.setThemeName(getName()); +} + FileData* GridGameListView::getCursor() { + if (mGrid.size() == 0) + return nullptr; + return mGrid.getSelected(); } void GridGameListView::setCursor(FileData* file) { - if(!mGrid.setCursor(file)) + if (!mGrid.setCursor(file) && file->getParent() != nullptr && !file->isPlaceHolder()) { - populateList(file->getParent()->getChildrenListToDisplay()); + auto children = mRoot->getChildrenListToDisplay(); + + auto gameIter = std::find(children.cbegin(), children.cend(), file); + if (gameIter == children.cend()) + { + children = file->getParent()->getChildrenListToDisplay(); + + // update our cursor stack in case our cursor just got set to some folder we weren't in before + if (mCursorStack.empty() || mCursorStack.top() != file->getParent()) + { + std::stack tmp; + FileData* ptr = file->getParent(); + while (ptr && ptr != mRoot) + { + tmp.push(ptr); + ptr = ptr->getParent(); + } + + // flip the stack and put it in mCursorStack + mCursorStack = std::stack(); + while (!tmp.empty()) + { + mCursorStack.push(tmp.top()); + tmp.pop(); + } + } + } + + populateList(children); mGrid.setCursor(file); + } } @@ -104,27 +203,145 @@ std::string GridGameListView::getQuickSystemSelectLeftButton() bool GridGameListView::input(InputConfig* config, Input input) { + if (!UIModeController::getInstance()->isUIModeKid() && config->isMappedTo("select", input) && input.value) + { + Sound::getFromTheme(mTheme, getName(), "menuOpen")->play(); + mWindow->pushGui(new GuiGamelistOptions(mWindow, this->mRoot->getSystem(), true)); + return true; + + // Ctrl-R to reload a view when debugging + } + if(config->isMappedLike("left", input) || config->isMappedLike("right", input)) return GuiComponent::input(config, input); return ISimpleGameListView::input(config, input); } +const std::string GridGameListView::getImagePath(FileData* file) +{ + ImageSource src = mGrid.getImageSource(); + + if (src == ImageSource::IMAGE) + return file->getImagePath(); + else if (src == ImageSource::MARQUEE) + return file->getMarqueePath(); + + return file->getThumbnailPath(); +} + +const bool GridGameListView::isVirtualFolder(FileData* file) +{ + return file->getType() == FOLDER && ((FolderData*)file)->isVirtualFolderDisplay(); +} + void GridGameListView::populateList(const std::vector& files) { - mGrid.clear(); - mHeaderText.setText(mRoot->getSystem()->getFullName()); + SystemData* system = mCursorStack.size() && mRoot->getSystem()->isGroupSystem() ? mCursorStack.top()->getSystem() : mRoot->getSystem(); + + auto groupTheme = system->getTheme(); + if (groupTheme) + { + const ThemeData::ThemeElement* logoElem = groupTheme->getElement("system", "logo", "image"); + if (logoElem && logoElem->has("path") && Utils::FileSystem::exists(logoElem->get("path"))) + mHeaderImage.setImage(logoElem->get("path")); + } + + mHeaderText.setText(system->getFullName()); + + mGrid.resetLastCursor(); + mGrid.clear(); + mGrid.resetLastCursor(); + if (files.size() > 0) { - for (auto it = files.cbegin(); it != files.cend(); it++) + if (mCursorStack.size()) + { + auto top = mCursorStack.top(); + + std::string imagePath; + bool displayAsVirtualFolder = true; + + // Find logo image from original system + if (mCursorStack.size() == 1 && top->getSystem()->isGroupChildSystem()) + { + std::string startPath = top->getSystem()->getStartPath(); + + auto parent = top->getSystem()->getParentGroupSystem(); + + auto theme = parent->getTheme(); + if (theme) + { + const ThemeData::ThemeElement* logoElem = theme->getElement("system", "logo", "image"); + if (logoElem && logoElem->has("path")) + imagePath = logoElem->get("path"); + } + + if (imagePath.empty()) + { + for (auto child : parent->getRootFolder()->getChildren()) + { + if (child->getPath() == startPath) + { + if (child->getType() == FOLDER) + displayAsVirtualFolder = ((FolderData*)child)->isVirtualFolderDisplayEnabled(); + + imagePath = child->getMetadata("image"); + break; + } + } + } + } + + + FileData* placeholder = new FileData(PLACEHOLDER, "..", this->mRoot->getSystem()); + mGrid.add(". .", imagePath, "", "", false, true, displayAsVirtualFolder && !imagePath.empty(), placeholder); + } + + std::string systemName = mRoot->getSystem()->getName(); + + bool favoritesFirst = Settings::getInstance()->getBool("FavoritesFirst"); + bool showFavoriteIcon = (systemName != "favorites" && systemName != "recent"); + if (!showFavoriteIcon) + favoritesFirst = false; + + if (favoritesFirst) + { + for (auto file : files) + { + if (file->getFavorite() && showFavoriteIcon) + mGrid.add(file->getName(), getImagePath(file), file->getVideoPath(), file->getMarqueePath(), true, file->getType() != GAME, isVirtualFolder(file), file); + } + } + + for (auto file : files) { - mGrid.add((*it)->getName(), (*it)->getThumbnailPath(), *it); + if (file->getFavorite()) + { + if (favoritesFirst) + continue; + + if (showFavoriteIcon) + { + mGrid.add(_U("\uF006 ") + file->getName(), getImagePath(file), file->getVideoPath(), file->getMarqueePath(), true, file->getType() != GAME, isVirtualFolder(file), file); + continue; + } + } + + if (file->getType() == FOLDER && Utils::FileSystem::exists(getImagePath(file))) + mGrid.add(_U("\uF114 ") + file->getName(), getImagePath(file), file->getVideoPath(), file->getMarqueePath(), file->getFavorite(), file->getType() != GAME, isVirtualFolder(file), file); + else + mGrid.add(file->getName(), getImagePath(file), file->getVideoPath(), file->getMarqueePath(), file->getFavorite(), file->getType() != GAME, isVirtualFolder(file), file); } + + // if we have the ".." PLACEHOLDER, then select the first game instead of the placeholder + if (mCursorStack.size() && mGrid.size() > 1 && mGrid.getCursorIndex() == 0) + mGrid.setCursorIndex(1); } else { addPlaceholder(); - } + } } void GridGameListView::onThemeChanged(const std::shared_ptr& theme) @@ -136,6 +353,54 @@ void GridGameListView::onThemeChanged(const std::shared_ptr& theme) mGrid.applyTheme(theme, getName(), "gamegrid", ALL); mName.applyTheme(theme, getName(), "md_name", ALL); + if (theme->getElement(getName(), "md_video", "video")) + { + createVideo(); + mVideo->applyTheme(theme, getName(), "md_video", ALL ^ (PATH)); + } + else if (mVideo != nullptr) + { + removeChild(mVideo); + delete mVideo; + mVideo = nullptr; + } + + if (theme->getElement(getName(), "md_image", "image")) + { + createImage(); + mImage->applyTheme(theme, getName(), "md_image", ALL ^ (PATH)); + } + else if (mImage != nullptr) + { + removeChild(mImage); + delete mImage; + mImage = nullptr; + } + + if (theme->getElement(getName(), "md_thumbnail", "image")) + { + createThumbnail(); + mThumbnail->applyTheme(theme, getName(), "md_thumbnail", ALL ^ (PATH)); + } + else if (mThumbnail != nullptr) + { + removeChild(mThumbnail); + delete mThumbnail; + mThumbnail = nullptr; + } + + if (theme->getElement(getName(), "md_marquee", "image")) + { + createMarquee(); + mMarquee->applyTheme(theme, getName(), "md_marquee", ALL ^ (PATH)); + } + else if (mMarquee != nullptr) + { + removeChild(mMarquee); + delete mMarquee; + mMarquee = nullptr; + } + initMDLabels(); std::vector labels = getMDLabels(); assert(labels.size() == 8); @@ -163,11 +428,68 @@ void GridGameListView::onThemeChanged(const std::shared_ptr& theme) values[i]->applyTheme(theme, getName(), valElements[i], ALL ^ ThemeFlags::TEXT); } - mDescContainer.applyTheme(theme, getName(), "md_description", POSITION | ThemeFlags::SIZE | Z_INDEX | VISIBLE); - mDescription.setSize(mDescContainer.getSize().x(), 0); - mDescription.applyTheme(theme, getName(), "md_description", ALL ^ (POSITION | ThemeFlags::SIZE | ThemeFlags::ORIGIN | TEXT | ROTATION)); + if (theme->getElement(getName(), "md_description", "text")) + { + mDescContainer.applyTheme(theme, getName(), "md_description", POSITION | ThemeFlags::SIZE | Z_INDEX); + mDescription.setSize(mDescContainer.getSize().x(), 0); + mDescription.applyTheme(theme, getName(), "md_description", ALL ^ (POSITION | ThemeFlags::SIZE | ThemeFlags::ORIGIN | TEXT | ROTATION)); + + if (!isChild(&mDescContainer)) + addChild(&mDescContainer); + } + else + removeChild(&mDescContainer); sortChildren(); + updateInfoPanel(); +} + +void GridGameListView::createImage() +{ + if (mImage != nullptr) + return; + + const float padding = 0.01f; + + // Image + mImage = new ImageComponent(mWindow); + mImage->setAllowFading(false); + mImage->setOrigin(0.5f, 0.5f); + mImage->setPosition(mSize.x() * 0.25f, mGrid.getPosition().y() + mSize.y() * 0.2125f); + mImage->setMaxSize(mSize.x() * (0.50f - 2 * padding), mSize.y() * 0.4f); + mImage->setDefaultZIndex(30); + addChild(mImage); +} + +void GridGameListView::createThumbnail() +{ + if (mThumbnail != nullptr) + return; + + const float padding = 0.01f; + + // Image + mThumbnail = new ImageComponent(mWindow); + mThumbnail->setAllowFading(false); + mThumbnail->setOrigin(0.5f, 0.5f); + mThumbnail->setPosition(mSize.x() * 0.25f, mGrid.getPosition().y() + mSize.y() * 0.2125f); + mThumbnail->setMaxSize(mSize.x() * (0.50f - 2 * padding), mSize.y() * 0.4f); + mThumbnail->setDefaultZIndex(30); + addChild(mThumbnail); +} + +void GridGameListView::createMarquee() +{ + const float padding = 0.01f; + + // Marquee + mMarquee = new ImageComponent(mWindow); + mMarquee->setAllowFading(false); + mMarquee->setOrigin(0.5f, 0.5f); + mMarquee->setPosition(mSize.x() * 0.25f, mSize.y() * 0.10f); + mMarquee->setMaxSize(mSize.x() * (0.5f - 2 * padding), mSize.y() * 0.18f); + mMarquee->setDefaultZIndex(35); + addChild(mMarquee); } void GridGameListView::initMDLabels() @@ -240,37 +562,94 @@ void GridGameListView::updateInfoPanel() FileData* file = (mGrid.size() == 0 || mGrid.isScrolling()) ? NULL : mGrid.getSelected(); bool fadingOut; - if(file == NULL) + if (file == NULL) { - //mDescription.setText(""); + if (mVideo != nullptr) + { + mVideo->setVideo(""); + mVideo->setImage(""); + } + + if(mImage != nullptr) + mImage->setImage(""); + + // mVideo->setImage(""); + // mDescription.setText(""); fadingOut = true; - }else{ - mDescription.setText(file->metadata.get("desc")); + } + else + { + std::string imagePath = file->getImagePath().empty() ? file->getThumbnailPath() : file->getImagePath(); + + if (mVideo != nullptr) + { + if (!mVideo->setVideo(file->getVideoPath())) + mVideo->setDefaultVideo(); + + std::string snapShot = imagePath; + + auto src = mVideo->getSnapshotSource(); + if (src == MARQUEE && !file->getMarqueePath().empty()) + snapShot = file->getMarqueePath(); + if (src == THUMBNAIL && !file->getThumbnailPath().empty()) + snapShot = file->getThumbnailPath(); + + mVideo->setImage(snapShot); + } + + if (mImage != nullptr) + mImage->setImage(imagePath); + + if (mThumbnail != nullptr) + mThumbnail->setImage(file->getThumbnailPath()); + + if (mMarquee != nullptr) + mMarquee->setImage(file->getMarqueePath(), false, mMarquee->getMaxSizeInfo()); + + mDescription.setText(file->getMetadata().get("desc")); mDescContainer.reset(); - mRating.setValue(file->metadata.get("rating")); - mReleaseDate.setValue(file->metadata.get("releasedate")); - mDeveloper.setValue(file->metadata.get("developer")); - mPublisher.setValue(file->metadata.get("publisher")); - mGenre.setValue(file->metadata.get("genre")); - mPlayers.setValue(file->metadata.get("players")); - mName.setValue(file->metadata.get("name")); + mRating.setValue(file->getMetadata().get("rating")); + mReleaseDate.setValue(file->getMetadata().get("releasedate")); + mDeveloper.setValue(file->getMetadata().get("developer")); + mPublisher.setValue(file->getMetadata().get("publisher")); + mGenre.setValue(file->getMetadata().get("genre")); + mPlayers.setValue(file->getMetadata().get("players")); + mName.setValue(file->getMetadata().get("name")); if(file->getType() == GAME) { - mLastPlayed.setValue(file->metadata.get("lastplayed")); - mPlayCount.setValue(file->metadata.get("playcount")); + mLastPlayed.setValue(file->getMetadata().get("lastplayed")); + mPlayCount.setValue(file->getMetadata().get("playcount")); } fadingOut = false; } + // We're clearing / populating : don't setup fade animations + if (file == nullptr && mGrid.getObjects().size() == 0 && mGrid.getCursorIndex() == 0 && mGrid.getScrollingVelocity() == 0) + return; + std::vector comps = getMDValues(); + + if (mVideo != nullptr) + comps.push_back(mVideo); + + if (mMarquee != nullptr) + comps.push_back(mMarquee); + + if (mThumbnail != nullptr) + comps.push_back(mThumbnail); + + if (mImage != nullptr) + comps.push_back(mImage); + comps.push_back(&mDescription); comps.push_back(&mName); + std::vector labels = getMDLabels(); comps.insert(comps.cend(), labels.cbegin(), labels.cend()); - + for(auto it = comps.cbegin(); it != comps.cend(); it++) { GuiComponent* comp = *it; @@ -278,8 +657,7 @@ void GridGameListView::updateInfoPanel() // then animate if reverse != fadingOut // an animation is not playing // then animate if opacity != our target opacity - if((comp->isAnimationPlaying(0) && comp->isAnimationReversed(0) != fadingOut) || - (!comp->isAnimationPlaying(0) && comp->getOpacity() != (fadingOut ? 0 : 255))) + if((comp->isAnimationPlaying(0) && comp->isAnimationReversed(0) != fadingOut) || (!comp->isAnimationPlaying(0) && comp->getOpacity() != (fadingOut ? 0 : 255))) { auto func = [comp](float t) { @@ -293,20 +671,34 @@ void GridGameListView::updateInfoPanel() void GridGameListView::addPlaceholder() { // empty grid - add a placeholder - FileData* placeholder = new FileData(PLACEHOLDER, "", this->mRoot->getSystem()->getSystemEnvData(), this->mRoot->getSystem()); - mGrid.add(placeholder->getName(), "", placeholder); + FileData* placeholder = new FileData(PLACEHOLDER, "<" + _("No Entries Found") + ">", mRoot->getSystem()); + mGrid.add(placeholder->getName(), "", "", "", false, false, false, placeholder); } void GridGameListView::launch(FileData* game) { - ViewController::get()->launch(game); + Vector3f target(Renderer::getScreenWidth() / 2.0f, Renderer::getScreenHeight() / 2.0f, 0); + + if (mVideoVisible && mVideo) + target = Vector3f(mVideo->getCenter().x(), mVideo->getCenter().y(), 0); + else if (mImage != nullptr) + target = Vector3f(mImage->getCenter().x(), mImage->getCenter().y(), 0); + else + { + auto tile = mGrid.getSelectedTile(); + if (tile != nullptr) + target = Vector3f(tile->getCenter().x(), tile->getCenter().y(), 0); + } + + ViewController::get()->launch(game, target); } void GridGameListView::remove(FileData *game, bool deleteFile) { if (deleteFile) Utils::FileSystem::removeFile(game->getPath()); // actually delete the file on the filesystem - FileData* parent = game->getParent(); + + FolderData* parent = game->getParent(); if (getCursor() == game) // Select next element in list, or prev if none { std::vector siblings = parent->getChildrenListToDisplay(); @@ -331,6 +723,19 @@ void GridGameListView::remove(FileData *game, bool deleteFile) onFileChanged(parent, FILE_REMOVED); // update the view, with game removed } +void GridGameListView::onFileChanged(FileData* file, FileChangeType change) +{ + if (change == FILE_METADATA_CHANGED) + { + // might switch to a detailed view + ViewController::get()->reloadGameListView(this); + return; + } + + ISimpleGameListView::onFileChanged(file, change); +} + + std::vector GridGameListView::getMDLabels() { std::vector ret; @@ -363,19 +768,29 @@ std::vector GridGameListView::getHelpPrompts() { std::vector prompts; - if(Settings::getInstance()->getBool("QuickSystemSelect")) - prompts.push_back(HelpPrompt("lr", "system")); - prompts.push_back(HelpPrompt("up/down/left/right", "choose")); - prompts.push_back(HelpPrompt("a", "launch")); - prompts.push_back(HelpPrompt("b", "back")); - if(!UIModeController::getInstance()->isUIModeKid()) - prompts.push_back(HelpPrompt("select", "options")); - if(mRoot->getSystem()->isGameSystem()) - prompts.push_back(HelpPrompt("x", "random")); - if(mRoot->getSystem()->isGameSystem() && !UIModeController::getInstance()->isUIModeKid()) + if (Settings::getInstance()->getBool("QuickSystemSelect")) + prompts.push_back(HelpPrompt("lr", _("SYSTEM"))); + prompts.push_back(HelpPrompt("up/down/left/right", _("CHOOSE"))); + prompts.push_back(HelpPrompt("a", _("LAUNCH"))); + + if (!Settings::getInstance()->getBool("HideSystemView")) + prompts.push_back(HelpPrompt("b", _("BACK"))); + + if (!UIModeController::getInstance()->isUIModeKid()) + prompts.push_back(HelpPrompt("select", _("OPTIONS"))); + + if (mRoot->getSystem()->isGameSystem()) + prompts.push_back(HelpPrompt("x", _("RANDOM"))); + + if (mRoot->getSystem()->isGameSystem() && !UIModeController::getInstance()->isUIModeKid()) { std::string prompt = CollectionSystemManager::get()->getEditingCollection(); prompts.push_back(HelpPrompt("y", prompt)); } return prompts; } + +std::vector GridGameListView::getFileDataEntries() +{ + return mGrid.getObjects(); +} diff --git a/es-app/src/views/gamelist/GridGameListView.h b/es-app/src/views/gamelist/GridGameListView.h index da09c87903..042227a702 100644 --- a/es-app/src/views/gamelist/GridGameListView.h +++ b/es-app/src/views/gamelist/GridGameListView.h @@ -7,11 +7,15 @@ #include "components/ScrollableContainer.h" #include "components/ImageGridComponent.h" #include "views/gamelist/ISimpleGameListView.h" +#include "views/gamelist/BasicGameListView.h" + +class VideoComponent; class GridGameListView : public ISimpleGameListView { public: - GridGameListView(Window* window, FileData* root); + GridGameListView(Window* window, FolderData* root, const std::shared_ptr& theme, std::string customThemeName, Vector2f gridSize); + ~GridGameListView(); virtual void onThemeChanged(const std::shared_ptr& theme) override; @@ -20,12 +24,23 @@ class GridGameListView : public ISimpleGameListView virtual bool input(InputConfig* config, Input input) override; - virtual const char* getName() const override { return "grid"; } + virtual const char* getName() const override + { + if (!mCustomThemeName.empty()) + return mCustomThemeName.c_str(); + + return "grid"; + } virtual std::vector getHelpPrompts() override; virtual void launch(FileData* game) override; + virtual void onFileChanged(FileData* file, FileChangeType change); + + virtual void setThemeName(std::string name); + virtual void onShow(); protected: + virtual std::vector getFileDataEntries() override; virtual std::string getQuickSystemSelectRightButton() override; virtual std::string getQuickSystemSelectLeftButton() override; virtual void populateList(const std::vector& files) override; @@ -36,6 +51,13 @@ class GridGameListView : public ISimpleGameListView private: void updateInfoPanel(); + const std::string getImagePath(FileData* file); + const bool isVirtualFolder(FileData* file); + + void createMarquee(); + void createImage(); + void createThumbnail(); + void createVideo(); void initMDLabels(); void initMDValues(); @@ -52,11 +74,19 @@ class GridGameListView : public ISimpleGameListView TextComponent mPlayCount; TextComponent mName; + ImageComponent* mImage; + ImageComponent* mThumbnail; + ImageComponent* mMarquee; + VideoComponent* mVideo; + bool mVideoVisible; + std::vector getMDLabels(); std::vector getMDValues(); ScrollableContainer mDescContainer; TextComponent mDescription; + + bool mLoaded; }; #endif // ES_APP_VIEWS_GAME_LIST_GRID_GAME_LIST_VIEW_H diff --git a/es-app/src/views/gamelist/IGameListView.cpp b/es-app/src/views/gamelist/IGameListView.cpp index 4b48bdd372..50b864c9af 100644 --- a/es-app/src/views/gamelist/IGameListView.cpp +++ b/es-app/src/views/gamelist/IGameListView.cpp @@ -6,6 +6,11 @@ #include "Sound.h" #include "Window.h" +void IGameListView::setThemeName(std::string name) +{ + mCustomThemeName = name; +} + bool IGameListView::input(InputConfig* config, Input input) { // select to open GuiGamelistOptions @@ -50,6 +55,10 @@ void IGameListView::render(const Transform4x4f& parentTrans) Vector2i pos((int)Math::round(trans.translation()[0]), (int)Math::round(trans.translation()[1])); Vector2i size((int)Math::round(mSize.x() * scaleX), (int)Math::round(mSize.y() * scaleY)); + Vector2f clipPos(trans.translation().x(), trans.translation().y()); + if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), size.x(), size.y())) + return; + Renderer::pushClipRect(pos, size); renderChildren(trans); Renderer::popClipRect(); diff --git a/es-app/src/views/gamelist/IGameListView.h b/es-app/src/views/gamelist/IGameListView.h index da6299d274..3f6d69495f 100644 --- a/es-app/src/views/gamelist/IGameListView.h +++ b/es-app/src/views/gamelist/IGameListView.h @@ -13,7 +13,7 @@ class Window; class IGameListView : public GuiComponent { public: - IGameListView(Window* window, FileData* root) : GuiComponent(window), mRoot(root) + IGameListView(Window* window, FolderData* root) : GuiComponent(window), mRoot(root) { setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); } virtual ~IGameListView() {} @@ -41,8 +41,15 @@ class IGameListView : public GuiComponent virtual HelpStyle getHelpStyle() override; void render(const Transform4x4f& parentTrans) override; + + virtual void setThemeName(std::string name); + + virtual std::vector getEntriesLetters() = 0; + protected: - FileData* mRoot; + std::string mCustomThemeName; + + FolderData* mRoot; std::shared_ptr mTheme; }; diff --git a/es-app/src/views/gamelist/ISimpleGameListView.cpp b/es-app/src/views/gamelist/ISimpleGameListView.cpp index 7d361fc940..cc49d25ea7 100644 --- a/es-app/src/views/gamelist/ISimpleGameListView.cpp +++ b/es-app/src/views/gamelist/ISimpleGameListView.cpp @@ -7,7 +7,7 @@ #include "Sound.h" #include "SystemData.h" -ISimpleGameListView::ISimpleGameListView(Window* window, FileData* root) : IGameListView(window, root), +ISimpleGameListView::ISimpleGameListView(Window* window, FolderData* root) : IGameListView(window, root), mHeaderText(window), mHeaderImage(window), mBackground(window) { mHeaderText.setText("Logo Text"); @@ -78,47 +78,89 @@ void ISimpleGameListView::onFileChanged(FileData* /*file*/, FileChangeType /*cha bool ISimpleGameListView::input(InputConfig* config, Input input) { - if(input.value != 0) + bool hideSystemView = Settings::getInstance()->getBool("HideSystemView"); + + if (input.value != 0) { - if(config->isMappedTo("a", input)) + if (config->isMappedTo("a", input)) { + // Don't launch game if transition is still running + if (ViewController::get()->isAnimationPlaying(0)) + return true; + FileData* cursor = getCursor(); - if(cursor->getType() == GAME) + FolderData* folder = NULL; + + if (mCursorStack.size() && cursor->getType() == PLACEHOLDER && cursor->getPath() == "..") + { + auto top = mCursorStack.top(); + mCursorStack.pop(); + + FolderData* folder = top->getParent(); + if (folder == nullptr) + folder = getCursor()->getSystem()->getParentGroupSystem()->getRootFolder(); + + populateList(folder->getChildrenListToDisplay()); + setCursor(top); + Sound::getFromTheme(getTheme(), getName(), "back")->play(); + } + else { - Sound::getFromTheme(getTheme(), getName(), "launch")->play(); - launch(cursor); - }else{ - // it's a folder - if(cursor->getChildren().size() > 0) + if (cursor->getType() == FOLDER) + folder = (FolderData*)cursor; + + if (cursor->getType() == GAME) { - mCursorStack.push(cursor); - populateList(cursor->getChildrenListToDisplay()); - FileData* cursor = getCursor(); - setCursor(cursor); + Sound::getFromTheme(getTheme(), getName(), "launch")->play(); + launch(cursor); + } + else { + // it's a folder + if (folder != nullptr && folder->getChildren().size() > 0) + { + mCursorStack.push(cursor); + populateList(folder->getChildrenListToDisplay()); + FileData* cursor = getCursor(); + setCursor(cursor); + } } } - return true; - }else if(config->isMappedTo("b", input)) + } + else if(config->isMappedTo("b", input)) { - if(mCursorStack.size()) + if (mCursorStack.size()) { - populateList(mCursorStack.top()->getParent()->getChildren()); - setCursor(mCursorStack.top()); + auto top = mCursorStack.top(); mCursorStack.pop(); + + FolderData* folder = top->getParent(); + if (folder == nullptr && getCursor()->getSystem()->getParentGroupSystem() != nullptr) + folder = getCursor()->getSystem()->getParentGroupSystem()->getRootFolder(); + + if (folder == nullptr) + return true; + + populateList(folder->getChildrenListToDisplay()); + setCursor(top); Sound::getFromTheme(getTheme(), getName(), "back")->play(); - }else{ + } + else if (!hideSystemView) + { onFocusLost(); SystemData* systemToView = getCursor()->getSystem(); - if (systemToView->isCollection()) - { + + if (systemToView->isGroupChildSystem()) + systemToView = systemToView->getParentGroupSystem(); + else if (systemToView->isCollection()) systemToView = CollectionSystemManager::get()->getSystemToView(systemToView); - } + ViewController::get()->goToSystemView(systemToView); } return true; - }else if(config->isMappedLike(getQuickSystemSelectRightButton(), input)) + } + else if (config->isMappedLike(getQuickSystemSelectRightButton(), input) || config->isMappedLike("rightshoulder", input)) { if(Settings::getInstance()->getBool("QuickSystemSelect")) { @@ -126,7 +168,8 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) ViewController::get()->goToNextGameList(); return true; } - }else if(config->isMappedLike(getQuickSystemSelectLeftButton(), input)) + } + else if (config->isMappedLike(getQuickSystemSelectLeftButton(), input) || config->isMappedLike("leftshoulder", input)) { if(Settings::getInstance()->getBool("QuickSystemSelect")) { @@ -134,7 +177,8 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) ViewController::get()->goToPrevGameList(); return true; } - }else if (config->isMappedTo("x", input)) + } + else if (config->isMappedTo("x", input)) { if (mRoot->getSystem()->isGameSystem()) { @@ -146,21 +190,36 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) } return true; } - }else if (config->isMappedTo("y", input) && !UIModeController::getInstance()->isUIModeKid()) + } + else if (config->isMappedTo("y", input) && !UIModeController::getInstance()->isUIModeKid()) { - if(mRoot->getSystem()->isGameSystem()) - { - if(CollectionSystemManager::get()->toggleGameInCollection(getCursor())) - { + if (mRoot->getSystem()->isGameSystem() || mRoot->getSystem()->isGroupSystem()) + if (CollectionSystemManager::get()->toggleGameInCollection(getCursor())) return true; - } - } } } return IGameListView::input(config, input); } +std::vector ISimpleGameListView::getEntriesLetters() +{ + std::set setOfLetters; + + for (auto file : getFileDataEntries()) + if (file->getType() == GAME) + setOfLetters.insert(std::string(1, toupper(file->getName()[0]))); + + std::vector letters; + + for (const auto letter : setOfLetters) + letters.push_back(letter); + + std::sort(letters.begin(), letters.end()); + return letters; +} + + diff --git a/es-app/src/views/gamelist/ISimpleGameListView.h b/es-app/src/views/gamelist/ISimpleGameListView.h index 785f3992ca..133a447a47 100644 --- a/es-app/src/views/gamelist/ISimpleGameListView.h +++ b/es-app/src/views/gamelist/ISimpleGameListView.h @@ -10,7 +10,7 @@ class ISimpleGameListView : public IGameListView { public: - ISimpleGameListView(Window* window, FileData* root); + ISimpleGameListView(Window* window, FolderData* root); virtual ~ISimpleGameListView() {} // Called when a new file is added, a file is removed, a file's metadata changes, or a file's children are sorted. @@ -27,7 +27,11 @@ class ISimpleGameListView : public IGameListView virtual bool input(InputConfig* config, Input input) override; virtual void launch(FileData* game) = 0; + virtual std::vector getEntriesLetters() override; + protected: + virtual std::vector getFileDataEntries() = 0; + virtual std::string getQuickSystemSelectRightButton() = 0; virtual std::string getQuickSystemSelectLeftButton() = 0; virtual void populateList(const std::vector& files) = 0; @@ -36,6 +40,7 @@ class ISimpleGameListView : public IGameListView ImageComponent mHeaderImage; ImageComponent mBackground; + std::vector mThemeExtras; std::stack mCursorStack; diff --git a/es-app/src/views/gamelist/VideoGameListView.cpp b/es-app/src/views/gamelist/VideoGameListView.cpp index 1daa91b1a1..170af90f69 100644 --- a/es-app/src/views/gamelist/VideoGameListView.cpp +++ b/es-app/src/views/gamelist/VideoGameListView.cpp @@ -11,13 +11,14 @@ #include "Settings.h" #endif -VideoGameListView::VideoGameListView(Window* window, FileData* root) : +VideoGameListView::VideoGameListView(Window* window, FolderData* root) : BasicGameListView(window, root), mDescContainer(window), mDescription(window), mMarquee(window), - mImage(window), + mImage(nullptr), mVideo(nullptr), mVideoPlaying(false), + mThumbnail(nullptr), mLblRating(window), mLblReleaseDate(window), mLblDeveloper(window), mLblPublisher(window), mLblGenre(window), mLblPlayers(window), mLblLastPlayed(window), mLblPlayCount(window), @@ -33,10 +34,12 @@ VideoGameListView::VideoGameListView(Window* window, FileData* root) : if (Settings::getInstance()->getBool("VideoOmxPlayer")) mVideo = new VideoPlayerComponent(window, ""); else - mVideo = new VideoVlcComponent(window, getTitlePath()); + mVideo = new VideoVlcComponent(window, ""); #else - mVideo = new VideoVlcComponent(window, getTitlePath()); + mVideo = new VideoVlcComponent(window, ""); #endif + + mVideo->setSnapshotSource(IMAGE); mList.setPosition(mSize.x() * (0.50f + padding), mList.getPosition().y()); mList.setSize(mSize.x() * (0.50f - padding), mList.getSize().y()); @@ -50,19 +53,11 @@ VideoGameListView::VideoGameListView(Window* window, FileData* root) : mMarquee.setDefaultZIndex(35); addChild(&mMarquee); - // Image - mImage.setOrigin(0.5f, 0.5f); - // Default to off the screen - mImage.setPosition(2.0f, 2.0f); - mImage.setMaxSize(1.0f, 1.0f); - mImage.setDefaultZIndex(30); - addChild(&mImage); - // video mVideo->setOrigin(0.5f, 0.5f); mVideo->setPosition(mSize.x() * 0.25f, mSize.y() * 0.4f); mVideo->setSize(mSize.x() * (0.5f - 2*padding), mSize.y() * 0.4f); - mVideo->setDefaultZIndex(30); + mVideo->setDefaultZIndex(31); addChild(mVideo); // metadata labels + values @@ -115,7 +110,47 @@ VideoGameListView::VideoGameListView(Window* window, FileData* root) : VideoGameListView::~VideoGameListView() { - delete mVideo; + if (mThumbnail != nullptr) + delete mThumbnail; + + if (mImage != nullptr) + delete mImage; + + if (mVideo != nullptr) + delete mVideo; +} + +void VideoGameListView::createThumbnail() +{ + if (mThumbnail != nullptr) + return; + + const float padding = 0.01f; + + // Image + mThumbnail = new ImageComponent(mWindow); + mThumbnail->setAllowFading(false); + mThumbnail->setOrigin(0.5f, 0.5f); + mThumbnail->setPosition(mSize.x() * 0.25f, mList.getPosition().y() + mSize.y() * 0.2125f); + mThumbnail->setMaxSize(mSize.x() * (0.50f - 2 * padding), mSize.y() * 0.4f); + mThumbnail->setDefaultZIndex(30); + addChild(mThumbnail); +} + +void VideoGameListView::createImage() +{ + if (mImage != nullptr) + return; + + const float padding = 0.01f; + + // Image + mImage = new ImageComponent(mWindow, true); + mImage->setOrigin(0.5f, 0.5f); + mImage->setPosition(2.0f, 2.0f); + mImage->setMaxSize(100.0f, 100.0f); + mImage->setDefaultZIndex(30); + addChild(mImage); } void VideoGameListView::onThemeChanged(const std::shared_ptr& theme) @@ -123,11 +158,35 @@ void VideoGameListView::onThemeChanged(const std::shared_ptr& theme) BasicGameListView::onThemeChanged(theme); using namespace ThemeFlags; - mMarquee.applyTheme(theme, getName(), "md_marquee", POSITION | ThemeFlags::SIZE | Z_INDEX | ROTATION | VISIBLE); - mImage.applyTheme(theme, getName(), "md_image", POSITION | ThemeFlags::SIZE | Z_INDEX | ROTATION | VISIBLE); - mVideo->applyTheme(theme, getName(), "md_video", POSITION | ThemeFlags::SIZE | ThemeFlags::DELAY | Z_INDEX | ROTATION | VISIBLE); + mMarquee.applyTheme(theme, getName(), "md_marquee", ALL ^ (PATH)); + + if (theme->getElement(getName(), "md_image", "image")) + { + createImage(); + mImage->applyTheme(theme, getName(), "md_image", ALL ^ (PATH)); + } + else if (mImage != nullptr) + { + removeChild(mImage); + delete mImage; + mImage = nullptr; + } + + mVideo->applyTheme(theme, getName(), "md_video", ALL ^ (PATH)); mName.applyTheme(theme, getName(), "md_name", ALL); + if (theme->getElement(getName(), "md_thumbnail", "image")) + { + createThumbnail(); + mThumbnail->applyTheme(theme, getName(), "md_thumbnail", ALL ^ (PATH)); + } + else if (mThumbnail != nullptr) + { + removeChild(mThumbnail); + delete mThumbnail; + mThumbnail = nullptr; + } + initMDLabels(); std::vector labels = getMDLabels(); assert(labels.size() == 8); @@ -251,36 +310,65 @@ void VideoGameListView::updateInfoPanel() mVideo->setDefaultVideo(); } mVideoPlaying = true; + + std::string snapShot = file->getThumbnailPath(); + + auto src = mVideo->getSnapshotSource(); + if (src == MARQUEE && !file->getMarqueePath().empty()) + snapShot = file->getMarqueePath(); + if (src == IMAGE && !file->getImagePath().empty()) + snapShot = file->getImagePath(); - mVideo->setImage(file->getThumbnailPath()); - mMarquee.setImage(file->getMarqueePath()); - mImage.setImage(file->getImagePath()); + mVideo->setImage(snapShot); - mDescription.setText(file->metadata.get("desc")); + mMarquee.setImage(file->getMarqueePath()/*, false, mMarquee.getMaxSizeInfo()*/); // Too slow on pi + + if (mThumbnail != nullptr) + { + if (mImage != nullptr) + mImage->setImage(file->getImagePath(), false, mImage->getMaxSizeInfo()); + + mThumbnail->setImage(file->getThumbnailPath(), false, mThumbnail->getMaxSizeInfo()); + } + else if (mImage != nullptr) + + mImage->setImage(file->getThumbnailPath()); + + mDescription.setText(file->getMetadata().get("desc")); mDescContainer.reset(); - mRating.setValue(file->metadata.get("rating")); - mReleaseDate.setValue(file->metadata.get("releasedate")); - mDeveloper.setValue(file->metadata.get("developer")); - mPublisher.setValue(file->metadata.get("publisher")); - mGenre.setValue(file->metadata.get("genre")); - mPlayers.setValue(file->metadata.get("players")); - mName.setValue(file->metadata.get("name")); + mRating.setValue(file->getMetadata().get("rating")); + mReleaseDate.setValue(file->getMetadata().get("releasedate")); + mDeveloper.setValue(file->getMetadata().get("developer")); + mPublisher.setValue(file->getMetadata().get("publisher")); + mGenre.setValue(file->getMetadata().get("genre")); + mPlayers.setValue(file->getMetadata().get("players")); + mName.setValue(file->getMetadata().get("name")); if(file->getType() == GAME) { - mLastPlayed.setValue(file->metadata.get("lastplayed")); - mPlayCount.setValue(file->metadata.get("playcount")); + mLastPlayed.setValue(file->getMetadata().get("lastplayed")); + mPlayCount.setValue(file->getMetadata().get("playcount")); } fadingOut = false; } + // We're clearing / populating : don't setup fade animations + if (file == nullptr && mList.getObjects().size() == 0 && mList.getCursorIndex() == 0 && mList.getScrollingVelocity() == 0) + return; + std::vector comps = getMDValues(); comps.push_back(&mMarquee); comps.push_back(mVideo); comps.push_back(&mDescription); - comps.push_back(&mImage); + + if (mImage != nullptr) + comps.push_back(mImage); + + if (mThumbnail != nullptr) + comps.push_back(mThumbnail); + comps.push_back(&mName); std::vector labels = getMDLabels(); comps.insert(comps.cend(), labels.cbegin(), labels.cend()); @@ -299,7 +387,19 @@ void VideoGameListView::updateInfoPanel() { comp->setOpacity((unsigned char)(Math::lerp(0.0f, 1.0f, t)*255)); }; - comp->setAnimation(new LambdaAnimation(func, 150), 0, nullptr, fadingOut); + + bool isFadeOut = fadingOut; + comp->setAnimation(new LambdaAnimation(func, 150), 0, [this, isFadeOut] + { + if (isFadeOut) + { + if (mVideo != nullptr) mVideo->setImage(""); + if (mThumbnail != nullptr) mThumbnail->setImage(""); + if (mImage != nullptr) mImage->setImage(""); + + mMarquee.setImage(""); + } + }, fadingOut); } } } @@ -317,11 +417,11 @@ void VideoGameListView::launch(FileData* game) { target = Vector3f(mMarquee.getCenter().x(), mMarquee.getCenter().y(), 0); } - else if(mImage.hasImage() && - (mImage.getPosition().x() < screenWidth && mImage.getPosition().x() > 2.0f && - mImage.getPosition().y() < screenHeight && mImage.getPosition().y() > 2.0f)) + else if(mImage != nullptr && mImage->hasImage() && + (mImage->getPosition().x() < screenWidth && mImage->getPosition().x() > 2.0f && + mImage->getPosition().y() < screenHeight && mImage->getPosition().y() > 2.0f)) { - target = Vector3f(mImage.getCenter().x(), mImage.getCenter().y(), 0); + target = Vector3f(mImage->getCenter().x(), mImage->getCenter().y(), 0); } else if(mHeaderImage.hasImage() && (mHeaderImage.getPosition().x() < screenWidth && mHeaderImage.getPosition().x() > 0.0f && @@ -334,6 +434,11 @@ void VideoGameListView::launch(FileData* game) { target = Vector3f(mVideo->getCenter().x(), mVideo->getCenter().y(), 0); } + else if (mThumbnail != nullptr && mThumbnail->getPosition().x() < screenWidth && mThumbnail->getPosition().x() > 0.0f && + mThumbnail->getPosition().y() < screenHeight && mThumbnail->getPosition().y() > 0.0f) + { + target = Vector3f(mThumbnail->getCenter().x(), mThumbnail->getCenter().y(), 0); + } ViewController::get()->launch(game, target); } diff --git a/es-app/src/views/gamelist/VideoGameListView.h b/es-app/src/views/gamelist/VideoGameListView.h index 8a8335866b..92cc090fe9 100644 --- a/es-app/src/views/gamelist/VideoGameListView.h +++ b/es-app/src/views/gamelist/VideoGameListView.h @@ -12,14 +12,21 @@ class VideoComponent; class VideoGameListView : public BasicGameListView { public: - VideoGameListView(Window* window, FileData* root); + VideoGameListView(Window* window, FolderData* root); virtual ~VideoGameListView(); virtual void onShow() override; virtual void onThemeChanged(const std::shared_ptr& theme) override; - virtual const char* getName() const override { return "video"; } + virtual const char* getName() const override + { + if (!mCustomThemeName.empty()) + return mCustomThemeName.c_str(); + + return "video"; + } + virtual void launch(FileData* game) override; protected: @@ -27,13 +34,16 @@ class VideoGameListView : public BasicGameListView private: void updateInfoPanel(); + void createImage(); + void createThumbnail(); void initMDLabels(); void initMDValues(); ImageComponent mMarquee; VideoComponent* mVideo; - ImageComponent mImage; + ImageComponent* mImage; + ImageComponent* mThumbnail; TextComponent mLblRating, mLblReleaseDate, mLblDeveloper, mLblPublisher, mLblGenre, mLblPlayers, mLblLastPlayed, mLblPlayCount; diff --git a/es-core/CMakeLists.txt b/es-core/CMakeLists.txt index 0503055575..0a4a90740f 100644 --- a/es-core/CMakeLists.txt +++ b/es-core/CMakeLists.txt @@ -18,6 +18,7 @@ set(CORE_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/src/Sound.h ${CMAKE_CURRENT_SOURCE_DIR}/src/ThemeData.h ${CMAKE_CURRENT_SOURCE_DIR}/src/Window.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/EsLocale.h # Animations ${CMAKE_CURRENT_SOURCE_DIR}/src/animations/Animation.h @@ -26,6 +27,7 @@ set(CORE_HEADERS # GuiComponents ${CMAKE_CURRENT_SOURCE_DIR}/src/components/AnimatedImageComponent.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/components/AsyncNotificationComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/BusyComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/ButtonComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/ComponentGrid.h @@ -47,13 +49,16 @@ set(CORE_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/src/components/TextEditComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/VideoComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/VideoPlayerComponent.h - ${CMAKE_CURRENT_SOURCE_DIR}/src/components/VideoVlcComponent.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/components/VideoVlcComponent.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/components/MultiLineMenuEntry.h # Guis ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiDetectDevice.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiInputConfig.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiInfoPopup.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiMsgBox.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiTextEditPopup.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiTextEditPopupKeyboard.h # Math ${CMAKE_CURRENT_SOURCE_DIR}/src/math/Misc.h @@ -76,6 +81,7 @@ set(CORE_HEADERS # Utils ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/FileSystemUtil.h ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/StringUtil.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/ThreadPool.h ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/TimeUtil.h ) @@ -97,12 +103,14 @@ set(CORE_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/Sound.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/ThemeData.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Window.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/EsLocale.cpp # Animations ${CMAKE_CURRENT_SOURCE_DIR}/src/animations/AnimationController.cpp # GuiComponents ${CMAKE_CURRENT_SOURCE_DIR}/src/components/AnimatedImageComponent.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/components/AsyncNotificationComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/components/BusyComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/components/ButtonComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/components/ComponentGrid.cpp @@ -126,8 +134,10 @@ set(CORE_SOURCES # Guis ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiDetectDevice.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiInputConfig.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiInfoPopup.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiMsgBox.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiTextEditPopup.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiTextEditPopupKeyboard.cpp # Math ${CMAKE_CURRENT_SOURCE_DIR}/src/math/Misc.cpp @@ -152,6 +162,7 @@ set(CORE_SOURCES # Utils ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/FileSystemUtil.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/StringUtil.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/ThreadPool.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/TimeUtil.cpp ) diff --git a/es-core/src/AsyncHandle.h b/es-core/src/AsyncHandle.h index 65ffb5317a..5639bdd248 100644 --- a/es-core/src/AsyncHandle.h +++ b/es-core/src/AsyncHandle.h @@ -15,7 +15,7 @@ enum AsyncHandleStatus class AsyncHandle { public: - AsyncHandle() : mStatus(ASYNC_IN_PROGRESS) {}; + AsyncHandle() : mStatus(ASYNC_IN_PROGRESS), mErrorCode(200) {}; virtual ~AsyncHandle() {}; virtual void update() = 0; @@ -23,6 +23,10 @@ class AsyncHandle // Update and return the latest status. inline AsyncHandleStatus status() { update(); return mStatus; } + virtual int getPercent() { return -1; } + + int getErrorCode() { return mErrorCode; } + // User-friendly string of our current status. Will return error message if status() == SEARCH_ERROR. inline std::string getStatusString() { @@ -41,8 +45,11 @@ class AsyncHandle protected: inline void setStatus(AsyncHandleStatus status) { mStatus = status; } + inline void setError(const std::string& error) { setStatus(ASYNC_ERROR); mError = error; } + inline void setError(int errorCode, const std::string& error) { setStatus(ASYNC_ERROR); mError = error; mErrorCode = errorCode; } + int mErrorCode; std::string mError; AsyncHandleStatus mStatus; }; diff --git a/es-core/src/AudioManager.cpp b/es-core/src/AudioManager.cpp index 1d0f8ac495..d1eccaa081 100644 --- a/es-core/src/AudioManager.cpp +++ b/es-core/src/AudioManager.cpp @@ -4,55 +4,21 @@ #include "Settings.h" #include "Sound.h" #include +#include +#include "utils/FileSystemUtil.h" +#include "utils/StringUtil.h" + +#ifdef WIN32 +#include +#else +#include +#endif std::vector> AudioManager::sSoundVector; -SDL_AudioSpec AudioManager::sAudioFormat; std::shared_ptr AudioManager::sInstance; - -void AudioManager::mixAudio(void* /*unused*/, Uint8 *stream, int len) -{ - bool stillPlaying = false; - - //initialize the buffer to "silence" - SDL_memset(stream, 0, len); - - //iterate through all our samples - std::vector>::const_iterator soundIt = sSoundVector.cbegin(); - while (soundIt != sSoundVector.cend()) - { - std::shared_ptr sound = *soundIt; - if(sound->isPlaying()) - { - //calculate rest length of current sample - Uint32 restLength = (sound->getLength() - sound->getPosition()); - if (restLength > (Uint32)len) { - //if stream length is smaller than smaple lenght, clip it - restLength = len; - } - //mix sample into stream - SDL_MixAudio(stream, &(sound->getData()[sound->getPosition()]), restLength, SDL_MIX_MAXVOLUME); - if (sound->getPosition() + restLength < sound->getLength()) - { - //sample hasn't ended yet - stillPlaying = true; - } - //set new sound position. if this is at or beyond the end of the sample, it will stop automatically - sound->setPosition(sound->getPosition() + restLength); - } - //advance to next sound - ++soundIt; - } - - //we have processed all samples. check if some will still be playing - if (!stillPlaying) { - //no. pause audio till a Sound::play() wakes us up - SDL_PauseAudio(1); - } -} - -AudioManager::AudioManager() -{ +AudioManager::AudioManager() : mCurrentMusic(NULL), mInitialized(false), mMusicVolume(MIX_MAX_VOLUME), mVideoPlaying(false) +{ init(); } @@ -63,52 +29,68 @@ AudioManager::~AudioManager() std::shared_ptr & AudioManager::getInstance() { - //check if an AudioManager instance is already created, if not create one - if (sInstance == nullptr && Settings::getInstance()->getBool("EnableSounds")) { + if (sInstance == nullptr) sInstance = std::shared_ptr(new AudioManager); - } + return sInstance; } +bool AudioManager::isInitialized() +{ + if (sInstance == nullptr) + return false; + + return sInstance->mInitialized; +} + void AudioManager::init() { + if (mInitialized) + return; + + mRunningFromPlaylist = false; + if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) { LOG(LogError) << "Error initializing SDL audio!\n" << SDL_GetError(); return; } - //stop playing all Sounds - for(unsigned int i = 0; i < sSoundVector.size(); i++) + //Open the audio device and pause + if (Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 4096) < 0) + LOG(LogError) << "MUSIC Error - Unable to open SDLMixer audio: " << SDL_GetError() << std::endl; + else { - if(sSoundVector.at(i)->isPlaying()) - { - sSoundVector[i]->stop(); - } - } + mInitialized = true; + LOG(LogInfo) << "SDL AUDIO Initialized"; - //Set up format and callback. Play 16-bit stereo audio at 44.1Khz - sAudioFormat.freq = 44100; - sAudioFormat.format = AUDIO_S16; - sAudioFormat.channels = 2; - sAudioFormat.samples = 4096; - sAudioFormat.callback = mixAudio; - sAudioFormat.userdata = NULL; - - //Open the audio device and pause - if (SDL_OpenAudio(&sAudioFormat, NULL) < 0) { - LOG(LogError) << "AudioManager Error - Unable to open SDL audio: " << SDL_GetError() << std::endl; + // Reload sounds + for (unsigned int i = 0; i < sSoundVector.size(); i++) + sSoundVector[i]->init(); } } void AudioManager::deinit() { + if (!mInitialized) + return; + + mInitialized = false; + //stop all playback stop(); + stopMusic(); + + // Stop playing all Sounds & reload them + for (unsigned int i = 0; i < sSoundVector.size(); i++) + sSoundVector[i]->deinit(); + + Mix_HookMusicFinished(nullptr); + Mix_HaltMusic(); + //completely tear down SDL audio. else SDL hogs audio resources and emulators might fail to start... - SDL_CloseAudio(); + Mix_CloseAudio(); SDL_QuitSubSystem(SDL_INIT_AUDIO); - sInstance = NULL; } void AudioManager::registerSound(std::shared_ptr & sound) @@ -120,9 +102,9 @@ void AudioManager::registerSound(std::shared_ptr & sound) void AudioManager::unregisterSound(std::shared_ptr & sound) { getInstance(); - for(unsigned int i = 0; i < sSoundVector.size(); i++) + for (unsigned int i = 0; i < sSoundVector.size(); i++) { - if(sSoundVector.at(i) == sound) + if (sSoundVector.at(i) == sound) { sSoundVector[i]->stop(); sSoundVector.erase(sSoundVector.cbegin() + i); @@ -135,21 +117,207 @@ void AudioManager::unregisterSound(std::shared_ptr & sound) void AudioManager::play() { getInstance(); - - //unpause audio, the mixer will figure out if samples need to be played... - SDL_PauseAudio(0); } void AudioManager::stop() { - //stop playing all Sounds - for(unsigned int i = 0; i < sSoundVector.size(); i++) + // Stop playing all Sounds + for (unsigned int i = 0; i < sSoundVector.size(); i++) + if (sSoundVector.at(i)->isPlaying()) + sSoundVector[i]->stop(); +} + +void AudioManager::findMusic(const std::string &path, std::vector& all_matching_files) +{ + if (!Utils::FileSystem::isDirectory(path)) + return; + + bool anySystem = !Settings::getInstance()->getBool("audio.persystem"); + + auto dirContent = Utils::FileSystem::getDirContent(path); + for (auto it = dirContent.cbegin(); it != dirContent.cend(); ++it) { - if(sSoundVector.at(i)->isPlaying()) + if (Utils::FileSystem::isDirectory(*it)) { - sSoundVector[i]->stop(); + if (*it == "." || *it == "..") + continue; + + if (anySystem || mSystemName == Utils::FileSystem::getFileName(*it)) + findMusic(*it, all_matching_files); } + else + { + std::string extension = Utils::String::toLower(Utils::FileSystem::getExtension(*it)); + if (extension == ".mp3" || extension == ".ogg") + all_matching_files.push_back(*it); + } + } +} + +void AudioManager::playRandomMusic(bool continueIfPlaying) +{ + if (!mInitialized) + return; + + std::vector musics; + + // check in Theme music directory + if (!mCurrentThemeMusicDirectory.empty()) + findMusic(mCurrentThemeMusicDirectory, musics); + + // check in User music directory + if (musics.empty() && !Settings::getInstance()->getString("UserMusicDirectory").empty()) + findMusic(Settings::getInstance()->getString("MusicDirectory"), musics); + + // check in System music directory + if (musics.empty() && !Settings::getInstance()->getString("MusicDirectory").empty()) + findMusic(Settings::getInstance()->getString("MusicDirectory"), musics); + + // check in .emulationstation/music directory + if (musics.empty()) + findMusic(Utils::FileSystem::getHomePath() + "/.emulationstation/music", musics); + + if (musics.empty()) + return; + +#if defined(WIN32) + srand(time(NULL) % getpid()); +#else + srand(time(NULL) % getpid() + getppid()); +#endif + + int randomIndex = rand() % musics.size(); + + // continue playing ? + if (mCurrentMusic != NULL && continueIfPlaying) + return; + + playMusic(musics.at(randomIndex)); + mRunningFromPlaylist = true; +} + +void AudioManager::playMusic(std::string path) +{ + if (!mInitialized) + return; + + // free the previous music + stopMusic(); + + // load a new music + mCurrentMusic = Mix_LoadMUS(path.c_str()); + if (mCurrentMusic == NULL) + { + LOG(LogError) << Mix_GetError() << " for " << path; + return; + } + + if (Mix_FadeInMusic(mCurrentMusic, 1, 1000) == -1) + { + stopMusic(); + return; + } + + Mix_HookMusicFinished(AudioManager::onMusicFinished); + mCurrentMusicPath = path; + mCurrentSong = Utils::FileSystem::getStem(path); +} + +void AudioManager::onMusicFinished() +{ + AudioManager::getInstance()->playRandomMusic(false); +} + +void AudioManager::stopMusic() +{ + if (mCurrentMusic == NULL) + return; + + Mix_HookMusicFinished(nullptr); + Mix_HaltMusic(); + Mix_FreeMusic(mCurrentMusic); + + mCurrentMusicPath = ""; + mCurrentMusic = NULL; +} + +void AudioManager::themeChanged(const std::shared_ptr& theme, bool force) +{ + if (!force && mSystemName == theme->getSystemThemeFolder()) + return; + + mSystemName = theme->getSystemThemeFolder(); + mCurrentThemeMusicDirectory = ""; + + if (!Settings::getInstance()->getBool("audio.bgmusic")) + return; + + const ThemeData::ThemeElement* elem = theme->getElement("system", "directory", "sound"); + + if (Settings::getInstance()->getBool("audio.thememusics")) + { + if (elem && elem->has("path")) + mCurrentThemeMusicDirectory = elem->get("path"); + + std::string bgSound; + + elem = theme->getElement("system", "bgsound", "sound"); + if (elem && elem->has("path") && Utils::FileSystem::exists(elem->get("path"))) + { + bgSound = Utils::FileSystem::getCanonicalPath(elem->get("path")); + if (bgSound == mCurrentMusicPath) + return; + } + + // Found a music for the system + if (!bgSound.empty()) + { + mRunningFromPlaylist = false; + playMusic(bgSound); + return; + } + } + + + mSystemName = theme->getSystemThemeFolder(); + if (!mRunningFromPlaylist || Settings::getInstance()->getBool("audio.persystem")) + playRandomMusic(false); +} + +void AudioManager::setVideoPlaying(bool state) +{ + if (sInstance == nullptr || !sInstance->mInitialized || !Settings::getInstance()->getBool("audio.bgmusic")) + return; + + if (state && !Settings::getInstance()->getBool("VideoLowersMusic")) + return; + + sInstance->mVideoPlaying = state; +} + +void AudioManager::update(int deltaTime) +{ + if (sInstance == nullptr || !sInstance->mInitialized || !Settings::getInstance()->getBool("audio.bgmusic")) + return; + + float deltaVol = deltaTime / 8.0f; + + #define MINVOL 5 + + if (sInstance->mVideoPlaying && sInstance->mMusicVolume > MINVOL) + { + sInstance->mMusicVolume -= deltaVol; + if (sInstance->mMusicVolume < MINVOL) + sInstance->mMusicVolume = MINVOL; + + Mix_VolumeMusic((int) sInstance->mMusicVolume); + } + else if (!sInstance->mVideoPlaying && sInstance->mMusicVolume < MIX_MAX_VOLUME) + { + sInstance->mMusicVolume += deltaVol; + if (sInstance->mMusicVolume > MIX_MAX_VOLUME) + sInstance->mMusicVolume = MIX_MAX_VOLUME; + + Mix_VolumeMusic((int)sInstance->mMusicVolume); } - //pause audio - SDL_PauseAudio(1); } diff --git a/es-core/src/AudioManager.h b/es-core/src/AudioManager.h index e27ce1dc2e..22598cbe3b 100644 --- a/es-core/src/AudioManager.h +++ b/es-core/src/AudioManager.h @@ -5,22 +5,19 @@ #include #include #include +#include "SDL_mixer.h" +#include "ThemeData.h" +#include class Sound; class AudioManager -{ - static SDL_AudioSpec sAudioFormat; - static std::vector> sSoundVector; - static std::shared_ptr sInstance; - - static void mixAudio(void *unused, Uint8 *stream, int len); - - AudioManager(); +{ public: static std::shared_ptr & getInstance(); - + static bool isInitialized(); + void init(); void deinit(); @@ -30,7 +27,56 @@ class AudioManager void play(); void stop(); + void playRandomMusic(bool continueIfPlaying = true); + void stopMusic(); + void themeChanged(const std::shared_ptr& theme, bool force=false); + + void setSystemName(std::string name) { + mSystemName = name; + } + + std::string popSongName() + { + if (!mCurrentSong.empty()) + { + std::string ret = mCurrentSong; + mCurrentSong = ""; + return ret; + } + + return ""; + } + virtual ~AudioManager(); + + float mMusicVolume; + int mVideoPlaying; + + static void setVideoPlaying(bool state); + static void update(int deltaTime); + +private: + AudioManager(); + + static std::vector> sSoundVector; + static std::shared_ptr sInstance; + + + static void onMusicFinished(); + + void findMusic(const std::string &path, std::vector& all_matching_files); + void playMusic(std::string path); + + std::string mCurrentSong; + std::string mCurrentMusicPath; + std::string mSystemName; + std::string mCurrentThemeMusicDirectory; + bool mRunningFromPlaylist; + bool mInitialized; + + Mix_Music* mCurrentMusic; + + }; #endif // ES_CORE_AUDIO_MANAGER_H diff --git a/es-core/src/EsLocale.cpp b/es-core/src/EsLocale.cpp new file mode 100644 index 0000000000..5b69ea5e33 --- /dev/null +++ b/es-core/src/EsLocale.cpp @@ -0,0 +1,220 @@ +#include "EsLocale.h" +#include "resources/ResourceManager.h" +#include "Settings.h" +#include "utils/FileSystemUtil.h" + +#include + +std::map EsLocale::mItems; +std::string EsLocale::mCurrentLanguage = "en"; +bool EsLocale::mCurrentLanguageLoaded = true; // By default, 'en' is considered loaded + +// List of all possible plural forms here +// https://github.com/translate/l10n-guide/blob/master/docs/l10n/pluralforms.rst +// Test rules without spaces & without parenthesis - there are 19 distinct rules + +PluralRule rules[] = { + { "en", "n!=1", [](int n) { return n != 1 ? 1 : 0; } }, + { "fr", "n>1", [](int n) { return n>1 ? 1 : 0; } }, + { "jp", "0", [](int n) { return 0; } }, + { "ru", "n%10==1&&n%100!=11?0:n%10>=2&&n%10<=4&&n%100<10||n%100>=20?1:2", [](int n) { return n % 10 == 1 && n % 100 != 11 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2; } }, + { "ar", "n==0?0:n==1?1:n==2?2:n%100>=3&&n%100<=10?3:n%100>=11?4:5", [](int n) { return n == 0 ? 0 : n == 1 ? 1 : n == 2 ? 2 : n % 100 >= 3 && n % 100 <= 10 ? 3 : n % 100 >= 11 ? 4 : 5; } }, + { "pl", "n==1?0:n%10>=2&&n%10<=4&&n%100<10||n%100>=20?1:2", [](int n) { return n == 1 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100<10 || n % 100 >= 20) ? 1 : 2; } }, + { "ga", "n==1?0:n==2?1:n>2&&n<7?2:n>6&&n<11?3:4", [](int n) { return n == 1 ? 0 : n == 2 ? 1 : (n>2 && n<7) ? 2 : (n>6 && n<11) ? 3 : 4; } }, + { "gd", "n==1||n==11?0:n==2||n==12?1:n>2&&n<20?2:3", [](int n) { return (n == 1 || n == 11) ? 0 : (n == 2 || n == 12) ? 1 : (n>2 && n<20) ? 2 : 3; } }, + { "mk", "n==1||n%10==1?0:1", [](int n) { return n == 1 || n % 10 == 1 ? 0 : 1; } }, + { "is", "n%10!=1||n%100==11", [](int n) { return (n % 10 != 1 || n % 100 == 11) ? 1 : 0; } }, + { "lv", "n%10==1&&n%100!=11?0:n!=0?1:2", [](int n) { return n % 10 == 1 && n % 100 != 11 ? 0 : n != 0 ? 1 : 2; } }, + { "lt", "n%10==1&&n%100!=11?0:n%10>=2&&n%100<10||n%100>=20?1:2", [](int n) { return n % 10 == 1 && n % 100 != 11 ? 0 : n % 10 >= 2 && (n % 100<10 || n % 100 >= 20) ? 1 : 2; } }, + { "mn", "n==0?0:n==1?1:2", [](int n) { return n == 0 ? 0 : n == 1 ? 1 : 2; } }, + { "ro", "n==1?0:n==0||n%100>0&&n%100<20?1:2", [](int n) { return n == 1 ? 0 : (n == 0 || (n % 100>0 && n % 100<20)) ? 1 : 2; } }, + { "cs", "n==1?0:n>=2&&n<=4?1:2", [](int n) { return (n == 1) ? 0 : (n >= 2 && n <= 4) ? 1 : 2; } }, + { "sl", "n%100==1?0:n%100==2?1:n%100==3||n%100==4?2:3", [](int n) { return n % 100 == 1 ? 0 : n % 100 == 2 ? 1 : n % 100 == 3 || n % 100 == 4 ? 2 : 3; } }, + { "mt", "n==1?0:n==0||n%100>1&&n%100<11?1:n%100>10&&n%100<20?2:3", [](int n) { return n == 1 ? 0 : n == 0 || (n % 100>1 && n % 100<11) ? 1 : (n % 100>10 && n % 100<20) ? 2 : 3; } }, + { "cy", "n==1?0:n==2?1:n!=8&&n!=11?2:3", [](int n) { return (n == 1) ? 0 : (n == 2) ? 1 : (n != 8 && n != 11) ? 2 : 3; } }, + { "kw", "n==1?0:n==2?1:n==3?2:3", [](int n) { return (n == 1) ? 0 : (n == 2) ? 1 : (n == 3) ? 2 : 3; } } +}; + +PluralRule EsLocale::mPluralRule = rules[0]; + +const std::string EsLocale::getText(const std::string text) +{ + checkLocalisationLoaded(); + + auto item = mItems.find(text); + if (item != mItems.cend()) + return item->second; + + return text; +} + +const std::string EsLocale::nGetText(const std::string msgid, const std::string msgid_plural, int n) +{ + if (mCurrentLanguage.empty() || mCurrentLanguage == "en") // English default + return n != 1 ? msgid_plural : msgid; + + if (mPluralRule.rule.empty()) + return n != 1 ? getText(msgid_plural) : getText(msgid); + + checkLocalisationLoaded(); + + int pluralId = mPluralRule.evaluate(n); + if (pluralId == 0) + return getText(msgid); + + auto item = mItems.find(std::to_string(pluralId) + "@" + msgid_plural); + if (item != mItems.cend()) + return item->second; + + item = mItems.find(msgid_plural); + if (item != mItems.cend()) + return item->second; + + return msgid_plural; +} + +const std::vector pluralRules(rules, rules + sizeof(rules) / sizeof(rules[0])); + +void EsLocale::checkLocalisationLoaded() +{ + if (mCurrentLanguageLoaded) + { + if (Settings::getInstance()->getString("Language") == mCurrentLanguage) + return; + + mCurrentLanguage = Settings::getInstance()->getString("Language"); + } + + mCurrentLanguageLoaded = true; + mPluralRule = rules[0]; + + mItems.clear(); + + std::string xmlpath = ResourceManager::getInstance()->getResourcePath(":/locale/" + mCurrentLanguage + "/emulationstation2.po"); + if (!Utils::FileSystem::exists(xmlpath)) + xmlpath = ResourceManager::getInstance()->getResourcePath(":/locale/" + mCurrentLanguage + "/LC_MESSAGES/emulationstation2.po"); + + if (!Utils::FileSystem::exists(xmlpath)) + { + auto shortNameDivider = mCurrentLanguage.find("_"); + if (shortNameDivider != std::string::npos) + { + auto shortName = mCurrentLanguage.substr(0, shortNameDivider); + + xmlpath = ResourceManager::getInstance()->getResourcePath(":/locale/" + shortName + "/emulationstation2.po"); + if (!Utils::FileSystem::exists(xmlpath)) + xmlpath = ResourceManager::getInstance()->getResourcePath(":/locale/" + shortName + "/LC_MESSAGES/emulationstation2.po"); + } + } + + std::string msgid; + std::string msgid_plural; + + std::string line; + + std::ifstream file(xmlpath); + while (std::getline(file, line)) + { + if (line.find("\"Plural-Forms:") == 0) + { + auto start = line.find("plural="); + if (start != std::string::npos) + { + std::string plural; + + auto end = line.find(";", start + 1); + if (end == std::string::npos) + { + plural = line.substr(start + 7, line.size() - start - 7 - 1); + + std::getline(file, line); + end = line.find(";", start + 1); + if (end != std::string::npos) + plural += line.substr(1, end - 1); + } + else + plural = line.substr(start + 7, end - start - 7); + + plural = Utils::String::replace(plural, " ", ""); + + if (Utils::String::endsWith(plural, ";")) + plural = plural.substr(0, plural.size() - 1); + + // if (Utils::String::startsWith(plural, "(") && Utils::String::endsWith(plural, ")")) + // plural = plural.substr(1, plural.size() - 2); + plural = Utils::String::replace(plural, "(", ""); + plural = Utils::String::replace(plural, ")", ""); + + for (auto iter = pluralRules.cbegin(); iter != pluralRules.cend(); iter++) + { + if (plural == iter->rule) + { + mPluralRule = *iter; + break; + } + } + } + } + else if (line.find("msgid_plural") == 0) + { + auto start = line.find("\""); + if (start != std::string::npos && !msgid.empty()) + { + auto end = line.find("\"", start + 1); + if (end != std::string::npos) + msgid_plural = line.substr(start + 1, end - start - 1); + } + } + else if (line.find("msgid") == 0) + { + msgid = ""; + msgid_plural = ""; + + auto start = line.find("\""); + if (start != std::string::npos) + { + auto end = line.find("\"", start + 1); + if (end != std::string::npos) + msgid = line.substr(start + 1, end - start - 1); + } + } + else if (line.find("msgstr") == 0) + { + std::string idx; + + if (!msgid_plural.empty()) + { + auto idxStart = line.find("["); + if (idxStart != std::string::npos) + { + auto idxEnd = line.find("]", idxStart + 1); + if (idxEnd != std::string::npos) + idx = line.substr(idxStart + 1, idxEnd - idxStart - 1); + } + } + + auto start = line.find("\""); + if (start != std::string::npos) + { + auto end = line.find("\"", start + 1); + if (end != std::string::npos) + { + std::string msgstr = line.substr(start + 1, end - start - 1); + if (!msgid.empty() && !msgstr.empty()) + if (idx.empty() || idx == "0") + mItems[msgid] = msgstr; + + if (!msgid_plural.empty() && !msgstr.empty()) + { + if (!idx.empty() && idx != "0") + mItems[idx + "@" + msgid_plural] = msgstr; + else + mItems[msgid_plural] = msgstr; + } + } + } + } + } +} + + diff --git a/es-core/src/EsLocale.h b/es-core/src/EsLocale.h new file mode 100644 index 0000000000..e6b0b82dfc --- /dev/null +++ b/es-core/src/EsLocale.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include +#include "utils/StringUtil.h" + +struct PluralRule +{ + std::string key; + std::string rule; + std::function evaluate; +}; + +class EsLocale +{ +public: + static const std::string getText(const std::string text); + static const std::string nGetText(const std::string msgid, const std::string msgid_plural, int n); + + static const std::string getLanguage() { return mCurrentLanguage; } + + static const void reset() { mCurrentLanguageLoaded = false; } + +private: + static void checkLocalisationLoaded(); + static std::map mItems; + static std::string mCurrentLanguage; + static bool mCurrentLanguageLoaded; + + static PluralRule mPluralRule; +}; + + +#if defined(_WIN32) + #define UNICODE_CHARTYPE wchar_t* + #define _L(x) L ## x + #define _U(x) Utils::String::convertFromWideString(L ## x) + + #define _(x) EsLocale::getText(x) +#else + + #define UNICODE_CHARTYPE char* + #define _L(x) x + #define _U(x) x + + #define _(x) EsLocale::getText(x) +#endif // _WIN32 + diff --git a/es-core/src/GuiComponent.cpp b/es-core/src/GuiComponent.cpp index c56fc68676..aae592e10c 100644 --- a/es-core/src/GuiComponent.cpp +++ b/es-core/src/GuiComponent.cpp @@ -2,15 +2,19 @@ #include "animations/Animation.h" #include "animations/AnimationController.h" -#include "renderers/Renderer.h" +#include "animations/LambdaAnimation.h" #include "Log.h" +#include "renderers/Renderer.h" #include "ThemeData.h" #include "Window.h" #include +bool GuiComponent::ALLOWANIMATIONS = true; + GuiComponent::GuiComponent(Window* window) : mWindow(window), mParent(NULL), mOpacity(255), mPosition(Vector3f::Zero()), mOrigin(Vector2f::Zero()), mRotationOrigin(0.5, 0.5), - mSize(Vector2f::Zero()), mTransform(Transform4x4f::Identity()), mIsProcessing(false), mVisible(true) + mSize(Vector2f::Zero()), mTransform(Transform4x4f::Identity()), mIsProcessing(false), mVisible(true), + mStaticExtra(false) { for(unsigned char i = 0; i < MAX_ANIMATIONS; i++) mAnimationMap[i] = NULL; @@ -117,7 +121,7 @@ Vector2f GuiComponent::getSize() const void GuiComponent::setSize(float w, float h) { mSize = Vector2f(w, h); - onSizeChanged(); + onSizeChanged(); } float GuiComponent::getRotation() const @@ -130,12 +134,12 @@ void GuiComponent::setRotation(float rotation) mRotation = rotation; } -float GuiComponent::getScale() const +Vector3f GuiComponent::getScale() const { return mScale; } -void GuiComponent::setScale(float scale) +void GuiComponent::setScale(Vector3f scale) { mScale = scale; } @@ -151,7 +155,7 @@ void GuiComponent::setZIndex(float z) } float GuiComponent::getDefaultZIndex() const -{ +{ return mDefaultZIndex; } @@ -175,6 +179,15 @@ Vector2f GuiComponent::getCenter() const mPosition.y() - (getSize().y() * mOrigin.y()) + getSize().y() / 2); } +bool GuiComponent::isChild(GuiComponent* cmp) +{ + for (auto i = mChildren.cbegin(); i != mChildren.cend(); i++) + if (*i == cmp) + return true; + + return false; +} + //Children stuff. void GuiComponent::addChild(GuiComponent* cmp) { @@ -293,6 +306,16 @@ std::string GuiComponent::getValue() const return ""; } +void GuiComponent::setTag(const std::string& value) +{ + mTag = value; +} + +std::string GuiComponent::getTag() const +{ + return mTag; +} + void GuiComponent::textInput(const char* text) { for(auto iter = mChildren.cbegin(); iter != mChildren.cend(); iter++) @@ -459,7 +482,15 @@ void GuiComponent::updateHelpPrompts() HelpStyle GuiComponent::getHelpStyle() { - return HelpStyle(); + HelpStyle style = HelpStyle(); + + if (ThemeData::getDefaultTheme() != nullptr) + { + std::shared_ptr theme = std::shared_ptr(ThemeData::getDefaultTheme(), [](ThemeData*) {}); + style.applyTheme(theme, "system"); + } + + return style; } bool GuiComponent::isProcessing() const @@ -495,4 +526,62 @@ void GuiComponent::topWindow(bool isTop) { for(unsigned int i = 0; i < getChildCount(); i++) getChild(i)->topWindow(isTop); +} + +void GuiComponent::animateTo(Vector2f from, Vector2f to, unsigned int flags, int delay) +{ + if ((flags & AnimateFlags::POSITION) == 0) + from = to; + + Vector3f scale = mScale; + + float x1 = from.x(); + float x2 = to.x(); + float y1 = from.y(); + float y2 = to.y(); + + if (Settings::getInstance()->getString("PowerSaverMode") == "instant" || Settings::getInstance()->getString("TransitionStyle") == "instant") + setPosition(x2, y2); + else + { + setPosition(x1, y1); + + if ((flags & AnimateFlags::OPACITY) == AnimateFlags::OPACITY) + setOpacity(0); + + if ((flags & AnimateFlags::SCALE) == AnimateFlags::SCALE) + mScale = Vector3f(0, 0, 1); + + auto fadeFunc = [this, x1, x2, y1, y2, flags, scale](float t) { + + t -= 1; // cubic ease out + float pct = Math::lerp(0, 1, t*t*t + 1); + + if ((flags & AnimateFlags::OPACITY) == AnimateFlags::OPACITY) + setOpacity(pct*255.0); + + if ((flags & AnimateFlags::SCALE) == AnimateFlags::SCALE) + mScale = Vector3f(pct * scale.x(), pct * scale.y(), pct * scale.z()); + + float x = (x1 + mSize.x() / 2 - (mSize.x() / 2 * mScale.x())) * (1 - pct) + (x2 + mSize.x() / 2 - (mSize.x() / 2 * mScale.x())) * pct; + float y = (y1 + mSize.x() / 2 - (mSize.y() / 2 * mScale.y())) * (1 - pct) + (y2 + mSize.y() / 2 - (mSize.y() / 2 * mScale.y())) * pct; + + if (mScale.x() != 0.0f) + setPosition(x, y); + }; + + setAnimation(new LambdaAnimation(fadeFunc, delay), 0, [this, fadeFunc, x2, y2, flags, scale] + { + if ((flags & AnimateFlags::SCALE) == AnimateFlags::SCALE) + mScale = scale; + + if ((flags & AnimateFlags::OPACITY) == AnimateFlags::OPACITY) + setOpacity(255); + + float x = x2 + mSize.x() / 2 - (mSize.x() / 2 * mScale.x()); + float y = y2 + mSize.y() / 2 - (mSize.y() / 2 * mScale.y()); + + setPosition(x, y); + }); + } } \ No newline at end of file diff --git a/es-core/src/GuiComponent.h b/es-core/src/GuiComponent.h index 0820773897..e10e6bb07d 100644 --- a/es-core/src/GuiComponent.h +++ b/es-core/src/GuiComponent.h @@ -7,6 +7,7 @@ #include "HelpPrompt.h" #include "HelpStyle.h" #include "InputConfig.h" +#include "EsLocale.h" #include #include @@ -17,12 +18,28 @@ class InputConfig; class ThemeData; class Window; +namespace AnimateFlags +{ + enum Flags : unsigned int + { + POSITION = 1, + SCALE = 2, + OPACITY = 4, + ALL = 0xFFFFFFFF + }; +} + class GuiComponent { public: GuiComponent(Window* window); virtual ~GuiComponent(); + template + bool isKindOf() { return (dynamic_cast(this) != nullptr); } + + static bool ALLOWANIMATIONS; + virtual void textInput(const char* text); //Called when input is received. @@ -61,14 +78,16 @@ class GuiComponent void setSize(float w, float h); virtual void onSizeChanged() {}; + virtual void setColor(unsigned int color) {}; + virtual Vector2f getRotationSize() const { return getSize(); }; float getRotation() const; void setRotation(float rotation); inline void setRotationDegrees(float rotation) { setRotation((float)ES_DEG_TO_RAD(rotation)); } - float getScale() const; - void setScale(float scale); + Vector3f getScale() const; + void setScale(Vector3f scale); float getZIndex() const; void setZIndex(float zIndex); @@ -91,6 +110,7 @@ class GuiComponent void sortChildren(); unsigned int getChildCount() const; GuiComponent* getChild(unsigned int i) const; + bool isChild(GuiComponent* cmp); // animation will be automatically deleted when it completes or is stopped. bool isAnimationPlaying(unsigned char slot) const; @@ -112,6 +132,10 @@ class GuiComponent virtual std::string getValue() const; virtual void setValue(const std::string& value); + + virtual std::string getTag() const; + virtual void setTag(const std::string& value); + virtual void onFocusGained() {}; virtual void onFocusLost() {}; @@ -137,6 +161,12 @@ class GuiComponent // Returns true if the component is busy doing background processing (e.g. HTTP downloads) bool isProcessing() const; + void animateTo(Vector2f from, Vector2f to, unsigned int flags = 0xFFFFFFFF, int delay = 350); + void animateTo(Vector2f from, unsigned int flags = AnimateFlags::OPACITY | AnimateFlags::SCALE, int delay = 350) { animateTo(from, from, flags, delay); } + + bool isStaticExtra() const { return mStaticExtra; } + void setIsStaticExtra(bool value) { mStaticExtra = value; } + protected: void renderChildren(const Transform4x4f& transform) const; void updateSelf(int deltaTime); // updates animations @@ -148,13 +178,15 @@ class GuiComponent GuiComponent* mParent; std::vector mChildren; + std::string mTag; + Vector3f mPosition; Vector2f mOrigin; Vector2f mRotationOrigin; Vector2f mSize; float mRotation = 0.0; - float mScale = 1.0; + Vector3f mScale = Vector3f(1.0, 1.0, 1.0); float mDefaultZIndex = 0; float mZIndex = 0; @@ -162,6 +194,8 @@ class GuiComponent bool mIsProcessing; bool mVisible; + bool mStaticExtra; + public: const static unsigned char MAX_ANIMATIONS = 4; diff --git a/es-core/src/HelpStyle.cpp b/es-core/src/HelpStyle.cpp index 3d3d4d6195..67635176c2 100644 --- a/es-core/src/HelpStyle.cpp +++ b/es-core/src/HelpStyle.cpp @@ -8,11 +8,10 @@ HelpStyle::HelpStyle() origin = Vector2f(0.0f, 0.0f); iconColor = 0x777777FF; textColor = 0x777777FF; + font = nullptr; - if(FONT_SIZE_SMALL != 0) + if (FONT_SIZE_SMALL != 0) font = Font::get(FONT_SIZE_SMALL); - else - font = nullptr; } void HelpStyle::applyTheme(const std::shared_ptr& theme, const std::string& view) @@ -35,4 +34,37 @@ void HelpStyle::applyTheme(const std::shared_ptr& theme, const std::s if(elem->has("fontPath") || elem->has("fontSize")) font = Font::getFromTheme(elem, ThemeFlags::ALL, font); + + if (elem->has("iconUpDown")) + iconMap["up/down"] = elem->get("iconUpDown"); + + if (elem->has("iconLeftRight")) + iconMap["left/right"] = elem->get("iconLeftRight"); + + if (elem->has("iconUpDownLeftRight")) + iconMap["up/down/left/right"] = elem->get("iconUpDownLeftRight"); + + if (elem->has("iconA")) + iconMap["a"] = elem->get("iconA"); + + if (elem->has("iconB")) + iconMap["b"] = elem->get("iconB"); + + if (elem->has("iconX")) + iconMap["x"] = elem->get("iconX"); + + if (elem->has("iconY")) + iconMap["y"] = elem->get("iconY"); + + if (elem->has("iconL")) + iconMap["l"] = elem->get("iconL"); + + if (elem->has("iconR")) + iconMap["r"] = elem->get("iconR"); + + if (elem->has("iconStart")) + iconMap["start"] = elem->get("iconStart"); + + if (elem->has("iconSelect")) + iconMap["select"] = elem->get("iconSelect"); } diff --git a/es-core/src/HelpStyle.h b/es-core/src/HelpStyle.h index 8682a8d864..72aa8df9b2 100644 --- a/es-core/src/HelpStyle.h +++ b/es-core/src/HelpStyle.h @@ -5,6 +5,7 @@ #include "math/Vector2f.h" #include #include +#include class Font; class ThemeData; @@ -16,7 +17,8 @@ struct HelpStyle unsigned int iconColor; unsigned int textColor; std::shared_ptr font; - + std::map iconMap; + HelpStyle(); // default values void applyTheme(const std::shared_ptr& theme, const std::string& view); }; diff --git a/es-core/src/HttpReq.cpp b/es-core/src/HttpReq.cpp index feaea8a3cb..a23983cc99 100644 --- a/es-core/src/HttpReq.cpp +++ b/es-core/src/HttpReq.cpp @@ -1,8 +1,20 @@ #include "HttpReq.h" #include "utils/FileSystemUtil.h" +#include "utils/StringUtil.h" #include "Log.h" #include +#include +#include + +#ifdef WIN32 +#include +#else +#include +#endif + +#include +static std::mutex mMutex; CURLM* HttpReq::s_multi_handle = curl_multi_init(); @@ -33,13 +45,60 @@ std::string HttpReq::urlEncode(const std::string &s) bool HttpReq::isUrl(const std::string& str) { //the worst guess - return (!str.empty() && !Utils::FileSystem::exists(str) && + return (!str.empty() && !Utils::FileSystem::exists(str) && (str.find("http://") != std::string::npos || str.find("https://") != std::string::npos || str.find("www.") != std::string::npos)); } -HttpReq::HttpReq(const std::string& url) +#ifdef WIN32 +LONG _regGetDWORD(HKEY hKey, const std::string &strPath, const std::string &strValueName) +{ + HKEY hSubKey; + LONG nRet = ::RegOpenKeyEx(hKey, strPath.c_str(), 0L, KEY_QUERY_VALUE, &hSubKey); + if (nRet == ERROR_SUCCESS) + { + DWORD dwBufferSize(sizeof(DWORD)); + DWORD nResult(0); + + nRet = ::RegQueryValueExA(hSubKey, strValueName.c_str(), 0, NULL, reinterpret_cast(&nResult), &dwBufferSize); + ::RegCloseKey(hSubKey); + + if (nRet == ERROR_SUCCESS) + return nResult; + } + + return 0; +} + +std::string _regGetString(HKEY hKey, const std::string &strPath, const std::string &strValueName) +{ + std::string ret; + + HKEY hSubKey; + LONG nRet = ::RegOpenKeyEx(hKey, strPath.c_str(), 0L, KEY_QUERY_VALUE, &hSubKey); + if (nRet == ERROR_SUCCESS) + { + char szBuffer[1024]; + DWORD dwBufferSize = sizeof(szBuffer); + + nRet = ::RegQueryValueExA(hSubKey, strValueName.c_str(), 0, NULL, (LPBYTE)szBuffer, &dwBufferSize); + ::RegCloseKey(hSubKey); + + if (nRet == ERROR_SUCCESS) + ret = szBuffer; + } + + return ret; +} +#endif + +HttpReq::HttpReq(const std::string& url, const std::string outputFilename) : mStatus(REQ_IN_PROGRESS), mHandle(NULL) { + mUrl = url; + mFilePath = outputFilename; + + mPosition = -1; + mPercent = -1; mHandle = curl_easy_init(); if(mHandle == NULL) @@ -77,7 +136,7 @@ HttpReq::HttpReq(const std::string& url) } //set curl restrict redirect protocols - err = curl_easy_setopt(mHandle, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); + err = curl_easy_setopt(mHandle, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); if(err != CURLE_OK) { mStatus = REQ_IO_ERROR; @@ -103,10 +162,69 @@ HttpReq::HttpReq(const std::string& url) return; } + // Set fake user agent + err = curl_easy_setopt(mHandle, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT x.y; Win64; x64; rv:10.0) Gecko/20100101 Firefox/10.0"); + if (err != CURLE_OK) + { + mStatus = REQ_IO_ERROR; + onError(curl_easy_strerror(err)); + return; + } + +#ifdef WIN32 + // Setup system proxy on Windows if required + if (_regGetDWORD(HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings", "ProxyEnable")) + { + auto proxyServer = _regGetString(HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings", "ProxyServer"); + if (!proxyServer.empty()) + { + std::string protocol = (url.find("https:/") == 0 ? "https=" : "http="); + + size_t pxs = proxyServer.find(protocol); + if (pxs != std::string::npos) + { + size_t pxe = proxyServer.find(";", pxs); + if (pxe == std::string::npos) + pxe = proxyServer.size() - 1; + + proxyServer = proxyServer.substr(pxs + protocol.size(), pxe - pxs - protocol.size()); + } + + if (!proxyServer.empty()) + { + CURLcode ret; + curl_easy_setopt(mHandle, CURLOPT_PROXY, proxyServer.c_str()); + curl_easy_setopt(mHandle, CURLOPT_PROXYTYPE, CURLPROXY_HTTP); + } + } + } +#endif + + std::unique_lock lock(mMutex); + + if (!mFilePath.empty()) + { + mTempStreamPath = outputFilename + ".tmp"; + + Utils::FileSystem::removeFile(mTempStreamPath); + + mStream.open(mTempStreamPath, std::ios_base::out | std::ios_base::binary); + if (!mStream.is_open()) + { + mStatus = REQ_IO_ERROR; + onError("IO Error (disk is Readonly ?)"); + return; + } + + Utils::FileSystem::removeFile(outputFilename); + } + //add the handle to our multi CURLMcode merr = curl_multi_add_handle(s_multi_handle, mHandle); if(merr != CURLM_OK) { + closeStream(); + mStatus = REQ_IO_ERROR; onError(curl_multi_strerror(merr)); return; @@ -115,8 +233,27 @@ HttpReq::HttpReq(const std::string& url) s_requests[mHandle] = this; } +void HttpReq::closeStream() +{ + if (mFilePath.empty()) + return; + + if (mStream.is_open()) + { + mStream.flush(); + mStream.close(); + } +} + HttpReq::~HttpReq() { + std::unique_lock lock(mMutex); + + closeStream(); + + if (!mTempStreamPath.empty()) + Utils::FileSystem::removeFile(mTempStreamPath); + if(mHandle) { s_requests.erase(mHandle); @@ -132,12 +269,16 @@ HttpReq::~HttpReq() HttpReq::Status HttpReq::status() { - if(mStatus == REQ_IN_PROGRESS) + std::unique_lock lock(mMutex); + + if (mStatus == REQ_IN_PROGRESS) { int handle_count; CURLMcode merr = curl_multi_perform(s_multi_handle, &handle_count); - if(merr != CURLM_OK && merr != CURLM_CALL_MULTI_PERFORM) + if (merr != CURLM_OK && merr != CURLM_CALL_MULTI_PERFORM) { + closeStream(); + mStatus = REQ_IO_ERROR; onError(curl_multi_strerror(merr)); return mStatus; @@ -145,22 +286,66 @@ HttpReq::Status HttpReq::status() int msgs_left; CURLMsg* msg; - while((msg = curl_multi_info_read(s_multi_handle, &msgs_left)) != nullptr) + while ((msg = curl_multi_info_read(s_multi_handle, &msgs_left)) != nullptr) { - if(msg->msg == CURLMSG_DONE) + if (msg->msg == CURLMSG_DONE) { HttpReq* req = s_requests[msg->easy_handle]; - - if(req == NULL) + if (req == NULL) { LOG(LogError) << "Cannot find easy handle!"; continue; } - if(msg->data.result == CURLE_OK) + req->closeStream(); + + if (req->mStatus == REQ_FILESTREAM_ERROR) + { + std::string err = "File stream error (disk full ?)"; + req->onError(err.c_str()); + } + else if (msg->data.result == CURLE_OK) + { + int http_status_code; + curl_easy_getinfo(msg->easy_handle, CURLINFO_RESPONSE_CODE, &http_status_code); + + if (http_status_code < 200 || http_status_code > 299) + { + std::string err; + + if (http_status_code >= 400 && http_status_code < 499) + { + if (mFilePath.empty()) + err = req->getContent(); + + req->mStatus = (Status)http_status_code; + } + else + req->mStatus = REQ_IO_ERROR; + + if (err.empty()) + err = "HTTP status " + std::to_string(http_status_code); + + req->onError(err.c_str()); + } + else + { + if (!mFilePath.empty()) + { + if (std::rename(mTempStreamPath.c_str(), mFilePath.c_str()) == 0) + req->mStatus = REQ_SUCCESS; + else + { + req->mStatus = REQ_IO_ERROR; + req->onError("file rename failed"); + } + } + else + req->mStatus = REQ_SUCCESS; + } + } + else { - req->mStatus = REQ_SUCCESS; - }else{ req->mStatus = REQ_IO_ERROR; req->onError(curl_easy_strerror(msg->data.result)); } @@ -171,15 +356,40 @@ HttpReq::Status HttpReq::status() return mStatus; } -std::string HttpReq::getContent() const +std::string HttpReq::getContent() { - assert(mStatus == REQ_SUCCESS); - return mContent.str(); + if (mFilePath.empty()) + return mContent.str(); + + try + { + closeStream(); + + if (!Utils::FileSystem::exists(mTempStreamPath)) + return ""; + + std::ifstream ifs(mTempStreamPath, std::ios_base::in | std::ios_base::binary); + if (ifs.bad()) + return ""; + + std::stringstream ofs; + ofs << ifs.rdbuf(); + ifs.close(); + + return ofs.str(); + } + catch (...) + { + LOG(LogError) << "Error getting Http request content"; + } + + return ""; } void HttpReq::onError(const char* msg) { mErrorMsg = msg; + LOG(LogError) << "HttpReq::onError (" + std::to_string(mStatus) << ") : " + mErrorMsg; } std::string HttpReq::getErrorMsg() @@ -192,14 +402,60 @@ std::string HttpReq::getErrorMsg() //return value is number of elements successfully read size_t HttpReq::write_content(void* buff, size_t size, size_t nmemb, void* req_ptr) { - std::stringstream& ss = ((HttpReq*)req_ptr)->mContent; - ss.write((char*)buff, size * nmemb); + HttpReq* request = ((HttpReq*)req_ptr); + + if (request->mFilePath.empty()) + { + ((HttpReq*)req_ptr)->mContent.write((char*)buff, size * nmemb); + return size * nmemb; + } + + std::ofstream& ss = request->mStream; + + try + { + if (!ss.is_open()) + return 0; + + ss.write((char*)buff, size * nmemb); + + if (ss.rdstate() != std::ofstream::goodbit) + { + request->closeStream(); + request->mStatus = REQ_FILESTREAM_ERROR; + request->mErrorMsg = "IO ERROR (DISK FULL?)"; + + return 0; + } + } + catch(...) + { + request->closeStream(); + request->mStatus = REQ_FILESTREAM_ERROR; + request->mErrorMsg = "IO ERROR (DISK FULL?)"; + + return 0; + } + + double cl; + if (!curl_easy_getinfo(request->mHandle, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &cl)) + { + double position = (double)ss.tellp(); + request->mPosition = position; + + if (cl <= 0) + request->mPercent = -1; + else + request->mPercent = (int) (position * 100.0 / cl); + } return nmemb; } -//used as a curl callback -/*int HttpReq::update_progress(void* req_ptr, double dlTotal, double dlNow, double ulTotal, double ulNow) +bool HttpReq::wait() { + while (status() == HttpReq::REQ_IN_PROGRESS) + std::this_thread::sleep_for(std::chrono::milliseconds(20)); -}*/ + return status() == HttpReq::REQ_SUCCESS; +} \ No newline at end of file diff --git a/es-core/src/HttpReq.h b/es-core/src/HttpReq.h index f6032a3c76..c2bd59b1b5 100644 --- a/es-core/src/HttpReq.h +++ b/es-core/src/HttpReq.h @@ -5,12 +5,13 @@ #include #include #include +#include /* Usage: * HttpReq myRequest("www.google.com", "/index.html"); * //for blocking behavior: while(myRequest.status() == HttpReq::REQ_IN_PROGRESS); * //for non-blocking behavior: check if(myRequest.status() != HttpReq::REQ_IN_PROGRESS) in some sort of update method - * + * * //once one of those completes, the request is ready * if(myRequest.status() != REQ_SUCCESS) * { @@ -26,30 +27,49 @@ class HttpReq { public: - HttpReq(const std::string& url); - + HttpReq(const std::string& url, const std::string outputFilename = ""); ~HttpReq(); enum Status { - REQ_IN_PROGRESS, //request is in progress - REQ_SUCCESS, //request completed successfully, get it with getContent() - - REQ_IO_ERROR, //some error happened, get it with getErrorMsg() - REQ_BAD_STATUS_CODE, //some invalid HTTP response status code happened (non-200) - REQ_INVALID_RESPONSE //the HTTP response was invalid + REQ_IN_PROGRESS = 0, + REQ_IO_ERROR = 3, + REQ_FILESTREAM_ERROR = 4, + + REQ_SUCCESS = 200, + REQ_400_BADREQUEST = 400, + REQ_401_FORBIDDEN = 401, + REQ_403_BADLOGIN = 403, + REQ_404_NOTFOUND = 404, + + REQ_426_SERVERMAINTENANCE = 423, + REQ_426_BLACKLISTED = 426, + REQ_429_TOOMANYREQUESTS = 429, + + REQ_430_TOOMANYSCRAPS = 430, + REQ_430_TOOMANYFAILURES = 431 }; Status status(); //process any received data and return the status afterwards std::string getErrorMsg(); - std::string getContent() const; // mStatus must be REQ_SUCCESS + std::string getContent(); // mStatus must be REQ_SUCCESS + + // int saveContent(const std::string filename, bool checkMedia = false); static std::string urlEncode(const std::string &s); static bool isUrl(const std::string& s); + int getPercent() { return mPercent; } + int getPosition() { return mPosition; } + + std::string getUrl() { return mUrl; } + bool wait(); + private: + void closeStream(); + static size_t write_content(void* buff, size_t size, size_t nmemb, void* req_ptr); //static int update_progress(void* req_ptr, double dlTotal, double dlNow, double ulTotal, double ulNow); @@ -65,8 +85,19 @@ class HttpReq Status mStatus; + // string steam mode std::stringstream mContent; + + // file stream mode + std::string mFilePath; + std::string mTempStreamPath; + std::ofstream mStream; + std::string mErrorMsg; + std::string mUrl; + + int mPercent; + double mPosition; }; #endif // ES_CORE_HTTP_REQ_H diff --git a/es-core/src/ImageIO.cpp b/es-core/src/ImageIO.cpp index 3285308cd2..9d64dd9704 100644 --- a/es-core/src/ImageIO.cpp +++ b/es-core/src/ImageIO.cpp @@ -4,13 +4,278 @@ #include #include +#include +#include +#include "math/Vector2i.h" +#include "utils/FileSystemUtil.h" +#include "utils/StringUtil.h" +#include "resources/ResourceManager.h" + +#include +#include + +struct CachedFileInfo +{ + CachedFileInfo(int sz, int sx, int sy) + { + size = sz; + x = sx; + y = sy; + }; + + CachedFileInfo() + { + size = 0; + x = 0; + y = 0; + }; + + int size; + int x; + int y; +}; + +static std::map sizeCache; +static bool sizeCacheDirty = false; + + +#include +#include + +std::string getImageCacheFilename() +{ + return Utils::FileSystem::getHomePath() + "/.emulationstation/imagecache.db"; +} + +void ImageIO::loadImageCache() +{ + std::string fname = getImageCacheFilename(); + + std::ifstream f(fname.c_str()); + if (f.fail()) + return; + + std::string relativeTo = Utils::FileSystem::getParent(Utils::FileSystem::getHomePath()); + + std::string line; + while (std::getline(f, line)) + { + auto splits = Utils::String::split(line, '|'); + if (splits.size() == 4) + { + std::string file = splits[0]; + file = Utils::FileSystem::resolveRelativePath(splits[0], relativeTo, true); + + CachedFileInfo fi; + fi.size = atoi(splits[1].c_str()); + fi.x = atoi(splits[2].c_str()); + fi.y = atoi(splits[3].c_str()); + + sizeCache[file] = fi; + } + } + + f.close(); +} + +void ImageIO::saveImageCache() +{ + if (!sizeCacheDirty) + return; + + std::string fname = getImageCacheFilename(); + std::ofstream f(fname.c_str(), std::ios::binary); + if (f.fail()) + return; + + std::string relativeTo = Utils::FileSystem::getParent(Utils::FileSystem::getHomePath()); + for (auto it : sizeCache) + { + if (it.second.size < 0) + continue; + + if (it.first.find("/themes/") != std::string::npos) + continue; + + std::string path = Utils::FileSystem::createRelativePath(it.first, "_path_", true); + if (path[0] != '~') + path = Utils::FileSystem::createRelativePath(it.first, relativeTo, false); + + f << path; + f << "|"; + f << std::to_string(it.second.size); + f << "|"; + f << std::to_string(it.second.x); + f << "|"; + f << std::to_string(it.second.y); + f << "\n"; + } + + f.close(); +} + +static std::mutex sizeCacheLock; + +void ImageIO::updateImageCache(const std::string fn, int sz, int x, int y) +{ + std::unique_lock lock(sizeCacheLock); + + auto it = sizeCache.find(fn); + if (it != sizeCache.cend()) + { + if (x != it->second.x || y != it->second.y || sz != it->second.size) + { + auto& item = it->second; + + item.x = x; + item.y = y; + item.size = sz; + + if (sz > 0 && x > 0 && fn.find("/themes/") == std::string::npos) + sizeCacheDirty = true; + } + } + else + { + sizeCache[fn] = CachedFileInfo(sz, x, y); + + if (sz > 0 && x > 0 && fn.find("/themes/") == std::string::npos) + sizeCacheDirty = true; + } +} + + +bool ImageIO::getImageSize(const char *fn, unsigned int *x, unsigned int *y) +{ + { + std::unique_lock lock(sizeCacheLock); + + auto it = sizeCache.find(fn); + if (it != sizeCache.cend()) + { + if (it->second.size < 0) + return false; + + *x = it->second.x; + *y = it->second.y; + return true; + } + } + + LOG(LogDebug) << "ImageIO::loadImageSize " << fn; + + auto ext = Utils::String::toLower(Utils::FileSystem::getExtension(fn)); + if (ext != ".jpg" && ext != ".png" && ext != ".jpeg" && ext != ".gif") + { + LOG(LogWarning) << "ImageIO::loadImageSize\tUnknown file type"; + return false; + } + + std::unique_lock lock(ResourceManager::FileSystemLock); + auto size = Utils::FileSystem::getFileSize(fn); + + FILE *f = fopen(fn, "rb"); + if (f == 0) + { + LOG(LogWarning) << "ImageIO::loadImageSize\tUnable to open file"; + updateImageCache(fn, -1, -1, -1); + return false; + } + + // Strategy: + // reading GIF dimensions requires the first 10 bytes of the file + // reading PNG dimensions requires the first 24 bytes of the file + // reading JPEG dimensions requires scanning through jpeg chunks + // In all formats, the file is at least 24 bytes big, so we'll read that always + unsigned char buf[24]; + if (fread(buf, 1, 24, f) != 24) + { + updateImageCache(fn, -1, -1, -1); + return false; + } + + // For JPEGs, we need to read the first 12 bytes of each chunk. + // We'll read those 12 bytes at buf+2...buf+14, i.e. overwriting the existing buf. + bool jfif = false; + + if ((buf[0] == 0xFF && buf[1] == 0xD8 && buf[2] == 0xFF && buf[3] == 0xE0 && buf[6] == 'J' && buf[7] == 'F' && buf[8] == 'I' && buf[9] == 'F') || + (buf[0] == 0xFF && buf[1] == 0xD8 && buf[2] == 0xFF && buf[3] == 0xE1 && buf[6] == 'E' && buf[7] == 'x' && buf[8] == 'i' && buf[9] == 'f')) + { + jfif = true; + + long pos = 2; + while (buf[2] == 0xFF) + { + if (buf[3] == 0xC0 || buf[3] == 0xC1 || buf[3] == 0xC2 || buf[3] == 0xC3 || buf[3] == 0xC9 || buf[3] == 0xCA || buf[3] == 0xCB) + break; + + pos += 2 + (buf[4] << 8) + buf[5]; + + if (fseek(f, pos, SEEK_SET) != 0) + break; + + if (fread(buf + 2, 1, 12, f) != 12) + break; + } + } + + fclose(f); + + // JPEG: (first two bytes of buf are first two bytes of the jpeg file; rest of buf is the DCT frame + if (jfif && buf[0] == 0xFF && buf[1] == 0xD8 && buf[2] == 0xFF) + { + *y = (buf[7] << 8) + buf[8]; + *x = (buf[9] << 8) + buf[10]; + + LOG(LogDebug) << "ImageIO::loadImageSize\tJPG size " << std::string(std::to_string(*x) + "x" + std::to_string(*y)).c_str(); + + if (*x > 5000) // security ? + { + updateImageCache(fn, -1, -1, -1); + return false; + } + + updateImageCache(fn, size, *x, *y); + return true; + } + + // GIF: first three bytes say "GIF", next three give version number. Then dimensions + if (buf[0] == 'G' && buf[1] == 'I' && buf[2] == 'F') + { + *x = buf[6] + (buf[7] << 8); + *y = buf[8] + (buf[9] << 8); + + LOG(LogDebug) << "ImageIO::loadImageSize\tGIF size " << std::string(std::to_string(*x) + "x" + std::to_string(*y)).c_str(); + + updateImageCache(fn, size, *x, *y); + return true; + } + + // PNG: the first frame is by definition an IHDR frame, which gives dimensions + if (buf[0] == 0x89 && buf[1] == 'P' && buf[2] == 'N' && buf[3] == 'G' && buf[4] == 0x0D && buf[5] == 0x0A && buf[6] == 0x1A && buf[7] == 0x0A && buf[12] == 'I' && buf[13] == 'H' && buf[14] == 'D' && buf[15] == 'R') + { + *x = (buf[16] << 24) + (buf[17] << 16) + (buf[18] << 8) + (buf[19] << 0); + *y = (buf[20] << 24) + (buf[21] << 16) + (buf[22] << 8) + (buf[23] << 0); + + LOG(LogDebug) << "ImageIO::loadImageSize\tPNG size " << std::string(std::to_string(*x) + "x" + std::to_string(*y)).c_str(); + + updateImageCache(fn, size, *x, *y); + return true; + } + + updateImageCache(fn, -1, -1, -1); + LOG(LogWarning) << "ImageIO::loadImageSize\tUnable to extract size"; + return false; +} + std::vector ImageIO::loadFromMemoryRGBA32(const unsigned char * data, const size_t size, size_t & width, size_t & height) { std::vector rawData; width = 0; height = 0; FIMEMORY * fiMemory = FreeImage_OpenMemory((BYTE *)data, (DWORD)size); - if (fiMemory != nullptr) { + if (fiMemory != nullptr) + { //detect the filetype from data FREE_IMAGE_FORMAT format = FreeImage_GetFileTypeFromMemory(fiMemory); if (format != FIF_UNKNOWN && FreeImage_FIFSupportsReading(format)) @@ -31,21 +296,27 @@ std::vector ImageIO::loadFromMemoryRGBA32(const unsigned char * d } } if (fiBitmap != nullptr) - { + { width = FreeImage_GetWidth(fiBitmap); height = FreeImage_GetHeight(fiBitmap); + //loop through scanlines and add all pixel data to the return vector //this is necessary, because width*height*bpp might not be == pitch + unsigned char * tempData = new unsigned char[width * height * 4]; + unsigned char * bytes = FreeImage_GetBits(fiBitmap); + + // memcpy(tempData, bytes, width * height * 4); +/* for (size_t i = 0; i < height; i++) { const BYTE * scanLine = FreeImage_GetScanLine(fiBitmap, (int)i); memcpy(tempData + (i * width * 4), scanLine, width * 4); - } + }*/ //convert from BGRA to RGBA for(size_t i = 0; i < width*height; i++) { - RGBQUAD bgra = ((RGBQUAD *)tempData)[i]; + RGBQUAD bgra = ((RGBQUAD *)bytes)[i]; RGBQUAD rgba; rgba.rgbBlue = bgra.rgbRed; rgba.rgbGreen = bgra.rgbGreen; @@ -53,6 +324,7 @@ std::vector ImageIO::loadFromMemoryRGBA32(const unsigned char * d rgba.rgbReserved = bgra.rgbReserved; ((RGBQUAD *)tempData)[i] = rgba; } + rawData = std::vector(tempData, tempData + width * height * 4); //free bitmap data FreeImage_Unload(fiBitmap); @@ -74,6 +346,155 @@ std::vector ImageIO::loadFromMemoryRGBA32(const unsigned char * d return rawData; } +Vector2i ImageIO::adjustPictureSize(Vector2i imageSize, Vector2i maxSize, bool externSize) +{ + if (externSize) + { + Vector2f szf = adjustExternPictureSizef(Vector2f(imageSize.x(), imageSize.y()), Vector2f(maxSize.x(), maxSize.y())); + return Vector2i(szf.x(), szf.y()); + } + + int cxDIB = imageSize.x(); + int cyDIB = imageSize.y(); + int iMaxX = maxSize.x(); + int iMaxY = maxSize.y(); + + double xCoef = (double)iMaxX / (double)cxDIB; + double yCoef = (double)iMaxY / (double)cyDIB; + +#if WIN32 + cyDIB = (int)((double)cyDIB * std::fmax(xCoef, yCoef)); + cxDIB = (int)((double)cxDIB * std::fmax(xCoef, yCoef)); +#else + cyDIB = (int)((double)cyDIB * std::max(xCoef, yCoef)); + cxDIB = (int)((double)cxDIB * std::max(xCoef, yCoef)); +#endif + + if (cxDIB > iMaxX) + { + cyDIB = (int)((double)cyDIB * (double)iMaxX / (double)cxDIB); + cxDIB = iMaxX; + } + + if (cyDIB > iMaxY) + { + cxDIB = (int)((double)cxDIB * (double)iMaxY / (double)cyDIB); + cyDIB = iMaxY; + } + + return Vector2i(cxDIB, cyDIB); +} + +Vector2f ImageIO::adjustExternPictureSizef(Vector2f imageSize, Vector2f maxSize) +{ + float cxDIB = maxSize.x(); + float cyDIB = maxSize.y(); + + float xCoef = maxSize.x() / imageSize.x(); + float yCoef = maxSize.y() / imageSize.y(); + + if (imageSize.x() * yCoef < maxSize.x()) + cyDIB = imageSize.y() * xCoef; + else + cxDIB = imageSize.x() * yCoef; + + return Vector2f(cxDIB, cyDIB); +} + +unsigned char* ImageIO::loadFromMemoryRGBA32Ex(const unsigned char * data, const size_t size, size_t & width, size_t & height, int maxWidth, int maxHeight, bool externZoom, Vector2i& baseSize, Vector2i& packedSize) +{ + baseSize = Vector2i(0, 0); + packedSize = Vector2i(0, 0); + + width = 0; + height = 0; + + FIMEMORY * fiMemory = FreeImage_OpenMemory((BYTE *)data, (DWORD)size); + if (fiMemory != nullptr) + { + //detect the filetype from data + FREE_IMAGE_FORMAT format = FreeImage_GetFileTypeFromMemory(fiMemory); + if (format != FIF_UNKNOWN && FreeImage_FIFSupportsReading(format)) + { + //file type is supported. load image + FIBITMAP * fiBitmap = FreeImage_LoadFromMemory(format, fiMemory); + if (fiBitmap != nullptr) + { + //loaded. convert to 32bit if necessary + if (FreeImage_GetBPP(fiBitmap) != 32) + { + FIBITMAP * fiConverted = FreeImage_ConvertTo32Bits(fiBitmap); + if (fiConverted != nullptr) + { + //free original bitmap data + FreeImage_Unload(fiBitmap); + fiBitmap = fiConverted; + } + } + + if (fiBitmap != nullptr) + { + width = FreeImage_GetWidth(fiBitmap); + height = FreeImage_GetHeight(fiBitmap); + + baseSize = Vector2i(width, height); + + if (maxWidth > 0 && maxHeight > 0 && (width > maxWidth || height > maxHeight)) + { + Vector2i sz = adjustPictureSize(Vector2i(width, height), Vector2i(maxWidth, maxHeight), externZoom); + if (sz.x() != width || sz.y() != height) + { + FIBITMAP* imageRescaled = FreeImage_Rescale(fiBitmap, sz.x(), sz.y(), FILTER_BOX); + FreeImage_Unload(fiBitmap); + fiBitmap = imageRescaled; + + width = FreeImage_GetWidth(fiBitmap); + height = FreeImage_GetHeight(fiBitmap); + + packedSize = Vector2i(width, height); + } + } + + //loop through scanlines and add all pixel data to the return vector + //this is necessary, because width*height*bpp might not be == pitch + + unsigned char* tempData = new unsigned char[width * height * 4]; + + int w = (int)width; + + for (int y = (int)height; --y >= 0; ) + { + unsigned int* argb = (unsigned int*)FreeImage_GetScanLine(fiBitmap, y); + unsigned int* abgr = (unsigned int*)(tempData + (y * width * 4)); + for (int x = w; --x >= 0;) + { + unsigned int c = argb[x]; + abgr[x] = (c & 0xFF00FF00) | ((c & 0xFF) << 16) | ((c >> 16) & 0xFF); + } + } + + FreeImage_Unload(fiBitmap); + FreeImage_CloseMemory(fiMemory); + + return tempData; + } + } + else + { + LOG(LogError) << "Error - Failed to load image from memory!"; + } + } + else + { + LOG(LogError) << "Error - File type " << (format == FIF_UNKNOWN ? "unknown" : "unsupported") << "!"; + } + //free FIMEMORY again + FreeImage_CloseMemory(fiMemory); + } + + return NULL; +} + void ImageIO::flipPixelsVert(unsigned char* imagePx, const size_t& width, const size_t& height) { unsigned int temp; diff --git a/es-core/src/ImageIO.h b/es-core/src/ImageIO.h index ad8ab48069..35656a7a03 100644 --- a/es-core/src/ImageIO.h +++ b/es-core/src/ImageIO.h @@ -5,11 +5,26 @@ #include #include +#include "math/Vector2i.h" +#include "math/Vector2f.h" + class ImageIO { public: static std::vector loadFromMemoryRGBA32(const unsigned char * data, const size_t size, size_t & width, size_t & height); + static unsigned char* loadFromMemoryRGBA32Ex(const unsigned char * data, const size_t size, size_t & width, size_t & height, int maxWidth, int maxHeight, bool externZoom, Vector2i& baseSize, Vector2i& packedSize); + + static bool getImageSize(const char *fn, unsigned int *x, unsigned int *y); + static void flipPixelsVert(unsigned char* imagePx, const size_t& width, const size_t& height); + static Vector2i adjustPictureSize(Vector2i imageSize, Vector2i maxSize, bool externSize = false); + static Vector2f adjustExternPictureSizef(Vector2f imageSize, Vector2f maxSize); + static Vector2f adjustPictureSizef(Vector2f imageSize, Vector2f maxSize); + + static void loadImageCache(); + static void saveImageCache(); + + static void updateImageCache(const std::string fn, int sz, int x, int y); }; #endif // ES_CORE_IMAGE_IO diff --git a/es-core/src/InputManager.cpp b/es-core/src/InputManager.cpp index 441812ca9d..6ab5e02150 100644 --- a/es-core/src/InputManager.cpp +++ b/es-core/src/InputManager.cpp @@ -426,7 +426,7 @@ void InputManager::doOnFinish() LOG(LogInfo) << " " << tocall; std::cout << "==============================================\ninput config finish command:\n"; - int exitCode = runSystemCommand(tocall); + int exitCode = runSystemCommand(tocall, "", NULL); std::cout << "==============================================\n"; if(exitCode != 0) diff --git a/es-core/src/Log.cpp b/es-core/src/Log.cpp index 951286519e..24b54afb25 100644 --- a/es-core/src/Log.cpp +++ b/es-core/src/Log.cpp @@ -3,9 +3,19 @@ #include "utils/FileSystemUtil.h" #include "platform.h" #include +#include +#include "Settings.h" +#include + +#if WIN32 +#include +#endif + +static std::mutex mLogLock; LogLevel Log::reportingLevel = LogInfo; -FILE* Log::file = NULL; //fopen(getLogPath().c_str(), "w"); +bool Log::dirty = false; +FILE* Log::file = NULL; LogLevel Log::getReportingLevel() { @@ -25,20 +35,47 @@ void Log::setReportingLevel(LogLevel level) void Log::init() { + std::unique_lock lock(mLogLock); + + if (file != NULL) + close(); + + if (Settings::getInstance()->getString("LogLevel") == "disabled") + { + remove(getLogPath().c_str()); + return; + } + remove((getLogPath() + ".bak").c_str()); + // rename previous log file rename(getLogPath().c_str(), (getLogPath() + ".bak").c_str()); - return; -} -void Log::open() -{ file = fopen(getLogPath().c_str(), "w"); + dirty = false; } std::ostringstream& Log::get(LogLevel level) { - os << "lvl" << level << ": \t"; + time_t t = time(nullptr); + os << std::put_time(localtime(&t), "%F %T\t"); + + switch (level) + { + case LogError: + os << "ERROR\t"; + break; + case LogWarning: + os << "WARNING\t"; + break; + case LogDebug: + os << "DEBUG\t"; + break; + default: + os << "INFO\t"; + break; + } + messageLevel = level; return os; @@ -46,36 +83,68 @@ std::ostringstream& Log::get(LogLevel level) void Log::flush() { - fflush(getOutput()); + if (!dirty) + return; + + if (file != nullptr) + fflush(file); + + dirty = false; } void Log::close() { - fclose(file); + if (file != NULL) + { + fflush(file); + fclose(file); + } + + dirty = false; file = NULL; } -FILE* Log::getOutput() +Log::~Log() { - return file; + std::unique_lock lock(mLogLock); + + if (file != NULL) + { + os << std::endl; + fprintf(file, "%s", os.str().c_str()); + dirty = true; + } + + // If it's an error, also print to console + // print all messages if using --debug + if (messageLevel == LogError || reportingLevel >= LogDebug) + { +#if WIN32 + OutputDebugStringA(os.str().c_str()); +#else + fprintf(stderr, "%s", os.str().c_str()); +#endif + } } -Log::~Log() +void Log::setupReportingLevel() { - os << std::endl; + LogLevel lvl = LogInfo; - if(getOutput() == NULL) + if (Settings::getInstance()->getBool("Debug")) + lvl = LogDebug; + else { - // not open yet, print to stdout - std::cerr << "ERROR - tried to write to log file before it was open! The following won't be logged:\n"; - std::cerr << os.str(); - return; + auto level = Settings::getInstance()->getString("LogLevel"); + if (level == "debug") + lvl = LogDebug; + else if (level == "information") + lvl = LogInfo; + else if (level == "warning") + lvl = LogWarning; + else if (level == "error") + lvl = LogError; } - fprintf(getOutput(), "%s", os.str().c_str()); - - //if it's an error, also print to console - //print all messages if using --debug - if(messageLevel == LogError || reportingLevel >= LogDebug) - fprintf(stderr, "%s", os.str().c_str()); + setReportingLevel(lvl); } diff --git a/es-core/src/Log.h b/es-core/src/Log.h index 0b69a74feb..1c005cf46f 100644 --- a/es-core/src/Log.h +++ b/es-core/src/Log.h @@ -3,10 +3,13 @@ #define ES_CORE_LOG_H #include +#include -#define LOG(level) \ -if(level > Log::getReportingLevel()) ; \ -else Log().get(level) +#define LOG(level) if(!Log::Enabled() || level > Log::getReportingLevel()) ; else Log().get(level) + +#define TRYCATCH(m, x) try { x; } \ +catch (const std::exception& e) { LOG(LogError) << m << " Exception " << e.what(); Log::flush(); throw e; } \ +catch (...) { LOG(LogError) << m << " Unknown Exception occured"; Log::flush(); throw; } enum LogLevel { LogError, LogWarning, LogInfo, LogDebug }; @@ -19,19 +22,24 @@ class Log static LogLevel getReportingLevel(); static void setReportingLevel(LogLevel level); + static void setupReportingLevel(); static std::string getLogPath(); static void flush(); static void init(); - static void open(); static void close(); + + static inline bool Enabled() { return file != NULL; } + protected: std::ostringstream os; static FILE* file; + private: static LogLevel reportingLevel; - static FILE* getOutput(); + static bool dirty; + LogLevel messageLevel; }; diff --git a/es-core/src/MameNames.cpp b/es-core/src/MameNames.cpp index e0dee5c4b6..7230b8a9df 100644 --- a/es-core/src/MameNames.cpp +++ b/es-core/src/MameNames.cpp @@ -77,7 +77,7 @@ MameNames::MameNames() for(pugi::xml_node biosNode = doc.child("bios"); biosNode; biosNode = biosNode.next_sibling("bios")) { std::string bios = biosNode.text().get(); - mMameBioses.push_back(bios); + mMameBioses.insert(bios); } // Read devices @@ -97,9 +97,9 @@ MameNames::MameNames() } for(pugi::xml_node deviceNode = doc.child("device"); deviceNode; deviceNode = deviceNode.next_sibling("device")) - { + { std::string device = deviceNode.text().get(); - mMameDevices.push_back(device); + mMameDevices.insert(device); } } // MameNames @@ -130,31 +130,10 @@ std::string MameNames::getRealName(const std::string& _mameName) const bool MameNames::isBios(const std::string& _biosName) { - return MameNames::find(mMameBioses, _biosName); - + return (mMameBioses.find(_biosName) != mMameBioses.cend()); } // isBios const bool MameNames::isDevice(const std::string& _deviceName) { - return MameNames::find(mMameDevices, _deviceName); - + return (mMameDevices.find(_deviceName) != mMameDevices.cend()); } // isDevice - -const bool MameNames::find(std::vector devices, const std::string& name) -{ - size_t start = 0; - size_t end = devices.size(); - - while(start < end) - { - const size_t index = (start + end) / 2; - const int compare = strcmp(devices[index].c_str(), name.c_str()); - - if(compare < 0) start = index + 1; - else if( compare > 0) end = index; - else return true; - } - - return false; - -} diff --git a/es-core/src/MameNames.h b/es-core/src/MameNames.h index 02036d65d4..45d21ec84b 100644 --- a/es-core/src/MameNames.h +++ b/es-core/src/MameNames.h @@ -4,6 +4,7 @@ #include #include +#include class MameNames { @@ -32,10 +33,9 @@ class MameNames static MameNames* sInstance; namePairVector mNamePairs; - std::vector mMameBioses; - std::vector mMameDevices; - const bool find(const std::vector devices, const std::string& name); + std::unordered_set mMameBioses; + std::unordered_set mMameDevices; }; // MameNames diff --git a/es-core/src/PowerSaver.cpp b/es-core/src/PowerSaver.cpp index 8c68955f90..bc1d651a5c 100644 --- a/es-core/src/PowerSaver.cpp +++ b/es-core/src/PowerSaver.cpp @@ -62,6 +62,7 @@ void PowerSaver::updateMode() } else { mMode = DEFAULT; } + updateTimeouts(); } diff --git a/es-core/src/Scripting.cpp b/es-core/src/Scripting.cpp index 3038db0a40..421d4fdb7e 100644 --- a/es-core/src/Scripting.cpp +++ b/es-core/src/Scripting.cpp @@ -28,7 +28,7 @@ namespace Scripting // append folder to path std::string script = *it + " \"" + arg1 + "\" \"" + arg2 + "\""; LOG(LogDebug) << " executing: " << script; - runSystemCommand(script); + runSystemCommand(script, "", NULL); } } } diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index 3ad7f65865..b655684020 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -1,247 +1,371 @@ -#include "Settings.h" - -#include "utils/FileSystemUtil.h" -#include "Log.h" -#include "Scripting.h" -#include "platform.h" -#include -#include -#include - -Settings* Settings::sInstance = NULL; - -// these values are NOT saved to es_settings.xml -// since they're set through command-line arguments, and not the in-program settings menu -std::vector settings_dont_save { - { "Debug" }, - { "DebugGrid" }, - { "DebugText" }, - { "DebugImage" }, - { "ForceKid" }, - { "ForceKiosk" }, - { "IgnoreGamelist" }, - { "HideConsole" }, - { "ShowExit" }, - { "SplashScreen" }, - { "SplashScreenProgress" }, - { "VSync" }, - { "FullscreenBorderless" }, - { "Windowed" }, - { "WindowWidth" }, - { "WindowHeight" }, - { "ScreenWidth" }, - { "ScreenHeight" }, - { "ScreenOffsetX" }, - { "ScreenOffsetY" }, - { "ScreenRotate" } -}; - -Settings::Settings() -{ - setDefaults(); - loadFile(); -} - -Settings* Settings::getInstance() -{ - if(sInstance == NULL) - sInstance = new Settings(); - - return sInstance; -} - -void Settings::setDefaults() -{ - mBoolMap.clear(); - mIntMap.clear(); - - mBoolMap["BackgroundJoystickInput"] = false; - mBoolMap["ParseGamelistOnly"] = false; - mBoolMap["ShowHiddenFiles"] = false; - mBoolMap["DrawFramerate"] = false; - mBoolMap["ShowExit"] = true; - mBoolMap["FullscreenBorderless"] = false; - mBoolMap["Windowed"] = false; - mBoolMap["SplashScreen"] = true; - mBoolMap["SplashScreenProgress"] = true; - mStringMap["StartupSystem"] = ""; - - mBoolMap["VSync"] = true; - - mBoolMap["EnableSounds"] = true; - mBoolMap["ShowHelpPrompts"] = true; - mBoolMap["ScrapeRatings"] = true; - mBoolMap["IgnoreGamelist"] = false; - mBoolMap["HideConsole"] = true; - mBoolMap["QuickSystemSelect"] = true; - mBoolMap["MoveCarousel"] = true; - mBoolMap["SaveGamelistsOnExit"] = true; - - mBoolMap["Debug"] = false; - mBoolMap["DebugGrid"] = false; - mBoolMap["DebugText"] = false; - mBoolMap["DebugImage"] = false; - - mIntMap["ScreenSaverTime"] = 5*60*1000; // 5 minutes - mIntMap["ScraperResizeWidth"] = 400; - mIntMap["ScraperResizeHeight"] = 0; - #ifdef _RPI_ - mIntMap["MaxVRAM"] = 80; - #else - mIntMap["MaxVRAM"] = 100; - #endif - - mStringMap["TransitionStyle"] = "fade"; - mStringMap["ThemeSet"] = ""; - mStringMap["ScreenSaverBehavior"] = "dim"; - mStringMap["Scraper"] = "TheGamesDB"; - mStringMap["GamelistViewStyle"] = "automatic"; - - mBoolMap["ScreenSaverControls"] = true; - mStringMap["ScreenSaverGameInfo"] = "never"; - mBoolMap["StretchVideoOnScreenSaver"] = false; - mStringMap["PowerSaverMode"] = "disabled"; - - mIntMap["ScreenSaverSwapImageTimeout"] = 10000; - mBoolMap["SlideshowScreenSaverStretch"] = false; - mStringMap["SlideshowScreenSaverBackgroundAudioFile"] = Utils::FileSystem::getHomePath() + "/.emulationstation/slideshow/audio/slideshow_bg.wav"; - mBoolMap["SlideshowScreenSaverCustomImageSource"] = false; - mStringMap["SlideshowScreenSaverImageDir"] = Utils::FileSystem::getHomePath() + "/.emulationstation/slideshow/image"; - mStringMap["SlideshowScreenSaverImageFilter"] = ".png,.jpg"; - mBoolMap["SlideshowScreenSaverRecurse"] = false; - - // This setting only applies to raspberry pi but set it for all platforms so - // we don't get a warning if we encounter it on a different platform - mBoolMap["VideoOmxPlayer"] = false; - #ifdef _RPI_ - // we're defaulting to OMX Player for full screen video on the Pi - mBoolMap["ScreenSaverOmxPlayer"] = true; - // use OMX Player defaults - mStringMap["SubtitleFont"] = "/usr/share/fonts/truetype/freefont/FreeSans.ttf"; - mStringMap["SubtitleItalicFont"] = "/usr/share/fonts/truetype/freefont/FreeSansOblique.ttf"; - mIntMap["SubtitleSize"] = 55; - mStringMap["SubtitleAlignment"] = "left"; - #else - mBoolMap["ScreenSaverOmxPlayer"] = false; - #endif - - mIntMap["ScreenSaverSwapVideoTimeout"] = 30000; - - mBoolMap["VideoAudio"] = true; - mBoolMap["CaptionsCompatibility"] = true; - // Audio out device for Video playback using OMX player. - mStringMap["OMXAudioDev"] = "both"; - mStringMap["CollectionSystemsAuto"] = ""; - mStringMap["CollectionSystemsCustom"] = ""; - mBoolMap["CollectionShowSystemInfo"] = true; - mBoolMap["SortAllSystems"] = false; - mBoolMap["UseCustomCollectionsSystem"] = true; - - mBoolMap["LocalArt"] = false; - - // Audio out device for volume control - #ifdef _RPI_ - mStringMap["AudioDevice"] = "PCM"; - #else - mStringMap["AudioDevice"] = "Master"; - #endif - - mStringMap["AudioCard"] = "default"; - mStringMap["UIMode"] = "Full"; - mStringMap["UIMode_passkey"] = "uuddlrlrba"; - mBoolMap["ForceKiosk"] = false; - mBoolMap["ForceKid"] = false; - mBoolMap["ForceDisableFilters"] = false; - - mIntMap["WindowWidth"] = 0; - mIntMap["WindowHeight"] = 0; - mIntMap["ScreenWidth"] = 0; - mIntMap["ScreenHeight"] = 0; - mIntMap["ScreenOffsetX"] = 0; - mIntMap["ScreenOffsetY"] = 0; - mIntMap["ScreenRotate"] = 0; -} - -template -void saveMap(pugi::xml_document& doc, std::map& map, const char* type) -{ - for(auto iter = map.cbegin(); iter != map.cend(); iter++) - { - // key is on the "don't save" list, so don't save it - if(std::find(settings_dont_save.cbegin(), settings_dont_save.cend(), iter->first) != settings_dont_save.cend()) - continue; - - pugi::xml_node node = doc.append_child(type); - node.append_attribute("name").set_value(iter->first.c_str()); - node.append_attribute("value").set_value(iter->second); - } -} - -void Settings::saveFile() -{ - LOG(LogDebug) << "Settings::saveFile() : Saving Settings to file."; - const std::string path = Utils::FileSystem::getHomePath() + "/.emulationstation/es_settings.cfg"; - - pugi::xml_document doc; - - saveMap(doc, mBoolMap, "bool"); - saveMap(doc, mIntMap, "int"); - saveMap(doc, mFloatMap, "float"); - - //saveMap(doc, mStringMap, "string"); - for(auto iter = mStringMap.cbegin(); iter != mStringMap.cend(); iter++) - { - pugi::xml_node node = doc.append_child("string"); - node.append_attribute("name").set_value(iter->first.c_str()); - node.append_attribute("value").set_value(iter->second.c_str()); - } - - doc.save_file(path.c_str()); - - Scripting::fireEvent("config-changed"); - Scripting::fireEvent("settings-changed"); -} - -void Settings::loadFile() -{ - const std::string path = Utils::FileSystem::getHomePath() + "/.emulationstation/es_settings.cfg"; - - if(!Utils::FileSystem::exists(path)) - return; - - pugi::xml_document doc; - pugi::xml_parse_result result = doc.load_file(path.c_str()); - if(!result) - { - LOG(LogError) << "Could not parse Settings file!\n " << result.description(); - return; - } - - for(pugi::xml_node node = doc.child("bool"); node; node = node.next_sibling("bool")) - setBool(node.attribute("name").as_string(), node.attribute("value").as_bool()); - for(pugi::xml_node node = doc.child("int"); node; node = node.next_sibling("int")) - setInt(node.attribute("name").as_string(), node.attribute("value").as_int()); - for(pugi::xml_node node = doc.child("float"); node; node = node.next_sibling("float")) - setFloat(node.attribute("name").as_string(), node.attribute("value").as_float()); - for(pugi::xml_node node = doc.child("string"); node; node = node.next_sibling("string")) - setString(node.attribute("name").as_string(), node.attribute("value").as_string()); -} - -//Print a warning message if the setting we're trying to get doesn't already exist in the map, then return the value in the map. -#define SETTINGS_GETSET(type, mapName, getMethodName, setMethodName) type Settings::getMethodName(const std::string& name) \ -{ \ - if(mapName.find(name) == mapName.cend()) \ - { \ - LOG(LogError) << "Tried to use unset setting " << name << "!"; \ - } \ - return mapName[name]; \ -} \ -void Settings::setMethodName(const std::string& name, type value) \ -{ \ - mapName[name] = value; \ -} - -SETTINGS_GETSET(bool, mBoolMap, getBool, setBool); -SETTINGS_GETSET(int, mIntMap, getInt, setInt); -SETTINGS_GETSET(float, mFloatMap, getFloat, setFloat); -SETTINGS_GETSET(const std::string&, mStringMap, getString, setString); +#include "Settings.h" + +#include "utils/FileSystemUtil.h" +#include "Log.h" +#include "Scripting.h" +#include "platform.h" +#include +#include +#include + +Settings* Settings::sInstance = NULL; +static std::string mEmptyString = ""; + +// these values are NOT saved to es_settings.xml +// since they're set through command-line arguments, and not the in-program settings menu +std::vector settings_dont_save { + { "Debug" }, + { "DebugGrid" }, + { "DebugText" }, + { "DebugImage" }, + { "ForceKid" }, + { "ForceKiosk" }, + { "IgnoreGamelist" }, + { "HideConsole" }, + { "ShowExit" }, + { "SplashScreen" }, + { "SplashScreenProgress" }, +#if !defined(_WIN32) + { "VSync" }, +#endif + { "FullscreenBorderless" }, + { "Windowed" }, + { "WindowWidth" }, + { "WindowHeight" }, + { "ScreenWidth" }, + { "ScreenHeight" }, + { "ScreenOffsetX" }, + { "ScreenOffsetY" }, + { "ScreenRotate" }, + { "MonitorID" }, + { "ExePath" }, + { "HomePath" }, + { "MusicDirectory"}, + { "UserMusicDirectory" } +}; + +Settings::Settings() +{ + mHasConfigRoot = false; + setDefaults(); + loadFile(); +} + +Settings* Settings::getInstance() +{ + if(sInstance == NULL) + sInstance = new Settings(); + + return sInstance; +} + +void Settings::setDefaults() +{ + mWasChanged = false; + + mBoolMap.clear(); + mIntMap.clear(); + + mBoolMap["BackgroundJoystickInput"] = false; + mBoolMap["ParseGamelistOnly"] = false; + mBoolMap["ShowHiddenFiles"] = false; + mBoolMap["DrawFramerate"] = false; + mBoolMap["ShowExit"] = true; + +#if WIN32 + mBoolMap["ShowOnlyExit"] = true; + mBoolMap["FullscreenBorderless"] = true; +#else + mBoolMap["ShowOnlyExit"] = false; + mBoolMap["FullscreenBorderless"] = false; +#endif + + mBoolMap["Windowed"] = false; + mBoolMap["SplashScreen"] = true; + mBoolMap["SplashScreenProgress"] = true; + mBoolMap["PreloadUI"] = false; + mBoolMap["StartupOnGameList"] = false; + mBoolMap["HideSystemView"] = false; + + mStringMap["StartupSystem"] = ""; + + mStringMap["FolderViewMode"] = "never"; + + mBoolMap["VSync"] = true; + mBoolMap["EnableSounds"] = true; + mBoolMap["ShowHelpPrompts"] = true; + mBoolMap["ScrapeRatings"] = true; + mBoolMap["IgnoreGamelist"] = false; + mBoolMap["HideConsole"] = true; + mBoolMap["QuickSystemSelect"] = true; + mBoolMap["MoveCarousel"] = true; + mBoolMap["SaveGamelistsOnExit"] = true; + mBoolMap["OptimizeVRAM"] = true; + mBoolMap["ThreadedLoading"] = true; + mBoolMap["MusicTitles"] = true; + + mBoolMap["Debug"] = false; + mBoolMap["DebugGrid"] = false; + mBoolMap["DebugText"] = false; + mBoolMap["DebugImage"] = false; + + mIntMap["ScreenSaverTime"] = 5*60*1000; // 5 minutes + mIntMap["ScraperResizeWidth"] = 400; + mIntMap["ScraperResizeHeight"] = 0; + +#if defined(_WIN32) + mIntMap["MaxVRAM"] = 256; +#else + #ifdef _RPI_ + mIntMap["MaxVRAM"] = 80; + #else + mIntMap["MaxVRAM"] = 100; + #endif +#endif + +#if defined(_WIN32) + mBoolMap["HideWindow"] = false; +#else + mBoolMap["HideWindow"] = true; +#endif + mStringMap["GameTransitionStyle"] = "fade"; + mStringMap["TransitionStyle"] = "auto"; + mStringMap["Language"] = "en"; + mStringMap["ThemeSet"] = ""; + mStringMap["ScreenSaverBehavior"] = "dim"; + mStringMap["GamelistViewStyle"] = "automatic"; + mStringMap["DefaultGridSize"] = ""; + mStringMap["HiddenSystems"] = ""; + + mStringMap["ThemeColorSet"] = ""; + mStringMap["ThemeIconSet"] = ""; + mStringMap["ThemeMenu"] = ""; + mStringMap["ThemeSystemView"] = ""; + mStringMap["ThemeGamelistView"] = ""; + mStringMap["ThemeRegionName"] = "eu"; + + mBoolMap["ScreenSaverControls"] = true; + mStringMap["ScreenSaverGameInfo"] = "never"; + mBoolMap["StretchVideoOnScreenSaver"] = false; + mStringMap["PowerSaverMode"] = "disabled"; + + mIntMap["ScreenSaverSwapImageTimeout"] = 10000; + mBoolMap["SlideshowScreenSaverStretch"] = false; + mBoolMap["SlideshowScreenSaverCustomImageSource"] = false; + mStringMap["SlideshowScreenSaverImageDir"] = Utils::FileSystem::getHomePath() + "/.emulationstation/slideshow/image"; + mStringMap["SlideshowScreenSaverImageFilter"] = ".png,.jpg"; + mBoolMap["SlideshowScreenSaverRecurse"] = false; + mBoolMap["SlideshowScreenSaverGameName"] = true; + + mBoolMap["ShowFilenames"] = false; + + // This setting only applies to raspberry pi but set it for all platforms so + // we don't get a warning if we encounter it on a different platform + mBoolMap["VideoOmxPlayer"] = false; + #ifdef _RPI_ + // we're defaulting to OMX Player for full screen video on the Pi + mBoolMap["ScreenSaverOmxPlayer"] = true; + // use OMX Player defaults + mStringMap["SubtitleFont"] = "/usr/share/fonts/truetype/freefont/FreeSans.ttf"; + mStringMap["SubtitleItalicFont"] = "/usr/share/fonts/truetype/freefont/FreeSansOblique.ttf"; + mIntMap["SubtitleSize"] = 55; + mStringMap["SubtitleAlignment"] = "left"; + #else + mBoolMap["ScreenSaverOmxPlayer"] = false; + #endif + + mIntMap["ScreenSaverSwapVideoTimeout"] = 30000; + + mBoolMap["VideoAudio"] = true; + mBoolMap["VideoLowersMusic"] = true; + mBoolMap["CaptionsCompatibility"] = true; + // Audio out device for Video playback using OMX player. + mStringMap["OMXAudioDev"] = "both"; + mStringMap["CollectionSystemsAuto"] = ""; + mStringMap["CollectionSystemsCustom"] = ""; + mBoolMap["CollectionShowSystemInfo"] = true; + mBoolMap["SortAllSystems"] = false; + mBoolMap["UseCustomCollectionsSystem"] = true; + mBoolMap["FavoritesFirst"] = true; + + + mBoolMap["LocalArt"] = false; + + // Audio out device for volume control + #ifdef _RPI_ + mStringMap["AudioDevice"] = "PCM"; + #else + mStringMap["AudioDevice"] = "Master"; + #endif + + mStringMap["AudioCard"] = "default"; + mStringMap["UIMode"] = "Full"; + mStringMap["UIMode_passkey"] = "uuddlrlrba"; + mBoolMap["ForceKiosk"] = false; + mBoolMap["ForceKid"] = false; + mBoolMap["ForceDisableFilters"] = false; + + mIntMap["WindowWidth"] = 0; + mIntMap["WindowHeight"] = 0; + mIntMap["ScreenWidth"] = 0; + mIntMap["ScreenHeight"] = 0; + mIntMap["ScreenOffsetX"] = 0; + mIntMap["ScreenOffsetY"] = 0; + mIntMap["ScreenRotate"] = 0; + mIntMap["MonitorID"] = -1; + mStringMap["ExePath"] = ""; + + mStringMap["Scraper"] = "ScreenScraper"; + mStringMap["ScrapperImageSrc"] = "ss"; + mStringMap["ScrapperThumbSrc"] = "box-2D"; + mStringMap["ScrapperLogoSrc"] = "wheel"; + + mBoolMap["ScrapeVideos"] = false; + + mBoolMap["audio.bgmusic"] = true; + mBoolMap["audio.persystem"] = false; + mBoolMap["audio.thememusics"] = true; + + mStringMap["MusicDirectory"] = ""; + mStringMap["UserMusicDirectory"] = ""; + + mBoolMap["updates.enabled"] = true; + + mBoolMap["DrawClock"] = true; + + mDefaultBoolMap = mBoolMap; + mDefaultIntMap = mIntMap; + mDefaultFloatMap = mFloatMap; + mDefaultStringMap = mStringMap; +} + +template +void saveMap(pugi::xml_node& doc, std::map& map, const char* type, std::map& defaultMap) +{ + for(auto iter = map.cbegin(); iter != map.cend(); iter++) + { + // key is on the "don't save" list, so don't save it + if(std::find(settings_dont_save.cbegin(), settings_dont_save.cend(), iter->first) != settings_dont_save.cend()) + continue; + + auto def = defaultMap.find(iter->first); + if (def != defaultMap.cend() && def->second == iter->second) + continue; + + pugi::xml_node node = doc.append_child(type); + node.append_attribute("name").set_value(iter->first.c_str()); + node.append_attribute("value").set_value(iter->second); + } +} + +bool Settings::saveFile() +{ + if (!mWasChanged) + return false; + + mWasChanged = false; + + LOG(LogDebug) << "Settings::saveFile() : Saving Settings to file."; + const std::string path = Utils::FileSystem::getHomePath() + "/.emulationstation/es_settings.cfg"; + + pugi::xml_document doc; + pugi::xml_node root = doc; + + if (mHasConfigRoot) + root = doc.append_child("config"); // batocera, root element + + saveMap(root, mBoolMap, "bool", mDefaultBoolMap); + saveMap(root, mIntMap, "int", mDefaultIntMap); + saveMap(root, mFloatMap, "float", mDefaultFloatMap); + + //saveMap(doc, mStringMap, "string"); + for(auto iter = mStringMap.cbegin(); iter != mStringMap.cend(); iter++) + { + // key is on the "don't save" list, so don't save it + if (std::find(settings_dont_save.cbegin(), settings_dont_save.cend(), iter->first) != settings_dont_save.cend()) + continue; + + auto def = mDefaultStringMap.find(iter->first); + if (def == mDefaultStringMap.cend() && iter->second.empty()) + continue; + + if (def != mDefaultStringMap.cend() && def->second == iter->second) + continue; + + pugi::xml_node node = root.append_child("string"); + node.append_attribute("name").set_value(iter->first.c_str()); + node.append_attribute("value").set_value(iter->second.c_str()); + } + + doc.save_file(path.c_str()); + + Scripting::fireEvent("config-changed"); + Scripting::fireEvent("settings-changed"); + + return true; +} + +void Settings::loadFile() +{ + const std::string path = Utils::FileSystem::getHomePath() + "/.emulationstation/es_settings.cfg"; + + if(!Utils::FileSystem::exists(path)) + return; + + pugi::xml_document doc; + pugi::xml_parse_result result = doc.load_file(path.c_str()); + if(!result) + { + LOG(LogError) << "Could not parse Settings file!\n " << result.description(); + return; + } + + pugi::xml_node root = doc; + + // Batocera has a root element, learn reading them + pugi::xml_node config = doc.child("config"); + if (config) + { + mHasConfigRoot = true; + root = config; + } + + for(pugi::xml_node node = root.child("bool"); node; node = node.next_sibling("bool")) + setBool(node.attribute("name").as_string(), node.attribute("value").as_bool()); + for(pugi::xml_node node = root.child("int"); node; node = node.next_sibling("int")) + setInt(node.attribute("name").as_string(), node.attribute("value").as_int()); + for(pugi::xml_node node = root.child("float"); node; node = node.next_sibling("float")) + setFloat(node.attribute("name").as_string(), node.attribute("value").as_float()); + for(pugi::xml_node node = root.child("string"); node; node = node.next_sibling("string")) + setString(node.attribute("name").as_string(), node.attribute("value").as_string()); + + mWasChanged = false; +} + +//Print a warning message if the setting we're trying to get doesn't already exist in the map, then return the value in the map. +#define SETTINGS_GETSET(type, mapName, getMethodName, setMethodName, defaultValue) type Settings::getMethodName(const std::string& name) \ +{ \ + if(mapName.find(name) == mapName.cend()) \ + { \ + /*LOG(LogError) << "Tried to use unset setting " << name << "!";*/ \ + return defaultValue; \ + } \ + return mapName[name]; \ +} \ +bool Settings::setMethodName(const std::string& name, type value) \ +{ \ + if (mapName.count(name) == 0 || mapName[name] != value) { \ + mapName[name] = value; \ +\ + if (std::find(settings_dont_save.cbegin(), settings_dont_save.cend(), name) == settings_dont_save.cend()) \ + mWasChanged = true; \ +\ + return true; \ + } \ + return false; \ +} + +SETTINGS_GETSET(bool, mBoolMap, getBool, setBool, false); +SETTINGS_GETSET(int, mIntMap, getInt, setInt, 0); +SETTINGS_GETSET(float, mFloatMap, getFloat, setFloat, 0.0f); +SETTINGS_GETSET(const std::string&, mStringMap, getString, setString, mEmptyString); diff --git a/es-core/src/Settings.h b/es-core/src/Settings.h index 413694b719..60dc0438ae 100644 --- a/es-core/src/Settings.h +++ b/es-core/src/Settings.h @@ -11,7 +11,7 @@ class Settings static Settings* getInstance(); void loadFile(); - void saveFile(); + bool saveFile(); //You will get a warning if you try a get on a key that is not already present. bool getBool(const std::string& name); @@ -19,10 +19,12 @@ class Settings float getFloat(const std::string& name); const std::string& getString(const std::string& name); - void setBool(const std::string& name, bool value); - void setInt(const std::string& name, int value); - void setFloat(const std::string& name, float value); - void setString(const std::string& name, const std::string& value); + bool setBool(const std::string& name, bool value); + bool setInt(const std::string& name, int value); + bool setFloat(const std::string& name, float value); + bool setString(const std::string& name, const std::string& value); + + std::map& getStringMap() { return mStringMap; } private: static Settings* sInstance; @@ -36,6 +38,15 @@ class Settings std::map mIntMap; std::map mFloatMap; std::map mStringMap; + + + std::map mDefaultBoolMap; + std::map mDefaultIntMap; + std::map mDefaultFloatMap; + std::map mDefaultStringMap; + + bool mWasChanged; + bool mHasConfigRoot; }; #endif // ES_CORE_SETTINGS_H diff --git a/es-core/src/Sound.cpp b/es-core/src/Sound.cpp index e3f46e279b..6b621e194b 100644 --- a/es-core/src/Sound.cpp +++ b/es-core/src/Sound.cpp @@ -1,5 +1,4 @@ #include "Sound.h" - #include "AudioManager.h" #include "Log.h" #include "Settings.h" @@ -24,7 +23,7 @@ std::shared_ptr Sound::getFromTheme(const std::shared_ptr& the LOG(LogInfo) << " req sound [" << view << "." << element << "]"; const ThemeData::ThemeElement* elem = theme->getElement(view, element, "sound"); - if(!elem || !elem->has("path")) + if(elem == nullptr || !elem->has("path")) { LOG(LogInfo) << " (missing)"; return get(""); @@ -33,7 +32,7 @@ std::shared_ptr Sound::getFromTheme(const std::shared_ptr& the return get(elem->get("path")); } -Sound::Sound(const std::string & path) : mSampleData(NULL), mSamplePos(0), mSampleLength(0), playing(false) +Sound::Sound(const std::string & path) : mSampleData(NULL), mPlaying(false) { loadFile(path); } @@ -51,131 +50,58 @@ void Sound::loadFile(const std::string & path) void Sound::init() { - if(mSampleData != NULL) - deinit(); + deinit(); - if(mPath.empty()) + if (!AudioManager::isInitialized()) + return; + + if (mPath.empty() || !Utils::FileSystem::exists(mPath)) + return; + + if (!Settings::getInstance()->getBool("EnableSounds")) return; //load wav file via SDL - SDL_AudioSpec wave; - Uint8 * data = NULL; - Uint32 dlen = 0; - if (SDL_LoadWAV(mPath.c_str(), &wave, &data, &dlen) == NULL) { + mSampleData = Mix_LoadWAV(mPath.c_str()); + if (mSampleData == nullptr) + { LOG(LogError) << "Error loading sound \"" << mPath << "\"!\n" << " " << SDL_GetError(); return; } - //build conversion buffer - SDL_AudioCVT cvt; - SDL_BuildAudioCVT(&cvt, wave.format, wave.channels, wave.freq, AUDIO_S16, 2, 44100); - //copy data to conversion buffer - cvt.len = dlen; - cvt.buf = new Uint8[cvt.len * cvt.len_mult]; - memcpy(cvt.buf, data, dlen); - //convert buffer to stereo, 16bit, 44.1kHz - if (SDL_ConvertAudio(&cvt) < 0) { - LOG(LogError) << "Error converting sound \"" << mPath << "\" to 44.1kHz, 16bit, stereo format!\n" << " " << SDL_GetError(); - delete[] cvt.buf; - } - else { - //worked. set up member data - SDL_LockAudio(); - mSampleData = cvt.buf; - mSampleLength = cvt.len_cvt; - mSamplePos = 0; - mSampleFormat.channels = 2; - mSampleFormat.freq = 44100; - mSampleFormat.format = AUDIO_S16; - SDL_UnlockAudio(); - } - //free wav data now - SDL_FreeWAV(data); } void Sound::deinit() { - playing = false; + mPlaying = false; - if(mSampleData != NULL) - { - SDL_LockAudio(); - delete[] mSampleData; - mSampleData = NULL; - mSampleLength = 0; - mSamplePos = 0; - SDL_UnlockAudio(); - } + if (mSampleData == nullptr) + return; + + Mix_FreeChunk(mSampleData); + mSampleData = nullptr; } void Sound::play() { - if(mSampleData == NULL) + if (mSampleData == nullptr) return; - if(!Settings::getInstance()->getBool("EnableSounds")) + if (!AudioManager::isInitialized()) return; - AudioManager::getInstance(); - - SDL_LockAudio(); - if (playing) - { - //replay from start. rewind the sample to the beginning - mSamplePos = 0; + if (!Settings::getInstance()->getBool("EnableSounds")) + return; - } - else - { - //flag our sample as playing - playing = true; - } - SDL_UnlockAudio(); - //tell the AudioManager to start playing samples - AudioManager::getInstance()->play(); + mPlaying = true; + Mix_PlayChannel(-1, mSampleData, 0); } bool Sound::isPlaying() const { - return playing; + return mPlaying; } void Sound::stop() { - //flag our sample as playing and rewind its position - SDL_LockAudio(); - playing = false; - mSamplePos = 0; - SDL_UnlockAudio(); -} - -const Uint8 * Sound::getData() const -{ - return mSampleData; -} - -Uint32 Sound::getPosition() const -{ - return mSamplePos; -} - -void Sound::setPosition(Uint32 newPosition) -{ - mSamplePos = newPosition; - if (mSamplePos >= mSampleLength) { - //got to or beyond the end of the sample. stop playing - playing = false; - mSamplePos = 0; - } -} - -Uint32 Sound::getLength() const -{ - return mSampleLength; -} - -Uint32 Sound::getLengthMS() const -{ - //44100 samples per second, 2 channels (stereo) - //I have no idea why the *0.75 is necessary, but otherwise it's inaccurate - return (Uint32)((mSampleLength / 44100.0f / 2.0f * 0.75f) * 1000); -} + mPlaying = false; +} \ No newline at end of file diff --git a/es-core/src/Sound.h b/es-core/src/Sound.h index d54688d8d8..0d77db5d52 100644 --- a/es-core/src/Sound.h +++ b/es-core/src/Sound.h @@ -2,20 +2,17 @@ #ifndef ES_CORE_SOUND_H #define ES_CORE_SOUND_H -#include "SDL_audio.h" #include #include +#include "SDL_mixer.h" class ThemeData; class Sound { std::string mPath; - SDL_AudioSpec mSampleFormat; - Uint8 * mSampleData; - Uint32 mSamplePos; - Uint32 mSampleLength; - bool playing; + Mix_Chunk* mSampleData; + bool mPlaying; public: static std::shared_ptr get(const std::string& path); @@ -32,12 +29,6 @@ class Sound bool isPlaying() const; void stop(); - const Uint8 * getData() const; - Uint32 getPosition() const; - void setPosition(Uint32 newPosition); - Uint32 getLength() const; - Uint32 getLengthMS() const; - private: Sound(const std::string & path = ""); static std::map< std::string, std::shared_ptr > sMap; diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 32faf12bb5..2223911fd9 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -2,14 +2,17 @@ #include "components/ImageComponent.h" #include "components/TextComponent.h" +#include "components/NinePatchComponent.h" +#include "components/VideoVlcComponent.h" #include "utils/FileSystemUtil.h" +#include "utils/StringUtil.h" #include "Log.h" #include "platform.h" #include "Settings.h" -#include #include +#include "EsLocale.h" -std::vector ThemeData::sSupportedViews { { "system" }, { "basic" }, { "detailed" }, { "grid" }, { "video" } }; +std::vector ThemeData::sSupportedViews { { "system" }, { "basic" }, { "detailed" }, { "grid" }, { "video" }, { "menu" }, { "screen" } }; std::vector ThemeData::sSupportedFeatures { { "video" }, { "carousel" }, { "z-index" }, { "visible" } }; std::map> ThemeData::sElementMap { @@ -17,6 +20,7 @@ std::map> The { "pos", NORMALIZED_PAIR }, { "size", NORMALIZED_PAIR }, { "maxSize", NORMALIZED_PAIR }, + { "minSize", NORMALIZED_PAIR }, { "origin", NORMALIZED_PAIR }, { "rotation", FLOAT }, { "rotationOrigin", NORMALIZED_PAIR }, @@ -27,23 +31,44 @@ std::map> The { "colorEnd", COLOR }, { "gradientType", STRING }, { "visible", BOOLEAN }, + { "reflexion", NORMALIZED_PAIR }, + { "reflexionOnFrame", BOOLEAN }, + { "horizontalAlignment", STRING }, + { "verticalAlignment", STRING }, + { "roundCorners", FLOAT }, + { "flipX", BOOLEAN }, + { "flipY", BOOLEAN }, + { "linearSmooth", BOOLEAN }, { "zIndex", FLOAT } } }, { "imagegrid", { { "pos", NORMALIZED_PAIR }, { "size", NORMALIZED_PAIR }, { "margin", NORMALIZED_PAIR }, + { "padding", NORMALIZED_RECT }, + { "autoLayout", NORMALIZED_PAIR }, + { "autoLayoutSelectedZoom", FLOAT }, + { "animateSelection", BOOLEAN }, + { "imageSource", STRING }, // image, thumbnail, marquee + { "zIndex", FLOAT }, { "gameImage", PATH }, { "folderImage", PATH }, - { "scrollDirection", STRING } } }, + { "showVideoAtDelay", FLOAT }, + { "scrollDirection", STRING }, + { "scrollSound", PATH }, + { "centerSelection", STRING }, + { "scrollLoop", BOOLEAN } } }, { "gridtile", { { "size", NORMALIZED_PAIR }, - { "padding", NORMALIZED_PAIR }, + { "padding", NORMALIZED_RECT }, { "imageColor", COLOR }, { "backgroundImage", PATH }, { "backgroundCornerSize", NORMALIZED_PAIR }, { "backgroundColor", COLOR }, { "backgroundCenterColor", COLOR }, - { "backgroundEdgeColor", COLOR } } }, + { "backgroundEdgeColor", COLOR }, + { "selectionMode", STRING }, + { "reflexion", NORMALIZED_PAIR }, + { "imageSizeMode", STRING } } }, { "text", { { "pos", NORMALIZED_PAIR }, { "size", NORMALIZED_PAIR }, @@ -56,9 +81,17 @@ std::map> The { "fontSize", FLOAT }, { "color", COLOR }, { "alignment", STRING }, + { "verticalAlignment", STRING }, { "forceUppercase", BOOLEAN }, { "lineSpacing", FLOAT }, { "value", STRING }, + { "reflexion", NORMALIZED_PAIR }, + { "reflexionOnFrame", BOOLEAN }, + { "glowColor", COLOR }, + { "glowSize", FLOAT }, + { "glowOffset", NORMALIZED_PAIR }, + { "singleLineScroll", BOOLEAN }, + { "padding", NORMALIZED_RECT }, { "visible", BOOLEAN }, { "zIndex", FLOAT } } }, { "textlist", { @@ -94,6 +127,12 @@ std::map> The { "size", NORMALIZED_PAIR }, { "path", PATH }, { "visible", BOOLEAN }, + { "color", COLOR }, + { "cornerSize", NORMALIZED_PAIR }, + { "centerColor", COLOR }, + { "edgeColor", COLOR }, + { "animateColor", COLOR }, + { "animateColorTime", FLOAT }, { "zIndex", FLOAT } } }, { "datetime", { { "pos", NORMALIZED_PAIR }, @@ -120,30 +159,60 @@ std::map> The { "rotation", FLOAT }, { "rotationOrigin", NORMALIZED_PAIR }, { "color", COLOR }, + { "unfilledColor", COLOR }, { "filledPath", PATH }, { "unfilledPath", PATH }, { "visible", BOOLEAN }, { "zIndex", FLOAT } } }, { "sound", { { "path", PATH } } }, + { "controllerActivity", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "itemSpacing", FLOAT }, + { "horizontalAlignment", STRING }, + { "imagePath", PATH }, + { "color", COLOR }, + { "activityColor", COLOR }, + { "hotkeyColor", COLOR }, + { "visible", BOOLEAN }, + { "zIndex", FLOAT } } }, { "helpsystem", { { "pos", NORMALIZED_PAIR }, { "origin", NORMALIZED_PAIR }, { "textColor", COLOR }, { "iconColor", COLOR }, { "fontPath", PATH }, - { "fontSize", FLOAT } } }, + { "fontSize", FLOAT }, + { "iconUpDown", PATH }, + { "iconLeftRight", PATH }, + { "iconUpDownLeftRight", PATH }, + { "iconA", PATH }, + { "iconB", PATH }, + { "iconX", PATH }, + { "iconY", PATH }, + { "iconL", PATH }, + { "iconR", PATH }, + { "iconStart", PATH }, + { "iconSelect", PATH } } }, { "video", { { "pos", NORMALIZED_PAIR }, { "size", NORMALIZED_PAIR }, { "maxSize", NORMALIZED_PAIR }, + { "minSize", NORMALIZED_PAIR }, { "origin", NORMALIZED_PAIR }, { "rotation", FLOAT }, { "rotationOrigin", NORMALIZED_PAIR }, { "default", PATH }, + { "path", PATH }, { "delay", FLOAT }, + { "effect", STRING }, { "visible", BOOLEAN }, - { "zIndex", FLOAT }, + { "roundCorners", FLOAT }, + { "color", COLOR }, + { "zIndex", FLOAT }, + { "snapshotSource", STRING }, // image, thumbnail, marquee + { "loops", FLOAT }, // Number of loops to do -1 (default) is infinite { "showSnapshotNoVideo", BOOLEAN }, { "showSnapshotDelay", BOOLEAN } } }, { "carousel", { @@ -158,29 +227,85 @@ std::map> The { "logoRotation", FLOAT }, { "logoRotationOrigin", NORMALIZED_PAIR }, { "logoSize", NORMALIZED_PAIR }, + { "logoPos", NORMALIZED_PAIR }, { "logoAlignment", STRING }, { "maxLogoCount", FLOAT }, - { "zIndex", FLOAT } } } + { "systemInfoDelay", FLOAT }, + { "defaultTransition", STRING }, + { "scrollSound", PATH }, + { "zIndex", FLOAT } } }, + { "menuText", { + { "fontPath", PATH }, + { "fontSize", FLOAT }, + { "separatorColor", COLOR }, + { "selectorColor", COLOR }, + { "selectorColorEnd", COLOR }, + { "selectorGradientType", STRING }, + { "selectedColor", COLOR }, + { "color", COLOR } } }, + { "menuTextSmall", { + { "fontPath", PATH }, + { "fontSize", FLOAT }, + { "color", COLOR } } }, + { "menuBackground", { + { "path", PATH }, + { "fadePath", PATH }, + { "color", COLOR }, + { "centerColor", COLOR }, + { "cornerSize", NORMALIZED_PAIR } } }, + { "menuIcons", { + { "iconSystem", PATH }, + { "iconUpdates", PATH }, + { "iconControllers", PATH }, + { "iconGames", PATH }, + { "iconUI", PATH }, + { "iconSound", PATH }, + { "iconNetwork", PATH }, + { "iconScraper", PATH }, + { "iconAdvanced", PATH }, + { "iconQuit", PATH } } }, + { "menuSwitch",{ + { "pathOn", PATH }, + { "pathOff", PATH } } }, + { "menuTextEdit",{ + { "active", PATH }, + { "inactive", PATH } } }, + { "menuSlider",{ + { "path", PATH } } }, + { "menuButton",{ + { "path", PATH }, + { "filledPath", PATH } } }, }; +std::shared_ptr ThemeData::mMenuTheme; +ThemeData* ThemeData::mDefaultTheme = nullptr; + #define MINIMUM_THEME_FORMAT_VERSION 3 #define CURRENT_THEME_FORMAT_VERSION 6 // helper unsigned int getHexColor(const char* str) { - ThemeException error; - if(!str) - throw error << "Empty color"; + + +// ThemeException error; + if (!str) + { + //throw error << "Empty color"; + LOG(LogWarning) << "Empty color"; + return 0; + } size_t len = strlen(str); if(len != 6 && len != 8) - throw error << "Invalid color (bad length, \"" << str << "\" - must be 6 or 8)"; + { + //throw error << "Invalid color (bad length, \"" << str << "\" - must be 6 or 8)"; + LOG(LogWarning) << "Invalid color (bad length, \"" << str << "\" - must be 6 or 8)"; + return 0; + } unsigned int val; - std::stringstream ss; - ss << str; - ss >> std::hex >> val; + sscanf(str, "%x", &val); if(len == 6) val = (val << 8) | 0xFF; @@ -188,19 +313,21 @@ unsigned int getHexColor(const char* str) return val; } -std::map mVariables; - -std::string resolvePlaceholders(const char* in) +std::string ThemeData::resolvePlaceholders(const char* in) { + if (in == nullptr || in[0] == 0) + return in; + std::string inStr(in); +// if(inStr.empty()) +// return inStr; - if(inStr.empty()) + const size_t variableBegin = inStr.find("${"); + if (variableBegin == std::string::npos) return inStr; - const size_t variableBegin = inStr.find("${"); const size_t variableEnd = inStr.find("}", variableBegin); - - if((variableBegin == std::string::npos) || (variableEnd == std::string::npos)) + if(variableEnd == std::string::npos) return inStr; std::string prefix = inStr.substr(0, variableBegin); @@ -211,25 +338,48 @@ std::string resolvePlaceholders(const char* in) } ThemeData::ThemeData() -{ +{ + mColorset = Settings::getInstance()->getString("ThemeColorSet"); + mIconset = Settings::getInstance()->getString("ThemeIconSet"); + mMenu = Settings::getInstance()->getString("ThemeMenu"); + mSystemview = Settings::getInstance()->getString("ThemeSystemView"); + mGamelistview = Settings::getInstance()->getString("ThemeGamelistView"); + mRegion = Settings::getInstance()->getString("ThemeRegionName"); + if (mRegion.empty()) + mRegion = "eu"; + + std::string language = EsLocale::getLanguage(); + if (language.empty()) + language = "en"; + else + { + auto shortNameDivider = language.find("_"); + if (shortNameDivider != std::string::npos) + language = Utils::String::toLower(language.substr(0, shortNameDivider)); + } + + mLanguage = Utils::String::toLower(language); mVersion = 0; } -void ThemeData::loadFile(std::map sysDataMap, const std::string& path) +void ThemeData::loadFile(const std::string system, std::map sysDataMap, const std::string& path) { mPaths.push_back(path); ThemeException error; error.setFiles(mPaths); - if(!Utils::FileSystem::exists(path)) + if (!Utils::FileSystem::exists(path)) throw error << "File does not exist!"; - + mVersion = 0; mViews.clear(); - mVariables.clear(); + mSystemThemeFolder = system; + + mVariables.clear(); mVariables.insert(sysDataMap.cbegin(), sysDataMap.cend()); + mVariables["lang"] = mLanguage; pugi::xml_document doc; pugi::xml_parse_result res = doc.load_file(path.c_str()); @@ -249,197 +399,754 @@ void ThemeData::loadFile(std::map sysDataMap, const st throw error << "Theme uses format version " << mVersion << ". Minimum supported version is " << MINIMUM_THEME_FORMAT_VERSION << "."; parseVariables(root); - parseIncludes(root); - parseViews(root); - parseFeatures(root); + parseTheme(root); + + mMenuTheme = nullptr; + mDefaultTheme = this; } -void ThemeData::parseIncludes(const pugi::xml_node& root) +const std::shared_ptr& ThemeData::getMenuTheme() { - ThemeException error; - error.setFiles(mPaths); - - for(pugi::xml_node node = root.child("include"); node; node = node.next_sibling("include")) + if (mMenuTheme == nullptr) { - std::string relPath = resolvePlaceholders(node.text().as_string()); - std::string path = Utils::FileSystem::resolveRelativePath(relPath, mPaths.back(), true); - if(!ResourceManager::getInstance()->fileExists(path)) - throw error << "Included file \"" << relPath << "\" not found! (resolved to \"" << path << "\")"; + if (mDefaultTheme != nullptr) + mMenuTheme = std::shared_ptr(new ThemeMenu(mDefaultTheme)); + else + { + auto emptyData = ThemeData(); + mMenuTheme = std::shared_ptr(new ThemeMenu(&emptyData)); + } + } - error << " from included file \"" << relPath << "\":\n "; + return mMenuTheme; +} - mPaths.push_back(path); +std::string ThemeData::resolveSystemVariable(const std::string& systemThemeFolder, const std::string& path) +{ + size_t start_pos = path.find("$system"); + if (start_pos == std::string::npos) + return path; - pugi::xml_document includeDoc; - pugi::xml_parse_result result = includeDoc.load_file(path.c_str()); - if(!result) - throw error << "Error parsing file: \n " << result.description(); + std::string result = path; + result.replace(start_pos, 7, systemThemeFolder); + return result; +} - pugi::xml_node theme = includeDoc.child("theme"); - if(!theme) - throw error << "Missing tag!"; +bool ThemeData::isFirstSubset(const pugi::xml_node& node) +{ + const std::string subsetToFind = resolvePlaceholders(node.attribute("subset").as_string()); + const std::string name = node.attribute("name").as_string(); - parseVariables(theme); - parseIncludes(theme); - parseViews(theme); - parseFeatures(theme); + for (const auto& it : mSubsets) + if (it.subset == subsetToFind) + return it.name == name; - mPaths.pop_back(); - } + return false; } -void ThemeData::parseFeatures(const pugi::xml_node& root) +bool ThemeData::parseSubset(const pugi::xml_node& node) { - ThemeException error; - error.setFiles(mPaths); + if (!node.attribute("subset")) + return true; + + const std::string subsetAttr = resolvePlaceholders(node.attribute("subset").as_string()); + const std::string nameAttr = resolvePlaceholders(node.attribute("name").as_string()); - for(pugi::xml_node node = root.child("feature"); node; node = node.next_sibling("feature")) + if (!subsetAttr.empty()) { - if(!node.attribute("supported")) - throw error << "Feature missing \"supported\" attribute!"; + std::string displayNameAttr = resolvePlaceholders(node.attribute("displayName").as_string()); + if (displayNameAttr.empty()) + displayNameAttr = nameAttr; + + std::string subSetDisplayNameAttr = resolvePlaceholders(node.attribute("subSetDisplayName").as_string()); + if (subSetDisplayNameAttr.empty()) + { + std::string byVarName = getVariable("subset." + subsetAttr); + if (!byVarName.empty()) + subSetDisplayNameAttr = byVarName; + } - const std::string supportedAttr = node.attribute("supported").as_string(); + bool add = true; - if (std::find(sSupportedFeatures.cbegin(), sSupportedFeatures.cend(), supportedAttr) != sSupportedFeatures.cend()) + for (auto sb : mSubsets) { + if (sb.subset == subsetAttr && sb.name == nameAttr) { + add = false; break; + } + } + + if (add) + { + Subset subSet(subsetAttr, nameAttr, displayNameAttr, subSetDisplayNameAttr); + + std::string appliesToAttr = resolvePlaceholders(node.attribute("appliesTo").as_string()); + if (!appliesToAttr.empty()) + subSet.appliesTo = Utils::String::splitAny(appliesToAttr, ","); + + mSubsets.push_back(subSet); + } + } + + + if (subsetAttr == "colorset") + { + std::string perSystemSetName = Settings::getInstance()->getString("subset." + mSystemThemeFolder + ".colorset"); + if (!perSystemSetName.empty()) + { + if (nameAttr == perSystemSetName) + return true; + } + else if (nameAttr == mColorset || (mColorset.empty() && isFirstSubset(node))) + return true; + } + else if (subsetAttr == "iconset") + { + std::string perSystemSetName = Settings::getInstance()->getString("subset." + mSystemThemeFolder + ".iconset"); + if (!perSystemSetName.empty()) { - parseViews(node); + if (nameAttr == perSystemSetName) + return true; } + else if (nameAttr == mIconset || (mIconset.empty() && isFirstSubset(node))) + return true; + } + else if (subsetAttr == "menu") + { + if (nameAttr == mMenu || (mMenu.empty() && isFirstSubset(node))) + return true; } + else if (subsetAttr == "systemview") + { + if (nameAttr == mSystemview || (mSystemview.empty() && isFirstSubset(node))) + return true; + } + else if (subsetAttr == "gamelistview") + { + std::string perSystemSetName = Settings::getInstance()->getString("subset." + mSystemThemeFolder + ".gamelistview"); + if (!perSystemSetName.empty()) + { + if (nameAttr == perSystemSetName) + return true; + } + else if (nameAttr == mGamelistview || (mGamelistview.empty() && isFirstSubset(node))) + return true; + } + else + { + std::string perSystemSetName = Settings::getInstance()->getString("subset." + mSystemThemeFolder + "." + subsetAttr); + if (!perSystemSetName.empty()) + { + if (nameAttr == perSystemSetName) + return true; + } + else + { + std::string setID = Settings::getInstance()->getString("subset." + subsetAttr); + if (nameAttr == setID || (setID.empty() && isFirstSubset(node))) + return true; + } + } + + return false; +} + + + +void ThemeData::parseInclude(const pugi::xml_node& node) +{ + if (!parseFilterAttributes(node)) + return; + + if (!parseSubset(node)) + return; + + std::string relPath = resolvePlaceholders(node.text().as_string()); + if (relPath.empty()) + return; + + std::string path = Utils::FileSystem::resolveRelativePath(relPath, Utils::FileSystem::getParent(mPaths.back()), true); + path = resolveSystemVariable(mSystemThemeFolder, path); + + if (!ResourceManager::getInstance()->fileExists(path)) + { + LOG(LogWarning) << "Included file \"" << relPath << "\" not found! (resolved to \"" << path << "\")"; + return; + } + + mPaths.push_back(path); + + pugi::xml_document includeDoc; + pugi::xml_parse_result result = includeDoc.load_file(path.c_str()); + if (!result) + { + LOG(LogWarning) << "Error parsing file: \n " << result.description() << " from included file \"" << relPath << "\":\n "; + return; + } + + pugi::xml_node theme = includeDoc.child("theme"); + if (!theme) + { + LOG(LogWarning) << "Missing tag!" << " from included file \"" << relPath << "\":\n "; + return; + } + + parseVariables(theme); + parseTheme(theme); + + mPaths.pop_back(); +} + +void ThemeData::parseFeature(const pugi::xml_node& node) +{ + if (!node.attribute("supported")) + { + LOG(LogWarning) << "Feature missing \"supported\" attribute!"; + return; + } + + if (!parseFilterAttributes(node)) + return; + + const std::string supportedAttr = node.attribute("supported").as_string(); + + if (std::find(sSupportedFeatures.cbegin(), sSupportedFeatures.cend(), supportedAttr) != sSupportedFeatures.cend()) + parseViews(node); +} + +void ThemeData::parseVariable(const pugi::xml_node& node) +{ + std::string key = node.name(); + if (key.empty()) + return; + + if (!parseFilterAttributes(node)) + return; + + std::string val = node.text().as_string(); + if (val.empty()) + return; + + mVariables.erase(key); + mVariables.insert(std::pair(key, val)); } void ThemeData::parseVariables(const pugi::xml_node& root) { - ThemeException error; - error.setFiles(mPaths); + // ThemeException error; + // error.setFiles(mPaths); + + for (pugi::xml_node variables = root.child("variables"); variables; variables = variables.next_sibling("variables")) + { + if (!parseFilterAttributes(variables)) + return; - pugi::xml_node variables = root.child("variables"); + for (pugi::xml_node_iterator it = variables.begin(); it != variables.end(); ++it) + parseVariable(*it); + } +} - if(!variables) +void ThemeData::parseViewElement(const pugi::xml_node& node) +{ + if (!node.attribute("name")) + { + LOG(LogWarning) << "View missing \"name\" attribute!"; return; + } - for(pugi::xml_node_iterator it = variables.begin(); it != variables.end(); ++it) + const char* delim = " \t\r\n,"; + const std::string nameAttr = node.attribute("name").as_string(); + size_t prevOff = nameAttr.find_first_not_of(delim, 0); + size_t off = nameAttr.find_first_of(delim, prevOff); + std::string viewKey; + while (off != std::string::npos || prevOff != std::string::npos) { - std::string key = it->name(); - std::string val = it->text().as_string(); + viewKey = nameAttr.substr(prevOff, off - prevOff); + prevOff = nameAttr.find_first_not_of(delim, off); + off = nameAttr.find_first_of(delim, prevOff); + + if (std::find(sSupportedViews.cbegin(), sSupportedViews.cend(), viewKey) != sSupportedViews.cend()) + { + ThemeView& view = mViews.insert(std::pair(viewKey, ThemeView())).first->second; + parseView(node, view); - if (!val.empty()) - mVariables.insert(std::pair(key, val)); + for (auto it = mViews.cbegin(); it != mViews.cend(); ++it) + { + if (it->second.isCustomView && it->second.baseType == viewKey) + { + ThemeView& customView = (ThemeView&)it->second; + parseView(node, customView); + } + } + } + } +} + +bool ThemeData::parseFilterAttributes(const pugi::xml_node& node) +{ + if (!parseRegion(node)) + return false; + + if (!parseLanguage(node)) + return false; + + if (node.attribute("tinyScreen")) + { + const std::string tinyScreenAttr = node.attribute("tinyScreen").as_string(); + + if (!Renderer::isSmallScreen() && tinyScreenAttr == "true") + return false; + else if (Renderer::isSmallScreen() && tinyScreenAttr == "false") + return false; + } + + if (node.attribute("ifHelpPrompts")) + { + const std::string helpVisibleAttr = node.attribute("ifHelpPrompts").as_string(); + bool help = Settings::getInstance()->getBool("ShowHelpPrompts"); + + if (!help && helpVisibleAttr == "true") + return false; + else if (help && helpVisibleAttr == "false") + return false; + } + + return true; +} + +void ThemeData::parseTheme(const pugi::xml_node& root) +{ + if (root.attribute("defaultView")) + mDefaultView = root.attribute("defaultView").as_string(); + + if (mVersion <= 4) + { + // Unfortunately, recalbox does not do things in order, features have to be loaded after + for (pugi::xml_node node = root.child("include"); node; node = node.next_sibling("include")) + parseInclude(node); + + for (pugi::xml_node node = root.child("view"); node; node = node.next_sibling("view")) + parseViewElement(node); + + for (pugi::xml_node node = root.child("customView"); node; node = node.next_sibling("customView")) + parseCustomView(node, root); + } + else + { + for (pugi::xml_node node = root.first_child(); node; node = node.next_sibling()) + { + if (!parseFilterAttributes(node)) + continue; + + std::string name = node.name(); + + if (name == "include") + parseInclude(node); + else if (name == "view") + parseViewElement(node); + else if (name == "customView") + parseCustomView(node, root); + else if (name == "subset") + parseSubsetElement(node); + } + } + + // Unfortunately, recalbox does not do things in order, features have to be loaded after + for (pugi::xml_node node = root.child("feature"); node; node = node.next_sibling("feature")) + parseFeature(node); +} + +void ThemeData::parseSubsetElement(const pugi::xml_node& root) +{ + if (!parseFilterAttributes(root)) + return; + + const std::string name = root.attribute("name").as_string(); + const std::string displayName = resolvePlaceholders(root.attribute("displayName").as_string()); + const std::string appliesTo = root.attribute("appliesTo").as_string(); + + for (pugi::xml_node node = root.child("include"); node; node = node.next_sibling("include")) + { + node.remove_attribute("subset"); + node.append_attribute("subset") = name.c_str(); + + if (!appliesTo.empty()) + { + node.remove_attribute("appliesTo"); + node.append_attribute("appliesTo") = appliesTo.c_str(); + } + + if (!displayName.empty()) + { + node.remove_attribute("subSetDisplayName"); + node.append_attribute("subSetDisplayName") = displayName.c_str(); + } + + parseInclude(node); } } void ThemeData::parseViews(const pugi::xml_node& root) { - ThemeException error; - error.setFiles(mPaths); + // ThemeException error; + // error.setFiles(mPaths); // parse views - for(pugi::xml_node node = root.child("view"); node; node = node.next_sibling("view")) + for (pugi::xml_node node = root.child("view"); node; node = node.next_sibling("view")) + parseViewElement(node); +} + +void ThemeData::parseCustomViewBaseClass(const pugi::xml_node& root, ThemeView& view, std::string baseClass) +{ + bool found = false; + + // Import original view properties + for (pugi::xml_node nodec = root.child("view"); nodec; nodec = nodec.next_sibling("view")) { - if(!node.attribute("name")) - throw error << "View missing \"name\" attribute!"; + if (!nodec.attribute("name")) + continue; const char* delim = " \t\r\n,"; - const std::string nameAttr = node.attribute("name").as_string(); + const std::string nameAttr = nodec.attribute("name").as_string(); + size_t prevOff = nameAttr.find_first_not_of(delim, 0); size_t off = nameAttr.find_first_of(delim, prevOff); std::string viewKey; - while(off != std::string::npos || prevOff != std::string::npos) + while (off != std::string::npos || prevOff != std::string::npos) { viewKey = nameAttr.substr(prevOff, off - prevOff); prevOff = nameAttr.find_first_not_of(delim, off); off = nameAttr.find_first_of(delim, prevOff); - if (std::find(sSupportedViews.cbegin(), sSupportedViews.cend(), viewKey) != sSupportedViews.cend()) + if (viewKey == baseClass) { - ThemeView& view = mViews.insert(std::pair(viewKey, ThemeView())).first->second; + found = true; + parseView(nodec, view); + } + } + } + + if (found) + return; + + // base class is a customview ? + for (pugi::xml_node nodec = root.child("customView"); nodec; nodec = nodec.next_sibling("customView")) + { + const std::string nameAttr = nodec.attribute("name").as_string(); + + if (!nameAttr.empty() && nameAttr == baseClass) + { + std::string inherits = nodec.attribute("inherits").as_string(); + if (!inherits.empty() && inherits != baseClass) + { + view.baseType = inherits; + view.baseTypes.push_back(inherits); + + parseCustomViewBaseClass(root, view, inherits); + } + + parseView(nodec, view); + } + } +} + +void ThemeData::parseCustomView(const pugi::xml_node& node, const pugi::xml_node& root) +{ + if (!node.attribute("name")) + return; + + if (!parseFilterAttributes(node)) + return; + + std::string viewKey = node.attribute("name").as_string(); + std::string inherits = node.attribute("inherits").as_string(); + + if (viewKey.find(",") != std::string::npos && inherits.empty()) + { + for (auto name : Utils::String::split(viewKey, ',')) + { + std::string trim = Utils::String::trim(name); + if (mViews.find(trim) != mViews.cend()) + { + ThemeView& view = mViews.insert(std::pair(trim, ThemeView())).first->second; + + if (node.attribute("displayName")) + view.displayName = resolvePlaceholders(node.attribute("displayName").as_string()); + parseView(node, view); } } + + return; } + + ThemeView& view = mViews.insert(std::pair(viewKey, ThemeView())).first->second; + + if (node.attribute("displayName")) + view.displayName = resolvePlaceholders(node.attribute("displayName").as_string()); + else if (view.displayName.empty()) + view.displayName = viewKey; + + view.isCustomView = true; + + if (!inherits.empty()) + { + view.baseType = inherits; + view.baseTypes.push_back(inherits); + parseCustomViewBaseClass(root, view, inherits); + } + + parseView(node, view); } -void ThemeData::parseView(const pugi::xml_node& root, ThemeView& view) +void ThemeData::parseView(const pugi::xml_node& root, ThemeView& view, bool overwriteElements) { - ThemeException error; - error.setFiles(mPaths); + // ThemeException error; + // error.setFiles(mPaths); + + if (!parseFilterAttributes(root)) + return; for(pugi::xml_node node = root.first_child(); node; node = node.next_sibling()) { if(!node.attribute("name")) - throw error << "Element of type \"" << node.name() << "\" missing \"name\" attribute!"; + { + LOG(LogWarning) << "Element of type \"" << node.name() << "\" missing \"name\" attribute!"; + continue; + } auto elemTypeIt = sElementMap.find(node.name()); if(elemTypeIt == sElementMap.cend()) - throw error << "Unknown element of type \"" << node.name() << "\"!"; + { + LOG(LogWarning) << "Unknown element of type \"" << node.name() << "\"!"; + continue; + } + if (!parseFilterAttributes(node)) + continue; + const char* delim = " \t\r\n,"; const std::string nameAttr = node.attribute("name").as_string(); size_t prevOff = nameAttr.find_first_not_of(delim, 0); - size_t off = nameAttr.find_first_of(delim, prevOff); - while(off != std::string::npos || prevOff != std::string::npos) + size_t off = nameAttr.find_first_of(delim, prevOff); + while (off != std::string::npos || prevOff != std::string::npos) { std::string elemKey = nameAttr.substr(prevOff, off - prevOff); prevOff = nameAttr.find_first_not_of(delim, off); off = nameAttr.find_first_of(delim, prevOff); parseElement(node, elemTypeIt->second, - view.elements.insert(std::pair(elemKey, ThemeElement())).first->second); + view.elements.insert(std::pair(elemKey, ThemeElement())).first->second, overwriteElements); - if(std::find(view.orderedKeys.cbegin(), view.orderedKeys.cend(), elemKey) == view.orderedKeys.cend()) + if (std::find(view.orderedKeys.cbegin(), view.orderedKeys.cend(), elemKey) == view.orderedKeys.cend()) view.orderedKeys.push_back(elemKey); - } + } } } +bool ThemeData::parseLanguage(const pugi::xml_node& node) +{ + if (!node.attribute("lang")) + return true; + + const std::string nameAttr = Utils::String::toLower(node.attribute("lang").as_string()); + if (nameAttr.empty() || nameAttr == "default") + return true; -void ThemeData::parseElement(const pugi::xml_node& root, const std::map& typeMap, ThemeElement& element) + const char* delim = " \t\r\n,"; + + size_t prevOff = nameAttr.find_first_not_of(delim, 0); + size_t off = nameAttr.find_first_of(delim, prevOff); + while (off != std::string::npos || prevOff != std::string::npos) + { + std::string elemKey = nameAttr.substr(prevOff, off - prevOff); + prevOff = nameAttr.find_first_not_of(delim, off); + off = nameAttr.find_first_of(delim, prevOff); + if (elemKey == mLanguage) + return true; + } + + return false; +} + +bool ThemeData::parseRegion(const pugi::xml_node& node) { - ThemeException error; - error.setFiles(mPaths); + if (!node.attribute("region")) + return true; + + const std::string nameAttr = Utils::String::toLower(node.attribute("region").as_string()); + if (nameAttr.empty() || nameAttr == "default") + return true; + + bool add = true; + + for (auto sb : mSubsets) { + if (sb.subset == "region" && sb.name == nameAttr) { + add = false; break; + } + } + + if (add) + mSubsets.push_back(Subset("region", nameAttr, nameAttr, "region")); + + const char* delim = " \t\r\n,"; + + size_t prevOff = nameAttr.find_first_not_of(delim, 0); + size_t off = nameAttr.find_first_of(delim, prevOff); + while (off != std::string::npos || prevOff != std::string::npos) + { + std::string elemKey = nameAttr.substr(prevOff, off - prevOff); + prevOff = nameAttr.find_first_not_of(delim, off); + off = nameAttr.find_first_of(delim, prevOff); + if (elemKey == mRegion) + return true; + } + + return false; +} + +void ThemeData::parseElement(const pugi::xml_node& root, const std::map& typeMap, ThemeElement& element, bool overwrite) +{ + // ThemeException error; + // error.setFiles(mPaths); element.type = root.name(); - element.extra = root.attribute("extra").as_bool(false); + + if (root.attribute("extra")) + { + std::string extra = Utils::String::toLower(root.attribute("extra").as_string()); + + if (extra == "true") + element.extra = 1; + else if (extra == "static") + element.extra = 2; + } for(pugi::xml_node node = root.first_child(); node; node = node.next_sibling()) { + if (!parseFilterAttributes(node)) + continue; + + ElementPropertyType type = STRING; + auto typeIt = typeMap.find(node.name()); if(typeIt == typeMap.cend()) - throw error << "Unknown property type \"" << node.name() << "\" (for element of type " << root.name() << ")."; + { + // Exception for menuIcons that can be extended + if (element.type == "menuIcons") + type = PATH; + else + { + LOG(LogWarning) << "Unknown property type \"" << node.name() << "\" (for element of type " << root.name() << ")."; + continue; + } + } + else + type = typeIt->second; + + if (!overwrite && element.properties.find(node.name()) != element.properties.cend()) + continue; - std::string str = resolvePlaceholders(node.text().as_string()); + std::string str = resolveSystemVariable(mSystemThemeFolder, resolvePlaceholders(node.text().as_string())); - switch(typeIt->second) + switch(type) { - case NORMALIZED_PAIR: + case NORMALIZED_RECT: { - size_t divider = str.find(' '); - if(divider == std::string::npos) - throw error << "invalid normalized pair (property \"" << node.name() << "\", value \"" << str.c_str() << "\")"; - - std::string first = str.substr(0, divider); - std::string second = str.substr(divider, std::string::npos); + Vector4f val; - Vector2f val((float)atof(first.c_str()), (float)atof(second.c_str())); + auto splits = Utils::String::split(str, ' '); + if (splits.size() == 2) + { + val = Vector4f((float)atof(splits.at(0).c_str()), (float)atof(splits.at(1).c_str()), + (float)atof(splits.at(0).c_str()), (float)atof(splits.at(1).c_str())); + } + else if (splits.size() == 4) + { + val = Vector4f((float)atof(splits.at(0).c_str()), (float)atof(splits.at(1).c_str()), + (float)atof(splits.at(2).c_str()), (float)atof(splits.at(3).c_str())); + } element.properties[node.name()] = val; break; } + case NORMALIZED_PAIR: + { + size_t divider = str.find(' '); + if(divider == std::string::npos) + { + if (str.empty()) + { + LOG(LogWarning) << "invalid normalized pair (property \"" << node.name() << "\", value \"" << str.c_str() << "\")"; + break; + } + + Vector2f val((float)atof(str.c_str()), (float)atof(str.c_str())); + element.properties[node.name()] = val; + break; + } + + float first = atof(str.substr(0, divider).c_str()); + float second = atof(str.substr(divider, std::string::npos).c_str()); + element.properties[node.name()] = Vector2f(first, second); + break; + } case STRING: element.properties[node.name()] = str; break; case PATH: { - std::string path = Utils::FileSystem::resolveRelativePath(str, mPaths.back(), true); + std::string path = Utils::FileSystem::resolveRelativePath(str, Utils::FileSystem::getParent(mPaths.back()), true); + + if (Utils::String::startsWith(path, "{random")) + { + pugi::xml_node parent = root.parent(); + + if (!element.extra) + LOG(LogWarning) << "random is only supported in extras"; + else if (element.type != "image" && element.type != "video") + LOG(LogWarning) << "random is only supported in video or image elements"; + else if (std::string(parent.name()) != "view" || std::string(parent.attribute("name").as_string()) != "system") + LOG(LogWarning) << "random is only supported in systemview"; + else if (element.type == "video" && path != "{random}") + LOG(LogWarning) << "video element only supports {random} element"; + else if (element.type == "image" && path != "{random}" && path != "{random:thumbnail}" && path != "{random:marquee}" && path != "{random:image}") + LOG(LogWarning) << "unknow random element " << path; + else + element.properties[node.name()] = path; + + break; + } + + if (path[0] == '/') + { +#if WIN32 + path = Utils::String::replace(path, + "/recalbox/share_init/system/.emulationstation/themes", + Utils::FileSystem::getHomePath() + "/.emulationstation/themes"); +#else + path = Utils::String::replace(path, + "/recalbox/share_init/system/.emulationstation/themes", + "/userdata/themes"); +#endif + } + + if(!ResourceManager::getInstance()->fileExists(path)) + { + std::string rootPath = Utils::FileSystem::resolveRelativePath(str, Utils::FileSystem::getParent(mPaths.front()), true); + if (rootPath != path && ResourceManager::getInstance()->fileExists(rootPath)) + path = rootPath; + } + if(!ResourceManager::getInstance()->fileExists(path)) { std::stringstream ss; - ss << " Warning " << error.msg; // "from theme yadda yadda, included file yadda yadda - ss << "could not find file \"" << node.text().get() << "\" "; + ss << "Warning : could not find file \"" << node.text().get() << "\" "; if(node.text().get() != path) ss << "(which resolved to \"" << path << "\") "; LOG(LogWarning) << ss.str(); } - element.properties[node.name()] = path; + else + element.properties[node.name()] = path; + break; } case COLOR: @@ -447,8 +1154,8 @@ void ThemeData::parseElement(const pugi::xml_node& root, const std::map(strtod(str.c_str(), 0)); - element.properties[node.name()] = floatVal; + //float floatVal = atof(str.c_str()); static_cast(strtod(str.c_str(), 0)); + element.properties[node.name()] = (float) atof(str.c_str()); //floatVal; break; } @@ -463,11 +1170,44 @@ void ThemeData::parseElement(const pugi::xml_node& root, const std::mapsecond.baseType; + + return ""; +} + +std::string ThemeData::getViewDisplayName(const std::string& view) +{ + auto viewIt = mViews.find(view); + if (viewIt != mViews.cend()) + { + if (viewIt->second.displayName.empty()) + return _(view.c_str()); + + return viewIt->second.displayName; + } + + return view; +} + +bool ThemeData::isCustomView(const std::string& view) +{ + auto viewIt = mViews.find(view); + if (viewIt != mViews.cend()) + return viewIt->second.isCustomView; + + return false; +} + bool ThemeData::hasView(const std::string& view) { auto viewIt = mViews.find(view); @@ -485,7 +1225,7 @@ const ThemeData::ThemeElement* ThemeData::getElement(const std::string& view, co if(elemIt->second.type != expectedType && !expectedType.empty()) { - LOG(LogWarning) << " requested mismatched theme type for [" << view << "." << element << "] - expected \"" + LOG(LogWarning) << " requested mismatched theme type for [" << view << "." << element << "] - expected \"" << expectedType << "\", got \"" << elemIt->second.type << "\""; return NULL; } @@ -506,7 +1246,7 @@ const std::shared_ptr& ThemeData::getDefault() try { std::map emptyMap; - theme->loadFile(emptyMap, path); + theme->loadFile("", emptyMap, path); } catch(ThemeException& e) { LOG(LogError) << e.what(); @@ -525,19 +1265,29 @@ std::vector ThemeData::makeExtras(const std::shared_ptrmViews.find(view); if(viewIt == theme->mViews.cend()) return comps; - + for(auto it = viewIt->second.orderedKeys.cbegin(); it != viewIt->second.orderedKeys.cend(); it++) { ThemeElement& elem = viewIt->second.elements.at(*it); if(elem.extra) { - GuiComponent* comp = NULL; + GuiComponent* comp = nullptr; + const std::string& t = elem.type; if(t == "image") comp = new ImageComponent(window); else if(t == "text") comp = new TextComponent(window); + else if (t == "ninepatch") + comp = new NinePatchComponent(window); + else if (t == "video") + comp = new VideoVlcComponent(window); + if (comp == nullptr) + continue; + + comp->setIsStaticExtra(elem.extra == 2); + comp->setTag((*it).c_str()); comp->setDefaultZIndex(10); comp->applyTheme(theme, view, *it, ThemeFlags::ALL); comps.push_back(comp); @@ -553,7 +1303,7 @@ std::map ThemeData::getThemeSets() static const size_t pathCount = 2; std::string paths[pathCount] = - { + { "/etc/emulationstation/themes", Utils::FileSystem::getHomePath() + "/.emulationstation/themes" }; @@ -597,3 +1347,219 @@ std::string ThemeData::getThemeFromCurrentSet(const std::string& system) return set->second.getThemePath(system); } + +ThemeData::ThemeMenu::ThemeMenu(ThemeData* theme) +{ + Title.font = Font::get(FONT_SIZE_LARGE); + Footer.font = Font::get(FONT_SIZE_SMALL); + Text.font = Font::get(FONT_SIZE_MEDIUM); + TextSmall.font = Font::get(FONT_SIZE_SMALL); + + auto elem = theme->getElement("menu", "menubg", "menuBackground"); + if (elem) + { + if (elem->has("path") && ResourceManager::getInstance()->fileExists(elem->get("path"))) + Background.path = elem->get("path"); + + if (elem->has("fadePath") && ResourceManager::getInstance()->fileExists(elem->get("fadePath"))) + Background.fadePath = elem->get("fadePath"); + + if (elem->has("color")) + { + Background.color = elem->get("color"); + Background.centerColor = Background.color; + } + + if (elem->has("centerColor")) + { + Background.centerColor = elem->get("centerColor"); + if (!elem->has("color")) + Background.color = Background.centerColor; + } + + if (elem->has("cornerSize")) + Background.cornerSize = elem->get("cornerSize"); + } + + elem = theme->getElement("menu", "menutitle", "menuText"); + if (elem) + { + if (elem->has("fontPath") || elem->has("fontSize")) + Title.font = Font::getFromTheme(elem, ThemeFlags::ALL, Font::get(FONT_SIZE_LARGE)); + if (elem->has("color")) + Title.color = elem->get("color"); + if (elem->has("selectorColor")) + Title.selectorColor = elem->get("selectorColor"); + } + + elem = theme->getElement("menu", "menufooter", "menuText"); + if (elem) + { + if (elem->has("fontPath") || elem->has("fontSize")) + Footer.font = Font::getFromTheme(elem, ThemeFlags::ALL, Font::get(FONT_SIZE_SMALL)); + if (elem->has("color")) + Footer.color = elem->get("color"); + if (elem->has("selectorColor")) + Footer.selectorColor = elem->get("selectorColor"); + } + + elem = theme->getElement("menu", "menutextsmall", "menuTextSmall"); + if (elem) + { + if (elem->has("fontPath") || elem->has("fontSize")) + TextSmall.font = Font::getFromTheme(elem, ThemeFlags::ALL, Font::get(FONT_SIZE_SMALL)); + + if (elem->has("color")) + TextSmall.color = elem->get("color"); + if (elem->has("selectedColor")) + Text.selectedColor = elem->get("selectedColor"); + if (elem->has("selectorColor")) + Text.selectedColor = elem->get("selectorColor"); + } + + elem = theme->getElement("menu", "menutext", "menuText"); + if (elem) + { + if (elem->has("fontPath") || elem->has("fontSize")) + Text.font = Font::getFromTheme(elem, ThemeFlags::ALL, Font::get(FONT_SIZE_MEDIUM)); + + if (elem->has("color")) + Text.color = elem->get("color"); + if (elem->has("separatorColor")) + Text.separatorColor = elem->get("separatorColor"); + if (elem->has("selectedColor")) + Text.selectedColor = elem->get("selectedColor"); + if (elem->has("selectorColor")) + { + Text.selectorColor = elem->get("selectorColor"); + Text.selectorGradientColor = Text.selectorColor; + } + if (elem->has("selectorColorEnd")) + Text.selectorGradientColor = elem->get("selectorColorEnd"); + if (elem->has("selectorGradientType")) + Text.selectorGradientType = (elem->get("selectorGradientType").compare("horizontal")); + } + + elem = theme->getElement("menu", "menubutton", "menuButton"); + if (elem) + { + if (elem->has("path")) + Icons.button = elem->get("path"); + if (elem->has("filledPath")) + Icons.button_filled = elem->get("filledPath"); + } + + elem = theme->getElement("menu", "menutextedit", "menuTextEdit"); + if (elem) + { + if (elem->has("active") && ResourceManager::getInstance()->fileExists(elem->get("active"))) + Icons.textinput_ninepatch_active = elem->get("active"); + if (elem->has("inactive") && ResourceManager::getInstance()->fileExists(elem->get("inactive"))) + Icons.textinput_ninepatch = elem->get("inactive"); + } + + elem = theme->getElement("menu", "menuswitch", "menuSwitch"); + if (elem) + { + if (elem->has("pathOn") && ResourceManager::getInstance()->fileExists(elem->get("pathOn"))) + Icons.on = elem->get("pathOn"); + if (elem->has("pathOff") && ResourceManager::getInstance()->fileExists(elem->get("pathOff"))) + Icons.off = elem->get("pathOff"); + } + + elem = theme->getElement("menu", "menuslider", "menuSlider"); + if (elem && elem->has("path") && ResourceManager::getInstance()->fileExists(elem->get("path"))) + Icons.knob = elem->get("path"); + + elem = theme->getElement("menu", "menuicons", "menuIcons"); + if (elem) + { + for (auto prop : elem->properties) + { + std::string path = prop.second.s; + if (!path.empty() && ResourceManager::getInstance()->fileExists(path)) + mMenuIcons[prop.first] = path; + } + } +} + +void ThemeData::setDefaultTheme(ThemeData* theme) +{ + mDefaultTheme = theme; + mMenuTheme = nullptr; +}; + +std::vector> ThemeData::getViewsOfTheme() +{ + std::vector> ret; + for (auto it = mViews.cbegin(); it != mViews.cend(); ++it) + { + if (it->first == "menu" || it->first == "system" || it->first == "screen") + continue; + + ret.push_back(std::pair(it->first, it->second.displayName.empty() ? it->first : it->second.displayName)); + } + + return ret; +} + +std::vector ThemeData::getSubSet(const std::vector& subsets, const std::string& subset) +{ + std::vector ret; + + for (const auto& it : subsets) + if (it.subset == subset) + ret.push_back(it); + + return ret; +} + +std::vector ThemeData::getSubSetNames(const std::string ofView) +{ + std::vector ret; + + for (const auto& it : mSubsets) + { + if (std::find(ret.cbegin(), ret.cend(), it.subset) == ret.cend()) + { + if (ofView.empty()) + ret.push_back(it.subset); + else + { + if (std::find(it.appliesTo.cbegin(), it.appliesTo.cend(), ofView) != it.appliesTo.cend()) + ret.push_back(it.subset); + /* else + { + auto viewIt = mViews.find(ofView); + if (viewIt != mViews.cend()) + { + for (auto applyTo : it.appliesTo) + { + if (viewIt->second.isOfType(applyTo)) + { + ret.push_back(it.subset); + break; + } + } + } + }*/ + } + } + } + + return ret; +} + +std::string ThemeData::getDefaultSubSetValue(const std::string subsetname) +{ + for (const auto& it : mSubsets) + if (it.subset == subsetname) + return it.name; + + return ""; +} + +bool ThemeData::ThemeView::isOfType(const std::string type) +{ + return baseType == type || std::find(baseTypes.cbegin(), baseTypes.cend(), type) != baseTypes.cend(); +}; \ No newline at end of file diff --git a/es-core/src/ThemeData.h b/es-core/src/ThemeData.h index 45c12d5e1d..46d58b2ed5 100644 --- a/es-core/src/ThemeData.h +++ b/es-core/src/ThemeData.h @@ -3,12 +3,15 @@ #define ES_CORE_THEME_DATA_H #include "math/Vector2f.h" +#include "math/Vector4f.h" #include "utils/FileSystemUtil.h" #include #include +#include #include #include #include +#include namespace pugi { class xml_node; } @@ -21,6 +24,7 @@ class NinePatchComponent; class Sound; class TextComponent; class Window; +class Font; namespace ThemeFlags { @@ -55,7 +59,7 @@ class ThemeException : public std::exception template friend ThemeException& operator<<(ThemeException& e, T msg); - + inline void setFiles(const std::deque& deque) { *this << "from theme \"" << deque.front() << "\"\n"; @@ -82,14 +86,96 @@ struct ThemeSet inline std::string getThemePath(const std::string& system) const { return path + "/" + system + "/theme.xml"; } }; + +struct Subset +{ + Subset(const std::string set, const std::string nm, const std::string dn, const std::string ssdn) + { + subset = set; + name = nm; + displayName = dn; + subSetDisplayName = ssdn; + } + + std::string subset; + std::string name; + + std::string displayName; + + std::vector appliesTo; + + std::string subSetDisplayName; +}; + +struct MenuElement +{ + unsigned int color; + unsigned int selectedColor; + unsigned int selectorColor; + unsigned int separatorColor; + unsigned int selectorGradientColor; + bool selectorGradientType; + std::string path; + std::shared_ptr font; +}; + +struct MenuBackground +{ + unsigned int color; + unsigned int centerColor; + std::string path; + std::string fadePath; + Vector2f cornerSize; +}; + +struct IconElement +{ + std::string button; + std::string button_filled; + std::string on; + std::string off; + std::string option_arrow; + std::string arrow; + std::string knob; + std::string textinput_ninepatch; + std::string textinput_ninepatch_active; +}; + class ThemeData { public: + class ThemeMenu + { + public: + ThemeMenu(ThemeData* theme); + + MenuBackground Background{ 0xFFFFFFFF, 0xFFFFFFFF, ":/frame.png", ":/scroll_gradient.png", Vector2f(16, 16) }; + MenuElement Title{ 0x555555FF, 0x555555FF, 0x555555FF, 0xFFFFFFFF, 0x555555FF, true, "", nullptr }; + MenuElement Text{ 0x777777FF, 0xFFFFFFFF, 0x878787FF, 0xC6C7C6FF, 0x878787FF, true, "", nullptr }; + MenuElement TextSmall{ 0x777777FF, 0xFFFFFFFF, 0x878787FF, 0xC6C7C6FF, 0x878787FF, true, "", nullptr }; + MenuElement Footer{ 0xC6C6C6FF, 0xC6C6C6FF, 0xC6C6C6FF, 0xFFFFFFFF, 0xC6C6C6FF, true, "", nullptr }; + IconElement Icons{ ":/button.png", ":/button_filled.png", ":/on.svg", ":/off.svg", ":/option_arrow.svg", ":/arrow.svg", ":/slider_knob.svg", ":/textinput_ninepatch.png", ":/textinput_ninepatch_active.png" }; + + std::string getMenuIcon(const std::string name) + { + auto it = mMenuIcons.find(name); + if (it != mMenuIcons.cend()) + return it->second; + + return ""; + } + + private: + std::map mMenuIcons; + }; class ThemeElement { public: - bool extra; + ThemeElement() { extra = 0; } + + int extra; + std::string type; struct Property @@ -99,12 +185,14 @@ class ThemeData void operator= (const unsigned int& value) { i = value; } void operator= (const float& value) { f = value; } void operator= (const bool& value) { b = value; } + void operator= (const Vector4f& value) { r = value; v = Vector2f(value.x(), value.y()); } Vector2f v; std::string s; unsigned int i; float f; bool b; + Vector4f r; }; std::map< std::string, Property > properties; @@ -117,6 +205,7 @@ class ThemeData else if(std::is_same::value) return *(const T*)&properties.at(prop).i; else if(std::is_same::value) return *(const T*)&properties.at(prop).f; else if(std::is_same::value) return *(const T*)&properties.at(prop).b; + else if (std::is_same::value) return *(const T*)&properties.at(prop).r; return T(); } @@ -127,8 +216,19 @@ class ThemeData class ThemeView { public: + ThemeView() { isCustomView = false; } + std::map elements; std::vector orderedKeys; + std::string baseType; + + std::vector baseTypes; + + bool isOfType(const std::string type); + + std::string displayName; + + bool isCustomView; }; public: @@ -136,10 +236,11 @@ class ThemeData ThemeData(); // throws ThemeException - void loadFile(std::map sysDataMap, const std::string& path); + void loadFile(const std::string system, std::map sysDataMap, const std::string& path); enum ElementPropertyType { + NORMALIZED_RECT, NORMALIZED_PAIR, PATH, STRING, @@ -150,6 +251,9 @@ class ThemeData bool hasView(const std::string& view); + bool isCustomView(const std::string& view); + std::string getCustomViewBaseType(const std::string& view); + // If expectedType is an empty string, will do no type checking. const ThemeElement* getElement(const std::string& view, const std::string& element, const std::string& expectedType) const; @@ -159,6 +263,34 @@ class ThemeData static std::map getThemeSets(); static std::string getThemeFromCurrentSet(const std::string& system); + + bool hasSubsets() { return mSubsets.size() > 0; } + static const std::shared_ptr& getMenuTheme(); + + std::vector getSubSets() { return mSubsets; } + std::vector getSubSetNames(const std::string ofView = ""); + + std::string getDefaultSubSetValue(const std::string subsetname); + + static std::vector getSubSet(const std::vector& subsets, const std::string& subset); + + static void setDefaultTheme(ThemeData* theme); + static ThemeData* getDefaultTheme() { return mDefaultTheme; } + + std::string getSystemThemeFolder() { return mSystemThemeFolder; } + + std::vector> getViewsOfTheme(); + std::string getDefaultView() { return mDefaultView; }; + + std::string getVariable(std::string name) + { + if (mVariables.find(name) != mVariables.cend()) + return mVariables[name]; + + return ""; + } + + std::string getViewDisplayName(const std::string& view); private: static std::map< std::string, std::map > sElementMap; @@ -167,15 +299,87 @@ class ThemeData std::deque mPaths; float mVersion; + std::string mDefaultView; + + void parseTheme(const pugi::xml_node& root); - void parseFeatures(const pugi::xml_node& themeRoot); - void parseIncludes(const pugi::xml_node& themeRoot); + void parseFeature(const pugi::xml_node& node); + void parseInclude(const pugi::xml_node& node); + void parseVariable(const pugi::xml_node& node); void parseVariables(const pugi::xml_node& root); void parseViews(const pugi::xml_node& themeRoot); - void parseView(const pugi::xml_node& viewNode, ThemeView& view); - void parseElement(const pugi::xml_node& elementNode, const std::map& typeMap, ThemeElement& element); + void parseCustomView(const pugi::xml_node& node, const pugi::xml_node& root); + void parseViewElement(const pugi::xml_node& node); + void parseView(const pugi::xml_node& viewNode, ThemeView& view, bool overwriteElements = true); + void parseElement(const pugi::xml_node& elementNode, const std::map& typeMap, ThemeElement& element, bool overwrite = true); + bool parseRegion(const pugi::xml_node& node); + bool parseSubset(const pugi::xml_node& node); + bool isFirstSubset(const pugi::xml_node& node); + bool parseLanguage(const pugi::xml_node& node); + bool parseFilterAttributes(const pugi::xml_node& node); + void parseSubsetElement(const pugi::xml_node& root); + + void parseCustomViewBaseClass(const pugi::xml_node& root, ThemeView& view, std::string baseClass); + + std::string resolveSystemVariable(const std::string& systemThemeFolder, const std::string& path); + std::string resolvePlaceholders(const char* in); + + std::string mColorset; + std::string mIconset; + std::string mMenu; + std::string mSystemview; + std::string mGamelistview; + std::string mSystemThemeFolder; + std::string mLanguage; + std::string mRegion; + + std::map mVariables; + + class UnsortedViewMap : public std::vector> + { + public: + std::vector>::const_iterator find(std::string view) const + { + for (std::vector>::const_iterator it = cbegin(); it != cend(); it++) + if (it->first == view) + return it; + + return cend(); + } + + std::vector>::iterator find(std::string view) + { + for (std::vector>::iterator it = begin(); it != end(); it++) + if (it->first == view) + return it; + + return end(); + } + + std::pair>::iterator, bool> insert(std::pair item) + { + std::pair>::iterator, bool> ret; + + ret.first = find(item.first); + ret.second = ret.first != cend(); + + if (ret.first == cend()) + { + push_back(item); + ret.first = find(item.first); + } + + return ret; + } + }; + + UnsortedViewMap mViews; + // std::map mViews; + + std::vector mSubsets; - std::map mViews; + static std::shared_ptr mMenuTheme; + static ThemeData* mDefaultTheme; }; #endif // ES_CORE_THEME_DATA_H diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index c51bf9ff58..c84858ea80 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -2,6 +2,7 @@ #include "components/HelpComponent.h" #include "components/ImageComponent.h" +#include "components/TextComponent.h" #include "resources/Font.h" #include "resources/TextureResource.h" #include "InputManager.h" @@ -9,16 +10,26 @@ #include "Scripting.h" #include #include +#include +#include "guis/GuiInfoPopup.h" +#include "components/AsyncNotificationComponent.h" +#include "guis/GuiMsgBox.h" +#include "AudioManager.h" Window::Window() : mNormalizeNextUpdate(false), mFrameTimeElapsed(0), mFrameCountElapsed(0), mAverageDeltaTime(10), - mAllowSleep(true), mSleeping(false), mTimeSinceLastInput(0), mScreenSaver(NULL), mRenderScreenSaver(false), mInfoPopup(NULL) -{ + mAllowSleep(true), mSleeping(false), mTimeSinceLastInput(0), mScreenSaver(NULL), mRenderScreenSaver(false), mInfoPopup(NULL), mClockElapsed(0) // batocera +{ mHelp = new HelpComponent(this); - mBackgroundOverlay = new ImageComponent(this); + mBackgroundOverlay = new ImageComponent(this); + + mSplash = NULL; } Window::~Window() { + for (auto extra : mScreenExtras) + delete extra; + delete mBackgroundOverlay; // delete all our GUIs @@ -66,16 +77,23 @@ GuiComponent* Window::peekGui() return mGuiStack.back(); } -bool Window::init() +bool Window::init(bool initRenderer) { - if(!Renderer::init()) + LOG(LogInfo) << "Window::init"; + + if (initRenderer) { - LOG(LogError) << "Renderer failed to initialize!"; - return false; - } - - InputManager::getInstance()->init(); + if (!Renderer::init()) + { + LOG(LogError) << "Renderer failed to initialize!"; + return false; + } + InputManager::getInstance()->init(); + } + else + Renderer::activateWindow(); + ResourceManager::getInstance()->reloadAll(); //keep a reference to the default fonts, so they don't keep getting destroyed/recreated @@ -89,23 +107,54 @@ bool Window::init() mBackgroundOverlay->setImage(":/scroll_gradient.png"); mBackgroundOverlay->setResize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); + if (mClock == nullptr) + { + mClock = std::make_shared(this); + mClock->setFont(Font::get(FONT_SIZE_SMALL)); + mClock->setHorizontalAlignment(ALIGN_RIGHT); + mClock->setVerticalAlignment(ALIGN_TOP); + mClock->setPosition(Renderer::getScreenWidth()*0.94, Renderer::getScreenHeight()*0.9965 - Font::get(FONT_SIZE_SMALL)->getHeight()); + mClock->setSize(Renderer::getScreenWidth()*0.05, 0); + mClock->setColor(0x777777FF); + } + // update our help because font sizes probably changed - if(peekGui()) + if (peekGui()) peekGui()->updateHelpPrompts(); - + return true; } -void Window::deinit() +void Window::reactivateGui() +{ + for (auto extra : mScreenExtras) + extra->onShow(); + + for (auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) + (*i)->onShow(); + + if (peekGui()) + peekGui()->updateHelpPrompts(); +} + +void Window::deinit(bool deinitRenderer) { + for (auto extra : mScreenExtras) + extra->onHide(); + // Hide all GUI elements on uninitialisation - this disable for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) - { (*i)->onHide(); - } - InputManager::getInstance()->deinit(); + + if (deinitRenderer) + InputManager::getInstance()->deinit(); + + TextureResource::resetCache(); + ResourceManager::getInstance()->unloadAll(); - Renderer::deinit(); + + if (deinitRenderer) + Renderer::deinit(); } void Window::textInput(const char* text) @@ -117,17 +166,17 @@ void Window::textInput(const char* text) void Window::input(InputConfig* config, Input input) { if (mScreenSaver) { - if(mScreenSaver->isScreenSaverActive() && Settings::getInstance()->getBool("ScreenSaverControls") && - (Settings::getInstance()->getString("ScreenSaverBehavior") == "random video")) + if (mScreenSaver->isScreenSaverActive() && Settings::getInstance()->getBool("ScreenSaverControls") && + ((Settings::getInstance()->getString("ScreenSaverBehavior") == "slideshow") || + (Settings::getInstance()->getString("ScreenSaverBehavior") == "random video"))) { if(mScreenSaver->getCurrentGame() != NULL && (config->isMappedLike("right", input) || config->isMappedTo("start", input) || config->isMappedTo("select", input))) { if(config->isMappedLike("right", input) || config->isMappedTo("select", input)) { - if (input.value != 0) { - // handle screensaver control + if (input.value != 0) // handle screensaver control mScreenSaver->nextVideo(); - } + return; } else if(config->isMappedTo("start", input) && input.value != 0) @@ -156,17 +205,17 @@ void Window::input(InputConfig* config, Input input) if (cancelScreenSaver()) return; - if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_g && SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug")) + if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_g && SDL_GetModState() & KMOD_LCTRL/* && Settings::getInstance()->getBool("Debug")*/) { // toggle debug grid with Ctrl-G Settings::getInstance()->setBool("DebugGrid", !Settings::getInstance()->getBool("DebugGrid")); } - else if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_t && SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug")) + else if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_t && SDL_GetModState() & KMOD_LCTRL/* && Settings::getInstance()->getBool("Debug")*/) { // toggle TextComponent debug view with Ctrl-T Settings::getInstance()->setBool("DebugText", !Settings::getInstance()->getBool("DebugText")); } - else if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_i && SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug")) + else if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_i && SDL_GetModState() & KMOD_LCTRL/* && Settings::getInstance()->getBool("Debug")*/) { // toggle TextComponent debug view with Ctrl-I Settings::getInstance()->setBool("DebugImage", !Settings::getInstance()->getBool("DebugImage")); @@ -181,7 +230,10 @@ void Window::input(InputConfig* config, Input input) } void Window::update(int deltaTime) -{ +{ + processPostedFunctions(); + processNotificationMessages(); + if(mNormalizeNextUpdate) { mNormalizeNextUpdate = false; @@ -217,6 +269,46 @@ void Window::update(int deltaTime) mFrameCountElapsed = 0; } + /* draw the clock */ // batocera + if (Settings::getInstance()->getBool("DrawClock") && mClock) + { + mClockElapsed -= deltaTime; + if (mClockElapsed <= 0) + { + time_t clockNow = time(0); + struct tm clockTstruct = *localtime(&clockNow); + + if (clockTstruct.tm_year > 100) + { + // Display the clock only if year is more than 1900+100 ; rpi have no internal clock and out of the networks, the date time information has no value */ + // Visit http://en.cppreference.com/w/cpp/chrono/c/strftime for more information about date/time format + + char clockBuf[32]; + +#if WIN32 + std::string oldLocale = setlocale(LC_TIME, nullptr); + setlocale(LC_TIME, ""); + + char ampm[32]; + strftime(ampm, sizeof(ampm), "%p", &clockTstruct); + + if (!std::string(&m[0]).empty()) + strftime(clockBuf, sizeof(clockBuf), "%I:%M %p", &clockTstruct); + else +#endif + strftime(clockBuf, sizeof(clockBuf), "%H:%M", &clockTstruct); + +#if WIN32 + setlocale(LC_TIME, oldLocale.c_str()); +#endif + + mClock->setText(clockBuf); + } + + mClockElapsed = 1000; // next update in 1000ms + } + } + mTimeSinceLastInput += deltaTime; if(peekGui()) @@ -225,6 +317,8 @@ void Window::update(int deltaTime) // Update the screensaver if (mScreenSaver) mScreenSaver->update(deltaTime); + + AudioManager::update(deltaTime); } void Window::render() @@ -242,13 +336,23 @@ void Window::render() bottom->render(transform); if(bottom != top) { + if (top->isKindOf() && mGuiStack.size() > 2) + { + auto& middle = mGuiStack.at(mGuiStack.size() - 2); + if (middle != bottom) + middle->render(transform); + } + mBackgroundOverlay->render(transform); top->render(transform); } } - if(!mRenderedHelpPrompts) - mHelp->render(transform); + + // GPI skip + if (mGuiStack.size() < 2 || !Renderer::isSmallScreen()) + if(!mRenderedHelpPrompts) + mHelp->render(transform); if(Settings::getInstance()->getBool("DrawFramerate") && mFrameDataText) { @@ -256,18 +360,38 @@ void Window::render() mDefaultFonts.at(1)->renderTextCache(mFrameDataText.get()); } + + // clock // batocera + if (Settings::getInstance()->getBool("DrawClock") && mClock && (mGuiStack.size() < 2 || !Renderer::isSmallScreen())) + { + mClock->render(transform); + // Renderer::setMatrix(Transform4x4f::Identity()); + /* + if (mClockFont == nullptr) + mClockFont = mDefaultFonts.at(0); + + mClockFont->renderTextCache(mClockText.get());*/ + } + + // pads // batocera + Renderer::setMatrix(Transform4x4f::Identity()); + unsigned int screensaverTime = (unsigned int)Settings::getInstance()->getInt("ScreenSaverTime"); if(mTimeSinceLastInput >= screensaverTime && screensaverTime != 0) startScreenSaver(); + if(!mRenderScreenSaver && mInfoPopup) + mInfoPopup->render(transform); + + renderRegisteredNotificationComponents(transform); + + // Always call the screensaver render function regardless of whether the screensaver is active // or not because it may perform a fade on transition renderScreenSaver(); - - if(!mRenderScreenSaver && mInfoPopup) - { - mInfoPopup->render(transform); - } + + for (auto extra : mScreenExtras) + extra->render(transform); if(mTimeSinceLastInput >= screensaverTime && screensaverTime != 0) { @@ -297,20 +421,114 @@ void Window::setAllowSleep(bool sleep) mAllowSleep = sleep; } -void Window::renderLoadingScreen(std::string text) +void Window::endRenderLoadingScreen() +{ + mSplash = NULL; + mCustomSplash = ""; +} + +void Window::renderLoadingScreen(std::string text, float percent, unsigned char opacity) +{ + if (mSplash == NULL) + mSplash = TextureResource::get(":/splash.svg", false, true, false, false); + + Transform4x4f trans = Transform4x4f::Identity(); + Renderer::setMatrix(trans); + Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x00000000 | opacity); + + if (percent >= 0) + { + float baseHeight = 0.04f; + + float w = Renderer::getScreenWidth() / 2; + float h = Renderer::getScreenHeight() * baseHeight; + + float x = Renderer::getScreenWidth() / 2 - w / 2; + float y = Renderer::getScreenHeight() - (Renderer::getScreenHeight() * 3 * baseHeight); + + Renderer::drawRect(x, y, w, h, 0x25252500 | opacity); + Renderer::drawRect(x, y, (w*percent), h, 0x006C9E00 | opacity, 0x003E5C00 | opacity, true); // 0xFFFFFFFF + } + + ImageComponent splash(this, true); + splash.setResize(Renderer::getScreenWidth() * 0.4f, 0.0f); + + if (mSplash != NULL) + splash.setImage(mSplash); + else + splash.setImage(":/splash.svg"); + + splash.setPosition((Renderer::getScreenWidth() - splash.getSize().x()) / 2, (Renderer::getScreenHeight() - splash.getSize().y()) / 2 * 0.7f); + splash.render(trans); + + auto& font = mDefaultFonts.at(1); + TextCache* cache = font->buildTextCache(text, 0, 0, 0x65656500 | opacity); + + float x = Math::round((Renderer::getScreenWidth() - cache->metrics.size.x()) / 2.0f); + float y = Math::round(Renderer::getScreenHeight() * 0.78f); // 35 + trans = trans.translate(Vector3f(x, y, 0.0f)); + Renderer::setMatrix(trans); + font->renderTextCache(cache); + delete cache; + + Renderer::swapBuffers(); + +#if defined(_WIN32) + // Avoid Window Freezing on Windows + SDL_Event event; + while (SDL_PollEvent(&event)); +#endif +} + +void Window::loadCustomImageLoadingScreen(std::string imagePath, std::string customText) +{ + if (!Utils::FileSystem::exists(imagePath)) + return; + + if (Settings::getInstance()->getBool("HideWindow")) + return; + + if (mSplash != NULL) + endRenderLoadingScreen(); + + mSplash = TextureResource::get(imagePath, false, false, true, false, false, MaxSizeInfo(Renderer::getScreenWidth() * 0.60f, Renderer::getScreenHeight() * 0.60f)); + mCustomSplash = customText; + + std::shared_ptr& rm = ResourceManager::getInstance(); + rm->removeReloadable(mSplash); +} + +void Window::renderGameLoadingScreen(float opacity, bool swapBuffers) { + if (mSplash == NULL) + mSplash = TextureResource::get(":/splash.svg", false, true, false, false); + Transform4x4f trans = Transform4x4f::Identity(); Renderer::setMatrix(trans); - Renderer::drawRect(0.0f, 0.0f, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x000000FF, 0x000000FF); + Renderer::drawRect(0.0f, 0.0f, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x00000000 | (unsigned char)(opacity * 255)); ImageComponent splash(this, true); - splash.setResize(Renderer::getScreenWidth() * 0.6f, 0.0f); - splash.setImage(":/splash.svg"); - splash.setPosition((Renderer::getScreenWidth() - splash.getSize().x()) / 2, (Renderer::getScreenHeight() - splash.getSize().y()) / 2 * 0.6f); + + if (mSplash != NULL) + splash.setImage(mSplash); + else + splash.setImage(":/splash.svg"); + + splash.setOrigin(0.5, 0.5); + splash.setPosition(Renderer::getScreenWidth() / 2.0f, Renderer::getScreenHeight() * 0.835f / 2.0f); + splash.setMaxSize(Renderer::getScreenWidth() * 0.60f, Renderer::getScreenHeight() * 0.60f); + + if (!mCustomSplash.empty()) + splash.setColorShift(0xFFFFFF00 | (unsigned char)(opacity * 210)); + else + splash.setColorShift(0xFFFFFF00 | (unsigned char)(opacity * 255)); + splash.render(trans); auto& font = mDefaultFonts.at(1); - TextCache* cache = font->buildTextCache(text, 0, 0, 0x656565FF); + font->reload(); // Ensure font is loaded + + TextCache* cache = font->buildTextCache(mCustomSplash.empty() ? _("Loading...") : mCustomSplash, 0, 0, 0x65656500 | (unsigned char)(opacity * 255)); float x = Math::round((Renderer::getScreenWidth() - cache->metrics.size.x()) / 2.0f); float y = Math::round(Renderer::getScreenHeight() * 0.835f); @@ -319,9 +537,11 @@ void Window::renderLoadingScreen(std::string text) font->renderTextCache(cache); delete cache; - Renderer::swapBuffers(); + if (swapBuffers) + Renderer::swapBuffers(); } + void Window::renderHelpPromptsEarly() { mHelp->render(Transform4x4f::Identity()); @@ -330,8 +550,14 @@ void Window::renderHelpPromptsEarly() void Window::setHelpPrompts(const std::vector& prompts, const HelpStyle& style) { + // Keep a temporary reference to the previous grid. + // It avoids unloading/reloading images if they are the same, and avoids flickerings + auto oldGrid = mHelp->getGrid(); + mHelp->clearPrompts(); mHelp->setStyle(style); + + mClockElapsed = -1; std::vector addPrompts; @@ -417,6 +643,9 @@ void Window::startScreenSaver() { if (mScreenSaver && !mRenderScreenSaver) { + for (auto extra : mScreenExtras) + extra->onScreenSaverActivate(); + // Tell the GUI components the screensaver is starting for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) (*i)->onScreenSaverActivate(); @@ -429,7 +658,7 @@ void Window::startScreenSaver() bool Window::cancelScreenSaver() { if (mScreenSaver && mRenderScreenSaver) - { + { mScreenSaver->stopScreenSaver(); mRenderScreenSaver = false; mScreenSaver->resetCounts(); @@ -438,6 +667,9 @@ bool Window::cancelScreenSaver() for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) (*i)->onScreenSaverDeactivate(); + for (auto extra : mScreenExtras) + extra->onScreenSaverDeactivate(); + return true; } @@ -449,3 +681,136 @@ void Window::renderScreenSaver() if (mScreenSaver) mScreenSaver->renderScreenSaver(); } + +static std::mutex mNotificationMessagesLock; + +void Window::displayNotificationMessage(std::string message, int duration) +{ + std::unique_lock lock(mNotificationMessagesLock); + + if (duration <= 0) + { + duration = Settings::getInstance()->getInt("audio.display_titles_time"); + if (duration <= 2 || duration > 120) + duration = 10; + + duration *= 1000; + } + + NotificationMessage msg; + msg.first = message; + msg.second = duration; + mNotificationMessages.push_back(msg); +} + +void Window::processNotificationMessages() +{ + std::unique_lock lock(mNotificationMessagesLock); + + if (mNotificationMessages.empty()) + return; + + NotificationMessage msg = mNotificationMessages.back(); + mNotificationMessages.pop_back(); + + LOG(LogDebug) << "Notification message :" << msg.first.c_str(); + + if (mInfoPopup) + delete mInfoPopup; + + mInfoPopup = new GuiInfoPopup(this, msg.first, msg.second); +} + +void Window::registerNotificationComponent(AsyncNotificationComponent* pc) +{ + std::unique_lock lock(mNotificationMessagesLock); + + if (std::find(mAsyncNotificationComponent.cbegin(), mAsyncNotificationComponent.cend(), pc) != mAsyncNotificationComponent.cend()) + return; + + mAsyncNotificationComponent.push_back(pc); +} + +void Window::unRegisterNotificationComponent(AsyncNotificationComponent* pc) +{ + std::unique_lock lock(mNotificationMessagesLock); + + auto it = std::find(mAsyncNotificationComponent.cbegin(), mAsyncNotificationComponent.cend(), pc); + if (it != mAsyncNotificationComponent.cend()) + mAsyncNotificationComponent.erase(it); +} + +void Window::renderRegisteredNotificationComponents(const Transform4x4f& trans) +{ + std::unique_lock lock(mNotificationMessagesLock); + +#define PADDING_H (Renderer::getScreenWidth()*0.01) + + float posY = Renderer::getScreenHeight() * 0.02f; + + for (auto child : mAsyncNotificationComponent) + { + float posX = Renderer::getScreenWidth()*0.99f - child->getSize().x(); + + child->setPosition(posX, posY, 0); + child->render(trans); + + posY += child->getSize().y() + PADDING_H; + } +} + +void Window::postToUiThread(const std::function& func) +{ + std::unique_lock lock(mNotificationMessagesLock); + + mFunctions.push_back(func); +} + +void Window::processPostedFunctions() +{ + std::unique_lock lock(mNotificationMessagesLock); + + for (auto func : mFunctions) + func(this); + + mFunctions.clear(); +} + +void Window::onThemeChanged(const std::shared_ptr& theme) +{ + for (auto extra : mScreenExtras) + delete extra; + + mScreenExtras.clear(); + mScreenExtras = ThemeData::makeExtras(theme, "screen", this); + + std::stable_sort(mScreenExtras.begin(), mScreenExtras.end(), [](GuiComponent* a, GuiComponent* b) { return b->getZIndex() > a->getZIndex(); }); + + if (mBackgroundOverlay) + mBackgroundOverlay->setImage(ThemeData::getMenuTheme()->Background.fadePath); + + if (mClock) + { + mClock->setFont(Font::get(FONT_SIZE_SMALL)); + mClock->setColor(0x777777FF); + mClock->setHorizontalAlignment(ALIGN_RIGHT); + mClock->setVerticalAlignment(ALIGN_TOP); + + // if clock element does not exist in screen view -> + // skin it from system.helpsystem -> ) + if (!theme->getElement("screen", "clock", "text")) + { + auto elem = theme->getElement("system", "help", "helpsystem"); + if (elem && elem->has("textColor")) + mClock->setColor(elem->get("textColor")); + + if (elem && (elem->has("fontPath") || elem->has("fontSize"))) + mClock->setFont(Font::getFromTheme(elem, ThemeFlags::ALL, Font::get(FONT_SIZE_MEDIUM))); + } + + mClock->setPosition(Renderer::getScreenWidth()*0.94, Renderer::getScreenHeight()*0.9965 - mClock->getFont()->getHeight()); + mClock->setSize(Renderer::getScreenWidth()*0.05, 0); + + mClock->applyTheme(theme, "screen", "clock", ThemeFlags::ALL ^ (ThemeFlags::TEXT)); + } +} diff --git a/es-core/src/Window.h b/es-core/src/Window.h index 3f104c648e..55187f7a45 100644 --- a/es-core/src/Window.h +++ b/es-core/src/Window.h @@ -7,6 +7,7 @@ #include "Settings.h" #include +#include class FileData; class Font; @@ -16,6 +17,11 @@ class ImageComponent; class InputConfig; class TextCache; class Transform4x4f; +class TextureResource; +class AsyncNotificationComponent; +class ThemeData; +class TextComponent; + struct HelpStyle; class Window @@ -55,8 +61,8 @@ class Window void update(int deltaTime); void render(); - bool init(); - void deinit(); + bool init(bool initRenderer); + void deinit(bool deinitRenderer); void normalizeNextUpdate(); @@ -64,20 +70,44 @@ class Window bool getAllowSleep(); void setAllowSleep(bool sleep); - void renderLoadingScreen(std::string text); + void endRenderLoadingScreen(); + void renderLoadingScreen(std::string text, float percent = -1, unsigned char opacity = 255); + void renderGameLoadingScreen(float opacity=1, bool swapBuffers=true); + + void loadCustomImageLoadingScreen(std::string imagePath, std::string customText); void renderHelpPromptsEarly(); // used to render HelpPrompts before a fade void setHelpPrompts(const std::vector& prompts, const HelpStyle& style); void setScreenSaver(ScreenSaver* screenSaver) { mScreenSaver = screenSaver; } - void setInfoPopup(InfoPopup* infoPopup) { delete mInfoPopup; mInfoPopup = infoPopup; } +// void setInfoPopup(InfoPopup* infoPopup) { delete mInfoPopup; mInfoPopup = infoPopup; } inline void stopInfoPopup() { if (mInfoPopup) mInfoPopup->stop(); }; void startScreenSaver(); bool cancelScreenSaver(); void renderScreenSaver(); + void displayNotificationMessage(std::string message, int duration = -1); + + void registerNotificationComponent(AsyncNotificationComponent* pc); + void unRegisterNotificationComponent(AsyncNotificationComponent* pc); + + void postToUiThread(const std::function& func); + void reactivateGui(); + + void onThemeChanged(const std::shared_ptr& theme); + private: + void processPostedFunctions(); + + void renderRegisteredNotificationComponents(const Transform4x4f& trans); + std::vector mAsyncNotificationComponent; + std::vector> mFunctions; + + typedef std::pair NotificationMessage; + std::vector mNotificationMessages; + void processNotificationMessages(); + void onSleep(); void onWake(); @@ -90,6 +120,10 @@ class Window InfoPopup* mInfoPopup; bool mRenderScreenSaver; + std::shared_ptr mSplash; + std::string mCustomSplash; + + std::vector mScreenExtras; std::vector mGuiStack; std::vector< std::shared_ptr > mDefaultFonts; @@ -100,6 +134,11 @@ class Window std::unique_ptr mFrameDataText; + // clock // batocera + int mClockElapsed; + + std::shared_ptr mClock; + bool mNormalizeNextUpdate; bool mAllowSleep; diff --git a/es-core/src/components/AsyncNotificationComponent.cpp b/es-core/src/components/AsyncNotificationComponent.cpp new file mode 100644 index 0000000000..4eb42d2d9e --- /dev/null +++ b/es-core/src/components/AsyncNotificationComponent.cpp @@ -0,0 +1,139 @@ +#include "AsyncNotificationComponent.h" +#include "ThemeData.h" +#include "PowerSaver.h" +#include "components/ComponentGrid.h" +#include "components/NinePatchComponent.h" +#include "components/TextComponent.h" +#include "EsLocale.h" + +#define PADDING_PX (Renderer::getScreenWidth()*0.01) + +AsyncNotificationComponent::AsyncNotificationComponent(Window* window, bool actionLine) + : GuiComponent(window) +{ + mPercent = -1; + + auto theme = ThemeData::getMenuTheme(); + + // Note : Don't localize this text -> It is only used to guess width calculation for the component. + float width = theme->TextSmall.font->sizeText("TEXT FOR SIZE CALCULATION").x(); // Renderer::getScreenWidth() * 0.14f; + + mTitle = std::make_shared(mWindow, "", theme->TextSmall.font, theme->TextSmall.color, ALIGN_LEFT); + mGameName = std::make_shared(mWindow, "", theme->TextSmall.font, theme->Text.color, ALIGN_LEFT); + + if (actionLine) + mAction = std::make_shared(mWindow, "", theme->TextSmall.font, theme->Text.color, ALIGN_LEFT); + + Vector2f fullSize(width + 2 * PADDING_PX, 2 * PADDING_PX + mTitle->getSize().y() + mGameName->getSize().y() + (mAction == nullptr ? 0 : mAction->getSize().y())); + Vector2f gridSize(width, mTitle->getSize().y() + mGameName->getSize().y() + (mAction == nullptr ? 0 : mAction->getSize().y())); + + setSize(fullSize); + + mFrame = new NinePatchComponent(window); + mFrame->setImagePath(theme->Background.path); + mFrame->setEdgeColor(theme->Background.color); + mFrame->setCenterColor(theme->Background.centerColor); + mFrame->setCornerSize(theme->Background.cornerSize); + mFrame->fitTo(mSize, Vector3f::Zero(), Vector2f(-32, -32)); + addChild(mFrame); + + mGrid = new ComponentGrid(window, Vector2i(1, mAction == nullptr ? 2 : 3)); + mGrid->setPosition((fullSize.x() - gridSize.x()) / 2.0, (fullSize.y() - gridSize.y()) / 2.0); + mGrid->setSize(gridSize); + mGrid->setEntry(mTitle, Vector2i(0, 0), false, true); + mGrid->setEntry(mGameName, Vector2i(0, 1), false, true); + + if (mAction != nullptr) + mGrid->setEntry(mAction, Vector2i(0, 2), false, true); + + addChild(mGrid); + + float posX = Renderer::getScreenWidth()*0.5f - mSize.x()*0.5f; + float posY = Renderer::getScreenHeight() * 0.02f; + + // FCA TopRight + posX = Renderer::getScreenWidth()*0.99f - mSize.x(); + posY = Renderer::getScreenHeight() * 0.02f; + + setPosition(posX, posY, 0); + setOpacity(200); + + PowerSaver::pause(); +} + +AsyncNotificationComponent::~AsyncNotificationComponent() +{ + delete mFrame; + delete mGrid; + + PowerSaver::resume(); +} + +void AsyncNotificationComponent::updateText(const std::string text, const std::string action) +{ + std::unique_lock lock(mMutex); + + mNextGameName = text; + mNextAction = action; +} + +void AsyncNotificationComponent::updatePercent(int percent) +{ + std::unique_lock lock(mMutex); + + mPercent = percent; +} + +void AsyncNotificationComponent::updateTitle(const std::string text) +{ + std::unique_lock lock(mMutex); + + mNextTitle = text; +} + +void AsyncNotificationComponent::render(const Transform4x4f& parentTrans) +{ + std::unique_lock lock(mMutex); + + Transform4x4f trans = parentTrans * getTransform(); + + if (mGameName != nullptr && mNextGameName != mGameName->getText()) + mGameName->setText(mNextGameName); + + if (mAction != nullptr && mNextAction != mAction->getText()) + mAction->setText(mNextAction); + + if (mTitle != nullptr && mNextTitle != mTitle->getText()) + mTitle->setText(mNextTitle); + + mFrame->render(trans); + + auto lastControl = mGameName; + if (mAction != nullptr) + lastControl = mAction; + + float x = mGrid->getPosition().x() + lastControl->getPosition().x(); + float y = mGrid->getPosition().y() + lastControl->getPosition().y(); + float w = lastControl->getSize().x(); + float h = lastControl->getSize().y(); + + h /= 10.0; + y += lastControl->getSize().y(); + + Renderer::setMatrix(trans); + + if (mPercent >= 0) + { + float percent = mPercent / 100.0; + if (percent < 0) + percent = 0; + if (percent > 100) + percent = 100; + + auto theme = ThemeData::getMenuTheme(); + auto color = theme->Text.color & 0xFFFFFF00 | (unsigned char)((theme->Text.color & 0xFF) * (mOpacity / 255.0)); + Renderer::drawRect(x, y, (w*percent), h, color); + } + + mGrid->render(trans); +} diff --git a/es-core/src/components/AsyncNotificationComponent.h b/es-core/src/components/AsyncNotificationComponent.h new file mode 100644 index 0000000000..39dd3c8296 --- /dev/null +++ b/es-core/src/components/AsyncNotificationComponent.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include "GuiComponent.h" + +class ComponentGrid; +class NinePatchComponent; +class TextComponent; +class Window; + +class AsyncNotificationComponent : public GuiComponent +{ +public: + AsyncNotificationComponent(Window* window, bool actionLine = true); + ~AsyncNotificationComponent(); + + void updateTitle(const std::string text); + void updateText(const std::string text, const std::string action = ""); + void updatePercent(int percent); + + void render(const Transform4x4f& parentTrans) override; + +private: + std::shared_ptr mTitle; + std::shared_ptr mGameName; + std::shared_ptr mAction; + + std::string mNextGameName; + std::string mNextTitle; + std::string mNextAction; + + ComponentGrid* mGrid; + NinePatchComponent* mFrame; + + std::mutex mMutex; + + int mPercent; +}; \ No newline at end of file diff --git a/es-core/src/components/BusyComponent.cpp b/es-core/src/components/BusyComponent.cpp index a37d690beb..aa911aab91 100644 --- a/es-core/src/components/BusyComponent.cpp +++ b/es-core/src/components/BusyComponent.cpp @@ -16,9 +16,17 @@ const AnimationDef BUSY_ANIMATION_DEF = { BUSY_ANIMATION_FRAMES, 4, true }; BusyComponent::BusyComponent(Window* window) : GuiComponent(window), mBackground(window, ":/frame.png"), mGrid(window, Vector2i(5, 3)) { + auto theme = ThemeData::getMenuTheme(); + mBackground.setImagePath(theme->Background.path); + mBackground.setEdgeColor(theme->Background.color); + mBackground.setCenterColor(theme->Background.centerColor); + mBackground.setCornerSize(theme->Background.cornerSize); + + mutex = SDL_CreateMutex(); + mAnimation = std::make_shared(mWindow); mAnimation->load(&BUSY_ANIMATION_DEF); - mText = std::make_shared(mWindow, "WORKING...", Font::get(FONT_SIZE_MEDIUM), 0x777777FF); + mText = std::make_shared(mWindow, _("WORKING..."), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color); // col 0 = animation, col 1 = spacer, col 2 = text mGrid.setEntry(mAnimation, Vector2i(1, 1), false, true); @@ -28,6 +36,40 @@ BusyComponent::BusyComponent(Window* window) : GuiComponent(window), addChild(&mGrid); } +BusyComponent::~BusyComponent() +{ + SDL_DestroyMutex(mutex); +} + +void BusyComponent::setText(std::string txt) +{ + if (SDL_LockMutex(mutex) == 0) + { + if (threadMessage != txt) + { + threadMessage = txt; + threadMessagechanged = true; + } + + SDL_UnlockMutex(mutex); + } +} + +void BusyComponent::render(const Transform4x4f& parentTrans) +{ + if (SDL_LockMutex(mutex) == 0) + { + if (threadMessagechanged) + { + threadMessagechanged = false; + mText->setText(threadMessage); + onSizeChanged(); + } + SDL_UnlockMutex(mutex); + } + GuiComponent::render(parentTrans); +} + void BusyComponent::onSizeChanged() { mGrid.setSize(mSize); diff --git a/es-core/src/components/BusyComponent.h b/es-core/src/components/BusyComponent.h index fe996a31cc..e589bb0e73 100644 --- a/es-core/src/components/BusyComponent.h +++ b/es-core/src/components/BusyComponent.h @@ -5,6 +5,7 @@ #include "components/ComponentGrid.h" #include "components/NinePatchComponent.h" #include "GuiComponent.h" +#include class AnimatedImageComponent; class TextComponent; @@ -13,17 +14,25 @@ class BusyComponent : public GuiComponent { public: BusyComponent(Window* window); + ~BusyComponent(); void onSizeChanged() override; + void setText(std::string txt); void reset(); // reset to frame 0 + virtual void render(const Transform4x4f& parentTrans); + private: NinePatchComponent mBackground; ComponentGrid mGrid; std::shared_ptr mAnimation; std::shared_ptr mText; + + SDL_mutex *mutex; + bool threadMessagechanged; + std::string threadMessage; }; #endif // ES_CORE_COMPONENTS_BUSY_COMPONENT_H diff --git a/es-core/src/components/ButtonComponent.cpp b/es-core/src/components/ButtonComponent.cpp index d0f47ce3a7..d19c262434 100644 --- a/es-core/src/components/ButtonComponent.cpp +++ b/es-core/src/components/ButtonComponent.cpp @@ -3,21 +3,34 @@ #include "resources/Font.h" #include "utils/StringUtil.h" -ButtonComponent::ButtonComponent(Window* window, const std::string& text, const std::string& helpText, const std::function& func) : GuiComponent(window), - mBox(window, ":/button.png"), - mFont(Font::get(FONT_SIZE_MEDIUM)), - mFocused(false), - mEnabled(true), +ButtonComponent::ButtonComponent(Window* window, const std::string& text, const std::string& helpText, const std::function& func, bool upperCase) : GuiComponent(window), + mBox(window, ThemeData::getMenuTheme()->Icons.button), + mFont(Font::get(FONT_SIZE_MEDIUM)), + mFocused(false), + mEnabled(true), mTextColorFocused(0xFFFFFFFF), mTextColorUnfocused(0x777777FF) { + auto menuTheme = ThemeData::getMenuTheme(); + + mFont = menuTheme->Text.font; + mTextColorUnfocused = menuTheme->Text.color; + mTextColorFocused = menuTheme->Text.selectedColor; + mColor = menuTheme->Text.color; + mColorFocused = menuTheme->Text.selectorColor; + mRenderNonFocusedBackground = true; + + if (Renderer::isSmallScreen()) + mBox.setCornerSize(8, 8); + setPressedFunc(func); - setText(text, helpText); + setText(text, helpText, upperCase); updateImage(); } void ButtonComponent::onSizeChanged() { - mBox.fitTo(mSize, Vector3f::Zero(), Vector2f(-32, -32)); + auto sz = mBox.getCornerSize(); + mBox.fitTo(mSize, Vector3f::Zero(), Vector2f(-sz.x() * 2, -sz.y() * 2)); } void ButtonComponent::setPressedFunc(std::function f) @@ -37,11 +50,11 @@ bool ButtonComponent::input(InputConfig* config, Input input) return GuiComponent::input(config, input); } -void ButtonComponent::setText(const std::string& text, const std::string& helpText) +void ButtonComponent::setText(const std::string& text, const std::string& helpText, bool upperCase) { - mText = Utils::String::toUpper(text); + mText = upperCase ? Utils::String::toUpper(text) : text; mHelpText = helpText; - + mTextCache = std::unique_ptr(mFont->buildTextCache(mText, 0, 0, getCurTextColor())); float minWidth = mFont->sizeText("DELETE").x() + 12; @@ -72,22 +85,32 @@ void ButtonComponent::updateImage() { if(!mEnabled || !mPressedFunc) { - mBox.setImagePath(":/button_filled.png"); + mBox.setImagePath(ThemeData::getMenuTheme()->Icons.button_filled); mBox.setCenterColor(0x770000FF); mBox.setEdgeColor(0x770000FF); return; } - mBox.setCenterColor(0xFFFFFFFF); - mBox.setEdgeColor(0xFFFFFFFF); - mBox.setImagePath(mFocused ? ":/button_filled.png" : ":/button.png"); + // If a new color has been set. + if (mNewColor) { + mBox.setImagePath(ThemeData::getMenuTheme()->Icons.button_filled); + mBox.setCenterColor(mModdedColor); + mBox.setEdgeColor(mModdedColor); + return; + } + + mBox.setCenterColor(getCurBackColor()); + mBox.setEdgeColor(getCurBackColor()); + mBox.setImagePath(mFocused ? ThemeData::getMenuTheme()->Icons.button_filled : ThemeData::getMenuTheme()->Icons.button); + //mBox.setImagePath(mFocused ? ":/button_filled.png" : ":/button.png"); } void ButtonComponent::render(const Transform4x4f& parentTrans) { Transform4x4f trans = parentTrans * getTransform(); - mBox.render(trans); + if (mRenderNonFocusedBackground || mFocused) + mBox.render(trans); if(mTextCache) { @@ -111,6 +134,14 @@ unsigned int ButtonComponent::getCurTextColor() const return mTextColorFocused; } +unsigned int ButtonComponent::getCurBackColor() const +{ + if (!mFocused) + return mColor; + else + return mColorFocused; +} + std::vector ButtonComponent::getHelpPrompts() { std::vector prompts; diff --git a/es-core/src/components/ButtonComponent.h b/es-core/src/components/ButtonComponent.h index 2c25881f04..905089de62 100644 --- a/es-core/src/components/ButtonComponent.h +++ b/es-core/src/components/ButtonComponent.h @@ -10,7 +10,7 @@ class TextCache; class ButtonComponent : public GuiComponent { public: - ButtonComponent(Window* window, const std::string& text = "", const std::string& helpText = "", const std::function& func = nullptr); + ButtonComponent(Window* window, const std::string& text = "", const std::string& helpText = "", const std::function& func = nullptr, bool upperCase = true); void setPressedFunc(std::function f); @@ -19,7 +19,7 @@ class ButtonComponent : public GuiComponent bool input(InputConfig* config, Input input) override; void render(const Transform4x4f& parentTrans) override; - void setText(const std::string& text, const std::string& helpText); + void setText(const std::string& text, const std::string& helpText, bool upperCase = true); inline const std::string& getText() const { return mText; }; inline const std::function& getPressedFunc() const { return mPressedFunc; }; @@ -28,24 +28,38 @@ class ButtonComponent : public GuiComponent void onFocusGained() override; void onFocusLost() override; + void setColorShift(unsigned int color) { mModdedColor = color; mNewColor = true; updateImage(); } + void removeColorShift() { mNewColor = false; updateImage(); } + virtual std::vector getHelpPrompts() override; + void setRenderNonFocusedBackground(bool value) { mRenderNonFocusedBackground = value; } + private: std::shared_ptr mFont; std::function mPressedFunc; bool mFocused; bool mEnabled; + bool mNewColor = false; + bool mRenderNonFocusedBackground; + unsigned int mTextColorFocused; unsigned int mTextColorUnfocused; + unsigned int mModdedColor; unsigned int getCurTextColor() const; + unsigned int getCurBackColor() const; + void updateImage(); std::string mText; std::string mHelpText; std::unique_ptr mTextCache; NinePatchComponent mBox; + + unsigned int mColor; + unsigned int mColorFocused; }; #endif // ES_CORE_COMPONENTS_BUTTON_COMPONENT_H diff --git a/es-core/src/components/ComponentGrid.cpp b/es-core/src/components/ComponentGrid.cpp index 90b6162d92..df038257af 100644 --- a/es-core/src/components/ComponentGrid.cpp +++ b/es-core/src/components/ComponentGrid.cpp @@ -1,6 +1,6 @@ #include "components/ComponentGrid.h" - #include "Settings.h" +#include "ThemeData.h" using namespace GridFlags; @@ -9,6 +9,7 @@ ComponentGrid::ComponentGrid(Window* window, const Vector2i& gridDimensions) : G { assert(gridDimensions.x() > 0 && gridDimensions.y() > 0); + mSeparatorColor = ThemeData::getMenuTheme()->Text.separatorColor; mCells.reserve(gridDimensions.x() * gridDimensions.y()); mColWidths = new float[gridDimensions.x()]; @@ -39,7 +40,7 @@ float ComponentGrid::getColWidth(int col) if(mColWidths[x] == 0) between++; } - + return (freeWidthPerc * mSize.x()) / between; } @@ -57,7 +58,7 @@ float ComponentGrid::getRowHeight(int row) if(mRowHeights[y] == 0) between++; } - + return (freeHeightPerc * mSize.y()) / between; } @@ -142,7 +143,7 @@ void ComponentGrid::updateCellComponent(const GridEntry& cell) // center component pos[0] = pos.x() + (size.x() - cell.component->getSize().x()) / 2; pos[1] = pos.y() + (size.y() - cell.component->getSize().y()) / 2; - + cell.component->setPosition(pos); } @@ -150,9 +151,10 @@ void ComponentGrid::updateSeparators() { mLines.clear(); - const unsigned int color = Renderer::convertColor(0xC6C7C6FF); bool drawAll = Settings::getInstance()->getBool("DebugGrid"); + const unsigned int color = Renderer::convertColor(mSeparatorColor); + Vector2f pos; Vector2f size; for(auto it = mCells.cbegin(); it != mCells.cend(); it++) @@ -172,25 +174,25 @@ void ComponentGrid::updateSeparators() for(int y = it->pos.y(); y < it->pos.y() + it->dim.y(); y++) size[1] += getRowHeight(y); - if(it->border & BORDER_TOP || drawAll) + if (it->border & BORDER_TOP || drawAll) { - mLines.push_back( { { pos.x(), pos.y() }, { 0.0f, 0.0f }, color } ); - mLines.push_back( { { pos.x() + size.x(), pos.y() }, { 0.0f, 0.0f }, color } ); + mLines.push_back({ { pos.x(), pos.y() }, { 0.0f, 0.0f }, color }); + mLines.push_back({ { pos.x() + size.x(), pos.y() }, { 0.0f, 0.0f }, color }); } - if(it->border & BORDER_BOTTOM || drawAll) + if (it->border & BORDER_BOTTOM || drawAll) { - mLines.push_back( { { pos.x(), pos.y() + size.y() }, { 0.0f, 0.0f }, color } ); - mLines.push_back( { { pos.x() + size.x(), mLines.back().pos.y() }, { 0.0f, 0.0f }, color } ); + mLines.push_back({ { pos.x(), pos.y() + size.y() }, { 0.0f, 0.0f }, color }); + mLines.push_back({ { pos.x() + size.x(), mLines.back().pos.y() }, { 0.0f, 0.0f }, color }); } - if(it->border & BORDER_LEFT || drawAll) + if (it->border & BORDER_LEFT || drawAll) { - mLines.push_back( { { pos.x(), pos.y() }, { 0.0f, 0.0f }, color } ); - mLines.push_back( { { pos.x(), pos.y() + size.y() }, { 0.0f, 0.0f }, color } ); + mLines.push_back({ { pos.x(), pos.y() }, { 0.0f, 0.0f }, color }); + mLines.push_back({ { pos.x(), pos.y() + size.y() }, { 0.0f, 0.0f }, color }); } - if(it->border & BORDER_RIGHT || drawAll) + if (it->border & BORDER_RIGHT || drawAll) { - mLines.push_back( { { pos.x() + size.x(), pos.y() }, { 0.0f, 0.0f }, color } ); - mLines.push_back( { { mLines.back().pos.x(), pos.y() + size.y() }, { 0.0f, 0.0f }, color } ); + mLines.push_back({ { pos.x() + size.x(), pos.y() }, { 0.0f, 0.0f }, color }); + mLines.push_back({ { mLines.back().pos.x(), pos.y() + size.y() }, { 0.0f, 0.0f }, color }); } } } @@ -206,7 +208,7 @@ void ComponentGrid::onSizeChanged() const ComponentGrid::GridEntry* ComponentGrid::getCellAt(int x, int y) const { assert(x >= 0 && x < mGridSize.x() && y >= 0 && y < mGridSize.y()); - + for(auto it = mCells.cbegin(); it != mCells.cend(); it++) { int xmin = it->pos.x(); @@ -230,24 +232,30 @@ bool ComponentGrid::input(InputConfig* config, Input input) if(!input.value) return false; + bool result = false; + if(config->isMappedLike("down", input)) { - return moveCursor(Vector2i(0, 1)); + result = moveCursor(Vector2i(0, 1)); } if(config->isMappedLike("up", input)) { - return moveCursor(Vector2i(0, -1)); + result = moveCursor(Vector2i(0, -1)); } if(config->isMappedLike("left", input)) { - return moveCursor(Vector2i(-1, 0)); + result = moveCursor(Vector2i(-1, 0)); } if(config->isMappedLike("right", input)) { - return moveCursor(Vector2i(1, 0)); + result = moveCursor(Vector2i(1, 0)); } - return false; + if (!result && mUnhandledInputCallback) { + return mUnhandledInputCallback(config, input); + } + + return result; } void ComponentGrid::resetCursor() @@ -276,7 +284,7 @@ bool ComponentGrid::moveCursor(Vector2i dir) const GridEntry* currentCursorEntry = getCellAt(mCursor); Vector2i searchAxis(dir.x() == 0, dir.y() == 0); - + while(mCursor.x() >= 0 && mCursor.y() >= 0 && mCursor.x() < mGridSize.x() && mCursor.y() < mGridSize.y()) { mCursor = mCursor + dir; @@ -357,7 +365,7 @@ void ComponentGrid::render(const Transform4x4f& parentTrans) Transform4x4f trans = parentTrans * getTransform(); renderChildren(trans); - + // draw cell separators if(mLines.size()) { @@ -410,7 +418,7 @@ std::vector ComponentGrid::getHelpPrompts() const GridEntry* e = getCellAt(mCursor); if(e) prompts = e->component->getHelpPrompts(); - + bool canScrollVert = mGridSize.y() > 1; bool canScrollHoriz = mGridSize.x() > 1; for(auto it = prompts.cbegin(); it != prompts.cend(); it++) @@ -430,11 +438,11 @@ std::vector ComponentGrid::getHelpPrompts() } if(canScrollHoriz && canScrollVert) - prompts.push_back(HelpPrompt("up/down/left/right", "choose")); + prompts.push_back(HelpPrompt("up/down/left/right", _("CHOOSE"))); else if(canScrollHoriz) - prompts.push_back(HelpPrompt("left/right", "choose")); + prompts.push_back(HelpPrompt("left/right", _("CHOOSE"))); else if(canScrollVert) - prompts.push_back(HelpPrompt("up/down", "choose")); + prompts.push_back(HelpPrompt("up/down", _("CHOOSE"))); return prompts; } diff --git a/es-core/src/components/ComponentGrid.h b/es-core/src/components/ComponentGrid.h index 5fad30e0e3..699b9d55ab 100644 --- a/es-core/src/components/ComponentGrid.h +++ b/es-core/src/components/ComponentGrid.h @@ -30,12 +30,14 @@ namespace GridFlags class ComponentGrid : public GuiComponent { public: - ComponentGrid(Window* window, const Vector2i& gridDimensions); + ComponentGrid(Window* window, const Vector2i& gridDimensions/*, unsigned int separatorColor = 0xC6C7C6FF*/); virtual ~ComponentGrid(); + void setSeparatorColor(unsigned int separatorColor) { mSeparatorColor = separatorColor; updateSeparators(); } + bool removeEntry(const std::shared_ptr& comp); - void setEntry(const std::shared_ptr& comp, const Vector2i& pos, bool canFocus, bool resize = true, + void setEntry(const std::shared_ptr& comp, const Vector2i& pos, bool canFocus, bool resize = true, const Vector2i& size = Vector2i(1, 1), unsigned int border = GridFlags::BORDER_NONE, GridFlags::UpdateType updateType = GridFlags::UPDATE_ALWAYS); void textInput(const char* text) override; @@ -70,6 +72,8 @@ class ComponentGrid : public GuiComponent virtual std::vector getHelpPrompts() override; + inline void setUnhandledInputCallback(const std::function& func) { mUnhandledInputCallback = func; } + private: class GridEntry { @@ -83,8 +87,8 @@ class ComponentGrid : public GuiComponent unsigned int border; GridEntry(const Vector2i& p = Vector2i::Zero(), const Vector2i& d = Vector2i::Zero(), - const std::shared_ptr& cmp = nullptr, bool f = false, bool r = true, - GridFlags::UpdateType u = GridFlags::UPDATE_ALWAYS, unsigned int b = GridFlags::BORDER_NONE) : + const std::shared_ptr& cmp = nullptr, bool f = false, bool r = true, + GridFlags::UpdateType u = GridFlags::UPDATE_ALWAYS, unsigned int b = GridFlags::BORDER_NONE) : pos(p), dim(d), component(cmp), canFocus(f), resize(r), updateType(u), border(b) {}; @@ -96,8 +100,10 @@ class ComponentGrid : public GuiComponent float* mRowHeights; float* mColWidths; - + std::vector mLines; + //std::vector mLineColors; + unsigned int mSeparatorColor; // Update position & size void updateCellComponent(const GridEntry& cell); @@ -105,13 +111,15 @@ class ComponentGrid : public GuiComponent const GridEntry* getCellAt(int x, int y) const; inline const GridEntry* getCellAt(const Vector2i& pos) const { return getCellAt(pos.x(), pos.y()); } - + Vector2i mGridSize; std::vector mCells; void onCursorMoved(Vector2i from, Vector2i to); Vector2i mCursor; + + std::function mUnhandledInputCallback; }; #endif // ES_CORE_COMPONENTS_COMPONENT_GRID_H diff --git a/es-core/src/components/ComponentList.cpp b/es-core/src/components/ComponentList.cpp index 1a3524f78c..dc11dd752b 100644 --- a/es-core/src/components/ComponentList.cpp +++ b/es-core/src/components/ComponentList.cpp @@ -1,4 +1,5 @@ #include "components/ComponentList.h" +#include "components/TextComponent.h" #define TOTAL_HORIZONTAL_PADDING_PX 20 @@ -24,6 +25,10 @@ void ComponentList::addRow(const ComponentListRow& row, bool setCursorHere) updateElementSize(mEntries.back().data); updateElementPosition(mEntries.back().data); + // Fix group initial cursor position + if (mCursor == 0 && mEntries.size() == 2 && !mEntries[0].data.selectable) + setCursorHere = true; + if(setCursorHere) { mCursor = (int)mEntries.size() - 1; @@ -31,6 +36,18 @@ void ComponentList::addRow(const ComponentListRow& row, bool setCursorHere) } } +void ComponentList::addGroup(const std::string& label) +{ + auto theme = ThemeData::getMenuTheme(); + + ComponentListRow row; + auto empty = std::make_shared(mWindow, label, theme->TextSmall.font, theme->TextSmall.color); + empty->setLineSpacing(2.5); + row.addElement(empty, true, false); + row.selectable = false; + addRow(row); +} + void ComponentList::onSizeChanged() { for(auto it = mEntries.cbegin(); it != mEntries.cend(); it++) @@ -133,7 +150,7 @@ void ComponentList::updateCameraOffset() { // move the camera to scroll const float totalHeight = getTotalRowHeight(); - if(totalHeight > mSize.y()) + if (totalHeight > mSize.y() && mCursor < mEntries.size()) { float target = mSelectorBarOffset + getRowHeight(mEntries.at(mCursor).data)/2 - (mSize.y() / 2); @@ -160,9 +177,18 @@ void ComponentList::render(const Transform4x4f& parentTrans) if(!size()) return; + auto menuTheme = ThemeData::getMenuTheme(); + unsigned int selectorColor = menuTheme->Text.selectorColor; + unsigned int selectorGradientColor = menuTheme->Text.selectorGradientColor; + unsigned int selectedColor = menuTheme->Text.selectedColor; + unsigned int bgColor = menuTheme->Background.color; + unsigned int separatorColor = menuTheme->Text.separatorColor; + unsigned int textColor = menuTheme->Text.color; + bool selectorGradientHorz = menuTheme->Text.selectorGradientType; + Transform4x4f trans = parentTrans * getTransform(); - // clip everything to be inside our bounds + // clip everything to be inside our boundsz Vector3f dim(mSize.x(), mSize.y(), 0); dim = trans * dim - trans.translation(); Renderer::pushClipRect(Vector2i((int)trans.translation().x(), (int)trans.translation().y()), @@ -177,13 +203,17 @@ void ComponentList::render(const Transform4x4f& parentTrans) for(unsigned int i = 0; i < mEntries.size(); i++) { auto& entry = mEntries.at(i); + drawAll = !mFocused || i != (unsigned int)mCursor; for(auto it = entry.data.elements.cbegin(); it != entry.data.elements.cend(); it++) { if(drawAll || it->invert_when_selected) { + if (entry.data.selectable) + it->component->setColor(textColor); + it->component->render(trans); - }else{ + } else { drawAfterCursor.push_back(it->component.get()); } } @@ -193,7 +223,7 @@ void ComponentList::render(const Transform4x4f& parentTrans) Renderer::setMatrix(trans); // draw selector bar - if(mFocused) + if (mFocused) { // inversion: src * (1 - dst) + dst * 0 = where src = 1 // need a function that goes roughly 0x777777 -> 0xFFFFFF @@ -201,18 +231,29 @@ void ComponentList::render(const Transform4x4f& parentTrans) // (1 - dst) + 0x77 const float selectedRowHeight = getRowHeight(mEntries.at(mCursor).data); - Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, 0xFFFFFFFF, 0xFFFFFFFF, false, Renderer::Blend::ONE_MINUS_DST_COLOR, Renderer::Blend::ZERO); - Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, 0x777777FF, 0x777777FF, false, Renderer::Blend::ONE, Renderer::Blend::ONE); - // hack to draw 2px dark on left/right of the bar - Renderer::drawRect(0.0f, mSelectorBarOffset, 2.0f, selectedRowHeight, 0x878787FF, 0x878787FF); - Renderer::drawRect(mSize.x() - 2.0f, mSelectorBarOffset, 2.0f, selectedRowHeight, 0x878787FF, 0x878787FF); + auto& entry = mEntries.at(mCursor); + + if (entry.data.selectable) + { + if ((selectorColor != bgColor) && ((selectorColor & 0xFF) != 0x00)) { + + Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, bgColor, Renderer::Blend::ZERO, Renderer::Blend::ONE_MINUS_SRC_COLOR); + Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, selectorColor, selectorGradientColor, selectorGradientHorz, Renderer::Blend::ONE, Renderer::Blend::ONE); + } + for (auto& element : entry.data.elements) + { + element.component->setColor(selectedColor); + drawAfterCursor.push_back(element.component.get()); + } + } + for(auto it = drawAfterCursor.cbegin(); it != drawAfterCursor.cend(); it++) (*it)->render(trans); // reset matrix if one of these components changed it - if(drawAfterCursor.size()) + if (drawAfterCursor.size()) Renderer::setMatrix(trans); } @@ -220,10 +261,10 @@ void ComponentList::render(const Transform4x4f& parentTrans) float y = 0; for(unsigned int i = 0; i < mEntries.size(); i++) { - Renderer::drawRect(0.0f, y, mSize.x(), 1.0f, 0xC6C7C6FF, 0xC6C7C6FF); + Renderer::drawRect(0.0f, y, mSize.x(), 1.0f, separatorColor); y += getRowHeight(mEntries.at(i).data); } - Renderer::drawRect(0.0f, y, mSize.x(), 1.0f, 0xC6C7C6FF, 0xC6C7C6FF); + Renderer::drawRect(0.0f, y, mSize.x(), 1.0f, separatorColor); Renderer::popClipRect(); } @@ -323,7 +364,7 @@ std::vector ComponentList::getHelpPrompts() } if(addMovePrompt) - prompts.push_back(HelpPrompt("up/down", "choose")); + prompts.push_back(HelpPrompt("up/down", _("CHOOSE"))); } return prompts; diff --git a/es-core/src/components/ComponentList.h b/es-core/src/components/ComponentList.h index c5c0f9c2e9..38f1f2b7a9 100644 --- a/es-core/src/components/ComponentList.h +++ b/es-core/src/components/ComponentList.h @@ -16,6 +16,12 @@ struct ComponentListElement struct ComponentListRow { + ComponentListRow() + { + selectable = true; + }; + + bool selectable; std::vector elements; // The input handler is called when the user enters any input while this row is highlighted (including up/down). @@ -49,6 +55,7 @@ class ComponentList : public IList ComponentList(Window* window); void addRow(const ComponentListRow& row, bool setCursorHere = false); + void addGroup(const std::string& label); void textInput(const char* text) override; bool input(InputConfig* config, Input input) override; @@ -72,6 +79,19 @@ class ComponentList : public IList protected: void onCursorChanged(const CursorState& state) override; + virtual int onBeforeScroll(int cursor, int direction) override + { + int looped = cursor; + while (cursor >= 0 && cursor < mEntries.size() && !mEntries[cursor].data.selectable) + { + cursor += direction; + if (cursor == looped || cursor < 0 || cursor >= mEntries.size()) + return mCursor; + } + + return cursor; + } + private: bool mFocused; diff --git a/es-core/src/components/DateTimeComponent.cpp b/es-core/src/components/DateTimeComponent.cpp index 2883b90561..8d78084e0a 100644 --- a/es-core/src/components/DateTimeComponent.cpp +++ b/es-core/src/components/DateTimeComponent.cpp @@ -50,7 +50,7 @@ std::string DateTimeComponent::getDisplayString() const if (mDisplayRelative) { //relative time if(mTime.getTime() == 0) - return "never"; + return _("never"); Utils::Time::DateTime now(Utils::Time::now()); Utils::Time::Duration dur(now.getTime() - mTime.getTime()); @@ -70,7 +70,7 @@ std::string DateTimeComponent::getDisplayString() const } if(mTime.getTime() == 0) - return "unknown"; + return _("unknown"); return Utils::Time::timeToString(mTime.getTime(), mFormat); } diff --git a/es-core/src/components/DateTimeEditComponent.cpp b/es-core/src/components/DateTimeEditComponent.cpp index 79bcb8e9c0..22ac420599 100644 --- a/es-core/src/components/DateTimeEditComponent.cpp +++ b/es-core/src/components/DateTimeEditComponent.cpp @@ -7,7 +7,13 @@ DateTimeEditComponent::DateTimeEditComponent(Window* window, DisplayMode dispMod mEditing(false), mEditIndex(0), mDisplayMode(dispMode), mRelativeUpdateAccumulator(0), mColor(0x777777FF), mFont(Font::get(FONT_SIZE_SMALL, FONT_PATH_LIGHT)), mUppercase(false), mAutoSize(true) { + auto menuTheme = ThemeData::getMenuTheme(); + + mFont = menuTheme->TextSmall.font; + mColor = menuTheme->Text.color; + updateTextCache(); + mAutoSize = true; } void DateTimeEditComponent::setDisplayMode(DisplayMode mode) @@ -199,13 +205,15 @@ std::string DateTimeEditComponent::getDisplayString(DisplayMode mode) const fmt = "%m/%d/%Y"; break; case DISP_DATE_TIME: + if(mTime.getTime() == 0) + return "unknown"; fmt = "%m/%d/%Y %H:%M:%S"; break; case DISP_RELATIVE_TO_NOW: { //relative time if(mTime.getTime() == 0) - return "never"; + return _("never"); Utils::Time::DateTime now(Utils::Time::now()); Utils::Time::Duration dur(now.getTime() - mTime.getTime()); @@ -227,7 +235,7 @@ std::string DateTimeEditComponent::getDisplayString(DisplayMode mode) const } if(mTime.getTime() == 0) - return "unknown"; + return _("unknown"); return Utils::Time::timeToString(mTime, fmt); } diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index 842c268b2b..694b843cae 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -3,129 +3,797 @@ #include "resources/TextureResource.h" #include "ThemeData.h" -GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBackground(window) -{ - mDefaultProperties.mSize = getDefaultTileSize(); - mDefaultProperties.mPadding = Vector2f(16.0f, 16.0f); - mDefaultProperties.mImageColor = 0xAAAAAABB; - mDefaultProperties.mBackgroundImage = ":/frame.png"; - mDefaultProperties.mBackgroundCornerSize = Vector2f(16 ,16); - mDefaultProperties.mBackgroundCenterColor = 0xAAAAEEFF; - mDefaultProperties.mBackgroundEdgeColor = 0xAAAAEEFF; - - mSelectedProperties.mSize = getSelectedTileSize(); - mSelectedProperties.mPadding = mDefaultProperties.mPadding; - mSelectedProperties.mImageColor = 0xFFFFFFFF; - mSelectedProperties.mBackgroundImage = mDefaultProperties.mBackgroundImage; - mSelectedProperties.mBackgroundCornerSize = mDefaultProperties.mBackgroundCornerSize; - mSelectedProperties.mBackgroundCenterColor = 0xFFFFFFFF; - mSelectedProperties.mBackgroundEdgeColor = 0xFFFFFFFF; - - mImage = std::make_shared(mWindow); +#include + +#include "animations/LambdaAnimation.h" +#include "ImageIO.h" + +#ifdef _RPI_ +#include "components/VideoPlayerComponent.h" +#endif +#include "components/VideoVlcComponent.h" +#include "utils/FileSystemUtil.h" + +#include "Settings.h" +#include "ImageGridComponent.h" + +#define VIDEODELAY 100 + +GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBackground(window), mLabel(window), mVideo(nullptr), mVideoPlaying(false), mShown(false) +{ + mSelectedZoomPercent = 1.0f; + mAnimPosition = Vector3f(0, 0); + mVideo = nullptr; + mMarquee = nullptr; + mFavorite = nullptr; + mImageOverlay = nullptr; + mIsDefaultImage = false; + + mLabelMerged = false; + + resetProperties(); + + mImage = new ImageComponent(mWindow); mImage->setOrigin(0.5f, 0.5f); - mBackground.setOrigin(0.5f, 0.5f); + mLabel.setDefaultZIndex(10); addChild(&mBackground); addChild(&(*mImage)); + addChild(&mLabel); setSelected(false); setVisible(true); } -void GridTileComponent::render(const Transform4x4f& parentTrans) +void GridTileComponent::resetProperties() +{ + mDefaultProperties.Size = getDefaultTileSize(); + mDefaultProperties.Padding = Vector4f(16.0f, 16.0f, 16.0f, 16.0f); + + mSelectedProperties.Size = getSelectedTileSize(); + mSelectedProperties.Padding = mDefaultProperties.Padding; + + mDefaultProperties.Label = mSelectedProperties.Label = GridTextProperties(); + mDefaultProperties.Image = mSelectedProperties.Image = GridImageProperties(); + mDefaultProperties.Marquee = mSelectedProperties.Marquee = GridImageProperties(); + mDefaultProperties.Favorite = mSelectedProperties.Favorite = GridImageProperties(); + mDefaultProperties.Background = mSelectedProperties.Background = GridNinePatchProperties(); + + mDefaultProperties.Background.centerColor = mDefaultProperties.Background.edgeColor = 0xAAAAEEFF; + mDefaultProperties.Image.color = mDefaultProperties.Image.colorEnd = 0xFFFFFFDD; + + mVideoPlayingProperties = mSelectedProperties; +} + +void GridTileComponent::forceSize(Vector2f size, float selectedZoom) +{ + mSize = size; + mDefaultProperties.Size = size; + mSelectedProperties.Size = size * selectedZoom; + mVideoPlayingProperties.Size = mSelectedProperties.Size; +} + +GridTileComponent::~GridTileComponent() { + if (mImage != nullptr) + delete mImage; + + if (mImageOverlay != nullptr) + delete mImageOverlay; + + if (mFavorite != nullptr) + delete mFavorite; + + if (mMarquee != nullptr) + delete mMarquee; + + if (mVideo != nullptr) + delete mVideo; + + mFavorite = nullptr; + mMarquee = nullptr; + mImage = nullptr; + mVideo = nullptr; + mImageOverlay = nullptr; +} + +std::shared_ptr GridTileComponent::getTexture(bool marquee) +{ + if (marquee && mMarquee != nullptr) + return mMarquee->getTexture(); + else if (!marquee && mImage != nullptr) + return mImage->getTexture(); + + return nullptr; +} + +void GridTileComponent::resize() +{ + auto currentProperties = getCurrentProperties(); + + Vector2f size = currentProperties.Size; + if (mSize != size) + setSize(size); + + bool isDefaultImage = mIsDefaultImage; // && (mCurrentPath == ":/folder.svg" || mCurrentPath == ":/cartridge.svg"); + + float height = (int) (size.y() * currentProperties.Label.size.y()); + float labelHeight = height; + + if (!currentProperties.Label.Visible || mLabelMerged || currentProperties.Label.size.x() == 0) + height = 0; + + float topPadding = currentProperties.Padding.y(); + float bottomPadding = std::max(currentProperties.Padding.w(), height); + + Vector2f imageOffset = Vector2f(currentProperties.Padding.x(), currentProperties.Padding.y()); + Vector2f imageSize(size.x() - currentProperties.Padding.x() - currentProperties.Padding.z(), size.y() - topPadding - bottomPadding); + + // Image + if (currentProperties.Image.Loaded) + { + if (isDefaultImage) + { + imageOffset.x() += imageSize.x() * 0.05; + imageSize.x() *= 0.90; + } + + currentProperties.Image.updateImageComponent(mImage, imageOffset, imageSize, false); + + if (mImage != nullptr && isDefaultImage) + mImage->setRoundCorners(0); + + if (mImage != nullptr && currentProperties.Image.sizeMode != "maxSize" && isDefaultImage) + mImage->setMaxSize(imageSize.x(), imageSize.y()); + } + else if (mImage != nullptr) + { + // Retrocompatibility : imagegrid.image is not defined + mImage->setOrigin(0.5f, 0.5f); + mImage->setPosition(imageOffset.x() + imageSize.x() / 2.0f, imageOffset.y() + imageSize.y() / 2.0f); + mImage->setColorShift(currentProperties.Image.color); + mImage->setMirroring(currentProperties.Image.reflexion); + + if (currentProperties.Image.sizeMode == "minSize" && !isDefaultImage) + mImage->setMinSize(imageSize.x(), imageSize.y()); + else if (currentProperties.Image.sizeMode == "size") + mImage->setSize(imageSize.x(), imageSize.x()); + else + mImage->setMaxSize(imageSize.x(), imageSize.y()); + +// imageOffset = Vector2f::Zero(); + } + + // Recompute final image size if necessary + if (mImage != nullptr && currentProperties.Image.sizeMode == "maxSize") + { + auto origin = mImage->getOrigin(); + auto pos = mImage->getPosition(); + imageSize = mImage->getSize(); + imageOffset = Vector2f(pos.x() - imageSize.x() * origin.x(), pos.y() - imageSize.y() * origin.y()); + } + + // Text + mLabel.setVisible(!mLabel.getText().empty() && (currentProperties.Label.Visible || mIsDefaultImage)); + + if (currentProperties.Label.Visible) + { + auto szRef = mLabelMerged ? mSize - imageOffset : mSize; + + currentProperties.Label.updateTextComponent(&mLabel, szRef); + + // Automatic layout for not merged labels + if (currentProperties.Label.pos.x() < 0) + { + if (currentProperties.Padding.x() == 0 && !mLabelMerged) + { + mLabel.setPosition(mImage->getPosition().x() - mImage->getSize().x() / 2, mImage->getSize().y()); + mLabel.setSize(mImage->getSize().x(), labelHeight); + } + else + { + mLabel.setPosition(0, szRef.y() - labelHeight); + mLabel.setSize(size.x(), labelHeight); + } + } + } + else if (mIsDefaultImage) + { + mLabel.setColor(0xFFFFFFFF); + mLabel.setGlowColor(0x00000010); + mLabel.setGlowSize(2); + mLabel.setOpacity(255); + mLabel.setPosition(mSize.x() * 0.1, mSize.y() * 0.2); + mLabel.setSize(mSize.x() - mSize.x() * 0.2, mSize.y() - mSize.y() * 0.3); + } + + // Other controls ( Favorite / Marquee / Overlay ) + if (currentProperties.Favorite.Loaded) + currentProperties.Favorite.updateImageComponent(mFavorite, imageOffset, imageSize, false); + + if (currentProperties.Marquee.Loaded) + currentProperties.Marquee.updateImageComponent(mMarquee, imageOffset, imageSize, true); + + if (currentProperties.ImageOverlay.Loaded) + currentProperties.ImageOverlay.updateImageComponent(mImageOverlay, imageOffset, imageSize, false); + + // Video + if (mVideo != nullptr && mVideo->isPlaying()) + { + if (currentProperties.Image.sizeMode != "size") + { + mVideo->setOrigin(0.5, 0.5); + mVideo->setPosition(imageOffset.x() + imageSize.x() / 2.0f, imageOffset.y() + imageSize.y() / 2.0f); + mVideo->setMinSize(imageSize.x(), imageSize.y()); + + if (mImage != nullptr) + mVideo->setRoundCorners(mImage->getRoundCorners()); + } + else + { + mVideo->setOrigin(0.5f, 0.5f); + mVideo->setPosition(size.x() / 2.0f, (size.y() - height) / 2.0f); + + if (currentProperties.Image.sizeMode == "size") + mVideo->setSize(imageSize.x(), size.y() - topPadding - bottomPadding); + else + mVideo->setMaxSize(imageSize.x(), size.y() - topPadding - bottomPadding); + } + } + + // Background when SelectionMode == "image" + Vector3f bkPosition = Vector3f::Zero(); + Vector2f bkSize = size; + + if (mImage != NULL && currentProperties.SelectionMode == "image" && mImage->getSize() != Vector2f(0, 0)) + { + if (currentProperties.Image.sizeMode == "minSize") + { + if (!mLabelMerged && currentProperties.Label.Visible) + bkSize = Vector2f(size.x(), size.y() - bottomPadding + topPadding); + } + else + { + bkPosition = Vector3f(imageOffset.x() - mSelectedProperties.Padding.x(), imageOffset.y() - mSelectedProperties.Padding.y(), 0); + bkSize = Vector2f(imageSize.x() + 2 * mSelectedProperties.Padding.x(), imageSize.y() + 2 * mSelectedProperties.Padding.y()); + } + } + + // Background when animating + if (mSelectedZoomPercent != 1.0f && mAnimPosition.x() != 0 && mAnimPosition.y() != 0 && mSelected) + { + float x = mPosition.x() + bkPosition.x(); + float y = mPosition.y() + bkPosition.y(); + + x = mAnimPosition.x() * (1.0 - mSelectedZoomPercent) + x * mSelectedZoomPercent; + y = mAnimPosition.y() * (1.0 - mSelectedZoomPercent) + y * mSelectedZoomPercent; + + bkPosition = Vector3f(x - mPosition.x(), y - mPosition.y(), 0); + } + + mBackground.setPosition(bkPosition); + mBackground.setSize(bkSize); + + currentProperties.Background.updateNinePatchComponent(&mBackground); + /* + mBackground.setCornerSize(currentProperties.Background.cornerSize); + mBackground.setCenterColor(currentProperties.Background.centerColor); + mBackground.setEdgeColor(currentProperties.Background.edgeColor); + mBackground.setImagePath(currentProperties.Background.path); + */ + if (mSelected && mAnimPosition == Vector3f(0, 0, 0) && mSelectedZoomPercent != 1.0) + mBackground.setOpacity(mSelectedZoomPercent * 255); + else + mBackground.setOpacity(255); +} + +void GridTileComponent::update(int deltaTime) +{ + GuiComponent::update(deltaTime); + + if (mVideo != nullptr && mVideo->isPlaying() && mVideo->isFading()) + resize(); +} + +void GridTileComponent::renderBackground(const Transform4x4f& parentTrans) +{ + if (!mVisible) + return; + Transform4x4f trans = getTransform() * parentTrans; + mBackground.render(trans); +} - if (mVisible) - renderChildren(trans); +void GridTileComponent::renderContent(const Transform4x4f& parentTrans) +{ + if (!mVisible) + return; + + Transform4x4f trans = getTransform() * parentTrans; + + Vector2f clipPos(trans.translation().x(), trans.translation().y()); + if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) + return; + + auto currentProperties = getCurrentProperties(false); + + float padding = currentProperties.Padding.x(); + float topPadding = currentProperties.Padding.y(); + float bottomPadding = topPadding; + + if (currentProperties.Label.Visible && !mLabelMerged) + bottomPadding = std::max((int)topPadding, (int)(mSize.y() * currentProperties.Label.size.y())); + + Vector2i pos((int)Math::round(trans.translation()[0] + padding), (int)Math::round(trans.translation()[1] + topPadding)); + Vector2i size((int)Math::round(mSize.x() - 2 * padding), (int)Math::round(mSize.y() - topPadding - bottomPadding)); + + bool isDefaultImage = mIsDefaultImage; // && (mCurrentPath == ":/folder.svg" || mCurrentPath == ":/cartridge.svg"); + bool isMinSize = !isDefaultImage && currentProperties.Image.sizeMode == "minSize"; + + if (isMinSize) + Renderer::pushClipRect(pos, size); + + if (mImage != NULL) + { + if (!isMinSize || !mSelected || mVideo == nullptr || !(mVideo->isPlaying() && !mVideo->isFading())) + mImage->render(trans); + } + + if (mSelected && !mVideoPath.empty() && mVideo != nullptr) + mVideo->render(trans); + + if (!mLabelMerged && isMinSize) + Renderer::popClipRect(); + + std::vector zOrdered; + + if (mMarquee != nullptr && mMarquee->hasImage()) + zOrdered.push_back(mMarquee); + else + zOrdered.push_back(&mLabel); + + if (mFavorite != nullptr && mFavorite->hasImage() && mFavorite->isVisible()) + zOrdered.push_back(mFavorite); + + if (mImageOverlay != nullptr && mImageOverlay->hasImage() && mImageOverlay->isVisible()) + zOrdered.push_back(mImageOverlay); + + std::stable_sort(zOrdered.begin(), zOrdered.end(), [](GuiComponent* a, GuiComponent* b) { return b->getZIndex() > a->getZIndex(); }); + + for (auto comp : zOrdered) + comp->render(trans); + + if (mLabelMerged && isMinSize) + Renderer::popClipRect(); } -// Update all the tile properties to the new status (selected or default) -void GridTileComponent::update() +void GridTileComponent::render(const Transform4x4f& parentTrans) { - const GridTileProperties& currentProperties = getCurrentProperties(); + if (!mVisible) + return; - mBackground.setImagePath(currentProperties.mBackgroundImage); + renderBackground(parentTrans); + renderContent(parentTrans); +} - mImage->setColorShift(currentProperties.mImageColor); - mBackground.setCenterColor(currentProperties.mBackgroundCenterColor); - mBackground.setEdgeColor(currentProperties.mBackgroundEdgeColor); +void GridTileComponent::createMarquee() +{ + if (mMarquee != nullptr) + return; - resize(); + mMarquee = new ImageComponent(mWindow); + mMarquee->setOrigin(0.5f, 0.5f); + mMarquee->setDefaultZIndex(20); + addChild(mMarquee); +} + +void GridTileComponent::createFavorite() +{ + if (mFavorite != nullptr) + return; + + mFavorite = new ImageComponent(mWindow); + mFavorite->setOrigin(0.5f, 0.5f); + mFavorite->setDefaultZIndex(15); + mFavorite->setVisible(false); + + addChild(mFavorite); } -void GridTileComponent::applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& /*element*/, unsigned int /*properties*/) +void GridTileComponent::createImageOverlay() { + if (mImageOverlay != nullptr) + return; + + mImageOverlay = new ImageComponent(mWindow); + mImageOverlay->setOrigin(0.5f, 0.5f); + mImageOverlay->setDefaultZIndex(25); + mImageOverlay->setVisible(false); + + addChild(mImageOverlay); +} + +void GridTileComponent::createVideo() +{ + if (mVideo != nullptr) + return; + + mVideo = new VideoVlcComponent(mWindow, ""); + + // video + mVideo->setOrigin(0.5f, 0.5f); + mVideo->setStartDelay(VIDEODELAY); + mVideo->setDefaultZIndex(11); + addChild(mVideo); +} + +void GridTileComponent::applyThemeToProperties(const ThemeData::ThemeElement* elem, GridTileProperties& properties) +{ + if (elem == nullptr) + return; + Vector2f screen = Vector2f((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); + if (elem->has("size")) + properties.Size = elem->get("size") * screen; + + if (elem->has("padding")) + properties.Padding = elem->get("padding"); + + if (elem && elem->has("selectionMode")) + properties.SelectionMode = elem->get("selectionMode"); + + // Retrocompatibility for Background properties + if (elem->has("backgroundImage")) + properties.Background.path = elem->get("backgroundImage"); + + if (elem->has("backgroundCornerSize")) + properties.Background.cornerSize = elem->get("backgroundCornerSize"); + + if (elem->has("backgroundColor")) + { + properties.Background.centerColor = elem->get("backgroundColor"); + properties.Background.edgeColor = elem->get("backgroundColor"); + } + + if (elem->has("backgroundCenterColor")) + properties.Background.centerColor = elem->get("backgroundCenterColor"); + + if (elem->has("backgroundEdgeColor")) + properties.Background.edgeColor = elem->get("backgroundEdgeColor"); + + // Retrocompatibility for Image properties + if (elem && elem->has("reflexion")) + properties.Image.reflexion = elem->get("reflexion"); + + if (elem->has("imageColor")) + properties.Image.color = properties.Image.colorEnd = elem->get("imageColor"); + + if (elem && elem->has("imageSizeMode")) + properties.Image.sizeMode = elem->get("imageSizeMode"); +} + +bool GridImageProperties::applyTheme(const ThemeData::ThemeElement* elem) +{ + if (!elem) + return false; + + Loaded = true; + Visible = true; + + if (elem && elem->has("visible")) + Visible = elem->get("visible"); + + if (elem && elem->has("origin")) + origin = elem->get("origin"); + + if (elem && elem->has("pos")) + pos = elem->get("pos"); + + if (elem && elem->has("size")) + { + sizeMode = "size"; + size = elem->get("size"); + } + else if (elem && elem->has("minSize")) + { + sizeMode = "minSize"; + size = elem->get("minSize"); + } + else if (elem && elem->has("maxSize")) + { + sizeMode = "maxSize"; + size = elem->get("maxSize"); + } + + if (elem && elem->has("color")) + color = colorEnd = elem->get("color"); + + if (elem && elem->has("colorEnd")) + colorEnd = elem->get("colorEnd"); + + if (elem && elem->has("reflexion")) + reflexion = elem->get("reflexion"); + + if (elem && elem->has("roundCorners")) + roundCorners = elem->get("roundCorners"); + + return true; +} + +bool GridTextProperties::applyTheme(const ThemeData::ThemeElement* elem) +{ + if (!elem) + { + Visible = false; + return false; + } + + Loaded = true; + Visible = true; + + if (elem && elem->has("visible")) + Visible = elem->get("visible"); + + if (elem && elem->has("pos")) + pos = elem->get("pos"); + + if (elem && elem->has("size")) + { + size = elem->get("size"); + if (size.y() == 0) + Visible = false; + } + + if (elem && elem->has("color")) + color = elem->get("color"); + + if (elem && elem->has("backgroundColor")) + backColor = elem->get("backgroundColor"); + + if (elem && elem->has("glowColor")) + glowColor = elem->get("glowColor"); + + if (elem && elem->has("glowSize")) + glowSize = elem->get("glowSize"); + + if (elem && elem->has("fontSize")) + fontSize = elem->get("fontSize"); + + if (elem && elem->has("fontPath")) + fontPath = elem->get("fontPath"); + + if (elem->has("singleLineScroll")) + autoScroll = elem->get("singleLineScroll"); + + return true; +} + +bool GridNinePatchProperties::applyTheme(const ThemeData::ThemeElement* elem) +{ + if (!elem) + { + Visible = false; + return false; + } + + Loaded = true; + Visible = true; + + if (elem && elem->has("visible")) + Visible = elem->get("visible"); + /* + if (elem && elem->has("pos")) + pos = elem->get("pos"); + + if (elem && elem->has("size")) + size = elem->get("size"); + */ + if (elem && elem->has("color")) + centerColor = edgeColor = elem->get("color"); + + if (elem && elem->has("centerColor")) + centerColor = elem->get("centerColor"); + + if (elem && elem->has("edgeColor")) + edgeColor = elem->get("edgeColor"); + + if (elem && elem->has("cornerSize")) + cornerSize = elem->get("cornerSize"); + + if (elem && elem->has("path")) + path = elem->get("path"); + + if (elem && elem->has("animateColor")) + animateColor = elem->get("animateColor"); + + if (elem && elem->has("animateColorTime")) + animateTime = elem->get("animateColorTime"); + + return true; +} + +void GridTileComponent::applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) +{ + if (mSize == Vector2f::Zero()) + setSize(getDefaultTileSize()); + + resetProperties(); + + const ThemeData::ThemeElement* grid = theme->getElement(view, "gamegrid", "imagegrid"); + if (grid && grid->has("showVideoAtDelay")) + { + createVideo(); + + /*if (theme->getElement(view, "gridtile", "video")) + mVideo->applyTheme(theme, view, "gridtile", ThemeFlags::ALL ^ (ThemeFlags::PATH)); + else */if (theme->getElement(view, "gridtile.video", "video")) + mVideo->applyTheme(theme, view, "gridtile.video", ThemeFlags::ALL ^ (ThemeFlags::PATH)); + } + else if (mVideo != nullptr) + { + removeChild(mVideo); + delete mVideo; + mVideo = nullptr; + } + // Apply theme to the default gridtile const ThemeData::ThemeElement* elem = theme->getElement(view, "default", "gridtile"); if (elem) { - if (elem->has("size")) - mDefaultProperties.mSize = elem->get("size") * screen; + applyThemeToProperties(elem, mDefaultProperties); + applyThemeToProperties(elem, mSelectedProperties); + } - if (elem->has("padding")) - mDefaultProperties.mPadding = elem->get("padding"); + // Apply theme to the selected gridtile + elem = theme->getElement(view, "selected", "gridtile"); + if (elem) + applyThemeToProperties(elem, mSelectedProperties); + - if (elem->has("imageColor")) - mDefaultProperties.mImageColor = elem->get("imageColor"); + // Apply theme to the element + elem = theme->getElement(view, "gridtile.image", "image"); + if (elem) + { + mImage->applyTheme(theme, view, "gridtile.image", ThemeFlags::ALL ^ (ThemeFlags::PATH)); - if (elem->has("backgroundImage")) - mDefaultProperties.mBackgroundImage = elem->get("backgroundImage"); + mDefaultProperties.Image.applyTheme(elem); + mSelectedProperties.Image.applyTheme(elem); + } - if (elem->has("backgroundCornerSize")) - mDefaultProperties.mBackgroundCornerSize = elem->get("backgroundCornerSize"); + // Apply theme to the element + elem = theme->getElement(view, "gridtile.image:selected", "image"); + if (elem) + mSelectedProperties.Image.applyTheme(elem); + - if (elem->has("backgroundColor")) - { - mDefaultProperties.mBackgroundCenterColor = elem->get("backgroundColor"); - mDefaultProperties.mBackgroundEdgeColor = elem->get("backgroundColor"); - } + // Apply theme to the element + elem = theme->getElement(view, "gridtile.marquee", "image"); + if (elem) + { + createMarquee(); + mMarquee->applyTheme(theme, view, "gridtile.marquee", ThemeFlags::ALL ^ (ThemeFlags::PATH)); - if (elem->has("backgroundCenterColor")) - mDefaultProperties.mBackgroundCenterColor = elem->get("backgroundCenterColor"); + mDefaultProperties.Marquee.applyTheme(elem); + mSelectedProperties.Marquee = mDefaultProperties.Marquee; - if (elem->has("backgroundEdgeColor")) - mDefaultProperties.mBackgroundEdgeColor = elem->get("backgroundEdgeColor"); + // Apply theme to the element + elem = theme->getElement(view, "gridtile.marquee:selected", "image"); + if (elem) + mSelectedProperties.Marquee.applyTheme(elem); + } + else if (mMarquee != nullptr) + { + removeChild(mMarquee); + delete mMarquee; + mMarquee = nullptr; } - // Apply theme to the selected gridtile - // NOTE that some of the default gridtile properties influence on the selected gridtile properties - // See THEMES.md for more informations - elem = theme->getElement(view, "selected", "gridtile"); - mSelectedProperties.mSize = elem && elem->has("size") ? - elem->get("size") * screen : - getSelectedTileSize(); + // Apply theme to the element + elem = theme->getElement(view, "gridtile.favorite", "image"); + if (elem) + { + createFavorite(); + mFavorite->applyTheme(theme, view, "gridtile.favorite", ThemeFlags::ALL); - mSelectedProperties.mPadding = elem && elem->has("padding") ? - elem->get("padding") : - mDefaultProperties.mPadding; + mDefaultProperties.Favorite.sizeMode = "size"; + mDefaultProperties.Favorite.applyTheme(elem); + mSelectedProperties.Favorite = mDefaultProperties.Favorite; - if (elem && elem->has("imageColor")) - mSelectedProperties.mImageColor = elem->get("imageColor"); + // Apply theme to the element + elem = theme->getElement(view, "gridtile.favorite:selected", "image"); + if (elem) + mSelectedProperties.Favorite.applyTheme(elem); + } + else if (mFavorite != nullptr) + { + removeChild(mFavorite); + delete mFavorite; + mFavorite = nullptr; + } - mSelectedProperties.mBackgroundImage = elem && elem->has("backgroundImage") ? - elem->get("backgroundImage") : - mDefaultProperties.mBackgroundImage; - mSelectedProperties.mBackgroundCornerSize = elem && elem->has("backgroundCornerSize") ? - elem->get("backgroundCornerSize") : - mDefaultProperties.mBackgroundCornerSize; + // Apply theme to the element + elem = theme->getElement(view, "gridtile.overlay", "image"); + if (elem) + { + createImageOverlay(); + mImageOverlay->applyTheme(theme, view, "gridtile.overlay", ThemeFlags::ALL); - if (elem && elem->has("backgroundColor")) + mDefaultProperties.ImageOverlay.sizeMode = "size"; + mDefaultProperties.ImageOverlay.applyTheme(elem); + mSelectedProperties.ImageOverlay = mDefaultProperties.ImageOverlay; + + // Apply theme to the element + elem = theme->getElement(view, "gridtile.overlay:selected", "image"); + if (elem) + mSelectedProperties.ImageOverlay.applyTheme(elem); + } + else if (mImageOverlay != nullptr) { - mSelectedProperties.mBackgroundCenterColor = elem->get("backgroundColor"); - mSelectedProperties.mBackgroundEdgeColor = elem->get("backgroundColor"); + removeChild(mImageOverlay); + delete mImageOverlay; + mImageOverlay = nullptr; } + + + // Apply theme to the element + elem = theme->getElement(view, "gridtile", "text"); + if (elem == nullptr) // Apply theme to the element + elem = theme->getElement(view, "gridtile.text", "text"); + + if (elem != NULL) + { + mLabel.applyTheme(theme, view, element, properties); - if (elem && elem->has("backgroundCenterColor")) - mSelectedProperties.mBackgroundCenterColor = elem->get("backgroundCenterColor"); + mDefaultProperties.Label.applyTheme(elem); + mSelectedProperties.Label.applyTheme(elem); - if (elem && elem->has("backgroundEdgeColor")) - mSelectedProperties.mBackgroundEdgeColor = elem->get("backgroundEdgeColor"); + bool hasVisible = elem->has("visible"); + mLabelMerged = elem->has("pos"); + if (!mLabelMerged && elem->has("size")) + mLabelMerged = mDefaultProperties.Label.size.x() == 0; + + // Apply theme to the element + elem = theme->getElement(view, "gridtile_selected", "text"); + if (elem == nullptr) + elem = theme->getElement(view, "gridtile:selected", "text"); + if (elem == nullptr) // Apply theme to the element + elem = theme->getElement(view, "gridtile.text:selected", "text"); + + if (elem) + { + mSelectedProperties.Label.applyTheme(elem); + if (hasVisible && !elem->has("visible")) + mSelectedProperties.Label.Visible = mDefaultProperties.Label.Visible; + } + } + + // Apply theme to the element + elem = theme->getElement(view, "gridtile.background", "ninepatch"); + if (elem != NULL) + { + mBackground.applyTheme(theme, view, element, properties); + mDefaultProperties.Background.applyTheme(elem); + mSelectedProperties.Background.applyTheme(elem); + } + + // Apply theme to the element + elem = theme->getElement(view, "gridtile.background:selected", "ninepatch"); + if (elem) + mSelectedProperties.Background.applyTheme(elem); + + mVideoPlayingProperties = mSelectedProperties; + + if (!mVideoPlayingProperties.Label.applyTheme(theme->getElement(view, "gridtile:videoplaying", "text"))) + mVideoPlayingProperties.Label.applyTheme(theme->getElement(view, "gridtile.text:videoplaying", "text")); + + mVideoPlayingProperties.Image.applyTheme(theme->getElement(view, "gridtile.image:videoplaying", "image")); + mVideoPlayingProperties.Marquee.applyTheme(theme->getElement(view, "gridtile.marquee:videoplaying", "image")); + mVideoPlayingProperties.Favorite.applyTheme(theme->getElement(view, "gridtile.favorite:selected", "image")); + mVideoPlayingProperties.ImageOverlay.applyTheme(theme->getElement(view, "gridtile.overlay:videoplaying", "image")); } // Made this a static function because the ImageGridComponent need to know the default tile size @@ -133,13 +801,12 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons Vector2f GridTileComponent::getDefaultTileSize() { Vector2f screen = Vector2f((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); - return screen * 0.22f; } Vector2f GridTileComponent::getSelectedTileSize() const { - return mDefaultProperties.mSize * 1.2f; + return mDefaultProperties.Size * 1.2f; } bool GridTileComponent::isSelected() const @@ -147,25 +814,211 @@ bool GridTileComponent::isSelected() const return mSelected; } -void GridTileComponent::setImage(const std::string& path) +void GridTileComponent::setImage(const std::string& path, bool isDefaultImage) +{ + if (path == ":/folder.svg" || path == ":/cartridge.svg") + mIsDefaultImage = true; + else + mIsDefaultImage = isDefaultImage; + + if (mCurrentPath == path) + return; + + mCurrentPath = path; + + if (mSelectedProperties.Size.x() > mSize.x()) + mImage->setImage(path, false, MaxSizeInfo(mSelectedProperties.Size, mSelectedProperties.Image.sizeMode != "maxSize")); + else + mImage->setImage(path, false, MaxSizeInfo(mSize, mSelectedProperties.Image.sizeMode != "maxSize")); + + resize(); +} + +void GridTileComponent::setMarquee(const std::string& path) +{ + if (mMarquee == nullptr) + return; + + if (mCurrentMarquee == path) + return; + + mCurrentMarquee = path; + + if (mSelectedProperties.Size.x() > mSize.x()) + mMarquee->setImage(path, false, MaxSizeInfo(mSelectedProperties.Size)); + else + mMarquee->setImage(path, false, MaxSizeInfo(mSize)); + + resize(); +} + +void GridTileComponent::setFavorite(bool favorite) { - mImage->setImage(path); + if (mFavorite == nullptr) + return; - // Resize now to prevent flickering images when scrolling + mFavorite->setVisible(favorite); resize(); } -void GridTileComponent::setImage(const std::shared_ptr& texture) +void GridTileComponent::resetImages() +{ + setLabel(""); + setImage(""); + setMarquee(""); + stopVideo(); +} + +void GridTileComponent::setLabel(std::string name) { - mImage->setImage(texture); + if (mLabel.getText() == name) + return; - // Resize now to prevent flickering images when scrolling + mLabel.setText(name); resize(); } -void GridTileComponent::setSelected(bool selected) +void GridTileComponent::setVideo(const std::string& path, float defaultDelay) { + if (mVideoPath == path) + return; + + mVideoPath = path; + + if (mVideo != nullptr) + { + if (defaultDelay >= 0.0) + mVideo->setStartDelay(defaultDelay); + + if (mVideoPath.empty()) + stopVideo(); + } + + resize(); +} + +void GridTileComponent::onShow() +{ + GuiComponent::onShow(); + mShown = true; + resize(); +} + +void GridTileComponent::onHide() +{ + GuiComponent::onHide(); + mShown = false; +} + +void GridTileComponent::startVideo() +{ + if (mVideo != nullptr) + { + // Inform video component about size before staring in order to be able to use OptimizeVideo parameter + if (mSelectedProperties.Image.sizeMode == "minSize") + mVideo->setMinSize(mSelectedProperties.Size); + else + mVideo->setResize(mSelectedProperties.Size); + + mVideo->setVideo(mVideoPath); + } +} + +void GridTileComponent::stopVideo() +{ + if (mVideo != nullptr) + mVideo->setVideo(""); +} + +void GridTileComponent::setSelected(bool selected, bool allowAnimation, Vector3f* pPosition, bool force) +{ + if (!mShown || !ALLOWANIMATIONS) + allowAnimation = false; + + if (mSelected == selected && !force) + { + if (mSelected) + startVideo(); + + return; + } + mSelected = selected; + + if (!mSelected) + stopVideo(); + + if (selected) + { + if (pPosition == NULL || !allowAnimation) + { + cancelAnimation(3); + + this->setSelectedZoom(1); + mAnimPosition = Vector3f(0, 0, 0); + startVideo(); + + resize(); + } + else + { + if (pPosition == NULL) + mAnimPosition = Vector3f(0, 0, 0); + else + mAnimPosition = Vector3f(pPosition->x(), pPosition->y(), pPosition->z()); + + auto func = [this](float t) + { + t -= 1; // cubic ease out + float pct = Math::lerp(0, 1, t*t*t + 1); + + this->setSelectedZoom(pct); + }; + + cancelAnimation(3); + setAnimation(new LambdaAnimation(func, 250), 0, [this] { + this->setSelectedZoom(1); + mAnimPosition = Vector3f(0, 0, 0); + startVideo(); + }, false, 3); + } + } + else // if (!selected) + { + if (!allowAnimation) + { + cancelAnimation(3); + this->setSelectedZoom(0); + stopVideo(); + resize(); + } + else + { + this->setSelectedZoom(1); + stopVideo(); + + auto func = [this](float t) + { + t -= 1; // cubic ease out + float pct = Math::lerp(0, 1, t*t*t + 1); + this->setSelectedZoom(1.0 - pct); + }; + + cancelAnimation(3); + setAnimation(new LambdaAnimation(func, 250), 0, [this] { + this->setSelectedZoom(0); + }, false, 3); + } + } +} + +void GridTileComponent::setSelectedZoom(float percent) +{ + if (mSelectedZoomPercent == percent) + return; + + mSelectedZoomPercent = percent; + resize(); } void GridTileComponent::setVisible(bool visible) @@ -173,16 +1026,145 @@ void GridTileComponent::setVisible(bool visible) mVisible = visible; } -void GridTileComponent::resize() +Vector3f GridTileComponent::getBackgroundPosition() +{ + return Vector3f(mBackground.getPosition().x() + mPosition.x(), mBackground.getPosition().y() + mPosition.y(), 0); +} + +static Vector2f mixVectors(const Vector2f& def, const Vector2f& sel, float percent) { - const GridTileProperties& currentProperties = getCurrentProperties(); + if (def == sel || percent == 0) + return def; + + if (percent == 1) + return sel; - mImage->setMaxSize(currentProperties.mSize - currentProperties.mPadding * 2); - mBackground.setCornerSize(currentProperties.mBackgroundCornerSize); - mBackground.fitTo(currentProperties.mSize - mBackground.getCornerSize() * 2); + float x = def.x() * (1.0 - percent) + sel.x() * percent; + float y = def.y() * (1.0 - percent) + sel.y() * percent; + return Vector2f(x, y); } -const GridTileProperties& GridTileComponent::getCurrentProperties() const +static Vector4f mixVectors(const Vector4f& def, const Vector4f& sel, float percent) { - return mSelected ? mSelectedProperties : mDefaultProperties; -} \ No newline at end of file + if (def == sel || percent == 0) + return def; + + if (percent == 1) + return sel; + + float x = def.x() * (1.0 - percent) + sel.x() * percent; + float y = def.y() * (1.0 - percent) + sel.y() * percent; + float z = def.z() * (1.0 - percent) + sel.z() * percent; + float w = def.w() * (1.0 - percent) + sel.w() * percent; + return Vector4f(x, y, z, w); +} + +static unsigned int mixUnsigned(const unsigned int def, const unsigned int sel, float percent) +{ + if (def == sel || percent == 0) + return def; + + if (percent == 1) + return sel; + + return def * (1.0 - percent) + sel * percent; +} + +static float mixFloat(const float def, const float sel, float percent) +{ + if (def == sel || percent == 0) + return def; + + if (percent == 1) + return sel; + + return def * (1.0 - percent) + sel * percent; +} + +void GridImageProperties::mixProperties(GridImageProperties& def, GridImageProperties& sel, float percent) +{ + if (!def.Loaded) + return; + + using namespace Renderer; + + pos = mixVectors(def.pos, sel.pos, percent); + size = mixVectors(def.size, sel.size, percent); + origin = mixVectors(def.origin, sel.origin, percent); + color = mixColors(def.color, sel.color, percent); + colorEnd = mixColors(def.colorEnd, sel.colorEnd, percent); + reflexion = mixVectors(def.reflexion, sel.reflexion, percent); + roundCorners = mixFloat(def.roundCorners, sel.roundCorners, percent); +} + +void GridTextProperties::mixProperties(GridTextProperties& def, GridTextProperties& sel, float percent) +{ + if (!def.Loaded) + return; + + using namespace Renderer; + + pos = mixVectors(def.pos, sel.pos, percent); + size = mixVectors(def.size, sel.size, percent); + color = mixColors(def.color, sel.color, percent); + backColor = mixColors(def.backColor, sel.backColor, percent); + glowColor = mixColors(def.glowColor, sel.glowColor, percent); + glowSize = mixFloat(def.glowSize, sel.glowSize, percent); + fontSize = mixFloat(def.fontSize, sel.fontSize, percent); +} + +GridTileProperties GridTileComponent::getCurrentProperties(bool mixValues) +{ + GridTileProperties prop = mSelected ? mSelectedProperties : mDefaultProperties; + + if (mSelectedZoomPercent == 0.0f || mSelectedZoomPercent == 1.0f) + if (!mSelected || (mVideo != nullptr && !mVideo->isPlaying())) + return prop; + + if (mixValues) + { + GridTileProperties* from = &mDefaultProperties; + GridTileProperties* to = &mSelectedProperties; + float pc = mSelectedZoomPercent; + + if (mSelected && mVideo != nullptr && mVideo->isPlaying()) + { + if (!mVideo->isFading()) + return mVideoPlayingProperties; + + from = &mSelectedProperties; + to = &mVideoPlayingProperties; + + float t = mVideo->getFade() - 1; // cubic ease in + pc = Math::lerp(0, 1, t*t*t + 1); + } + + prop.Size = mixVectors(from->Size, to->Size, pc); + prop.Padding = mixVectors(from->Padding, to->Padding, pc); + + prop.Label.mixProperties(from->Label, to->Label, pc); + prop.Image.mixProperties(from->Image, to->Image, pc); + prop.Marquee.mixProperties(from->Marquee, to->Marquee, pc); + + prop.Favorite.mixProperties(from->Favorite, to->Favorite, pc); + prop.ImageOverlay.mixProperties(from->ImageOverlay, to->ImageOverlay, pc); + } + + return prop; +} + +void GridTileComponent::onScreenSaverActivate() +{ + GuiComponent::onScreenSaverActivate(); + + if (mVideo) + mVideo->onScreenSaverActivate(); +} + +void GridTileComponent::onScreenSaverDeactivate() +{ + GuiComponent::onScreenSaverDeactivate(); + + if (mVideo) + mVideo->onScreenSaverDeactivate(); +} diff --git a/es-core/src/components/GridTileComponent.h b/es-core/src/components/GridTileComponent.h index c191ce94ab..41549910ec 100644 --- a/es-core/src/components/GridTileComponent.h +++ b/es-core/src/components/GridTileComponent.h @@ -4,25 +4,190 @@ #include "NinePatchComponent.h" #include "ImageComponent.h" +#include "TextComponent.h" +#include "ThemeData.h" + +class VideoComponent; + +struct GridImageProperties +{ +public: + GridImageProperties() + { + Loaded = false; + Visible = false; + + reflexion = Vector2f::Zero(); + pos = Vector2f(0.5f, 0.5f); + size = Vector2f(1.0f, 1.0f); + origin = Vector2f(0.5f, 0.5f); + color = colorEnd = 0xFFFFFFFF; + sizeMode = "maxSize"; + roundCorners = 0; + } + + void mixProperties(GridImageProperties& def, GridImageProperties& sel, float percent); + bool applyTheme(const ThemeData::ThemeElement* elem); + + void updateImageComponent(ImageComponent* image, Vector2f offsetPos, Vector2f parentSize, bool disableSize = false) + { + if (image == nullptr) + return; + + image->setPosition(offsetPos.x() + pos.x() * parentSize.x(), offsetPos.y() + pos.y() * parentSize.y()); + + if (!disableSize && sizeMode == "size") + image->setSize(size.x() * parentSize.x(), size.y() * parentSize.y()); + else if (sizeMode == "minSize") + image->setMinSize(size.x() * parentSize.x(), size.y() * parentSize.y()); + else + image->setMaxSize(size.x() * parentSize.x(), size.y() * parentSize.y()); + + image->setOrigin(origin); + image->setColorShift(color); + image->setColorShiftEnd(colorEnd); + image->setMirroring(reflexion); + image->setRoundCorners(roundCorners); + } + + bool Loaded; + bool Visible; + + Vector2f pos; + Vector2f size; + Vector2f origin; + Vector2f reflexion; + + unsigned int color; + unsigned int colorEnd; + + std::string sizeMode; + + float roundCorners; +}; + +struct GridTextProperties +{ +public: + GridTextProperties() + { + Loaded = false; + Visible = false; + + pos = Vector2f(-1, -1); + size = Vector2f(1.0f, 0.30f); + color = 0xFFFFFFFF; + backColor = 0; + fontSize = 0; + glowColor = 0; + glowSize = 0; + } + + void mixProperties(GridTextProperties& def, GridTextProperties& sel, float percent); + bool applyTheme(const ThemeData::ThemeElement* elem); + + void updateTextComponent(TextComponent* text, Vector2f parentSize, bool disableSize = false) + { + if (text == nullptr) + return; + + text->setPosition(pos.x() * parentSize.x(), pos.y() * parentSize.y()); + text->setSize(size.x() * parentSize.x(), size.y() * parentSize.y()); + text->setColor(color); + text->setBackgroundColor(backColor); + text->setGlowColor(glowColor); + text->setGlowSize(glowSize); + text->setAutoScroll(autoScroll); + text->setFont(fontPath, fontSize * (float)Renderer::getScreenHeight()); + } + + + + bool Loaded; + bool Visible; + + Vector2f pos; + Vector2f size; + + unsigned int color; + unsigned int backColor; + + unsigned int glowColor; + float glowSize; + + std::string fontPath; + float fontSize; + bool autoScroll; +}; + +struct GridNinePatchProperties +{ +public: + GridNinePatchProperties() + { + Loaded = false; + Visible = false; + + edgeColor = centerColor = animateColor = 0xFFFFFFFF; + cornerSize = Vector2f(16, 16); + path = ":/frame.png"; + animateTime = 0; + } + + + + bool applyTheme(const ThemeData::ThemeElement* elem); + + void updateNinePatchComponent(NinePatchComponent* ctl) + { + if (ctl == nullptr) + return; + + ctl->setCenterColor(centerColor); + ctl->setEdgeColor(edgeColor); + ctl->setCornerSize(cornerSize); + ctl->setAnimateTiming(animateTime); + ctl->setAnimateColor(animateColor); + ctl->setImagePath(path); + } + + + + bool Loaded; + bool Visible; + + Vector2f cornerSize; + unsigned int centerColor; + unsigned int edgeColor; + std::string path; + + unsigned int animateColor; + float animateTime; +}; struct GridTileProperties { - Vector2f mSize; - Vector2f mPadding; - unsigned int mImageColor; - std::string mBackgroundImage; - Vector2f mBackgroundCornerSize; - unsigned int mBackgroundCenterColor; - unsigned int mBackgroundEdgeColor; + Vector2f Size; + Vector4f Padding; + std::string SelectionMode; + + GridNinePatchProperties Background; + + GridTextProperties Label; + GridImageProperties Image; + GridImageProperties Marquee; + GridImageProperties Favorite; + GridImageProperties ImageOverlay; }; class GridTileComponent : public GuiComponent { public: GridTileComponent(Window* window); + ~GridTileComponent(); void render(const Transform4x4f& parentTrans) override; - void update(); + virtual void applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties); // Made this a static function because the ImageGridComponent need to know the default tile max size @@ -31,23 +196,85 @@ class GridTileComponent : public GuiComponent Vector2f getSelectedTileSize() const; bool isSelected() const; - void setImage(const std::string& path); - void setImage(const std::shared_ptr& texture); - void setSelected(bool selected); + void resetImages(); + + void setLabel(std::string name); + void setVideo(const std::string& path, float defaultDelay = -1.0); + + void setImage(const std::string& path, bool isDefaultImage = false); + void setMarquee(const std::string& path); + + void setFavorite(bool favorite); + bool hasFavoriteMedia() { return mFavorite != nullptr; } + + void setSelected(bool selected, bool allowAnimation = true, Vector3f* pPosition = NULL, bool force = false); void setVisible(bool visible); + void forceSize(Vector2f size, float selectedZoom = 1.0); + + void renderBackground(const Transform4x4f& parentTrans); + void renderContent(const Transform4x4f& parentTrans); + + bool shouldSplitRendering() { return isAnimationPlaying(3); }; + + Vector3f getBackgroundPosition(); + + virtual void onShow(); + virtual void onHide(); + virtual void update(int deltaTime); + virtual void onScreenSaverActivate(); + virtual void onScreenSaverDeactivate(); + + std::shared_ptr getTexture(bool marquee = false); + private: + void resetProperties(); + void createVideo(); + void createMarquee(); + void createFavorite(); + void createImageOverlay(); + void startVideo(); + void stopVideo(); + void resize(); - const GridTileProperties& getCurrentProperties() const; - std::shared_ptr mImage; + static void applyThemeToProperties(const ThemeData::ThemeElement* elem, GridTileProperties& properties); + + GridTileProperties getCurrentProperties(bool mixValues = true); + + TextComponent mLabel; + + // bool mLabelVisible; + bool mLabelMerged; + NinePatchComponent mBackground; GridTileProperties mDefaultProperties; GridTileProperties mSelectedProperties; + GridTileProperties mVideoPlayingProperties; + + std::string mCurrentMarquee; + std::string mCurrentPath; + std::string mVideoPath; + + void setSelectedZoom(float percent); + float mSelectedZoomPercent; bool mSelected; bool mVisible; + + bool mIsDefaultImage; + + Vector3f mAnimPosition; + + VideoComponent* mVideo; + ImageComponent* mImage; + ImageComponent* mMarquee; + ImageComponent* mFavorite; + ImageComponent* mImageOverlay; + + bool mVideoPlaying; + bool mShown; }; #endif // ES_CORE_COMPONENTS_GRID_TILE_COMPONENT_H diff --git a/es-core/src/components/HelpComponent.cpp b/es-core/src/components/HelpComponent.cpp index 1d7ef55291..6c2e26c060 100644 --- a/es-core/src/components/HelpComponent.cpp +++ b/es-core/src/components/HelpComponent.cpp @@ -72,7 +72,12 @@ void HelpComponent::updateGrid() for(auto it = mPrompts.cbegin(); it != mPrompts.cend(); it++) { auto icon = std::make_shared(mWindow); - icon->setImage(getIconTexture(it->first.c_str())); + + if (mStyle.iconMap.find(it->first) != mStyle.iconMap.end() && Utils::FileSystem::exists(mStyle.iconMap[it->first])) + icon->setImage(mStyle.iconMap[it->first]); + else + icon->setImage(getIconTexture(it->first.c_str())); + icon->setColorShift(mStyle.iconColor); icon->setResize(0, height); icons.push_back(icon); diff --git a/es-core/src/components/HelpComponent.h b/es-core/src/components/HelpComponent.h index e9c5d2e07d..bdebf42612 100644 --- a/es-core/src/components/HelpComponent.h +++ b/es-core/src/components/HelpComponent.h @@ -22,6 +22,8 @@ class HelpComponent : public GuiComponent void setStyle(const HelpStyle& style); + std::shared_ptr getGrid() { return mGrid; }; + private: std::shared_ptr getIconTexture(const char* name); std::map< std::string, std::shared_ptr > mIconCache; diff --git a/es-core/src/components/IList.h b/es-core/src/components/IList.h index 93f61ce9a8..a7ecf8ba98 100644 --- a/es-core/src/components/IList.h +++ b/es-core/src/components/IList.h @@ -5,6 +5,7 @@ #include "components/ImageComponent.h" #include "resources/Font.h" #include "PowerSaver.h" +#include "ThemeData.h" enum CursorState { @@ -89,7 +90,7 @@ class IList : public GuiComponent mTitleOverlayOpacity = 0x00; mTitleOverlayColor = 0xFFFFFF00; mGradient.setResize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); - mGradient.setImage(":/scroll_gradient.png"); + mGradient.setImage(ThemeData::getMenuTheme()->Background.fadePath); // ":/scroll_gradient.png"); mTitleOverlayFont = Font::get(FONT_SIZE_LARGE); } @@ -109,6 +110,28 @@ class IList : public GuiComponent onCursorChanged(CURSOR_STOPPED); } + // batocera + void setCursorIndex(int index, bool force = false) + { + if (mCursor == index && !force) + return; + + if(index >= 0 && index < (int)mEntries.size()) + { + mCursor = onBeforeScroll(index, 1); + + listInput(0); + onCursorChanged(CURSOR_STOPPED); + } + } + + // batocera + int getCursorIndex() + { + return mCursor; + } + + void clear() { mEntries.clear(); @@ -174,6 +197,15 @@ class IList : public GuiComponent inline int size() const { return (int)mEntries.size(); } + inline std::vector getObjects() + { + std::vector objects; + for (auto it = mEntries.begin(); it != mEntries.end(); it++) + objects.push_back((*it).object); + + return objects; + } + protected: void remove(typename std::vector::const_iterator& it) { @@ -298,6 +330,9 @@ class IList : public GuiComponent cursor -= size(); } + if (amt != 0) + cursor = onBeforeScroll(cursor, amt > 0 ? 1 : -1); + if(cursor != mCursor) onScroll(absAmt); @@ -305,8 +340,11 @@ class IList : public GuiComponent onCursorChanged((mScrollTier > 0) ? CURSOR_SCROLLING : CURSOR_STOPPED); } + virtual void onCursorChanged(const CursorState& /*state*/) {} virtual void onScroll(int /*amt*/) {} + + virtual int onBeforeScroll(int cursor, int direction) { return cursor; } }; #endif // ES_CORE_COMPONENTS_ILIST_H diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index 6ad41d18e8..c12b29984a 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -2,9 +2,13 @@ #include "resources/TextureResource.h" #include "Log.h" +#include "renderers/Renderer.h" #include "Settings.h" #include "ThemeData.h" +#include "resources/TextureData.h" +#include "utils/FileSystemUtil.h" + Vector2i ImageComponent::getTextureSize() const { if(mTexture) @@ -19,11 +23,20 @@ Vector2f ImageComponent::getSize() const } ImageComponent::ImageComponent(Window* window, bool forceLoad, bool dynamic) : GuiComponent(window), - mTargetIsMax(false), mTargetIsMin(false), mFlipX(false), mFlipY(false), mTargetSize(0, 0), mColorShift(0xFFFFFFFF), - mColorShiftEnd(0xFFFFFFFF), mColorGradientHorizontal(true), mForceLoad(forceLoad), mDynamic(dynamic), - mFadeOpacity(0), mFading(false), mRotateByTargetSize(false), mTopLeftCrop(0.0f, 0.0f), mBottomRightCrop(1.0f, 1.0f) -{ - updateColors(); + mTargetIsMax(false), mTargetIsMin(false), mFlipX(false), mFlipY(false), mTargetSize(0, 0), mColorShift(0xFFFFFFFF), mColorShiftEnd(0xFFFFFFFF), + mForceLoad(forceLoad), mDynamic(dynamic), mFadeOpacity(0), mFading(false), mRotateByTargetSize(false), mVisible(true), + mTopLeftCrop(0.0f, 0.0f), mBottomRightCrop(1.0f, 1.0f), mMirror(0.0f, 0.0f), mAllowAsync(false), + mPadding(Vector4f(0, 0, 0, 0)) +{ + mLinear = false; + mHorizontalAlignment = ALIGN_CENTER; + mVerticalAlignment = ALIGN_CENTER; + mReflectOnBorders = false; + mLoadingTexture = nullptr; + mAllowFading = true; + mRoundCorners = 0.0f; + mShowing = false; + mPlaylistTimer = 0; } ImageComponent::~ImageComponent() @@ -32,25 +45,32 @@ ImageComponent::~ImageComponent() void ImageComponent::resize() { - if(!mTexture) + if (!mTexture) return; const Vector2f textureSize = mTexture->getSourceImageSize(); - if(textureSize == Vector2f::Zero()) + if (textureSize == Vector2f::Zero()) return; - if(mTexture->isTiled()) + int ox = mSize.x(); + int oy = mSize.y(); + + if (mTexture->isTiled()) { + uncrop(); mSize = mTargetSize; - }else{ + } + else + { // SVG rasterization is determined by height (see SVGResource.cpp), and rasterization is done in terms of pixels // if rounding is off enough in the rasterization step (for images with extreme aspect ratios), it can cause cutoff when the aspect ratio breaks - // so, we always make sure the resultant height is an integer to make sure cutoff doesn't happen, and scale width from that + // so, we always make sure the resultant height is an integer to make sure cutoff doesn't happen, and scale width from that // (you'll see this scattered throughout the function) // this is probably not the best way, so if you're familiar with this problem and have a better solution, please make a pull request! if(mTargetIsMax) { + uncrop(); mSize = textureSize; Vector2f resizeScale((mTargetSize.x() / mSize.x()), (mTargetSize.y() / mSize.y())); @@ -62,7 +82,7 @@ void ImageComponent::resize() mSize[1] = Math::min(Math::round(mSize[1] *= resizeScale.x()), mTargetSize.y()); }else{ mSize[1] = Math::round(mSize[1] * resizeScale.y()); // this will be mTargetSize.y(). We can't exceed it. - + // for SVG rasterization, always calculate width from rounded height (see comment above) // we need to make sure we're not creating an image larger than max size mSize[0] = Math::min((mSize[1] / textureSize.y()) * textureSize.x(), mTargetSize.x()); @@ -93,7 +113,10 @@ void ImageComponent::resize() mSize[1] = Math::max(Math::round(mSize[1]), mTargetSize.y()); mSize[0] = Math::max((mSize[1] / textureSize.y()) * textureSize.x(), mTargetSize.x()); - }else{ + } + else + { + uncrop(); // if both components are set, we just stretch // if no components are set, we don't resize at all mSize = mTargetSize == Vector2f::Zero() ? textureSize : mTargetSize; @@ -114,8 +137,8 @@ void ImageComponent::resize() mSize[0] = Math::round(mSize.x()); mSize[1] = Math::round(mSize.y()); - // mSize.y() should already be rounded - mTexture->rasterizeAt((size_t)mSize.x(), (size_t)mSize.y()); + + mTexture->rasterizeAt(mSize.x(), mSize.y()); onSizeChanged(); } @@ -130,28 +153,46 @@ void ImageComponent::setDefaultImage(std::string path) mDefaultPath = path; } -void ImageComponent::setImage(std::string path, bool tile) +void ImageComponent::setImage(std::string path, bool tile, MaxSizeInfo maxSize) { - if(path.empty() || !ResourceManager::getInstance()->fileExists(path)) + std::string canonicalPath = Utils::FileSystem::getCanonicalPath(path); + if (mPath == canonicalPath) + return; + + mPath = canonicalPath; + + // If the previous image is in the async queue, remove it + TextureResource::cancelAsync(mLoadingTexture); + TextureResource::cancelAsync(mTexture); + mLoadingTexture.reset(); + + if (mPath.empty() || !ResourceManager::getInstance()->fileExists(mPath)) { - if(mDefaultPath.empty() || !ResourceManager::getInstance()->fileExists(mDefaultPath)) + if (mDefaultPath.empty() || !ResourceManager::getInstance()->fileExists(mDefaultPath)) mTexture.reset(); else - mTexture = TextureResource::get(mDefaultPath, tile, mForceLoad, mDynamic); - } else { - mTexture = TextureResource::get(path, tile, mForceLoad, mDynamic); + mTexture = TextureResource::get(mDefaultPath, tile, mLinear, mForceLoad, mDynamic, true, maxSize); } + else + { + std::shared_ptr texture = TextureResource::get(mPath, tile, mLinear, mForceLoad, mDynamic, true, maxSize); - resize(); + if (!mForceLoad && mDynamic && !mAllowFading && texture != nullptr && !texture->isLoaded()) + mLoadingTexture = texture; + else + mTexture = texture; + } + + if (mLoadingTexture == nullptr) + resize(); } void ImageComponent::setImage(const char* path, size_t length, bool tile) { mTexture.reset(); - mTexture = TextureResource::get("", tile); mTexture->initFromMemory(path, length); - + resize(); } @@ -163,6 +204,9 @@ void ImageComponent::setImage(const std::shared_ptr& texture) void ImageComponent::setResize(float width, float height) { + if (mSize.x() != 0 && mSize.y() != 0 && !mTargetIsMax && !mTargetIsMin && mTargetSize.x() == width && mTargetSize.y() == height) + return; + mTargetSize = Vector2f(width, height); mTargetIsMax = false; mTargetIsMin = false; @@ -171,6 +215,9 @@ void ImageComponent::setResize(float width, float height) void ImageComponent::setMaxSize(float width, float height) { + if (mSize.x() != 0 && mSize.y() != 0 && mTargetIsMax && !mTargetIsMin && mTargetSize.x() == width && mTargetSize.y() == height) + return; + mTargetSize = Vector2f(width, height); mTargetIsMax = true; mTargetIsMin = false; @@ -179,6 +226,9 @@ void ImageComponent::setMaxSize(float width, float height) void ImageComponent::setMinSize(float width, float height) { + if (mSize.x() != 0 && mSize.y() != 0 && mTargetIsMin && !mTargetIsMax && mTargetSize.x() == width && mTargetSize.y() == height) + return; + mTargetSize = Vector2f(width, height); mTargetIsMax = false; mTargetIsMin = true; @@ -247,37 +297,32 @@ void ImageComponent::setFlipY(bool flip) void ImageComponent::setColorShift(unsigned int color) { mColorShift = color; + mColorShiftEnd = color; // Grab the opacity from the color shift because we may need to apply it if // fading textures in - mOpacity = color & 0xff; - updateColors(); + //mOpacity = color & 0xff; } void ImageComponent::setColorShiftEnd(unsigned int color) { mColorShiftEnd = color; - // Grab the opacity from the color shift because we may need to apply it if - // fading textures in - mOpacity = color & 0xff; - updateColors(); } void ImageComponent::setColorGradientHorizontal(bool horizontal) { mColorGradientHorizontal = horizontal; - updateColors(); } void ImageComponent::setOpacity(unsigned char opacity) { mOpacity = opacity; - mColorShift = (mColorShift >> 8 << 8) | mOpacity; - updateColors(); + //mColorShift = (mColorShift >> 8 << 8) | mOpacity; + //mColorShiftEnd = (mColorShiftEnd >> 8 << 8) | mOpacity; } void ImageComponent::updateVertices() { - if(!mTexture || !mTexture->isInitialized()) + if (!mTexture) return; // we go through this mess to make sure everything is properly rounded @@ -289,10 +334,17 @@ void ImageComponent::updateVertices() const unsigned int color = Renderer::convertColor(mColorShift); const unsigned int colorEnd = Renderer::convertColor(mColorShiftEnd); - mVertices[0] = { { topLeft.x(), topLeft.y() }, { mTopLeftCrop.x(), py - mTopLeftCrop.y() }, color }; - mVertices[1] = { { topLeft.x(), bottomRight.y() }, { mTopLeftCrop.x(), 1.0f - mBottomRightCrop.y() }, mColorGradientHorizontal ? colorEnd : color }; - mVertices[2] = { { bottomRight.x(), topLeft.y() }, { mBottomRightCrop.x() * px, py - mTopLeftCrop.y() }, mColorGradientHorizontal ? color : colorEnd }; - mVertices[3] = { { bottomRight.x(), bottomRight.y() }, { mBottomRightCrop.x() * px, 1.0f - mBottomRightCrop.y() }, color }; + mVertices[0] = { { topLeft.x() + mPadding.x(), topLeft.y() + mPadding.y() }, + { mTopLeftCrop.x(), py - mTopLeftCrop.y() }, color }; + + mVertices[1] = { { topLeft.x() + mPadding.x(), bottomRight.y() - mPadding.w() }, + { mTopLeftCrop.x(), 1.0f - mBottomRightCrop.y() }, mColorGradientHorizontal ? colorEnd : color }; + + mVertices[2] = { { bottomRight.x() - mPadding.z(), topLeft.y() + mPadding.y() }, + { mBottomRightCrop.x() * px, py - mTopLeftCrop.y() }, mColorGradientHorizontal ? color : colorEnd }; + + mVertices[3] = { { bottomRight.x() - mPadding.z(), bottomRight.y() - mPadding.w() }, + { mBottomRightCrop.x() * px, 1.0f - mBottomRightCrop.y() }, color }; // round vertices for(int i = 0; i < 4; ++i) @@ -300,56 +352,144 @@ void ImageComponent::updateVertices() if(mFlipX) { - for(int i = 0; i < 4; ++i) + for (int i = 0; i < 4; i++) mVertices[i].tex[0] = px - mVertices[i].tex[0]; } - - if(mFlipY) + if (mFlipY) { - for(int i = 0; i < 4; ++i) + for (int i = 0; i < 4; i++) mVertices[i].tex[1] = py - mVertices[i].tex[1]; } } -void ImageComponent::updateColors() -{ - const unsigned int color = Renderer::convertColor(mColorShift); - const unsigned int colorEnd = Renderer::convertColor(mColorShiftEnd); - - mVertices[0].col = color; - mVertices[1].col = mColorGradientHorizontal ? colorEnd : color; - mVertices[2].col = mColorGradientHorizontal ? color : colorEnd; - mVertices[3].col = colorEnd; -} - void ImageComponent::render(const Transform4x4f& parentTrans) { - if (!isVisible()) + if (!mVisible) return; + if (mLoadingTexture != nullptr && mLoadingTexture->isLoaded()) + { + mTexture = mLoadingTexture; + mLoadingTexture.reset(); + resize(); + } + Transform4x4f trans = parentTrans * getTransform(); + + // Don't use soft clip if rotation applied : let renderer do the work + if (mRotation == 0 && !Renderer::isVisibleOnScreen(trans.translation().x(), trans.translation().y(), mSize.x(), mSize.y())) + return; + Renderer::setMatrix(trans); - if(mTexture && mOpacity > 0) + if (mTexture && mOpacity > 0) { - if(Settings::getInstance()->getBool("DebugImage")) { - Vector2f targetSizePos = (mTargetSize - mSize) * mOrigin * -1; - Renderer::drawRect(targetSizePos.x(), targetSizePos.y(), mTargetSize.x(), mTargetSize.y(), 0xFF000033, 0xFF000033); - Renderer::drawRect(0.0f, 0.0f, mSize.x(), mSize.y(), 0x00000033, 0x00000033); + Vector2f targetSizePos = (mTargetSize - mSize) * mOrigin * -1; + + if (Settings::getInstance()->getBool("DebugImage")) + { + Renderer::drawRect(targetSizePos.x(), targetSizePos.y(), mTargetSize.x(), mTargetSize.y(), 0xFF000033); + Renderer::drawRect(0.0f, 0.0f, mSize.x(), mSize.y(), 0x00000033); } - if(mTexture->isInitialized()) + + if (!mTexture->bind()) { - // actually draw the image - // The bind() function returns false if the texture is not currently loaded. A blank - // texture is bound in this case but we want to handle a fade so it doesn't just 'jump' in - // when it finally loads - fadeIn(mTexture->bind()); - Renderer::drawTriangleStrips(&mVertices[0], 4); - - }else{ - LOG(LogError) << "Image texture is not initialized!"; - mTexture.reset(); + fadeIn(false); + return; } + + if (mVerticalAlignment == ALIGN_TOP) + trans.translate(Vector3f(0, targetSizePos.y(), 0.0f)); + else if (mVerticalAlignment == ALIGN_BOTTOM) + trans.translate(Vector3f(targetSizePos.x(), targetSizePos.y() + mTargetSize.y() - mSize.y(), 0.0f)); + + if (mHorizontalAlignment == ALIGN_LEFT) + trans.translate(Vector3f(targetSizePos.x(), 0, 0.0f)); + else if (mHorizontalAlignment == ALIGN_RIGHT) + trans.translate(Vector3f(targetSizePos.x() + mTargetSize.x() - mSize.x(), targetSizePos.y(), 0.0f)); + + Renderer::setMatrix(trans); + + fadeIn(true); + + float opacity = (mOpacity * (mFading ? mFadeOpacity / 255.0 : 1.0)) / 255.0; + + const unsigned int color = Renderer::convertColor(mColorShift & 0xFFFFFF00 | (unsigned char)((mColorShift & 0xFF) * opacity)); + const unsigned int colorEnd = Renderer::convertColor(mColorShiftEnd & 0xFFFFFF00 | (unsigned char)((mColorShiftEnd & 0xFF) * opacity)); + + mVertices[0].col = color; + mVertices[1].col = mColorGradientHorizontal ? colorEnd : color; + mVertices[2].col = mColorGradientHorizontal ? color : colorEnd; + mVertices[3].col = colorEnd; + + if (mRoundCorners > 0) + { + float x = 0; + float y = 0; + float size_x = mSize.x(); + float size_y = mSize.y(); + + + if (mTargetIsMin) + { + x = targetSizePos.x(); + y = targetSizePos.y(); + size_x = mTargetSize.x(); + size_y = mTargetSize.y(); + } + + float radius = Math::max(size_x, size_y) * mRoundCorners; + + Renderer::enableRoundCornerStencil(x, y, size_x, size_y, radius); + + mTexture->bind(); + } + + Renderer::drawTriangleStrips(&mVertices[0], 4); + + if (mRoundCorners > 0) + Renderer::disableStencil(); + + if (mMirror.x() != 0 || mMirror.y() != 0) + { + float alpha = ((mColorShift & 0x000000ff)) / 255.0; + float alpha2 = alpha * mMirror.y(); + + alpha *= mMirror.x(); + + const unsigned int colorT = Renderer::convertColor((mColorShift & 0xffffff00) + (unsigned char)(255.0*alpha)); + const unsigned int colorB = Renderer::convertColor((mColorShift & 0xffffff00) + (unsigned char)(255.0*alpha2)); + + int h = mVertices[1].pos.y() - mVertices[0].pos.y(); + if (mReflectOnBorders) + h = mTargetSize.y(); + + Renderer::Vertex mirrorVertices[4]; + + mirrorVertices[0] = { + { mVertices[0].pos.x(), mVertices[0].pos.y() + h }, + { mVertices[0].tex.x(), mVertices[1].tex.y() }, + colorT }; + + mirrorVertices[1] = { + { mVertices[1].pos.x(), mVertices[1].pos.y() + h }, + { mVertices[1].tex.x(), mVertices[0].tex.y() }, + colorB }; + + mirrorVertices[2] = { + { mVertices[2].pos.x(), mVertices[2].pos.y() + h }, + { mVertices[2].tex.x(), mVertices[3].tex.y() }, + colorT }; + + mirrorVertices[3] = { + { mVertices[3].pos.x(), mVertices[3].pos.y() + h }, + { mVertices[3].tex.x(), mVertices[2].tex.y() }, + colorB }; + + Renderer::drawTriangleStrips(&mirrorVertices[0], 4); + } + + Renderer::bindTexture(0); } GuiComponent::renderChildren(trans); @@ -357,6 +497,9 @@ void ImageComponent::render(const Transform4x4f& parentTrans) void ImageComponent::fadeIn(bool textureLoaded) { + if (!mAllowFading) + return; + if (!mForceLoad) { if (!textureLoaded) @@ -368,11 +511,11 @@ void ImageComponent::fadeIn(bool textureLoaded) mFadeOpacity = 0; mFading = true; // Set the colours to be translucent - mColorShift = (mColorShift >> 8 << 8) | 0; - updateColors(); + // mColorShift = (mColorShift >> 8 << 8) | 0; + // mColorShiftEnd = (mColorShiftEnd >> 8 << 8) | 0; } } - else if (mFading) + else if (mFading && textureLoaded) { // The texture is loaded and we need to fade it in. The fade is based on the frame rate // and is 1/4 second if running at 60 frames per second although the actual value is not @@ -389,9 +532,9 @@ void ImageComponent::fadeIn(bool textureLoaded) mFadeOpacity = (unsigned char)opacity; } // Apply the combination of the target opacity and current fade - float newOpacity = (float)mOpacity * ((float)mFadeOpacity / 255.0f); - mColorShift = (mColorShift >> 8 << 8) | (unsigned char)newOpacity; - updateColors(); + // float newOpacity = (float)mOpacity * ((float)mFadeOpacity / 255.0f); + // mColorShift = (mColorShift >> 8 << 8) | (unsigned char)newOpacity; + // mColorShiftEnd = (mColorShiftEnd >> 8 << 8) | (unsigned char)newOpacity; } } } @@ -411,8 +554,11 @@ void ImageComponent::applyTheme(const std::shared_ptr& theme, const s return; } - Vector2f scale = getParent() ? getParent()->getSize() : Vector2f((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); + if (elem->has("linearSmooth")) + mLinear = elem->get("linearSmooth"); + Vector2f scale = getParent() ? getParent()->getSize() : Vector2f((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); + if(properties & POSITION && elem->has("pos")) { Vector2f denormalized = elem->get("pos") * scale; @@ -422,13 +568,27 @@ void ImageComponent::applyTheme(const std::shared_ptr& theme, const s if(properties & ThemeFlags::SIZE) { if(elem->has("size")) - setResize(elem->get("size") * scale); + { + auto sz = elem->get("size"); + if (sz.x() == 0 && sz.y() != 0 && Settings::getInstance()->getInt("ScreenRotate") != 0) + { + sz.x() = sz.y(); + setMinSize(sz * scale); + } + else if (sz.y() == 0 && sz.x() != 0 && Settings::getInstance()->getInt("ScreenRotate") != 0) + { + sz.y() = sz.x(); + setMinSize(sz * scale); + } + else + setResize(sz * scale); + } else if(elem->has("maxSize")) setMaxSize(elem->get("maxSize") * scale); else if(elem->has("minSize")) setMinSize(elem->get("minSize") * scale); } - + // position + size also implies origin if((properties & ORIGIN || (properties & POSITION && properties & ThemeFlags::SIZE)) && elem->has("origin")) setOrigin(elem->get("origin")); @@ -437,32 +597,75 @@ void ImageComponent::applyTheme(const std::shared_ptr& theme, const s setDefaultImage(elem->get("default")); } + + if(properties & PATH && elem->has("path")) { - bool tile = (elem->has("tile") && elem->get("tile")); - setImage(elem->get("path"), tile); + auto path = elem->get("path"); + if (ResourceManager::getInstance()->fileExists(path)) + { + bool tile = (elem->has("tile") && elem->get("tile")); + setImage(path, tile/*, Vector2f(mTargetSize.x(), mTargetSize.y())*/); + } } - if(properties & COLOR) + if (properties & COLOR) { - if(elem->has("color")) - { + if (elem->has("color")) setColorShift(elem->get("color")); - setColorShiftEnd(elem->get("color")); - } if (elem->has("colorEnd")) setColorShiftEnd(elem->get("colorEnd")); if (elem->has("gradientType")) - setColorGradientHorizontal(!(elem->get("gradientType").compare("horizontal"))); + setColorGradientHorizontal(elem->get("gradientType").compare("horizontal")); + + if (elem->has("reflexion")) + mMirror = elem->get("reflexion"); + else + mMirror = Vector2f::Zero(); + + if (elem->has("reflexionOnFrame")) + mReflectOnBorders = elem->get("reflexionOnFrame"); + else + mReflectOnBorders = false; } - if(properties & ThemeFlags::ROTATION) { + if(properties & ThemeFlags::ROTATION) + { if(elem->has("rotation")) setRotationDegrees(elem->get("rotation")); + if(elem->has("rotationOrigin")) setRotationOrigin(elem->get("rotationOrigin")); + + if (elem->has("flipX")) + setFlipX(elem->get("flipX")); + + if (elem->has("flipY")) + setFlipY(elem->get("flipY")); + } + + if (properties & ALIGNMENT && elem->has("horizontalAlignment")) + { + std::string str = elem->get("horizontalAlignment"); + if (str == "left") + setHorizontalAlignment(ALIGN_LEFT); + else if (str == "right") + setHorizontalAlignment(ALIGN_RIGHT); + else + setHorizontalAlignment(ALIGN_CENTER); + } + + if (properties & ALIGNMENT && elem->has("verticalAlignment")) + { + std::string str = elem->get("verticalAlignment"); + if (str == "top") + setVerticalAlignment(ALIGN_TOP); + else if (str == "bottom") + setVerticalAlignment(ALIGN_BOTTOM); + else + setVerticalAlignment(ALIGN_CENTER); } if(properties & ThemeFlags::Z_INDEX && elem->has("zIndex")) @@ -470,6 +673,9 @@ void ImageComponent::applyTheme(const std::shared_ptr& theme, const s else setZIndex(getDefaultZIndex()); + if (properties & ALIGNMENT && elem->has("roundCorners")) + mRoundCorners = elem->get("roundCorners"); + if(properties & ThemeFlags::VISIBLE && elem->has("visible")) setVisible(elem->get("visible")); else @@ -479,6 +685,64 @@ void ImageComponent::applyTheme(const std::shared_ptr& theme, const s std::vector ImageComponent::getHelpPrompts() { std::vector ret; - ret.push_back(HelpPrompt("a", "select")); + ret.push_back(HelpPrompt("a", _("SELECT"))); return ret; } + + + +void ImageComponent::setPlaylist(std::shared_ptr playList) +{ + mPlaylist = playList; + if (mPlaylist == nullptr) + return; + + auto image = mPlaylist->getNextItem(); + if (!image.empty()) + setImage(image); +} + +void ImageComponent::onShow() +{ + GuiComponent::onShow(); + + if (!mShowing && mPlaylist != nullptr && !mPath.empty()) + { + auto item = mPlaylist->getNextItem(); + if (!item.empty()) + setImage(item, false, getMaxSizeInfo()); + } + + mShowing = true; +} + +void ImageComponent::onHide() +{ + GuiComponent::onHide(); + mShowing = false; +} + + +void ImageComponent::update(int deltaTime) +{ + GuiComponent::update(deltaTime); + + if (mPlaylist != nullptr && mShowing) + { + mPlaylistTimer += deltaTime; + + if (mPlaylistTimer >= 10000) + { + auto item = mPlaylist->getNextItem(); + if (!item.empty()) + setImage(item, false, getMaxSizeInfo()); + + mPlaylistTimer = 0.0; + } + } +} + +bool ImageComponent::isTiled() +{ + return mTexture != nullptr && mTexture->isTiled(); +} \ No newline at end of file diff --git a/es-core/src/components/ImageComponent.h b/es-core/src/components/ImageComponent.h index 6d19454568..67226d5761 100644 --- a/es-core/src/components/ImageComponent.h +++ b/es-core/src/components/ImageComponent.h @@ -2,11 +2,17 @@ #ifndef ES_CORE_COMPONENTS_IMAGE_COMPONENT_H #define ES_CORE_COMPONENTS_IMAGE_COMPONENT_H -#include "renderers/Renderer.h" +#include "renderers/Renderer.h" #include "math/Vector2i.h" #include "GuiComponent.h" +#include "resources/TextureResource.h" +#include "resources/Font.h" -class TextureResource; +class IPlaylist +{ +public: + virtual std::string getNextItem() = 0; +}; class ImageComponent : public GuiComponent { @@ -14,10 +20,12 @@ class ImageComponent : public GuiComponent ImageComponent(Window* window, bool forceLoad = false, bool dynamic = true); virtual ~ImageComponent(); + std::string getValue() const override { return mPath; } + void setDefaultImage(std::string path); //Loads the image at the given filepath. Will tile if tile is true (retrieves texture as tiling, creates vertices accordingly). - void setImage(std::string path, bool tile = false); + void setImage(std::string path, bool tile = false, MaxSizeInfo maxSize = MaxSizeInfo()); //Loads an image from memory. void setImage(const char* image, size_t length, bool tile = false); //Use an already existing texture. @@ -58,6 +66,8 @@ class ImageComponent : public GuiComponent void setColorShiftEnd(unsigned int color); void setColorGradientHorizontal(bool horizontal); + virtual void setColor(unsigned int color) { setColorShift(color); } + void setFlipX(bool flip); // Mirror on the X axis. void setFlipY(bool flip); // Mirror on the Y axis. @@ -68,13 +78,55 @@ class ImageComponent : public GuiComponent Vector2f getSize() const override; + bool isVisible() { + return mVisible; + } + void setVisible(bool visible) { + mVisible = visible; + } + bool hasImage(); void render(const Transform4x4f& parentTrans) override; + void setAllowAsync(bool async) { mAllowAsync = async; }; + void setAllowFading(bool fade) { mAllowFading = fade; }; + void setMirroring(Vector2f mirror) { mMirror = mirror; }; + virtual void applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) override; virtual std::vector getHelpPrompts() override; + + std::shared_ptr getTexture() { return mTexture; }; + + const MaxSizeInfo getMaxSizeInfo() + { + if (mTargetSize == Vector2f(0, 0)) + return MaxSizeInfo(mSize, mTargetIsMax); + + return MaxSizeInfo(mTargetSize, mTargetIsMax); + }; + + void setPadding(const Vector4f padding) { mPadding = padding; updateVertices(); } + + void setHorizontalAlignment(Alignment align) { mHorizontalAlignment = align; } + void setVerticalAlignment(Alignment align) { mVerticalAlignment = align; } + + float getRoundCorners() { return mRoundCorners; } + void setRoundCorners(float value) { mRoundCorners = value; } + + virtual void onShow() override; + virtual void onHide() override; + virtual void update(int deltaTime); + + void setPlaylist(std::shared_ptr playList); + + std::string getImagePath() { return mPath; } + bool isTiled(); + + bool isLinear() { return mLinear; } + void setIsLinear(bool value) { mLinear = value; } + private: Vector2f mTargetSize; @@ -85,9 +137,10 @@ class ImageComponent : public GuiComponent void resize(); Renderer::Vertex mVertices[4]; +// GLubyte mColors[6*4]; void updateVertices(); - void updateColors(); +// void updateColors(); void fadeIn(bool textureLoaded); unsigned int mColorShift; @@ -98,13 +151,34 @@ class ImageComponent : public GuiComponent std::shared_ptr mTexture; unsigned char mFadeOpacity; + bool mFading; bool mForceLoad; bool mDynamic; bool mRotateByTargetSize; + bool mVisible; + bool mAllowFading; + bool mAllowAsync; + float mRoundCorners; Vector2f mTopLeftCrop; - Vector2f mBottomRightCrop; + Vector2f mBottomRightCrop; + + Vector2f mMirror; + bool mReflectOnBorders; + + std::string mPath; + std::shared_ptr mLoadingTexture; + + Vector4f mPadding; + Alignment mHorizontalAlignment; + Alignment mVerticalAlignment; + + bool mShowing; + std::shared_ptr mPlaylist; + float mPlaylistTimer; + + bool mLinear; }; #endif // ES_CORE_COMPONENTS_IMAGE_COMPONENT_H diff --git a/es-core/src/components/ImageGridComponent.h b/es-core/src/components/ImageGridComponent.h index 65b329adcb..a0d57394cc 100644 --- a/es-core/src/components/ImageGridComponent.h +++ b/es-core/src/components/ImageGridComponent.h @@ -6,6 +6,12 @@ #include "components/IList.h" #include "resources/TextureResource.h" #include "GridTileComponent.h" +#include "animations/LambdaAnimation.h" +#include "Settings.h" +#include "Sound.h" +#include "EsLocale.h" + +#define EXTRAITEMS 2 enum ScrollDirection { @@ -13,9 +19,29 @@ enum ScrollDirection SCROLL_HORIZONTALLY }; +enum ImageSource +{ + THUMBNAIL, + IMAGE, + MARQUEE +}; + +enum CenterSelection +{ + FULL, + PARTIAL, + NEVER +}; + + struct ImageGridData { std::string texturePath; + std::string marqueePath; + std::string videoPath; + bool favorite; + bool folder; + bool virtualFolder; }; template @@ -40,7 +66,7 @@ class ImageGridComponent : public IList ImageGridComponent(Window* window); - void add(const std::string& name, const std::string& imagePath, const T& obj); + void add(const std::string& name, const std::string& imagePath, const std::string& videoPath, const std::string& marqueePath, bool favorite, bool folder, bool virtualFolder, const T& obj); bool input(InputConfig* config, Input input) override; void update(int deltaTime) override; @@ -50,20 +76,35 @@ class ImageGridComponent : public IList void onSizeChanged() override; inline void setCursorChangedCallback(const std::function& func) { mCursorChangedCallback = func; } + void setThemeName(std::string name) { mName = name; }; + + virtual void topWindow(bool isTop); + virtual void onShow(); + virtual void onHide(); + virtual void onScreenSaverActivate(); + virtual void onScreenSaverDeactivate(); + + ImageSource getImageSource() { return mImageSource; }; + + void setGridSizeOverride(Vector2f size); + + std::shared_ptr getSelectedTile(); + + void resetLastCursor() { mLastCursor = -1; } + protected: - virtual void onCursorChanged(const CursorState& state) override; + virtual void onCursorChanged(const CursorState& state) override; + virtual void onScroll(int /*amt*/) { if (!mScrollSound.empty()) Sound::get(mScrollSound)->play(); } private: // TILES void buildTiles(); - void updateTiles(); - void updateTileAtPos(int tilePos, int imgPos, int bufferTop, int bufferBot); - int getStartPosition() const; + void updateTiles(bool allowAnimation = true, bool updateSelectedState = true); + void updateTileAtPos(int tilePos, int imgPos, bool allowAnimation = true, bool updateSelectedState = true); void calcGridDimension(); + + bool isVertical() { return mScrollDirection == SCROLL_VERTICALLY; }; - // IMAGES & ENTRIES - const int texBuffersBehind[4] = { 1, 1, 1, 1 }; - const int texBuffersForward[4] = { 1, 2, 3, 3 }; bool mEntriesDirty; int mLastCursor; std::string mDefaultGameTexture; @@ -71,14 +112,38 @@ class ImageGridComponent : public IList // TILES bool mLastRowPartial; + bool mAnimateSelection; + Vector2f mAutoLayout; + float mAutoLayoutZoom; + Vector4f mPadding; Vector2f mMargin; Vector2f mTileSize; Vector2i mGridDimension; + Vector2f mGridSizeOverride; + + std::string mScrollSound; + std::shared_ptr mTheme; std::vector< std::shared_ptr > mTiles; + std::string mName; + + int mStartPosition; + + bool mAllowVideo; + float mVideoDelay; + + float mCamera; + float mCameraDirection; + // MISCELLANEOUS + CenterSelection mCenterSelection; + + bool mScrollLoop; + ScrollDirection mScrollDirection; + ImageSource mImageSource; + std::function mCursorChangedCallback; }; @@ -87,25 +152,47 @@ ImageGridComponent::ImageGridComponent(Window* window) : IList -void ImageGridComponent::add(const std::string& name, const std::string& imagePath, const T& obj) +void ImageGridComponent::add(const std::string& name, const std::string& imagePath, const std::string& videoPath, const std::string& marqueePath, bool favorite, bool folder, bool virtualFolder, const T& obj) { typename IList::Entry entry; entry.name = name; entry.object = obj; entry.data.texturePath = imagePath; + entry.data.videoPath = videoPath; + entry.data.marqueePath = marqueePath; + entry.data.favorite = favorite; + entry.data.folder = folder; + entry.data.virtualFolder = virtualFolder; static_cast*>(this)->add(entry); mEntriesDirty = true; @@ -116,19 +203,25 @@ bool ImageGridComponent::input(InputConfig* config, Input input) { if(input.value != 0) { + int idx = isVertical() ? 0 : 1; + Vector2i dir = Vector2i::Zero(); if(config->isMappedLike("up", input)) - dir[1 ^ mScrollDirection] = -1; + dir[1 ^ idx] = -1; else if(config->isMappedLike("down", input)) - dir[1 ^ mScrollDirection] = 1; + dir[1 ^ idx] = 1; else if(config->isMappedLike("left", input)) - dir[0 ^ mScrollDirection] = -1; + dir[0 ^ idx] = -1; else if(config->isMappedLike("right", input)) - dir[0 ^ mScrollDirection] = 1; + dir[0 ^ idx] = 1; if(dir != Vector2i::Zero()) { - listInput(dir.x() + dir.y() * mGridDimension.x()); + if (isVertical()) + listInput(dir.x() + dir.y() * mGridDimension.x()); + else + listInput(dir.x() + dir.y() * mGridDimension.y()); + return true; } }else{ @@ -144,18 +237,122 @@ bool ImageGridComponent::input(InputConfig* config, Input input) template void ImageGridComponent::update(int deltaTime) { + GuiComponent::update(deltaTime); listUpdate(deltaTime); - + for(auto it = mTiles.begin(); it != mTiles.end(); it++) - (*it)->update(); + (*it)->update(deltaTime); +} + +template +void ImageGridComponent::topWindow(bool isTop) +{ + GuiComponent::topWindow(isTop); + + for (int ti = 0; ti < (int)mTiles.size(); ti++) + { + std::shared_ptr tile = mTiles.at(ti); + tile->topWindow(isTop); + } +} + +template +void ImageGridComponent::onShow() +{ + if (mEntriesDirty) + { + updateTiles(); + mEntriesDirty = false; + } + + GuiComponent::onShow(); + + for (int ti = 0; ti < (int)mTiles.size(); ti++) + { + std::shared_ptr tile = mTiles.at(ti); + tile->onShow(); + } +} + +template +void ImageGridComponent::onScreenSaverActivate() +{ + GuiComponent::onScreenSaverActivate(); + + for (int ti = 0; ti < (int)mTiles.size(); ti++) + { + std::shared_ptr tile = mTiles.at(ti); + tile->onScreenSaverActivate(); + } +} + +template +void ImageGridComponent::onScreenSaverDeactivate() +{ + GuiComponent::onScreenSaverDeactivate(); + + for (int ti = 0; ti < (int)mTiles.size(); ti++) + { + std::shared_ptr tile = mTiles.at(ti); + tile->onScreenSaverDeactivate(); + } +} + + +template +void ImageGridComponent::setGridSizeOverride(Vector2f size) +{ + mGridSizeOverride = size; +} + +template +std::shared_ptr ImageGridComponent::getSelectedTile() +{ + for (int ti = 0; ti < (int)mTiles.size(); ti++) + { + std::shared_ptr tile = mTiles.at(ti); + if (tile->isSelected()) + return tile; + } + + return nullptr; +} + +template +void ImageGridComponent::onHide() +{ + GuiComponent::onHide(); + + for (int ti = 0; ti < (int)mTiles.size(); ti++) + { + std::shared_ptr tile = mTiles.at(ti); + tile->onHide(); + } } template void ImageGridComponent::render(const Transform4x4f& parentTrans) { Transform4x4f trans = getTransform() * parentTrans; + Transform4x4f tileTrans = trans; + + Vector2f clipPos(trans.translation().x(), trans.translation().y()); + if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) + return; - if(mEntriesDirty) + if (Settings::getInstance()->getBool("DebugGrid")) + { + Renderer::setMatrix(trans); + Renderer::drawRect(0.0f, 0.0f, mSize.x(), mSize.y(), 0xFF000033); + Renderer::setMatrix(parentTrans); + } + + float offsetX = isVertical() ? 0 : mCamera * mCameraDirection * (mTileSize.x() + mMargin.x()); + float offsetY = isVertical() ? mCamera * mCameraDirection * (mTileSize.y() + mMargin.y()) : 0; + + tileTrans.translate(Vector3f(offsetX, offsetY, 0.0)); + + if (mEntriesDirty) { updateTiles(); mEntriesDirty = false; @@ -168,26 +365,57 @@ void ImageGridComponent::render(const Transform4x4f& parentTrans) Vector2i pos((int)Math::round(trans.translation()[0]), (int)Math::round(trans.translation()[1])); Vector2i size((int)Math::round(mSize.x() * scaleX), (int)Math::round(mSize.y() * scaleY)); + if (Settings::getInstance()->getBool("DebugGrid")) + { + for (auto it = mTiles.begin(); it != mTiles.end(); it++) + { + std::shared_ptr tile = (*it); + + auto tt = tile->getTransform() * trans; + Renderer::setMatrix(tt); + Renderer::drawRect(0.0, 0.0, tile->getSize().x(), tile->getSize().y(), 0x00FF0033); + } + + Renderer::setMatrix(parentTrans); + } + + bool splittedRendering = (mAnimateSelection && mAutoLayout.x() != 0); + Renderer::pushClipRect(pos, size); - // Render all the tiles but the selected one + // Render the selected image background on bottom of the others if needed std::shared_ptr selectedTile = NULL; for(auto it = mTiles.begin(); it != mTiles.end(); it++) { std::shared_ptr tile = (*it); - - // If it's the selected image, keep it for later, otherwise render it now - if(tile->isSelected()) + if (tile->isSelected()) + { selectedTile = tile; - else - tile->render(trans); - } - Renderer::popClipRect(); + if (splittedRendering && tile->shouldSplitRendering()) + tile->renderBackground(tileTrans); - // Render the selected image on top of the others + break; + } + } + + for (auto it = mTiles.begin(); it != mTiles.end(); it++) + { + std::shared_ptr tile = (*it); + if (!tile->isSelected()) + tile->render(tileTrans); + } + + // Render the selected image content on top of the others if (selectedTile != NULL) - selectedTile->render(trans); + { + if (splittedRendering && selectedTile->shouldSplitRendering()) + selectedTile->renderContent(tileTrans); + else + selectedTile->render(tileTrans); + } + + Renderer::popClipRect(); listRenderTitleOverlay(trans); @@ -197,12 +425,12 @@ void ImageGridComponent::render(const Transform4x4f& parentTrans) template void ImageGridComponent::applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) { + // Keep the theme pointer to apply it on the tiles later on + mTheme = nullptr; + // Apply theme to GuiComponent but not size property, which will be applied at the end of this function GuiComponent::applyTheme(theme, view, element, properties ^ ThemeFlags::SIZE); - // Keep the theme pointer to apply it on the tiles later on - mTheme = theme; - Vector2f screen = Vector2f((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); const ThemeData::ThemeElement* elem = theme->getElement(view, element, "imagegrid"); @@ -211,8 +439,78 @@ void ImageGridComponent::applyTheme(const std::shared_ptr& theme, if (elem->has("margin")) mMargin = elem->get("margin") * screen; + if (elem->has("padding")) + mPadding = elem->get("padding") * Vector4f(screen.x(), screen.y(), screen.x(), screen.y()); + + if (elem->has("autoLayout")) + mAutoLayout = elem->get("autoLayout"); + + if (elem->has("animateSelection")) + mAnimateSelection = elem->get("animateSelection"); + + if (elem->has("autoLayoutSelectedZoom")) + mAutoLayoutZoom = elem->get("autoLayoutSelectedZoom"); + + if (elem->has("imageSource")) + { + auto direction = elem->get("imageSource"); + if (direction == "image") + mImageSource = IMAGE; + else if (direction == "marquee") + mImageSource = MARQUEE; + else + mImageSource = THUMBNAIL; + } + else + mImageSource = THUMBNAIL; + if (elem->has("scrollDirection")) - mScrollDirection = (ScrollDirection)(elem->get("scrollDirection") == "horizontal"); + { + auto direction = elem->get("scrollDirection"); + if (direction == "horizontal") + { + mCenterSelection = CenterSelection::PARTIAL; + mScrollDirection = SCROLL_HORIZONTALLY; + } + else if (direction == "horizontalCenter") + { + mCenterSelection = CenterSelection::FULL; + mScrollDirection = SCROLL_HORIZONTALLY; + } + else if (direction == "verticalCenter") + { + mCenterSelection = CenterSelection::FULL; + mScrollDirection = SCROLL_VERTICALLY; + } + else + { + mCenterSelection = CenterSelection::NEVER; + mScrollDirection = SCROLL_VERTICALLY; + } + } + + if (elem->has("showVideoAtDelay")) + { + mVideoDelay = elem->get("showVideoAtDelay"); + mAllowVideo = (mVideoDelay >= 0); + } + else + mAllowVideo = false; + + if (elem->has("centerSelection")) + { + if (!(elem->get("centerSelection").compare("true"))) + mCenterSelection = CenterSelection::FULL; + else if (!(elem->get("centerSelection").compare("partial"))) + mCenterSelection = CenterSelection::PARTIAL; + else + mCenterSelection = CenterSelection::NEVER; + } + + if (mCenterSelection != CenterSelection::NEVER && elem->has("scrollLoop")) + mScrollLoop = (elem->get("scrollLoop")); + else + mScrollLoop = false; if (elem->has("gameImage")) { @@ -257,6 +555,9 @@ void ImageGridComponent::applyTheme(const std::shared_ptr& theme, } } + if (elem->has("scrollSound")) + mScrollSound = elem->get("scrollSound"); + // We still need to manually get the grid tile size here, // so we can recalculate the new grid dimension, and THEN (re)build the tiles elem = theme->getElement(view, "default", "gridtile"); @@ -266,75 +567,202 @@ void ImageGridComponent::applyTheme(const std::shared_ptr& theme, GridTileComponent::getDefaultTileSize(); // Apply size property, will trigger a call to onSizeChanged() which will build the tiles - GuiComponent::applyTheme(theme, view, element, ThemeFlags::SIZE); + GuiComponent::applyTheme(theme, view, element, ThemeFlags::SIZE | ThemeFlags::Z_INDEX); + + // Keep the theme pointer to apply it on the tiles later on + mTheme = theme; // Trigger the call manually if the theme have no "imagegrid" element - if (!elem) - buildTiles(); + buildTiles(); + updateTiles(false, false); } template void ImageGridComponent::onSizeChanged() { + if (mTheme == nullptr) + return; + buildTiles(); - updateTiles(); + updateTiles(false, false); } template void ImageGridComponent::onCursorChanged(const CursorState& state) { - updateTiles(); + if (mLastCursor == mCursor) + { + if (state == CURSOR_STOPPED && mCursorChangedCallback) + mCursorChangedCallback(state); - if(mCursorChangedCallback) - mCursorChangedCallback(state); -} + return; + } + + + bool direction = mCursor >= mLastCursor; -// Create and position tiles (mTiles) -template -void ImageGridComponent::buildTiles() -{ - mTiles.clear(); + int diff = direction ? mCursor - mLastCursor : mLastCursor - mCursor; + if (mScrollLoop && diff == mEntries.size() - 1) + direction = !direction; - calcGridDimension(); + int oldStart = mStartPosition; - Vector2f tileDistance = mTileSize + mMargin; - Vector2f bufferSize = Vector2f(mScrollDirection == SCROLL_HORIZONTALLY ? tileDistance.x() * texBuffersForward[3] : 0, - mScrollDirection == SCROLL_VERTICALLY ? tileDistance.y() * texBuffersForward[3] : 0); - Vector2f startPosition = mTileSize / 2 - bufferSize; + float dimScrollable = isVertical() ? mGridDimension.y() - 2 * EXTRAITEMS : mGridDimension.x() - 2 * EXTRAITEMS; + float dimOpposite = isVertical() ? mGridDimension.x() : mGridDimension.y(); - int X, Y; + int centralCol = (int)(dimScrollable - 0.5) / 2; + int maxCentralCol = (int)(dimScrollable) / 2; - // Layout tile size and position - for(int y = 0; y < mGridDimension.y(); y++) + int oldCol = (mLastCursor / dimOpposite); + int col = (mCursor / dimOpposite); + + int lastCol = ((mEntries.size() - 1) / dimOpposite); + + int lastScroll = std::max(0, (int)(lastCol + 1 - dimScrollable)); + + float startPos = 0; + float endPos = 1; + + if (((GuiComponent*)this)->isAnimationPlaying(2)) + { + startPos = 0; + ((GuiComponent*)this)->cancelAnimation(2); + updateTiles(false, !GuiComponent::ALLOWANIMATIONS); + } + + if (GuiComponent::ALLOWANIMATIONS) { - for(int x = 0; x < mGridDimension.x(); x++) + std::shared_ptr oldTile = nullptr; + std::shared_ptr newTile = nullptr; + + int oldIdx = mLastCursor - mStartPosition + (dimOpposite * EXTRAITEMS); + if (oldIdx >= 0 && oldIdx < mTiles.size()) + oldTile = mTiles[oldIdx]; + + int newIdx = mCursor - mStartPosition + (dimOpposite * EXTRAITEMS); + + if (mScrollLoop && mScrollTier == 0 && diff == mEntries.size() - 1) { - // Create tiles - auto tile = std::make_shared(mWindow); + if (direction) + newIdx += mEntries.size(); + else + newIdx -= mEntries.size(); + } - // In Vertical mod, tiles are ordered from left to right, then from top to bottom - // In Horizontal mod, tiles are ordered from top to bottom, then from left to right - X = mScrollDirection == SCROLL_VERTICALLY ? x : y; - Y = mScrollDirection == SCROLL_VERTICALLY ? y : x; + if (newIdx >= 0 && newIdx < mTiles.size()) + newTile = mTiles[newIdx]; - tile->setPosition(X * tileDistance.x() + startPosition.x(), Y * tileDistance.y() + startPosition.y()); - tile->setOrigin(0.5f, 0.5f); - tile->setImage(""); + for (auto it = mTiles.begin(); it != mTiles.end(); it++) + { + if ((*it)->isSelected() && *it != oldTile && *it != newTile) + { + startPos = 0; + (*it)->setSelected(false, false, nullptr); + } + } - if (mTheme) - tile->applyTheme(mTheme, "grid", "gridtile", ThemeFlags::ALL); + Vector3f oldPos = Vector3f::Zero(); - mTiles.push_back(tile); + if (oldTile != nullptr && oldTile != newTile) + { + oldPos = oldTile->getBackgroundPosition(); + oldTile->setSelected(false, true, nullptr, true); } + + if (newTile != nullptr) + { + if (!mAnimateSelection) + { + oldPos = Vector3f(0, 0); + newTile->setSelected(true, true, &oldPos, true); + } + else + newTile->setSelected(true, true, oldPos == Vector3f(0, 0) ? nullptr : &oldPos, true); + } + } + + int firstVisibleCol = mStartPosition / dimOpposite; + + if (mCenterSelection == CenterSelection::NEVER) + { + if (col == 0) + mStartPosition = 0; + if (col < firstVisibleCol) + mStartPosition = col * dimOpposite; + else if (col >= firstVisibleCol + dimScrollable) + mStartPosition = (col - dimScrollable + 1) * dimOpposite; } + else + { + if ((col < centralCol || (col == 0 && col == centralCol)) && mCenterSelection == CenterSelection::PARTIAL) + mStartPosition = 0; + else if ((col - centralCol) > lastScroll && mCenterSelection == CenterSelection::PARTIAL && !mScrollLoop) + mStartPosition = lastScroll * dimOpposite; + else if (maxCentralCol != centralCol && col == firstVisibleCol + maxCentralCol || col == firstVisibleCol + centralCol) + { + if (col == firstVisibleCol + maxCentralCol) + mStartPosition = (col - maxCentralCol) * dimOpposite; + else + mStartPosition = (col - centralCol) * dimOpposite; + } + else + { + if (oldCol == firstVisibleCol + maxCentralCol) + mStartPosition = (col - maxCentralCol) * dimOpposite; + else + mStartPosition = (col - centralCol) * dimOpposite; + } + } + + auto lastCursor = mLastCursor; + mLastCursor = mCursor; + + mCameraDirection = direction ? -1.0 : 1.0; + mCamera = 0; + + if (lastCursor < 0 || !GuiComponent::ALLOWANIMATIONS) + { + updateTiles((lastCursor >= 0 || mScrollLoop) && GuiComponent::ALLOWANIMATIONS); + + if (mCursorChangedCallback) + mCursorChangedCallback(state); + + return; + } + + if (mCursorChangedCallback) + mCursorChangedCallback(state); + + bool moveCamera = (oldStart != mStartPosition); + + auto func = [this, startPos, endPos, moveCamera](float t) + { + if (!moveCamera) + return; + + t -= 1; // cubic ease out + float pct = Math::lerp(0, 1, t*t*t + 1); + t = startPos * (1.0 - pct) + endPos * pct; + + mCamera = t; + }; + + ((GuiComponent*)this)->setAnimation(new LambdaAnimation(func, 250), 0, [this, direction] { + mCamera = 0; + updateTiles(false); + }, false, 2); } + template -void ImageGridComponent::updateTiles() +void ImageGridComponent::updateTiles(bool allowAnimation, bool updateSelectedState) { if (!mTiles.size()) return; + if (!mEntries.size()) + return; + // Stop updating the tiles at highest scroll speed if (mScrollTier == 3) { @@ -342,113 +770,241 @@ void ImageGridComponent::updateTiles() { std::shared_ptr tile = mTiles.at(ti); - tile->setSelected(false); + tile->setSelected(false, allowAnimation); + tile->setLabel(""); tile->setImage(mDefaultGameTexture); + tile->setMarquee(""); tile->setVisible(false); } return; } - // 1 if scrolling down, -1 if scrolling up - int scrollDirection = mCursor >= mLastCursor ? 1 : -1; - - // If going down, update from top to bottom - // If going up, update from bottom to top - int ti = scrollDirection == 1 ? 0 : (int)mTiles.size() - 1; - int end = scrollDirection == 1 ? (int)mTiles.size() : -1; + // Temporary store previous texture so they can't be unloaded - avoids flickering & reloading + std::vector> previousTextures; + for (int ti = 0; ti < (int)mTiles.size(); ti++) + { + previousTextures.push_back(mTiles.at(ti)->getTexture(true)); + previousTextures.push_back(mTiles.at(ti)->getTexture(false)); + } - int img = getStartPosition(); - if (scrollDirection == -1) - img += (int)mTiles.size() - 1; + int i = 0; + int end = (int)mTiles.size(); + int img = mStartPosition; - // Calculate buffer size depending on scroll speed and direction - int bufferBehind = (texBuffersForward[3] - texBuffersBehind[mScrollTier]) * mGridDimension.x(); - int bufferForward = (texBuffersForward[3] - texBuffersForward[mScrollTier]) * mGridDimension.x(); + img -= EXTRAITEMS * (isVertical() ? mGridDimension.x() : mGridDimension.y()); - int bufferTop = scrollDirection == 1 ? bufferBehind : bufferForward; - int bufferBot = scrollDirection == 1 ? bufferForward : bufferBehind; + while (i != end) + { + updateTileAtPos(i, img, allowAnimation, updateSelectedState); + i++; img++; + } + + // Collect new textures + std::vector> newTextures; + for (int ti = 0; ti < (int)mTiles.size(); ti++) + { + newTextures.push_back(mTiles.at(ti)->getTexture(true)); + newTextures.push_back(mTiles.at(ti)->getTexture(false)); + } - // Update the tiles - while (ti != end) + // Compare old texture with new textures -> Remove missing from async queue if existing + for (auto tex : previousTextures) { - updateTileAtPos(ti, img, bufferTop, bufferBot); + if (tex == nullptr) + continue; - ti += scrollDirection; - img += scrollDirection; + if (std::find(newTextures.cbegin(), newTextures.cend(), tex) == newTextures.cend()) + TextureResource::cancelAsync(tex); } - mLastCursor = mCursor; + if (updateSelectedState) + mLastCursor = mCursor; + + mEntriesDirty = false; } template -void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos, int bufferTop, int bufferBot) +void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos, bool allowAnimation, bool updateSelectedState) { std::shared_ptr tile = mTiles.at(tilePos); + bool loopedIndex = false; + + if (mScrollLoop && mEntries.size() > 0) + { + int dimOpposite = isVertical() ? mGridDimension.x() : mGridDimension.y(); + + int min = dimOpposite == 1 ? 0 : (mEntries.size() % dimOpposite) - dimOpposite; + int max = mEntries.size() - min; + + if (imgPos < min) + { + loopedIndex = true; + + while (imgPos < min) + imgPos += max; + } + else if (imgPos >= max) + { + loopedIndex = true; + + while (imgPos >= max) + imgPos -= max; + } + } + // If we have more tiles than we have to display images on screen, hide them - if(imgPos < 0 || imgPos >= size() - || tilePos < bufferTop || tilePos >= (int)mTiles.size() - bufferBot) // Same for tiles out of the buffer + if(imgPos < 0 || imgPos >= size() || tilePos < 0 || tilePos >= (int) mTiles.size()) // Same for tiles out of the buffer { - tile->setSelected(false); - tile->setImage(""); + if (updateSelectedState) + tile->setSelected(false, allowAnimation); + + tile->resetImages(); tile->setVisible(false); } else { - tile->setSelected(imgPos == mCursor); tile->setVisible(true); + std::string name = mEntries.at(imgPos).name; + + if (!mEntries.at(imgPos).data.favorite || tile->hasFavoriteMedia()) + tile->setLabel(name); + else + tile->setLabel(_U("\uF006 ") + name); + std::string imagePath = mEntries.at(imgPos).data.texturePath; + if (ResourceManager::getInstance()->fileExists(imagePath)) { - tile->setImage(imagePath); + if (mEntries.at(imgPos).data.virtualFolder) + tile->setLabel(""); // _U("\uF114")); + + tile->setImage(imagePath, mEntries.at(imgPos).data.virtualFolder); + } + else if (mEntries.at(imgPos).data.folder) + tile->setImage(mDefaultFolderTexture, mDefaultFolderTexture == ":/folder.svg"); + else + tile->setImage(mDefaultGameTexture, mDefaultGameTexture == ":/cartridge.svg"); + + // Marquee + std::string marqueePath = mEntries.at(imgPos).data.marqueePath; + + if (!marqueePath.empty() && ResourceManager::getInstance()->fileExists(marqueePath)) + tile->setMarquee(marqueePath); + else + tile->setMarquee(""); + + tile->setFavorite(mEntries.at(imgPos).data.favorite); + + // Video + if (mAllowVideo && imgPos == mCursor) + { + std::string videoPath = mEntries.at(imgPos).data.videoPath; + + if (!videoPath.empty() && ResourceManager::getInstance()->fileExists(videoPath)) + tile->setVideo(videoPath, mVideoDelay); + else + tile->setVideo(""); } else + tile->setVideo(""); + + if (updateSelectedState) { - // FileType::FOLDER = 2, but FileData is our template parameter T, - // so we don't want to bring that dependence to FileData here - if (mEntries.at(imgPos).object->getType() == 2) - tile->setImage(mDefaultFolderTexture); + if (!loopedIndex && imgPos == mCursor && mCursor != mLastCursor) + { + int dif = mCursor - tilePos; + int idx = mLastCursor - dif; + + if (idx < 0 || idx >= mTiles.size()) + idx = 0; + + Vector3f pos = mTiles.at(idx)->getBackgroundPosition(); + if (!mAnimateSelection) + pos = Vector3f(0, 0, 0); + + tile->setSelected(true, allowAnimation, &pos); + } else - tile->setImage(mDefaultGameTexture); + tile->setSelected(!loopedIndex && imgPos == mCursor, allowAnimation); } } } -// Return the starting position (the number of the game which will be displayed on top left of the screen) + +// Create and position tiles (mTiles) template -int ImageGridComponent::getStartPosition() const +void ImageGridComponent::buildTiles() { - // The "partialRow" variable exist because we want to keep the same positioning behavior in both - // case, whenever we have an integer number of rows or not (the last partial row is ignored when - // calculating position and the cursor shouldn't end up in this row when close to the end) - int partialRow = (int)mLastRowPartial; + if (mGridSizeOverride.x() != 0 && mGridSizeOverride.y() != 0) + mAutoLayout = mGridSizeOverride; - int cursorRow = mCursor / mGridDimension.x(); + mStartPosition = 0; + mTiles.clear(); - int start = (cursorRow - ((mGridDimension.y() - partialRow) / 2)) * mGridDimension.x(); + calcGridDimension(); - // Number of tiles which are just used as a buffer for texture loading - int bufferSize = texBuffersForward[3] * mGridDimension.x(); + if (mCenterSelection != CenterSelection::NEVER) + { + int dimScrollable = (isVertical() ? mGridDimension.y() : mGridDimension.x()) - 2 * EXTRAITEMS; + mStartPosition -= (int)Math::floorf(dimScrollable / 2.0f); + } - if(start + (mGridDimension.x() * (mGridDimension.y() - partialRow)) >= (int)mEntries.size() + bufferSize) + Vector2f tileDistance = mTileSize + mMargin; + Vector2f tileSize = mTileSize; + + if (mAutoLayout.x() != 0 && mAutoLayout.y() != 0) { - // If we are at the end put the row as close as we can and no higher, using the following formula - // Where E is the nb of entries, X the grid x dim (nb of column), Y the grid y dim (nb of line) - // start = first tile of last row - nb column * (nb line - 1) - // = (E - 1) / X * X - X * (Y - 1) - // = X * ((E - 1) / X - Y + 1) - start = mGridDimension.x() * (((int)mEntries.size() - 1) / mGridDimension.x() - mGridDimension.y() + 1 + partialRow) + bufferSize; + auto x = (mSize.x() - (mMargin.x() * (mAutoLayout.x() - 1)) - mPadding.x() - mPadding.z()) / (int) mAutoLayout.x(); + auto y = (mSize.y() - (mMargin.y() * (mAutoLayout.y() - 1)) - mPadding.y() - mPadding.w()) / (int) mAutoLayout.y(); + + tileSize = Vector2f(x, y); + mTileSize = tileSize; + tileDistance = tileSize + mMargin; } - if(start < -bufferSize) + bool vert = isVertical(); + + Vector2f bufferSize = Vector2f(/*vert && mGridDimension.y() == 1 ? tileDistance.x() :*/ 0, 0); + Vector2f startPosition = tileSize / 2 - bufferSize; + + startPosition += Vector2f(mPadding.x(), mPadding.y()); + + int X, Y; + + // Layout tile size and position + for (int y = 0; y < (vert ? mGridDimension.y() : mGridDimension.x()); y++) { - start = -bufferSize; + for (int x = 0; x < (vert ? mGridDimension.x() : mGridDimension.y()); x++) + { + // Create tiles + auto tile = std::make_shared(mWindow); + + // In Vertical mod, tiles are ordered from left to right, then from top to bottom + // In Horizontal mod, tiles are ordered from top to bottom, then from left to right + X = vert ? x : y - EXTRAITEMS; + Y = vert ? y - EXTRAITEMS : x; + + tile->setOrigin(0.5f, 0.5f); + tile->setPosition(X * tileDistance.x() + startPosition.x(), Y * tileDistance.y() + startPosition.y()); + tile->setSize(mTileSize); + + if (mTheme) + tile->applyTheme(mTheme, mName, "gridtile", ThemeFlags::ALL); + + if (mAutoLayout.x() != 0 && mAutoLayout.y() != 0) + tile->forceSize(mTileSize, mAutoLayoutZoom); + + mTiles.push_back(tile); + } } - return start; + mLastCursor = -1; + onCursorChanged(CURSOR_STOPPED); } + // Calculate how much tiles of size mTileSize we can fit in a grid of size mSize using a margin of size mMargin template void ImageGridComponent::calcGridDimension() @@ -456,15 +1012,11 @@ void ImageGridComponent::calcGridDimension() // GRID_SIZE = COLUMNS * TILE_SIZE + (COLUMNS - 1) * MARGIN // <=> COLUMNS = (GRID_SIZE + MARGIN) / (TILE_SIZE + MARGIN) Vector2f gridDimension = (mSize + mMargin) / (mTileSize + mMargin); + if (mAutoLayout.x() != 0 && mAutoLayout.y() != 0) + gridDimension = mAutoLayout; mLastRowPartial = Math::floorf(gridDimension.y()) != gridDimension.y(); - - // Ceil y dim so we can display partial last row - mGridDimension = Vector2i(gridDimension.x(), Math::ceilf(gridDimension.y())); - - // Invert dimensions for horizontally scrolling grid - if (mScrollDirection == SCROLL_HORIZONTALLY) - mGridDimension = Vector2i(mGridDimension.y(), mGridDimension.x()); + mGridDimension = Vector2i((int) gridDimension.x(), (int) gridDimension.y()); // Grid dimension validation if (mGridDimension.x() < 1) @@ -472,8 +1024,11 @@ void ImageGridComponent::calcGridDimension() if (mGridDimension.y() < 1) LOG(LogError) << "Theme defined grid Y dimension below 1"; - // Add extra tiles to both side depending on max texture buffer - mGridDimension.y() += texBuffersForward[3] * 2; + // Add extra tiles to both sides : Add EXTRAITEMS before, EXTRAITEMS after + if (isVertical()) + mGridDimension.y() += 2 * EXTRAITEMS; + else + mGridDimension.x() += 2 * EXTRAITEMS; }; diff --git a/es-core/src/components/MenuComponent.cpp b/es-core/src/components/MenuComponent.cpp index 2e60b59459..02b7c10459 100644 --- a/es-core/src/components/MenuComponent.cpp +++ b/es-core/src/components/MenuComponent.cpp @@ -2,50 +2,211 @@ #include "components/ButtonComponent.h" -#define BUTTON_GRID_VERT_PADDING 32 -#define BUTTON_GRID_HORIZ_PADDING 10 +#define BUTTON_GRID_VERT_PADDING (Renderer::getScreenHeight()*0.0296296) //32 +#define BUTTON_GRID_HORIZ_PADDING (Renderer::getScreenWidth()*0.0052083333) //10 -#define TITLE_HEIGHT (mTitle->getFont()->getLetterHeight() + TITLE_VERT_PADDING) +#define TITLE_HEIGHT (mTitle->getFont()->getLetterHeight() + (mSubtitle ? TITLE_WITHSUB_VERT_PADDING : TITLE_VERT_PADDING) + (mSubtitle ? mSubtitle->getSize().y() + SUBTITLE_VERT_PADDING : 0)) -MenuComponent::MenuComponent(Window* window, const char* title, const std::shared_ptr& titleFont) : GuiComponent(window), +MenuComponent::MenuComponent(Window* window, + const std::string title, const std::shared_ptr& titleFont, + const std::string subTitle) + : GuiComponent(window), mBackground(window), mGrid(window, Vector2i(1, 3)) { + mMaxHeight = 0; + + auto theme = ThemeData::getMenuTheme(); + addChild(&mBackground); addChild(&mGrid); - - mBackground.setImagePath(":/frame.png"); + mBackground.setImagePath(theme->Background.path); + mBackground.setEdgeColor(theme->Background.color); + mBackground.setCenterColor(theme->Background.centerColor); + mBackground.setCornerSize(theme->Background.cornerSize); // set up title mTitle = std::make_shared(mWindow); mTitle->setHorizontalAlignment(ALIGN_CENTER); - mTitle->setColor(0x555555FF); - setTitle(title, titleFont); - mGrid.setEntry(mTitle, Vector2i(0, 0), false); + mTitle->setColor(theme->Title.color); // 0x555555FF + + if (theme->Title.selectorColor != 0x555555FF) + { + mTitle->setBackgroundColor(theme->Title.selectorColor); + mTitle->setRenderBackground(true); + } + + mHeaderGrid = std::make_shared(mWindow, Vector2i(1, 2)); + mHeaderGrid->setEntry(mTitle, Vector2i(0, 0), false, true); + + setTitle(title, theme->Title.font); // titleFont + setSubTitle(subTitle); + + mGrid.setEntry(mHeaderGrid, Vector2i(0, 0), false, true); + //mGrid.setEntry(mTitle, Vector2i(0, 0), false); // set up list which will never change (externally, anyway) mList = std::make_shared(mWindow); mGrid.setEntry(mList, Vector2i(0, 1), true); + mGrid.setUnhandledInputCallback([this](InputConfig* config, Input input) -> bool { + if (config->isMappedLike("down", input)) { + mGrid.setCursorTo(mList); + mList->setCursorIndex(0); + return true; + } + if (config->isMappedLike("up", input)) { + mList->setCursorIndex(mList->size() - 1); + if (mButtons.size()) { + mGrid.moveCursor(Vector2i(0, 1)); + } + else { + mGrid.setCursorTo(mList); + } + return true; + } + return false; + }); + updateGrid(); updateSize(); mGrid.resetCursor(); } -void MenuComponent::setTitle(const char* title, const std::shared_ptr& font) +void MenuComponent::addWithLabel(const std::string& label, const std::shared_ptr& comp, const std::string iconName, bool setCursorHere, bool invert_when_selected) +{ + auto theme = ThemeData::getMenuTheme(); + + ComponentListRow row; + + if (!iconName.empty()) + { + std::string iconPath = theme->getMenuIcon(iconName); + if (!iconPath.empty()) + { + // icon + auto icon = std::make_shared(mWindow); + icon->setImage(iconPath); + icon->setColorShift(theme->Text.color); + icon->setResize(0, theme->Text.font->getLetterHeight() * 1.25f); + row.addElement(icon, false); + + // spacer between icon and text + auto spacer = std::make_shared(mWindow); + spacer->setSize(10, 0); + row.addElement(spacer, false); + } + } + + row.addElement(std::make_shared(mWindow, Utils::String::toUpper(label), theme->Text.font, theme->Text.color), true); + row.addElement(comp, false, invert_when_selected); + addRow(row, setCursorHere); +} + +void MenuComponent::addEntry(const std::string name, bool add_arrow, const std::function& func, const std::string iconName, bool setCursorHere, bool invert_when_selected) +{ + auto theme = ThemeData::getMenuTheme(); + std::shared_ptr font = theme->Text.font; + unsigned int color = theme->Text.color; + + // populate the list + ComponentListRow row; + + if (!iconName.empty()) + { + std::string iconPath = theme->getMenuIcon(iconName); + if (!iconPath.empty()) + { + // icon + auto icon = std::make_shared(mWindow); + icon->setImage(iconPath); + icon->setColorShift(theme->Text.color); + icon->setResize(0, theme->Text.font->getLetterHeight() * 1.25f); + row.addElement(icon, false); + + // spacer between icon and text + auto spacer = std::make_shared(mWindow); + spacer->setSize(10, 0); + row.addElement(spacer, false); + } + } + + row.addElement(std::make_shared(mWindow, name, font, color), true, invert_when_selected); + + if (add_arrow) + row.addElement(makeArrow(mWindow), false); + + row.makeAcceptInputHandler(func); + + addRow(row, setCursorHere); +} + +void MenuComponent::setTitle(const std::string title, const std::shared_ptr& font) { mTitle->setText(Utils::String::toUpper(title)); - mTitle->setFont(font); + + if (font != nullptr) + mTitle->setFont(font); +} + +void MenuComponent::setSubTitle(const std::string text) +{ + if (text.empty()) + { + if (mSubtitle != nullptr) + { + mHeaderGrid->removeEntry(mSubtitle); + mSubtitle = nullptr; + } + + mHeaderGrid->setRowHeightPerc(0, 1); + mHeaderGrid->setRowHeightPerc(1, 0); + + return; + } + + if (mSubtitle == nullptr) + { + auto theme = ThemeData::getMenuTheme(); + + mSubtitle = std::make_shared(mWindow, + Utils::String::toUpper(Utils::FileSystem::getFileName(text)), + theme->TextSmall.font, theme->TextSmall.color, ALIGN_CENTER); + + mHeaderGrid->setEntry(mSubtitle, Vector2i(0, 1), false, true); + } + + mSubtitle->setText(Utils::String::toUpper(text)); + mSubtitle->setVerticalAlignment(Alignment::ALIGN_TOP); + mSubtitle->setSize(Renderer::getScreenWidth() * 0.88f, 0); + mSubtitle->setLineSpacing(1.1); + + updateSize(); + + const float titleHeight = mTitle->getFont()->getLetterHeight() + (mSubtitle ? TITLE_WITHSUB_VERT_PADDING : TITLE_VERT_PADDING); + const float subtitleHeight = mSubtitle->getSize().y() + SUBTITLE_VERT_PADDING; + + mHeaderGrid->setRowHeightPerc(0, titleHeight / TITLE_HEIGHT); } float MenuComponent::getButtonGridHeight() const { - return (mButtonGrid ? mButtonGrid->getSize().y() : Font::get(FONT_SIZE_MEDIUM)->getHeight() + BUTTON_GRID_VERT_PADDING); + auto menuTheme = ThemeData::getMenuTheme(); + return (mButtonGrid ? mButtonGrid->getSize().y() : menuTheme->Text.font->getHeight() + BUTTON_GRID_VERT_PADDING); + //return (mButtonGrid ? mButtonGrid->getSize().y() : Font::get(FONT_SIZE_MEDIUM)->getHeight() + BUTTON_GRID_VERT_PADDING); } void MenuComponent::updateSize() { - const float maxHeight = Renderer::getScreenHeight() * 0.75f; + // GPI + if (Renderer::isSmallScreen()) + { + setSize(Renderer::getScreenWidth(), Renderer::getScreenHeight()); + return; + } + + const float maxHeight = mMaxHeight <= 0 ? Renderer::getScreenHeight() * 0.75f : mMaxHeight; + float height = TITLE_HEIGHT + mList->getTotalRowHeight() + getButtonGridHeight() + 2; if(height > maxHeight) { @@ -103,6 +264,43 @@ std::vector MenuComponent::getHelpPrompts() return mGrid.getHelpPrompts(); } +std::shared_ptr makeMultiDimButtonGrid(Window* window, const std::vector< std::vector< std::shared_ptr > >& buttons, float outerWidth) +{ + + const int sizeX = (int)buttons.at(0).size(); + const int sizeY = (int)buttons.size(); + const float buttonHeight = buttons.at(0).at(0)->getSize().y(); + const float gridHeight = (buttonHeight + BUTTON_GRID_VERT_PADDING + 2) * sizeY; + + float horizPadding = (float)BUTTON_GRID_HORIZ_PADDING; + float gridWidth, buttonWidth; + +// do { + gridWidth = outerWidth - horizPadding; // to get centered because size * (button size + BUTTON_GRID_VERT_PADDING) let a half BUTTON_GRID_VERT_PADDING left / right marge + buttonWidth = (gridWidth / sizeX) - horizPadding; + // horizPadding -= 2; +// } while ((buttonWidth < 100) && (horizPadding > 2)); + + std::shared_ptr grid = std::make_shared(window, Vector2i(sizeX, sizeY)); + + grid->setSize(gridWidth, gridHeight); + + for (int x = 0; x < sizeX; x++) + grid->setColWidthPerc(x, (float)1 / sizeX); + + for (int y = 0; y < sizeY; y++) + { + for (int x = 0; x < sizeX; x++) + { + const std::shared_ptr& button = buttons.at(y).at(x); + button->setSize(buttonWidth, buttonHeight); + grid->setEntry(button, Vector2i(x, y), true, false); + } + } + + return grid; +} + std::shared_ptr makeButtonGrid(Window* window, const std::vector< std::shared_ptr >& buttons) { std::shared_ptr buttonGrid = std::make_shared(window, Vector2i((int)buttons.size(), 2)); @@ -126,8 +324,12 @@ std::shared_ptr makeButtonGrid(Window* window, const std::vector< std::shared_ptr makeArrow(Window* window) { + auto menuTheme = ThemeData::getMenuTheme(); + auto bracket = std::make_shared(window); - bracket->setImage(":/arrow.svg"); - bracket->setResize(0, Math::round(Font::get(FONT_SIZE_MEDIUM)->getLetterHeight())); + bracket->setImage(ThemeData::getMenuTheme()->Icons.arrow); // ":/arrow.svg"); + bracket->setColorShift(menuTheme->Text.color); + bracket->setResize(0, round(menuTheme->Text.font->getLetterHeight())); + //bracket->setResize(0, Math::round(Font::get(FONT_SIZE_MEDIUM)->getLetterHeight())); return bracket; } diff --git a/es-core/src/components/MenuComponent.h b/es-core/src/components/MenuComponent.h index ad2f69db10..b91a7cdebc 100644 --- a/es-core/src/components/MenuComponent.h +++ b/es-core/src/components/MenuComponent.h @@ -12,48 +12,64 @@ class ButtonComponent; class ImageComponent; std::shared_ptr makeButtonGrid(Window* window, const std::vector< std::shared_ptr >& buttons); +std::shared_ptr makeMultiDimButtonGrid(Window* window, const std::vector< std::vector< std::shared_ptr > >& buttons, float outerWidth); std::shared_ptr makeArrow(Window* window); #define TITLE_VERT_PADDING (Renderer::getScreenHeight()*0.0637f) +#define TITLE_WITHSUB_VERT_PADDING (Renderer::getScreenHeight()*0.05f) +#define SUBTITLE_VERT_PADDING (Renderer::getScreenHeight()*0.019f) class MenuComponent : public GuiComponent { public: - MenuComponent(Window* window, const char* title, const std::shared_ptr& titleFont = Font::get(FONT_SIZE_LARGE)); + MenuComponent(Window* window, + const std::string title, const std::shared_ptr& titleFont = Font::get(FONT_SIZE_LARGE), + const std::string subTitle = ""); void onSizeChanged() override; inline void addRow(const ComponentListRow& row, bool setCursorHere = false) { mList->addRow(row, setCursorHere); updateSize(); } - inline void addWithLabel(const std::string& label, const std::shared_ptr& comp, bool setCursorHere = false, bool invert_when_selected = true) - { - ComponentListRow row; - row.addElement(std::make_shared(mWindow, Utils::String::toUpper(label), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - row.addElement(comp, false, invert_when_selected); - addRow(row, setCursorHere); - } + void addWithLabel(const std::string& label, const std::shared_ptr& comp, const std::string iconName = "", bool setCursorHere = false, bool invert_when_selected = true); + void addEntry(const std::string name, bool add_arrow, const std::function& func, const std::string iconName="", bool setCursorHere = false, bool invert_when_selected = true); + void addGroup(const std::string& label) { mList->addGroup(label); updateSize(); } void addButton(const std::string& label, const std::string& helpText, const std::function& callback); - void setTitle(const char* title, const std::shared_ptr& font); + void setTitle(const std::string title, const std::shared_ptr& font = nullptr); + void setSubTitle(const std::string text); inline void setCursorToList() { mGrid.setCursorTo(mList); } inline void setCursorToButtons() { assert(mButtonGrid); mGrid.setCursorTo(mButtonGrid); } virtual std::vector getHelpPrompts() override; + float getButtonGridHeight() const; + + void setMaxHeight(float maxHeight) + { + if (mMaxHeight == maxHeight) + return; + + mMaxHeight = maxHeight; + updateSize(); + } + private: void updateSize(); void updateGrid(); - float getButtonGridHeight() const; NinePatchComponent mBackground; ComponentGrid mGrid; + std::shared_ptr mHeaderGrid; std::shared_ptr mTitle; + std::shared_ptr mSubtitle; std::shared_ptr mList; std::shared_ptr mButtonGrid; std::vector< std::shared_ptr > mButtons; + + float mMaxHeight; }; #endif // ES_CORE_COMPONENTS_MENU_COMPONENT_H diff --git a/es-core/src/components/MultiLineMenuEntry.h b/es-core/src/components/MultiLineMenuEntry.h new file mode 100644 index 0000000000..476a06dc46 --- /dev/null +++ b/es-core/src/components/MultiLineMenuEntry.h @@ -0,0 +1,39 @@ + +#pragma once + +#include "Window.h" +#include "components/TextComponent.h" +#include "components/ComponentGrid.h" +#include "math/Vector2i.h" +#include "math/Vector2f.h" +#include "ThemeData.h" + +class MultiLineMenuEntry : public ComponentGrid +{ +public: + MultiLineMenuEntry(Window* window, const std::string& text, const std::string& substring) : + ComponentGrid(window, Vector2i(1, 2)) + { + auto theme = ThemeData::getMenuTheme(); + + mText = std::make_shared(mWindow, text.c_str(), theme->Text.font, theme->Text.color); + mText->setVerticalAlignment(ALIGN_TOP); + + mSubstring = std::make_shared(mWindow, substring.c_str(), theme->TextSmall.font, theme->Text.color); + mSubstring->setOpacity(192); + + setEntry(mText, Vector2i(0, 0), true, true); + setEntry(mSubstring, Vector2i(0, 1), false, true); + + setSize(Vector2f(0, mText->getSize().y() + mSubstring->getSize().y())); + } + + virtual void setColor(unsigned int color) + { + mText->setColor(color); + mSubstring->setColor(color); + } + + std::shared_ptr mText; + std::shared_ptr mSubstring; +}; diff --git a/es-core/src/components/NinePatchComponent.cpp b/es-core/src/components/NinePatchComponent.cpp index 6ab1c89a04..5fbd7c46c0 100644 --- a/es-core/src/components/NinePatchComponent.cpp +++ b/es-core/src/components/NinePatchComponent.cpp @@ -5,13 +5,25 @@ #include "ThemeData.h" NinePatchComponent::NinePatchComponent(Window* window, const std::string& path, unsigned int edgeColor, unsigned int centerColor) : GuiComponent(window), - mCornerSize(16, 16), - mEdgeColor(edgeColor), mCenterColor(centerColor), - mPath(path), - mVertices(NULL) +mCornerSize(16, 16), +mEdgeColor(edgeColor), mCenterColor(centerColor), +mVertices(NULL) { - if(!mPath.empty()) - buildVertices(); + mTimer = 0; + mAnimateTiming = 0; + mAnimateColor = 0xFFFFFFFF; + + mPreviousSize = Vector2f(0, 0); + setImagePath(path); +} + +void NinePatchComponent::setOpacity(unsigned char opacity) +{ + if (mOpacity == opacity) + return; + + mOpacity = opacity; + updateColors(); } NinePatchComponent::~NinePatchComponent() @@ -20,26 +32,54 @@ NinePatchComponent::~NinePatchComponent() delete[] mVertices; } +void NinePatchComponent::update(int deltaTime) +{ + GuiComponent::update(deltaTime); + + if (mAnimateTiming > 0) + { + mTimer += deltaTime; + if (mTimer >= 2 * mAnimateTiming) + mTimer = 0; + } +} + void NinePatchComponent::updateColors() { - const unsigned int edgeColor = Renderer::convertColor(mEdgeColor); - const unsigned int centerColor = Renderer::convertColor(mCenterColor); + if (mVertices == nullptr) + return; + + float opacity = mOpacity / 255.0; - for(int i = 0; i < 6*9; ++i) + unsigned int e = mEdgeColor; + unsigned int c = mCenterColor; + + if (mAnimateTiming > 0) + { + float percent = std::abs(mAnimateTiming - mTimer) / mAnimateTiming; + e = Renderer::mixColors(e, mAnimateColor, percent); + c = Renderer::mixColors(e, mAnimateColor, percent); + } + + const unsigned int edgeColor = Renderer::convertColor(e & 0xFFFFFF00 | (unsigned char)((e & 0xFF) * opacity)); + const unsigned int centerColor = Renderer::convertColor(c & 0xFFFFFF00 | (unsigned char)((c & 0xFF) * opacity)); + + for(int i = 0; i < 6*9; i++) mVertices[i].col = edgeColor; - for(int i = 6*4; i < 6; ++i) - mVertices[(6*4)+i].col = mCenterColor; + for(int i = 0; i < 6; i++) + mVertices[(4 * 6) + i].col = centerColor; } void NinePatchComponent::buildVertices() { - if(mVertices != NULL) - delete[] mVertices; + if (mTexture == nullptr) + return; - mTexture = TextureResource::get(mPath); + if (mVertices != NULL) + delete[] mVertices; - if(mTexture->getSize() == Vector2i::Zero()) + if (mTexture->getSize() == Vector2i::Zero()) { mVertices = NULL; LOG(LogWarning) << "NinePatchComponent missing texture!"; @@ -49,26 +89,28 @@ void NinePatchComponent::buildVertices() mVertices = new Renderer::Vertex[6 * 9]; const Vector2f texSize = Vector2f((float)mTexture->getSize().x(), (float)mTexture->getSize().y()); + if (texSize.x() <= 0 || texSize.y() <= 0) + return; - const float imgSizeX[3] = { mCornerSize.x(), mSize.x() - mCornerSize.x() * 2, mCornerSize.x()}; - const float imgSizeY[3] = { mCornerSize.y(), mSize.y() - mCornerSize.y() * 2, mCornerSize.y()}; - const float imgPosX[3] = { 0, imgSizeX[0], imgSizeX[0] + imgSizeX[1]}; - const float imgPosY[3] = { 0, imgSizeY[0], imgSizeY[0] + imgSizeY[1]}; + const float imgSizeX[3] = { mCornerSize.x(), mSize.x() - mCornerSize.x() * 2, mCornerSize.x() }; + const float imgSizeY[3] = { mCornerSize.y(), mSize.y() - mCornerSize.y() * 2, mCornerSize.y() }; + const float imgPosX[3] = { 0, imgSizeX[0], imgSizeX[0] + imgSizeX[1] }; + const float imgPosY[3] = { 0, imgSizeY[0], imgSizeY[0] + imgSizeY[1] }; //the "1 +" in posY and "-" in sizeY is to deal with texture coordinates having a bottom left corner origin vs. verticies having a top left origin - const float texSizeX[3] = { mCornerSize.x() / texSize.x(), (texSize.x() - mCornerSize.x() * 2) / texSize.x(), mCornerSize.x() / texSize.x() }; + const float texSizeX[3] = { mCornerSize.x() / texSize.x(), (texSize.x() - mCornerSize.x() * 2) / texSize.x(), mCornerSize.x() / texSize.x() }; const float texSizeY[3] = { -mCornerSize.y() / texSize.y(), -(texSize.y() - mCornerSize.y() * 2) / texSize.y(), -mCornerSize.y() / texSize.y() }; - const float texPosX[3] = { 0, texSizeX[0], texSizeX[0] + texSizeX[1] }; - const float texPosY[3] = { 1, 1 + texSizeY[0], 1 + texSizeY[0] + texSizeY[1] }; + const float texPosX[3] = { 0, texSizeX[0], texSizeX[0] + texSizeX[1] }; + const float texPosY[3] = { 1, 1 + texSizeY[0], 1 + texSizeY[0] + texSizeY[1] }; int v = 0; - for(int slice = 0; slice < 9; slice++) + for (int slice = 0; slice < 9; slice++) { - const int sliceX = slice % 3; - const int sliceY = slice / 3; - const Vector2f imgPos = Vector2f(imgPosX[sliceX], imgPosY[sliceY]); + const int sliceX = slice % 3; + const int sliceY = slice / 3; + const Vector2f imgPos = Vector2f(imgPosX[sliceX], imgPosY[sliceY]); const Vector2f imgSize = Vector2f(imgSizeX[sliceX], imgSizeY[sliceY]); - const Vector2f texPos = Vector2f(texPosX[sliceX], texPosY[sliceY]); + const Vector2f texPos = Vector2f(texPosX[sliceX], texPosY[sliceY]); const Vector2f texSize = Vector2f(texSizeX[sliceX], texSizeY[sliceY]); mVertices[v + 1] = { { imgPos.x() , imgPos.y() }, { texPos.x(), texPos.y() }, 0 }; @@ -77,7 +119,7 @@ void NinePatchComponent::buildVertices() mVertices[v + 4] = { { imgPos.x() + imgSize.x(), imgPos.y() + imgSize.y() }, { texPos.x() + texSize.x(), texPos.y() + texSize.y() }, 0 }; // round vertices - for(int i = 1; i < 5; ++i) + for (int i = 1; i < 5; ++i) mVertices[v + i].pos.round(); // make duplicates of first and last vertex so this can be rendered as a triangle strip @@ -92,17 +134,65 @@ void NinePatchComponent::buildVertices() void NinePatchComponent::render(const Transform4x4f& parentTrans) { - if (!isVisible()) + if (!isVisible() || mTexture == nullptr || mVertices == nullptr) return; Transform4x4f trans = parentTrans * getTransform(); + if (!Renderer::isVisibleOnScreen(trans.translation().x(), trans.translation().y(), mSize.x(), mSize.y())) + return; - if(mTexture && mVertices != NULL) + if (mCornerSize.x() <= 1 && mCornerSize.y() <= 1 && mCornerSize.x() == mCornerSize.y()) { + float opacity = mOpacity / 255.0; + + unsigned int e = mEdgeColor; + + if (mAnimateTiming > 0) + { + float percent = std::abs(mAnimateTiming - mTimer) / mAnimateTiming; + e = Renderer::mixColors(e, mAnimateColor, percent); + } + + const unsigned int edgeColor = e & 0xFFFFFF00 | (unsigned char)((e & 0xFF) * opacity); + Renderer::setMatrix(trans); + + if (mCornerSize.x() > 0) + { + int radius = Math::max(mSize.x(), mSize.y()) * mCornerSize.x(); + Renderer::drawRoundRect(0, 0, mSize.x(), mSize.y(), radius, edgeColor); + } + else + Renderer::drawRect(0.0, 0.0, mSize.x(), mSize.y(), edgeColor, edgeColor); + } + else if (mTexture->bind()) + { + if (mAnimateTiming > 0) + { + float opacity = mOpacity / 255.0; + + unsigned int e = mEdgeColor; + unsigned int c = mCenterColor; + + float percent = std::abs(mAnimateTiming - mTimer) / mAnimateTiming; + e = Renderer::mixColors(e, mAnimateColor, percent); + c = Renderer::mixColors(c, mAnimateColor, percent); + + const unsigned int edgeColor = Renderer::convertColor(e & 0xFFFFFF00 | (unsigned char)((e & 0xFF) * opacity)); + const unsigned int centerColor = Renderer::convertColor(c & 0xFFFFFF00 | (unsigned char)((c & 0xFF) * opacity)); + + for (int i = 0; i < 6 * 9; i++) + mVertices[i].col = edgeColor; + + for (int i = 0; i < 6; i++) + mVertices[(4 * 6) + i].col = centerColor; + } - mTexture->bind(); - Renderer::drawTriangleStrips(&mVertices[0], 6*9); + Renderer::setMatrix(trans); + Renderer::drawTriangleStrips(&mVertices[0], 6 * 9); + Renderer::bindTexture(0); + + updateColors(); } renderChildren(trans); @@ -110,6 +200,10 @@ void NinePatchComponent::render(const Transform4x4f& parentTrans) void NinePatchComponent::onSizeChanged() { + if (mPreviousSize == mSize) + return; + + mPreviousSize = mSize; buildVertices(); } @@ -118,8 +212,11 @@ const Vector2f& NinePatchComponent::getCornerSize() const return mCornerSize; } -void NinePatchComponent::setCornerSize(int sizeX, int sizeY) +void NinePatchComponent::setCornerSize(float sizeX, float sizeY) { + if (mCornerSize.x() == sizeX && mCornerSize.y() == sizeY) + return; + mCornerSize = Vector2f(sizeX, sizeY); buildVertices(); } @@ -132,23 +229,33 @@ void NinePatchComponent::fitTo(Vector2f size, Vector3f position, Vector2f paddin setSize(size + mCornerSize * 2); setPosition(position.x() + Math::lerp(-mCornerSize.x(), mCornerSize.x(), mOrigin.x()), - position.y() + Math::lerp(-mCornerSize.y(), mCornerSize.y(), mOrigin.y())); + position.y() + Math::lerp(-mCornerSize.y(), mCornerSize.y(), mOrigin.y())); } void NinePatchComponent::setImagePath(const std::string& path) { + if (mPath == path) + return; + mPath = path; + mTexture = TextureResource::get(mPath); buildVertices(); } void NinePatchComponent::setEdgeColor(unsigned int edgeColor) { + if (mEdgeColor == edgeColor) + return; + mEdgeColor = edgeColor; updateColors(); } void NinePatchComponent::setCenterColor(unsigned int centerColor) { + if (mCenterColor == centerColor) + return; + mCenterColor = centerColor; updateColors(); } @@ -160,9 +267,33 @@ void NinePatchComponent::applyTheme(const std::shared_ptr& theme, con using namespace ThemeFlags; const ThemeData::ThemeElement* elem = theme->getElement(view, element, "ninepatch"); - if(!elem) + if (!elem) return; - if(properties & PATH && elem->has("path")) + if (properties & PATH && elem->has("path")) setImagePath(elem->get("path")); + + if (properties & COLOR) + { + if (elem->has("color")) + { + setCenterColor(elem->get("color")); + setEdgeColor(elem->get("color")); + } + + if (elem->has("centerColor")) + setCenterColor(elem->get("centerColor")); + + if (elem->has("edgeColor")) + setEdgeColor(elem->get("edgeColor")); + } + + if (elem->has("cornerSize")) + setCornerSize(elem->get("cornerSize")); + + if (elem->has("animateColor")) + setAnimateColor(elem->get("animateColor")); + + if (elem->has("animateColorTime")) + setAnimateTiming(elem->get("animateColorTime")); } diff --git a/es-core/src/components/NinePatchComponent.h b/es-core/src/components/NinePatchComponent.h index 584dc61e3f..cb4ff95d34 100644 --- a/es-core/src/components/NinePatchComponent.h +++ b/es-core/src/components/NinePatchComponent.h @@ -25,6 +25,7 @@ class NinePatchComponent : public GuiComponent virtual ~NinePatchComponent(); void render(const Transform4x4f& parentTrans) override; + void update(int deltaTime) override; void onSizeChanged() override; @@ -37,9 +38,14 @@ class NinePatchComponent : public GuiComponent virtual void applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) override; const Vector2f& getCornerSize() const; - void setCornerSize(int sizeX, int sizeY); + void setCornerSize(float sizeX, float sizeY); inline void setCornerSize(const Vector2f& size) { setCornerSize(size.x(), size.y()); } + virtual void setOpacity(unsigned char opacity); + + void setAnimateColor(unsigned int color) { mAnimateColor = color; }; + void setAnimateTiming(float timing) { mAnimateTiming = timing; }; + private: void buildVertices(); void updateColors(); @@ -51,6 +57,13 @@ class NinePatchComponent : public GuiComponent unsigned int mEdgeColor; unsigned int mCenterColor; std::shared_ptr mTexture; + + Vector2f mPreviousSize; + + + float mTimer; + float mAnimateTiming; + unsigned int mAnimateColor; }; #endif // ES_CORE_COMPONENTS_NINE_PATCH_COMPONENT_H diff --git a/es-core/src/components/OptionListComponent.h b/es-core/src/components/OptionListComponent.h index bf922acdcb..475b46c993 100644 --- a/es-core/src/components/OptionListComponent.h +++ b/es-core/src/components/OptionListComponent.h @@ -1,339 +1,504 @@ -#pragma once -#ifndef ES_CORE_COMPONENTS_OPTION_LIST_COMPONENT_H -#define ES_CORE_COMPONENTS_OPTION_LIST_COMPONENT_H - -#include "GuiComponent.h" -#include "Log.h" -#include "Window.h" - -//Used to display a list of options. -//Can select one or multiple options. - -// if !multiSelect -// * <- curEntry -> - -// always -// * press a -> open full list - -#define CHECKED_PATH ":/checkbox_checked.svg" -#define UNCHECKED_PATH ":/checkbox_unchecked.svg" - -template -class OptionListComponent : public GuiComponent -{ -private: - struct OptionListData - { - std::string name; - T object; - bool selected; - }; - - class OptionListPopup : public GuiComponent - { - private: - MenuComponent mMenu; - OptionListComponent* mParent; - - public: - OptionListPopup(Window* window, OptionListComponent* parent, const std::string& title) : GuiComponent(window), - mMenu(window, title.c_str()), mParent(parent) - { - auto font = Font::get(FONT_SIZE_MEDIUM); - ComponentListRow row; - - // for select all/none - std::vector checkboxes; - - for(auto it = mParent->mEntries.begin(); it != mParent->mEntries.end(); it++) - { - row.elements.clear(); - row.addElement(std::make_shared(mWindow, Utils::String::toUpper(it->name), font, 0x777777FF), true); - - OptionListData& e = *it; - - if(mParent->mMultiSelect) - { - // add checkbox - auto checkbox = std::make_shared(mWindow); - checkbox->setImage(it->selected ? CHECKED_PATH : UNCHECKED_PATH); - checkbox->setResize(0, font->getLetterHeight()); - row.addElement(checkbox, false); - - // input handler - // update checkbox state & selected value - row.makeAcceptInputHandler([this, &e, checkbox] - { - e.selected = !e.selected; - checkbox->setImage(e.selected ? CHECKED_PATH : UNCHECKED_PATH); - mParent->onSelectedChanged(); - }); - - // for select all/none - checkboxes.push_back(checkbox.get()); - }else{ - // input handler for non-multiselect - // update selected value and close - row.makeAcceptInputHandler([this, &e] - { - mParent->mEntries.at(mParent->getSelectedId()).selected = false; - e.selected = true; - mParent->onSelectedChanged(); - delete this; - }); - } - - // also set cursor to this row if we're not multi-select and this row is selected - mMenu.addRow(row, (!mParent->mMultiSelect && it->selected)); - } - - mMenu.addButton("BACK", "accept", [this] { delete this; }); - - if(mParent->mMultiSelect) - { - mMenu.addButton("SELECT ALL", "select all", [this, checkboxes] { - for(unsigned int i = 0; i < mParent->mEntries.size(); i++) - { - mParent->mEntries.at(i).selected = true; - checkboxes.at(i)->setImage(CHECKED_PATH); - } - mParent->onSelectedChanged(); - }); - - mMenu.addButton("SELECT NONE", "select none", [this, checkboxes] { - for(unsigned int i = 0; i < mParent->mEntries.size(); i++) - { - mParent->mEntries.at(i).selected = false; - checkboxes.at(i)->setImage(UNCHECKED_PATH); - } - mParent->onSelectedChanged(); - }); - } - - mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); - addChild(&mMenu); - } - - bool input(InputConfig* config, Input input) override - { - if(config->isMappedTo("b", input) && input.value != 0) - { - delete this; - return true; - } - - return GuiComponent::input(config, input); - } - - std::vector getHelpPrompts() override - { - auto prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", "back")); - return prompts; - } - }; - -public: - OptionListComponent(Window* window, const std::string& name, bool multiSelect = false) : GuiComponent(window), mMultiSelect(multiSelect), mName(name), - mText(window), mLeftArrow(window), mRightArrow(window) - { - auto font = Font::get(FONT_SIZE_MEDIUM, FONT_PATH_LIGHT); - mText.setFont(font); - mText.setColor(0x777777FF); - mText.setHorizontalAlignment(ALIGN_CENTER); - addChild(&mText); - - mLeftArrow.setResize(0, mText.getFont()->getLetterHeight()); - mRightArrow.setResize(0, mText.getFont()->getLetterHeight()); - - if(mMultiSelect) - { - mRightArrow.setImage(":/arrow.svg"); - addChild(&mRightArrow); - }else{ - mLeftArrow.setImage(":/option_arrow.svg"); - mLeftArrow.setFlipX(true); - addChild(&mLeftArrow); - - mRightArrow.setImage(":/option_arrow.svg"); - addChild(&mRightArrow); - } - - setSize(mLeftArrow.getSize().x() + mRightArrow.getSize().x(), font->getHeight()); - } - - // handles positioning/resizing of text and arrows - void onSizeChanged() override - { - mLeftArrow.setResize(0, mText.getFont()->getLetterHeight()); - mRightArrow.setResize(0, mText.getFont()->getLetterHeight()); - - if(mSize.x() < (mLeftArrow.getSize().x() + mRightArrow.getSize().x())) - LOG(LogWarning) << "OptionListComponent too narrow!"; - - mText.setSize(mSize.x() - mLeftArrow.getSize().x() - mRightArrow.getSize().x(), mText.getFont()->getHeight()); - - // position - mLeftArrow.setPosition(0, (mSize.y() - mLeftArrow.getSize().y()) / 2); - mText.setPosition(mLeftArrow.getPosition().x() + mLeftArrow.getSize().x(), (mSize.y() - mText.getSize().y()) / 2); - mRightArrow.setPosition(mText.getPosition().x() + mText.getSize().x(), (mSize.y() - mRightArrow.getSize().y()) / 2); - } - - bool input(InputConfig* config, Input input) override - { - if(input.value != 0) - { - if(config->isMappedTo("a", input)) - { - open(); - return true; - } - if(!mMultiSelect) - { - if(config->isMappedLike("left", input)) - { - // move selection to previous - unsigned int i = getSelectedId(); - int next = (int)i - 1; - if(next < 0) - next += (int)mEntries.size(); - - mEntries.at(i).selected = false; - mEntries.at(next).selected = true; - onSelectedChanged(); - return true; - - }else if(config->isMappedLike("right", input)) - { - // move selection to next - unsigned int i = getSelectedId(); - int next = (i + 1) % mEntries.size(); - mEntries.at(i).selected = false; - mEntries.at(next).selected = true; - onSelectedChanged(); - return true; - - } - } - } - return GuiComponent::input(config, input); - } - - std::vector getSelectedObjects() - { - std::vector ret; - for(auto it = mEntries.cbegin(); it != mEntries.cend(); it++) - { - if(it->selected) - ret.push_back(it->object); - } - - return ret; - } - - T getSelected() - { - assert(mMultiSelect == false); - auto selected = getSelectedObjects(); - assert(selected.size() == 1); - return selected.at(0); - } - - void add(const std::string& name, const T& obj, bool selected) - { - OptionListData e; - e.name = name; - e.object = obj; - e.selected = selected; - - mEntries.push_back(e); - onSelectedChanged(); - } - - void selectAll() - { - for(unsigned int i = 0; i < mEntries.size(); i++) - { - mEntries.at(i).selected = true; - } - onSelectedChanged(); - } - - void selectNone() - { - for(unsigned int i = 0; i < mEntries.size(); i++) - { - mEntries.at(i).selected = false; - } - onSelectedChanged(); - } - -private: - unsigned int getSelectedId() - { - assert(mMultiSelect == false); - for(unsigned int i = 0; i < mEntries.size(); i++) - { - if(mEntries.at(i).selected) - return i; - } - - LOG(LogWarning) << "OptionListComponent::getSelectedId() - no selected element found, defaulting to 0"; - return 0; - } - - void open() - { - mWindow->pushGui(new OptionListPopup(mWindow, this, mName)); - } - - void onSelectedChanged() - { - if(mMultiSelect) - { - // display # selected - std::stringstream ss; - ss << getSelectedObjects().size() << " SELECTED"; - mText.setText(ss.str()); - mText.setSize(0, mText.getSize().y()); - setSize(mText.getSize().x() + mRightArrow.getSize().x() + 24, mText.getSize().y()); - if(mParent) // hack since theres no "on child size changed" callback atm... - mParent->onSizeChanged(); - }else{ - // display currently selected + l/r cursors - for(auto it = mEntries.cbegin(); it != mEntries.cend(); it++) - { - if(it->selected) - { - mText.setText(Utils::String::toUpper(it->name)); - mText.setSize(0, mText.getSize().y()); - setSize(mText.getSize().x() + mLeftArrow.getSize().x() + mRightArrow.getSize().x() + 24, mText.getSize().y()); - if(mParent) // hack since theres no "on child size changed" callback atm... - mParent->onSizeChanged(); - break; - } - } - } - } - - std::vector getHelpPrompts() override - { - std::vector prompts; - if(!mMultiSelect) - prompts.push_back(HelpPrompt("left/right", "change")); - - prompts.push_back(HelpPrompt("a", "select")); - return prompts; - } - - bool mMultiSelect; - - std::string mName; - TextComponent mText; - ImageComponent mLeftArrow; - ImageComponent mRightArrow; - - std::vector mEntries; -}; - -#endif // ES_CORE_COMPONENTS_OPTION_LIST_COMPONENT_H +#pragma once +#ifndef ES_CORE_COMPONENTS_OPTION_LIST_COMPONENT_H +#define ES_CORE_COMPONENTS_OPTION_LIST_COMPONENT_H + +#include "GuiComponent.h" +#include "Log.h" +#include "Window.h" +#include "EsLocale.h" +#include "ThemeData.h" +#include "components/MultiLineMenuEntry.h" + +//Used to display a list of options. +//Can select one or multiple options. + +// if !multiSelect +// * <- curEntry -> + +// always +// * press a -> open full list + +#define CHECKED_PATH ":/checkbox_checked.svg" +#define UNCHECKED_PATH ":/checkbox_unchecked.svg" + +template +class OptionListComponent : public GuiComponent +{ +private: + struct OptionListData + { + std::string name; + std::string description; + + T object; + bool selected; + + std::string group; + }; + + class OptionListPopup : public GuiComponent + { + private: + MenuComponent mMenu; + OptionListComponent* mParent; + + public: + OptionListPopup(Window* window, OptionListComponent* parent, const std::string& title, + const std::function callback = nullptr) : GuiComponent(window), + mMenu(window, title.c_str()), mParent(parent) + { + auto menuTheme = ThemeData::getMenuTheme(); + auto font = menuTheme->Text.font; + auto color = menuTheme->Text.color; + + ComponentListRow row; + + // for select all/none + std::vector checkboxes; + + for(auto it = mParent->mEntries.begin(); it != mParent->mEntries.end(); it++) + { + row.elements.clear(); + + OptionListData& e = *it; + + if (callback != nullptr) + { + callback(e.object, row); + + if (!mParent->mMultiSelect) + { + row.makeAcceptInputHandler([this, &e] + { + e.selected = !e.selected; + mParent->onSelectedChanged(); + }); + } + else + { + row.makeAcceptInputHandler([this, &e] + { + mParent->mEntries.at(mParent->getSelectedId()).selected = false; + e.selected = true; + mParent->onSelectedChanged(); + delete this; + }); + } + } + else + { + if (!it->description.empty()) + row.addElement(std::make_shared(mWindow, Utils::String::toUpper(it->name), it->description), true); + else + row.addElement(std::make_shared(mWindow, Utils::String::toUpper(it->name), font, color), true); + + if (mParent->mMultiSelect) + { + // add checkbox + auto checkbox = std::make_shared(mWindow); + checkbox->setImage(it->selected ? CHECKED_PATH : UNCHECKED_PATH); + checkbox->setResize(0, font->getLetterHeight()); + row.addElement(checkbox, false); + + // input handler + // update checkbox state & selected value + row.makeAcceptInputHandler([this, &e, checkbox] + { + e.selected = !e.selected; + checkbox->setImage(e.selected ? CHECKED_PATH : UNCHECKED_PATH); + mParent->onSelectedChanged(); + }); + + // for select all/none + checkboxes.push_back(checkbox.get()); + } + else { + // input handler for non-multiselect + // update selected value and close + row.makeAcceptInputHandler([this, &e] + { + mParent->mEntries.at(mParent->getSelectedId()).selected = false; + e.selected = true; + mParent->onSelectedChanged(); + delete this; + }); + } + } + + if (!e.group.empty()) + mMenu.addGroup(e.group); + + // also set cursor to this row if we're not multi-select and this row is selected + mMenu.addRow(row, (!mParent->mMultiSelect && it->selected)); + } + + mMenu.addButton(_("BACK"), _("accept"), [this] { delete this; }); + + if(mParent->mMultiSelect) + { + mMenu.addButton(_("SELECT ALL"), _("SELECT ALL"), [this, checkboxes] { + for(unsigned int i = 0; i < mParent->mEntries.size(); i++) + { + mParent->mEntries.at(i).selected = true; + checkboxes.at(i)->setImage(CHECKED_PATH); + } + mParent->onSelectedChanged(); + }); + + mMenu.addButton(_("SELECT NONE"), _("SELECT NONE"), [this, checkboxes] { + for(unsigned int i = 0; i < mParent->mEntries.size(); i++) + { + mParent->mEntries.at(i).selected = false; + checkboxes.at(i)->setImage(UNCHECKED_PATH); + } + mParent->onSelectedChanged(); + }); + } + + mMenu.setPosition( + (Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, + (Renderer::getScreenHeight() - mMenu.getSize().y()) / 2); + //Renderer::getScreenHeight() * 0.15f); + addChild(&mMenu); + } + + bool input(InputConfig* config, Input input) override + { + if(config->isMappedTo("b", input) && input.value != 0) + { + delete this; + return true; + } + + return GuiComponent::input(config, input); + } + + std::vector getHelpPrompts() override + { + auto prompts = mMenu.getHelpPrompts(); + prompts.push_back(HelpPrompt("b", _("BACK"))); + return prompts; + } + }; + +public: + OptionListComponent(Window* window, const std::string& name, bool multiSelect = false) : GuiComponent(window), mMultiSelect(multiSelect), mName(name), + mText(window), mLeftArrow(window), mRightArrow(window) + { + auto theme = ThemeData::getMenuTheme(); + + mAddRowCallback = nullptr; + + mText.setFont(theme->Text.font); + mText.setColor(theme->Text.color); + mText.setHorizontalAlignment(ALIGN_CENTER); + addChild(&mText); + + mLeftArrow.setResize(0, mText.getFont()->getLetterHeight()); + mRightArrow.setResize(0, mText.getFont()->getLetterHeight()); + + if (mMultiSelect) + { + mRightArrow.setImage(ThemeData::getMenuTheme()->Icons.arrow);// ":/arrow.svg"); + mRightArrow.setColorShift(theme->Text.color); + addChild(&mRightArrow); + } else { + mLeftArrow.setImage(ThemeData::getMenuTheme()->Icons.option_arrow); // ":/option_arrow.svg" + mLeftArrow.setColorShift(theme->Text.color); + mLeftArrow.setFlipX(true); + addChild(&mLeftArrow); + + mRightArrow.setImage(ThemeData::getMenuTheme()->Icons.option_arrow); // ":/option_arrow.svg"); + mRightArrow.setColorShift(theme->Text.color); + addChild(&mRightArrow); + } + + setSize(mLeftArrow.getSize().x() + mRightArrow.getSize().x(), theme->Text.font->getHeight()); + } + + + virtual void setColor(unsigned int color) + { + mText.setColor(color); + mLeftArrow.setColorShift(color); + mRightArrow.setColorShift(color); + } + + // handles positioning/resizing of text and arrows + void onSizeChanged() override + { + mLeftArrow.setResize(0, mText.getFont()->getLetterHeight()); + mRightArrow.setResize(0, mText.getFont()->getLetterHeight()); + + if(mSize.x() < (mLeftArrow.getSize().x() + mRightArrow.getSize().x())) + LOG(LogWarning) << "OptionListComponent too narrow!"; + + mText.setSize(mSize.x() - mLeftArrow.getSize().x() - mRightArrow.getSize().x(), mText.getFont()->getHeight()); + + // position + mLeftArrow.setPosition(0, (mSize.y() - mLeftArrow.getSize().y()) / 2); + mText.setPosition(mLeftArrow.getPosition().x() + mLeftArrow.getSize().x(), (mSize.y() - mText.getSize().y()) / 2); + mRightArrow.setPosition(mText.getPosition().x() + mText.getSize().x(), (mSize.y() - mRightArrow.getSize().y()) / 2); + } + + bool input(InputConfig* config, Input input) override + { + if(input.value != 0) + { + if(config->isMappedTo("a", input)) + { + open(); + return true; + } + if(!mMultiSelect) + { + if(config->isMappedLike("left", input)) + { + // move selection to previous + unsigned int i = getSelectedId(); + int next = (int)i - 1; + if(next < 0) + next += (int)mEntries.size(); + + mEntries.at(i).selected = false; + mEntries.at(next).selected = true; + onSelectedChanged(); + return true; + + }else if(config->isMappedLike("right", input)) + { + if (mEntries.size() == 0) + return true; + + // move selection to next + unsigned int i = getSelectedId(); + int next = (i + 1) % mEntries.size(); + mEntries.at(i).selected = false; + mEntries.at(next).selected = true; + onSelectedChanged(); + return true; + + } + } + } + return GuiComponent::input(config, input); + } + + std::vector getSelectedObjects() + { + std::vector ret; + for(auto it = mEntries.cbegin(); it != mEntries.cend(); it++) + { + if(it->selected) + ret.push_back(it->object); + } + + return ret; + } + + T getSelected() + { + assert(mMultiSelect == false); + auto selected = getSelectedObjects(); + assert(selected.size() == 1); + return selected.at(0); + } + + void addEx(const std::string name, const std::string description, const T& obj, bool selected) + { + for (auto sysIt = mEntries.cbegin(); sysIt != mEntries.cend(); sysIt++) + if (sysIt->name == name) + return; + + OptionListData e; + e.name = name; + e.description = description; + e.object = obj; + e.selected = selected; + + e.group = mGroup; + mGroup = ""; + + mEntries.push_back(e); + onSelectedChanged(); + } + + void add(const std::string& name, const T& obj, bool selected) + { + for (auto sysIt = mEntries.cbegin(); sysIt != mEntries.cend(); sysIt++) + if (sysIt->name == name) + return; + + OptionListData e; + e.name = name; + e.object = obj; + e.selected = selected; + e.group = mGroup; + mGroup = ""; + + mEntries.push_back(e); + onSelectedChanged(); + } + + void addGroup(const std::string name) + { + mGroup = name; + } + + void remove(const std::string& name) + { + for (auto sysIt = mEntries.cbegin(); sysIt != mEntries.cend(); sysIt++) + { + if (sysIt->name == name) + { + bool isSelect = sysIt->selected; + + mEntries.erase(sysIt); + + if (isSelect) + selectFirstItem(); + + break; + } + } + } + + void selectAll() + { + for(unsigned int i = 0; i < mEntries.size(); i++) + { + mEntries.at(i).selected = true; + } + onSelectedChanged(); + } + + void selectNone() + { + for(unsigned int i = 0; i < mEntries.size(); i++) + { + mEntries.at(i).selected = false; + } + onSelectedChanged(); + } + + bool hasSelection() + { + for (unsigned int i = 0; i < mEntries.size(); i++) + if (mEntries.at(i).selected) + return true; + + return false; + } + + void selectFirstItem() + { + for (unsigned int i = 0; i < mEntries.size(); i++) + mEntries.at(i).selected = false; + + if (mEntries.size() > 0) + mEntries.at(0).selected = true; + + onSelectedChanged(); + } + + void clear() { + mEntries.clear(); + } + + inline void invalidate() { + onSelectedChanged(); + } + + void setSelectedChangedCallback(const std::function& callback) + { + mSelectedChangedCallback = callback; + } + + void setRowTemplate(std::function callback) + { + mAddRowCallback = callback; + } + +private: + std::function mAddRowCallback; + + void open() + { + mWindow->pushGui(new OptionListPopup(mWindow, this, mName, mAddRowCallback)); + } + + unsigned int getSelectedId() + { + assert(mMultiSelect == false); + for(unsigned int i = 0; i < mEntries.size(); i++) + { + if(mEntries.at(i).selected) + return i; + } + + LOG(LogWarning) << "OptionListComponent::getSelectedId() - no selected element found, defaulting to 0"; + return 0; + } + + void onSelectedChanged() + { + if(mMultiSelect) + { + // display # selected + + + + char strbuf[256]; + int x = getSelectedObjects().size(); + snprintf(strbuf, 256, EsLocale::nGetText("%i SELECTED", "%i SELECTED", x).c_str(), x); + mText.setText(strbuf); + + + mText.setSize(0, mText.getSize().y()); + setSize(mText.getSize().x() + mRightArrow.getSize().x() + 24, mText.getSize().y()); + if(mParent) // hack since theres no "on child size changed" callback atm... + mParent->onSizeChanged(); + }else{ + // display currently selected + l/r cursors + for(auto it = mEntries.cbegin(); it != mEntries.cend(); it++) + { + if (it->selected) + { + mText.setText(Utils::String::toUpper(it->name)); + mText.setSize(0, mText.getSize().y()); + setSize(mText.getSize().x() + mLeftArrow.getSize().x() + mRightArrow.getSize().x() + 24, mText.getSize().y()); + if (mParent) // hack since theres no "on child size changed" callback atm... + mParent->onSizeChanged(); + break; + } + } + } + + if (mSelectedChangedCallback) + mSelectedChangedCallback(mEntries.at(getSelectedId()).object); + } + + std::vector getHelpPrompts() override + { + std::vector prompts; + if(!mMultiSelect) + prompts.push_back(HelpPrompt("left/right", "MODIFIER")); + + prompts.push_back(HelpPrompt("a", "SELECTIONNER")); + return prompts; + } + + bool mMultiSelect; + + std::string mName; + std::string mGroup; + + TextComponent mText; + ImageComponent mLeftArrow; + ImageComponent mRightArrow; + + std::vector mEntries; + std::function mSelectedChangedCallback; +}; + +#endif // ES_CORE_COMPONENTS_OPTION_LIST_COMPONENT_H diff --git a/es-core/src/components/ScrollableContainer.cpp b/es-core/src/components/ScrollableContainer.cpp index 5157b977bf..ac95c979ca 100644 --- a/es-core/src/components/ScrollableContainer.cpp +++ b/es-core/src/components/ScrollableContainer.cpp @@ -4,7 +4,7 @@ #include "renderers/Renderer.h" #define AUTO_SCROLL_RESET_DELAY 3000 // ms to reset to top after we reach the bottom -#define AUTO_SCROLL_DELAY 1000 // ms to wait before we start to scroll +#define AUTO_SCROLL_DELAY 3000 // ms to wait before we start to scroll #define AUTO_SCROLL_SPEED 50 // ms between scrolls ScrollableContainer::ScrollableContainer(Window* window) : GuiComponent(window), diff --git a/es-core/src/components/SliderComponent.cpp b/es-core/src/components/SliderComponent.cpp index aba8174c45..a1279db469 100644 --- a/es-core/src/components/SliderComponent.cpp +++ b/es-core/src/components/SliderComponent.cpp @@ -1,7 +1,6 @@ #include "components/SliderComponent.h" #include "resources/Font.h" - #define MOVE_REPEAT_DELAY 500 #define MOVE_REPEAT_RATE 40 @@ -13,10 +12,20 @@ SliderComponent::SliderComponent(Window* window, float min, float max, float inc // some sane default value mValue = (max + min) / 2; + auto menuTheme = ThemeData::getMenuTheme(); + mColor = menuTheme->Text.color; + mKnob.setOrigin(0.5f, 0.5f); - mKnob.setImage(":/slider_knob.svg"); + mKnob.setImage(ThemeData::getMenuTheme()->Icons.knob); // ":/slider_knob.svg"); + mKnob.setColorShift(mColor); + + setSize(Renderer::getScreenWidth() * 0.15f, menuTheme->Text.font->getLetterHeight()); +} - setSize(Renderer::getScreenWidth() * 0.15f, Font::get(FONT_SIZE_MEDIUM)->getLetterHeight()); +void SliderComponent::setColor(unsigned int color) { + mColor = color; + mKnob.setColorShift(mColor); + onValueChanged(); } bool SliderComponent::input(InputConfig* config, Input input) @@ -28,7 +37,7 @@ bool SliderComponent::input(InputConfig* config, Input input) mMoveRate = input.value ? -mSingleIncrement : 0; mMoveAccumulator = -MOVE_REPEAT_DELAY; - return true; + return input.value; } if(config->isMappedLike("right", input)) { @@ -37,7 +46,7 @@ bool SliderComponent::input(InputConfig* config, Input input) mMoveRate = input.value ? mSingleIncrement : 0; mMoveAccumulator = -MOVE_REPEAT_DELAY; - return true; + return input.value; } return GuiComponent::input(config, input); @@ -54,7 +63,7 @@ void SliderComponent::update(int deltaTime) mMoveAccumulator -= MOVE_REPEAT_RATE; } } - + GuiComponent::update(deltaTime); } @@ -71,16 +80,19 @@ void SliderComponent::render(const Transform4x4f& parentTrans) //render line const float lineWidth = 2; - Renderer::drawRect(mKnob.getSize().x() / 2, mSize.y() / 2 - lineWidth / 2, width, lineWidth, 0x777777FF, 0x777777FF); + Renderer::drawRect(mKnob.getSize().x() / 2, mSize.y() / 2 - lineWidth / 2, width, lineWidth, mColor); //render knob mKnob.render(trans); - + GuiComponent::renderChildren(trans); } void SliderComponent::setValue(float value) { + if (mValue == value) + return; + mValue = value; if(mValue < mMin) mValue = mMin; @@ -88,6 +100,9 @@ void SliderComponent::setValue(float value) mValue = mMax; onValueChanged(); + + if (mValueChanged) + mValueChanged(mValue); } float SliderComponent::getValue() @@ -99,14 +114,14 @@ void SliderComponent::onSizeChanged() { if(!mSuffix.empty()) mFont = Font::get((int)(mSize.y()), FONT_PATH_LIGHT); - + onValueChanged(); } void SliderComponent::onValueChanged() { // update suffix textcache - if(mFont) + if (mFont) { std::stringstream ss; ss << std::fixed; @@ -124,7 +139,7 @@ void SliderComponent::onValueChanged() const std::string max = ss.str(); Vector2f textSize = mFont->sizeText(max); - mValueCache = std::shared_ptr(mFont->buildTextCache(val, mSize.x() - textSize.x(), (mSize.y() - textSize.y()) / 2, 0x777777FF)); + mValueCache = std::shared_ptr(mFont->buildTextCache(val, mSize.x() - textSize.x(), (mSize.y() - textSize.y()) / 2, mColor)); mValueCache->metrics.size[0] = textSize.x(); // fudge the width } @@ -132,11 +147,12 @@ void SliderComponent::onValueChanged() mKnob.setResize(0, mSize.y() * 0.7f); float lineLength = mSize.x() - mKnob.getSize().x() - (mValueCache ? mValueCache->metrics.size.x() + 4 : 0); mKnob.setPosition(((mValue + mMin) / mMax) * lineLength + mKnob.getSize().x()/2, mSize.y() / 2); + } std::vector SliderComponent::getHelpPrompts() { std::vector prompts; - prompts.push_back(HelpPrompt("left/right", "change")); + prompts.push_back(HelpPrompt("left/right", _("CHANGE"))); return prompts; } diff --git a/es-core/src/components/SliderComponent.h b/es-core/src/components/SliderComponent.h index f3ae181fce..d66b00b395 100644 --- a/es-core/src/components/SliderComponent.h +++ b/es-core/src/components/SliderComponent.h @@ -25,6 +25,9 @@ class SliderComponent : public GuiComponent void onSizeChanged() override; virtual std::vector getHelpPrompts() override; + virtual void setColor(unsigned int color); + + inline void setOnValueChanged(const std::function& callback) { mValueChanged = callback; } private: void onValueChanged(); @@ -40,6 +43,9 @@ class SliderComponent : public GuiComponent std::string mSuffix; std::shared_ptr mFont; std::shared_ptr mValueCache; + + unsigned int mColor; + std::function mValueChanged; }; #endif // ES_CORE_COMPONENTS_SLIDER_COMPONENT_H diff --git a/es-core/src/components/SwitchComponent.cpp b/es-core/src/components/SwitchComponent.cpp index ac413c2fc7..ce1fe892d6 100644 --- a/es-core/src/components/SwitchComponent.cpp +++ b/es-core/src/components/SwitchComponent.cpp @@ -4,11 +4,21 @@ SwitchComponent::SwitchComponent(Window* window, bool state) : GuiComponent(window), mImage(window), mState(state) { - mImage.setImage(":/off.svg"); - mImage.setResize(0, Font::get(FONT_SIZE_MEDIUM)->getLetterHeight()); + auto menuTheme = ThemeData::getMenuTheme(); + + float height = Font::get(FONT_SIZE_MEDIUM)->getLetterHeight(); + + mImage.setImage(ThemeData::getMenuTheme()->Icons.off); + mImage.setResize(0, height); + mImage.setColorShift(menuTheme->Text.color); + mSize = mImage.getSize(); } +void SwitchComponent::setColor(unsigned int color) { + mImage.setColorShift(color); +} + void SwitchComponent::onSizeChanged() { mImage.setSize(mSize); @@ -65,12 +75,16 @@ void SwitchComponent::setValue(const std::string& statestring) void SwitchComponent::onStateChanged() { - mImage.setImage(mState ? ":/on.svg" : ":/off.svg"); + auto theme = ThemeData::getMenuTheme(); + mImage.setImage(mState ? theme->Icons.on : theme->Icons.off); + + if (mOnChangedCallback != nullptr) + mOnChangedCallback(); } std::vector SwitchComponent::getHelpPrompts() { std::vector prompts; - prompts.push_back(HelpPrompt("a", "change")); + prompts.push_back(HelpPrompt("a", "MODIFIER")); return prompts; } diff --git a/es-core/src/components/SwitchComponent.h b/es-core/src/components/SwitchComponent.h index 2e2d83f6b4..d77525e06f 100644 --- a/es-core/src/components/SwitchComponent.h +++ b/es-core/src/components/SwitchComponent.h @@ -23,11 +23,19 @@ class SwitchComponent : public GuiComponent virtual std::vector getHelpPrompts() override; + void setColor(unsigned int color); + + inline void setOnChangedCallback(const std::function& callback) { + mOnChangedCallback = callback; + } + private: void onStateChanged(); ImageComponent mImage; bool mState; + + std::function mOnChangedCallback; // batocera }; #endif // ES_CORE_COMPONENTS_SWITCH_COMPONENT_H diff --git a/es-core/src/components/TextComponent.cpp b/es-core/src/components/TextComponent.cpp index bd8b7cdc0d..1eaf4659c4 100644 --- a/es-core/src/components/TextComponent.cpp +++ b/es-core/src/components/TextComponent.cpp @@ -7,15 +7,23 @@ TextComponent::TextComponent(Window* window) : GuiComponent(window), mFont(Font::get(FONT_SIZE_MEDIUM)), mUppercase(false), mColor(0x000000FF), mAutoCalcExtent(true, true), mHorizontalAlignment(ALIGN_LEFT), mVerticalAlignment(ALIGN_CENTER), mLineSpacing(1.5f), mBgColor(0), - mRenderBackground(false) -{ + mRenderBackground(false), mGlowColor(0), mGlowSize(2), mPadding(Vector4f(0, 0, 0, 0)), mGlowOffset(Vector2f(0, 0)), + mReflection(0.0f, 0.0f), mReflectOnBorders(false) +{ + mMarqueeOffset = 0; + mMarqueeOffset2 = 0; + mMarqueeTime = 0; + + mAutoScroll = false; + } TextComponent::TextComponent(Window* window, const std::string& text, const std::shared_ptr& font, unsigned int color, Alignment align, Vector3f pos, Vector2f size, unsigned int bgcolor) : GuiComponent(window), mFont(NULL), mUppercase(false), mColor(0x000000FF), mAutoCalcExtent(true, true), mHorizontalAlignment(align), mVerticalAlignment(ALIGN_CENTER), mLineSpacing(1.5f), mBgColor(0), - mRenderBackground(false) + mRenderBackground(false), mGlowColor(0), mGlowSize(2), mPadding(Vector4f(0, 0, 0, 0)), mGlowOffset(Vector2f(0, 0)), + mReflection(0.0f, 0.0f), mReflectOnBorders(false) { setFont(font); setColor(color); @@ -23,6 +31,12 @@ TextComponent::TextComponent(Window* window, const std::string& text, const std: setText(text); setPosition(pos); setSize(size); + + mMarqueeOffset = 0; + mMarqueeOffset2 = 0; + mMarqueeTime = 0; + + mAutoScroll = false; } void TextComponent::onSizeChanged() @@ -37,11 +51,27 @@ void TextComponent::setFont(const std::shared_ptr& font) onTextChanged(); } +void TextComponent::setFont(std::string path, int size) +{ + std::shared_ptr font; + int fontSize = size > 0 ? size : (mFont ? mFont->getSize() : FONT_SIZE_MEDIUM); + std::string fontPath = !path.empty() ? path : (mFont ? mFont->getPath() : Font::getDefaultPath()); + + font = Font::get(fontSize, fontPath); + if (mFont != font) + { + mFont = font; + onTextChanged(); + } +} + // Set the color of the font/text void TextComponent::setColor(unsigned int color) { + if (mColor == color) + return; + mColor = color; - mColorOpacity = mColor & 0x000000FF; onColorChanged(); } @@ -49,7 +79,6 @@ void TextComponent::setColor(unsigned int color) void TextComponent::setBackgroundColor(unsigned int color) { mBgColor = color; - mBgColorOpacity = mBgColor & 0x000000FF; } void TextComponent::setRenderBackground(bool render) @@ -60,29 +89,23 @@ void TextComponent::setRenderBackground(bool render) // Scale the opacity void TextComponent::setOpacity(unsigned char opacity) { - // This method is mostly called to do fading in-out of the Text component element. - // Therefore, we assume here that opacity is a fractional value (expressed as an int 0-255), - // of the opacity originally set with setColor() or setBackgroundColor(). - - unsigned char o = (unsigned char)((float)opacity / 255.f * (float) mColorOpacity); - mColor = (mColor & 0xFFFFFF00) | (unsigned char) o; - - unsigned char bgo = (unsigned char)((float)opacity / 255.f * (float)mBgColorOpacity); - mBgColor = (mBgColor & 0xFFFFFF00) | (unsigned char)bgo; + if (opacity == mOpacity) + return; + mOpacity = opacity; onColorChanged(); - - GuiComponent::setOpacity(opacity); -} - -unsigned char TextComponent::getOpacity() const -{ - return mColor & 0x000000FF; } void TextComponent::setText(const std::string& text) { + if (mText == text) + return; + mText = text; + mMarqueeOffset = 0; + mMarqueeOffset2 = 0; + mMarqueeTime = 0; + onTextChanged(); } @@ -92,6 +115,46 @@ void TextComponent::setUppercase(bool uppercase) onTextChanged(); } +void TextComponent::renderSingleGlow(const Transform4x4f& parentTrans, float yOff, float x, float y) +{ + Vector3f off = Vector3f(mPadding.x() + x + mGlowOffset.x(), mPadding.y() + yOff + y + mGlowOffset.y(), 0); + Transform4x4f trans = parentTrans * getTransform(); + + trans.translate(off); + trans.round(); + + Renderer::setMatrix(trans); + + unsigned char alpha = (unsigned char)((mGlowColor & 0xFF) * (mOpacity / 255.0)); + unsigned int color = (mGlowColor & 0xFFFFFF00) | alpha; + + mTextCache->setColor(color); + mFont->renderTextCache(mTextCache.get()); +} + +void TextComponent::renderGlow(const Transform4x4f& parentTrans, float yOff, float xOff) +{ + Transform4x4f glowTrans = parentTrans; + if (xOff != 0.0) + glowTrans.translate(Vector3f(xOff, 0, 0)); + + int x = -mGlowSize; + int y = -mGlowSize; + renderSingleGlow(glowTrans, yOff, x, y); + + for (int i = 0; i < 2 * mGlowSize; i++) + renderSingleGlow(glowTrans, yOff, ++x, y); + + for (int i = 0; i < 2 * mGlowSize; i++) + renderSingleGlow(glowTrans, yOff, x, ++y); + + for (int i = 0; i < 2 * mGlowSize; i++) + renderSingleGlow(glowTrans, yOff, --x, y); + + for (int i = 0; i < 2 * mGlowSize; i++) + renderSingleGlow(glowTrans, yOff, x, --y); +} + void TextComponent::render(const Transform4x4f& parentTrans) { if (!isVisible()) @@ -99,44 +162,65 @@ void TextComponent::render(const Transform4x4f& parentTrans) Transform4x4f trans = parentTrans * getTransform(); + if (!Renderer::isVisibleOnScreen(trans.translation().x(), trans.translation().y(), mSize.x(), mSize.y())) + return; + if (mRenderBackground) { Renderer::setMatrix(trans); - Renderer::drawRect(0.0f, 0.0f, mSize.x(), mSize.y(), mBgColor, mBgColor); + + auto bgColor = mBgColor & 0xFFFFFF00 | (unsigned char)((mBgColor & 0xFF) * (mOpacity / 255.0)); + Renderer::drawRect(0.0f, 0.0f, mSize.x(), mSize.y(), bgColor, bgColor); } - if(mTextCache) + if (mAutoScroll) + Renderer::pushClipRect(Vector2i(trans.translation().x(), trans.translation().y()), Vector2i(mSize.x(), mSize.y())); + + if (mTextCache && mFont) { const Vector2f& textSize = mTextCache->metrics.size; float yOff = 0; - switch(mVerticalAlignment) + switch (mVerticalAlignment) { - case ALIGN_TOP: - yOff = 0; - break; - case ALIGN_BOTTOM: - yOff = (getSize().y() - textSize.y()); - break; - case ALIGN_CENTER: - yOff = (getSize().y() - textSize.y()) / 2.0f; - break; + case ALIGN_TOP: + yOff = 0; + break; + case ALIGN_BOTTOM: + yOff = (getSize().y() - textSize.y()); + break; + case ALIGN_CENTER: + yOff = (getSize().y() - textSize.y()) / 2.0f; + break; } - Vector3f off(0, yOff, 0); + Vector3f off(mPadding.x(), mPadding.y() + yOff, 0); - if(Settings::getInstance()->getBool("DebugText")) + if (Settings::getInstance()->getBool("DebugText")) { // draw the "textbox" area, what we are aligned within Renderer::setMatrix(trans); Renderer::drawRect(0.0f, 0.0f, mSize.x(), mSize.y(), 0xFF000033, 0xFF000033); } - trans.translate(off); + if ((mGlowColor & 0x000000FF) != 0 && mGlowSize > 0) + { + renderGlow(parentTrans, yOff, -mMarqueeOffset); + onColorChanged(); + } + + Transform4x4f drawTrans = trans; + + if (mMarqueeOffset != 0.0) + trans.translate(off - Vector3f((float)mMarqueeOffset, 0, 0)); + else + trans.translate(off); + +// trans.translate(off); Renderer::setMatrix(trans); // draw the text area, where the text actually is going - if(Settings::getInstance()->getBool("DebugText")) + if (Settings::getInstance()->getBool("DebugText")) { - switch(mHorizontalAlignment) + switch (mHorizontalAlignment) { case ALIGN_LEFT: Renderer::drawRect(0.0f, 0.0f, mTextCache->metrics.size.x(), mTextCache->metrics.size.y(), 0x00000033, 0x00000033); @@ -149,17 +233,67 @@ void TextComponent::render(const Transform4x4f& parentTrans) break; } } + mFont->renderTextCache(mTextCache.get()); + + // render currently selected item text again if + // marquee is scrolled far enough for it to repeat + + if (mMarqueeOffset2 != 0.0) + { + trans = drawTrans; + trans.translate(off - Vector3f((float)mMarqueeOffset2, 0, 0)); + + if ((mGlowColor & 0x000000FF) != 0 && mGlowSize > 0) + { + renderGlow(parentTrans, yOff, -mMarqueeOffset2); + onColorChanged(); + } + + Renderer::setMatrix(trans); + mFont->renderTextCache(mTextCache.get()); + Renderer::setMatrix(drawTrans); + } + + if (mReflection.x() != 0 || mReflection.y() != 0) + { + Transform4x4f mirror = trans; + mirror.translate(-off); + mirror.r1().y() = -mirror.r1().y(); + mirror.r3().y() = mirror.r3().y() + off.y() + textSize.y(); + + if (mReflectOnBorders) + mirror.r3().y() = mirror.r3().y() + mSize.y(); + else + mirror.r3().y() = mirror.r3().y() + textSize.y(); + + Renderer::setMatrix(mirror); + + float baseOpacity = mOpacity / 255.0; + float alpha = baseOpacity * ((mColor & 0x000000ff)) / 255.0; + float alpha2 = baseOpacity * alpha * mReflection.y(); + + alpha *= mReflection.x(); + + const unsigned int colorT = Renderer::convertColor((mColor & 0xffffff00) + (unsigned char)(255.0*alpha)); + const unsigned int colorB = Renderer::convertColor((mColor & 0xffffff00) + (unsigned char)(255.0*alpha2)); + + mFont->renderGradientTextCache(mTextCache.get(), colorB, colorT); + } + + if (mAutoScroll) + Renderer::popClipRect(); } } void TextComponent::calculateExtent() { - if(mAutoCalcExtent.x()) + if (mAutoCalcExtent.x()) { mSize = mFont->sizeText(mUppercase ? Utils::String::toUpper(mText) : mText, mLineSpacing); - }else{ - if(mAutoCalcExtent.y()) + } + else { + if (mAutoCalcExtent.y()) { mSize[1] = mFont->sizeWrappedText(mUppercase ? Utils::String::toUpper(mText) : mText, getSize().x(), mLineSpacing).y(); } @@ -170,57 +304,118 @@ void TextComponent::onTextChanged() { calculateExtent(); - if(!mFont || mText.empty()) + if (!mFont || mText.empty()) { mTextCache.reset(); return; } + int sx = mSize.x() - mPadding.x() - mPadding.z(); + int sy = mSize.y() - mPadding.y() - mPadding.w(); + std::string text = mUppercase ? Utils::String::toUpper(mText) : mText; std::shared_ptr f = mFont; - const bool isMultiline = (mSize.y() == 0 || mSize.y() > f->getHeight()*1.2f); + const bool isMultiline = !mAutoScroll && (mSize.y() == 0 || sy > f->getHeight()*1.95f); bool addAbbrev = false; - if(!isMultiline) + if (!isMultiline) { size_t newline = text.find('\n'); text = text.substr(0, newline); // single line of text - stop at the first newline since it'll mess everything up addAbbrev = newline != std::string::npos; } + auto color = mColor & 0xFFFFFF00 | (unsigned char)((mColor & 0xFF) * (mOpacity / 255.0)); + Vector2f size = f->sizeText(text); - if(!isMultiline && mSize.x() && text.size() && (size.x() > mSize.x() || addAbbrev)) + if (!isMultiline) { - // abbreviate text - const std::string abbrev = "..."; - Vector2f abbrevSize = f->sizeText(abbrev); - - while(text.size() && size.x() + abbrevSize.x() > mSize.x()) + if (sx && text.size() && (size.x() > sx || addAbbrev) && !mAutoScroll) { - size_t newSize = Utils::String::prevCursor(text, text.size()); - text.erase(newSize, text.size() - newSize); - size = f->sizeText(text); + // abbreviate text + const std::string abbrev = "..."; + Vector2f abbrevSize = f->sizeText(abbrev); + + while (text.size() && size.x() + abbrevSize.x() > sx) + { + size_t newSize = Utils::String::prevCursor(text, text.size()); + text.erase(newSize, text.size() - newSize); + size = f->sizeText(text); + } + + text.append(abbrev); } - text.append(abbrev); + mTextCache = std::shared_ptr(f->buildTextCache(text, Vector2f(0, 0), color, sx, mHorizontalAlignment, mLineSpacing)); + } + else + mTextCache = std::shared_ptr(f->buildTextCache(f->wrapText(text, sx), Vector2f(0, 0), color, sx, mHorizontalAlignment, mLineSpacing)); +} + +void TextComponent::update(int deltaTime) +{ + GuiComponent::update(deltaTime); + + int sy = mSize.y() - mPadding.y() - mPadding.w(); + const bool isMultiline = !mAutoScroll && (mSize.y() == 0 || sy > mFont->getHeight()*1.95f); + + if (mAutoScroll && !isMultiline && mSize.x() > 0) + { + // always reset the marquee offsets + mMarqueeOffset = 0; + mMarqueeOffset2 = 0; - mTextCache = std::shared_ptr(f->buildTextCache(text, Vector2f(0, 0), (mColor >> 8 << 8) | mOpacity, mSize.x(), mHorizontalAlignment, mLineSpacing)); - }else{ - mTextCache = std::shared_ptr(f->buildTextCache(f->wrapText(text, mSize.x()), Vector2f(0, 0), (mColor >> 8 << 8) | mOpacity, mSize.x(), mHorizontalAlignment, mLineSpacing)); + std::string text = mUppercase ? Utils::String::toUpper(mText) : mText; + + // if we're not scrolling and this object's text goes outside our size, marquee it! + const float textLength = mFont->sizeText(text).x(); + const float limit = mSize.x() - mPadding.x() - mPadding.z(); + + if (textLength > limit) + { + // loop + // pixels per second ( based on nes-mini font at 1920x1080 to produce a speed of 200 ) + const float speed = mFont->sizeText("ABCDEFGHIJKLMNOPQRSTUVWXYZ").x() * 0.247f; + const float delay = 1000; + const float scrollLength = textLength; + const float returnLength = speed * 1.5f; + const float scrollTime = (scrollLength * 1000) / speed; + const float returnTime = (returnLength * 1000) / speed; + const int maxTime = (int)(delay + scrollTime + returnTime); + + mMarqueeTime += deltaTime; + while (mMarqueeTime > maxTime) + mMarqueeTime -= maxTime; + + mMarqueeOffset = (int)(Math::Scroll::loop(delay, scrollTime + returnTime, (float)mMarqueeTime, scrollLength + returnLength)); + + if (mMarqueeOffset > (scrollLength - (limit - returnLength))) + mMarqueeOffset2 = (int)(mMarqueeOffset - (scrollLength + returnLength)); + } + } + else + { + mMarqueeTime = 0; + mMarqueeOffset = 0; + mMarqueeOffset2 = 0; } } void TextComponent::onColorChanged() { - if(mTextCache) + if (mTextCache) { - mTextCache->setColor(mColor); + auto color = mColor & 0xFFFFFF00 | (unsigned char)((mColor & 0xFF) * (mOpacity / 255.0)); + mTextCache->setColor(color); } } void TextComponent::setHorizontalAlignment(Alignment align) { + if (mHorizontalAlignment == align) + return; + mHorizontalAlignment = align; onTextChanged(); } @@ -253,39 +448,103 @@ void TextComponent::applyTheme(const std::shared_ptr& theme, const st using namespace ThemeFlags; const ThemeData::ThemeElement* elem = theme->getElement(view, element, "text"); - if(!elem) + if (!elem) return; - if (properties & COLOR && elem->has("color")) - setColor(elem->get("color")); + if (properties & ALIGNMENT) + { + if (elem->has("alignment")) + { + std::string str = elem->get("alignment"); + if (str == "left") + setHorizontalAlignment(ALIGN_LEFT); + else if (str == "center") + setHorizontalAlignment(ALIGN_CENTER); + else if (str == "right") + setHorizontalAlignment(ALIGN_RIGHT); + else + LOG(LogError) << "Unknown text alignment string: " << str; + } - setRenderBackground(false); - if (properties & COLOR && elem->has("backgroundColor")) { - setBackgroundColor(elem->get("backgroundColor")); - setRenderBackground(true); - } + if (elem->has("verticalAlignment")) + { + std::string str = elem->get("verticalAlignment"); + if (str == "top") + setVerticalAlignment(ALIGN_TOP); + else if (str == "center") + setVerticalAlignment(ALIGN_CENTER); + else if (str == "bottom") + setVerticalAlignment(ALIGN_BOTTOM); + else + LOG(LogError) << "Unknown text alignment string: " << str; + } - if(properties & ALIGNMENT && elem->has("alignment")) - { - std::string str = elem->get("alignment"); - if(str == "left") - setHorizontalAlignment(ALIGN_LEFT); - else if(str == "center") - setHorizontalAlignment(ALIGN_CENTER); - else if(str == "right") - setHorizontalAlignment(ALIGN_RIGHT); + if (elem->has("padding")) + { + Vector2f scale = getParent() ? getParent()->getSize() : Vector2f((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); + mPadding = elem->get("padding") * Vector4f(scale.x(), scale.y(), scale.x(), scale.y()); + } else - LOG(LogError) << "Unknown text alignment string: " << str; + mPadding = Vector4f::Zero(); } - if(properties & TEXT && elem->has("text")) + if (properties & TEXT && elem->has("text")) setText(elem->get("text")); - if(properties & FORCE_UPPERCASE && elem->has("forceUppercase")) + if (properties & FORCE_UPPERCASE && elem->has("forceUppercase")) setUppercase(elem->get("forceUppercase")); - if(properties & LINE_SPACING && elem->has("lineSpacing")) + if (properties & LINE_SPACING && elem->has("lineSpacing")) setLineSpacing(elem->get("lineSpacing")); + if (properties & COLOR) + { + if (elem->has("color")) + setColor(elem->get("color")); + + if (elem->has("backgroundColor")) + { + setBackgroundColor(elem->get("backgroundColor")); + setRenderBackground(true); + } + else + setRenderBackground(false); + + if (elem->has("glowColor")) + mGlowColor = elem->get("glowColor"); + else + mGlowColor = 0; + + if (elem->has("glowSize")) + mGlowSize = (int)elem->get("glowSize"); + + if (elem->has("glowOffset")) + mGlowOffset = elem->get("glowOffset"); + + if (elem->has("reflexion")) + mReflection = elem->get("reflexion"); + else + mReflection = Vector2f::Zero(); + + if (elem->has("reflexionOnFrame")) + mReflectOnBorders = elem->get("reflexionOnFrame"); + else + mReflectOnBorders = false; + + if (elem->has("singleLineScroll")) + mAutoScroll = elem->get("singleLineScroll"); + else + mAutoScroll = false; + } + setFont(Font::getFromTheme(elem, properties, mFont)); } + +void TextComponent::setAutoScroll(bool value) +{ + if (mAutoScroll == value) + return; + + mAutoScroll = value; + onTextChanged(); +} \ No newline at end of file diff --git a/es-core/src/components/TextComponent.h b/es-core/src/components/TextComponent.h index 1721105772..4229c9eaa6 100644 --- a/es-core/src/components/TextComponent.h +++ b/es-core/src/components/TextComponent.h @@ -20,8 +20,10 @@ class TextComponent : public GuiComponent Vector3f pos = Vector3f::Zero(), Vector2f size = Vector2f::Zero(), unsigned int bgcolor = 0x00000000); void setFont(const std::shared_ptr& font); + void setFont(std::string path, int size); void setUppercase(bool uppercase); void onSizeChanged() override; + const std::string getText() { return mText; } void setText(const std::string& text); void setColor(unsigned int color); void setHorizontalAlignment(Alignment align); @@ -35,13 +37,22 @@ class TextComponent : public GuiComponent std::string getValue() const override; void setValue(const std::string& value) override; - unsigned char getOpacity() const override; void setOpacity(unsigned char opacity) override; inline std::shared_ptr getFont() const { return mFont; } virtual void applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) override; + void setGlowColor(unsigned int color) { mGlowColor = color; }; + void setGlowSize(unsigned int size) { mGlowSize = size; }; + + void setPadding(const Vector4f padding) { mPadding = padding; } + + virtual void update(int deltaTime); + + bool getAutoScroll() { return mAutoScroll; } + void setAutoScroll(bool value); + protected: virtual void onTextChanged(); @@ -50,13 +61,14 @@ class TextComponent : public GuiComponent private: void calculateExtent(); + void renderSingleGlow(const Transform4x4f& parentTrans, float yOff, float x, float y); + void renderGlow(const Transform4x4f& parentTrans, float yOff, float xOff); void onColorChanged(); unsigned int mColor; unsigned int mBgColor; - unsigned char mColorOpacity; - unsigned char mBgColorOpacity; + bool mRenderBackground; bool mUppercase; @@ -65,6 +77,20 @@ class TextComponent : public GuiComponent Alignment mHorizontalAlignment; Alignment mVerticalAlignment; float mLineSpacing; + + unsigned int mGlowColor; + unsigned int mGlowSize; + Vector2f mGlowOffset; + Vector4f mPadding; + + Vector2f mReflection; + bool mReflectOnBorders; + + int mMarqueeOffset; + int mMarqueeOffset2; + int mMarqueeTime; + + bool mAutoScroll; }; #endif // ES_CORE_COMPONENTS_TEXT_COMPONENT_H diff --git a/es-core/src/components/TextEditComponent.cpp b/es-core/src/components/TextEditComponent.cpp index 6d98139bb2..38ecfa421d 100644 --- a/es-core/src/components/TextEditComponent.cpp +++ b/es-core/src/components/TextEditComponent.cpp @@ -2,6 +2,7 @@ #include "resources/Font.h" #include "utils/StringUtil.h" +#include "EsLocale.h" #define TEXT_PADDING_HORIZ 10 #define TEXT_PADDING_VERT 2 @@ -9,11 +10,18 @@ #define CURSOR_REPEAT_START_DELAY 500 #define CURSOR_REPEAT_SPEED 28 // lower is faster +#define BLINKTIME 1000 + TextEditComponent::TextEditComponent(Window* window) : GuiComponent(window), - mBox(window, ":/textinput_ninepatch.png"), mFocused(false), - mScrollOffset(0.0f, 0.0f), mCursor(0), mEditing(false), mFont(Font::get(FONT_SIZE_MEDIUM, FONT_PATH_LIGHT)), - mCursorRepeatDir(0) +mBox(window, ":/textinput_ninepatch.png"), mFocused(false), +mScrollOffset(0.0f, 0.0f), mCursor(0), mEditing(false), mFont(Font::get(FONT_SIZE_MEDIUM, FONT_PATH_LIGHT)), +mCursorRepeatDir(0) { + mBlinkTime = 0; + + auto theme = ThemeData::getMenuTheme(); + mBox.setImagePath(ThemeData::getMenuTheme()->Icons.textinput_ninepatch); + addChild(&mBox); onFocusLost(); @@ -24,13 +32,13 @@ TextEditComponent::TextEditComponent(Window* window) : GuiComponent(window), void TextEditComponent::onFocusGained() { mFocused = true; - mBox.setImagePath(":/textinput_ninepatch_active.png"); + mBox.setImagePath(ThemeData::getMenuTheme()->Icons.textinput_ninepatch_active); } void TextEditComponent::onFocusLost() { mFocused = false; - mBox.setImagePath(":/textinput_ninepatch.png"); + mBox.setImagePath(ThemeData::getMenuTheme()->Icons.textinput_ninepatch); } void TextEditComponent::onSizeChanged() @@ -52,18 +60,19 @@ std::string TextEditComponent::getValue() const void TextEditComponent::textInput(const char* text) { - if(mEditing) + if (mEditing) { mCursorRepeatDir = 0; - if(text[0] == '\b') + if (text[0] == '\b') { - if(mCursor > 0) + if (mCursor > 0) { size_t newCursor = Utils::String::prevCursor(mText, mCursor); mText.erase(mText.begin() + newCursor, mText.begin() + mCursor); mCursor = (unsigned int)newCursor; } - }else{ + } + else { mText.insert(mCursor, text); mCursor += (unsigned int)strlen(text); } @@ -94,71 +103,76 @@ bool TextEditComponent::input(InputConfig* config, Input input) bool const cursor_right = (config->getDeviceId() != DEVICE_KEYBOARD && config->isMappedLike("right", input)) || (config->getDeviceId() == DEVICE_KEYBOARD && input.id == SDLK_RIGHT); - if(input.value == 0) + if (input.value == 0) { - if(cursor_left || cursor_right) + if (cursor_left || cursor_right) mCursorRepeatDir = 0; return false; } - if((config->isMappedTo("a", input) || (config->getDeviceId() == DEVICE_KEYBOARD && input.id == SDLK_RETURN)) && mFocused && !mEditing) + if ((config->isMappedTo("a", input) || (config->getDeviceId() == DEVICE_KEYBOARD && input.id == SDLK_RETURN)) && mFocused && !mEditing) { startEditing(); return true; } - if(mEditing) + if (mEditing) { - if(config->getDeviceId() == DEVICE_KEYBOARD && input.id == SDLK_RETURN) + if (config->getDeviceId() == DEVICE_KEYBOARD && input.id == SDLK_RETURN) { - if(isMultiline()) + if (isMultiline()) { textInput("\n"); - }else{ + } + else { stopEditing(); } return true; } - if((config->getDeviceId() == DEVICE_KEYBOARD && input.id == SDLK_ESCAPE) || (config->getDeviceId() != DEVICE_KEYBOARD && config->isMappedTo("b", input))) + if ((config->getDeviceId() == DEVICE_KEYBOARD && input.id == SDLK_ESCAPE) || (config->getDeviceId() != DEVICE_KEYBOARD && config->isMappedTo("b", input))) { stopEditing(); return true; } - if(config->getDeviceId() != DEVICE_KEYBOARD && config->isMappedLike("up", input)) + if (config->getDeviceId() != DEVICE_KEYBOARD && config->isMappedLike("up", input)) { // TODO - }else if(config->getDeviceId() != DEVICE_KEYBOARD && config->isMappedLike("down", input)) + } + else if (config->getDeviceId() != DEVICE_KEYBOARD && config->isMappedLike("down", input)) { // TODO - }else if(cursor_left || cursor_right) + } + else if (cursor_left || cursor_right) { + mBlinkTime = 0; mCursorRepeatDir = cursor_left ? -1 : 1; mCursorRepeatTimer = -(CURSOR_REPEAT_START_DELAY - CURSOR_REPEAT_SPEED); moveCursor(mCursorRepeatDir); - } else if(config->getDeviceId() == DEVICE_KEYBOARD) + } + else if (config->getDeviceId() == DEVICE_KEYBOARD) { - switch(input.id) + switch (input.id) { - case SDLK_HOME: - setCursor(0); - break; - - case SDLK_END: - setCursor(std::string::npos); - break; - - case SDLK_DELETE: - if(mCursor < mText.length()) - { - // Fake as Backspace one char to the right - moveCursor(1); - textInput("\b"); - } - break; + case SDLK_HOME: + setCursor(0); + break; + + case SDLK_END: + setCursor(std::string::npos); + break; + + case SDLK_DELETE: + if (mCursor < mText.length()) + { + // Fake as Backspace one char to the right + moveCursor(1); + textInput("\b"); + } + break; } } @@ -171,17 +185,21 @@ bool TextEditComponent::input(InputConfig* config, Input input) void TextEditComponent::update(int deltaTime) { + mBlinkTime += deltaTime; + if (mBlinkTime >= BLINKTIME) + mBlinkTime = 0; + updateCursorRepeat(deltaTime); GuiComponent::update(deltaTime); } void TextEditComponent::updateCursorRepeat(int deltaTime) { - if(mCursorRepeatDir == 0) + if (mCursorRepeatDir == 0) return; mCursorRepeatTimer += deltaTime; - while(mCursorRepeatTimer >= CURSOR_REPEAT_SPEED) + while (mCursorRepeatTimer >= CURSOR_REPEAT_SPEED) { moveCursor(mCursorRepeatDir); mCursorRepeatTimer -= CURSOR_REPEAT_SPEED; @@ -196,7 +214,7 @@ void TextEditComponent::moveCursor(int amt) void TextEditComponent::setCursor(size_t pos) { - if(pos == std::string::npos) + if (pos == std::string::npos) mCursor = (unsigned int)mText.length(); else mCursor = (int)pos; @@ -207,32 +225,35 @@ void TextEditComponent::setCursor(size_t pos) void TextEditComponent::onTextChanged() { std::string wrappedText = (isMultiline() ? mFont->wrapText(mText, getTextAreaSize().x()) : mText); - mTextCache = std::unique_ptr(mFont->buildTextCache(wrappedText, 0, 0, 0x77777700 | getOpacity())); + mTextCache = std::unique_ptr(mFont->buildTextCache(wrappedText, 0, 0, (ThemeData::getMenuTheme()->Text.color & 0xFFFFFF00) | getOpacity())); - if(mCursor > (int)mText.length()) + if (mCursor > (int)mText.length()) mCursor = (unsigned int)mText.length(); } void TextEditComponent::onCursorChanged() { - if(isMultiline()) + if (isMultiline()) { Vector2f textSize = mFont->getWrappedTextCursorOffset(mText, getTextAreaSize().x(), mCursor); - if(mScrollOffset.y() + getTextAreaSize().y() < textSize.y() + mFont->getHeight()) //need to scroll down? + if (mScrollOffset.y() + getTextAreaSize().y() < textSize.y() + mFont->getHeight()) //need to scroll down? { mScrollOffset[1] = textSize.y() - getTextAreaSize().y() + mFont->getHeight(); - }else if(mScrollOffset.y() > textSize.y()) //need to scroll up? + } + else if (mScrollOffset.y() > textSize.y()) //need to scroll up? { mScrollOffset[1] = textSize.y(); } - }else{ + } + else { Vector2f cursorPos = mFont->sizeText(mText.substr(0, mCursor)); - if(mScrollOffset.x() + getTextAreaSize().x() < cursorPos.x()) + if (mScrollOffset.x() + getTextAreaSize().x() < cursorPos.x()) { mScrollOffset[0] = cursorPos.x() - getTextAreaSize().x(); - }else if(mScrollOffset.x() > cursorPos.x()) + } + else if (mScrollOffset.x() > cursorPos.x()) { mScrollOffset[0] = cursorPos.x(); } @@ -242,6 +263,10 @@ void TextEditComponent::onCursorChanged() void TextEditComponent::render(const Transform4x4f& parentTrans) { Transform4x4f trans = getTransform() * parentTrans; + + if (!Renderer::isVisibleOnScreen(trans.translation().x(), trans.translation().y(), mSize.x(), mSize.y())) + return; + renderChildren(trans); // text + cursor rendering @@ -256,7 +281,7 @@ void TextEditComponent::render(const Transform4x4f& parentTrans) trans.translate(Vector3f(-mScrollOffset.x(), -mScrollOffset.y(), 0)); Renderer::setMatrix(trans); - if(mTextCache) + if (mTextCache) { mFont->renderTextCache(mTextCache.get()); } @@ -265,19 +290,25 @@ void TextEditComponent::render(const Transform4x4f& parentTrans) Renderer::popClipRect(); // draw cursor - if(mEditing) + if (mEditing) { Vector2f cursorPos; - if(isMultiline()) + if (isMultiline()) { cursorPos = mFont->getWrappedTextCursorOffset(mText, getTextAreaSize().x(), mCursor); - }else{ + } + else + { cursorPos = mFont->sizeText(mText.substr(0, mCursor)); cursorPos[1] = 0; } - float cursorHeight = mFont->getHeight() * 0.8f; - Renderer::drawRect(cursorPos.x(), cursorPos.y() + (mFont->getHeight() - cursorHeight) / 2, 2.0f, cursorHeight, 0x000000FF, 0x000000FF); + if (mBlinkTime < BLINKTIME / 2) + { + float cursorHeight = mFont->getHeight() * 0.8f; + auto cursorColor = (ThemeData::getMenuTheme()->Text.color & 0xFFFFFF00) | getOpacity(); + Renderer::drawRect(cursorPos.x(), cursorPos.y() + (mFont->getHeight() - cursorHeight) / 2, 2.0f, cursorHeight, cursorColor, cursorColor); // 0x000000FF + } } } @@ -299,12 +330,13 @@ Vector2f TextEditComponent::getTextAreaSize() const std::vector TextEditComponent::getHelpPrompts() { std::vector prompts; - if(mEditing) + if (mEditing) { - prompts.push_back(HelpPrompt("up/down/left/right", "move cursor")); - prompts.push_back(HelpPrompt("b", "stop editing")); - }else{ - prompts.push_back(HelpPrompt("a", "edit")); + prompts.push_back(HelpPrompt("up/down/left/right", _("MOVE CURSOR"))); // batocera + prompts.push_back(HelpPrompt("b", _("STOP EDITING"))); + } + else { + prompts.push_back(HelpPrompt("a", _("EDIT"))); } return prompts; } diff --git a/es-core/src/components/TextEditComponent.h b/es-core/src/components/TextEditComponent.h index c388b30aac..206c542cac 100644 --- a/es-core/src/components/TextEditComponent.h +++ b/es-core/src/components/TextEditComponent.h @@ -34,9 +34,9 @@ class TextEditComponent : public GuiComponent virtual std::vector getHelpPrompts() override; + void startEditing(); // batocera + void stopEditing(); // batocera private: - void startEditing(); - void stopEditing(); void onTextChanged(); void onCursorChanged(); @@ -62,6 +62,8 @@ class TextEditComponent : public GuiComponent std::shared_ptr mFont; std::unique_ptr mTextCache; + + int mBlinkTime; }; #endif // ES_CORE_COMPONENTS_TEXT_EDIT_COMPONENT_H diff --git a/es-core/src/components/VideoComponent.cpp b/es-core/src/components/VideoComponent.cpp index 0bd73c9c00..fb1bb373e1 100644 --- a/es-core/src/components/VideoComponent.cpp +++ b/es-core/src/components/VideoComponent.cpp @@ -7,7 +7,7 @@ #include "Window.h" #include -#define FADE_TIME_MS 200 +#define FADE_TIME_MS 900 std::string getTitlePath() { std::string titleFolder = getTitleFolder(); @@ -15,34 +15,36 @@ std::string getTitlePath() { } std::string getTitleFolder() { - std::string home = Utils::FileSystem::getHomePath(); - return home + "/.emulationstation/tmp/"; + return Utils::FileSystem::getGenericPath(Utils::FileSystem::getHomePath()) + "/.emulationstation/tmp/"; } void writeSubtitle(const char* gameName, const char* systemName, bool always) { FILE* file = fopen(getTitlePath().c_str(), "w"); - int end = (int)(Settings::getInstance()->getInt("ScreenSaverSwapVideoTimeout") / (1000)); - if (always) { - fprintf(file, "1\n00:00:01,000 --> 00:00:%d,000\n", end); - } - else + if (file) { - fprintf(file, "1\n00:00:01,000 --> 00:00:08,000\n"); - } - fprintf(file, "%s\n", gameName); - fprintf(file, "%s\n\n", systemName); - - if (!always) { - if (end > 12) + int end = (int)(Settings::getInstance()->getInt("ScreenSaverSwapVideoTimeout") / (1000)); + if (always) { + fprintf(file, "1\n00:00:01,000 --> 00:00:%d,000\n", end); + } + else { - fprintf(file, "2\n00:00:%d,000 --> 00:00:%d,000\n%s\n%s\n", end-4, end, gameName, systemName); + fprintf(file, "1\n00:00:01,000 --> 00:00:08,000\n"); } - } + fprintf(file, "%s\n", gameName); + fprintf(file, "%s\n\n", systemName); - fflush(file); - fclose(file); - file = NULL; + if (!always) { + if (end > 12) + { + fprintf(file, "2\n00:00:%d,000 --> 00:00:%d,000\n%s\n%s\n", end - 4, end, gameName, systemName); + } + } + + fflush(file); + fclose(file); + file = NULL; + } } void VideoComponent::setScreensaverMode(bool isScreensaver) @@ -52,7 +54,7 @@ void VideoComponent::setScreensaverMode(bool isScreensaver) VideoComponent::VideoComponent(Window* window) : GuiComponent(window), - mStaticImage(window), + mStaticImage(window, true), mVideoHeight(0), mVideoWidth(0), mStartDelayed(false), @@ -62,11 +64,20 @@ VideoComponent::VideoComponent(Window* window) : mDisable(false), mScreensaverMode(false), mTargetIsMax(false), + mTargetIsMin(false), mTargetSize(0, 0) { + mVideoEnded = nullptr; + mRoundCorners = 0.0f; + mFadeIn = 0.0f; + mIsWaitingForVideoToStart = false; + + mStaticImage.setAllowFading(false); + // Setup the default configuration mConfig.showSnapshotDelay = false; mConfig.showSnapshotNoVideo = false; + mConfig.snapshotSource = IMAGE; mConfig.startDelay = 0; if (mWindow->getGuiStackSize() > 1) { topWindow(false); @@ -93,12 +104,15 @@ void VideoComponent::onOriginChanged() void VideoComponent::onSizeChanged() { - // Update the embeded static image + // Update the embeded static image mStaticImage.onSizeChanged(); } bool VideoComponent::setVideo(std::string path) { + if (path == mVideoPath) + return !path.empty(); + // Convert the path into a generic format std::string fullPath = Utils::FileSystem::getCanonicalPath(path); @@ -108,6 +122,7 @@ bool VideoComponent::setVideo(std::string path) // Store the path mVideoPath = fullPath; + mStartDelayed = false; // If the file exists then set the new video if (!fullPath.empty() && ResourceManager::getInstance()->fileExists(fullPath)) @@ -119,13 +134,13 @@ bool VideoComponent::setVideo(std::string path) return false; } -void VideoComponent::setImage(std::string path) +void VideoComponent::setImage(std::string path, bool tile, MaxSizeInfo maxSize) { // Check that the image has changed if (path == mStaticImagePath) return; - mStaticImage.setImage(path); + mStaticImage.setImage(path, tile, maxSize); mFadeIn = 0.0f; mStaticImagePath = path; } @@ -149,9 +164,18 @@ void VideoComponent::render(const Transform4x4f& parentTrans) Transform4x4f trans = parentTrans * getTransform(); GuiComponent::renderChildren(trans); + + VideoComponent::renderSnapshot(parentTrans); Renderer::setMatrix(trans); + if (Settings::getInstance()->getBool("DebugImage")) + { + Vector2f targetSizePos = (mTargetSize - mSize) * mOrigin * -1; + Renderer::drawRect(targetSizePos.x(), targetSizePos.y(), mTargetSize.x(), mTargetSize.y(), 0xFF000033); + Renderer::drawRect(0.0f, 0.0f, mSize.x(), mSize.y(), 0x00000033); + } + // Handle the case where the video is delayed handleStartDelay(); @@ -163,11 +187,18 @@ void VideoComponent::renderSnapshot(const Transform4x4f& parentTrans) { // This is the case where the video is not currently being displayed. Work out // if we need to display a static image - if ((mConfig.showSnapshotNoVideo && mVideoPath.empty()) || (mStartDelayed && mConfig.showSnapshotDelay)) - { - // Display the static image instead - mStaticImage.setOpacity((unsigned char)(mFadeIn * 255.0f)); - mStaticImage.render(parentTrans); + if ((mConfig.showSnapshotNoVideo && mVideoPath.empty()) || ((mStartDelayed || mFadeIn < 1.0) && mConfig.showSnapshotDelay)) + { + float t = 1.0 - mFadeIn; + t -= 1; // cubic ease out + t = Math::lerp(0, 1, t*t*t + 1); + t = (t * (float)mOpacity); + + if (t == 0.0) + return; + + mStaticImage.setOpacity((unsigned char)t); + mStaticImage.render(parentTrans); } } @@ -196,6 +227,8 @@ void VideoComponent::applyTheme(const std::shared_ptr& theme, const s setResize(elem->get("size") * scale); else if(elem->has("maxSize")) setMaxSize(elem->get("maxSize") * scale); + else if (elem->has("minSize")) + setMinSize(elem->get("minSize") * scale); } // position + size also implies origin @@ -214,6 +247,17 @@ void VideoComponent::applyTheme(const std::shared_ptr& theme, const s if (elem->has("showSnapshotDelay")) mConfig.showSnapshotDelay = elem->get("showSnapshotDelay"); + if (elem->has("snapshotSource")) + { + auto direction = elem->get("snapshotSource"); + if (direction == "image") + mConfig.snapshotSource = IMAGE; + else if (direction == "marquee") + mConfig.snapshotSource = MARQUEE; + else + mConfig.snapshotSource = THUMBNAIL; + } + if(properties & ThemeFlags::ROTATION) { if(elem->has("rotation")) setRotationDegrees(elem->get("rotation")); @@ -230,59 +274,97 @@ void VideoComponent::applyTheme(const std::shared_ptr& theme, const s setVisible(elem->get("visible")); else setVisible(true); + + if (elem->has("path")) + { + if (Utils::FileSystem::exists(elem->get("path"))) + mVideoPath = elem->get("path"); + else + mVideoPath = mConfig.defaultVideoPath; + } } std::vector VideoComponent::getHelpPrompts() { std::vector ret; - ret.push_back(HelpPrompt("a", "select")); + ret.push_back(HelpPrompt("a", _("SELECT"))); return ret; } +void VideoComponent::handleLooping() +{ +} + void VideoComponent::handleStartDelay() { // Only play if any delay has timed out - if (mStartDelayed) - { - if (mStartTime > SDL_GetTicks()) - { - // Timeout not yet completed - return; - } - // Completed - mStartDelayed = false; - // Clear the playing flag so startVideo works - mIsPlaying = false; - startVideo(); - } + if (!mStartDelayed || mIsWaitingForVideoToStart) + return; + + // Timeout not yet completed + if (mStartTime > SDL_GetTicks()) + return; + + // Completed + mStartDelayed = false; + // Clear the playing flag so startVideo works + mIsPlaying = false; + + mIsWaitingForVideoToStart = true; + + startVideo(); + + if (mIsPlaying) + mIsWaitingForVideoToStart = false; + +// mFadeIn = 0.0f; +// mIsPlaying = true; } -void VideoComponent::handleLooping() +void VideoComponent::onVideoStarted() { + mIsWaitingForVideoToStart = false; + + if (mConfig.startDelay == 0 || PowerSaver::getMode() == PowerSaver::INSTANT) + { + mFadeIn = 1.0f; + mIsPlaying = true; + } + else + { + mFadeIn = 0.0f; + mIsPlaying = true; + } } void VideoComponent::startVideoWithDelay() { // If not playing then either start the video or initiate the delay - if (!mIsPlaying) + if (mIsPlaying || mStartDelayed || mIsWaitingForVideoToStart) + return; + + // Set the video that we are going to be playing so we don't attempt to restart it + mPlayingVideoPath = mVideoPath; + + if (mConfig.startDelay == 0 || PowerSaver::getMode() == PowerSaver::INSTANT) { - // Set the video that we are going to be playing so we don't attempt to restart it - mPlayingVideoPath = mVideoPath; + // No delay. Just start the video + mStartDelayed = false; + mIsPlaying = false; + + mIsWaitingForVideoToStart = true; - if (mConfig.startDelay == 0 || PowerSaver::getMode() == PowerSaver::INSTANT) - { - // No delay. Just start the video - mStartDelayed = false; - startVideo(); - } - else - { - // Configure the start delay - mStartDelayed = true; - mFadeIn = 0.0f; - mStartTime = SDL_GetTicks() + mConfig.startDelay; - } - mIsPlaying = true; + startVideo(); + + if (mIsPlaying) + mIsWaitingForVideoToStart = false; + } + else + { + // Configure the start delay + mStartDelayed = true; + mFadeIn = 0.0f; + mStartTime = SDL_GetTicks() + mConfig.startDelay; } } @@ -290,43 +372,56 @@ void VideoComponent::update(int deltaTime) { manageState(); - // If the video start is delayed and there is less than the fade time then set the image fade - // accordingly - if (mStartDelayed) + if (mIsPlaying) { - Uint32 ticks = SDL_GetTicks(); - if (mStartTime > ticks) + // If the video start is delayed and there is less than the fade time then set the image fade + // accordingly + + if (mStartDelayed) { - Uint32 diff = mStartTime - ticks; - if (diff < FADE_TIME_MS) + Uint32 ticks = SDL_GetTicks(); + if (mStartTime > ticks) { - mFadeIn = (float)diff / (float)FADE_TIME_MS; - return; + Uint32 diff = mStartTime - ticks; + if (diff < FADE_TIME_MS) + { + mFadeIn = (float)diff / (float)FADE_TIME_MS; + return; + } } } + + // If the fade in is less than 1 then increment it + if (mFadeIn < 1.0f) + { + mFadeIn += deltaTime / (float)FADE_TIME_MS; + if (mFadeIn > 1.0f) + mFadeIn = 1.0f; + } } - // If the fade in is less than 1 then increment it - if (mFadeIn < 1.0f) - { - mFadeIn += deltaTime / (float)FADE_TIME_MS; - if (mFadeIn > 1.0f) - mFadeIn = 1.0f; - } + GuiComponent::update(deltaTime); } void VideoComponent::manageState() { + if (mIsWaitingForVideoToStart && mIsPlaying) + mIsWaitingForVideoToStart = false; + // We will only show if the component is on display and the screensaver // is not active bool show = mShowing && !mScreensaverActive && !mDisable; + if (!show) + mStartDelayed = false; // See if we're already playing - if (mIsPlaying) + if (mIsPlaying || mIsWaitingForVideoToStart) { // If we are not on display then stop the video from playing if (!show) { + mIsWaitingForVideoToStart = false; + mStartDelayed = false; stopVideo(); } else @@ -335,6 +430,8 @@ void VideoComponent::manageState() { // Path changed. Stop the video. We will start it again below because // mIsPlaying will be modified by stopVideo to be false + mStartDelayed = false; + mIsWaitingForVideoToStart = false; stopVideo(); } } @@ -347,11 +444,18 @@ void VideoComponent::manageState() { startVideoWithDelay(); } - } + } } void VideoComponent::onShow() { + if (!mShowing && mPlaylist != nullptr && !mVideoPath.empty()) + { + auto video = mPlaylist->getNextItem(); + if (!video.empty()) + mVideoPath = video; + } + mShowing = true; manageState(); } @@ -379,3 +483,21 @@ void VideoComponent::topWindow(bool isTop) mDisable = !isTop; manageState(); } + + +void VideoComponent::setPlaylist(std::shared_ptr playList) +{ + mPlaylist = playList; + if (mPlaylist == nullptr) + return; + + auto video = mPlaylist->getNextItem(); + if (!video.empty()) + setVideo(video); +} + +void VideoComponent::setRoundCorners(float value) +{ + mRoundCorners = value; + mStaticImage.setRoundCorners(value); +} diff --git a/es-core/src/components/VideoComponent.h b/es-core/src/components/VideoComponent.h index 8d8207b1c7..db871d19e7 100644 --- a/es-core/src/components/VideoComponent.h +++ b/es-core/src/components/VideoComponent.h @@ -3,6 +3,7 @@ #define ES_CORE_COMPONENTS_VIDEO_COMPONENT_H #include "components/ImageComponent.h" +#include "components/ImageGridComponent.h" #include "GuiComponent.h" #include @@ -11,6 +12,14 @@ class TextureResource; std::string getTitlePath(); std::string getTitleFolder(); void writeSubtitle(const char* gameName, const char* systemName, bool always); +/* +enum ImageSource +{ + THUMBNAIL, + IMAGE, + MARQUEE +}; +*/ class VideoComponent : public GuiComponent { @@ -20,6 +29,7 @@ class VideoComponent : public GuiComponent unsigned startDelay; bool showSnapshotNoVideo; bool showSnapshotDelay; + ImageSource snapshotSource; std::string defaultVideoPath; }; @@ -27,10 +37,18 @@ class VideoComponent : public GuiComponent VideoComponent(Window* window); virtual ~VideoComponent(); + std::string getValue() const override + { + if (mPlayingVideoPath.empty()) + return mPlayingVideoPath; + + return mVideoPath; + } + // Loads the video at the given filepath bool setVideo(std::string path); // Loads a static image that is displayed if the video cannot be played - void setImage(std::string path); + void setImage(std::string path, bool tile = false, MaxSizeInfo maxSize = MaxSizeInfo()); // Configures the component to show the default video void setDefaultVideo(); @@ -38,6 +56,8 @@ class VideoComponent : public GuiComponent // sets whether it's going to render in screensaver mode void setScreensaverMode(bool isScreensaver); + void setStartDelay(int delay) { mConfig.startDelay = delay; } + virtual void onShow() override; virtual void onHide() override; virtual void onScreenSaverActivate() override; @@ -70,6 +90,64 @@ class VideoComponent : public GuiComponent virtual void setMaxSize(float width, float height) = 0; inline void setMaxSize(const Vector2f& size) { setMaxSize(size.x(), size.y()); } + virtual void setMinSize(float width, float height) = 0; + inline void setMinSize(const Vector2f& size) { setMinSize(size.x(), size.y()); } + + Vector2f getVideoSize() { return Vector2f(mVideoWidth, mVideoHeight); } + bool isPlaying() { + return mIsPlaying; + } + + bool isWaitingForVideoToStart() { + return mIsWaitingForVideoToStart; + } + + virtual void onVideoStarted(); + + const MaxSizeInfo getMaxSizeInfo() + { + if (mTargetSize == Vector2f(0, 0)) + return MaxSizeInfo(mSize, mTargetIsMax); + + return MaxSizeInfo(mTargetSize, mTargetIsMax); + }; + + ImageSource getSnapshotSource() { return mConfig.snapshotSource; }; + void setSnapshotSource(ImageSource source) { mConfig.snapshotSource = source; }; + + inline void setOnVideoEnded(const std::function& callback) { + mVideoEnded = callback; + } + + float getRoundCorners() { return mRoundCorners; } + void setRoundCorners(float value); + + bool isFading() { + return mIsPlaying && mFadeIn < 1.0; + } + + float getFade() + { + if (!mIsPlaying) + return 0.0; + + return mFadeIn; + } + + std::string getVideoPath() + { + if (mPlayingVideoPath.empty()) + return mPlayingVideoPath; + + return mVideoPath; + } + + void setPlaylist(std::shared_ptr playList); + +protected: + std::shared_ptr mPlaylist; + std::function mVideoEnded; + private: // Start the video Immediately virtual void startVideo() = 0; @@ -87,7 +165,9 @@ class VideoComponent : public GuiComponent // Manage the playing state of the component void manageState(); + protected: + unsigned mVideoWidth; unsigned mVideoHeight; Vector2f mTargetSize; @@ -106,6 +186,11 @@ class VideoComponent : public GuiComponent bool mScreensaverActive; bool mScreensaverMode; bool mTargetIsMax; + bool mTargetIsMin; + + bool mIsWaitingForVideoToStart; + + float mRoundCorners; Configuration mConfig; }; diff --git a/es-core/src/components/VideoPlayerComponent.cpp b/es-core/src/components/VideoPlayerComponent.cpp index 1527f671f1..a601b8ab6e 100644 --- a/es-core/src/components/VideoPlayerComponent.cpp +++ b/es-core/src/components/VideoPlayerComponent.cpp @@ -35,8 +35,8 @@ void VideoPlayerComponent::render(const Transform4x4f& parentTrans) VideoComponent::render(parentTrans); - if (!mIsPlaying || mPlayerPid == -1) - VideoComponent::renderSnapshot(parentTrans); +// if (!mIsPlaying || mPlayerPid == -1) + // VideoComponent::renderSnapshot(parentTrans); } void VideoPlayerComponent::setResize(float width, float height) @@ -57,6 +57,17 @@ void VideoPlayerComponent::setMaxSize(float width, float height) onSizeChanged(); } +void VideoPlayerComponent::setMinSize(float width, float height) +{ + setSize(width, height); + mTargetSize = Vector2f(width, height); + mTargetIsMax = false; + mStaticImage.setMinSize(width, height); + onSizeChanged(); + + // TODO add cropping with --crop 100,100,300,300 +} + void VideoPlayerComponent::startVideo() { if (!mIsPlaying) diff --git a/es-core/src/components/VideoPlayerComponent.h b/es-core/src/components/VideoPlayerComponent.h index a68c337a31..6ac417524e 100644 --- a/es-core/src/components/VideoPlayerComponent.h +++ b/es-core/src/components/VideoPlayerComponent.h @@ -25,6 +25,7 @@ class VideoPlayerComponent : public VideoComponent // Can be set before or after a video is loaded. // Never breaks the aspect ratio. setMaxSize() and setResize() are mutually exclusive. void setMaxSize(float width, float height); + void setMinSize(float width, float height); private: // Start the video Immediately diff --git a/es-core/src/components/VideoVlcComponent.cpp b/es-core/src/components/VideoVlcComponent.cpp index a4aeaf111a..d1b127f400 100644 --- a/es-core/src/components/VideoVlcComponent.cpp +++ b/es-core/src/components/VideoVlcComponent.cpp @@ -7,42 +7,72 @@ #include "Settings.h" #include #include +#include +#include "ThemeData.h" +#include +#include "AudioManager.h" + #ifdef WIN32 #include #endif +#include "ImageIO.h" + +#define MATHPI 3.141592653589793238462643383279502884L + libvlc_instance_t* VideoVlcComponent::mVLC = NULL; // VLC prepares to render a video frame. -static void *lock(void *data, void **p_pixels) { +static void *lock(void *data, void **p_pixels) +{ struct VideoContext *c = (struct VideoContext *)data; - SDL_LockMutex(c->mutex); - SDL_LockSurface(c->surface); - *p_pixels = c->surface->pixels; + + int frame = (c->surfaceId ^ 1); + + c->mutexes[frame].lock(); + c->hasFrame[frame] = false; + *p_pixels = c->surfaces[frame]; return NULL; // Picture identifier, not needed here. } // VLC just rendered a video frame. -static void unlock(void *data, void* /*id*/, void *const* /*p_pixels*/) { +static void unlock(void *data, void* /*id*/, void *const* /*p_pixels*/) +{ struct VideoContext *c = (struct VideoContext *)data; - SDL_UnlockSurface(c->surface); - SDL_UnlockMutex(c->mutex); + + int frame = (c->surfaceId ^ 1); + + c->surfaceId = frame; + c->hasFrame[frame] = true; + c->mutexes[frame].unlock(); } // VLC wants to display a video frame. -static void display(void* /*data*/, void* /*id*/) { - //Data to be displayed +static void display(void* data, void* id) +{ + if (data == NULL) + return; + + struct VideoContext *c = (struct VideoContext *)data; + if (c->valid && c->component != NULL && !c->component->isPlaying() && c->component->isWaitingForVideoToStart()) + c->component->onVideoStarted(); } VideoVlcComponent::VideoVlcComponent(Window* window, std::string subtitles) : VideoComponent(window), - mMediaPlayer(nullptr) + mMediaPlayer(nullptr), + mMedia(nullptr) { - memset(&mContext, 0, sizeof(mContext)); + mElapsed = 0; + mColorShift = 0xFFFFFFFF; + + mLoops = -1; + mCurrentLoop = 0; // Get an empty texture for rendering the video - mTexture = TextureResource::get(""); + mTexture = nullptr;// TextureResource::get(""); + mEffect = VideoVlcFlags::VideoVlcEffect::BUMP; // Make sure VLC has been initialised setupVLC(subtitles); @@ -55,73 +85,106 @@ VideoVlcComponent::~VideoVlcComponent() void VideoVlcComponent::setResize(float width, float height) { + if (mSize.x() != 0 && mSize.y() != 0 && !mTargetIsMax && !mTargetIsMin && mTargetSize.x() == width && mTargetSize.y() == height) + return; + mTargetSize = Vector2f(width, height); mTargetIsMax = false; + mTargetIsMin = false; mStaticImage.setResize(width, height); resize(); } void VideoVlcComponent::setMaxSize(float width, float height) { + if (mSize.x() != 0 && mSize.y() != 0 && mTargetIsMax && !mTargetIsMin && mTargetSize.x() == width && mTargetSize.y() == height) + return; + mTargetSize = Vector2f(width, height); mTargetIsMax = true; + mTargetIsMin = false; mStaticImage.setMaxSize(width, height); resize(); } +void VideoVlcComponent::setMinSize(float width, float height) +{ + if (mSize.x() != 0 && mSize.y() != 0 && mTargetIsMin && !mTargetIsMax && mTargetSize.x() == width && mTargetSize.y() == height) + return; + + mTargetSize = Vector2f(width, height); + mTargetIsMax = false; + mTargetIsMin = true; + mStaticImage.setMinSize(width, height); + resize(); +} + +void VideoVlcComponent::onVideoStarted() +{ + VideoComponent::onVideoStarted(); + resize(); +} + void VideoVlcComponent::resize() { - if(!mTexture) + if (!mTexture) return; const Vector2f textureSize((float)mVideoWidth, (float)mVideoHeight); - if(textureSize == Vector2f::Zero()) + if (textureSize == Vector2f::Zero()) return; - // SVG rasterization is determined by height (see SVGResource.cpp), and rasterization is done in terms of pixels - // if rounding is off enough in the rasterization step (for images with extreme aspect ratios), it can cause cutoff when the aspect ratio breaks - // so, we always make sure the resultant height is an integer to make sure cutoff doesn't happen, and scale width from that - // (you'll see this scattered throughout the function) - // this is probably not the best way, so if you're familiar with this problem and have a better solution, please make a pull request! - - if(mTargetIsMax) - { + // SVG rasterization is determined by height (see SVGResource.cpp), and rasterization is done in terms of pixels + // if rounding is off enough in the rasterization step (for images with extreme aspect ratios), it can cause cutoff when the aspect ratio breaks + // so, we always make sure the resultant height is an integer to make sure cutoff doesn't happen, and scale width from that + // (you'll see this scattered throughout the function) + // this is probably not the best way, so if you're familiar with this problem and have a better solution, please make a pull request! - mSize = textureSize; + if (mTargetIsMax) + { - Vector2f resizeScale((mTargetSize.x() / mSize.x()), (mTargetSize.y() / mSize.y())); + mSize = textureSize; - if(resizeScale.x() < resizeScale.y()) - { - mSize[0] *= resizeScale.x(); - mSize[1] *= resizeScale.x(); - }else{ - mSize[0] *= resizeScale.y(); - mSize[1] *= resizeScale.y(); - } + Vector2f resizeScale((mTargetSize.x() / mSize.x()), (mTargetSize.y() / mSize.y())); - // for SVG rasterization, always calculate width from rounded height (see comment above) - mSize[1] = Math::round(mSize[1]); - mSize[0] = (mSize[1] / textureSize.y()) * textureSize.x(); + if (resizeScale.x() < resizeScale.y()) + { + mSize[0] *= resizeScale.x(); + mSize[1] *= resizeScale.x(); + } + else { + mSize[0] *= resizeScale.y(); + mSize[1] *= resizeScale.y(); + } - }else{ - // if both components are set, we just stretch - // if no components are set, we don't resize at all - mSize = mTargetSize == Vector2f::Zero() ? textureSize : mTargetSize; + // for SVG rasterization, always calculate width from rounded height (see comment above) + mSize[1] = Math::round(mSize[1]); + mSize[0] = (mSize[1] / textureSize.y()) * textureSize.x(); - // if only one component is set, we resize in a way that maintains aspect ratio - // for SVG rasterization, we always calculate width from rounded height (see comment above) - if(!mTargetSize.x() && mTargetSize.y()) - { - mSize[1] = Math::round(mTargetSize.y()); - mSize[0] = (mSize.y() / textureSize.y()) * textureSize.x(); - }else if(mTargetSize.x() && !mTargetSize.y()) - { - mSize[1] = Math::round((mTargetSize.x() / textureSize.x()) * textureSize.y()); - mSize[0] = (mSize.y() / textureSize.y()) * textureSize.x(); - } + } + else if (mTargetIsMin) + { + mSize = ImageIO::adjustExternPictureSizef(textureSize, mTargetSize); + } + else { + // if both components are set, we just stretch + // if no components are set, we don't resize at all + mSize = mTargetSize == Vector2f::Zero() ? textureSize : mTargetSize; + + // if only one component is set, we resize in a way that maintains aspect ratio + // for SVG rasterization, we always calculate width from rounded height (see comment above) + if (!mTargetSize.x() && mTargetSize.y()) + { + mSize[1] = Math::round(mTargetSize.y()); + mSize[0] = (mSize.y() / textureSize.y()) * textureSize.x(); + } + else if (mTargetSize.x() && !mTargetSize.y()) + { + mSize[1] = Math::round((mTargetSize.x() / textureSize.x()) * textureSize.y()); + mSize[0] = (mSize.y() / textureSize.y()) * textureSize.x(); } + } // mSize.y() should already be rounded mTexture->rasterizeAt((size_t)Math::round(mSize.x()), (size_t)Math::round(mSize.y())); @@ -129,88 +192,256 @@ void VideoVlcComponent::resize() onSizeChanged(); } +void VideoVlcComponent::setColorShift(unsigned int color) +{ + mColorShift = color; +} + void VideoVlcComponent::render(const Transform4x4f& parentTrans) { + if (!mShowing) + return; + if (!isVisible()) return; VideoComponent::render(parentTrans); + + bool initFromPixels = true; + + if (!mIsPlaying || !mContext.valid) + { + // If video is still attached to the path & texture is initialized, we suppose it had just been stopped (onhide, ondisable, screensaver...) + // still render the last frame + if (mTexture != nullptr && !mVideoPath.empty() && mPlayingVideoPath == mVideoPath && mTexture->isLoaded()) + initFromPixels = false; + else + return; + } + + float t = mFadeIn; + if (mFadeIn < 1.0) + { + t = 1.0 - mFadeIn; + t -= 1; // cubic ease in + t = Math::lerp(0, 1, t*t*t + 1); + t = 1.0 - t; + } + + if (t == 0.0) + return; + + Transform4x4f trans = parentTrans * getTransform(); + + if (mRotation == 0 && !mTargetIsMin && !Renderer::isVisibleOnScreen(trans.translation().x(), trans.translation().y(), mSize.x(), mSize.y())) + return; + GuiComponent::renderChildren(trans); Renderer::setMatrix(trans); - if (mIsPlaying && mContext.valid) + // Build a texture for the video frame + if (initFromPixels) { - const unsigned int fadeIn = (unsigned int)(Math::clamp(0.0f, mFadeIn, 1.0f) * 255.0f); - const unsigned int color = Renderer::convertColor((fadeIn << 24) | (fadeIn << 16) | (fadeIn << 8) | 255); - Renderer::Vertex vertices[4]; + int frame = mContext.surfaceId; + if (mContext.hasFrame[frame]) + { + if (mTexture == nullptr) + { + mTexture = TextureResource::get(""); + resize(); + } - vertices[0] = { { 0.0f , 0.0f }, { 0.0f, 0.0f }, color }; - vertices[1] = { { 0.0f , mSize.y() }, { 0.0f, 1.0f }, color }; - vertices[2] = { { mSize.x(), 0.0f }, { 1.0f, 0.0f }, color }; - vertices[3] = { { mSize.x(), mSize.y() }, { 1.0f, 1.0f }, color }; +#ifdef _RPI_ + // Rpi : A lot of videos are encoded in 60fps on screenscraper + // Try to limit transfert to opengl textures to 30fps to save CPU + if (!Settings::getInstance()->getBool("OptimizeVideo") || mElapsed >= 40) // 40ms = 25fps, 33.33 = 30 fps +#endif + { + mContext.mutexes[frame].lock(); + mTexture->initFromExternalPixels(mContext.surfaces[frame], mVideoWidth, mVideoHeight); + mContext.hasFrame[frame] = false; + mContext.mutexes[frame].unlock(); - // round vertices - for(int i = 0; i < 4; ++i) - vertices[i].pos.round(); + mElapsed = 0; + } + } + } - // Build a texture for the video frame - mTexture->initFromPixels((unsigned char*)mContext.surface->pixels, mContext.surface->w, mContext.surface->h); - mTexture->bind(); + if (mTexture == nullptr) + return; + + float opacity = (mOpacity / 255.0f) * t; + unsigned int color = Renderer::convertColor(mColorShift & 0xFFFFFF00 | (unsigned char)((mColorShift & 0xFF) * opacity)); - // Render it - Renderer::drawTriangleStrips(&vertices[0], 4); + Renderer::Vertex vertices[4]; + + if (mEffect == VideoVlcFlags::VideoVlcEffect::SLIDERIGHT && mFadeIn > 0.0 && mFadeIn < 1.0 && mConfig.startDelay > 0) + { + float t = 1.0 - mFadeIn; + t -= 1; // cubic ease in + t = Math::lerp(0, 1, t*t*t + 1); + //t = 1.0 - t; + + vertices[0] = { { 0.0f , 0.0f },{ t, 0.0f }, color }; + vertices[1] = { { 0.0f , mSize.y() },{ t, 1.0f }, color }; + vertices[2] = { { mSize.x(), 0.0f },{ t + 1.0f, 0.0f }, color }; + vertices[3] = { { mSize.x(), mSize.y() },{ t + 1.0f, 1.0f }, color }; } else + if (mEffect == VideoVlcFlags::VideoVlcEffect::SIZE && mFadeIn > 0.0 && mFadeIn < 1.0 && mConfig.startDelay > 0) + { + float t = 1.0 - mFadeIn; + t -= 1; // cubic ease in + t = Math::lerp(0, 1, t*t*t + 1); + t = 1.0 - t; + + float w = mSize.x() * t; + float h = mSize.y() * t; + float centerX = mSize.x() / 2.0f; + float centerY = mSize.y() / 2.0f; + + Vector2f topLeft(Math::round(centerX - w / 2.0f), Math::round(centerY - h / 2.0f)); + Vector2f bottomRight(Math::round(centerX + w / 2.0f), Math::round(centerY + h / 2.0f)); + + vertices[0] = { { topLeft.x() , topLeft.y() },{ 0.0f, 0.0f }, color }; + vertices[1] = { { topLeft.x() , bottomRight.y() },{ 0.0f, 1.0f }, color }; + vertices[2] = { { bottomRight.x() , topLeft.y() },{ 1.0f, 0.0f }, color }; + vertices[3] = { { bottomRight.x() , bottomRight.y() },{ 1.0f, 1.0f }, color }; + } + else if (mEffect == VideoVlcFlags::VideoVlcEffect::BUMP && mFadeIn > 0.0 && mFadeIn < 1.0 && mConfig.startDelay > 0) { - VideoComponent::renderSnapshot(parentTrans); + // Bump Effect + float bump = sin((MATHPI / 2.0) * mFadeIn) + sin(MATHPI * mFadeIn) / 2.0; + + float w = mSize.x() * bump; + float h = mSize.y() * bump; + float centerX = mSize.x() / 2.0f; + float centerY = mSize.y() / 2.0f; + + Vector2f topLeft(Math::round(centerX - w / 2.0f), Math::round(centerY - h / 2.0f)); + Vector2f bottomRight(Math::round(centerX + w / 2.0f), Math::round(centerY + h / 2.0f)); + + vertices[0] = { { topLeft.x() , topLeft.y() },{ 0.0f, 0.0f }, color }; + vertices[1] = { { topLeft.x() , bottomRight.y() },{ 0.0f, 1.0f }, color }; + vertices[2] = { { bottomRight.x() , topLeft.y() },{ 1.0f, 0.0f }, color }; + vertices[3] = { { bottomRight.x() , bottomRight.y() },{ 1.0f, 1.0f }, color }; + } + else + { + vertices[0] = { { 0.0f , 0.0f },{ 0.0f, 0.0f }, color }; + vertices[1] = { { 0.0f , mSize.y() },{ 0.0f, 1.0f }, color }; + vertices[2] = { { mSize.x(), 0.0f },{ 1.0f, 0.0f }, color }; + vertices[3] = { { mSize.x(), mSize.y() },{ 1.0f, 1.0f }, color }; + } + + // round vertices + for (int i = 0; i < 4; ++i) + vertices[i].pos.round(); + + if (mTexture->bind()) + { + Vector2f targetSizePos = (mTargetSize - mSize) * mOrigin * -1; + + if (mTargetIsMin) + { + Vector2i pos(trans.translation().x() + (int)targetSizePos.x(), trans.translation().y() + (int)targetSizePos.y()); + Vector2i size((int)mTargetSize.round().x(), (int)mTargetSize.round().y()); + Renderer::pushClipRect(pos, size); + } + + if (mRoundCorners > 0) + { + float x = 0; + float y = 0; + float size_x = mSize.x(); + float size_y = mSize.y(); + + if (mTargetIsMin) + { + x = targetSizePos.x(); + y = targetSizePos.y(); + size_x = mTargetSize.x(); + size_y = mTargetSize.y(); + } + + float radius = Math::max(size_x, size_y) * mRoundCorners; + Renderer::enableRoundCornerStencil(x, y, size_x, size_y, radius); + + mTexture->bind(); + } + + // Render it + Renderer::drawTriangleStrips(&vertices[0], 4); + + if (mRoundCorners > 0) + Renderer::disableStencil(); + + if (mTargetIsMin) + Renderer::popClipRect(); + + Renderer::bindTexture(0); } } void VideoVlcComponent::setupContext() { - if (!mContext.valid) - { - // Create an RGBA surface to render the video into - mContext.surface = SDL_CreateRGBSurface(SDL_SWSURFACE, (int)mVideoWidth, (int)mVideoHeight, 32, 0xff000000, 0x00ff0000, 0x0000ff00, 0x000000ff); - mContext.mutex = SDL_CreateMutex(); - mContext.valid = true; - resize(); - } + if (mContext.valid) + return; + + // Create an RGBA surface to render the video into + mContext.surfaces[0] = new unsigned char[mVideoWidth * mVideoHeight * 4]; + mContext.surfaces[1] = new unsigned char[mVideoWidth * mVideoHeight * 4]; + mContext.hasFrame[0] = false; + mContext.hasFrame[1] = false; + mContext.component = this; + mContext.valid = true; + resize(); } void VideoVlcComponent::freeContext() { - if (mContext.valid) + if (!mContext.valid) + return; + + if (!mDisable) { - SDL_FreeSurface(mContext.surface); - SDL_DestroyMutex(mContext.mutex); - mContext.valid = false; + // Release texture memory -> except if mDisable by topWindow ( ex: menu was poped ) + mTexture = nullptr; } + + delete[] mContext.surfaces[0]; + delete[] mContext.surfaces[1]; + mContext.surfaces[0] = nullptr; + mContext.surfaces[1] = nullptr; + mContext.hasFrame[0] = false; + mContext.hasFrame[1] = false; + mContext.component = NULL; + mContext.valid = false; } void VideoVlcComponent::setupVLC(std::string subtitles) { - // If VLC hasn't been initialised yet then do it now - if (!mVLC) - { - const char** args; - const char* newargs[] = { "--quiet", "--sub-file", subtitles.c_str() }; - const char* singleargs[] = { "--quiet" }; - int argslen = 0; + if (mVLC != nullptr) + return; - if (!subtitles.empty()) - { - argslen = sizeof(newargs) / sizeof(newargs[0]); - args = newargs; - } - else - { - argslen = sizeof(singleargs) / sizeof(singleargs[0]); - args = singleargs; - } - mVLC = libvlc_new(argslen, args); + std::vector cmdline; + cmdline.push_back("--quiet"); + cmdline.push_back("--no-video-title-show"); + + if (!subtitles.empty()) + { + cmdline.push_back("--sub-file"); + cmdline.push_back(subtitles); } + const char* *theArgs = new const char*[10]; + + for (int i = 0; i < cmdline.size(); i++) + theArgs[i] = cmdline[i].c_str(); + + mVLC = libvlc_new(cmdline.size(), theArgs); + + delete[] theArgs; } void VideoVlcComponent::handleLooping() @@ -220,12 +451,51 @@ void VideoVlcComponent::handleLooping() libvlc_state_t state = libvlc_media_player_get_state(mMediaPlayer); if (state == libvlc_Ended) { + if (mLoops >= 0) + { + mCurrentLoop++; + if (mCurrentLoop > mLoops) + { + stopVideo(); + + mFadeIn = 0.0; + mPlayingVideoPath = ""; + mVideoPath = ""; + return; + } + } + + if (mPlaylist != nullptr) + { + auto nextVideo = mPlaylist->getNextItem(); + if (!nextVideo.empty()) + { + stopVideo(); + setVideo(nextVideo); + return; + } + else + mPlaylist = nullptr; + } + + if (mVideoEnded != nullptr) + { + bool cont = mVideoEnded(); + if (!cont) + { + stopVideo(); + return; + } + } + if (!Settings::getInstance()->getBool("VideoAudio")) { libvlc_audio_set_mute(mMediaPlayer, 1); } //libvlc_media_player_set_position(mMediaPlayer, 0.0f); - libvlc_media_player_set_media(mMediaPlayer, mMedia); + if (mMedia) + libvlc_media_player_set_media(mMediaPlayer, mMedia); + libvlc_media_player_play(mMediaPlayer); } } @@ -233,81 +503,102 @@ void VideoVlcComponent::handleLooping() void VideoVlcComponent::startVideo() { - if (!mIsPlaying) { - mVideoWidth = 0; - mVideoHeight = 0; + if (mIsPlaying) + return; + + mCurrentLoop = 0; + mVideoWidth = 0; + mVideoHeight = 0; #ifdef WIN32 - std::string path(Utils::String::replace(mVideoPath, "/", "\\")); + std::string path(Utils::String::replace(mVideoPath, "/", "\\")); #else - std::string path(mVideoPath); + std::string path(mVideoPath); #endif - // Make sure we have a video path - if (mVLC && (path.size() > 0)) - { - // Set the video that we are going to be playing so we don't attempt to restart it - mPlayingVideoPath = mVideoPath; + // Make sure we have a video path + if (mVLC && (path.size() > 0)) + { + // Set the video that we are going to be playing so we don't attempt to restart it + mPlayingVideoPath = mVideoPath; - // Open the media - mMedia = libvlc_media_new_path(mVLC, path.c_str()); - if (mMedia) + // Open the media + mMedia = libvlc_media_new_path(mVLC, path.c_str()); + if (mMedia) + { + // If we have a playlist : most videos have a fader, skip it 1 second + if (mPlaylist != nullptr && mConfig.startDelay == 0 && !mConfig.showSnapshotDelay && !mConfig.showSnapshotNoVideo) + libvlc_media_add_option(mMedia, ":start-time=0.7"); + + bool hasAudioTrack = false; + + unsigned track_count; + // Get the media metadata so we can find the aspect ratio + libvlc_media_parse(mMedia); + libvlc_media_track_t** tracks; + track_count = libvlc_media_tracks_get(mMedia, &tracks); + for (unsigned track = 0; track < track_count; ++track) { - unsigned track_count; - // Get the media metadata so we can find the aspect ratio - libvlc_media_parse(mMedia); - libvlc_media_track_t** tracks; - track_count = libvlc_media_tracks_get(mMedia, &tracks); - for (unsigned track = 0; track < track_count; ++track) + if (tracks[track]->i_type == libvlc_track_audio) + hasAudioTrack = true; + else if (tracks[track]->i_type == libvlc_track_video) { - if (tracks[track]->i_type == libvlc_track_video) - { - mVideoWidth = tracks[track]->video->i_width; - mVideoHeight = tracks[track]->video->i_height; + mVideoWidth = tracks[track]->video->i_width; + mVideoHeight = tracks[track]->video->i_height; + + if (hasAudioTrack) break; - } } - libvlc_media_tracks_release(tracks, track_count); + } + libvlc_media_tracks_release(tracks, track_count); - // Make sure we found a valid video track - if ((mVideoWidth > 0) && (mVideoHeight > 0)) + // Make sure we found a valid video track + if ((mVideoWidth > 0) && (mVideoHeight > 0)) + { + if (Settings::getInstance()->getBool("OptimizeVideo")) { -#ifndef _RPI_ - if (mScreensaverMode) - { - if(!Settings::getInstance()->getBool("CaptionsCompatibility")) { - - Vector2f resizeScale((Renderer::getScreenWidth() / (float)mVideoWidth), (Renderer::getScreenHeight() / (float)mVideoHeight)); - - if(resizeScale.x() < resizeScale.y()) - { - mVideoWidth = (unsigned int) (mVideoWidth * resizeScale.x()); - mVideoHeight = (unsigned int) (mVideoHeight * resizeScale.x()); - }else{ - mVideoWidth = (unsigned int) (mVideoWidth * resizeScale.y()); - mVideoHeight = (unsigned int) (mVideoHeight * resizeScale.y()); - } - } - } + // Avoid videos bigger than resolution + Vector2f maxSize(Renderer::getScreenWidth(), Renderer::getScreenHeight()); + +#ifdef _RPI_ + // Temporary -> RPI -> Try to limit videos to 400x300 for performance benchmark + if (!Renderer::isSmallScreen()) + maxSize = Vector2f(400, 300); #endif - PowerSaver::pause(); - setupContext(); - // Setup the media player - mMediaPlayer = libvlc_media_player_new_from_media(mMedia); + if (!mTargetSize.empty() && (mTargetSize.x() < maxSize.x() || mTargetSize.y() < maxSize.y())) + maxSize = mTargetSize; - if (!Settings::getInstance()->getBool("VideoAudio")) + + // If video is bigger than display, ask VLC for a smaller image + auto sz = ImageIO::adjustPictureSize(Vector2i(mVideoWidth, mVideoHeight), Vector2i(mTargetSize.x(), mTargetSize.y()), mTargetIsMin); + if (sz.x() < mVideoWidth || sz.y() < mVideoHeight) { - libvlc_audio_set_mute(mMediaPlayer, 1); + mVideoWidth = sz.x(); + mVideoHeight = sz.y(); } + } - libvlc_media_player_play(mMediaPlayer); - libvlc_video_set_callbacks(mMediaPlayer, lock, unlock, display, (void*)&mContext); - libvlc_video_set_format(mMediaPlayer, "RGBA", (int)mVideoWidth, (int)mVideoHeight, (int)mVideoWidth * 4); + PowerSaver::pause(); + setupContext(); - // Update the playing state - mIsPlaying = true; - mFadeIn = 0.0f; + // Setup the media player + mMediaPlayer = libvlc_media_player_new_from_media(mMedia); + + if (hasAudioTrack) + { + if (!Settings::getInstance()->getBool("VideoAudio")) + libvlc_audio_set_mute(mMediaPlayer, 1); + else + AudioManager::setVideoPlaying(true); } + + libvlc_media_player_play(mMediaPlayer); + libvlc_video_set_callbacks(mMediaPlayer, lock, unlock, display, (void*)&mContext); + libvlc_video_set_format(mMediaPlayer, "RGBA", (int)mVideoWidth, (int)mVideoHeight, (int)mVideoWidth * 4); + + // Update the playing state -> Useless now set by display() & onVideoStarted + //mIsPlaying = true; + //mFadeIn = 0.0f; } } } @@ -316,15 +607,65 @@ void VideoVlcComponent::startVideo() void VideoVlcComponent::stopVideo() { mIsPlaying = false; + mIsWaitingForVideoToStart = false; mStartDelayed = false; + // Release the media player so it stops calling back to us if (mMediaPlayer) { libvlc_media_player_stop(mMediaPlayer); libvlc_media_player_release(mMediaPlayer); - libvlc_media_release(mMedia); mMediaPlayer = NULL; - freeContext(); - PowerSaver::resume(); } + + // Release the media + if (mMedia) + { + libvlc_media_release(mMedia); + mMedia = NULL; + } + + freeContext(); + PowerSaver::resume(); + AudioManager::setVideoPlaying(false); +} + +void VideoVlcComponent::applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) +{ + VideoComponent::applyTheme(theme, view, element, properties); + + using namespace ThemeFlags; + + const ThemeData::ThemeElement* elem = theme->getElement(view, element, "video"); + if (elem && elem->has("effect")) + { + if (!(elem->get("effect").compare("slideRight"))) + mEffect = VideoVlcFlags::VideoVlcEffect::SLIDERIGHT; + else if (!(elem->get("effect").compare("size"))) + mEffect = VideoVlcFlags::VideoVlcEffect::SIZE; + else if (!(elem->get("effect").compare("bump"))) + mEffect = VideoVlcFlags::VideoVlcEffect::BUMP; + else + mEffect = VideoVlcFlags::VideoVlcEffect::NONE; + } + + if (elem && elem->has("roundCorners")) + setRoundCorners(elem->get("roundCorners")); + + if (properties & COLOR) + { + if (elem && elem->has("color")) + setColorShift(elem->get("color")); + } + + if (elem && elem->has("loops")) + mLoops = (int)elem->get("loops"); + else + mLoops = -1; +} + +void VideoVlcComponent::update(int deltaTime) +{ + mElapsed += deltaTime; + VideoComponent::update(deltaTime); } diff --git a/es-core/src/components/VideoVlcComponent.h b/es-core/src/components/VideoVlcComponent.h index c350b8cb3c..030aa56be2 100644 --- a/es-core/src/components/VideoVlcComponent.h +++ b/es-core/src/components/VideoVlcComponent.h @@ -3,19 +3,46 @@ #define ES_CORE_COMPONENTS_VIDEO_VLC_COMPONENT_H #include "VideoComponent.h" +#include -struct SDL_mutex; -struct SDL_Surface; struct libvlc_instance_t; struct libvlc_media_t; struct libvlc_media_player_t; -struct VideoContext { - SDL_Surface* surface; - SDL_mutex* mutex; +struct VideoContext +{ + VideoContext() + { + surfaces[0] = nullptr; + surfaces[1] = nullptr; + component = nullptr; + valid = false; + hasFrame[0] = false; + hasFrame[1] = false; + surfaceId = 0; + } + + int surfaceId; + unsigned char* surfaces[2]; + std::mutex mutexes[2]; + bool hasFrame[2]; + + VideoComponent* component; bool valid; }; + +namespace VideoVlcFlags +{ + enum VideoVlcEffect + { + NONE, + BUMP, + SIZE, + SLIDERIGHT + }; +} + class VideoVlcComponent : public VideoComponent { // Structure that groups together the configuration of the video component @@ -30,12 +57,11 @@ class VideoVlcComponent : public VideoComponent public: static void setupVLC(std::string subtitles); - VideoVlcComponent(Window* window, std::string subtitles); + VideoVlcComponent(Window* window, std::string subtitles = ""); virtual ~VideoVlcComponent(); void render(const Transform4x4f& parentTrans) override; - // Resize the video to fit this size. If one axis is zero, scale that axis to maintain aspect ratio. // If both are non-zero, potentially break the aspect ratio. If both are zero, no resizing. // Can be set before or after a video is loaded. @@ -46,6 +72,12 @@ class VideoVlcComponent : public VideoComponent // Can be set before or after a video is loaded. // Never breaks the aspect ratio. setMaxSize() and setResize() are mutually exclusive. void setMaxSize(float width, float height); + void setMinSize(float width, float height); + + virtual void applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties); + virtual void update(int deltaTime); + + void setColorShift(unsigned int color); private: // Calculates the correct mSize from our resizing information (set by setResize/setMaxSize). @@ -58,15 +90,30 @@ class VideoVlcComponent : public VideoComponent // Handle looping the video. Must be called periodically virtual void handleLooping(); + virtual void onVideoStarted(); + void setupContext(); void freeContext(); + void setEffect(VideoVlcFlags::VideoVlcEffect effect) { mEffect = effect; } + private: static libvlc_instance_t* mVLC; libvlc_media_t* mMedia; libvlc_media_player_t* mMediaPlayer; VideoContext mContext; std::shared_ptr mTexture; + + std::string mSubtitlePath; + std::string mSubtitleTmpFile; + + VideoVlcFlags::VideoVlcEffect mEffect; + + unsigned int mColorShift; + int mElapsed; + + int mCurrentLoop; + int mLoops; }; #endif // ES_CORE_COMPONENTS_VIDEO_VLC_COMPONENT_H diff --git a/es-core/src/guis/GuiDetectDevice.cpp b/es-core/src/guis/GuiDetectDevice.cpp index 45d9a6bd23..1112ced71a 100644 --- a/es-core/src/guis/GuiDetectDevice.cpp +++ b/es-core/src/guis/GuiDetectDevice.cpp @@ -10,38 +10,51 @@ #define HOLD_TIME 1000 -GuiDetectDevice::GuiDetectDevice(Window* window, bool firstRun, const std::function& doneCallback) : GuiComponent(window), mFirstRun(firstRun), +GuiDetectDevice::GuiDetectDevice(Window* window, bool firstRun, const std::function& doneCallback) : GuiComponent(window), mFirstRun(firstRun), mBackground(window, ":/frame.png"), mGrid(window, Vector2i(1, 5)) { + auto theme = ThemeData::getMenuTheme(); + mBackground.setImagePath(theme->Background.path); + mBackground.setEdgeColor(theme->Background.color); + mBackground.setCenterColor(theme->Background.centerColor); + mBackground.setCornerSize(theme->Background.cornerSize); + mGrid.setSeparatorColor(theme->Text.separatorColor); + mHoldingConfig = NULL; mHoldTime = 0; mDoneCallback = doneCallback; addChild(&mBackground); addChild(&mGrid); - + // title - mTitle = std::make_shared(mWindow, firstRun ? "WELCOME" : "CONFIGURE INPUT", - Font::get(FONT_SIZE_LARGE), 0x555555FF, ALIGN_CENTER); - mGrid.setEntry(mTitle, Vector2i(0, 0), false, true, Vector2i(1, 1), GridFlags::BORDER_BOTTOM); + mTitle = std::make_shared(mWindow, firstRun ? _("WELCOME") : _("CONFIGURE INPUT"), + ThemeData::getMenuTheme()->Title.font, ThemeData::getMenuTheme()->Title.color, ALIGN_CENTER); + mGrid.setEntry(mTitle, Vector2i(0, 0), false, true, Vector2i(1, 1), GridFlags::BORDER_BOTTOM); + // device info std::stringstream deviceInfo; int numDevices = InputManager::getInstance()->getNumJoysticks(); - - if(numDevices > 0) - deviceInfo << numDevices << " GAMEPAD" << (numDevices > 1 ? "S" : "") << " DETECTED"; + + if (numDevices > 0) + { + char strbuf[256]; + snprintf(strbuf, 256, EsLocale::nGetText("%i GAMEPAD DETECTED", "%i GAMEPADS DETECTED", numDevices).c_str(), numDevices); // batocera + deviceInfo << strbuf; + } else - deviceInfo << "NO GAMEPADS DETECTED"; - mDeviceInfo = std::make_shared(mWindow, deviceInfo.str(), Font::get(FONT_SIZE_SMALL), 0x999999FF, ALIGN_CENTER); + deviceInfo << _("NO GAMEPADS DETECTED"); + + mDeviceInfo = std::make_shared(mWindow, deviceInfo.str(), ThemeData::getMenuTheme()->TextSmall.font, 0x999999FF, ALIGN_CENTER); mGrid.setEntry(mDeviceInfo, Vector2i(0, 1), false, true); // message - mMsg1 = std::make_shared(mWindow, "HOLD A BUTTON ON YOUR DEVICE TO CONFIGURE IT.", Font::get(FONT_SIZE_SMALL), 0x777777FF, ALIGN_CENTER); + mMsg1 = std::make_shared(mWindow, _("HOLD A BUTTON ON YOUR DEVICE TO CONFIGURE IT."), ThemeData::getMenuTheme()->TextSmall.font, ThemeData::getMenuTheme()->TextSmall.color, ALIGN_CENTER); mGrid.setEntry(mMsg1, Vector2i(0, 2), false, true); - const char* msg2str = firstRun ? "PRESS F4 TO QUIT AT ANY TIME." : "PRESS ESC TO CANCEL."; - mMsg2 = std::make_shared(mWindow, msg2str, Font::get(FONT_SIZE_SMALL), 0x777777FF, ALIGN_CENTER); + std::string msg2str = firstRun ? _("PRESS F4 TO QUIT AT ANY TIME.") : _("PRESS ESC TO CANCEL."); + mMsg2 = std::make_shared(mWindow, msg2str, ThemeData::getMenuTheme()->TextSmall.font, ThemeData::getMenuTheme()->TextSmall.color, ALIGN_CENTER); mGrid.setEntry(mMsg2, Vector2i(0, 3), false, true); // currently held device diff --git a/es-app/src/guis/GuiInfoPopup.cpp b/es-core/src/guis/GuiInfoPopup.cpp similarity index 81% rename from es-app/src/guis/GuiInfoPopup.cpp rename to es-core/src/guis/GuiInfoPopup.cpp index e759e27f72..dc859e258d 100644 --- a/es-app/src/guis/GuiInfoPopup.cpp +++ b/es-core/src/guis/GuiInfoPopup.cpp @@ -8,6 +8,9 @@ GuiInfoPopup::GuiInfoPopup(Window* window, std::string message, int duration) : GuiComponent(window), mMessage(message), mDuration(duration), running(true) { + auto theme = ThemeData::getMenuTheme(); + mBackColor = theme->Background.color; + mFrame = new NinePatchComponent(window); float maxWidth = Renderer::getScreenWidth() * 0.9f; float maxHeight = Renderer::getScreenHeight() * 0.2f; @@ -15,7 +18,7 @@ GuiInfoPopup::GuiInfoPopup(Window* window, std::string message, int duration) : std::shared_ptr s = std::make_shared(mWindow, "", Font::get(FONT_SIZE_MINI), - 0x444444FF, + theme->Text.color, //0x444444FF, ALIGN_CENTER); // we do this to force the text container to resize and return an actual expected popup size @@ -42,9 +45,16 @@ GuiInfoPopup::GuiInfoPopup(Window* window, std::string message, int duration) : float posX = Renderer::getScreenWidth()*0.5f - mSize.x()*0.5f; float posY = Renderer::getScreenHeight() * 0.02f; - setPosition(posX, posY, 0); + // FCA TopRight + // posX = Renderer::getScreenWidth()*0.98f - mSize.x()*0.98f; + // posY = Renderer::getScreenHeight() * 0.02f; - mFrame->setImagePath(":/frame.png"); + setPosition(posX, posY, 0); + + mFrame->setImagePath(theme->Background.path); + mFrame->setCenterColor(mBackColor); + mFrame->setEdgeColor(mBackColor); + mFrame->setCornerSize(theme->Background.cornerSize); mFrame->fitTo(mSize, Vector3f::Zero(), Vector2f(-32, -32)); addChild(mFrame); @@ -107,10 +117,14 @@ bool GuiInfoPopup::updateState() { alpha = ((-(curTime - mStartTime - mDuration)*255)/500); } + + if (alpha > mBackColor & 0xff) + alpha = mBackColor & 0xff; + mGrid->setOpacity((unsigned char)alpha); // apply fade in effect to popup frame - mFrame->setEdgeColor(0xFFFFFF00 | (unsigned char)(alpha)); - mFrame->setCenterColor(0xFFFFFF00 | (unsigned char)(alpha)); + mFrame->setEdgeColor((mBackColor & 0xffffff00) | (unsigned char)(alpha)); + mFrame->setCenterColor((mBackColor & 0xffffff00) | (unsigned char)(alpha)); return true; } \ No newline at end of file diff --git a/es-app/src/guis/GuiInfoPopup.h b/es-core/src/guis/GuiInfoPopup.h similarity index 96% rename from es-app/src/guis/GuiInfoPopup.h rename to es-core/src/guis/GuiInfoPopup.h index 8d4d3cfd17..fdd2e9ade7 100644 --- a/es-app/src/guis/GuiInfoPopup.h +++ b/es-core/src/guis/GuiInfoPopup.h @@ -24,6 +24,7 @@ class GuiInfoPopup : public GuiComponent, public Window::InfoPopup ComponentGrid* mGrid; NinePatchComponent* mFrame; bool running; + unsigned int mBackColor; }; #endif // ES_APP_GUIS_GUI_INFO_POPUP_H diff --git a/es-core/src/guis/GuiInputConfig.cpp b/es-core/src/guis/GuiInputConfig.cpp old mode 100755 new mode 100644 index ff86224289..20c2231253 --- a/es-core/src/guis/GuiInputConfig.cpp +++ b/es-core/src/guis/GuiInputConfig.cpp @@ -50,10 +50,18 @@ static const InputConfigStructure GUI_INPUT_CONFIG_LIST[inputCount] = #define HOLD_TO_SKIP_MS 1000 -GuiInputConfig::GuiInputConfig(Window* window, InputConfig* target, bool reconfigureAll, const std::function& okCallback) : GuiComponent(window), +GuiInputConfig::GuiInputConfig(Window* window, InputConfig* target, bool reconfigureAll, const std::function& okCallback) : GuiComponent(window), mBackground(window, ":/frame.png"), mGrid(window, Vector2i(1, 7)), mTargetConfig(target), mHoldingInput(false), mBusyAnim(window) { + auto theme = ThemeData::getMenuTheme(); + mBackground.setImagePath(theme->Background.path); // ":/frame.png" + mBackground.setEdgeColor(theme->Background.color); + mBackground.setCenterColor(theme->Background.centerColor); + mBackground.setCornerSize(theme->Background.cornerSize); + + mGrid.setSeparatorColor(theme->Text.separatorColor); + LOG(LogInfo) << "Configuring device " << target->getDeviceId() << " (" << target->getDeviceName() << ")."; if(reconfigureAll) @@ -70,7 +78,7 @@ GuiInputConfig::GuiInputConfig(Window* window, InputConfig* target, bool reconfi mTitle = std::make_shared(mWindow, "CONFIGURING", Font::get(FONT_SIZE_LARGE), 0x555555FF, ALIGN_CENTER); mGrid.setEntry(mTitle, Vector2i(0, 1), false, true); - + std::stringstream ss; if(target->getDeviceId() == DEVICE_KEYBOARD) ss << "KEYBOARD"; @@ -91,11 +99,11 @@ GuiInputConfig::GuiInputConfig(Window* window, InputConfig* target, bool reconfi for(int i = 0; i < inputCount; i++) { ComponentListRow row; - + // icon auto icon = std::make_shared(mWindow); icon->setImage(GUI_INPUT_CONFIG_LIST[i].icon); - icon->setColorShift(0x777777FF); + icon->setColorShift(ThemeData::getMenuTheme()->Text.color); icon->setResize(0, Font::get(FONT_SIZE_MEDIUM)->getLetterHeight() * 1.25f); row.addElement(icon, false); @@ -104,7 +112,7 @@ GuiInputConfig::GuiInputConfig(Window* window, InputConfig* target, bool reconfi spacer->setSize(16, 0); row.addElement(spacer, false); - auto text = std::make_shared(mWindow, GUI_INPUT_CONFIG_LIST[i].dispName, Font::get(FONT_SIZE_MEDIUM), 0x777777FF); + auto text = std::make_shared(mWindow, GUI_INPUT_CONFIG_LIST[i].dispName, ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color); row.addElement(text, true); auto mapping = std::make_shared(mWindow, "-NOT DEFINED-", Font::get(FONT_SIZE_MEDIUM, FONT_PATH_LIGHT), 0x999999FF, ALIGN_RIGHT); @@ -128,13 +136,14 @@ GuiInputConfig::GuiInputConfig(Window* window, InputConfig* target, bool reconfi setPress(mapping); return true; } - + // we're not configuring and they didn't press A to start, so ignore this return false; } - // apply filtering for quirks related to trigger mapping - if(filterTrigger(input, config, i)) + + // filter for input quirks specific to Sony DualShock 3 + if(filterTrigger(input, config)) return false; // we are configuring @@ -185,7 +194,7 @@ GuiInputConfig::GuiInputConfig(Window* window, InputConfig* target, bool reconfi InputManager::getInstance()->writeDeviceConfig(mTargetConfig); // save if(okCallback) okCallback(); - delete this; + delete this; }; buttons.push_back(std::make_shared(mWindow, "OK", "ok", [this, okFunction] { // check if the hotkey enable button is set. if not prompt the user to use select or nothing. @@ -257,13 +266,13 @@ void GuiInputConfig::update(int deltaTime) std::stringstream ss; ss << "HOLD FOR " << HOLD_TO_SKIP_MS/1000 - curSec << "S TO SKIP"; text->setText(ss.str()); - text->setColor(0x777777FF); + text->setColor(ThemeData::getMenuTheme()->Text.color); } } } } -// move cursor to the next thing if we're configuring all, +// move cursor to the next thing if we're configuring all, // or come out of "configure mode" if we were only configuring one row void GuiInputConfig::rowDone() { @@ -300,7 +309,7 @@ void GuiInputConfig::setNotDefined(const std::shared_ptr& text) void GuiInputConfig::setAssignedTo(const std::shared_ptr& text, Input input) { text->setText(Utils::String::toUpper(input.string())); - text->setColor(0x777777FF); + text->setColor(ThemeData::getMenuTheme()->Text.color); } void GuiInputConfig::error(const std::shared_ptr& text, const std::string& /*msg*/) @@ -322,7 +331,7 @@ bool GuiInputConfig::assign(Input input, int inputId) } setAssignedTo(mMappings.at(inputId), input); - + input.configured = true; mTargetConfig->mapInput(GUI_INPUT_CONFIG_LIST[inputId].name, input); @@ -336,7 +345,7 @@ void GuiInputConfig::clearAssignment(int inputId) mTargetConfig->unmapInput(GUI_INPUT_CONFIG_LIST[inputId].name); } -bool GuiInputConfig::filterTrigger(Input input, InputConfig* config, int inputId) +bool GuiInputConfig::filterTrigger(Input input, InputConfig* config) { #if defined(__linux__) // on Linux, some gamepads return both an analog axis and a digital button for the trigger; @@ -354,19 +363,10 @@ bool GuiInputConfig::filterTrigger(Input input, InputConfig* config, int inputId // digital triggers are unwanted if (input.type == TYPE_BUTTON && (input.id == 6 || input.id == 7)) return true; + // ignore analog values < 0 + if (input.type == TYPE_AXIS && (input.id == 2 || input.id == 5) && input.value < 0) + return true; } - - // ignore negative pole for axes 2/5 only when triggers are being configured - if((mSkipAxis || strstr(GUI_INPUT_CONFIG_LIST[inputId].name, "Trigger") != NULL) \ - && input.type == TYPE_AXIS && (input.id == 2 || input.id == 5) && input.value < 0) - { - mSkipAxis = true; - return true; - } -#else - (void)input; - (void)config; - (void)inputId; #endif return false; diff --git a/es-core/src/guis/GuiInputConfig.h b/es-core/src/guis/GuiInputConfig.h index 3cdf0f9101..2afcb4f5e6 100644 --- a/es-core/src/guis/GuiInputConfig.h +++ b/es-core/src/guis/GuiInputConfig.h @@ -28,7 +28,7 @@ class GuiInputConfig : public GuiComponent bool assign(Input input, int inputId); void clearAssignment(int inputId); - bool filterTrigger(Input input, InputConfig* config, int inputId); + bool filterTrigger(Input input, InputConfig* config); void rowDone(); @@ -50,7 +50,6 @@ class GuiInputConfig : public GuiComponent Input mHeldInput; int mHeldTime; int mHeldInputId; - bool mSkipAxis; BusyComponent mBusyAnim; }; diff --git a/es-core/src/guis/GuiMsgBox.cpp b/es-core/src/guis/GuiMsgBox.cpp index 9db9ffff11..3f7779e1ce 100644 --- a/es-core/src/guis/GuiMsgBox.cpp +++ b/es-core/src/guis/GuiMsgBox.cpp @@ -2,20 +2,85 @@ #include "components/ButtonComponent.h" #include "components/MenuComponent.h" +#include "components/ImageComponent.h" +#include "resources/ResourceManager.h" +#include "EsLocale.h" -#define HORIZONTAL_PADDING_PX 20 +#define HORIZONTAL_PADDING_PX (Renderer::getScreenWidth()*0.01) + +GuiMsgBox::GuiMsgBox(Window* window, const std::string& text, const std::string& name1, const std::function& func1, GuiMsgBoxIcon icon) + : GuiMsgBox(window, text, name1, func1, "", nullptr, "", nullptr, icon) { } GuiMsgBox::GuiMsgBox(Window* window, const std::string& text, const std::string& name1, const std::function& func1, const std::string& name2, const std::function& func2, - const std::string& name3, const std::function& func3) : GuiComponent(window), - mBackground(window, ":/frame.png"), mGrid(window, Vector2i(1, 2)) + GuiMsgBoxIcon icon) + : GuiMsgBox(window, text, name1, func1, name2, func2, "", nullptr, icon) { } + +GuiMsgBox::GuiMsgBox(Window* window, const std::string& text, + const std::string& name1, const std::function& func1, + const std::string& name2, const std::function& func2, + const std::string& name3, const std::function& func3, + GuiMsgBoxIcon icon) : GuiComponent(window), + mBackground(window, ":/frame.png"), mGrid(window, Vector2i(2, 2)) + { + auto theme = ThemeData::getMenuTheme(); + mBackground.setImagePath(theme->Background.path); + mBackground.setEdgeColor(theme->Background.color); + mBackground.setCenterColor(theme->Background.centerColor); + mBackground.setCornerSize(theme->Background.cornerSize); + float width = Renderer::getScreenWidth() * 0.6f; // max width float minWidth = Renderer::getScreenWidth() * 0.3f; // minimum width + + mImage = nullptr; - mMsg = std::make_shared(mWindow, text, Font::get(FONT_SIZE_MEDIUM), 0x777777FF, ALIGN_CENTER); - mGrid.setEntry(mMsg, Vector2i(0, 0), false, false); + std::string imageFile; + + switch (icon) + { + case ICON_INFORMATION: + imageFile = ":/info.svg"; + break; + case ICON_QUESTION: + imageFile = ":/question.svg"; + break; + case ICON_WARNING: + imageFile = ":/warning.svg"; + break; + case ICON_ERROR: + imageFile = ":/alert.svg"; + break; + case ICON_AUTOMATIC: + + if (text.rfind("?") != std::string::npos || name1 == _("YES")) + imageFile = ":/question.svg"; + else if (name1 == _("OK")) + { + if (name2.empty()) + imageFile = ":/info.svg"; + else + imageFile = ":/question.svg"; + } + + break; + } + + if (!imageFile.empty() && ResourceManager::getInstance()->fileExists(imageFile) && !Renderer::isSmallScreen()) + { + mImage = std::make_shared(window); + mImage->setImage(imageFile); + mImage->setColorShift(theme->Text.color); + mImage->setMaxSize(theme->Text.font->getLetterHeight() * 2.0f, theme->Text.font->getLetterHeight() * 2.0f); + + mGrid.setEntry(mImage, Vector2i(0, 0), false, false); + } + + mMsg = std::make_shared(mWindow, text, ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color, mImage == nullptr || Renderer::isSmallScreen() ? ALIGN_CENTER : ALIGN_LEFT); // CENTER + mMsg->setPadding(Vector4f(Renderer::getScreenWidth()*0.015f, 0, Renderer::getScreenWidth()*0.015f, 0)); + + mGrid.setEntry(mMsg, Vector2i(mImage == nullptr ? 0 : 1, 0), false, false, Vector2i(mImage == nullptr ? 2 : 1, 1)); // create the buttons mButtons.push_back(std::make_shared(mWindow, name1, name1, std::bind(&GuiMsgBox::deleteMeAndCall, this, func1))); @@ -24,14 +89,14 @@ GuiMsgBox::GuiMsgBox(Window* window, const std::string& text, if(!name3.empty()) mButtons.push_back(std::make_shared(mWindow, name3, name3, std::bind(&GuiMsgBox::deleteMeAndCall, this, func3))); - // set accelerator automatically (button to press when "b" is pressed) + // set accelerator automatically (button to press when BUTTON_BACK is pressed) if(mButtons.size() == 1) { mAcceleratorFunc = mButtons.front()->getPressedFunc(); }else{ for(auto it = mButtons.cbegin(); it != mButtons.cend(); it++) { - if(Utils::String::toUpper((*it)->getText()) == "OK" || Utils::String::toUpper((*it)->getText()) == "NO") + if(Utils::String::toUpper((*it)->getText()) == _("OK") || Utils::String::toUpper((*it)->getText()) == _("NO")) { mAcceleratorFunc = (*it)->getPressedFunc(); break; @@ -41,20 +106,33 @@ GuiMsgBox::GuiMsgBox(Window* window, const std::string& text, // put the buttons into a ComponentGrid mButtonGrid = makeButtonGrid(mWindow, mButtons); - mGrid.setEntry(mButtonGrid, Vector2i(0, 1), true, false, Vector2i(1, 1), GridFlags::BORDER_TOP); + mGrid.setEntry(mButtonGrid, Vector2i(0, 1), true, false, Vector2i(2, 1), GridFlags::BORDER_TOP); // decide final width if(mMsg->getSize().x() < width && mButtonGrid->getSize().x() < width) { // mMsg and buttons are narrower than width - width = Math::max(mButtonGrid->getSize().x(), mMsg->getSize().x()); + width = Math::max(mButtonGrid->getSize().x(), mMsg->getSize().x() + 3 * HORIZONTAL_PADDING_PX); + + if (mImage != nullptr) + width += mImage->getSize().x() + 2 * HORIZONTAL_PADDING_PX; + width = Math::max(width, minWidth); } - + // now that we know width, we can find height mMsg->setSize(width, 0); // mMsg->getSize.y() now returns the proper length - const float msgHeight = Math::max(Font::get(FONT_SIZE_LARGE)->getHeight(), mMsg->getSize().y()*1.225f); - setSize(width + HORIZONTAL_PADDING_PX*2, msgHeight + mButtonGrid->getSize().y()); + + float msgHeight = Math::max(Font::get(FONT_SIZE_LARGE)->getHeight(), mMsg->getSize().y()*1.225f); + + if (msgHeight + mButtonGrid->getSize().y() > Renderer::getScreenHeight()) + { + setSize(Renderer::getScreenWidth(), Renderer::getScreenHeight()); + if (mImage != nullptr) + mMsg->setSize(Renderer::getScreenWidth() - mImage->getSize().x() - 4* HORIZONTAL_PADDING_PX, 0); + } + else + setSize(width + HORIZONTAL_PADDING_PX*2, msgHeight + mButtonGrid->getSize().y()); // center for good measure setPosition((Renderer::getScreenWidth() - mSize.x()) / 2.0f, (Renderer::getScreenHeight() - mSize.y()) / 2.0f); @@ -66,14 +144,15 @@ GuiMsgBox::GuiMsgBox(Window* window, const std::string& text, bool GuiMsgBox::input(InputConfig* config, Input input) { // special case for when GuiMsgBox comes up to report errors before anything has been configured - if(config->getDeviceId() == DEVICE_KEYBOARD && !config->isConfigured() && input.value && + if(config->getDeviceId() == DEVICE_KEYBOARD && !config->isConfigured() && input.value && (input.id == SDLK_RETURN || input.id == SDLK_ESCAPE || input.id == SDLK_SPACE)) { mAcceleratorFunc(); return true; } - if(mAcceleratorFunc && config->isMappedTo("b", input) && input.value != 0) + /* when it's not configured, allow to remove the message box too to allow the configdevice window a chance */ + if(mAcceleratorFunc && ((config->isMappedTo("b", input) && input.value != 0) || (config->isConfigured() == false && input.type == TYPE_BUTTON))) // batocera { mAcceleratorFunc(); return true; @@ -85,9 +164,15 @@ bool GuiMsgBox::input(InputConfig* config, Input input) void GuiMsgBox::onSizeChanged() { mGrid.setSize(mSize); - mGrid.setRowHeightPerc(1, mButtonGrid->getSize().y() / mSize.y()); - // update messagebox size + if (mImage != nullptr) + { + auto width = mImage->getSize().x() + (Renderer::isSmallScreen() ? 5 : 2) * HORIZONTAL_PADDING_PX; + mGrid.setColWidthPerc(0, width / mSize.x(), true); + } + + mGrid.setRowHeightPerc(1, mButtonGrid->getSize().y() / mSize.y()); + mMsg->setSize(mSize.x() - HORIZONTAL_PADDING_PX*2, mGrid.getRowHeight(0)); mGrid.onSizeChanged(); diff --git a/es-core/src/guis/GuiMsgBox.h b/es-core/src/guis/GuiMsgBox.h index cd9883244b..a710002377 100644 --- a/es-core/src/guis/GuiMsgBox.h +++ b/es-core/src/guis/GuiMsgBox.h @@ -8,26 +8,49 @@ class ButtonComponent; class TextComponent; +class ImageComponent; + +enum GuiMsgBoxIcon +{ + ICON_AUTOMATIC, + ICON_INFORMATION, + ICON_QUESTION, + ICON_WARNING, + ICON_ERROR +}; class GuiMsgBox : public GuiComponent { public: + GuiMsgBox(Window* window, const std::string& text, + const std::string& name1, const std::function& func1, + const std::string& name2, const std::function& func2, + const std::string& name3, const std::function& func3, + GuiMsgBoxIcon icon = ICON_AUTOMATIC); + + + GuiMsgBox(Window* window, const std::string& text, + const std::string& name1, const std::function& func1, + const std::string& name2, const std::function& func2, + GuiMsgBoxIcon icon = ICON_AUTOMATIC); + GuiMsgBox(Window* window, const std::string& text, const std::string& name1 = "OK", const std::function& func1 = nullptr, - const std::string& name2 = "", const std::function& func2 = nullptr, - const std::string& name3 = "", const std::function& func3 = nullptr); + GuiMsgBoxIcon icon = ICON_AUTOMATIC); bool input(InputConfig* config, Input input) override; void onSizeChanged() override; std::vector getHelpPrompts() override; + std::string getValue() const override { return "GuiMsgBox"; } + private: void deleteMeAndCall(const std::function& func); NinePatchComponent mBackground; - ComponentGrid mGrid; - + ComponentGrid mGrid; + std::shared_ptr mImage; std::shared_ptr mMsg; std::vector< std::shared_ptr > mButtons; std::shared_ptr mButtonGrid; diff --git a/es-core/src/guis/GuiTextEditPopup.cpp b/es-core/src/guis/GuiTextEditPopup.cpp index 4c14bddb47..94674bd62d 100644 --- a/es-core/src/guis/GuiTextEditPopup.cpp +++ b/es-core/src/guis/GuiTextEditPopup.cpp @@ -8,6 +8,12 @@ GuiTextEditPopup::GuiTextEditPopup(Window* window, const std::string& title, con const std::function& okCallback, bool multiLine, const char* acceptBtnText) : GuiComponent(window), mBackground(window, ":/frame.png"), mGrid(window, Vector2i(1, 3)), mMultiLine(multiLine) { + auto theme = ThemeData::getMenuTheme(); + mBackground.setImagePath(theme->Background.path); // ":/frame.png" + mBackground.setEdgeColor(theme->Background.color); + mBackground.setCenterColor(theme->Background.centerColor); + mBackground.setCornerSize(theme->Background.cornerSize); + addChild(&mBackground); addChild(&mGrid); @@ -21,7 +27,7 @@ GuiTextEditPopup::GuiTextEditPopup(Window* window, const std::string& title, con std::vector< std::shared_ptr > buttons; buttons.push_back(std::make_shared(mWindow, acceptBtnText, acceptBtnText, [this, okCallback] { okCallback(mText->getValue()); delete this; })); - buttons.push_back(std::make_shared(mWindow, "CANCEL", "discard changes", [this] { delete this; })); + buttons.push_back(std::make_shared(mWindow, _("CANCEL"), _("discard changes"), [this] { delete this; })); mButtonGrid = makeButtonGrid(mWindow, buttons); @@ -68,6 +74,6 @@ bool GuiTextEditPopup::input(InputConfig* config, Input input) std::vector GuiTextEditPopup::getHelpPrompts() { std::vector prompts = mGrid.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", "back")); + prompts.push_back(HelpPrompt("b", _("BACK"))); return prompts; } diff --git a/es-core/src/guis/GuiTextEditPopupKeyboard.cpp b/es-core/src/guis/GuiTextEditPopupKeyboard.cpp new file mode 100644 index 0000000000..01f4c65e3f --- /dev/null +++ b/es-core/src/guis/GuiTextEditPopupKeyboard.cpp @@ -0,0 +1,323 @@ +#include "guis/GuiTextEditPopupKeyboard.h" +#include "components/MenuComponent.h" +#include "utils/StringUtil.h" +#include "Log.h" +#include "EsLocale.h" + +std::vector> kbUs { + + { "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "_", "+" }, + { "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "-", "=" }, + + { "ร ", "รค", "รจ", "รซ", "รฌ", "รฏ", "รฒ", "รถ", "รน", "รผ", "ยจ", "ยฟ" }, + { "รก", "รข", "รฉ", "รช", "รญ", "รฎ", "รณ", "รด", "รบ", "รป", "รฑ", "ยก" }, + + { "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "{", "}" }, + { "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "[", "]" }, + + { "a", "s", "d", "f", "g", "h", "j", "k", "l", ";", "\"", "|" }, + { "A", "S", "D", "F", "G", "H", "J", "K", "L", ":", "'", "\\" }, + + { "SHIFT", "~", "z", "x", "c", "v", "b", "n", "m", ",", ".", "?" }, + { "SHIFT", "`", "Z", "X", "C", "V", "B", "N", "M", "<", ">", "/" }, +}; + +std::vector> kbFr { + { "&", "รฉ", "\"", "'", "(", "#", "รจ", "!", "รง", "ร ", ")", "-" }, + { "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "@", "_" }, + + { "ร ", "รค", "รซ", "รฌ", "รฏ", "รฒ", "รถ", "รผ", "\\", "|", "ยง", "ยฐ" }, + { "รก", "รข", "รช", "รญ", "รฎ", "รณ", "รด", "รบ", "รป", "รฑ", "ยก", "ยฟ" }, + + { "a", "z", "e", "r", "t", "y", "u", "i", "o", "p", "^", "$" }, + { "A", "Z", "E", "R", "T", "Y", "U", "I", "O", "P", "ยจ", "*" }, + + { "q", "s", "d", "f", "g", "h", "j", "k", "l", "m", "รน", "`" }, + { "Q", "S", "D", "F", "G", "H", "J", "K", "L", "M", "%", "ยฃ" }, + + { "SHIFT", "<", "w", "x", "c", "v", "b", "n", ",", ";", ":", "=" }, + { "SHIFT", ">", "W", "X", "C", "V", "B", "N", "?", ".", "/", "+" } +}; + +GuiTextEditPopupKeyboard::GuiTextEditPopupKeyboard(Window* window, const std::string& title, const std::string& initValue, + const std::function& okCallback, bool multiLine, const std::string acceptBtnText) + : GuiComponent(window), mBackground(window, ":/frame.png"), mGrid(window, Vector2i(1, 7)), mMultiLine(multiLine) +{ + mOkCallback = okCallback; + + auto theme = ThemeData::getMenuTheme(); + mBackground.setImagePath(theme->Background.path); + mBackground.setEdgeColor(theme->Background.color); + mBackground.setCenterColor(theme->Background.centerColor); + mBackground.setCornerSize(theme->Background.cornerSize); + + addChild(&mBackground); + addChild(&mGrid); + + mTitle = std::make_shared(mWindow, Utils::String::toUpper(title), theme->Title.font, theme->Title.color, ALIGN_CENTER); + + // Accept/Cancel/Delete/Space buttons + std::vector > buttons; + + buttons.push_back(std::make_shared(mWindow, acceptBtnText, acceptBtnText, [this, okCallback] { okCallback(mText->getValue()); delete this; })); + auto space = std::make_shared(mWindow, _("SPACE"), _("SPACE"), [this] { + mText->startEditing(); + mText->textInput(" "); + mText->stopEditing(); + }); + + if (Renderer::isSmallScreen()) + space->setSize(space->getSize().x(), space->getSize().y()); + else + space->setSize(space->getSize().x() * 3, space->getSize().y()); + + buttons.push_back(space); + buttons.push_back(std::make_shared(mWindow, _("DELETE"), _("DELETE A CHAR"), [this] { + mText->startEditing(); + mText->textInput("\b"); + mText->stopEditing(); + })); + + buttons.push_back(std::make_shared(mWindow, _("RESET"), _("RESET"), [this, okCallback] { okCallback(""); delete this; })); + + buttons.push_back(std::make_shared(mWindow, _("CANCEL"), _("DISCARD CHANGES"), [this] { delete this; })); + + // Add buttons + mButtons = makeButtonGrid(mWindow, buttons); + + mKeyboardGrid = std::make_shared(mWindow, Vector2i(12, 5)); + + mText = std::make_shared(mWindow); + mText->setValue(initValue); + + if (!multiLine) + mText->setCursor(initValue.size()); + + // Header + mGrid.setEntry(mTitle, Vector2i(0, 0), false, true); + + // Text edit add + mGrid.setEntry(mText, Vector2i(0, 1), true, false, Vector2i(1, 1), GridFlags::BORDER_TOP | GridFlags::BORDER_BOTTOM); + + std::vector< std::vector< std::shared_ptr > > buttonList; + + // Keyboard + // Case for if multiline is enabled, then don't create the keyboard. + if (!mMultiLine) + { + std::vector>* layout = &kbUs; + + std::string language = EsLocale::getLanguage(); + if (!language.empty()) + { + auto shortNameDivider = language.find("_"); + if (shortNameDivider != std::string::npos) + language = Utils::String::toLower(language.substr(0, shortNameDivider)); + } + + if (language == "fr") + layout = &kbFr; + + for (unsigned int i = 0; i < 5; i++) + { + std::vector> buttons; + for (unsigned int j = 0; j < 12; j++) + { + std::string lower = (*layout)[2 * i][j]; + std::string upper = (*layout)[2 * i + 1][j]; + + std::shared_ptr button = nullptr; + + if (lower == "SHIFT") + { + // Special case for shift key + mShiftButton = std::make_shared(mWindow, _U("\uF176"), _("SHIFTS FOR UPPER,LOWER, AND SPECIAL"), [this] { + shiftKeys(); + }, false); + + button = mShiftButton; + } + else + button = makeButton(lower, upper); + + button->setRenderNonFocusedBackground(false); + buttons.push_back(button); + + button->setSize(getButtonSize()); + mKeyboardGrid->setEntry(button, Vector2i(j, i), true, false); + buttonList.push_back(buttons); + } + } + // END KEYBOARD IF + } + + // Add keyboard keys + mGrid.setEntry(mKeyboardGrid, Vector2i(0, 2), true, true, Vector2i(2, 4)); + mGrid.setEntry(mButtons, Vector2i(0, 6), true, false); + + // Determine size from text size + float textHeight = mText->getFont()->getHeight(); + if (multiLine) + textHeight *= 6; + mText->setSize(0, textHeight); + + // If multiline, set all diminsions back to default, else draw size for keyboard. + if (mMultiLine) + { + if (Renderer::isSmallScreen()) + setSize(Renderer::getScreenWidth(), Renderer::getScreenHeight()); + else + setSize(Renderer::getScreenWidth() * 0.5f, mTitle->getFont()->getHeight() + textHeight + mKeyboardGrid->getSize().y() + 40); + + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, (Renderer::getScreenHeight() - mSize.y()) / 2); + } + else + { + if (Renderer::isSmallScreen()) + setSize(Renderer::getScreenWidth(), Renderer::getScreenHeight()); + else // Set size based on ScreenHieght * .08f by the amount of keyboard rows there are. + setSize(Renderer::getScreenWidth() * 0.95f, mTitle->getFont()->getHeight() + textHeight + 40 + (Renderer::getScreenHeight() * 0.085f) * 6); + + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, (Renderer::getScreenHeight() - mSize.y()) / 2); + } +} + + +void GuiTextEditPopupKeyboard::onSizeChanged() +{ + mBackground.fitTo(mSize, Vector3f::Zero(), Vector2f(-32, -32)); + + mText->setSize(mSize.x() - 40, mText->getSize().y()); + + // update grid + mGrid.setRowHeightPerc(0, mTitle->getFont()->getHeight() / mSize.y()); + mGrid.setRowHeightPerc(2, mKeyboardGrid->getSize().y() / mSize.y()); + mGrid.setRowHeightPerc(6, mButtons->getSize().y() / mSize.y()); + + mGrid.setSize(mSize); + + // force the keyboard size and position here + // for an unknown reason, without setting that, the position is "sometimes" (1/2 on s905x for example) not displayed correctly + // as if a variable were not correctly initialized + + if (Renderer::isSmallScreen()) // small screens // batocera + { + mKeyboardGrid->setSize(getButtonSize().x() * 12.0f, getButtonSize().y() * 5.0f); + mKeyboardGrid->setPosition(Renderer::getScreenWidth() * 0.05f / 2.00f, mTitle->getFont()->getHeight() + mText->getFont()->getHeight() + 15 + 6); + } + else + { + mKeyboardGrid->setSize(getButtonSize().x() * 12.2f, getButtonSize().y() * 5.2f); // Small margin between buttons + mKeyboardGrid->setPosition(Renderer::getScreenWidth() * 0.05f / 2.00f, mTitle->getFont()->getHeight() + mText->getFont()->getHeight() + 40 + 6); + } +} + +bool GuiTextEditPopupKeyboard::input(InputConfig* config, Input input) +{ + if (GuiComponent::input(config, input)) + return true; + + // pressing back when not text editing closes us + if (config->isMappedTo("b", input) && input.value) + { + delete this; + return true; + } + + // For deleting a chara (Left Top Button) + if (config->isMappedTo("pageup", input) && input.value) { + mText->startEditing(); + mText->textInput("\b"); + mText->stopEditing(); + } + + // For Adding a space (Right Top Button) + if (config->isMappedTo("pagedown", input) && input.value) { + mText->startEditing(); + mText->textInput(" "); + } + + // For Shifting (Y) + if (config->isMappedTo("y", input) && input.value) { + if (mShift) mShift = false; + else mShift = true; + shiftKeys(); + } + + + + if (config->isMappedTo("x", input) && input.value && mOkCallback != nullptr) + { + mOkCallback(""); + delete this; + return true; + } + + return false; +} +/* +void GuiTextEditPopupKeyboard::update(int deltatime) { + +}*/ + +// Shifts the keys when user hits the shift button. +void GuiTextEditPopupKeyboard::shiftKeys() +{ + mShift = !mShift; + + if (mShift) + mShiftButton->setColorShift(0xFF0000FF); + else + mShiftButton->removeColorShift(); + + for (auto & kb : keyboardButtons) + { + const std::string& text = mShift ? kb.shiftedKey : kb.key; + kb.button->setText(text, text, false); + kb.button->setSize(getButtonSize()); + } +} + +std::vector GuiTextEditPopupKeyboard::getHelpPrompts() +{ + std::vector prompts = mGrid.getHelpPrompts(); + + if (mOkCallback != nullptr) + prompts.push_back(HelpPrompt("x", _("RESET"))); + + prompts.push_back(HelpPrompt("y", _("SHIFT"))); + prompts.push_back(HelpPrompt("b", _("BACK"))); + prompts.push_back(HelpPrompt("r", _("SPACE"))); + prompts.push_back(HelpPrompt("l", _("DELETE"))); + return prompts; +} + +std::shared_ptr GuiTextEditPopupKeyboard::makeButton(const std::string& key, const std::string& shiftedKey) +{ + std::shared_ptr button = std::make_shared(mWindow, key, key, [this, key, shiftedKey] + { + mText->startEditing(); + + if (mShift) + mText->textInput(shiftedKey.c_str()); + else + mText->textInput(key.c_str()); + + mText->stopEditing(); + }, false); + + KeyboardButton kb(button, key, shiftedKey); + keyboardButtons.push_back(kb); + return button; +} + +const Vector2f GuiTextEditPopupKeyboard::getButtonSize() +{ + if (Renderer::isSmallScreen()) + { + float height = (Renderer::getScreenHeight() - mText->getSize().y() - mTitle->getSize().y() - mButtons->getSize().y()) / 6.0; + return Vector2f((Renderer::getScreenWidth() * 0.95f) / 12.0f, height); + } + + return Vector2f((Renderer::getScreenWidth() * 0.89f) / 12.0f, mText->getFont()->getHeight() + 6.0f); +} \ No newline at end of file diff --git a/es-core/src/guis/GuiTextEditPopupKeyboard.h b/es-core/src/guis/GuiTextEditPopupKeyboard.h new file mode 100644 index 0000000000..98f8d90fad --- /dev/null +++ b/es-core/src/guis/GuiTextEditPopupKeyboard.h @@ -0,0 +1,51 @@ +#include "GuiComponent.h" + +#include "components/NinePatchComponent.h" +#include "components/ButtonComponent.h" +#include "components/ComponentGrid.h" +#include "components/TextEditComponent.h" +#include "components/TextComponent.h" +#include + +class GuiTextEditPopupKeyboard : public GuiComponent +{ +public: + GuiTextEditPopupKeyboard(Window* window, const std::string& title, const std::string& initValue, + const std::function& okCallback, bool multiLine, const std::string acceptBtnText = "OK"); + + bool input(InputConfig* config, Input input); + //void update(int deltatime) override; + void onSizeChanged(); + std::vector getHelpPrompts() override; + +private: + class KeyboardButton + { + public: + std::shared_ptr button; + const std::string key; + const std::string shiftedKey; + KeyboardButton(const std::shared_ptr b, const std::string& k, const std::string& sk) : button(b), key(k), shiftedKey(sk) {}; + }; + + std::shared_ptr makeButton(const std::string& key, const std::string& shiftedKey); + std::vector keyboardButtons; + std::shared_ptr mShiftButton; + const Vector2f getButtonSize(); + + void shiftKeys(); + + NinePatchComponent mBackground; + ComponentGrid mGrid; + + std::shared_ptr mTitle; + std::shared_ptr mText; + std::shared_ptr mKeyboardGrid; + std::shared_ptr mButtons; + + std::function mOkCallback; + + bool mMultiLine; + bool mShift = false; +}; + diff --git a/es-core/src/math/Vector2f.cpp b/es-core/src/math/Vector2f.cpp index f98f17194c..ddf8fc351a 100644 --- a/es-core/src/math/Vector2f.cpp +++ b/es-core/src/math/Vector2f.cpp @@ -17,3 +17,25 @@ Vector2f& Vector2f::lerp(const Vector2f& _start, const Vector2f& _end, const flo return *this; } // lerp + +const Vector2f Vector2f::parseString(const std::string& _input) +{ + Vector2f ret = Vector2f(0, 0); + + size_t divider = _input.find(' '); + if (divider != std::string::npos) + { + std::string first = _input.substr(0, divider); + std::string second = _input.substr(divider, std::string::npos); + + ret = Vector2f((float)atof(first.c_str()), (float)atof(second.c_str())); + } + + return ret; +} + + +const std::string Vector2f::toString() +{ + return std::to_string(mX) + " " + std::to_string(mY); +} \ No newline at end of file diff --git a/es-core/src/math/Vector2f.h b/es-core/src/math/Vector2f.h index b9c91fda08..dab7eb0372 100644 --- a/es-core/src/math/Vector2f.h +++ b/es-core/src/math/Vector2f.h @@ -2,6 +2,7 @@ #ifndef ES_CORE_MATH_VECTOR2F_H #define ES_CORE_MATH_VECTOR2F_H +#include #include "math/Misc.h" #include @@ -11,6 +12,8 @@ class Vector4f; class Vector2f { public: + static const Vector2f parseString(const std::string& _input); + const std::string toString(); Vector2f() { } Vector2f(const float _f) : mX(_f), mY(_f) { } @@ -58,6 +61,8 @@ class Vector2f static const Vector2f UnitX() { return { 1, 0 }; } static const Vector2f UnitY() { return { 0, 1 }; } + inline bool empty() { return mX == 0.0 && mY == 0.0; } + private: float mX; diff --git a/es-core/src/platform.cpp b/es-core/src/platform.cpp index 81262310b7..e50a98e1d5 100644 --- a/es-core/src/platform.cpp +++ b/es-core/src/platform.cpp @@ -1,49 +1,203 @@ -#include "platform.h" - -#include -#ifdef WIN32 -#include -#else -#include -#endif -#include - -#include "Log.h" - -int runShutdownCommand() -{ -#ifdef WIN32 // windows - return system("shutdown -s -t 0"); -#else // osx / linux - return system("sudo shutdown -h now"); -#endif -} - -int runRestartCommand() -{ -#ifdef WIN32 // windows - return system("shutdown -r -t 0"); -#else // osx / linux - return system("sudo shutdown -r now"); -#endif -} - -int runSystemCommand(const std::string& cmd_utf8) -{ -#ifdef WIN32 - // on Windows we use _wsystem to support non-ASCII paths - // which requires converting from utf8 to a wstring - typedef std::codecvt_utf8 convert_type; - std::wstring_convert converter; - std::wstring wchar_str = converter.from_bytes(cmd_utf8); - return _wsystem(wchar_str.c_str()); -#else - return system(cmd_utf8.c_str()); -#endif -} - -QuitMode quitMode = QuitMode::QUIT; - +#include "platform.h" +#include + +#ifdef WIN32 +#include +#include +#else +#include +#endif +#include + +#include "Window.h" +#include "Log.h" + +#include "GuiComponent.h" +#include "utils/FileSystemUtil.h" +#include "utils/StringUtil.h" + +int runShutdownCommand() +{ +#ifdef WIN32 // windows + return system("shutdown -s -t 0"); +#else // osx / linux + return system("sudo shutdown -h now"); +#endif +} + +int runRestartCommand() +{ +#ifdef WIN32 // windows + return system("shutdown -r -t 0"); +#else // osx / linux + return system("sudo shutdown -r now"); +#endif +} + +void splitCommand(std::string cmd, std::string* executable, std::string* parameters) +{ + std::string c = Utils::String::trim(cmd); + size_t exec_end; + + if (c[0] == '\"') + { + exec_end = c.find_first_of('\"', 1); + if (std::string::npos != exec_end) + { + *executable = c.substr(1, exec_end - 1); + *parameters = c.substr(exec_end + 1); + } + else + { + *executable = c.substr(1, exec_end); + std::string().swap(*parameters); + } + } + else + { + exec_end = c.find_first_of(' ', 0); + if (std::string::npos != exec_end) + { + *executable = c.substr(0, exec_end); + *parameters = c.substr(exec_end + 1); + } + else + { + *executable = c.substr(0, exec_end); + std::string().swap(*parameters); + } + } +} + +#ifdef WIN32 +int _monitorEnumIndex = 0; +HMONITOR _monitorEnumHandle = 0; + +BOOL CALLBACK MonitorEnumProc(HMONITOR hMonitor, HDC hdcMonitor, LPRECT lprcMonitor, LPARAM dwData) +{ + if (_monitorEnumIndex == dwData) + { + _monitorEnumHandle = hMonitor; + return FALSE; + } + + _monitorEnumIndex++; + return TRUE; // continue enumerating +} +#endif + +int runSystemCommand(const std::string& cmd_utf8, const std::string& name, Window* window) +{ +#ifdef WIN32 + +#if _DEBUG + return 0; +#endif + + if (window != NULL) + window->renderGameLoadingScreen(); + + // on Windows we use _wsystem to support non-ASCII paths + // which requires converting from utf8 to a wstring + //typedef std::codecvt_utf8 convert_type; + //std::wstring_convert converter; + //std::wstring wchar_str = converter.from_bytes(cmd_utf8); + std::string command = cmd_utf8; + + #define BUFFER_SIZE 8192 + + TCHAR szEnvPath[BUFFER_SIZE]; + DWORD dwLen = ExpandEnvironmentStringsA(command.c_str(), szEnvPath, BUFFER_SIZE); + if (dwLen > 0 && dwLen < BUFFER_SIZE) + command = std::string(szEnvPath); + + std::string exe; + std::string args; + + splitCommand(command, &exe, &args); + exe = Utils::FileSystem::getPreferredPath(exe); + + SHELLEXECUTEINFO lpExecInfo; + lpExecInfo.cbSize = sizeof(SHELLEXECUTEINFO); + lpExecInfo.lpFile = exe.c_str(); + lpExecInfo.fMask = SEE_MASK_DOENVSUBST | SEE_MASK_NOCLOSEPROCESS; + lpExecInfo.hwnd = NULL; + lpExecInfo.lpVerb = "open"; // to open program + + lpExecInfo.lpDirectory = NULL; + lpExecInfo.nShow = SW_SHOW; // show command prompt with normal window size + lpExecInfo.hInstApp = (HINSTANCE)SE_ERR_DDEFAIL; //WINSHELLAPI BOOL WINAPI result; + + std::string extraConfigFile; + + int monitorId = Settings::getInstance()->getInt("MonitorID"); + if (monitorId > 0) + { + _monitorEnumIndex = 0; + _monitorEnumHandle = 0; + EnumDisplayMonitors(NULL, NULL, MonitorEnumProc, monitorId); + if (_monitorEnumHandle != 0) + { + // Special processing for retroarch -> Set monitor index in the extra config file ( and disable save_on_exit ) + if (Utils::String::toLower(exe).find("retroarch.") != std::string::npos) + { + std::string video_monitor_index = "video_monitor_index = \""+ std::to_string(monitorId+1) +"\"\r\nconfig_save_on_exit = \"false\"\r\n"; + extraConfigFile = Utils::FileSystem::getGenericPath(Utils::FileSystem::getHomePath() + "/retroarch.custom.cfg"); + Utils::FileSystem::writeAllText(extraConfigFile, video_monitor_index); + args = args + " --appendconfig \""+ extraConfigFile +"\""; + } + + lpExecInfo.fMask |= SEE_MASK_HMONITOR; + lpExecInfo.hIcon = _monitorEnumHandle; + } + } + + lpExecInfo.lpParameters = args.c_str(); // file name as an argument + + // Don't set directory for relative paths + if (!Utils::String::startsWith(exe, ".") && !Utils::String::startsWith(exe, "/") && !Utils::String::startsWith(exe, "\\")) + lpExecInfo.lpDirectory = Utils::FileSystem::getAbsolutePath(Utils::FileSystem::getParent(exe)).c_str(); + + ShellExecuteEx(&lpExecInfo); + + if (lpExecInfo.hProcess != NULL) + { + if (window == NULL) + WaitForSingleObject(lpExecInfo.hProcess, INFINITE); + else + { + while (WaitForSingleObject(lpExecInfo.hProcess, 50) == 0x00000102L) + { + bool polled = false; + + SDL_Event event; + while (SDL_PollEvent(&event)) + polled = true; + + if (window != NULL && polled) + window->renderGameLoadingScreen(); + } + } + + if (Utils::FileSystem::exists(extraConfigFile)) + Utils::FileSystem::removeFile(extraConfigFile); + + CloseHandle(lpExecInfo.hProcess); + return 0; + } + + if (Utils::FileSystem::exists(extraConfigFile)) + Utils::FileSystem::removeFile(extraConfigFile); + + return 1; + //return _wsystem(wchar_str.c_str()); +#else + return system(cmd_utf8.c_str()); +#endif +} + +QuitMode quitMode = QuitMode::QUIT; + int quitES(QuitMode mode) { quitMode = mode; @@ -52,38 +206,38 @@ int quitES(QuitMode mode) quit->type = SDL_QUIT; SDL_PushEvent(quit); return 0; -} - -void touch(const std::string& filename) -{ -#ifdef WIN32 - FILE* fp = fopen(filename.c_str(), "ab+"); - if (fp != NULL) - fclose(fp); -#else - int fd = open(filename.c_str(), O_CREAT|O_WRONLY, 0644); - if (fd >= 0) - close(fd); -#endif -} - -void processQuitMode() -{ - switch (quitMode) - { - case QuitMode::RESTART: - LOG(LogInfo) << "Restarting EmulationStation"; - touch("/tmp/es-restart"); - break; - case QuitMode::REBOOT: - LOG(LogInfo) << "Rebooting system"; - touch("/tmp/es-sysrestart"); - runRestartCommand(); - break; - case QuitMode::SHUTDOWN: - LOG(LogInfo) << "Shutting system down"; - touch("/tmp/es-shutdown"); - runShutdownCommand(); - break; - } -} +} + +void touch(const std::string& filename) +{ +#ifdef WIN32 + FILE* fp = fopen(filename.c_str(), "ab+"); + if (fp != NULL) + fclose(fp); +#else + int fd = open(filename.c_str(), O_CREAT|O_WRONLY, 0644); + if (fd >= 0) + close(fd); +#endif +} + +void processQuitMode() +{ + switch (quitMode) + { + case QuitMode::RESTART: + LOG(LogInfo) << "Restarting EmulationStation"; + touch("/tmp/es-restart"); + break; + case QuitMode::REBOOT: + LOG(LogInfo) << "Rebooting system"; + touch("/tmp/es-sysrestart"); + runRestartCommand(); + break; + case QuitMode::SHUTDOWN: + LOG(LogInfo) << "Shutting system down"; + touch("/tmp/es-shutdown"); + runShutdownCommand(); + break; + } +} \ No newline at end of file diff --git a/es-core/src/platform.h b/es-core/src/platform.h index 1df4d56c68..82f090fc95 100644 --- a/es-core/src/platform.h +++ b/es-core/src/platform.h @@ -9,6 +9,8 @@ #define sleep Sleep #endif +class Window; + enum QuitMode { QUIT = 0, @@ -17,8 +19,65 @@ enum QuitMode REBOOT = 3 }; -int runSystemCommand(const std::string& cmd_utf8); // run a utf-8 encoded in the shell (requires wstring conversion on Windows) +int runSystemCommand(const std::string& cmd_utf8, const std::string& name, Window* window); // run a utf-8 encoded in the shell (requires wstring conversion on Windows) int quitES(QuitMode mode = QuitMode::QUIT); void processQuitMode(); +#if defined(WIN32) +#include +#include +#include "Log.h" +#endif + +#if !defined(TRACE) +#if defined(WIN32) && defined(_DEBUG) + #include + + #define TRACE( s ) \ + { \ + std::ostringstream os_; \ + os_ << s << std::endl; \ + OutputDebugStringA( os_.str().c_str() ); \ + } +#else + #define TRACE(s) +#endif +#endif + +class StopWatch +{ +public: + StopWatch(std::string name) + { +#if defined(WIN32) + mName = name; + mTicks = ::GetTickCount(); +#endif + } + + ~StopWatch() + { +#if defined(WIN32) + int now = ::GetTickCount(); + + mTicks = now - mTicks; + + LOG(LogInfo) << mName << " " << mTicks << " ms" << " on CPU " << GetCurrentProcessorNumber(); + TRACE(mName << " " << mTicks << " ms" << " on CPU " << GetCurrentProcessorNumber()); +#endif + } + +private: +#if defined(WIN32) + DWORD GetCurrentProcessorNumber() + { + int CPUInfo[4]; + __cpuid(CPUInfo, 1); + if ((CPUInfo[3] & (1 << 9)) == 0) return -1; // no APIC on chip + return (unsigned)CPUInfo[1] >> 24; + } +#endif + int mTicks; + std::string mName; +}; #endif // ES_CORE_PLATFORM_H diff --git a/es-core/src/renderers/Renderer.cpp b/es-core/src/renderers/Renderer.cpp index 003e4ac489..362b082523 100644 --- a/es-core/src/renderers/Renderer.cpp +++ b/es-core/src/renderers/Renderer.cpp @@ -10,9 +10,15 @@ #include #include +#if WIN32 +#include +#endif + namespace Renderer { static std::stack clipStack; + static std::stack nativeClipStack; + static SDL_Window* sdlWindow = nullptr; static int windowWidth = 0; static int windowHeight = 0; @@ -23,6 +29,8 @@ namespace Renderer static int screenRotate = 0; static bool initialCursorState = 1; + static Vector2i sdlWindowPosition = Vector2i(SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED); + static void setIcon() { size_t width = 0; @@ -69,8 +77,21 @@ namespace Renderer initialCursorState = (SDL_ShowCursor(0) != 0); + if (!Settings::getInstance()->getBool("Windowed")) + SDL_SetHint(SDL_HINT_VIDEO_HIGHDPI_DISABLED, "1"); + SDL_DisplayMode dispMode; SDL_GetDesktopDisplayMode(0, &dispMode); + +#if WIN32 + if (!Settings::getInstance()->getBool("Windowed") && !Settings::getInstance()->getInt("WindowWidth")) + { + ::SetProcessDPIAware(); + dispMode.w = ::GetSystemMetrics(SM_CXSCREEN); + dispMode.h = ::GetSystemMetrics(SM_CYSCREEN); + } +#endif + windowWidth = Settings::getInstance()->getInt("WindowWidth") ? Settings::getInstance()->getInt("WindowWidth") : dispMode.w; windowHeight = Settings::getInstance()->getInt("WindowHeight") ? Settings::getInstance()->getInt("WindowHeight") : dispMode.h; screenWidth = Settings::getInstance()->getInt("ScreenWidth") ? Settings::getInstance()->getInt("ScreenWidth") : windowWidth; @@ -79,11 +100,51 @@ namespace Renderer screenOffsetY = Settings::getInstance()->getInt("ScreenOffsetY") ? Settings::getInstance()->getInt("ScreenOffsetY") : 0; screenRotate = Settings::getInstance()->getInt("ScreenRotate") ? Settings::getInstance()->getInt("ScreenRotate") : 0; + int monitorId = Settings::getInstance()->getInt("MonitorID"); + if (monitorId >= 0 && sdlWindowPosition == Vector2i(SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED)) + { + int displays = SDL_GetNumVideoDisplays(); + if (displays > monitorId) + { + SDL_Rect rc; + SDL_GetDisplayBounds(monitorId, &rc); + + sdlWindowPosition = Vector2i(rc.x, rc.y); + + if (Settings::getInstance()->getBool("Windowed") && (Settings::getInstance()->getInt("WindowWidth") || Settings::getInstance()->getInt("ScreenWidth"))) + { + if (windowWidth != rc.w || windowHeight != rc.h) + { + sdlWindowPosition = Vector2i( + rc.x + (rc.w - windowWidth) / 2, + rc.y + (rc.h - windowHeight) / 2 + ); + } + } + else + { + windowWidth = rc.w; + windowHeight = rc.h; + screenWidth = rc.w; + screenHeight = rc.h; + } + } + } + setupWindow(); unsigned int windowFlags = (Settings::getInstance()->getBool("Windowed") ? 0 : (Settings::getInstance()->getBool("FullscreenBorderless") ? SDL_WINDOW_BORDERLESS : SDL_WINDOW_FULLSCREEN)) | getWindowFlags(); - if((sdlWindow = SDL_CreateWindow("EmulationStation", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, windowWidth, windowHeight, windowFlags)) == nullptr) + if (Settings::getInstance()->getBool("AlwaysOnTop")) + windowFlags |= SDL_WINDOW_ALWAYS_ON_TOP; + + windowFlags |= SDL_WINDOW_ALLOW_HIGHDPI; + + if((sdlWindow = SDL_CreateWindow("EmulationStation", + sdlWindowPosition.x(), + sdlWindowPosition.y(), + windowWidth, windowHeight, + windowFlags)) == nullptr) { LOG(LogError) << "Error creating SDL window!\n\t" << SDL_GetError(); return false; @@ -101,6 +162,13 @@ namespace Renderer static void destroyWindow() { + if (Settings::getInstance()->getBool("Windowed") && Settings::getInstance()->getInt("WindowWidth") && Settings::getInstance()->getInt("WindowHeight")) + { + int x; int y; + SDL_GetWindowPosition(sdlWindow, &x, &y); + sdlWindowPosition = Vector2i(x, y); // Save position to restore it later + } + destroyContext(); SDL_DestroyWindow(sdlWindow); @@ -112,6 +180,12 @@ namespace Renderer } // destroyWindow + void activateWindow() + { + SDL_RaiseWindow(sdlWindow); + SDL_SetWindowInputFocus(sdlWindow); + } + bool init() { if(!createWindow()) @@ -216,6 +290,7 @@ namespace Renderer if(box.h < 0) box.h = 0; clipStack.push(box); + nativeClipStack.push(Rect(_pos.x(), _pos.y(), _size.x(), _size.y())); setScissor(box); @@ -230,14 +305,63 @@ namespace Renderer } clipStack.pop(); + nativeClipStack.pop(); if(clipStack.empty()) setScissor(Rect(0, 0, 0, 0)); else setScissor(clipStack.top()); } // popClipRect + bool isClippingEnabled() { return !clipStack.empty(); } + + bool valueInRange(int value, int min, int max) + { + return (value >= min) && (value <= max); + } + + bool rectOverlap(Rect &A, Rect &B) + { + bool xOverlap = valueInRange(A.x, B.x, B.x + B.w) || + valueInRange(B.x, A.x, A.x + A.w); + + bool yOverlap = valueInRange(A.y, B.y, B.y + B.h) || + valueInRange(B.y, A.y, A.y + A.h); + + return xOverlap && yOverlap; + } + + bool isVisibleOnScreen(float x, float y, float w, float h) + { + Rect screen = Rect(0, 0, Renderer::getWindowWidth(), Renderer::getWindowHeight()); + Rect box = Rect(x, y, w, h); + + if (w > 0 && x + w <= 0) + return false; + + if (h > 0 && y + h <= 0) + return false; + + if (x == screen.w || y == screen.h) + return false; + + if (!rectOverlap(box, screen)) + return false; + + if (clipStack.empty()) + return true; + + screen = nativeClipStack.top(); + return rectOverlap(screen, box); + } + + void drawRect(const float _x, const float _y, const float _w, const float _h, const unsigned int _color, const Blend::Factor _srcBlendFactor, const Blend::Factor _dstBlendFactor) + { + drawRect(_x, _y, _w, _h, _color, _color, true, _srcBlendFactor, _dstBlendFactor); + } // drawRect + void drawRect(const float _x, const float _y, const float _w, const float _h, const unsigned int _color, const unsigned int _colorEnd, bool horizontalGradient, const Blend::Factor _srcBlendFactor, const Blend::Factor _dstBlendFactor) { + const unsigned int color = convertColor(_color); const unsigned int colorEnd = convertColor(_colorEnd); Vertex vertices[4]; @@ -265,4 +389,25 @@ namespace Renderer int getScreenOffsetY() { return screenOffsetY; } int getScreenRotate() { return screenRotate; } + bool isSmallScreen() { return screenWidth < 400 || screenHeight < 400; }; + + unsigned int mixColors(unsigned int first, unsigned int second, float percent) + { + unsigned char alpha0 = (first >> 24) & 0xFF; + unsigned char blue0 = (first >> 16) & 0xFF; + unsigned char green0 = (first >> 8) & 0xFF; + unsigned char red0 = first & 0xFF; + + unsigned char alpha1 = (second >> 24) & 0xFF; + unsigned char blue1 = (second >> 16) & 0xFF; + unsigned char green1 = (second >> 8) & 0xFF; + unsigned char red1 = second & 0xFF; + + unsigned char alpha = (unsigned char)(alpha0 * (1.0 - percent) + alpha1 * percent); + unsigned char blue = (unsigned char)(blue0 * (1.0 - percent) + blue1 * percent); + unsigned char green = (unsigned char)(green0 * (1.0 - percent) + green1 * percent); + unsigned char red = (unsigned char)(red0 * (1.0 - percent) + red1 * percent); + + return (alpha << 24) | (blue << 16) | (green << 8) | red; + } } // Renderer:: diff --git a/es-core/src/renderers/Renderer.h b/es-core/src/renderers/Renderer.h index 3b41e338d4..79d01f4347 100644 --- a/es-core/src/renderers/Renderer.h +++ b/es-core/src/renderers/Renderer.h @@ -66,7 +66,8 @@ namespace Renderer void deinit (); void pushClipRect (const Vector2i& _pos, const Vector2i& _size); void popClipRect (); - void drawRect (const float _x, const float _y, const float _w, const float _h, const unsigned int _color, const unsigned int _colorEnd, bool horizontalGradient = false, const Blend::Factor _srcBlendFactor = Blend::SRC_ALPHA, const Blend::Factor _dstBlendFactor = Blend::ONE_MINUS_SRC_ALPHA); + void drawRect (const float _x, const float _y, const float _w, const float _h, const unsigned int _color, const Blend::Factor _srcBlendFactor = Blend::SRC_ALPHA, const Blend::Factor _dstBlendFactor = Blend::ONE_MINUS_SRC_ALPHA); + void drawRect (const float _x, const float _y, const float _w, const float _h, const unsigned int _color, const unsigned int _colorEnd, bool horizontalGradient = false, const Blend::Factor _srcBlendFactor = Blend::SRC_ALPHA, const Blend::Factor _dstBlendFactor = Blend::ONE_MINUS_SRC_ALPHA); SDL_Window* getSDLWindow (); int getWindowWidth (); @@ -96,6 +97,23 @@ namespace Renderer void setSwapInterval (); void swapBuffers (); + // FCA methods + bool isClippingEnabled(); + bool isVisibleOnScreen(float x, float y, float w, float h); + void activateWindow(); +// void drawGradientRect(int _x, int _y, int _w, int _h, unsigned int _color, unsigned int _colorBottom, bool _horz = false, const Blend::Factor _srcBlendFactor = Blend::SRC_ALPHA, const Blend::Factor _dstBlendFactor = Blend::ONE_MINUS_SRC_ALPHA); + + // GPI Case + bool isSmallScreen(); + + unsigned int mixColors(unsigned int first, unsigned int second, float percent); + + + void drawRoundRect(float x, float y, float w, float h, float radius, unsigned int color, const Blend::Factor _srcBlendFactor = Blend::SRC_ALPHA, const Blend::Factor _dstBlendFactor = Blend::ONE_MINUS_SRC_ALPHA); + + void enableRoundCornerStencil(float x, float y, float size_x, float size_y, float radius); + void disableStencil(); + } // Renderer:: #endif // ES_CORE_RENDERER_RENDERER_H diff --git a/es-core/src/renderers/Renderer_GL21.cpp b/es-core/src/renderers/Renderer_GL21.cpp index a792bfab24..aee6aba9d2 100644 --- a/es-core/src/renderers/Renderer_GL21.cpp +++ b/es-core/src/renderers/Renderer_GL21.cpp @@ -7,6 +7,7 @@ #include #include +#include namespace Renderer { @@ -62,6 +63,8 @@ namespace Renderer void setupWindow() { + SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 1); + SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8); SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8); SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8); @@ -71,6 +74,9 @@ namespace Renderer SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 1); + // Antialias : Not supported on every machine + // SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, 2); + } // setupWindow void createContext() @@ -78,7 +84,7 @@ namespace Renderer sdlContext = SDL_GL_CreateContext(getSDLWindow()); SDL_GL_MakeCurrent(getSDLWindow(), sdlContext); - glClearColor(0.5f, 0.5f, 0.5f, 0.0f); + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); std::string glExts = (const char*)glGetString(GL_EXTENSIONS); LOG(LogInfo) << "Checking available OpenGL extensions..."; @@ -104,7 +110,7 @@ namespace Renderer glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, _repeat ? GL_REPEAT : GL_CLAMP_TO_EDGE); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, _repeat ? GL_REPEAT : GL_CLAMP_TO_EDGE); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, _linear ? GL_LINEAR : GL_NEAREST); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, _linear ? GL_LINEAR : GL_NEAREST); glPixelStorei(GL_PACK_ALIGNMENT, 1); @@ -125,7 +131,15 @@ namespace Renderer void updateTexture(const unsigned int _texture, const Texture::Type _type, const unsigned int _x, const unsigned _y, const unsigned int _width, const unsigned int _height, void* _data) { bindTexture(_texture); - glTexSubImage2D(GL_TEXTURE_2D, 0, _x, _y, _width, _height, convertTextureType(_type), GL_UNSIGNED_BYTE, _data); + + if (_x == -1 && _y == -1) + { + const GLenum type = convertTextureType(_type); + glTexImage2D(GL_TEXTURE_2D, 0, type, _width, _height, 0, type, GL_UNSIGNED_BYTE, _data); + } + else + glTexSubImage2D(GL_TEXTURE_2D, 0, _x, _y, _width, _height, convertTextureType(_type), GL_UNSIGNED_BYTE, _data); + bindTexture(0); } // updateTexture @@ -244,11 +258,126 @@ namespace Renderer void swapBuffers() { +#ifdef WIN32 + glFlush(); + glFinish(); + Sleep(0); +#endif + SDL_GL_SwapWindow(getSDLWindow()); + +#ifdef WIN32 + Sleep(0); +#endif + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); } // swapBuffers + #define ROUNDING_PIECES 8.0f + + void drawGLRoundedCorner(float x, float y, double sa, double arc, float r, unsigned int color, std::vector &vertex) + { + float red = (((color & 0xff000000) >> 24) & 255) / 255.0f; + float g = (((color & 0x00ff0000) >> 16) & 255) / 255.0f; + float b = (((color & 0x0000ff00) >> 8) & 255) / 255.0f; + float a = (((color & 0x000000ff)) & 255) / 255.0f; + + // centre of the arc, for clockwise sense + float cent_x = x + r * Math::cosf(sa + ES_PI / 2.0f); + float cent_y = y + r * Math::sinf(sa + ES_PI / 2.0f); + + // build up piecemeal including end of the arc + int n = ceil(ROUNDING_PIECES * arc / ES_PI * 2.0f); + for (int i = 0; i <= n; i++) + { + float ang = sa + arc * (double)i / (double)n; + + // compute the next point + float next_x = cent_x + r * Math::sinf(ang); + float next_y = cent_y - r * Math::cosf(ang); + + Vertex vx; + vx.pos = Vector2f(next_x, next_y); + vx.tex = Vector2f(0, 0); + vx.col = color; + vertex.push_back(vx); + } + } + + void drawRoundRect(float x, float y, float width, float height, float radius, unsigned int color, const Blend::Factor _srcBlendFactor, const Blend::Factor _dstBlendFactor) + { + auto finalColor = convertColor(color); + + std::vector vertex; + drawGLRoundedCorner(x, y + radius, 3.0f * ES_PI / 2.0f, ES_PI / 2.0f, radius, finalColor, vertex); + drawGLRoundedCorner(x + width - radius, y, 0.0, ES_PI / 2.0f, radius, finalColor, vertex); + drawGLRoundedCorner(x + width, y + height - radius, ES_PI / 2.0f, ES_PI / 2.0f, radius, finalColor, vertex); + drawGLRoundedCorner(x + radius, y + height, ES_PI, ES_PI / 2.0f, radius, finalColor, vertex); + + Vertex* vxs = new Vertex[vertex.size()]; + for (int i = 0; i < vertex.size(); i++) + vxs[i] = vertex[i]; + + bindTexture(0); + + glEnable(GL_MULTISAMPLE); + + glEnable(GL_BLEND); + glBlendFunc(convertBlendFactor(_srcBlendFactor), convertBlendFactor(_dstBlendFactor)); + + glEnableClientState(GL_VERTEX_ARRAY); + glEnableClientState(GL_TEXTURE_COORD_ARRAY); + glEnableClientState(GL_COLOR_ARRAY); + + glVertexPointer(2, GL_FLOAT, sizeof(Vertex), &vxs[0].pos); + glTexCoordPointer(2, GL_FLOAT, sizeof(Vertex), &vxs[0].tex); + glColorPointer(4, GL_UNSIGNED_BYTE, sizeof(Vertex), &vxs[0].col); + + glDrawArrays(GL_TRIANGLE_FAN, 0, vertex.size()); + + glDisableClientState(GL_COLOR_ARRAY); + glDisableClientState(GL_TEXTURE_COORD_ARRAY); + glDisableClientState(GL_VERTEX_ARRAY); + + delete[] vxs; + + glDisable(GL_BLEND); + glDisable(GL_MULTISAMPLE); + } + + void enableRoundCornerStencil(float x, float y, float width, float height, float radius) + { + bool tx = glIsEnabled(GL_TEXTURE_2D); + glDisable(GL_TEXTURE_2D); + + glClear(GL_DEPTH_BUFFER_BIT); + glEnable(GL_STENCIL_TEST); + glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); + glDepthMask(GL_FALSE); + glStencilFunc(GL_NEVER, 1, 0xFF); + glStencilOp(GL_REPLACE, GL_KEEP, GL_KEEP); + + glStencilMask(0xFF); + glClear(GL_STENCIL_BUFFER_BIT); + + drawRoundRect(x, y, width, height, radius, 0xFFFFFFFF); + + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + glDepthMask(GL_TRUE); + glStencilMask(0x00); + glStencilFunc(GL_EQUAL, 0, 0xFF); + glStencilFunc(GL_EQUAL, 1, 0xFF); + + if (tx) + glEnable(GL_TEXTURE_2D); + } + + void disableStencil() + { + glDisable(GL_STENCIL_TEST); + } + } // Renderer:: #endif // USE_OPENGL_21 diff --git a/es-core/src/renderers/Renderer_GLES10.cpp b/es-core/src/renderers/Renderer_GLES10.cpp index adb417ea1f..0963b97069 100644 --- a/es-core/src/renderers/Renderer_GLES10.cpp +++ b/es-core/src/renderers/Renderer_GLES10.cpp @@ -3,9 +3,11 @@ #include "renderers/Renderer.h" #include "Log.h" #include "Settings.h" +#include "math/Transform4x4f.h" #include #include +#include namespace Renderer { @@ -77,7 +79,7 @@ namespace Renderer sdlContext = SDL_GL_CreateContext(getSDLWindow()); SDL_GL_MakeCurrent(getSDLWindow(), sdlContext); - glClearColor(0.5f, 0.5f, 0.5f, 0.0f); + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); std::string glExts = (const char*)glGetString(GL_EXTENSIONS); LOG(LogInfo) << "Checking available OpenGL extensions..."; @@ -103,7 +105,7 @@ namespace Renderer glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, _repeat ? GL_REPEAT : GL_CLAMP_TO_EDGE); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, _repeat ? GL_REPEAT : GL_CLAMP_TO_EDGE); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, _linear ? GL_LINEAR : GL_NEAREST); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, _linear ? GL_LINEAR : GL_NEAREST); glPixelStorei(GL_PACK_ALIGNMENT, 1); @@ -124,7 +126,15 @@ namespace Renderer void updateTexture(const unsigned int _texture, const Texture::Type _type, const unsigned int _x, const unsigned _y, const unsigned int _width, const unsigned int _height, void* _data) { bindTexture(_texture); - glTexSubImage2D(GL_TEXTURE_2D, 0, _x, _y, _width, _height, convertTextureType(_type), GL_UNSIGNED_BYTE, _data); + + if (_x == -1 && _y == -1) + { + const GLenum type = convertTextureType(_type); + glTexImage2D(GL_TEXTURE_2D, 0, type, _width, _height, 0, type, GL_UNSIGNED_BYTE, _data); + } + else + glTexSubImage2D(GL_TEXTURE_2D, 0, _x, _y, _width, _height, convertTextureType(_type), GL_UNSIGNED_BYTE, _data); + bindTexture(0); } // updateTexture @@ -243,11 +253,121 @@ namespace Renderer void swapBuffers() { +#ifdef WIN32 + glFlush(); + glFinish(); + Sleep(0); +#endif + SDL_GL_SwapWindow(getSDLWindow()); + +#ifdef WIN32 + Sleep(0); +#endif + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); } // swapBuffers +#define ROUNDING_PIECES 8.0f + + void drawGLRoundedCorner(float x, float y, double sa, double arc, float r, unsigned int color, std::vector &vertex) + { + float red = (((color & 0xff000000) >> 24) & 255) / 255.0f; + float g = (((color & 0x00ff0000) >> 16) & 255) / 255.0f; + float b = (((color & 0x0000ff00) >> 8) & 255) / 255.0f; + float a = (((color & 0x000000ff)) & 255) / 255.0f; + + // centre of the arc, for clockwise sense + float cent_x = x + r * Math::cosf(sa + ES_PI / 2.0f); + float cent_y = y + r * Math::sinf(sa + ES_PI / 2.0f); + + // build up piecemeal including end of the arc + int n = ceil(ROUNDING_PIECES * arc / ES_PI * 2.0f); + for (int i = 0; i <= n; i++) + { + float ang = sa + arc * (double)i / (double)n; + + // compute the next point + float next_x = cent_x + r * Math::sinf(ang); + float next_y = cent_y - r * Math::cosf(ang); + + Vertex vx; + vx.pos = Vector2f(next_x, next_y); + vx.tex = Vector2f(0, 0); + vx.col = color; + vertex.push_back(vx); + } + } + + void drawRoundRect(float x, float y, float width, float height, float radius, unsigned int color, const Blend::Factor _srcBlendFactor, const Blend::Factor _dstBlendFactor) + { + auto finalColor = convertColor(color); + + std::vector vertex; + drawGLRoundedCorner(x, y + radius, 3.0f * ES_PI / 2.0f, ES_PI / 2.0f, radius, finalColor, vertex); + drawGLRoundedCorner(x + width - radius, y, 0.0, ES_PI / 2.0f, radius, finalColor, vertex); + drawGLRoundedCorner(x + width, y + height - radius, ES_PI / 2.0f, ES_PI / 2.0f, radius, finalColor, vertex); + drawGLRoundedCorner(x + radius, y + height, ES_PI, ES_PI / 2.0f, radius, finalColor, vertex); + + Vertex* vxs = new Vertex[vertex.size()]; + for (int i = 0; i < vertex.size(); i++) + vxs[i] = vertex[i]; + + bindTexture(0); + + glEnable(GL_BLEND); + glBlendFunc(convertBlendFactor(_srcBlendFactor), convertBlendFactor(_dstBlendFactor)); + + glEnableClientState(GL_VERTEX_ARRAY); + glEnableClientState(GL_TEXTURE_COORD_ARRAY); + glEnableClientState(GL_COLOR_ARRAY); + + glVertexPointer(2, GL_FLOAT, sizeof(Vertex), &vxs[0].pos); + glTexCoordPointer(2, GL_FLOAT, sizeof(Vertex), &vxs[0].tex); + glColorPointer(4, GL_UNSIGNED_BYTE, sizeof(Vertex), &vxs[0].col); + + glDrawArrays(GL_TRIANGLE_FAN, 0, vertex.size()); + + glDisableClientState(GL_COLOR_ARRAY); + glDisableClientState(GL_TEXTURE_COORD_ARRAY); + glDisableClientState(GL_VERTEX_ARRAY); + glDisable(GL_BLEND); + + delete[] vxs; + } + + void enableRoundCornerStencil(float x, float y, float width, float height, float radius) + { + bool tx = glIsEnabled(GL_TEXTURE_2D); + glDisable(GL_TEXTURE_2D); + + glClear(GL_DEPTH_BUFFER_BIT); + glEnable(GL_STENCIL_TEST); + glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); + glDepthMask(GL_FALSE); + glStencilFunc(GL_NEVER, 1, 0xFF); + glStencilOp(GL_REPLACE, GL_KEEP, GL_KEEP); + + glStencilMask(0xFF); + glClear(GL_STENCIL_BUFFER_BIT); + + drawRoundRect(x, y, width, height, radius, 0xFFFFFFFF); + + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + glDepthMask(GL_TRUE); + glStencilMask(0x00); + glStencilFunc(GL_EQUAL, 0, 0xFF); + glStencilFunc(GL_EQUAL, 1, 0xFF); + + if (tx) + glEnable(GL_TEXTURE_2D); + } + + void disableStencil() + { + glDisable(GL_STENCIL_TEST); + } } // Renderer:: #endif // USE_OPENGLES_10 diff --git a/es-core/src/resources/Font.cpp b/es-core/src/resources/Font.cpp index 440540cf28..270a708cf8 100644 --- a/es-core/src/resources/Font.cpp +++ b/es-core/src/resources/Font.cpp @@ -1,6 +1,6 @@ #include "resources/Font.h" -#include "renderers/Renderer.h" +#include "renderers/Renderer.h" #include "utils/FileSystemUtil.h" #include "utils/StringUtil.h" #include "Log.h" @@ -19,7 +19,7 @@ Font::FontFace::FontFace(ResourceData&& d, int size) : data(d) { int err = FT_New_Memory_Face(sLibrary, data.ptr.get(), (FT_Long)data.length, 0, &face); assert(!err); - + if(!err) FT_Set_Pixel_Sizes(face, 0, size); } @@ -74,33 +74,64 @@ size_t Font::getTotalMemUsage() Font::Font(int size, const std::string& path) : mSize(size), mPath(path) { - assert(mSize > 0); + mSize = size; + + if (Renderer::isSmallScreen()) + { + float sz = Math::min(Renderer::getScreenWidth(), Renderer::getScreenHeight()); + if (sz >= 320) // ODROID 480x320; + mSize = size * 1.31; + else // GPI 320x240 + mSize = size * 1.5; + } + + assert(mSize > 0); + mLoaded = true; mMaxGlyphHeight = 0; - if(!sLibrary) + if (!sLibrary) initLibrary(); + for (unsigned int i = 0; i < 255; i++) + mGlyphCacheArray[i] = NULL; + // always initialize ASCII characters - for(unsigned int i = 32; i < 128; i++) + for (unsigned int i = 32; i < 128; i++) getGlyph(i); + // getGlyph(61446); + clearFaceCache(); } Font::~Font() { - unload(ResourceManager::getInstance()); + for (auto it = mGlyphMap.cbegin(); it != mGlyphMap.cend(); it++) + delete it->second; + + unload(); } -void Font::reload(std::shared_ptr& /*rm*/) +void Font::reload() { + if (mLoaded) + return; + rebuildTextures(); + mLoaded = true; } -void Font::unload(std::shared_ptr& /*rm*/) +bool Font::unload() { - unloadTextures(); + if (mLoaded) + { + unloadTextures(); + mLoaded = false; + return true; + } + + return false; } std::shared_ptr Font::get(int size, const std::string& path) @@ -144,7 +175,7 @@ Font::FontTexture::~FontTexture() bool Font::FontTexture::findEmpty(const Vector2i& size, Vector2i& cursor_out) { - if(size.x() >= textureSize.x() || size.y() >= textureSize.y()) + if (size.x() >= textureSize.x() || size.y() >= textureSize.y()) return false; if(writePos.x() + size.x() >= textureSize.x() && @@ -204,7 +235,7 @@ void Font::getTextureForNewGlyph(const Vector2i& glyphSize, FontTexture*& tex_ou mTextures.push_back(FontTexture()); tex_out = &mTextures.back(); tex_out->initTexture(); - + bool ok = tex_out->findEmpty(glyphSize, cursor_out); if(!ok) { @@ -226,9 +257,10 @@ std::vector getFallbackFontPaths() fontDir += "\\Fonts\\"; const char* fontNames[] = { - "meiryo.ttc", // japanese - "simhei.ttf", // chinese - "arial.ttf" // latin + ":/glyphs.ttf", // latin + ":/DroidSansFallbackFull.ttf",// japanese, chinese, present on Debian + ":/NanumMyeongjo.ttf" // korean font + "arial.ttf", // latin }; //prepend to font file names @@ -237,8 +269,8 @@ std::vector getFallbackFontPaths() for(unsigned int i = 0; i < sizeof(fontNames) / sizeof(fontNames[0]); i++) { - std::string path = fontDir + fontNames[i]; - if(ResourceManager::getInstance()->fileExists(path)) + std::string path = Utils::String::startsWith(fontNames[i], ":/") ? fontNames[i] : fontDir + fontNames[i]; + if (ResourceManager::getInstance()->fileExists(path)) fontPaths.push_back(path); } @@ -248,7 +280,7 @@ std::vector getFallbackFontPaths() #else // Linux - const char* paths[] = { + const char* paths[] = { "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", "/usr/share/fonts/truetype/freefont/FreeMono.ttf", "/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf" // japanese, chinese, present on Debian @@ -275,18 +307,17 @@ FT_Face Font::getFaceForChar(unsigned int id) for(unsigned int i = 0; i < fallbackFonts.size() + 1; i++) { auto fit = mFaceCache.find(i); - - if(fit == mFaceCache.cend()) // doesn't exist yet - { + if (fit == mFaceCache.cend()) // doesn't exist yet + { // i == 0 -> mPath // otherwise, take from fallbackFonts const std::string& path = (i == 0 ? mPath : fallbackFonts.at(i - 1)); - ResourceData data = ResourceManager::getInstance()->getFileData(path); - mFaceCache[i] = std::unique_ptr(new FontFace(std::move(data), mSize)); + ResourceData data = ResourceManager::getInstance()->getFileData(path); + mFaceCache[i] = std::unique_ptr(new FontFace(std::move(data), i == 1 && mMaxGlyphHeight > 0 ? mMaxGlyphHeight : mSize)); // Reduce size of gyphs ???? fit = mFaceCache.find(i); } - if(FT_Get_Char_Index(fit->second->face, id) != 0) + if (FT_Get_Char_Index(fit->second->face, id) != 0) return fit->second->face; } @@ -301,10 +332,21 @@ void Font::clearFaceCache() Font::Glyph* Font::getGlyph(unsigned int id) { - // is it already loaded? - auto it = mGlyphMap.find(id); - if(it != mGlyphMap.cend()) - return &it->second; + if (id < 255) + { + // FCA ptimisation : array is always really fastest than a map + // When computing long descriptions, it can come here size*2 times per frame + Glyph* fastCache = mGlyphCacheArray[id]; + if (fastCache != NULL) + return fastCache; + } + else + { + // is it already loaded? + auto it = mGlyphMap.find(id); + if (it != mGlyphMap.cend()) + return it->second; + } // nope, need to make a glyph FT_Face face = getFaceForChar(id); @@ -316,7 +358,7 @@ Font::Glyph* Font::getGlyph(unsigned int id) FT_GlyphSlot g = face->glyph; - if(FT_Load_Char(face, id, FT_LOAD_RENDER)) + if (FT_Load_Char(face, id, FT_LOAD_RENDER)) { LOG(LogError) << "Could not find glyph for character " << id << " for font " << mPath << ", size " << mSize << "!"; return NULL; @@ -336,24 +378,27 @@ Font::Glyph* Font::getGlyph(unsigned int id) } // create glyph - Glyph& glyph = mGlyphMap[id]; - - glyph.texture = tex; - glyph.texPos = Vector2f(cursor.x() / (float)tex->textureSize.x(), cursor.y() / (float)tex->textureSize.y()); - glyph.texSize = Vector2f(glyphSize.x() / (float)tex->textureSize.x(), glyphSize.y() / (float)tex->textureSize.y()); - - glyph.advance = Vector2f((float)g->metrics.horiAdvance / 64.0f, (float)g->metrics.vertAdvance / 64.0f); - glyph.bearing = Vector2f((float)g->metrics.horiBearingX / 64.0f, (float)g->metrics.horiBearingY / 64.0f); + Glyph* pGlyph = new Glyph(); + + pGlyph->texture = tex; + pGlyph->texPos = Vector2f(cursor.x() / (float)tex->textureSize.x(), cursor.y() / (float)tex->textureSize.y()); + pGlyph->texSize = Vector2f(glyphSize.x() / (float)tex->textureSize.x(), glyphSize.y() / (float)tex->textureSize.y()); + pGlyph->advance = Vector2f((float)g->metrics.horiAdvance / 64.0f, (float)g->metrics.vertAdvance / 64.0f); + pGlyph->bearing = Vector2f((float)g->metrics.horiBearingX / 64.0f, (float)g->metrics.horiBearingY / 64.0f); // upload glyph bitmap to texture Renderer::updateTexture(tex->textureId, Renderer::Texture::ALPHA, cursor.x(), cursor.y(), glyphSize.x(), glyphSize.y(), g->bitmap.buffer); // update max glyph height - if(glyphSize.y() > mMaxGlyphHeight) + if (id != 61446 && glyphSize.y() > mMaxGlyphHeight) mMaxGlyphHeight = glyphSize.y(); - // done - return &glyph; + mGlyphMap[id] = pGlyph; + + if (id < 255) + mGlyphCacheArray[id] = pGlyph; + + return pGlyph; } // completely recreate the texture data for all textures based on mGlyphs information @@ -361,9 +406,7 @@ void Font::rebuildTextures() { // recreate OpenGL textures for(auto it = mTextures.begin(); it != mTextures.end(); it++) - { it->initTexture(); - } // reupload the texture data for(auto it = mGlyphMap.cbegin(); it != mGlyphMap.cend(); it++) @@ -374,12 +417,12 @@ void Font::rebuildTextures() // load the glyph bitmap through FT FT_Load_Char(face, it->first, FT_LOAD_RENDER); - FontTexture* tex = it->second.texture; - + FontTexture* tex = it->second->texture; + // find the position/size - Vector2i cursor((int)(it->second.texPos.x() * tex->textureSize.x()), (int)(it->second.texPos.y() * tex->textureSize.y())); - Vector2i glyphSize((int)(it->second.texSize.x() * tex->textureSize.x()), (int)(it->second.texSize.y() * tex->textureSize.y())); - + Vector2i cursor((int)(it->second->texPos.x() * tex->textureSize.x()), (int)(it->second->texPos.y() * tex->textureSize.y())); + Vector2i glyphSize((int)(it->second->texSize.x() * tex->textureSize.x()), (int)(it->second->texSize.y() * tex->textureSize.y())); + // upload to texture Renderer::updateTexture(tex->textureId, Renderer::Texture::ALPHA, cursor.x(), cursor.y(), glyphSize.x(), glyphSize.y(), glyphSlot->bitmap.buffer); } @@ -397,13 +440,67 @@ void Font::renderTextCache(TextCache* cache) { assert(*it->textureIdPtr != 0); - auto vertexList = *it; - Renderer::bindTexture(*it->textureIdPtr); Renderer::drawTriangleStrips(&it->verts[0], it->verts.size()); } } +void Font::renderGradientTextCache(TextCache* cache, unsigned int colorTop, unsigned int colorBottom, bool horz) +{ + if (cache == NULL) + { + LOG(LogError) << "Attempted to draw NULL TextCache!"; + return; + } + + for (auto it = cache->vertexLists.cbegin(); it != cache->vertexLists.cend(); it++) + { + assert(*it->textureIdPtr != 0); + + std::vector vxs; + vxs.resize(it->verts.size()); + + float maxY = -1; + + for (int i = 0; i < it->verts.size(); i += 6) + if (maxY == -1 || maxY < it->verts[i + 2].pos.y()) + maxY = it->verts[i + 2].pos.y(); + + for (int i = 0; i < it->verts.size(); i += 6) + { + float topOffset = it->verts[i + 1].pos.y(); + float bottomOffset = it->verts[i + 2].pos.y(); + + float topPercent = (maxY == 0 ? 1.0 : topOffset / maxY); + float bottomPercent = (maxY == 0 ? 1.0 : bottomOffset / maxY); + + const unsigned int colorT = Renderer::mixColors(colorTop, colorBottom, topPercent); + const unsigned int colorB = Renderer::mixColors(colorTop, colorBottom, bottomPercent); + + vxs[i + 1] = it->verts[i + 1]; + vxs[i + 1].col = colorT; + + vxs[i + 2] = it->verts[i + 2]; + vxs[i + 2].col = colorB; + + vxs[i + 3] = it->verts[i + 3]; + vxs[i + 3].col = colorT; + + vxs[i + 4] = it->verts[i + 4]; + vxs[i + 4].col = colorB; + + // make duplicates of first and last vertex so this can be rendered as a triangle strip + vxs[i + 0] = vxs[i + 1]; + vxs[i + 5] = vxs[i + 4]; + } + + Renderer::bindTexture(*it->textureIdPtr); + Renderer::drawTriangleStrips(&vxs[0], vxs.size()); + Renderer::bindTexture(0); + } +} + + Vector2f Font::sizeText(std::string text, float lineSpacing) { float lineWidth = 0.0f; @@ -566,7 +663,7 @@ float Font::getNewlineStartOffset(const std::string& text, const unsigned int& c TextCache* Font::buildTextCache(const std::string& text, Vector2f offset, unsigned int color, float xLen, Alignment alignment, float lineSpacing) { float x = offset[0] + (xLen != 0 ? getNewlineStartOffset(text, 0, xLen, alignment) : 0); - + float yTop = getGlyph('S')->bearing.y(); float yBot = getHeight(lineSpacing); float y = offset[1] + (yBot + yTop)/2.0f; @@ -600,8 +697,8 @@ TextCache* Font::buildTextCache(const std::string& text, Vector2f offset, unsign verts.resize(oldVertSize + 6); Renderer::Vertex* vertices = verts.data() + oldVertSize; - const float glyphStartX = x + glyph->bearing.x(); - const Vector2i& textureSize = glyph->texture->textureSize; + const float glyphStartX = x + glyph->bearing.x(); + const Vector2i& textureSize = glyph->texture->textureSize; const unsigned int convertedColor = Renderer::convertColor(color); vertices[1] = { { glyphStartX , y - glyph->bearing.y() }, { glyph->texPos.x(), glyph->texPos.y() }, convertedColor }; @@ -650,15 +747,15 @@ void TextCache::setColor(unsigned int color) { const unsigned int convertedColor = Renderer::convertColor(color); - for(auto it = vertexLists.begin(); it != vertexLists.end(); it++) - for(auto it2 = it->verts.begin(); it2 != it->verts.end(); it2++) + for (auto it = vertexLists.begin(); it != vertexLists.end(); it++) + for (auto it2 = it->verts.begin(); it2 != it->verts.end(); it2++) it2->col = convertedColor; } std::shared_ptr Font::getFromTheme(const ThemeData::ThemeElement* elem, unsigned int properties, const std::shared_ptr& orig) { using namespace ThemeFlags; - if(!(properties & FONT_PATH) && !(properties & FONT_SIZE)) + if (!(properties & FONT_PATH) && !(properties & FONT_SIZE)) return orig; std::shared_ptr font; @@ -666,10 +763,18 @@ std::shared_ptr Font::getFromTheme(const ThemeData::ThemeElement* elem, un std::string path = (orig ? orig->mPath : getDefaultPath()); float sh = (float)Renderer::getScreenHeight(); - if(properties & FONT_SIZE && elem->has("fontSize")) - size = (int)(sh * elem->get("fontSize")); - if(properties & FONT_PATH && elem->has("fontPath")) - path = elem->get("fontPath"); + if (properties & FONT_SIZE && elem->has("fontSize")) + { + if ((int)(sh * elem->get("fontSize")) > 0) + size = (int)(sh * elem->get("fontSize")); + } + + if (properties & FONT_PATH && elem->has("fontPath")) + { + std::string tmppath = elem->get("fontPath"); + if (ResourceManager::getInstance()->fileExists(tmppath)) + path = tmppath; + } return get(size, path); } diff --git a/es-core/src/resources/Font.h b/es-core/src/resources/Font.h index 448ab5f9ba..0214094e54 100644 --- a/es-core/src/resources/Font.h +++ b/es-core/src/resources/Font.h @@ -44,7 +44,9 @@ class Font : public IReloadable Vector2f sizeText(std::string text, float lineSpacing = 1.5f); // Returns the expected size of a string when rendered. Extra spacing is applied to the Y axis. TextCache* buildTextCache(const std::string& text, float offsetX, float offsetY, unsigned int color); TextCache* buildTextCache(const std::string& text, Vector2f offset, unsigned int color, float xLen, Alignment alignment = ALIGN_LEFT, float lineSpacing = 1.5f); + void renderTextCache(TextCache* cache); + void renderGradientTextCache(TextCache* cache, unsigned int colorTop, unsigned int colorBottom, bool horz = false); std::string wrapText(std::string text, float xLen); // Inserts newlines into text to make it wrap properly. Vector2f sizeWrappedText(std::string text, float xLen, float lineSpacing = 1.5f); // Returns the expected size of a string after wrapping is applied. @@ -53,8 +55,8 @@ class Font : public IReloadable float getHeight(float lineSpacing = 1.5f) const; float getLetterHeight(); - void unload(std::shared_ptr& rm) override; - void reload(std::shared_ptr& rm) override; + bool unload() override; + void reload() override; int getSize() const; inline const std::string& getPath() const { return mPath; } @@ -120,17 +122,24 @@ class Font : public IReloadable Vector2f bearing; }; - std::map mGlyphMap; + // used to cache 255 first chars + Glyph* mGlyphCacheArray[255]; + + // used to cache every char + std::map mGlyphMap; Glyph* getGlyph(unsigned int id); int mMaxGlyphHeight; - const int mSize; + int mSize; const std::string mPath; float getNewlineStartOffset(const std::string& text, const unsigned int& charStart, const float& xLen, const Alignment& alignment); + + bool mLoaded; + friend TextCache; }; diff --git a/es-core/src/resources/ResourceManager.cpp b/es-core/src/resources/ResourceManager.cpp index 9f5014e432..18b4bf28c7 100644 --- a/es-core/src/resources/ResourceManager.cpp +++ b/es-core/src/resources/ResourceManager.cpp @@ -7,6 +7,7 @@ auto array_deleter = [](unsigned char* p) { delete[] p; }; auto nop_deleter = [](unsigned char* /*p*/) { }; std::shared_ptr ResourceManager::sInstance = nullptr; +std::mutex ResourceManager::FileSystemLock; ResourceManager::ResourceManager() { @@ -52,9 +53,10 @@ const ResourceData ResourceManager::getFileData(const std::string& path) const //check if its a resource const std::string respath = getResourcePath(path); - if(Utils::FileSystem::exists(respath)) + auto size = Utils::FileSystem::getFileSize(respath); + if (size > 0) { - ResourceData data = loadFile(respath); + ResourceData data = loadFile(respath, size); return data; } @@ -63,13 +65,20 @@ const ResourceData ResourceManager::getFileData(const std::string& path) const return data; } -ResourceData ResourceManager::loadFile(const std::string& path) const + + +ResourceData ResourceManager::loadFile(const std::string& path, size_t size) const { + std::unique_lock lock(FileSystemLock); + std::ifstream stream(path, std::ios::binary); - stream.seekg(0, stream.end); - size_t size = (size_t)stream.tellg(); - stream.seekg(0, stream.beg); + if (size == 0) + { + stream.seekg(0, stream.end); + size = (size_t)stream.tellg(); + stream.seekg(0, stream.beg); + } //supply custom deleter to properly free array std::shared_ptr data(new unsigned char[size], array_deleter); @@ -89,18 +98,26 @@ bool ResourceManager::fileExists(const std::string& path) const return Utils::FileSystem::exists(path); } +#include "resources/TextureResource.h" + void ResourceManager::unloadAll() { auto iter = mReloadables.cbegin(); while(iter != mReloadables.cend()) - { - if(!iter->expired()) - { - iter->lock()->unload(sInstance); + { + std::shared_ptr info = *iter; + + if (!info->data.expired()) + { + if (!info->locked) + info->reload = info->data.lock()->unload(); + else + info->locked = false; + iter++; - }else{ - iter = mReloadables.erase(iter); } + else + iter = mReloadables.erase(iter); } } @@ -109,17 +126,50 @@ void ResourceManager::reloadAll() auto iter = mReloadables.cbegin(); while(iter != mReloadables.cend()) { - if(!iter->expired()) + std::shared_ptr info = *iter; + + if (!info->data.expired()) { - iter->lock()->reload(sInstance); + if (info->reload) + { + info->data.lock()->reload(); + info->reload = false; + } + iter++; - }else{ - iter = mReloadables.erase(iter); } + else + iter = mReloadables.erase(iter); } } void ResourceManager::addReloadable(std::weak_ptr reloadable) { - mReloadables.push_back(reloadable); + std::shared_ptr info = std::make_shared(); + info->data = reloadable; + info->reload = false; + info->locked = false; + mReloadables.push_back(info); } + +void ResourceManager::removeReloadable(std::weak_ptr reloadable) +{ + auto iter = mReloadables.cbegin(); + while (iter != mReloadables.cend()) + { + std::shared_ptr info = *iter; + + if (!info->data.expired()) + { + if (info->data.lock() == reloadable.lock()) + { + info->locked = true; + break; + } + + iter++; + } + else + iter = mReloadables.erase(iter); + } +} \ No newline at end of file diff --git a/es-core/src/resources/ResourceManager.h b/es-core/src/resources/ResourceManager.h index 96eab6455e..b5d26411a5 100644 --- a/es-core/src/resources/ResourceManager.h +++ b/es-core/src/resources/ResourceManager.h @@ -4,6 +4,8 @@ #include #include +#include + //The ResourceManager exists to... //Allow loading resources embedded into the executable like an actual file. @@ -20,8 +22,8 @@ class ResourceManager; class IReloadable { public: - virtual void unload(std::shared_ptr& rm) = 0; - virtual void reload(std::shared_ptr& rm) = 0; + virtual bool unload() = 0; + virtual void reload() = 0; }; class ResourceManager @@ -30,6 +32,7 @@ class ResourceManager static std::shared_ptr& getInstance(); void addReloadable(std::weak_ptr reloadable); + void removeReloadable(std::weak_ptr reloadable); void unloadAll(); void reloadAll(); @@ -38,14 +41,24 @@ class ResourceManager const ResourceData getFileData(const std::string& path) const; bool fileExists(const std::string& path) const; + static std::mutex FileSystemLock; + private: ResourceManager(); static std::shared_ptr sInstance; - ResourceData loadFile(const std::string& path) const; + ResourceData loadFile(const std::string& path, size_t size) const; + + class ReloadableInfo + { + public: + std::weak_ptr data; + bool reload; + bool locked; + }; - std::list< std::weak_ptr > mReloadables; + std::list> mReloadables; // std::weak_ptr }; #endif // ES_CORE_RESOURCES_RESOURCE_MANAGER_H diff --git a/es-core/src/resources/TextureData.cpp b/es-core/src/resources/TextureData.cpp index 11cc756379..00a8f5d8fb 100644 --- a/es-core/src/resources/TextureData.cpp +++ b/es-core/src/resources/TextureData.cpp @@ -1,7 +1,7 @@ #include "resources/TextureData.h" #include "math/Misc.h" -#include "renderers/Renderer.h" +#include "renderers/Renderer.h" #include "resources/ResourceManager.h" #include "ImageIO.h" #include "Log.h" @@ -12,9 +12,12 @@ #define DPI 96 -TextureData::TextureData(bool tile) : mTile(tile), mTextureID(0), mDataRGBA(nullptr), mScalable(false), - mWidth(0), mHeight(0), mSourceWidth(0.0f), mSourceHeight(0.0f) +bool TextureData::OPTIMIZEVRAM = false; + +TextureData::TextureData(bool tile, bool linear) : mTile(tile), mLinear(linear), mTextureID(0), mDataRGBA(nullptr), mScalable(false), + mWidth(0), mHeight(0), mSourceWidth(0.0f), mSourceHeight(0.0f), mMaxSize(MaxSizeInfo()), mPackedSize(Vector2i(0,0)), mBaseSize(Vector2i(0, 0)) { + mIsExternalDataRGBA = false; } TextureData::~TextureData() @@ -52,6 +55,9 @@ bool TextureData::initSVGFromMemory(const unsigned char* fileData, size_t length return false; } + if (svgImage->width == 0 || svgImage->height == 0) + return false; + // We want to rasterise this texture at a specific resolution. If the source size // variables are set then use them otherwise set them from the parsed file if ((mSourceWidth == 0.0f) && (mSourceHeight == 0.0f)) @@ -59,6 +65,9 @@ bool TextureData::initSVGFromMemory(const unsigned char* fileData, size_t length mSourceWidth = svgImage->width; mSourceHeight = svgImage->height; } + else + mSourceWidth = (mSourceHeight * svgImage->width) / svgImage->height; // FCATMP : Always keep source aspect ratio + mWidth = (size_t)Math::round(mSourceWidth); mHeight = (size_t)Math::round(mSourceHeight); @@ -73,10 +82,35 @@ bool TextureData::initSVGFromMemory(const unsigned char* fileData, size_t length mHeight = (size_t)Math::round(((float)mWidth / svgImage->width) * svgImage->height); } + mBaseSize = Vector2i(mWidth, mHeight); + + if (mMaxSize.x() > 0 && mMaxSize.y() > 0 && mHeight < mMaxSize.y() && mWidth < mMaxSize.x()) // FCATMP + { + Vector2i sz = ImageIO::adjustPictureSize(Vector2i(mWidth, mHeight), Vector2i(mMaxSize.x(), mMaxSize.y()), mMaxSize.externalZoom()); + mHeight = sz.y(); + mWidth = (int)((mHeight * svgImage->width) / svgImage->height); + } + + if (OPTIMIZEVRAM && mMaxSize.x() > 0 && mMaxSize.y() > 0 && (mWidth > mMaxSize.x() || mHeight > mMaxSize.y())) + { + Vector2i sz = ImageIO::adjustPictureSize(Vector2i(mWidth, mHeight), Vector2i(mMaxSize.x(), mMaxSize.y()), mMaxSize.externalZoom()); + mHeight = sz.y(); + mWidth = (mHeight * svgImage->width) / svgImage->height; + + mPackedSize = Vector2i(mWidth, mHeight); + } + else + mPackedSize = Vector2i(0, 0); + unsigned char* dataRGBA = new unsigned char[mWidth * mHeight * 4]; + double scale = ((float) ((int) mHeight)) / svgImage->height; + double scaleV = ((float) ((int) mWidth)) / svgImage->width; + if (scaleV < scale) + scale = scaleV; + NSVGrasterizer* rast = nsvgCreateRasterizer(); - nsvgRasterize(rast, svgImage, 0, 0, mHeight / svgImage->height, dataRGBA, (int)mWidth, (int)mHeight, (int)mWidth * 4); + nsvgRasterize(rast, svgImage, 0, 0, scale, dataRGBA, (int)mWidth, (int)mHeight, (int)mWidth * 4); nsvgDeleteRasterizer(rast); ImageIO::flipPixelsVert(dataRGBA, mWidth, mHeight); @@ -86,6 +120,29 @@ bool TextureData::initSVGFromMemory(const unsigned char* fileData, size_t length return true; } +bool TextureData::isRequiredTextureSizeOk() +{ + if (!OPTIMIZEVRAM) + return true; + + if (mPackedSize == Vector2i(0, 0)) + return true; + + if (mBaseSize == Vector2i(0, 0)) + return true; + + if (mMaxSize.empty()) + return true; + + if ((int) mMaxSize.x() <= mPackedSize.x() || (int) mMaxSize.y() <= mPackedSize.y()) + return true; + + if (mBaseSize.x() <= mPackedSize.x() || mBaseSize.y() <= mPackedSize.y()) + return true; + + return false; +} + bool TextureData::initImageFromMemory(const unsigned char* fileData, size_t length) { size_t width, height; @@ -96,9 +153,18 @@ bool TextureData::initImageFromMemory(const unsigned char* fileData, size_t leng if (mDataRGBA) return true; } + + + auto x = OPTIMIZEVRAM ? mMaxSize.x() : Renderer::getScreenWidth(); + if (x > Renderer::getScreenWidth()) + x = Renderer::getScreenWidth(); + + auto y = OPTIMIZEVRAM ? mMaxSize.y() : Renderer::getScreenHeight(); + if (y > Renderer::getScreenHeight()) + y = Renderer::getScreenHeight(); - std::vector imageRGBA = ImageIO::loadFromMemoryRGBA32((const unsigned char*)(fileData), length, width, height); - if (imageRGBA.size() == 0) + unsigned char* imageRGBA = ImageIO::loadFromMemoryRGBA32Ex((const unsigned char*)(fileData), length, width, height, x, y, mMaxSize.externalZoom(), mBaseSize, mPackedSize); + if (imageRGBA == NULL) { LOG(LogError) << "Could not initialize texture from memory, invalid data! (file path: " << mPath << ", data ptr: " << (size_t)fileData << ", reported size: " << length << ")"; return false; @@ -108,13 +174,38 @@ bool TextureData::initImageFromMemory(const unsigned char* fileData, size_t leng mSourceHeight = (float) height; mScalable = false; - return initFromRGBA(imageRGBA.data(), width, height); + return initFromRGBAEx(imageRGBA, width, height); } + +void TextureData::setMaxSize(MaxSizeInfo maxSize) +{ + if (mSourceWidth == 0 || mSourceHeight == 0) + mMaxSize = maxSize; + else + { + Vector2i value = ImageIO::adjustPictureSize(Vector2i(mSourceWidth, mSourceHeight), Vector2i(mMaxSize.x(), mMaxSize.y()), mMaxSize.externalZoom()); + Vector2i newVal = ImageIO::adjustPictureSize(Vector2i(mSourceWidth, mSourceHeight), Vector2i(maxSize.x(), maxSize.y()), mMaxSize.externalZoom()); + + if (newVal.x() > value.x() || newVal.y() > value.y()) + mMaxSize = maxSize; + + //if (mMaxSize.x() < maxSize.x() || mMaxSize.y() < maxSize.y()) + } +}; + + bool TextureData::initFromRGBA(const unsigned char* dataRGBA, size_t width, size_t height) { // If already initialised then don't read again std::unique_lock lock(mMutex); + + if (mIsExternalDataRGBA) + { + mIsExternalDataRGBA = false; + mDataRGBA = nullptr; + } + if (mDataRGBA) return true; @@ -126,7 +217,48 @@ bool TextureData::initFromRGBA(const unsigned char* dataRGBA, size_t width, size return true; } -bool TextureData::load() +bool TextureData::initFromRGBAEx(unsigned char* dataRGBA, size_t width, size_t height) +{ + // If already initialised then don't read again + std::unique_lock lock(mMutex); + + if (mIsExternalDataRGBA) + { + mIsExternalDataRGBA = false; + mDataRGBA = nullptr; + } + + if (mDataRGBA) + return true; + + // Take a copy + mDataRGBA = dataRGBA; + mWidth = width; + mHeight = height; + + return true; +} + +bool TextureData::initFromExternalRGBA(unsigned char* dataRGBA, size_t width, size_t height) +{ + // If already initialised then don't read again + std::unique_lock lock(mMutex); + + if (!mIsExternalDataRGBA && mDataRGBA != nullptr) + delete[] mDataRGBA; + + mIsExternalDataRGBA = true; + mDataRGBA = dataRGBA; + mWidth = width; + mHeight = height; + + if (mTextureID != 0) + Renderer::updateTexture(mTextureID, Renderer::Texture::RGBA, -1, -1, mWidth, mHeight, mDataRGBA); + + return true; +} + +bool TextureData::load(bool updateCache) { bool retval = false; @@ -134,16 +266,21 @@ bool TextureData::load() if (!mPath.empty()) { std::shared_ptr& rm = ResourceManager::getInstance(); + const ResourceData& data = rm->getFileData(mPath); // is it an SVG? if (mPath.substr(mPath.size() - 4, std::string::npos) == ".svg") { - mScalable = true; + mScalable = true; // ??? interest ? retval = initSVGFromMemory((const unsigned char*)data.ptr.get(), data.length); } else retval = initImageFromMemory((const unsigned char*)data.ptr.get(), data.length); + + if (updateCache && retval) + ImageIO::updateImageCache(mPath, data.length, mBaseSize.x(), mBaseSize.y()); } + return retval; } @@ -152,6 +289,7 @@ bool TextureData::isLoaded() std::unique_lock lock(mMutex); if (mDataRGBA || (mTextureID != 0)) return true; + return false; } @@ -174,9 +312,16 @@ bool TextureData::uploadAndBind() if ((mWidth == 0) || (mHeight == 0) || (mDataRGBA == nullptr)) return false; - // Upload texture - mTextureID = Renderer::createTexture(Renderer::Texture::RGBA, true, mTile, mWidth, mHeight, mDataRGBA); + mTextureID = Renderer::createTexture(Renderer::Texture::RGBA, mLinear, mTile, mWidth, mHeight, mDataRGBA); + if (mTextureID) + { + if (mDataRGBA != nullptr && !mIsExternalDataRGBA) + delete[] mDataRGBA; + + mDataRGBA = nullptr; + } } + return true; } @@ -193,7 +338,10 @@ void TextureData::releaseVRAM() void TextureData::releaseRAM() { std::unique_lock lock(mMutex); - delete[] mDataRGBA; + + if (mDataRGBA != nullptr && !mIsExternalDataRGBA) + delete[] mDataRGBA; + mDataRGBA = 0; } @@ -225,14 +373,24 @@ float TextureData::sourceHeight() return mSourceHeight; } +void TextureData::setTemporarySize(float width, float height) +{ + mWidth = width; + mHeight = height; + mSourceWidth = width; + mSourceHeight = height; +} + void TextureData::setSourceSize(float width, float height) { if (mScalable) { - if ((mSourceWidth != width) || (mSourceHeight != height)) + //if ((mSourceWidth != width) || (mSourceHeight != height)) + if (mSourceHeight < height) // FCATMP { mSourceWidth = width; mSourceHeight = height; + releaseVRAM(); releaseRAM(); } diff --git a/es-core/src/resources/TextureData.h b/es-core/src/resources/TextureData.h index 44dc92623c..ef025ad5df 100644 --- a/es-core/src/resources/TextureData.h +++ b/es-core/src/resources/TextureData.h @@ -5,14 +5,20 @@ #include #include -class TextureResource; +#include "math/Vector2f.h" +#include "math/Vector2i.h" +#include "resources/TextureResource.h" + +// class TextureResource; class TextureData { public: - TextureData(bool tile); + TextureData(bool tile, bool linear); ~TextureData(); + static bool OPTIMIZEVRAM; + // These functions populate mDataRGBA but do not upload the texture to VRAM //!!!! Needs to be canonical path. Caller should check for duplicates before calling this @@ -20,9 +26,11 @@ class TextureData bool initSVGFromMemory(const unsigned char* fileData, size_t length); bool initImageFromMemory(const unsigned char* fileData, size_t length); bool initFromRGBA(const unsigned char* dataRGBA, size_t width, size_t height); + bool initFromRGBAEx(unsigned char* dataRGBA, size_t width, size_t height); + bool initFromExternalRGBA(unsigned char* dataRGBA, size_t width, size_t height); // Read the data into memory if necessary - bool load(); + bool load(bool updateCache = false); bool isLoaded(); @@ -36,6 +44,8 @@ class TextureData // Release the texture from conventional RAM void releaseRAM(); + void setMaxSize(MaxSizeInfo maxSize); + // Get the amount of VRAM currenty used by this texture size_t getVRAMUsage(); @@ -45,20 +55,36 @@ class TextureData float sourceHeight(); void setSourceSize(float width, float height); + void setTemporarySize(float width, float height); + bool tiled() { return mTile; } + bool isRequiredTextureSizeOk(); + + std::string mPath; + unsigned int mTextureID; + + unsigned char* getDataRGBA() { + return mDataRGBA; + } + private: std::mutex mMutex; bool mTile; - std::string mPath; - unsigned int mTextureID; + bool mLinear; unsigned char* mDataRGBA; size_t mWidth; size_t mHeight; float mSourceWidth; - float mSourceHeight; + float mSourceHeight; bool mScalable; bool mReloadable; + + Vector2i mPackedSize; + Vector2i mBaseSize; + MaxSizeInfo mMaxSize; + + bool mIsExternalDataRGBA; }; #endif // ES_CORE_RESOURCES_TEXTURE_DATA_H diff --git a/es-core/src/resources/TextureDataManager.cpp b/es-core/src/resources/TextureDataManager.cpp index f64719003d..e0b94dce8c 100644 --- a/es-core/src/resources/TextureDataManager.cpp +++ b/es-core/src/resources/TextureDataManager.cpp @@ -3,20 +3,24 @@ #include "resources/TextureData.h" #include "resources/TextureResource.h" #include "Settings.h" +#include "utils/StringUtil.h" +#include "utils/FileSystemUtil.h" +#include TextureDataManager::TextureDataManager() { unsigned char data[5 * 5 * 4]; - mBlank = std::shared_ptr(new TextureData(false)); + mBlank = std::shared_ptr(new TextureData(false, false)); for (int i = 0; i < (5 * 5); ++i) { - data[i*4] = (i % 2) * 255; - data[i*4+1] = (i % 2) * 255; - data[i*4+2] = (i % 2) * 255; - data[i*4+3] = 0; + data[i * 4] = 0; // (i % 2) * 255; + data[i * 4 + 1] = 0; // (i % 2) * 255; + data[i * 4 + 2] = 0; // (i % 2) * 255; + data[i * 4 + 3] = 0; } mBlank->initFromRGBA(data, 5, 5); - mLoader = new TextureLoader; + + mLoader = new TextureLoader(this); } TextureDataManager::~TextureDataManager() @@ -24,17 +28,47 @@ TextureDataManager::~TextureDataManager() delete mLoader; } -std::shared_ptr TextureDataManager::add(const TextureResource* key, bool tiled) +void TextureDataManager::onTextureLoaded(std::shared_ptr tex) { - remove(key); - std::shared_ptr data(new TextureData(tiled)); + std::unique_lock lock(mMutex); + + for (auto it = mTextureLookup.cbegin(); it != mTextureLookup.cend(); it++) + { + std::shared_ptr texture = *(*it).second; + if (texture == tex) + { + const TextureResource* pResource = it->first; + ((TextureResource*)pResource)->onTextureLoaded(tex); + } + } +} + + +std::shared_ptr TextureDataManager::add(const TextureResource* key, bool tiled, bool linear) +{ + std::unique_lock lock(mMutex); + + // Find the entry in the list + auto it = mTextureLookup.find(key); + if (it != mTextureLookup.cend()) + { + // Remove the list entry + mTextures.erase((*it).second); + // And the lookup + mTextureLookup.erase(it); + } + + std::shared_ptr data = std::make_shared(tiled, linear); mTextures.push_front(data); mTextureLookup[key] = mTextures.cbegin(); + return data; } void TextureDataManager::remove(const TextureResource* key) { + std::unique_lock lock(mMutex); + // Find the entry in the list auto it = mTextureLookup.find(key); if (it != mTextureLookup.cend()) @@ -46,8 +80,19 @@ void TextureDataManager::remove(const TextureResource* key) } } -std::shared_ptr TextureDataManager::get(const TextureResource* key) +void TextureDataManager::cancelAsync(const TextureResource* key) { + std::unique_lock lock(mMutex); + + auto it = mTextureLookup.find(key); + if (it != mTextureLookup.cend()) + mLoader->remove(*(*it).second); +} + +std::shared_ptr TextureDataManager::get(const TextureResource* key, bool enableLoading) +{ + std::unique_lock lock(mMutex); + // If it's in the cache then we want to remove it from it's current location and // move it to the top std::shared_ptr tex; @@ -55,15 +100,23 @@ std::shared_ptr TextureDataManager::get(const TextureResource* key) if (it != mTextureLookup.cend()) { tex = *(*it).second; - // Remove the list entry - mTextures.erase((*it).second); - // Put it at the top - mTextures.push_front(tex); - // Store it back in the lookup - mTextureLookup[key] = mTextures.cbegin(); + + if (mTextures.cbegin() != (*it).second) + { + // Remove the list entry + mTextures.erase((*it).second); + // Put it at the top + mTextures.push_front(tex); + // Store it back in the lookup + mTextureLookup[key] = mTextures.cbegin(); + } // Make sure it's loaded or queued for loading - load(tex); + if (enableLoading && !tex->isLoaded()) // FCATMP + { + lock.unlock(); + load(tex); + } } return tex; } @@ -81,17 +134,23 @@ bool TextureDataManager::bind(const TextureResource* key) size_t TextureDataManager::getTotalSize() { + std::unique_lock lock(mMutex); + size_t total = 0; for (auto tex : mTextures) total += tex->width() * tex->height() * 4; + return total; } size_t TextureDataManager::getCommittedSize() { + std::unique_lock lock(mMutex); + size_t total = 0; for (auto tex : mTextures) total += tex->getVRAMUsage(); + return total; } @@ -100,127 +159,207 @@ size_t TextureDataManager::getQueueSize() return mLoader->getQueueSize(); } +bool compareTextures(const std::shared_ptr& first, const std::shared_ptr& second) +{ + bool isResource = first->mPath.rfind(":/") == 0; + bool secondIsResource = second->mPath.rfind(":/") == 0; + if (isResource && !secondIsResource) + return true; + + return false; +} + void TextureDataManager::load(std::shared_ptr tex, bool block) { // See if it's already loaded if (tex->isLoaded()) - return; + { + if (tex->isRequiredTextureSizeOk()) + return; + + tex->releaseVRAM(); + tex->releaseRAM(); + + mLoader->remove(tex); + block = true; // Reload instantly or other instances will fade again + } + // Not loaded. Make sure there is room size_t size = TextureResource::getTotalMemUsage(); size_t max_texture = (size_t)Settings::getInstance()->getInt("MaxVRAM") * 1024 * 1024; - for (auto it = mTextures.crbegin(); it != mTextures.crend(); ++it) + if (size >= max_texture) { - if (size < max_texture) - break; - //size -= (*it)->getVRAMUsage(); - (*it)->releaseVRAM(); - (*it)->releaseRAM(); - // It may be already in the loader queue. In this case it wouldn't have been using - // any VRAM yet but it will be. Remove it from the loader queue - mLoader->remove(*it); - size = TextureResource::getTotalMemUsage(); + std::unique_lock lock(mMutex); + + std::list> orderedTextures(mTextures); + orderedTextures.sort(compareTextures); + + for (auto it = orderedTextures.crbegin(); it != orderedTextures.crend(); ++it) + { + if (size < max_texture) + break; + + if ((*it) == tex) + continue; + + bool changed = false; + + if ((*it)->isLoaded()) + { + (*it)->releaseVRAM(); + (*it)->releaseRAM(); + + changed = true; + } + + // It may be already in the loader queue. In this case it wouldn't have been using + // any VRAM yet but it will be. Remove it from the loader queue + if (mLoader->remove(*it)) + changed = true; + + if (changed) + { + lock.unlock(); + size = TextureResource::getTotalMemUsage(); + lock.lock(); + } + } } + if (!block) + { mLoader->load(tex); + } else + { + mLoader->remove(tex); tex->load(); + } } -TextureLoader::TextureLoader() : mExit(false) +TextureLoader::TextureLoader(TextureDataManager* mgr) : mExit(false) { - mThread = new std::thread(&TextureLoader::threadProc, this); + mManager = mgr; + + int num_threads = std::thread::hardware_concurrency() / 2; + if (num_threads == 0) + num_threads = 1; + + for (size_t i = 0; i < num_threads; i++) + mThreads.push_back(std::thread(&TextureLoader::threadProc, this)); } TextureLoader::~TextureLoader() { // Just abort any waiting texture - mTextureDataQ.clear(); - mTextureDataLookup.clear(); + clearQueue(); // Exit the thread mExit = true; - mEvent.notify_one(); - mThread->join(); - delete mThread; + mEvent.notify_all(); + + for (std::thread& t : mThreads) + t.join(); } void TextureLoader::threadProc() { - while (!mExit) + while (true) { - std::shared_ptr textureData; - { - // Wait for an event to say there is something in the queue - std::unique_lock lock(mMutex); - mEvent.wait(lock); - if (!mTextureDataQ.empty()) - { - textureData = mTextureDataQ.front(); - mTextureDataQ.pop_front(); - mTextureDataLookup.erase(mTextureDataLookup.find(textureData.get())); - } - } - // Queue has been released here but we might have a texture to process - while (textureData) + // Wait for an event to say there is something in the queue + std::unique_lock lock(mLoaderLock); + mEvent.wait(lock, [this]() { return mExit || !mTextureDataQ.empty(); }); + + if (mExit) + break; + + if (!mTextureDataQ.empty()) { - textureData->load(); + std::shared_ptr textureData = mTextureDataQ.front(); + mTextureDataQ.pop_front(); - // See if there is another item in the queue - textureData = nullptr; - std::unique_lock lock(mMutex); - if (!mTextureDataQ.empty()) + mProcessingTextureDataQ.push_back(textureData); + + lock.unlock(); + + if (textureData && !textureData->isLoaded()) { - textureData = mTextureDataQ.front(); - mTextureDataQ.pop_front(); - mTextureDataLookup.erase(mTextureDataLookup.find(textureData.get())); + std::this_thread::yield(); + + textureData->load(true); + // mManager->onTextureLoaded(textureData); + + lock.lock(); + mProcessingTextureDataQ.remove(textureData); + lock.unlock(); } + + std::this_thread::yield(); } } } void TextureLoader::load(std::shared_ptr textureData) { + std::unique_lock lock(mLoaderLock); + // Make sure it's not already loaded - if (!textureData->isLoaded()) - { - std::unique_lock lock(mMutex); - // Remove it from the queue if it is already there - auto td = mTextureDataLookup.find(textureData.get()); - if (td != mTextureDataLookup.cend()) - { - mTextureDataQ.erase((*td).second); - mTextureDataLookup.erase(td); - } + if (textureData->isLoaded()) + return; - // Put it on the start of the queue as we want the newly requested textures to load first - mTextureDataQ.push_front(textureData); - mTextureDataLookup[textureData.get()] = mTextureDataQ.cbegin(); - mEvent.notify_one(); - } + // If is is currently loading, don't add again + if (std::find(mProcessingTextureDataQ.cbegin(), mProcessingTextureDataQ.cend(), textureData) != mProcessingTextureDataQ.cend()) + return; + + // Remove it from the queue if it is already there + auto tx = std::find(mTextureDataQ.cbegin(), mTextureDataQ.cend(), textureData); + if (tx != mTextureDataQ.cend()) + mTextureDataQ.erase(tx); + + // Put it on the start of the queue as we want the newly requested textures to load first + mTextureDataQ.push_front(textureData); + mEvent.notify_one(); } -void TextureLoader::remove(std::shared_ptr textureData) +bool TextureLoader::remove(std::shared_ptr textureData) { // Just remove it from the queue so we don't attempt to load it - std::unique_lock lock(mMutex); - auto td = mTextureDataLookup.find(textureData.get()); - if (td != mTextureDataLookup.cend()) + std::unique_lock lock(mLoaderLock); + + auto tx = std::find(mTextureDataQ.cbegin(), mTextureDataQ.cend(), textureData); + if (tx != mTextureDataQ.cend()) { - mTextureDataQ.erase((*td).second); - mTextureDataLookup.erase(td); + mTextureDataQ.erase(tx); + return true; } + + return false; } size_t TextureLoader::getQueueSize() { + std::unique_lock lock(mLoaderLock); + // Gets the amount of video memory that will be used once all textures in // the queue are loaded size_t mem = 0; - std::unique_lock lock(mMutex); - for (auto tex : mTextureDataQ) - { + for (auto tex : mTextureDataQ) mem += tex->width() * tex->height() * 4; - } + return mem; } + +void TextureLoader::clearQueue() +{ + std::unique_lock lock(mLoaderLock); + + // Just abort any waiting texture + mTextureDataQ.clear(); +} + +void TextureDataManager::clearQueue() +{ + if (mLoader != nullptr) + mLoader->clearQueue(); +} \ No newline at end of file diff --git a/es-core/src/resources/TextureDataManager.h b/es-core/src/resources/TextureDataManager.h index c7b3a029d8..d107548677 100644 --- a/es-core/src/resources/TextureDataManager.h +++ b/es-core/src/resources/TextureDataManager.h @@ -7,33 +7,40 @@ #include #include #include +#include #include +#include class TextureData; class TextureResource; +class TextureDataManager; class TextureLoader { public: - TextureLoader(); + TextureLoader(TextureDataManager* mgr); ~TextureLoader(); void load(std::shared_ptr textureData); - void remove(std::shared_ptr textureData); + bool remove(std::shared_ptr textureData); + void clearQueue(); size_t getQueueSize(); -private: - void processQueue(); +private: void threadProc(); - std::list > mTextureDataQ; + std::list> mProcessingTextureDataQ; + + std::list> mTextureDataQ; std::map >::const_iterator > mTextureDataLookup; - std::thread* mThread; - std::mutex mMutex; + std::vector mThreads; + std::mutex mLoaderLock; std::condition_variable mEvent; bool mExit; + + TextureDataManager* mManager; }; // @@ -56,14 +63,15 @@ class TextureDataManager TextureDataManager(); ~TextureDataManager(); - std::shared_ptr add(const TextureResource* key, bool tiled); + std::shared_ptr add(const TextureResource* key, bool tiled, bool linear); // The texturedata being removed may be loading in a different thread. However it will // be referenced by a smart point so we only need to remove it from our array and it // will be deleted when the other thread has finished with it void remove(const TextureResource* key); + void cancelAsync(const TextureResource* key); - std::shared_ptr get(const TextureResource* key); + std::shared_ptr get(const TextureResource* key, bool enableLoading = true); bool bind(const TextureResource* key); // Get the total size of all textures managed by this object, loaded and unloaded in bytes @@ -76,7 +84,12 @@ class TextureDataManager // Load a texture, freeing resources as necessary to make space void load(std::shared_ptr tex, bool block = false); + void clearQueue(); + + void onTextureLoaded(std::shared_ptr tex); + private: + std::mutex mMutex; std::list > mTextures; std::map >::const_iterator > mTextureLookup; diff --git a/es-core/src/resources/TextureResource.cpp b/es-core/src/resources/TextureResource.cpp index c3d9eff0b0..e6149ea153 100644 --- a/es-core/src/resources/TextureResource.cpp +++ b/es-core/src/resources/TextureResource.cpp @@ -2,13 +2,21 @@ #include "utils/FileSystemUtil.h" #include "resources/TextureData.h" +#include "ImageIO.h" +#include "Settings.h" +#include TextureDataManager TextureResource::sTextureDataManager; -std::map< TextureResource::TextureKeyType, std::weak_ptr > TextureResource::sTextureMap; + +std::map< TextureResource::TextureKeyType, std::weak_ptr> TextureResource::sTextureMap; std::set TextureResource::sAllTextures; -TextureResource::TextureResource(const std::string& path, bool tile, bool dynamic) : mTextureData(nullptr), mForceLoad(false) +TextureResource::TextureResource(const std::string& path, bool tile, bool linear, bool dynamic, bool allowAsync, MaxSizeInfo maxSize) : mTextureData(nullptr), mForceLoad(false) { +#if _DEBUG + mPath = path; +#endif + // Create a texture data object for this texture if (!path.empty()) { @@ -16,30 +24,60 @@ TextureResource::TextureResource(const std::string& path, bool tile, bool dynami // data manager to manage loading/unloading of this texture std::shared_ptr data; if (dynamic) - { - data = sTextureDataManager.add(this, tile); + { + data = sTextureDataManager.add(this, tile, linear); + data->setMaxSize(maxSize); data->initFromPath(path); + + bool async = false; + + std::shared_ptr& rm = ResourceManager::getInstance(); + auto fullpath = rm->getResourcePath(path); + + unsigned int width, height; + + if (allowAsync && Settings::getInstance()->getBool("ThreadedLoading") && ImageIO::getImageSize(fullpath.c_str(), &width, &height)) + { + data->setTemporarySize(width, height); + async = true; + } + // Force the texture manager to load it using a blocking load - sTextureDataManager.load(data, true); + sTextureDataManager.load(data, !async); // + + if (async) + { + mSize = Vector2i(width, height); + mSourceSize = Vector2f(width, height); + } + else + { + mSize = Vector2i((int)data->width(), (int)data->height()); + mSourceSize = Vector2f(data->sourceWidth(), data->sourceHeight()); + } } else { - mTextureData = std::shared_ptr(new TextureData(tile)); + mTextureData = std::shared_ptr(new TextureData(tile, linear)); + data = mTextureData; + data->setMaxSize(maxSize); data->initFromPath(path); // Load it so we can read the width/height data->load(); - } - mSize = Vector2i((int)data->width(), (int)data->height()); - mSourceSize = Vector2f(data->sourceWidth(), data->sourceHeight()); + mSize = Vector2i((int)data->width(), (int)data->height()); + mSourceSize = Vector2f(data->sourceWidth(), data->sourceHeight()); + } } else { // Create a texture managed by this class because it cannot be dynamically loaded and unloaded - mTextureData = std::shared_ptr(new TextureData(tile)); + mTextureData = std::shared_ptr(new TextureData(tile, linear)); } - sAllTextures.insert(this); + + if (sAllTextures.find(this) == sAllTextures.end()) + sAllTextures.insert(this); } TextureResource::~TextureResource() @@ -47,14 +85,42 @@ TextureResource::~TextureResource() if (mTextureData == nullptr) sTextureDataManager.remove(this); - sAllTextures.erase(sAllTextures.find(this)); + if (sAllTextures.size() > 0) + { + auto pthis = sAllTextures.find(this); + if (pthis != sAllTextures.end()) + sAllTextures.erase(pthis); + } +} + +void TextureResource::onTextureLoaded(std::shared_ptr tex) +{ + mSize = Vector2i((int)tex->width(), (int)tex->height()); + mSourceSize = Vector2f(tex->sourceWidth(), tex->sourceHeight()); +} + +void TextureResource::initFromExternalPixels(unsigned char* dataRGBA, size_t width, size_t height) +{ + mTextureData->initFromExternalRGBA(dataRGBA, width, height); + + // Cache the image dimensions + mSize = Vector2i((int)width, (int)height); + mSourceSize = Vector2f(mTextureData->sourceWidth(), mTextureData->sourceHeight()); } void TextureResource::initFromPixels(const unsigned char* dataRGBA, size_t width, size_t height) { // This is only valid if we have a local texture data object - assert(mTextureData != nullptr); + assert(mTextureData != nullptr); mTextureData->releaseVRAM(); + + // FCA optimisation, if streamed image size is already the same, don't free/reallocate memory (which is slow), just copy bytes + if (mTextureData->getDataRGBA() != nullptr && mSize.x() == width && mSize.y() == height) + { + memcpy(mTextureData->getDataRGBA(), dataRGBA, width * height * 4); + return; + } + mTextureData->releaseRAM(); mTextureData->initFromRGBA(dataRGBA, width, height); // Cache the image dimensions @@ -83,7 +149,8 @@ bool TextureResource::isTiled() const { if (mTextureData != nullptr) return mTextureData->tiled(); - std::shared_ptr data = sTextureDataManager.get(this); + + std::shared_ptr data = sTextureDataManager.get(this, false); return data->tiled(); } @@ -100,63 +167,107 @@ bool TextureResource::bind() } } -std::shared_ptr TextureResource::get(const std::string& path, bool tile, bool forceLoad, bool dynamic) +void TextureResource::resetCache() +{ + sTextureDataManager.clearQueue(); +} + +void TextureResource::cancelAsync(std::shared_ptr texture) +{ + if (texture != nullptr) + sTextureDataManager.cancelAsync(texture.get()); +} + +std::shared_ptr TextureResource::get(const std::string& path, bool tile, bool linear, bool forceLoad, bool dynamic, bool asReloadable, MaxSizeInfo maxSize) { std::shared_ptr& rm = ResourceManager::getInstance(); const std::string canonicalPath = Utils::FileSystem::getCanonicalPath(path); if(canonicalPath.empty()) { - std::shared_ptr tex(new TextureResource("", tile, false)); + std::shared_ptr tex(new TextureResource("", tile, linear, dynamic, !forceLoad, maxSize)); rm->addReloadable(tex); //make sure we get properly deinitialized even though we do nothing on reinitialization return tex; } - TextureKeyType key(canonicalPath, tile); + // internal resources should not be dynamic + if (canonicalPath.length() > 0 && canonicalPath[0] == ':') + dynamic = false; + + TextureKeyType key(canonicalPath, tile, linear); auto foundTexture = sTextureMap.find(key); if(foundTexture != sTextureMap.cend()) { - if(!foundTexture->second.expired()) - return foundTexture->second.lock(); + if (!foundTexture->second.expired()) + { + std::shared_ptr rc = foundTexture->second.lock(); + + if (!maxSize.empty() && TextureData::OPTIMIZEVRAM) + { + auto dt = sTextureDataManager.get(rc.get()); + if (dt != nullptr) + { + dt->setMaxSize(maxSize); + + if (dt->isLoaded() && !dt->isRequiredTextureSizeOk()) + { + dt->releaseVRAM(); + dt->releaseRAM(); + dt->load(); + } + } + } + + return rc; + } + else + sTextureMap.erase(foundTexture); } // need to create it std::shared_ptr tex; - tex = std::shared_ptr(new TextureResource(key.first, tile, dynamic)); - std::shared_ptr data = sTextureDataManager.get(tex.get()); - - // is it an SVG? - if(key.first.substr(key.first.size() - 4, std::string::npos) != ".svg") + + tex = std::shared_ptr(new TextureResource(std::get<0>(key), tile, linear, dynamic, !forceLoad, maxSize)); + std::shared_ptr data = sTextureDataManager.get(tex.get(), !forceLoad); + + if (asReloadable) // // is it an SVG // if (key.first.substr(key.first.size() - 4, std::string::npos) != ".svg") // FCATMP { - // Probably not. Add it to our map. We don't add SVGs because 2 svgs might be rasterized at different sizes sTextureMap[key] = std::weak_ptr(tex); + rm->addReloadable(tex); } - - // Add it to the reloadable list - rm->addReloadable(tex); + + if (data != nullptr) + data->setMaxSize(maxSize); // Force load it if necessary. Note that it may get dumped from VRAM if we run low if (forceLoad) { tex->mForceLoad = forceLoad; - data->load(); + if (data != nullptr && !data->isLoaded()) + data->load(); } return tex; } // For scalable source images in textures we want to set the resolution to rasterize at -void TextureResource::rasterizeAt(size_t width, size_t height) +void TextureResource::rasterizeAt(float width, float height) { + if (width < 0) width = -width; + if (height < 0) height = -height; + std::shared_ptr data; if (mTextureData != nullptr) data = mTextureData; else data = sTextureDataManager.get(this); - mSourceSize = Vector2f((float)width, (float)height); + + // mSourceSize = Vector2f((float)width, (float)height); data->setSourceSize((float)width, (float)height); + if (mForceLoad || (mTextureData != nullptr)) - data->load(); + if (!data->isLoaded()) // FCATMP + data->load(); } Vector2f TextureResource::getSourceImageSize() const @@ -164,8 +275,15 @@ Vector2f TextureResource::getSourceImageSize() const return mSourceSize; } -bool TextureResource::isInitialized() const +bool TextureResource::isLoaded() const { + if (mTextureData != nullptr) + return mTextureData->isLoaded(); + + auto data = sTextureDataManager.get(this, false); + if (data != nullptr) + return data->isLoaded(); + return true; } @@ -199,23 +317,32 @@ size_t TextureResource::getTotalTextureSize() return total; } -void TextureResource::unload(std::shared_ptr& /*rm*/) +bool TextureResource::unload() { // Release the texture's resources std::shared_ptr data; if (mTextureData == nullptr) - data = sTextureDataManager.get(this); + data = sTextureDataManager.get(this, false); else data = mTextureData; - data->releaseVRAM(); - data->releaseRAM(); + if (data != nullptr && data->isLoaded()) + { + data->releaseVRAM(); + data->releaseRAM(); + + return true; + } + + return false; } -void TextureResource::reload(std::shared_ptr& /*rm*/) +void TextureResource::reload() { // For dynamically loaded textures the texture manager will load them on demand. // For manually loaded textures we have to reload them here - if (mTextureData) + if (mTextureData && !mTextureData->isLoaded()) mTextureData->load(); + else if (mTextureData == nullptr) + sTextureDataManager.get(this); } diff --git a/es-core/src/resources/TextureResource.h b/es-core/src/resources/TextureResource.h index c2be59f1d7..bbca831106 100644 --- a/es-core/src/resources/TextureResource.h +++ b/es-core/src/resources/TextureResource.h @@ -11,22 +11,60 @@ class TextureData; +class MaxSizeInfo +{ +public: + MaxSizeInfo() : mSize(Vector2f(0, 0)), mExternalZoom(false) {} + + MaxSizeInfo(float x, float y) : mSize(Vector2f(x, y)), mExternalZoom(false), mExternalZoomKnown(false) { } + MaxSizeInfo(Vector2f size) : mSize(size), mExternalZoom(false), mExternalZoomKnown(false) { } + + MaxSizeInfo(float x, float y, bool externalZoom) : mSize(Vector2f(x, y)), mExternalZoom(externalZoom), mExternalZoomKnown(true){ } + MaxSizeInfo(Vector2f size, bool externalZoom) : mSize(size), mExternalZoom(externalZoom), mExternalZoomKnown(true) { } + + bool empty() { return mSize.x() <= 1 && mSize.y() <= 1; } + + float x() { return mSize.x(); } + float y() { return mSize.y(); } + + bool externalZoom() + { + return mExternalZoom; + } + + bool isExternalZoomKnown() + { + return mExternalZoomKnown; + } + +private: + Vector2f mSize; + bool mExternalZoom; + bool mExternalZoomKnown; +}; + // An OpenGL texture. // Automatically recreates the texture with renderer deinit/reinit. class TextureResource : public IReloadable { +protected: + TextureResource(const std::string& path, bool tile, bool linear, bool dynamic, bool allowAsync, MaxSizeInfo maxSize); + public: - static std::shared_ptr get(const std::string& path, bool tile = false, bool forceLoad = false, bool dynamic = true); + static std::shared_ptr get(const std::string& path, bool tile = false, bool linear = false, bool forceLoad = false, bool dynamic = true, bool asReloadable = true, MaxSizeInfo maxSize = MaxSizeInfo()); + static void cancelAsync(std::shared_ptr texture); + void initFromPixels(const unsigned char* dataRGBA, size_t width, size_t height); + void initFromExternalPixels(unsigned char* dataRGBA, size_t width, size_t height); virtual void initFromMemory(const char* file, size_t length); // For scalable source images in textures we want to set the resolution to rasterize at - void rasterizeAt(size_t width, size_t height); + void rasterizeAt(float width, float height); Vector2f getSourceImageSize() const; virtual ~TextureResource(); - bool isInitialized() const; + bool isLoaded() const; bool isTiled() const; const Vector2i getSize() const; @@ -34,11 +72,13 @@ class TextureResource : public IReloadable static size_t getTotalMemUsage(); // returns an approximation of total VRAM used by textures (in bytes) static size_t getTotalTextureSize(); // returns the number of bytes that would be used if all textures were in memory + static void resetCache(); -protected: - TextureResource(const std::string& path, bool tile, bool dynamic); - virtual void unload(std::shared_ptr& rm); - virtual void reload(std::shared_ptr& rm); +public: + virtual bool unload(); + virtual void reload(); + + void onTextureLoaded(std::shared_ptr tex); private: // mTextureData is used for textures that are not loaded from a file - these ones @@ -52,9 +92,14 @@ class TextureResource : public IReloadable Vector2f mSourceSize; bool mForceLoad; - typedef std::pair TextureKeyType; + typedef std::tuple TextureKeyType; static std::map< TextureKeyType, std::weak_ptr > sTextureMap; // map of textures, used to prevent duplicate textures + static std::map< TextureKeyType, std::shared_ptr > sPermanentTextureMap; // map of textures, used to prevent duplicate textures // FCAWEAK static std::set sAllTextures; // Set of all textures, used for memory management + +#if _DEBUG + std::string mPath; +#endif }; #endif // ES_CORE_RESOURCES_TEXTURE_RESOURCE_H diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index a30d0ca2d7..36eb356b53 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -1,661 +1,1075 @@ -#define _FILE_OFFSET_BITS 64 - -#include "utils/FileSystemUtil.h" - -#include -#include - -#if defined(_WIN32) -// because windows... -#include -#include -#define getcwd _getcwd -#define mkdir(x,y) _mkdir(x) -#define snprintf _snprintf -#define stat64 _stat64 -#define unlink _unlink -#define S_ISREG(x) (((x) & S_IFMT) == S_IFREG) -#define S_ISDIR(x) (((x) & S_IFMT) == S_IFDIR) -#else // _WIN32 -#include -#include -#endif // _WIN32 - -namespace Utils -{ - namespace FileSystem - { - static std::string homePath = ""; - static std::string exePath = ""; - -#if defined(_WIN32) - static std::string convertFromWideString(const std::wstring wstring) - { - int numBytes = WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), nullptr, 0, nullptr, nullptr); - std::string string; - - string.resize(numBytes); - WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), (char*)string.c_str(), numBytes, nullptr, nullptr); - - return std::string(string); - - } // convertFromWideString -#endif // _WIN32 - - stringList getDirContent(const std::string& _path, const bool _recursive) - { - std::string path = getGenericPath(_path); - stringList contentList; - - // only parse the directory, if it's a directory - if(isDirectory(path)) - { - -#if defined(_WIN32) - WIN32_FIND_DATAW findData; - std::string wildcard = path + "/*"; - HANDLE hFind = FindFirstFileW(std::wstring(wildcard.begin(), wildcard.end()).c_str(), &findData); - - if(hFind != INVALID_HANDLE_VALUE) - { - // loop over all files in the directory - do - { - std::string name = convertFromWideString(findData.cFileName); - - // ignore "." and ".." - if((name != ".") && (name != "..")) - { - std::string fullName(getGenericPath(path + "/" + name)); - contentList.push_back(fullName); - - if(_recursive && isDirectory(fullName)) - contentList.merge(getDirContent(fullName, true)); - } - } - while(FindNextFileW(hFind, &findData)); - - FindClose(hFind); - } -#else // _WIN32 - DIR* dir = opendir(path.c_str()); - - if(dir != NULL) - { - struct dirent* entry; - - // loop over all files in the directory - while((entry = readdir(dir)) != NULL) - { - std::string name(entry->d_name); - - // ignore "." and ".." - if((name != ".") && (name != "..")) - { - std::string fullName(getGenericPath(path + "/" + name)); - contentList.push_back(fullName); - - if(_recursive && isDirectory(fullName)) - contentList.merge(getDirContent(fullName, true)); - } - } - - closedir(dir); - } -#endif // _WIN32 - - } - - // sort the content list - contentList.sort(); - - // return the content list - return contentList; - - } // getDirContent - - stringList getPathList(const std::string& _path) - { - stringList pathList; - std::string path = getGenericPath(_path); - size_t start = 0; - size_t end = 0; - - // split at '/' - while((end = path.find("/", start)) != std::string::npos) - { - if(end != start) - pathList.push_back(std::string(path, start, end - start)); - - start = end + 1; - } - - // add last folder / file to pathList - if(start != path.size()) - pathList.push_back(std::string(path, start, path.size() - start)); - - // return the path list - return pathList; - - } // getPathList - - void setHomePath(const std::string& _path) - { - homePath = getGenericPath(_path); - - } // setHomePath - - std::string getHomePath() - { - // only construct the homepath once - if(homePath.length()) - return homePath; - - // check if "getExePath()/.emulationstation/es_systems.cfg" exists - if(Utils::FileSystem::exists(getExePath() + "/.emulationstation/es_systems.cfg")) - homePath = getExePath(); - - // check for HOME environment variable - if(!homePath.length()) - { - char* envHome = getenv("HOME"); - if(envHome) - homePath = getGenericPath(envHome); - } - -#if defined(_WIN32) - // on Windows we need to check HOMEDRIVE and HOMEPATH - if(!homePath.length()) - { - char* envHomeDrive = getenv("HOMEDRIVE"); - char* envHomePath = getenv("HOMEPATH"); - if(envHomeDrive && envHomePath) - homePath = getGenericPath(std::string(envHomeDrive) + "/" + envHomePath); - } -#endif // _WIN32 - - // no homepath found, fall back to current working directory - if(!homePath.length()) - homePath = getCWDPath(); - - // return constructed homepath - return homePath; - - } // getHomePath - - std::string getCWDPath() - { - char temp[512]; - - // return current working directory path - return (getcwd(temp, 512) ? getGenericPath(temp) : ""); - - } // getCWDPath - - void setExePath(const std::string& _path) - { - exePath = getCanonicalPath(_path); - - if(isRegularFile(exePath)) - exePath = getParent(exePath); - - } // setExePath - - std::string getExePath() - { - // return constructed exepath - return exePath; - - } // getExePath - - std::string getPreferredPath(const std::string& _path) - { - std::string path = _path; - size_t offset = std::string::npos; -#if defined(_WIN32) - // convert '/' to '\\' - while((offset = path.find('/')) != std::string::npos) - path.replace(offset, 1, "\\"); -#endif // _WIN32 - return path; - } - - std::string getGenericPath(const std::string& _path) - { - std::string path = _path; - size_t offset = std::string::npos; - - // remove "\\\\?\\" - if((path.find("\\\\?\\")) == 0) - path.erase(0, 4); - - // convert '\\' to '/' - while((offset = path.find('\\')) != std::string::npos) - path.replace(offset, 1 ,"/"); - - // remove double '/' - while((offset = path.find("//")) != std::string::npos) - path.erase(offset, 1); - - // remove trailing '/' - while(path.length() && ((offset = path.find_last_of('/')) == (path.length() - 1))) - path.erase(offset, 1); - - // return generic path - return path; - - } // getGenericPath - - std::string getEscapedPath(const std::string& _path) - { - std::string path = getGenericPath(_path); - -#if defined(_WIN32) - // windows escapes stuff by just putting everything in quotes - return '"' + getPreferredPath(path) + '"'; -#else // _WIN32 - // insert a backslash before most characters that would mess up a bash path - const char* invalidChars = "\\ '\"!$^&*(){}[]?;<>"; - const char* invalidChar = invalidChars; - - while(*invalidChar) - { - size_t start = 0; - size_t offset = 0; - - while((offset = path.find(*invalidChar, start)) != std::string::npos) - { - start = offset + 1; - - if((offset == 0) || (path[offset - 1] != '\\')) - { - path.insert(offset, 1, '\\'); - ++start; - } - } - - ++invalidChar; - } - - // return escaped path - return path; -#endif // _WIN32 - - } // getEscapedPath - - std::string getCanonicalPath(const std::string& _path) - { - // temporary hack for builtin resources - if((_path[0] == ':') && (_path[1] == '/')) - return _path; - - std::string path = exists(_path) ? getAbsolutePath(_path) : getGenericPath(_path); - - // cleanup path - bool scan = true; - while(scan) - { - stringList pathList = getPathList(path); - - path.clear(); - scan = false; - - for(stringList::const_iterator it = pathList.cbegin(); it != pathList.cend(); ++it) - { - // ignore empty - if((*it).empty()) - continue; - - // remove "/./" - if((*it) == ".") - continue; - - // resolve "/../" - if((*it) == "..") - { - path = getParent(path); - continue; - } - -#if defined(_WIN32) - // append folder to path - path += (path.size() == 0) ? (*it) : ("/" + (*it)); -#else // _WIN32 - // append folder to path - path += ("/" + (*it)); -#endif // _WIN32 - - // resolve symlink - if(isSymlink(path)) - { - std::string resolved = resolveSymlink(path); - - if(resolved.empty()) - return ""; - - if(isAbsolute(resolved)) - path = resolved; - else - path = getParent(path) + "/" + resolved; - - for(++it; it != pathList.cend(); ++it) - path += (path.size() == 0) ? (*it) : ("/" + (*it)); - - scan = true; - break; - } - } - } - - // return canonical path - return path; - - } // getCanonicalPath - - std::string getAbsolutePath(const std::string& _path, const std::string& _base) - { - std::string path = getGenericPath(_path); - std::string base = isAbsolute(_base) ? getGenericPath(_base) : getAbsolutePath(_base); - - // return absolute path - return isAbsolute(path) ? path : getGenericPath(base + "/" + path); - - } // getAbsolutePath - - std::string getParent(const std::string& _path) - { - std::string path = getGenericPath(_path); - size_t offset = std::string::npos; - - // find last '/' and erase it - if((offset = path.find_last_of('/')) != std::string::npos) - return path.erase(offset); - - // no parent found - return path; - - } // getParent - - std::string getFileName(const std::string& _path) - { - std::string path = getGenericPath(_path); - size_t offset = std::string::npos; - - // find last '/' and return the filename - if((offset = path.find_last_of('/')) != std::string::npos) - return ((path[offset + 1] == 0) ? "." : std::string(path, offset + 1)); - - // no '/' found, entire path is a filename - return path; - - } // getFileName - - std::string getStem(const std::string& _path) - { - std::string fileName = getFileName(_path); - size_t offset = std::string::npos; - - // empty fileName - if(fileName == ".") - return fileName; - - // find last '.' and erase the extension - if((offset = fileName.find_last_of('.')) != std::string::npos) - return fileName.erase(offset); - - // no '.' found, filename has no extension - return fileName; - - } // getStem - - std::string getExtension(const std::string& _path) - { - std::string fileName = getFileName(_path); - size_t offset = std::string::npos; - - // empty fileName - if(fileName == ".") - return fileName; - - // find last '.' and return the extension - if((offset = fileName.find_last_of('.')) != std::string::npos) - return std::string(fileName, offset); - - // no '.' found, filename has no extension - return "."; - - } // getExtension - - std::string resolveRelativePath(const std::string& _path, const std::string& _relativeTo, const bool _allowHome) - { - std::string path = getGenericPath(_path); - std::string relativeTo = isDirectory(_relativeTo) ? getGenericPath(_relativeTo) : getParent(_relativeTo); - - // nothing to resolve - if(!path.length()) - return path; - - // replace '.' with relativeTo - if((path[0] == '.') && (path[1] == '/')) - return (relativeTo + &(path[1])); - - // replace '~' with homePath - if(_allowHome && (path[0] == '~') && (path[1] == '/')) - return (getHomePath() + &(path[1])); - - // nothing to resolve - return path; - - } // resolveRelativePath - - std::string createRelativePath(const std::string& _path, const std::string& _relativeTo, const bool _allowHome) - { - bool contains = false; - std::string path = removeCommonPath(_path, _relativeTo, contains); - - // success - if(contains) - return ("./" + path); - - if(_allowHome) - { - path = removeCommonPath(_path, getHomePath(), contains); - - // success - if(contains) - return ("~/" + path); - } - - // nothing to resolve - return path; - - } // createRelativePath - - std::string removeCommonPath(const std::string& _path, const std::string& _common, bool& _contains) - { - std::string path = getGenericPath(_path); - std::string common = isDirectory(_common) ? getGenericPath(_common) : getParent(_common); - - // check if path contains common - if(path.find(common) == 0) - { - _contains = true; - return path.substr(common.length() + 1); - } - - // it didn't - _contains = false; - return path; - - } // removeCommonPath - - std::string resolveSymlink(const std::string& _path) - { - std::string path = getGenericPath(_path); - std::string resolved; - -#if defined(_WIN32) - HANDLE hFile = CreateFile(path.c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0); - - if(hFile != INVALID_HANDLE_VALUE) - { - resolved.resize(GetFinalPathNameByHandle(hFile, nullptr, 0, FILE_NAME_NORMALIZED) + 1); - if(GetFinalPathNameByHandle(hFile, (LPSTR)resolved.data(), (DWORD)resolved.size(), FILE_NAME_NORMALIZED) > 0) - { - resolved.resize(resolved.size() - 1); - resolved = getGenericPath(resolved); - } - CloseHandle(hFile); - } -#else // _WIN32 - struct stat info; - - // check if lstat succeeded - if(lstat(path.c_str(), &info) == 0) - { - resolved.resize(info.st_size); - if(readlink(path.c_str(), (char*)resolved.data(), resolved.size()) > 0) - resolved = getGenericPath(resolved); - } -#endif // _WIN32 - - // return resolved path - return resolved; - - } // resolveSymlink - - bool removeFile(const std::string& _path) - { - std::string path = getGenericPath(_path); - - // don't remove if it doesn't exists - if(!exists(path)) - return true; - - // try to remove file - return (unlink(path.c_str()) == 0); - - } // removeFile - - bool createDirectory(const std::string& _path) - { - std::string path = getGenericPath(_path); - - // don't create if it already exists - if(exists(path)) - return true; - - // try to create directory - if(mkdir(path.c_str(), 0755) == 0) - return true; - - // failed to create directory, try to create the parent - std::string parent = getParent(path); - - // only try to create parent if it's not identical to path - if(parent != path) - createDirectory(parent); - - // try to create directory again now that the parent should exist - return (mkdir(path.c_str(), 0755) == 0); - - } // createDirectory - - bool exists(const std::string& _path) - { - std::string path = getGenericPath(_path); - struct stat64 info; - - // check if stat64 succeeded - return (stat64(path.c_str(), &info) == 0); - - } // exists - - bool isAbsolute(const std::string& _path) - { - std::string path = getGenericPath(_path); - -#if defined(_WIN32) - return ((path.size() > 1) && (path[1] == ':')); -#else // _WIN32 - return ((path.size() > 0) && (path[0] == '/')); -#endif // _WIN32 - - } // isAbsolute - - bool isRegularFile(const std::string& _path) - { - std::string path = getGenericPath(_path); - struct stat64 info; - - // check if stat64 succeeded - if(stat64(path.c_str(), &info) != 0) - return false; - - // check for S_IFREG attribute - return (S_ISREG(info.st_mode)); - - } // isRegularFile - - bool isDirectory(const std::string& _path) - { - std::string path = getGenericPath(_path); - struct stat info; - - // check if stat succeeded - if(stat(path.c_str(), &info) != 0) - return false; - - // check for S_IFDIR attribute - return (S_ISDIR(info.st_mode)); - - } // isDirectory - - bool isSymlink(const std::string& _path) - { - std::string path = getGenericPath(_path); - -#if defined(_WIN32) - // check for symlink attribute - const DWORD Attributes = GetFileAttributes(path.c_str()); - if((Attributes != INVALID_FILE_ATTRIBUTES) && (Attributes & FILE_ATTRIBUTE_REPARSE_POINT)) - return true; -#else // _WIN32 - struct stat info; - - // check if lstat succeeded - if(lstat(path.c_str(), &info) != 0) - return false; - - // check for S_IFLNK attribute - return (S_ISLNK(info.st_mode)); -#endif // _WIN32 - - // not a symlink - return false; - - } // isSymlink - - bool isHidden(const std::string& _path) - { - std::string path = getGenericPath(_path); - -#if defined(_WIN32) - // check for hidden attribute - const DWORD Attributes = GetFileAttributes(path.c_str()); - if((Attributes != INVALID_FILE_ATTRIBUTES) && (Attributes & FILE_ATTRIBUTE_HIDDEN)) - return true; -#endif // _WIN32 - - // filenames starting with . are hidden in linux, we do this check for windows as well - if(getFileName(path)[0] == '.') - return true; - - // not hidden - return false; - - } // isHidden - - } // FileSystem:: - -} // Utils:: +#define _FILE_OFFSET_BITS 64 + +#include "utils/FileSystemUtil.h" +#include "utils/StringUtil.h" + +#include "Settings.h" +#include +#include +#include "platform.h" + +#if defined(_WIN32) +// because windows... +#include +#include +#include +#define getcwd _getcwd +#define mkdir(x,y) _mkdir(x) +#define snprintf _snprintf +#define stat64 _stat64 +#define unlink _unlink +#define S_ISREG(x) (((x) & S_IFMT) == S_IFREG) +#define S_ISDIR(x) (((x) & S_IFMT) == S_IFDIR) +#else // _WIN32 +#include +#include +#include +#endif // _WIN32 +#include +#include + +namespace Utils +{ + namespace FileSystem + { + static std::string homePath; + static std::string exePath; + + struct FileCache + { + FileCache() {} + + FileCache(bool _exists, bool _dir) + { + directory = _dir; + exists = _exists; + hidden = false; + isSymLink = false; + } + +#if WIN32 + FileCache(DWORD dwFileAttributes) + { + if (0xFFFFFFFF == dwFileAttributes) + { + directory = false; + exists = false; + hidden = false; + isSymLink = false; + } + else + { + exists = true; + directory = dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY; + hidden = dwFileAttributes & FILE_ATTRIBUTE_HIDDEN; + isSymLink = dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT; + } + } +#else + FileCache(const std::string& name, dirent* entry) + { + exists = true; + hidden = (getFileName(name)[0] == '.'); + directory = (entry->d_type == 4); // DT_DIR; + isSymLink = (entry->d_type == 10); // DT_LNK; + } + + FileCache(dirent* entry, bool _hidden) + { + exists = true; + hidden = _hidden; + directory = (entry->d_type == 4); // DT_DIR; + isSymLink = (entry->d_type == 10); // DT_LNK; + } +#endif + + bool exists; + bool directory; + bool hidden; + bool isSymLink; + + static int fromStat64(const std::string& key, struct stat64* info) + { + int ret = stat64(key.c_str(), info); + + std::unique_lock lock(mFileCacheMutex); + + FileCache cache(ret == 0, false); + if (cache.exists) + { + cache.directory = S_ISDIR(info->st_mode); +#ifndef WIN32 + cache.isSymLink = S_ISLNK(info->st_mode); +#endif + } + + mFileCache[key] = cache; + + return ret; + } + + static void add(const std::string& key, FileCache cache) + { + if (!mEnabled) + return; + + std::unique_lock lock(mFileCacheMutex); + mFileCache[key] = cache; + } + + static FileCache* get(const std::string& key) + { + if (!mEnabled) + return nullptr; + + std::unique_lock lock(mFileCacheMutex); + + auto it = mFileCache.find(key); + if (it != mFileCache.cend()) + return &it->second; + + it = mFileCache.find(Utils::FileSystem::getParent(key)+"/*"); + if (it != mFileCache.cend()) + { + mFileCache[key] = FileCache(false, false); + return &mFileCache[key]; + } + + return nullptr; + } + + static void resetCache() + { + std::unique_lock lock(mFileCacheMutex); + mFileCache.clear(); + } + + static void setEnabled(bool value) { mEnabled = value; } + + private: + static std::map mFileCache; + static std::mutex mFileCacheMutex; + static bool mEnabled; + }; + + std::map FileCache::mFileCache; + std::mutex FileCache::mFileCacheMutex; + bool FileCache::mEnabled = false; + + FileSystemCacheActivator::FileSystemCacheActivator() + { + if (mReferenceCount == 0) + { + FileCache::setEnabled(true); + FileCache::resetCache(); + } + + mReferenceCount++; + } + + FileSystemCacheActivator::~FileSystemCacheActivator() + { + mReferenceCount--; + + if (mReferenceCount <= 0) + { + FileCache::setEnabled(false); + FileCache::resetCache(); + } + } + + int FileSystemCacheActivator::mReferenceCount = 0; + + fileList getDirInfo(const std::string& _path/*, const bool _recursive*/) + { + std::string path = getGenericPath(_path); + fileList contentList; + + // only parse the directory, if it's a directory + if (isDirectory(path)) + { + FileCache::add(path + "/*", FileCache(true, true)); + +#if defined(_WIN32) + WIN32_FIND_DATAW findData; + std::string wildcard = path + "/*"; + + HANDLE hFind = FindFirstFileExW(std::wstring(wildcard.begin(), wildcard.end()).c_str(), + FINDEX_INFO_LEVELS::FindExInfoStandard, &findData, FINDEX_SEARCH_OPS::FindExSearchNameMatch + , NULL, FIND_FIRST_EX_LARGE_FETCH); + + if (hFind != INVALID_HANDLE_VALUE) + { + // loop over all files in the directory + do + { + std::string name = Utils::String::convertFromWideString(findData.cFileName); + + if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY && name == "." || name == "..") + continue; + + FileInfo fi; + fi.path = path + "/" + getGenericPath(name); + fi.hidden = (findData.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN) == FILE_ATTRIBUTE_HIDDEN; + fi.directory = (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY; + contentList.push_back(fi); + + FileCache::add(fi.path, FileCache((DWORD)findData.dwFileAttributes)); + } + while (FindNextFileW(hFind, &findData)); + + FindClose(hFind); + } +#else // _WIN32 + DIR* dir = opendir(path.c_str()); + + if (dir != NULL) + { + struct dirent* entry; + + // loop over all files in the directory + while ((entry = readdir(dir)) != NULL) + { + std::string name(entry->d_name); + + // ignore "." and ".." + if ((name != ".") && (name != "..")) + { + std::string fullName(path + "/" + getGenericPath(name)); + + FileInfo fi; + fi.path = fullName; + fi.hidden = Utils::FileSystem::isHidden(fullName); + fi.directory = (entry->d_type == 4); // DT_DIR; + contentList.push_back(fi); + + FileCache::add(fullName, FileCache(entry, fi.hidden)); + } + } + + closedir(dir); + } +#endif // _WIN32 + + } + + // return the content list + return contentList; + + } // getDirContent + + stringList getDirContent(const std::string& _path, const bool _recursive, const bool includeHidden) + { + std::string path = getGenericPath(_path); + stringList contentList; + + // only parse the directory, if it's a directory + if(isDirectory(path)) + { + FileCache::add(path + "/*", FileCache(true, true)); + +#if defined(_WIN32) + WIN32_FIND_DATAW findData; + std::string wildcard = path + "/*"; + + HANDLE hFind = FindFirstFileExW(std::wstring(wildcard.begin(), wildcard.end()).c_str(), + FINDEX_INFO_LEVELS::FindExInfoStandard, &findData, FINDEX_SEARCH_OPS::FindExSearchNameMatch + , NULL, FIND_FIRST_EX_LARGE_FETCH); + + if(hFind != INVALID_HANDLE_VALUE) + { + // loop over all files in the directory + do + { + std::string name = Utils::String::convertFromWideString(findData.cFileName); + + // ignore "." and ".." + if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY && name == "." || name == "..") + continue; + + std::string fullName(path + "/" + getGenericPath(name)); + + if (!includeHidden && (findData.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN) == FILE_ATTRIBUTE_HIDDEN) + continue; + + contentList.push_back(fullName); + + FileCache::add(fullName, FileCache((DWORD)findData.dwFileAttributes)); + + if (_recursive && (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY) + { + for (auto child : getDirContent(fullName, true, includeHidden)) + contentList.push_back(child); + } + } + while(FindNextFileW(hFind, &findData)); + + FindClose(hFind); + } +#else // _WIN32 + DIR* dir = opendir(path.c_str()); + + if(dir != NULL) + { + struct dirent* entry; + + // loop over all files in the directory + while((entry = readdir(dir)) != NULL) + { + std::string name(entry->d_name); + + // ignore "." and ".." + if((name != ".") && (name != "..")) + { + std::string fullName(getGenericPath(path + "/" + name)); + + FileCache::add(fullName, FileCache(fullName, entry)); + + if (!includeHidden && Utils::FileSystem::isHidden(fullName)) + continue; + + contentList.push_back(fullName); + + if(_recursive && isDirectory(fullName)) + contentList.merge(getDirContent(fullName, true)); + } + } + + closedir(dir); + } +#endif // _WIN32 + + } + + // return the content list + return contentList; + + } // getDirContent + + stringList getPathList(const std::string& _path) + { + stringList pathList; + std::string path = getGenericPath(_path); + size_t start = 0; + size_t end = 0; + + // split at '/' + while((end = path.find("/", start)) != std::string::npos) + { + if(end != start) + pathList.push_back(std::string(path, start, end - start)); + + start = end + 1; + } + + // add last folder / file to pathList + if(start != path.size()) + pathList.push_back(std::string(path, start, path.size() - start)); + + // return the path list + return pathList; + + } // getPathList + + void setHomePath(const std::string& _path) + { + homePath = Utils::FileSystem::getGenericPath(_path); + } + + std::string getHomePath() + { + // only construct the homepath once + if (homePath.length()) + return homePath; + + // check if "getExePath()/.emulationstation/es_systems.cfg" exists + if (Utils::FileSystem::exists(getExePath() + "/.emulationstation/es_systems.cfg")) + homePath = getExePath(); + + // check for HOME environment variable + if (!homePath.length()) + { + char* envHome = getenv("HOME"); + if (envHome) + homePath = getGenericPath(envHome); + } + +#if defined(_WIN32) + // on Windows we need to check HOMEDRIVE and HOMEPATH + if (!homePath.length()) + { + char* envHomeDrive = getenv("HOMEDRIVE"); + char* envHomePath = getenv("HOMEPATH"); + if (envHomeDrive && envHomePath) + homePath = getGenericPath(std::string(envHomeDrive) + "/" + envHomePath); + } +#endif // _WIN32 + + // no homepath found, fall back to current working directory + if (!homePath.length()) + homePath = getCWDPath(); + + homePath = getGenericPath(homePath); + + // return constructed homepath + return homePath; + + } // getHomePath + + std::string getCWDPath() + { + char temp[512]; + return (getcwd(temp, 512) ? getGenericPath(temp) : ""); + } // getCWDPath + + void setExePath(const std::string& _path) + { + constexpr int path_max = 32767; +#if defined(_WIN32) + std::wstring result(path_max, 0); + if (GetModuleFileNameW(nullptr, &result[0], path_max) != 0) { + exePath = getCanonicalPath(Utils::String::convertFromWideString(result)); + } +#else + std::string result(path_max, 0); + if (readlink("/proc/self/exe", &result[0], path_max) != -1) { + exePath = getCanonicalPath(result); + } +#endif + // If the native implementations fail, fallback to argv[0] + if (exePath.empty()) { + exePath = getCanonicalPath(_path); + } + if (isRegularFile(exePath)) { + exePath = getParent(exePath); + } + } // setExePath + + std::string getExePath() + { + // return constructed exepath + return exePath; + + } // getExePath + + std::string getPreferredPath(const std::string& _path) + { + std::string path = _path; + size_t offset = std::string::npos; +#if defined(_WIN32) + // convert '/' to '\\' + while((offset = path.find('/')) != std::string::npos) + path.replace(offset, 1, "\\"); +#endif // _WIN32 + return path; + } + + std::string getGenericPath(const std::string& _path) + { + std::string path = _path; + size_t offset = std::string::npos; + + // remove "\\\\?\\" + if((path.find("\\\\?\\")) == 0) + path.erase(0, 4); + + // convert '\\' to '/' + while((offset = path.find('\\')) != std::string::npos) + path.replace(offset, 1 ,"/"); + + // remove double '/' + while((offset = path.find("//")) != std::string::npos) + path.erase(offset, 1); + + // remove trailing '/' + while(path.length() && ((offset = path.find_last_of('/')) == (path.length() - 1))) + path.erase(offset, 1); + + // return generic path + return path; + + } // getGenericPath + + std::string getEscapedPath(const std::string& _path) + { + std::string path = getGenericPath(_path); + +#if defined(_WIN32) + // windows escapes stuff by just putting everything in quotes + return '"' + getPreferredPath(path) + '"'; +#else // _WIN32 + // insert a backslash before most characters that would mess up a bash path + const char* invalidChars = "\\ '\"!$^&*(){}[]?;<>"; + const char* invalidChar = invalidChars; + + while(*invalidChar) + { + size_t start = 0; + size_t offset = 0; + + while((offset = path.find(*invalidChar, start)) != std::string::npos) + { + start = offset + 1; + + if((offset == 0) || (path[offset - 1] != '\\')) + { + path.insert(offset, 1, '\\'); + ++start; + } + } + + ++invalidChar; + } + + // return escaped path + return path; +#endif // _WIN32 + + } // getEscapedPath + + std::string getCanonicalPath(const std::string& _path) + { + // temporary hack for builtin resources + if(_path.size() >= 2 && _path[0] == ':' && _path[1] == '/') + return _path; + +#if WIN32 + std::string path = _path[0] == '.' ? getAbsolutePath(_path) : getGenericPath(_path); + if (path.find("./") == std::string::npos) + return path; +#else + std::string path = exists(_path) ? getAbsolutePath(_path) : getGenericPath(_path); +#endif + + // cleanup path + bool scan = true; + while(scan) + { + stringList pathList = getPathList(path); + + path.clear(); + scan = false; + + for(stringList::const_iterator it = pathList.cbegin(); it != pathList.cend(); ++it) + { + // ignore empty + if((*it).empty()) + continue; + + // remove "/./" + if((*it) == ".") + continue; + + // resolve "/../" + if((*it) == "..") + { + path = getParent(path); + continue; + } + +#if defined(_WIN32) + // append folder to path + path += (path.size() == 0) ? (*it) : ("/" + (*it)); +#else // _WIN32 + // append folder to path + path += ("/" + (*it)); + + + // resolve symlink + if(isSymlink(path)) + { + std::string resolved = resolveSymlink(path); + + if(resolved.empty()) + return ""; + + if(isAbsolute(resolved)) + path = resolved; + else + path = getParent(path) + "/" + resolved; + + for(++it; it != pathList.cend(); ++it) + path += (path.size() == 0) ? (*it) : ("/" + (*it)); + + scan = true; + break; + } +#endif // _WIN32 + } + } + + // return canonical path + return path; + + } // getCanonicalPath + + std::string getAbsolutePath(const std::string& _path, const std::string& _base) + { + if (_path.empty() || isAbsolute(_path)) + return getGenericPath(_path); + + return getCanonicalPath(_base + "/" + _path); + } // getAbsolutePath + + std::string getParent(const std::string& _path) + { + std::string path = getGenericPath(_path); + size_t offset = std::string::npos; + + // find last '/' and erase it + if((offset = path.find_last_of('/')) != std::string::npos) + return path.erase(offset); + + // no parent found + return path; + + } // getParent + + std::string getFileName(const std::string& _path) + { + std::string path = getGenericPath(_path); + size_t offset = std::string::npos; + + // find last '/' and return the filename + if((offset = path.find_last_of('/')) != std::string::npos) + return ((path[offset + 1] == 0) ? "." : std::string(path, offset + 1)); + + // no '/' found, entire path is a filename + return path; + + } // getFileName + + std::string getStem(const std::string& _path) + { + std::string fileName = getFileName(_path); + size_t offset = std::string::npos; + + // empty fileName + if(fileName == ".") + return fileName; + + // find last '.' and erase the extension + if((offset = fileName.find_last_of('.')) != std::string::npos) + return fileName.erase(offset); + + // no '.' found, filename has no extension + return fileName; + + } // getStem + + std::string getExtension(const std::string& _path) + { + std::string fileName = getFileName(_path); + size_t offset = std::string::npos; + + // empty fileName + if(fileName == ".") + return fileName; + + // find last '.' and return the extension + if((offset = fileName.find_last_of('.')) != std::string::npos) + return std::string(fileName, offset); + + // no '.' found, filename has no extension + return "."; + + } // getExtension + + std::string resolveRelativePath(const std::string& _path, const std::string& _relativeTo, const bool _allowHome) + { + // nothing to resolve + if(!_path.length()) + return _path; + + if (_path.length() == 1 && _path[0] == '.') + return getGenericPath(_relativeTo); + + // replace '.' with relativeTo + if((_path[0] == '.') && (_path[1] == '/' || _path[1] == '\\')) + return getGenericPath(_relativeTo + &(_path[1])); + + // replace '~' with homePath + if(_allowHome && (_path[0] == '~') && (_path[1] == '/' || _path[1] == '\\')) + return getCanonicalPath(getHomePath() + &(_path[1])); + + // nothing to resolve + return getGenericPath(_path); + + } // resolveRelativePath + + std::string createRelativePath(const std::string& _path, const std::string& _relativeTo, const bool _allowHome) + { + if (_relativeTo.empty()) + return _path; + + if (_path == _relativeTo) + return ""; + + bool contains = false; + std::string path = removeCommonPath(_path, _relativeTo, contains); + + if(contains) + { + // success + return ("./" + path); + } + + if(_allowHome) + { + path = removeCommonPath(_path, getHomePath(), contains); + + if(contains) + { + // success + return ("~/" + path); + } + } + + // nothing to resolve + return path; + + } // createRelativePath + + std::string removeCommonPath(const std::string& _path, const std::string& _common, bool& _contains) + { + std::string path = _path; // getGenericPath(_path); + //std::string common = isDirectory(_common) ? getGenericPath(_common) : getParent(_common); + + // check if path contains common + if(path.find(_common) == 0 && path != _common) + { + _contains = true; + int trailingSlash = _common.find_last_of('/') == (_common.length() - 1) ? 0 : 1; + return path.substr(_common.length() + trailingSlash); + } + + // it didn't + _contains = false; + return path; + + } // removeCommonPath + + std::string resolveSymlink(const std::string& _path) + { + std::string path = getGenericPath(_path); + std::string resolved; + +#if defined(_WIN32) + HANDLE hFile = CreateFile(path.c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0); + + if(hFile != INVALID_HANDLE_VALUE) + { + resolved.resize(GetFinalPathNameByHandle(hFile, nullptr, 0, FILE_NAME_NORMALIZED) + 1); + if(GetFinalPathNameByHandle(hFile, (LPSTR)resolved.data(), (DWORD)resolved.size(), FILE_NAME_NORMALIZED) > 0) + { + resolved.resize(resolved.size() - 1); + resolved = getGenericPath(resolved); + } + CloseHandle(hFile); + } +#else // _WIN32 + struct stat info; + + // check if lstat succeeded + if(lstat(path.c_str(), &info) == 0) + { + resolved.resize(info.st_size); + if(readlink(path.c_str(), (char*)resolved.data(), resolved.size()) > 0) + resolved = getGenericPath(resolved); + } +#endif // _WIN32 + + // return resolved path + return resolved; + + } // resolveSymlink + + bool removeFile(const std::string& _path) + { + std::string path = getGenericPath(_path); + + // don't remove if it doesn't exists + if(!exists(path)) + return true; + + // try to remove file + return (unlink(path.c_str()) == 0); + + } // removeFile + + bool copyFile(const std::string src, const std::string dst) + { + std::string path = getGenericPath(src); + std::string pathD = getGenericPath(dst); + + // don't remove if it doesn't exists + if (!exists(path)) + return true; + + char buf[512]; + size_t size; + + FILE* source = fopen(path.c_str(), "rb"); + if (source == nullptr) + return false; + + FILE* dest = fopen(pathD.c_str(), "wb"); + if (dest == nullptr) + { + fclose(source); + return false; + } + + while (size = fread(buf, 1, 512, source)) + fwrite(buf, 1, size, dest); + + fclose(dest); + fclose(source); + + return true; + } // removeFile + + bool createDirectory(const std::string& _path) + { + FileCache::resetCache(); + + std::string path = getGenericPath(_path); + + // don't create if it already exists + if(exists(path)) + return true; + + // try to create directory + if(mkdir(path.c_str(), 0755) == 0) + return true; + + // failed to create directory, try to create the parent + std::string parent = getParent(path); + + // only try to create parent if it's not identical to path + if(parent != path) + createDirectory(parent); + + // try to create directory again now that the parent should exist + return (mkdir(path.c_str(), 0755) == 0); + + } // createDirectory + + bool exists(const std::string& _path) + { + if (_path.empty()) + return false; + + auto it = FileCache::get(_path); + if (it != nullptr) + return it->exists; + +#ifdef WIN32 + DWORD dwAttr = GetFileAttributes(_path.c_str()); + FileCache::add(_path, FileCache(dwAttr)); + if (0xFFFFFFFF == dwAttr) + return false; + + return true; +#else + std::string path = getGenericPath(_path); + struct stat64 info; + + // check if stat64 succeeded + return FileCache::fromStat64(path, &info) == 0; +#endif + } // exists + + size_t getFileSize(const std::string& _path) + { + if (!exists(_path)) + return 0; + + std::string path = getGenericPath(_path); + struct stat64 info; + + // check if stat64 succeeded + if ((stat64(path.c_str(), &info) == 0)) + return (size_t) info.st_size; + + return 0; + } + + bool isAbsolute(const std::string& _path) + { + if (_path.size() >= 2 && _path[0] == ':' && _path[1] == '/') + return true; + + std::string path = getGenericPath(_path); + +#ifdef WIN32 + return ((path.size() > 1) && (path[1] == ':')); +#else // _WIN32 + return ((path.size() > 0) && (path[0] == '/')); +#endif // _WIN32 + + } // isAbsolute + + bool isRegularFile(const std::string& _path) + { + auto it = FileCache::get(_path); + if (it != nullptr) + return it->exists && !it->directory && !it->isSymLink; + + std::string path = getGenericPath(_path); + struct stat64 info; + + // check if stat64 succeeded + if (FileCache::fromStat64(path, &info) != 0) //if(stat64(path.c_str(), &info) != 0) + return false; + + // check for S_IFREG attribute + return (S_ISREG(info.st_mode)); + + } // isRegularFile + + bool isDirectory(const std::string& _path) + { + auto it = FileCache::get(_path); + if (it != nullptr && !it->isSymLink) + return it->exists && it->directory; + +#ifdef WIN32 + DWORD dwAttr = GetFileAttributes(_path.c_str()); + FileCache::add(_path, FileCache(dwAttr)); + return (dwAttr != INVALID_FILE_ATTRIBUTES) && (dwAttr & FILE_ATTRIBUTE_DIRECTORY); +#else + std::string path = getGenericPath(_path); + struct stat64 info; + + // check if stat succeeded + if (FileCache::fromStat64(path, &info) != 0) //if(stat64(path.c_str(), &info) != 0) + return false; + + // check for S_IFDIR attribute + return (S_ISDIR(info.st_mode)); +#endif + } // isDirectory + + bool isSymlink(const std::string& _path) + { + auto it = FileCache::get(_path); + if (it != nullptr) + return it->exists && it->isSymLink; + + std::string path = getGenericPath(_path); + +#ifdef WIN32 + // check for symlink attribute + DWORD Attributes = GetFileAttributes(path.c_str()); + FileCache::add(_path, FileCache(Attributes)); + return (Attributes != INVALID_FILE_ATTRIBUTES) && (Attributes & FILE_ATTRIBUTE_REPARSE_POINT); +#else // WIN32 + struct stat64 info; + + // check if lstat succeeded + if (FileCache::fromStat64(path, &info) != 0) //if(stat64(path.c_str(), &info) != 0) + return false; + + // check for S_IFLNK attribute + return (S_ISLNK(info.st_mode)); +#endif //_WIN32 + + } // isSymlink + + bool isHidden(const std::string& _path) + { + auto it = FileCache::get(_path); + if (it != nullptr) + return it->exists && it->hidden; + + std::string path = getGenericPath(_path); + +#ifdef WIN32 + // check for hidden attribute + DWORD Attributes = GetFileAttributes(path.c_str()); + FileCache::add(_path, FileCache(Attributes)); + return (Attributes != INVALID_FILE_ATTRIBUTES && Attributes & FILE_ATTRIBUTE_HIDDEN); +#endif // _WIN32 + + // filenames starting with . are hidden in linux, we do not do this check for windows as well + if (getFileName(path)[0] == '.') + return true; + + // not hidden + return false; + + } // isHidden + + + std::string combine(const std::string& _path, const std::string& filename) + { + std::string gp = getGenericPath(_path); + + if (Utils::String::startsWith(filename, "/..")) + { + auto f = getPathList(filename); + + int count = 0; + for (auto it = f.cbegin(); it != f.cend(); ++it) + { + if (*it != "..") + break; + + count++; + } + + if (count > 0) + { + auto list = getPathList(gp); + std::vector p(list.begin(), list.end()); + + std::string result; + + for (int i = 0; i < p.size() - count; i++) + { + if (result.empty()) + result = p.at(i); + else + result = result + "/" + p.at(i); + } + + std::vector fn(f.begin(), f.end()); + for (int i = count; i < fn.size(); i++) + { + if (result.empty()) + result = fn.at(i); + else + result = result + "/" + fn.at(i); + } + + return result; + } + } + + + if (!Utils::String::endsWith(gp, "/") && !Utils::String::endsWith(gp, "\\")) + if (!Utils::String::startsWith(filename, "/") && !Utils::String::startsWith(filename, "\\")) + gp += "/"; + + return gp + filename; + } + + std::string readAllText(const std::string fileName) + { + std::ifstream t(fileName); + std::stringstream buffer; + buffer << t.rdbuf(); + return buffer.str(); + } + + void writeAllText(const std::string fileName, const std::string text) + { + std::fstream fs; + fs.open(fileName.c_str(), std::fstream::out); + fs << text; + fs.close(); + } + } // FileSystem:: + +} // Utils:: diff --git a/es-core/src/utils/FileSystemUtil.h b/es-core/src/utils/FileSystemUtil.h index db45a1c538..9d3e88f825 100644 --- a/es-core/src/utils/FileSystemUtil.h +++ b/es-core/src/utils/FileSystemUtil.h @@ -11,7 +11,7 @@ namespace Utils { typedef std::list stringList; - stringList getDirContent (const std::string& _path, const bool _recursive = false); + stringList getDirContent (const std::string& _path, const bool _recursive = false, const bool includeHidden = true); stringList getPathList (const std::string& _path); void setHomePath (const std::string& _path); std::string getHomePath (); @@ -31,17 +31,48 @@ namespace Utils std::string createRelativePath (const std::string& _path, const std::string& _relativeTo, const bool _allowHome); std::string removeCommonPath (const std::string& _path, const std::string& _common, bool& _contains); std::string resolveSymlink (const std::string& _path); + std::string combine(const std::string& _path, const std::string& filename); bool removeFile (const std::string& _path); bool createDirectory (const std::string& _path); bool exists (const std::string& _path); + size_t getFileSize(const std::string& _path); bool isAbsolute (const std::string& _path); bool isRegularFile (const std::string& _path); bool isDirectory (const std::string& _path); bool isSymlink (const std::string& _path); bool isHidden (const std::string& _path); + + // FCA + struct FileInfo + { + public: + std::string path; + bool hidden; + bool directory; + }; + typedef std::list fileList; + + fileList getDirInfo(const std::string& _path/*, const bool _recursive = false*/); + + std::string readAllText(const std::string fileName); + void writeAllText (const std::string fileName, const std::string text); + bool copyFile(const std::string src, const std::string dst); + + class FileSystemCacheActivator + { + public: + FileSystemCacheActivator(); + ~FileSystemCacheActivator(); + + private: + static int mReferenceCount; + }; + } // FileSystem:: + + } // Utils:: #endif // ES_CORE_UTILS_FILE_SYSTEM_UTIL_H diff --git a/es-core/src/utils/StringUtil.cpp b/es-core/src/utils/StringUtil.cpp index b9ba8cbe6c..057e1c20c6 100644 --- a/es-core/src/utils/StringUtil.cpp +++ b/es-core/src/utils/StringUtil.cpp @@ -1,298 +1,571 @@ -#include "utils/StringUtil.h" - -#include -#include - -namespace Utils -{ - namespace String - { - unsigned int chars2Unicode(const std::string& _string, size_t& _cursor) - { - const char& c = _string[_cursor]; - unsigned int result = '?'; - - if((c & 0x80) == 0) // 0xxxxxxx, one byte character - { - // 0xxxxxxx - result = ((_string[_cursor++] ) ); - } - else if((c & 0xE0) == 0xC0) // 110xxxxx, two byte character - { - // 110xxxxx 10xxxxxx - result = ((_string[_cursor++] & 0x1F) << 6) | - ((_string[_cursor++] & 0x3F) ); - } - else if((c & 0xF0) == 0xE0) // 1110xxxx, three byte character - { - // 1110xxxx 10xxxxxx 10xxxxxx - result = ((_string[_cursor++] & 0x0F) << 12) | - ((_string[_cursor++] & 0x3F) << 6) | - ((_string[_cursor++] & 0x3F) ); - } - else if((c & 0xF8) == 0xF0) // 11110xxx, four byte character - { - // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx - result = ((_string[_cursor++] & 0x07) << 18) | - ((_string[_cursor++] & 0x3F) << 12) | - ((_string[_cursor++] & 0x3F) << 6) | - ((_string[_cursor++] & 0x3F) ); - } - else - { - // error, invalid unicode - ++_cursor; - } - - return result; - - } // chars2Unicode - - std::string unicode2Chars(const unsigned int _unicode) - { - std::string result; - - if(_unicode < 0x80) // one byte character - { - result += ((_unicode ) & 0xFF); - } - else if(_unicode < 0x800) // two byte character - { - result += ((_unicode >> 6) & 0xFF) | 0xC0; - result += ((_unicode ) & 0x3F) | 0x80; - } - else if(_unicode < 0xFFFF) // three byte character - { - result += ((_unicode >> 12) & 0xFF) | 0xE0; - result += ((_unicode >> 6) & 0x3F) | 0x80; - result += ((_unicode ) & 0x3F) | 0x80; - } - else if(_unicode <= 0x1fffff) // four byte character - { - result += ((_unicode >> 18) & 0xFF) | 0xF0; - result += ((_unicode >> 12) & 0x3F) | 0x80; - result += ((_unicode >> 6) & 0x3F) | 0x80; - result += ((_unicode ) & 0x3F) | 0x80; - } - else - { - // error, invalid unicode - result += '?'; - } - - return result; - - } // unicode2Chars - - size_t nextCursor(const std::string& _string, const size_t _cursor) - { - size_t result = _cursor; - - while(result < _string.length()) - { - ++result; - - if((_string[result] & 0xC0) != 0x80) // break if current character is not 10xxxxxx - break; - } - - return result; - - } // nextCursor - - size_t prevCursor(const std::string& _string, const size_t _cursor) - { - size_t result = _cursor; - - while(result > 0) - { - --result; - - if((_string[result] & 0xC0) != 0x80) // break if current character is not 10xxxxxx - break; - } - - return result; - - } // prevCursor - - size_t moveCursor(const std::string& _string, const size_t _cursor, const int _amount) - { - size_t result = _cursor; - - if(_amount > 0) - { - for(int i = 0; i < _amount; ++i) - result = nextCursor(_string, result); - } - else if(_amount < 0) - { - for(int i = _amount; i < 0; ++i) - result = prevCursor(_string, result); - } - - return result; - - } // moveCursor - - std::string toLower(const std::string& _string) - { - std::string string; - - for(size_t i = 0; i < _string.length(); ++i) - string += (char)tolower(_string[i]); - - return string; - - } // toLower - - std::string toUpper(const std::string& _string) - { - std::string string; - - for(size_t i = 0; i < _string.length(); ++i) - string += (char)toupper(_string[i]); - - return string; - - } // toUpper - - std::string trim(const std::string& _string) - { - const size_t strBegin = _string.find_first_not_of(" \t"); - const size_t strEnd = _string.find_last_not_of(" \t"); - - if(strBegin == std::string::npos) - return ""; - - return _string.substr(strBegin, strEnd - strBegin + 1); - - } // trim - - std::string replace(const std::string& _string, const std::string& _replace, const std::string& _with) - { - std::string string = _string; - size_t pos; - - while((pos = string.find(_replace)) != std::string::npos) - string = string.replace(pos, _replace.length(), _with.c_str(), _with.length()); - - return string; - - } // replace - - bool startsWith(const std::string& _string, const std::string& _start) - { - return (_string.find(_start) == 0); - - } // startsWith - - bool endsWith(const std::string& _string, const std::string& _end) - { - return (_string.find(_end) == (_string.size() - _end.size())); - - } // endsWith - - std::string removeParenthesis(const std::string& _string) - { - static const char remove[4] = { '(', ')', '[', ']' }; - std::string string = _string; - size_t start; - size_t end; - bool done = false; - - while(!done) - { - done = true; - - for(size_t i = 0; i < sizeof(remove); i += 2) - { - end = string.find_first_of(remove[i + 1]); - start = string.find_last_of( remove[i + 0], end); - - if((start != std::string::npos) && (end != std::string::npos)) - { - string.erase(start, end - start + 1); - done = false; - } - } - } - - return trim(string); - - } // removeParenthesis - - stringVector commaStringToVector(const std::string& _string) - { - stringVector vector; - size_t start = 0; - size_t comma = _string.find(","); - - while(comma != std::string::npos) - { - vector.push_back(_string.substr(start, comma - start)); - start = comma + 1; - comma = _string.find(",", start); - } - - vector.push_back(_string.substr(start)); - std::sort(vector.begin(), vector.end()); - - return vector; - - } // commaStringToVector - - std::string vectorToCommaString(stringVector _vector) - { - std::string string; - - std::sort(_vector.begin(), _vector.end()); - - for(stringVector::const_iterator it = _vector.cbegin(); it != _vector.cend(); ++it) - string += (string.length() ? "," : "") + (*it); - - return string; - - } // vectorToCommaString - - std::string format(const char* _format, ...) - { - va_list args; - va_list copy; - - va_start(args, _format); - - va_copy(copy, args); - const int length = vsnprintf(nullptr, 0, _format, copy); - va_end(copy); - - char* buffer = new char[length + 1]; - va_copy(copy, args); - vsnprintf(buffer, length + 1, _format, copy); - va_end(copy); - - va_end(args); - - std::string out(buffer); - delete buffer; - - return out; - - } // format - - std::string scramble(const std::string& _input, const std::string& _key) - { - std::string buffer = _input; - - for(size_t i = 0; i < _input.size(); ++i) - { - buffer[i] = _input[i] ^ _key[i]; - } - - return buffer; - - } // scramble - - } // String:: - -} // Utils:: +#include "utils/StringUtil.h" + +#include +#include +#include + +#if defined(_WIN32) +#include +#endif + +namespace Utils +{ + namespace String + { + static wchar_t unicode_lowers[] = + { + (wchar_t)0x0061, (wchar_t)0x0062, (wchar_t)0x0063, (wchar_t)0x0064, (wchar_t)0x0065, (wchar_t)0x0066, (wchar_t)0x0067, (wchar_t)0x0068, (wchar_t)0x0069, + (wchar_t)0x006A, (wchar_t)0x006B, (wchar_t)0x006C, (wchar_t)0x006D, (wchar_t)0x006E, (wchar_t)0x006F, (wchar_t)0x0070, (wchar_t)0x0071, (wchar_t)0x0072, + (wchar_t)0x0073, (wchar_t)0x0074, (wchar_t)0x0075, (wchar_t)0x0076, (wchar_t)0x0077, (wchar_t)0x0078, (wchar_t)0x0079, (wchar_t)0x007A, (wchar_t)0x00E0, + (wchar_t)0x00E1, (wchar_t)0x00E2, (wchar_t)0x00E3, (wchar_t)0x00E4, (wchar_t)0x00E5, (wchar_t)0x00E6, (wchar_t)0x00E7, (wchar_t)0x00E8, (wchar_t)0x00E9, + (wchar_t)0x00EA, (wchar_t)0x00EB, (wchar_t)0x00EC, (wchar_t)0x00ED, (wchar_t)0x00EE, (wchar_t)0x00EF, (wchar_t)0x00F0, (wchar_t)0x00F1, (wchar_t)0x00F2, + (wchar_t)0x00F3, (wchar_t)0x00F4, (wchar_t)0x00F5, (wchar_t)0x00F6, (wchar_t)0x00F8, (wchar_t)0x00F9, (wchar_t)0x00FA, (wchar_t)0x00FB, (wchar_t)0x00FC, + (wchar_t)0x00FD, (wchar_t)0x00FE, (wchar_t)0x00FF, (wchar_t)0x0101, (wchar_t)0x0103, (wchar_t)0x0105, (wchar_t)0x0107, (wchar_t)0x0109, (wchar_t)0x010B, + (wchar_t)0x010D, (wchar_t)0x010F, (wchar_t)0x0111, (wchar_t)0x0113, (wchar_t)0x0115, (wchar_t)0x0117, (wchar_t)0x0119, (wchar_t)0x011B, (wchar_t)0x011D, + (wchar_t)0x011F, (wchar_t)0x0121, (wchar_t)0x0123, (wchar_t)0x0125, (wchar_t)0x0127, (wchar_t)0x0129, (wchar_t)0x012B, (wchar_t)0x012D, (wchar_t)0x012F, + (wchar_t)0x0131, (wchar_t)0x0133, (wchar_t)0x0135, (wchar_t)0x0137, (wchar_t)0x013A, (wchar_t)0x013C, (wchar_t)0x013E, (wchar_t)0x0140, (wchar_t)0x0142, + (wchar_t)0x0144, (wchar_t)0x0146, (wchar_t)0x0148, (wchar_t)0x014B, (wchar_t)0x014D, (wchar_t)0x014F, (wchar_t)0x0151, (wchar_t)0x0153, (wchar_t)0x0155, + (wchar_t)0x0157, (wchar_t)0x0159, (wchar_t)0x015B, (wchar_t)0x015D, (wchar_t)0x015F, (wchar_t)0x0161, (wchar_t)0x0163, (wchar_t)0x0165, (wchar_t)0x0167, + (wchar_t)0x0169, (wchar_t)0x016B, (wchar_t)0x016D, (wchar_t)0x016F, (wchar_t)0x0171, (wchar_t)0x0173, (wchar_t)0x0175, (wchar_t)0x0177, (wchar_t)0x017A, + (wchar_t)0x017C, (wchar_t)0x017E, (wchar_t)0x0183, (wchar_t)0x0185, (wchar_t)0x0188, (wchar_t)0x018C, (wchar_t)0x0192, (wchar_t)0x0199, (wchar_t)0x01A1, + (wchar_t)0x01A3, (wchar_t)0x01A5, (wchar_t)0x01A8, (wchar_t)0x01AD, (wchar_t)0x01B0, (wchar_t)0x01B4, (wchar_t)0x01B6, (wchar_t)0x01B9, (wchar_t)0x01BD, + (wchar_t)0x01C6, (wchar_t)0x01C9, (wchar_t)0x01CC, (wchar_t)0x01CE, (wchar_t)0x01D0, (wchar_t)0x01D2, (wchar_t)0x01D4, (wchar_t)0x01D6, (wchar_t)0x01D8, + (wchar_t)0x01DA, (wchar_t)0x01DC, (wchar_t)0x01DF, (wchar_t)0x01E1, (wchar_t)0x01E3, (wchar_t)0x01E5, (wchar_t)0x01E7, (wchar_t)0x01E9, (wchar_t)0x01EB, + (wchar_t)0x01ED, (wchar_t)0x01EF, (wchar_t)0x01F3, (wchar_t)0x01F5, (wchar_t)0x01FB, (wchar_t)0x01FD, (wchar_t)0x01FF, (wchar_t)0x0201, (wchar_t)0x0203, + (wchar_t)0x0205, (wchar_t)0x0207, (wchar_t)0x0209, (wchar_t)0x020B, (wchar_t)0x020D, (wchar_t)0x020F, (wchar_t)0x0211, (wchar_t)0x0213, (wchar_t)0x0215, + (wchar_t)0x0217, (wchar_t)0x0253, (wchar_t)0x0254, (wchar_t)0x0257, (wchar_t)0x0258, (wchar_t)0x0259, (wchar_t)0x025B, (wchar_t)0x0260, (wchar_t)0x0263, + (wchar_t)0x0268, (wchar_t)0x0269, (wchar_t)0x026F, (wchar_t)0x0272, (wchar_t)0x0275, (wchar_t)0x0283, (wchar_t)0x0288, (wchar_t)0x028A, (wchar_t)0x028B, + (wchar_t)0x0292, (wchar_t)0x03AC, (wchar_t)0x03AD, (wchar_t)0x03AE, (wchar_t)0x03AF, (wchar_t)0x03B1, (wchar_t)0x03B2, (wchar_t)0x03B3, (wchar_t)0x03B4, + (wchar_t)0x03B5, (wchar_t)0x03B6, (wchar_t)0x03B7, (wchar_t)0x03B8, (wchar_t)0x03B9, (wchar_t)0x03BA, (wchar_t)0x03BB, (wchar_t)0x03BC, (wchar_t)0x03BD, + (wchar_t)0x03BE, (wchar_t)0x03BF, (wchar_t)0x03C0, (wchar_t)0x03C1, (wchar_t)0x03C3, (wchar_t)0x03C4, (wchar_t)0x03C5, (wchar_t)0x03C6, (wchar_t)0x03C7, + (wchar_t)0x03C8, (wchar_t)0x03C9, (wchar_t)0x03CA, (wchar_t)0x03CB, (wchar_t)0x03CC, (wchar_t)0x03CD, (wchar_t)0x03CE, (wchar_t)0x03E3, (wchar_t)0x03E5, + (wchar_t)0x03E7, (wchar_t)0x03E9, (wchar_t)0x03EB, (wchar_t)0x03ED, (wchar_t)0x03EF, (wchar_t)0x0430, (wchar_t)0x0431, (wchar_t)0x0432, (wchar_t)0x0433, + (wchar_t)0x0434, (wchar_t)0x0435, (wchar_t)0x0436, (wchar_t)0x0437, (wchar_t)0x0438, (wchar_t)0x0439, (wchar_t)0x043A, (wchar_t)0x043B, (wchar_t)0x043C, + (wchar_t)0x043D, (wchar_t)0x043E, (wchar_t)0x043F, (wchar_t)0x0440, (wchar_t)0x0441, (wchar_t)0x0442, (wchar_t)0x0443, (wchar_t)0x0444, (wchar_t)0x0445, + (wchar_t)0x0446, (wchar_t)0x0447, (wchar_t)0x0448, (wchar_t)0x0449, (wchar_t)0x044A, (wchar_t)0x044B, (wchar_t)0x044C, (wchar_t)0x044D, (wchar_t)0x044E, + (wchar_t)0x044F, (wchar_t)0x0451, (wchar_t)0x0452, (wchar_t)0x0453, (wchar_t)0x0454, (wchar_t)0x0455, (wchar_t)0x0456, (wchar_t)0x0457, (wchar_t)0x0458, + (wchar_t)0x0459, (wchar_t)0x045A, (wchar_t)0x045B, (wchar_t)0x045C, (wchar_t)0x045E, (wchar_t)0x045F, (wchar_t)0x0461, (wchar_t)0x0463, (wchar_t)0x0465, + (wchar_t)0x0467, (wchar_t)0x0469, (wchar_t)0x046B, (wchar_t)0x046D, (wchar_t)0x046F, (wchar_t)0x0471, (wchar_t)0x0473, (wchar_t)0x0475, (wchar_t)0x0477, + (wchar_t)0x0479, (wchar_t)0x047B, (wchar_t)0x047D, (wchar_t)0x047F, (wchar_t)0x0481, (wchar_t)0x0491, (wchar_t)0x0493, (wchar_t)0x0495, (wchar_t)0x0497, + (wchar_t)0x0499, (wchar_t)0x049B, (wchar_t)0x049D, (wchar_t)0x049F, (wchar_t)0x04A1, (wchar_t)0x04A3, (wchar_t)0x04A5, (wchar_t)0x04A7, (wchar_t)0x04A9, + (wchar_t)0x04AB, (wchar_t)0x04AD, (wchar_t)0x04AF, (wchar_t)0x04B1, (wchar_t)0x04B3, (wchar_t)0x04B5, (wchar_t)0x04B7, (wchar_t)0x04B9, (wchar_t)0x04BB, + (wchar_t)0x04BD, (wchar_t)0x04BF, (wchar_t)0x04C2, (wchar_t)0x04C4, (wchar_t)0x04C8, (wchar_t)0x04CC, (wchar_t)0x04D1, (wchar_t)0x04D3, (wchar_t)0x04D5, + (wchar_t)0x04D7, (wchar_t)0x04D9, (wchar_t)0x04DB, (wchar_t)0x04DD, (wchar_t)0x04DF, (wchar_t)0x04E1, (wchar_t)0x04E3, (wchar_t)0x04E5, (wchar_t)0x04E7, + (wchar_t)0x04E9, (wchar_t)0x04EB, (wchar_t)0x04EF, (wchar_t)0x04F1, (wchar_t)0x04F3, (wchar_t)0x04F5, (wchar_t)0x04F9, (wchar_t)0x0561, (wchar_t)0x0562, + (wchar_t)0x0563, (wchar_t)0x0564, (wchar_t)0x0565, (wchar_t)0x0566, (wchar_t)0x0567, (wchar_t)0x0568, (wchar_t)0x0569, (wchar_t)0x056A, (wchar_t)0x056B, + (wchar_t)0x056C, (wchar_t)0x056D, (wchar_t)0x056E, (wchar_t)0x056F, (wchar_t)0x0570, (wchar_t)0x0571, (wchar_t)0x0572, (wchar_t)0x0573, (wchar_t)0x0574, + (wchar_t)0x0575, (wchar_t)0x0576, (wchar_t)0x0577, (wchar_t)0x0578, (wchar_t)0x0579, (wchar_t)0x057A, (wchar_t)0x057B, (wchar_t)0x057C, (wchar_t)0x057D, + (wchar_t)0x057E, (wchar_t)0x057F, (wchar_t)0x0580, (wchar_t)0x0581, (wchar_t)0x0582, (wchar_t)0x0583, (wchar_t)0x0584, (wchar_t)0x0585, (wchar_t)0x0586, + (wchar_t)0x10D0, (wchar_t)0x10D1, (wchar_t)0x10D2, (wchar_t)0x10D3, (wchar_t)0x10D4, (wchar_t)0x10D5, (wchar_t)0x10D6, (wchar_t)0x10D7, (wchar_t)0x10D8, + (wchar_t)0x10D9, (wchar_t)0x10DA, (wchar_t)0x10DB, (wchar_t)0x10DC, (wchar_t)0x10DD, (wchar_t)0x10DE, (wchar_t)0x10DF, (wchar_t)0x10E0, (wchar_t)0x10E1, + (wchar_t)0x10E2, (wchar_t)0x10E3, (wchar_t)0x10E4, (wchar_t)0x10E5, (wchar_t)0x10E6, (wchar_t)0x10E7, (wchar_t)0x10E8, (wchar_t)0x10E9, (wchar_t)0x10EA, + (wchar_t)0x10EB, (wchar_t)0x10EC, (wchar_t)0x10ED, (wchar_t)0x10EE, (wchar_t)0x10EF, (wchar_t)0x10F0, (wchar_t)0x10F1, (wchar_t)0x10F2, (wchar_t)0x10F3, + (wchar_t)0x10F4, (wchar_t)0x10F5, (wchar_t)0x1E01, (wchar_t)0x1E03, (wchar_t)0x1E05, (wchar_t)0x1E07, (wchar_t)0x1E09, (wchar_t)0x1E0B, (wchar_t)0x1E0D, + (wchar_t)0x1E0F, (wchar_t)0x1E11, (wchar_t)0x1E13, (wchar_t)0x1E15, (wchar_t)0x1E17, (wchar_t)0x1E19, (wchar_t)0x1E1B, (wchar_t)0x1E1D, (wchar_t)0x1E1F, + (wchar_t)0x1E21, (wchar_t)0x1E23, (wchar_t)0x1E25, (wchar_t)0x1E27, (wchar_t)0x1E29, (wchar_t)0x1E2B, (wchar_t)0x1E2D, (wchar_t)0x1E2F, (wchar_t)0x1E31, + (wchar_t)0x1E33, (wchar_t)0x1E35, (wchar_t)0x1E37, (wchar_t)0x1E39, (wchar_t)0x1E3B, (wchar_t)0x1E3D, (wchar_t)0x1E3F, (wchar_t)0x1E41, (wchar_t)0x1E43, + (wchar_t)0x1E45, (wchar_t)0x1E47, (wchar_t)0x1E49, (wchar_t)0x1E4B, (wchar_t)0x1E4D, (wchar_t)0x1E4F, (wchar_t)0x1E51, (wchar_t)0x1E53, (wchar_t)0x1E55, + (wchar_t)0x1E57, (wchar_t)0x1E59, (wchar_t)0x1E5B, (wchar_t)0x1E5D, (wchar_t)0x1E5F, (wchar_t)0x1E61, (wchar_t)0x1E63, (wchar_t)0x1E65, (wchar_t)0x1E67, + (wchar_t)0x1E69, (wchar_t)0x1E6B, (wchar_t)0x1E6D, (wchar_t)0x1E6F, (wchar_t)0x1E71, (wchar_t)0x1E73, (wchar_t)0x1E75, (wchar_t)0x1E77, (wchar_t)0x1E79, + (wchar_t)0x1E7B, (wchar_t)0x1E7D, (wchar_t)0x1E7F, (wchar_t)0x1E81, (wchar_t)0x1E83, (wchar_t)0x1E85, (wchar_t)0x1E87, (wchar_t)0x1E89, (wchar_t)0x1E8B, + (wchar_t)0x1E8D, (wchar_t)0x1E8F, (wchar_t)0x1E91, (wchar_t)0x1E93, (wchar_t)0x1E95, (wchar_t)0x1EA1, (wchar_t)0x1EA3, (wchar_t)0x1EA5, (wchar_t)0x1EA7, + (wchar_t)0x1EA9, (wchar_t)0x1EAB, (wchar_t)0x1EAD, (wchar_t)0x1EAF, (wchar_t)0x1EB1, (wchar_t)0x1EB3, (wchar_t)0x1EB5, (wchar_t)0x1EB7, (wchar_t)0x1EB9, + (wchar_t)0x1EBB, (wchar_t)0x1EBD, (wchar_t)0x1EBF, (wchar_t)0x1EC1, (wchar_t)0x1EC3, (wchar_t)0x1EC5, (wchar_t)0x1EC7, (wchar_t)0x1EC9, (wchar_t)0x1ECB, + (wchar_t)0x1ECD, (wchar_t)0x1ECF, (wchar_t)0x1ED1, (wchar_t)0x1ED3, (wchar_t)0x1ED5, (wchar_t)0x1ED7, (wchar_t)0x1ED9, (wchar_t)0x1EDB, (wchar_t)0x1EDD, + (wchar_t)0x1EDF, (wchar_t)0x1EE1, (wchar_t)0x1EE3, (wchar_t)0x1EE5, (wchar_t)0x1EE7, (wchar_t)0x1EE9, (wchar_t)0x1EEB, (wchar_t)0x1EED, (wchar_t)0x1EEF, + (wchar_t)0x1EF1, (wchar_t)0x1EF3, (wchar_t)0x1EF5, (wchar_t)0x1EF7, (wchar_t)0x1EF9, (wchar_t)0x1F00, (wchar_t)0x1F01, (wchar_t)0x1F02, (wchar_t)0x1F03, + (wchar_t)0x1F04, (wchar_t)0x1F05, (wchar_t)0x1F06, (wchar_t)0x1F07, (wchar_t)0x1F10, (wchar_t)0x1F11, (wchar_t)0x1F12, (wchar_t)0x1F13, (wchar_t)0x1F14, + (wchar_t)0x1F15, (wchar_t)0x1F20, (wchar_t)0x1F21, (wchar_t)0x1F22, (wchar_t)0x1F23, (wchar_t)0x1F24, (wchar_t)0x1F25, (wchar_t)0x1F26, (wchar_t)0x1F27, + (wchar_t)0x1F30, (wchar_t)0x1F31, (wchar_t)0x1F32, (wchar_t)0x1F33, (wchar_t)0x1F34, (wchar_t)0x1F35, (wchar_t)0x1F36, (wchar_t)0x1F37, (wchar_t)0x1F40, + (wchar_t)0x1F41, (wchar_t)0x1F42, (wchar_t)0x1F43, (wchar_t)0x1F44, (wchar_t)0x1F45, (wchar_t)0x1F51, (wchar_t)0x1F53, (wchar_t)0x1F55, (wchar_t)0x1F57, + (wchar_t)0x1F60, (wchar_t)0x1F61, (wchar_t)0x1F62, (wchar_t)0x1F63, (wchar_t)0x1F64, (wchar_t)0x1F65, (wchar_t)0x1F66, (wchar_t)0x1F67, (wchar_t)0x1F80, + (wchar_t)0x1F81, (wchar_t)0x1F82, (wchar_t)0x1F83, (wchar_t)0x1F84, (wchar_t)0x1F85, (wchar_t)0x1F86, (wchar_t)0x1F87, (wchar_t)0x1F90, (wchar_t)0x1F91, + (wchar_t)0x1F92, (wchar_t)0x1F93, (wchar_t)0x1F94, (wchar_t)0x1F95, (wchar_t)0x1F96, (wchar_t)0x1F97, (wchar_t)0x1FA0, (wchar_t)0x1FA1, (wchar_t)0x1FA2, + (wchar_t)0x1FA3, (wchar_t)0x1FA4, (wchar_t)0x1FA5, (wchar_t)0x1FA6, (wchar_t)0x1FA7, (wchar_t)0x1FB0, (wchar_t)0x1FB1, (wchar_t)0x1FD0, (wchar_t)0x1FD1, + (wchar_t)0x1FE0, (wchar_t)0x1FE1, (wchar_t)0x24D0, (wchar_t)0x24D1, (wchar_t)0x24D2, (wchar_t)0x24D3, (wchar_t)0x24D4, (wchar_t)0x24D5, (wchar_t)0x24D6, + (wchar_t)0x24D7, (wchar_t)0x24D8, (wchar_t)0x24D9, (wchar_t)0x24DA, (wchar_t)0x24DB, (wchar_t)0x24DC, (wchar_t)0x24DD, (wchar_t)0x24DE, (wchar_t)0x24DF, + (wchar_t)0x24E0, (wchar_t)0x24E1, (wchar_t)0x24E2, (wchar_t)0x24E3, (wchar_t)0x24E4, (wchar_t)0x24E5, (wchar_t)0x24E6, (wchar_t)0x24E7, (wchar_t)0x24E8, + (wchar_t)0x24E9, (wchar_t)0xFF41, (wchar_t)0xFF42, (wchar_t)0xFF43, (wchar_t)0xFF44, (wchar_t)0xFF45, (wchar_t)0xFF46, (wchar_t)0xFF47, (wchar_t)0xFF48, + (wchar_t)0xFF49, (wchar_t)0xFF4A, (wchar_t)0xFF4B, (wchar_t)0xFF4C, (wchar_t)0xFF4D, (wchar_t)0xFF4E, (wchar_t)0xFF4F, (wchar_t)0xFF50, (wchar_t)0xFF51, + (wchar_t)0xFF52, (wchar_t)0xFF53, (wchar_t)0xFF54, (wchar_t)0xFF55, (wchar_t)0xFF56, (wchar_t)0xFF57, (wchar_t)0xFF58, (wchar_t)0xFF59, (wchar_t)0xFF5A}; + + static const wchar_t unicode_uppers[] = + { + (wchar_t)0x0041, (wchar_t)0x0042, (wchar_t)0x0043, (wchar_t)0x0044, (wchar_t)0x0045, (wchar_t)0x0046, (wchar_t)0x0047, (wchar_t)0x0048, (wchar_t)0x0049, + (wchar_t)0x004A, (wchar_t)0x004B, (wchar_t)0x004C, (wchar_t)0x004D, (wchar_t)0x004E, (wchar_t)0x004F, (wchar_t)0x0050, (wchar_t)0x0051, (wchar_t)0x0052, + (wchar_t)0x0053, (wchar_t)0x0054, (wchar_t)0x0055, (wchar_t)0x0056, (wchar_t)0x0057, (wchar_t)0x0058, (wchar_t)0x0059, (wchar_t)0x005A, (wchar_t)0x00C0, + (wchar_t)0x00C1, (wchar_t)0x00C2, (wchar_t)0x00C3, (wchar_t)0x00C4, (wchar_t)0x00C5, (wchar_t)0x00C6, (wchar_t)0x00C7, (wchar_t)0x00C8, (wchar_t)0x00C9, + (wchar_t)0x00CA, (wchar_t)0x00CB, (wchar_t)0x00CC, (wchar_t)0x00CD, (wchar_t)0x00CE, (wchar_t)0x00CF, (wchar_t)0x00D0, (wchar_t)0x00D1, (wchar_t)0x00D2, + (wchar_t)0x00D3, (wchar_t)0x00D4, (wchar_t)0x00D5, (wchar_t)0x00D6, (wchar_t)0x00D8, (wchar_t)0x00D9, (wchar_t)0x00DA, (wchar_t)0x00DB, (wchar_t)0x00DC, + (wchar_t)0x00DD, (wchar_t)0x00DE, (wchar_t)0x0178, (wchar_t)0x0100, (wchar_t)0x0102, (wchar_t)0x0104, (wchar_t)0x0106, (wchar_t)0x0108, (wchar_t)0x010A, + (wchar_t)0x010C, (wchar_t)0x010E, (wchar_t)0x0110, (wchar_t)0x0112, (wchar_t)0x0114, (wchar_t)0x0116, (wchar_t)0x0118, (wchar_t)0x011A, (wchar_t)0x011C, + (wchar_t)0x011E, (wchar_t)0x0120, (wchar_t)0x0122, (wchar_t)0x0124, (wchar_t)0x0126, (wchar_t)0x0128, (wchar_t)0x012A, (wchar_t)0x012C, (wchar_t)0x012E, + (wchar_t)0x0049, (wchar_t)0x0132, (wchar_t)0x0134, (wchar_t)0x0136, (wchar_t)0x0139, (wchar_t)0x013B, (wchar_t)0x013D, (wchar_t)0x013F, (wchar_t)0x0141, + (wchar_t)0x0143, (wchar_t)0x0145, (wchar_t)0x0147, (wchar_t)0x014A, (wchar_t)0x014C, (wchar_t)0x014E, (wchar_t)0x0150, (wchar_t)0x0152, (wchar_t)0x0154, + (wchar_t)0x0156, (wchar_t)0x0158, (wchar_t)0x015A, (wchar_t)0x015C, (wchar_t)0x015E, (wchar_t)0x0160, (wchar_t)0x0162, (wchar_t)0x0164, (wchar_t)0x0166, + (wchar_t)0x0168, (wchar_t)0x016A, (wchar_t)0x016C, (wchar_t)0x016E, (wchar_t)0x0170, (wchar_t)0x0172, (wchar_t)0x0174, (wchar_t)0x0176, (wchar_t)0x0179, + (wchar_t)0x017B, (wchar_t)0x017D, (wchar_t)0x0182, (wchar_t)0x0184, (wchar_t)0x0187, (wchar_t)0x018B, (wchar_t)0x0191, (wchar_t)0x0198, (wchar_t)0x01A0, + (wchar_t)0x01A2, (wchar_t)0x01A4, (wchar_t)0x01A7, (wchar_t)0x01AC, (wchar_t)0x01AF, (wchar_t)0x01B3, (wchar_t)0x01B5, (wchar_t)0x01B8, (wchar_t)0x01BC, + (wchar_t)0x01C4, (wchar_t)0x01C7, (wchar_t)0x01CA, (wchar_t)0x01CD, (wchar_t)0x01CF, (wchar_t)0x01D1, (wchar_t)0x01D3, (wchar_t)0x01D5, (wchar_t)0x01D7, + (wchar_t)0x01D9, (wchar_t)0x01DB, (wchar_t)0x01DE, (wchar_t)0x01E0, (wchar_t)0x01E2, (wchar_t)0x01E4, (wchar_t)0x01E6, (wchar_t)0x01E8, (wchar_t)0x01EA, + (wchar_t)0x01EC, (wchar_t)0x01EE, (wchar_t)0x01F1, (wchar_t)0x01F4, (wchar_t)0x01FA, (wchar_t)0x01FC, (wchar_t)0x01FE, (wchar_t)0x0200, (wchar_t)0x0202, + (wchar_t)0x0204, (wchar_t)0x0206, (wchar_t)0x0208, (wchar_t)0x020A, (wchar_t)0x020C, (wchar_t)0x020E, (wchar_t)0x0210, (wchar_t)0x0212, (wchar_t)0x0214, + (wchar_t)0x0216, (wchar_t)0x0181, (wchar_t)0x0186, (wchar_t)0x018A, (wchar_t)0x018E, (wchar_t)0x018F, (wchar_t)0x0190, (wchar_t)0x0193, (wchar_t)0x0194, + (wchar_t)0x0197, (wchar_t)0x0196, (wchar_t)0x019C, (wchar_t)0x019D, (wchar_t)0x019F, (wchar_t)0x01A9, (wchar_t)0x01AE, (wchar_t)0x01B1, (wchar_t)0x01B2, + (wchar_t)0x01B7, (wchar_t)0x0386, (wchar_t)0x0388, (wchar_t)0x0389, (wchar_t)0x038A, (wchar_t)0x0391, (wchar_t)0x0392, (wchar_t)0x0393, (wchar_t)0x0394, + (wchar_t)0x0395, (wchar_t)0x0396, (wchar_t)0x0397, (wchar_t)0x0398, (wchar_t)0x0399, (wchar_t)0x039A, (wchar_t)0x039B, (wchar_t)0x039C, (wchar_t)0x039D, + (wchar_t)0x039E, (wchar_t)0x039F, (wchar_t)0x03A0, (wchar_t)0x03A1, (wchar_t)0x03A3, (wchar_t)0x03A4, (wchar_t)0x03A5, (wchar_t)0x03A6, (wchar_t)0x03A7, + (wchar_t)0x03A8, (wchar_t)0x03A9, (wchar_t)0x03AA, (wchar_t)0x03AB, (wchar_t)0x038C, (wchar_t)0x038E, (wchar_t)0x038F, (wchar_t)0x03E2, (wchar_t)0x03E4, + (wchar_t)0x03E6, (wchar_t)0x03E8, (wchar_t)0x03EA, (wchar_t)0x03EC, (wchar_t)0x03EE, (wchar_t)0x0410, (wchar_t)0x0411, (wchar_t)0x0412, (wchar_t)0x0413, + (wchar_t)0x0414, (wchar_t)0x0415, (wchar_t)0x0416, (wchar_t)0x0417, (wchar_t)0x0418, (wchar_t)0x0419, (wchar_t)0x041A, (wchar_t)0x041B, (wchar_t)0x041C, + (wchar_t)0x041D, (wchar_t)0x041E, (wchar_t)0x041F, (wchar_t)0x0420, (wchar_t)0x0421, (wchar_t)0x0422, (wchar_t)0x0423, (wchar_t)0x0424, (wchar_t)0x0425, + (wchar_t)0x0426, (wchar_t)0x0427, (wchar_t)0x0428, (wchar_t)0x0429, (wchar_t)0x042A, (wchar_t)0x042B, (wchar_t)0x042C, (wchar_t)0x042D, (wchar_t)0x042E, + (wchar_t)0x042F, (wchar_t)0x0401, (wchar_t)0x0402, (wchar_t)0x0403, (wchar_t)0x0404, (wchar_t)0x0405, (wchar_t)0x0406, (wchar_t)0x0407, (wchar_t)0x0408, + (wchar_t)0x0409, (wchar_t)0x040A, (wchar_t)0x040B, (wchar_t)0x040C, (wchar_t)0x040E, (wchar_t)0x040F, (wchar_t)0x0460, (wchar_t)0x0462, (wchar_t)0x0464, + (wchar_t)0x0466, (wchar_t)0x0468, (wchar_t)0x046A, (wchar_t)0x046C, (wchar_t)0x046E, (wchar_t)0x0470, (wchar_t)0x0472, (wchar_t)0x0474, (wchar_t)0x0476, + (wchar_t)0x0478, (wchar_t)0x047A, (wchar_t)0x047C, (wchar_t)0x047E, (wchar_t)0x0480, (wchar_t)0x0490, (wchar_t)0x0492, (wchar_t)0x0494, (wchar_t)0x0496, + (wchar_t)0x0498, (wchar_t)0x049A, (wchar_t)0x049C, (wchar_t)0x049E, (wchar_t)0x04A0, (wchar_t)0x04A2, (wchar_t)0x04A4, (wchar_t)0x04A6, (wchar_t)0x04A8, + (wchar_t)0x04AA, (wchar_t)0x04AC, (wchar_t)0x04AE, (wchar_t)0x04B0, (wchar_t)0x04B2, (wchar_t)0x04B4, (wchar_t)0x04B6, (wchar_t)0x04B8, (wchar_t)0x04BA, + (wchar_t)0x04BC, (wchar_t)0x04BE, (wchar_t)0x04C1, (wchar_t)0x04C3, (wchar_t)0x04C7, (wchar_t)0x04CB, (wchar_t)0x04D0, (wchar_t)0x04D2, (wchar_t)0x04D4, + (wchar_t)0x04D6, (wchar_t)0x04D8, (wchar_t)0x04DA, (wchar_t)0x04DC, (wchar_t)0x04DE, (wchar_t)0x04E0, (wchar_t)0x04E2, (wchar_t)0x04E4, (wchar_t)0x04E6, + (wchar_t)0x04E8, (wchar_t)0x04EA, (wchar_t)0x04EE, (wchar_t)0x04F0, (wchar_t)0x04F2, (wchar_t)0x04F4, (wchar_t)0x04F8, (wchar_t)0x0531, (wchar_t)0x0532, + (wchar_t)0x0533, (wchar_t)0x0534, (wchar_t)0x0535, (wchar_t)0x0536, (wchar_t)0x0537, (wchar_t)0x0538, (wchar_t)0x0539, (wchar_t)0x053A, (wchar_t)0x053B, + (wchar_t)0x053C, (wchar_t)0x053D, (wchar_t)0x053E, (wchar_t)0x053F, (wchar_t)0x0540, (wchar_t)0x0541, (wchar_t)0x0542, (wchar_t)0x0543, (wchar_t)0x0544, + (wchar_t)0x0545, (wchar_t)0x0546, (wchar_t)0x0547, (wchar_t)0x0548, (wchar_t)0x0549, (wchar_t)0x054A, (wchar_t)0x054B, (wchar_t)0x054C, (wchar_t)0x054D, + (wchar_t)0x054E, (wchar_t)0x054F, (wchar_t)0x0550, (wchar_t)0x0551, (wchar_t)0x0552, (wchar_t)0x0553, (wchar_t)0x0554, (wchar_t)0x0555, (wchar_t)0x0556, + (wchar_t)0x10A0, (wchar_t)0x10A1, (wchar_t)0x10A2, (wchar_t)0x10A3, (wchar_t)0x10A4, (wchar_t)0x10A5, (wchar_t)0x10A6, (wchar_t)0x10A7, (wchar_t)0x10A8, + (wchar_t)0x10A9, (wchar_t)0x10AA, (wchar_t)0x10AB, (wchar_t)0x10AC, (wchar_t)0x10AD, (wchar_t)0x10AE, (wchar_t)0x10AF, (wchar_t)0x10B0, (wchar_t)0x10B1, + (wchar_t)0x10B2, (wchar_t)0x10B3, (wchar_t)0x10B4, (wchar_t)0x10B5, (wchar_t)0x10B6, (wchar_t)0x10B7, (wchar_t)0x10B8, (wchar_t)0x10B9, (wchar_t)0x10BA, + (wchar_t)0x10BB, (wchar_t)0x10BC, (wchar_t)0x10BD, (wchar_t)0x10BE, (wchar_t)0x10BF, (wchar_t)0x10C0, (wchar_t)0x10C1, (wchar_t)0x10C2, (wchar_t)0x10C3, + (wchar_t)0x10C4, (wchar_t)0x10C5, (wchar_t)0x1E00, (wchar_t)0x1E02, (wchar_t)0x1E04, (wchar_t)0x1E06, (wchar_t)0x1E08, (wchar_t)0x1E0A, (wchar_t)0x1E0C, + (wchar_t)0x1E0E, (wchar_t)0x1E10, (wchar_t)0x1E12, (wchar_t)0x1E14, (wchar_t)0x1E16, (wchar_t)0x1E18, (wchar_t)0x1E1A, (wchar_t)0x1E1C, (wchar_t)0x1E1E, + (wchar_t)0x1E20, (wchar_t)0x1E22, (wchar_t)0x1E24, (wchar_t)0x1E26, (wchar_t)0x1E28, (wchar_t)0x1E2A, (wchar_t)0x1E2C, (wchar_t)0x1E2E, (wchar_t)0x1E30, + (wchar_t)0x1E32, (wchar_t)0x1E34, (wchar_t)0x1E36, (wchar_t)0x1E38, (wchar_t)0x1E3A, (wchar_t)0x1E3C, (wchar_t)0x1E3E, (wchar_t)0x1E40, (wchar_t)0x1E42, + (wchar_t)0x1E44, (wchar_t)0x1E46, (wchar_t)0x1E48, (wchar_t)0x1E4A, (wchar_t)0x1E4C, (wchar_t)0x1E4E, (wchar_t)0x1E50, (wchar_t)0x1E52, (wchar_t)0x1E54, + (wchar_t)0x1E56, (wchar_t)0x1E58, (wchar_t)0x1E5A, (wchar_t)0x1E5C, (wchar_t)0x1E5E, (wchar_t)0x1E60, (wchar_t)0x1E62, (wchar_t)0x1E64, (wchar_t)0x1E66, + (wchar_t)0x1E68, (wchar_t)0x1E6A, (wchar_t)0x1E6C, (wchar_t)0x1E6E, (wchar_t)0x1E70, (wchar_t)0x1E72, (wchar_t)0x1E74, (wchar_t)0x1E76, (wchar_t)0x1E78, + (wchar_t)0x1E7A, (wchar_t)0x1E7C, (wchar_t)0x1E7E, (wchar_t)0x1E80, (wchar_t)0x1E82, (wchar_t)0x1E84, (wchar_t)0x1E86, (wchar_t)0x1E88, (wchar_t)0x1E8A, + (wchar_t)0x1E8C, (wchar_t)0x1E8E, (wchar_t)0x1E90, (wchar_t)0x1E92, (wchar_t)0x1E94, (wchar_t)0x1EA0, (wchar_t)0x1EA2, (wchar_t)0x1EA4, (wchar_t)0x1EA6, + (wchar_t)0x1EA8, (wchar_t)0x1EAA, (wchar_t)0x1EAC, (wchar_t)0x1EAE, (wchar_t)0x1EB0, (wchar_t)0x1EB2, (wchar_t)0x1EB4, (wchar_t)0x1EB6, (wchar_t)0x1EB8, + (wchar_t)0x1EBA, (wchar_t)0x1EBC, (wchar_t)0x1EBE, (wchar_t)0x1EC0, (wchar_t)0x1EC2, (wchar_t)0x1EC4, (wchar_t)0x1EC6, (wchar_t)0x1EC8, (wchar_t)0x1ECA, + (wchar_t)0x1ECC, (wchar_t)0x1ECE, (wchar_t)0x1ED0, (wchar_t)0x1ED2, (wchar_t)0x1ED4, (wchar_t)0x1ED6, (wchar_t)0x1ED8, (wchar_t)0x1EDA, (wchar_t)0x1EDC, + (wchar_t)0x1EDE, (wchar_t)0x1EE0, (wchar_t)0x1EE2, (wchar_t)0x1EE4, (wchar_t)0x1EE6, (wchar_t)0x1EE8, (wchar_t)0x1EEA, (wchar_t)0x1EEC, (wchar_t)0x1EEE, + (wchar_t)0x1EF0, (wchar_t)0x1EF2, (wchar_t)0x1EF4, (wchar_t)0x1EF6, (wchar_t)0x1EF8, (wchar_t)0x1F08, (wchar_t)0x1F09, (wchar_t)0x1F0A, (wchar_t)0x1F0B, + (wchar_t)0x1F0C, (wchar_t)0x1F0D, (wchar_t)0x1F0E, (wchar_t)0x1F0F, (wchar_t)0x1F18, (wchar_t)0x1F19, (wchar_t)0x1F1A, (wchar_t)0x1F1B, (wchar_t)0x1F1C, + (wchar_t)0x1F1D, (wchar_t)0x1F28, (wchar_t)0x1F29, (wchar_t)0x1F2A, (wchar_t)0x1F2B, (wchar_t)0x1F2C, (wchar_t)0x1F2D, (wchar_t)0x1F2E, (wchar_t)0x1F2F, + (wchar_t)0x1F38, (wchar_t)0x1F39, (wchar_t)0x1F3A, (wchar_t)0x1F3B, (wchar_t)0x1F3C, (wchar_t)0x1F3D, (wchar_t)0x1F3E, (wchar_t)0x1F3F, (wchar_t)0x1F48, + (wchar_t)0x1F49, (wchar_t)0x1F4A, (wchar_t)0x1F4B, (wchar_t)0x1F4C, (wchar_t)0x1F4D, (wchar_t)0x1F59, (wchar_t)0x1F5B, (wchar_t)0x1F5D, (wchar_t)0x1F5F, + (wchar_t)0x1F68, (wchar_t)0x1F69, (wchar_t)0x1F6A, (wchar_t)0x1F6B, (wchar_t)0x1F6C, (wchar_t)0x1F6D, (wchar_t)0x1F6E, (wchar_t)0x1F6F, (wchar_t)0x1F88, + (wchar_t)0x1F89, (wchar_t)0x1F8A, (wchar_t)0x1F8B, (wchar_t)0x1F8C, (wchar_t)0x1F8D, (wchar_t)0x1F8E, (wchar_t)0x1F8F, (wchar_t)0x1F98, (wchar_t)0x1F99, + (wchar_t)0x1F9A, (wchar_t)0x1F9B, (wchar_t)0x1F9C, (wchar_t)0x1F9D, (wchar_t)0x1F9E, (wchar_t)0x1F9F, (wchar_t)0x1FA8, (wchar_t)0x1FA9, (wchar_t)0x1FAA, + (wchar_t)0x1FAB, (wchar_t)0x1FAC, (wchar_t)0x1FAD, (wchar_t)0x1FAE, (wchar_t)0x1FAF, (wchar_t)0x1FB8, (wchar_t)0x1FB9, (wchar_t)0x1FD8, (wchar_t)0x1FD9, + (wchar_t)0x1FE8, (wchar_t)0x1FE9, (wchar_t)0x24B6, (wchar_t)0x24B7, (wchar_t)0x24B8, (wchar_t)0x24B9, (wchar_t)0x24BA, (wchar_t)0x24BB, (wchar_t)0x24BC, + (wchar_t)0x24BD, (wchar_t)0x24BE, (wchar_t)0x24BF, (wchar_t)0x24C0, (wchar_t)0x24C1, (wchar_t)0x24C2, (wchar_t)0x24C3, (wchar_t)0x24C4, (wchar_t)0x24C5, + (wchar_t)0x24C6, (wchar_t)0x24C7, (wchar_t)0x24C8, (wchar_t)0x24C9, (wchar_t)0x24CA, (wchar_t)0x24CB, (wchar_t)0x24CC, (wchar_t)0x24CD, (wchar_t)0x24CE, + (wchar_t)0x24CF, (wchar_t)0xFF21, (wchar_t)0xFF22, (wchar_t)0xFF23, (wchar_t)0xFF24, (wchar_t)0xFF25, (wchar_t)0xFF26, (wchar_t)0xFF27, (wchar_t)0xFF28, + (wchar_t)0xFF29, (wchar_t)0xFF2A, (wchar_t)0xFF2B, (wchar_t)0xFF2C, (wchar_t)0xFF2D, (wchar_t)0xFF2E, (wchar_t)0xFF2F, (wchar_t)0xFF30, (wchar_t)0xFF31, + (wchar_t)0xFF32, (wchar_t)0xFF33, (wchar_t)0xFF34, (wchar_t)0xFF35, (wchar_t)0xFF36, (wchar_t)0xFF37, (wchar_t)0xFF38, (wchar_t)0xFF39, (wchar_t)0xFF3A}; + + static int compareWchar(const void* a, const void* b) + { + if (*(wchar_t*)a < *(wchar_t*)b) + return -1; + else if (*(wchar_t*)a > *(wchar_t*)b) + return 1; + + return 0; + } + + static wchar_t tolowerUnicode(const wchar_t& c) + { + wchar_t* p = (wchar_t*)bsearch(&c, unicode_uppers, sizeof(unicode_uppers) / sizeof(wchar_t), sizeof(wchar_t), compareWchar); + if (p) + return *(unicode_lowers + (p - unicode_uppers)); + + return c; + } + + static wchar_t toupperUnicode(const wchar_t& c) + { + wchar_t* p = (wchar_t*)bsearch(&c, unicode_lowers, sizeof(unicode_lowers) / sizeof(wchar_t), sizeof(wchar_t), compareWchar); + if (p) + return *(unicode_uppers + (p - unicode_lowers)); + + return c; + } + + unsigned int chars2Unicode(const std::string& _string, size_t& _cursor) + { + const char& c = _string[_cursor]; + unsigned int result = '?'; + + if((c & 0x80) == 0) // 0xxxxxxx, one byte character + { + // 0xxxxxxx + result = ((_string[_cursor++] ) ); + } + else if((c & 0xE0) == 0xC0) // 110xxxxx, two byte character + { + // 110xxxxx 10xxxxxx + result = ((_string[_cursor++] & 0x1F) << 6) | + ((_string[_cursor++] & 0x3F) ); + } + else if((c & 0xF0) == 0xE0) // 1110xxxx, three byte character + { + // 1110xxxx 10xxxxxx 10xxxxxx + result = ((_string[_cursor++] & 0x0F) << 12) | + ((_string[_cursor++] & 0x3F) << 6) | + ((_string[_cursor++] & 0x3F) ); + } + else if((c & 0xF8) == 0xF0) // 11110xxx, four byte character + { + // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + result = ((_string[_cursor++] & 0x07) << 18) | + ((_string[_cursor++] & 0x3F) << 12) | + ((_string[_cursor++] & 0x3F) << 6) | + ((_string[_cursor++] & 0x3F) ); + } + else + { + // error, invalid unicode + ++_cursor; + } + + return result; + + } // chars2Unicode + + std::string unicode2Chars(const unsigned int _unicode) + { + std::string result; + + if(_unicode < 0x80) // one byte character + { + result += ((_unicode ) & 0xFF); + } + else if(_unicode < 0x800) // two byte character + { + result += ((_unicode >> 6) & 0xFF) | 0xC0; + result += ((_unicode ) & 0x3F) | 0x80; + } + else if(_unicode < 0xFFFF) // three byte character + { + result += ((_unicode >> 12) & 0xFF) | 0xE0; + result += ((_unicode >> 6) & 0x3F) | 0x80; + result += ((_unicode ) & 0x3F) | 0x80; + } + else if(_unicode <= 0x1fffff) // four byte character + { + result += ((_unicode >> 18) & 0xFF) | 0xF0; + result += ((_unicode >> 12) & 0x3F) | 0x80; + result += ((_unicode >> 6) & 0x3F) | 0x80; + result += ((_unicode ) & 0x3F) | 0x80; + } + else + { + // error, invalid unicode + result += '?'; + } + + return result; + + } // unicode2Chars + + size_t nextCursor(const std::string& _string, const size_t _cursor) + { + size_t result = _cursor; + + while(result < _string.length()) + { + ++result; + + if((_string[result] & 0xC0) != 0x80) // break if current character is not 10xxxxxx + break; + } + + return result; + + } // nextCursor + + size_t prevCursor(const std::string& _string, const size_t _cursor) + { + size_t result = _cursor; + + while(result > 0) + { + --result; + + if((_string[result] & 0xC0) != 0x80) // break if current character is not 10xxxxxx + break; + } + + return result; + + } // prevCursor + + size_t moveCursor(const std::string& _string, const size_t _cursor, const int _amount) + { + size_t result = _cursor; + + if(_amount > 0) + { + for(int i = 0; i < _amount; ++i) + result = nextCursor(_string, result); + } + else if(_amount < 0) + { + for(int i = _amount; i < 0; ++i) + result = prevCursor(_string, result); + } + + return result; + + } // moveCursor + + static std::string changeUnicodeCasing(const std::string& _string, bool toUpper) + { + std::string text = _string; + + size_t i = 0; + while (i < text.length()) + { + if ((text[i] & 0x80) == 0) + { + text[i] = toUpper ? toupper(text[i]) : tolower(text[i]); + i++; + continue; + } + + int pos = i; + wchar_t character = (wchar_t)chars2Unicode(text, i); + wchar_t unicode = toUpper ? toupperUnicode(character) : tolowerUnicode(character); + if (unicode != character) + { + int charSize = i - pos; + + if (charSize == 2) + { + text[pos] = (char)(((unicode >> 6) & 0xFF) | 0xC0); + text[pos + 1] = (char)((unicode & 0x3F) | 0x80); + } + else if (charSize == 3) + { + text[pos] += (char)(((unicode >> 12) & 0xFF) | 0xE0); + text[pos + 1] += (char)(((unicode >> 6) & 0x3F) | 0x80); + text[pos + 2] += (char)((unicode & 0x3F) | 0x80); + + } + } + } + + return text; + } + + std::string toLower(const std::string& _string) { return changeUnicodeCasing(_string, false); } + std::string toUpper(const std::string& _string) { return changeUnicodeCasing(_string, true); } + + std::string trim(const std::string& _string) + { + const size_t strBegin = _string.find_first_not_of(" \t"); + const size_t strEnd = _string.find_last_not_of(" \t"); + + if(strBegin == std::string::npos) + return ""; + + return _string.substr(strBegin, strEnd - strBegin + 1); + + } // trim + + std::string replace(const std::string& _string, const std::string& _replace, const std::string& _with) + { + std::string string = _string; + size_t pos; + + while((pos = string.find(_replace)) != std::string::npos) + string = string.replace(pos, _replace.length(), _with.c_str(), _with.length()); + + return string; + + } // replace + + bool startsWith(const std::string& _string, const std::string& _start) + { + return (_string.find(_start) == 0); + + } // startsWith + + bool endsWith(const std::string& _string, const std::string& _end) + { + if (_end.size() > _string.size()) return false; + return std::equal(_end.rbegin(), _end.rend(), _string.rbegin()); + //return (_string.find(_end, 0, ) == (_string.size() - _end.size())); + + } // endsWith + + std::string removeParenthesis(const std::string& _string) + { + static const char remove[4] = { '(', ')', '[', ']' }; + std::string string = _string; + size_t start; + size_t end; + bool done = false; + + while(!done) + { + done = true; + + for(int i = 0; i < sizeof(remove); i += 2) + { + end = string.find_first_of(remove[i + 1]); + start = string.find_last_of( remove[i + 0], end); + + if((start != std::string::npos) && (end != std::string::npos)) + { + string.erase(start, end - start + 1); + done = false; + } + } + } + + return trim(string); + + } // removeParenthesis + + stringVector commaStringToVector(const std::string& _string) + { + stringVector vector; + size_t start = 0; + size_t comma = _string.find(","); + + while(comma != std::string::npos) + { + vector.push_back(_string.substr(start, comma - start)); + start = comma + 1; + comma = _string.find(",", start); + } + + vector.push_back(_string.substr(start)); + std::sort(vector.begin(), vector.end()); + + return vector; + + } // commaStringToVector + + std::string vectorToCommaString(stringVector _vector) + { + std::string string; + + std::sort(_vector.begin(), _vector.end()); + + for(stringVector::const_iterator it = _vector.cbegin(); it != _vector.cend(); ++it) + string += (string.length() ? "," : "") + (*it); + + return string; + + } // vectorToCommaString + + std::string format(const char* _format, ...) + { + va_list args; + va_list copy; + + va_start(args, _format); + + va_copy(copy, args); + const int length = vsnprintf(nullptr, 0, _format, copy); + va_end(copy); + + char* buffer = new char[length + 1]; + va_copy(copy, args); + vsnprintf(buffer, length + 1, _format, copy); + va_end(copy); + + va_end(args); + + std::string out(buffer); + delete buffer; + + return out; + + } // format + + // Simple XOR scrambling of a string, with an accompanying key + std::string scramble(const std::string& _input, const std::string& key) + { + std::string buffer = _input; + + for (size_t i = 0; i < _input.size(); ++i) + { + buffer[i] = _input[i] ^ key[i]; + } + + return buffer; + + } // scramble + + std::vector split(const std::string& s, char seperator) + { + std::vector output; + + std::string::size_type prev_pos = 0, pos = 0; + while ((pos = s.find(seperator, pos)) != std::string::npos) + { + std::string substring(s.substr(prev_pos, pos - prev_pos)); + + output.push_back(substring); + + prev_pos = ++pos; + } + + output.push_back(s.substr(prev_pos, pos - prev_pos)); // Last word + + return output; + } + + + +#if defined(_WIN32) + const std::string convertFromWideString(const std::wstring wstring) + { + int numBytes = WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), nullptr, 0, nullptr, nullptr); + + std::string string; + string.resize(numBytes); + WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), (char*)string.c_str(), numBytes, nullptr, nullptr); + + return string; + } + + const std::wstring convertToWideString(const std::string string) + { + int numBytes = MultiByteToWideChar(CP_UTF8, 0, string.c_str(), (int)string.length(), nullptr, 0); + + std::wstring wstring; + wstring.resize(numBytes); + MultiByteToWideChar(CP_UTF8, 0, string.c_str(), (int)string.length(), (WCHAR*)wstring.c_str(), numBytes); + + return wstring; + } +#endif + + std::vector splitAny(const std::string& s, const std::string& seperator) + { + std::vector output; + + char* str = new char[s.length() + 1]; + std::strcpy(str, s.c_str()); + + char* pch = strtok(str, seperator.c_str()); + while (pch != NULL) + { + output.push_back(pch); + pch = strtok(NULL, seperator.c_str()); + } + + delete str; + + return output; + } + } // String:: + +} // Utils:: diff --git a/es-core/src/utils/StringUtil.h b/es-core/src/utils/StringUtil.h index 39fd9559dc..79159b68ba 100644 --- a/es-core/src/utils/StringUtil.h +++ b/es-core/src/utils/StringUtil.h @@ -27,7 +27,13 @@ namespace Utils std::string vectorToCommaString(stringVector _vector); std::string format (const char* _string, ...); std::string scramble (const std::string& _input, const std::string& key); + std::vector split (const std::string& s, char seperator); + std::vector splitAny(const std::string& s, const std::string& seperator); +#if defined(_WIN32) + const std::string convertFromWideString(const std::wstring wstring); + const std::wstring convertToWideString(const std::string string); +#endif } // String:: } // Utils:: diff --git a/es-core/src/utils/ThreadPool.cpp b/es-core/src/utils/ThreadPool.cpp new file mode 100644 index 0000000000..bc1f35e054 --- /dev/null +++ b/es-core/src/utils/ThreadPool.cpp @@ -0,0 +1,93 @@ +#include "ThreadPool.h" + +#if WIN32 +#include +#endif + +namespace Utils +{ + ThreadPool::ThreadPool() : mRunning(true), mWaiting(false), mNumWork(0) + { + size_t num_threads = std::thread::hardware_concurrency() * 2; + + auto doWork = [&](size_t id) + { +#if WIN32 + auto mask = (static_cast(1) << id); + SetThreadAffinityMask(GetCurrentThread(), mask); +#endif + + while (mRunning) + { + _mutex.lock(); + if (!mWorkQueue.empty()) + { + auto work = mWorkQueue.front(); + mWorkQueue.pop(); + _mutex.unlock(); + + try + { + work(); + } + catch (...) {} + + mNumWork--; + } + else + { + _mutex.unlock(); + + // Extra code : Exit finished threads + if (mWaiting) + return; + + std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + } + }; + + mThreads.reserve(num_threads); + + for (size_t i = 0; i < num_threads; i++) + mThreads.push_back(std::thread(doWork, i)); + } + + ThreadPool::~ThreadPool() + { + mRunning = false; + + for (std::thread& t : mThreads) + if (t.joinable()) + t.join(); + } + + void ThreadPool::queueWorkItem(work_function work) + { + _mutex.lock(); + mWorkQueue.push(work); + mNumWork++; + _mutex.unlock(); + } + + void ThreadPool::wait() + { + mWaiting = true; + while (mNumWork.load() > 0) + std::this_thread::yield(); + } + + void ThreadPool::wait(work_function work, int delay) + { + mWaiting = true; + + while (mNumWork.load() > 0) + { + work(); + + std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(delay)); + } + } +} \ No newline at end of file diff --git a/es-core/src/utils/ThreadPool.h b/es-core/src/utils/ThreadPool.h new file mode 100644 index 0000000000..34eb844696 --- /dev/null +++ b/es-core/src/utils/ThreadPool.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace Utils +{ + class ThreadPool + { + public: + typedef std::function work_function; + + ThreadPool(); + ~ThreadPool(); + + void queueWorkItem(work_function work); + void wait(); + void wait(work_function work, int delay = 50); + + private: + bool mRunning; + bool mWaiting; + std::queue mWorkQueue; + std::atomic mNumWork; + std::mutex _mutex; + std::vector mThreads; + + }; +} \ No newline at end of file diff --git a/resources/DroidSansFallbackFull.ttf b/resources/DroidSansFallbackFull.ttf new file mode 100644 index 0000000000..89959f52fd Binary files /dev/null and b/resources/DroidSansFallbackFull.ttf differ diff --git a/resources/NanumMyeongjo.ttf b/resources/NanumMyeongjo.ttf new file mode 100644 index 0000000000..5d753f0aa4 Binary files /dev/null and b/resources/NanumMyeongjo.ttf differ diff --git a/resources/OpenSans-CondBold.ttf b/resources/OpenSans-CondBold.ttf new file mode 100644 index 0000000000..83966f2138 Binary files /dev/null and b/resources/OpenSans-CondBold.ttf differ diff --git a/resources/OpenSans-CondLight.ttf b/resources/OpenSans-CondLight.ttf new file mode 100644 index 0000000000..97c355b9f6 Binary files /dev/null and b/resources/OpenSans-CondLight.ttf differ diff --git a/resources/alert.svg b/resources/alert.svg new file mode 100644 index 0000000000..dd5fb44755 --- /dev/null +++ b/resources/alert.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/arrow.svg b/resources/arrow.svg index 505356adee..e0a9f008da 100644 --- a/resources/arrow.svg +++ b/resources/arrow.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/resources/button.png b/resources/button.png index 7ac2297f93..69a88cbaf5 100644 Binary files a/resources/button.png and b/resources/button.png differ diff --git a/resources/button_filled.png b/resources/button_filled.png index ac543584f5..01588f7c91 100644 Binary files a/resources/button_filled.png and b/resources/button_filled.png differ diff --git a/resources/checkbox_checked.svg b/resources/checkbox_checked.svg index 5f19b53021..fd39d10d67 100644 --- a/resources/checkbox_checked.svg +++ b/resources/checkbox_checked.svg @@ -1,6 +1,61 @@ - - - - - + + + + + + image/svg+xml + + + + + + + + + diff --git a/resources/checkbox_unchecked.svg b/resources/checkbox_unchecked.svg index 5808eb13df..110977c6a9 100644 --- a/resources/checkbox_unchecked.svg +++ b/resources/checkbox_unchecked.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/resources/fav_add.svg b/resources/fav_add.svg index 34402f462b..ca9d965f9c 100644 --- a/resources/fav_add.svg +++ b/resources/fav_add.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/resources/fav_remove.svg b/resources/fav_remove.svg index bb5f3f2a72..7d78461644 100644 --- a/resources/fav_remove.svg +++ b/resources/fav_remove.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/resources/glyphs.ttf b/resources/glyphs.ttf new file mode 100644 index 0000000000..ed9372f8ea Binary files /dev/null and b/resources/glyphs.ttf differ diff --git a/resources/info.svg b/resources/info.svg new file mode 100644 index 0000000000..8b858e4f1e --- /dev/null +++ b/resources/info.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/locale/br/emulationstation2.po b/resources/locale/br/emulationstation2.po new file mode 100644 index 0000000000..fff73047d1 --- /dev/null +++ b/resources/locale/br/emulationstation2.po @@ -0,0 +1,2155 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: recalbox-emulationstation\n" +"Language: BR Traduzido por JTulho_LTDA\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: +msgid "extracting update..." +msgstr "Extraindo Atualizaรงรตes..." + +#: +msgid "Loading..." +msgstr "Carregando..." + +#: +msgid "Preloading UI" +msgstr "Iniciando..." + +#: +msgid "GRID SIZE" +msgstr "TAMANHO DA GRADE" + +#: +msgid "DEFAULT GRID SIZE" +msgstr "TAMANHO DE GRADE PADRรƒO" + +#: +msgid "RESET GAMELIST CUSTOMISATIONS" +msgstr "RESETAR CUSTOMIZAร‡ร•ES DO GAMELIST" + +#: +msgid "UI MODE" +msgstr "MODO DE INTERFACE" + +#: +msgid "PRELOAD UI" +msgstr "Prรฉ-carga UI" + +#: +msgid "VSYNC" +msgstr "SINCRONIZAร‡รƒO VERTICAL" + +#: +msgid "DISPLAY FAVORITES FIRST IN GAMELIST" +msgstr "EXIBIR FAVORITOS PRIMEIRO EM GAMELIST" + +#: +msgid "OPTIMIZE IMAGES VRAM USE" +msgstr "OTIMIZE O USO DO VRMAM COM IMAGENS" + +#: +msgid "Full" +msgstr "Completo" + +#: +msgid "Kiosk" +msgstr "Quiosque" + +#: +msgid "Kid" +msgstr "Crianรงa" + +#: +msgstr "GAME COLLECTION SETTINGS" +msgstr "LISTAS PERSONALIZADAS" + +#: +msgstr "CANCEL" +msgstr "CANCELAR" + +#: +msgid "automatic" +msgstr "automรกtico" + +#: +msgid "basic" +msgstr "bรกsico" + +#: +msgid "detailed" +msgstr "detalhado" + +#: +msgid "grid" +msgstr "grade" + +#: +msgid "fade" +msgstr "desaparecer" + +#: +msgid "instant" +msgstr "instantรขneo" + +#: +msgid "slide" +msgstr "animado" + +#: +msgid "gridex" +msgstr "grade detalhada" + +#: +msgid "ENABLE NAVIGATION SOUNDS" +msgstr "ATIVAR SOM DE NAVEGAร‡รƒO" + +#: +msgid "ENABLE VIDEO AUDIO" +msgstr "ATIVAR AUDIO DE VIDEO" + +#: +msgid "APPLY FILTER" +msgstr "APLICAR FILTRO" + +#: +msgid "AUTOMATIC GAME COLLECTIONS" +msgstr "COLEร‡ร•ES DE JOGO AUTOMรTICAS" + +#: +msgid "SELECT COLLECTIONS" +msgstr "SELECIONAR COLEร‡ร•ES" + +#: +msgid "CUSTOM GAME COLLECTIONS" +msgstr "COLEร‡ร•ES PERSONALIZADAS" + +#: +msgid "CREATE NEW CUSTOM COLLECTION FROM THEME" +msgstr "CRIAR UMA COLEร‡รƒO DE SISTEMAS" + +#: +msgid "GAME COLLECTION SETTINGS" +msgstr "CONFIGURAR COLEร‡รƒO DE JOGOS" + +#: +msgid "SELECT THEME FOLDER" +msgstr "SELECIONAR PASTA" + +#: +msgid "CREATE NEW CUSTOM COLLECTION" +msgstr "CRIAR NOVA COLEร‡รƒO PERSONALIZADA" + +#: +msgid "New Collection Name" +msgstr "novo nome da coleรงรฃo" + +#: +msgid "Loading..." +msgstr "Carregando..." + +#: +msgid "GROUP UNTHEMED CUSTOM COLLECTIONS" +msgstr "COLEร‡ร•ES DE GRUPO SEM SISTEMA" + +#: +msgid "SORT CUSTOM COLLECTIONS AND SYSTEMS" +msgstr "ORGANIZAร‡รƒO DE COLEร‡ร•ES E SISTEMAS" + +#: +msgid "SHOW SYSTEM NAME IN COLLECTIONS" +msgstr "EXIBIR NOME DO SISTEMA NAS COLEร‡ร•ES" + +#: +msgid "FINISH EDITING COLLECTION" +msgstr "FINALIZAR COLEร‡รƒO" + +#: +msgid "PARSE GAMESLISTS ONLY" +msgstr "APENAS LISTA DE JOGOS XML" + +#: +msgid "EMULATOR SETTINGS" +msgstr "CONFIGURAร‡ร•ES DO EMULADOR" + +#: +msgid "POWER SAVER MODES" +msgstr "MODO DE POUPANร‡A DE ENERGIA" + +#: +msgid "SAVE METADATA ON EXIT" +msgstr "SALVAR METADATA NA SAรDA" + +#: +msgid "SHOW HIDDEN FILES" +msgstr "MOSTRAR ARQUIVOS OCULTOS" + +#: +msgid "VRAM LIMIT" +msgstr "LIMITE DE VRAM" + +#: +msgid "RESET ALL FILTERS" +msgstr "REINICIAR TODOS OS FILTROS" + +#: +msgid "GAMELIST VIEW STYLE" +msgstr "MODO DE VISUALIZAร‡รƒO, LISTA DE JOGO" + +#: +msgid "GAME LAUNCH TRANSITION" +msgstr "TRANSIร‡รƒO PARA LANร‡AR O JOGO" + +#: +msgid "RANDOM" +msgstr "ALEATร“RIO" + +#: +msgid "Rating" +msgstr "Avaliaรงรฃo" + +#: +msgid "Released" +msgstr "Publicado" + +#: +msgid "Developer" +msgstr "Desenvolvido" + +#: +msgid "Publisher" +msgstr "Editor" + +#: +msgid "Genre" +msgstr "Gรฉnero" + +#: +msgid "Players" +msgstr "Jogadores" + +#: +msgid "NO GAMES FOUND - SKIP" +msgstr "NENHUM JOGO ENCONTRADO - PULAR" + +#: +msgid "RETRY" +msgstr "REPETIR" + +#: +msgid "SKIP" +msgstr "PULAR" + +#: +msgid "SEARCH FOR" +msgstr "BUSCAR POR:" + +#: +msgid "SEARCH" +msgstr "BUSCAR" + +#: +msgid "SCRAPING IN PROGRESS" +msgstr "SCRAPING EM PROGRESSO" + +#: +msgid "SYSTEM" +msgstr "SISTEMA" + +#: +msgid "subtitle text" +msgstr "texto de legenda" + +#: +msgid "INPUT" +msgstr "ENTRADA" + +#: +msgid "search" +msgstr "BUSCAR" + +#: +msgid "STOP" +msgstr "PARAR" + +#: +msgid "stop (progress saved)" +msgstr "parar (progresso salvo)" + +#: +msgid "GAME %i OF %i" +msgstr "JOGO %i DE %i" + +#: +msgid "WE CAN'T FIND ANY SYSTEMS!\n" +"CHECK THAT YOUR PATHS ARE CORRECT IN THE SYSTEMS CONFIGURATION FILE, AND YOUR GAME DIRECTORY HAS AT LEAST ONE GAME WITH THE CORRECT EXTENSION.\n" +"\n" +"" +msgstr "ยกNรƒO PODEMOS ENCONTRAR SISTEMAS! \ N " +"" VERIFIQUE SE SUAS ESTRADAS ESTรƒO CORRETAS NO ARQUIVO DE CONFIGURAร‡รƒO DO SISTEMA E SEU DIRETร“RIO DE JOGOS TEM PELO MENOS UM JOGO COM A EXTENSรƒO CORRETA.\n" +"\n" +"" + +#: +msgid "%i i ยกSCRAPER DE JOGO COMPLETO!" +msgid_plural "%i ยกSCRAPERS DE JOGOS COMPLETOS!" +msgstr[0] "%i ยกSCRAPER DO JOGO!" +msgstr[1] "%i ยกSCRAPERS DOS JOGOS!" + +#: +msgid "%i i ยกJOGO PULADO!" +msgid_plural "%i JOGOS PULADOS." +msgstr[0] "%i ยกJOGO SALTADO!" +msgstr[1] "%i ยกJOGOS SALTADOS!" + +#: +msgid "OK" +msgstr "OK" + +#: +msgid "EDIT METADATA" +msgstr "EDITAR METADATA" + +#: +msgid "SCRAPE" +msgstr "SCRAPER DE MEDIA" + +#: +msgid "SAVE" +msgstr "SALVAR" + +#: +msgid "THIS WILL DELETE A FILE!\n" +"ARE YOU SURE?" +msgstr "ยกISTO APAGARร UM ARQUIVO! \ n " +"TEM CERTEZA?" + +#: +msgid "YES" +msgstr "SIM" + +#: +msgid "NO" +msgstr "NรƒO" + +#: +msgid "DELETE" +msgstr "APAGAR" + +#: +msgid "SAVE CHANGES?" +msgstr "ยฟSALVAR MUDANร‡AS?" + +#: +msgid "BACK" +msgstr "VOLTAR" + +#: +msgid "CLOSE" +msgstr "FECHAR" + +#: +msgid "MAIN MENU" +msgstr "MENU PRINCIPAL" + +#: +msgid "KODI MEDIA CENTER" +msgstr "KODI MEDIA CENTER" + +#: +msgid "SYSTEM SETTINGS" +msgstr "OPร‡ร•ES DE SISTEMA" + +#: +msgid "VERSION" +msgstr "VERSรƒO" + +#: +msgid "DISK USAGE" +msgstr "ESPAร‡O EM DISCO" + +#: +msgid "STORAGE DEVICE" +msgstr "DISPOSITIVO DE ARMAZENAMENTO" + +#: +msgid "LANGUAGE" +msgstr "IDIOMA" + +#: +msgid "OVERCLOCK" +msgstr "OVERCLOCK" +#: +msgid "EXTREM (1100Mhz)" +msgstr "EXTREMO (1100Mhz)" + +#: +msgid "TURBO (1000Mhz)" +msgstr "TURBO (1000Mhz)" + +#: +msgid "HIGH (950Mhz)" +msgstr "ALTO (950Mhz)" + +#: +msgid "NONE (700Mhz)" +msgstr "NENHUM (900Mhz)" + +#: +msgid "TURBO (1050Mhz)+" +msgstr "TURBO (1050Mhz)+" + +#: +msgid "HIGH (1050Mhz)" +msgstr "ALTO (1050Mhz)" + +#: +msgid "NONE (900Mhz)" +msgstr "NENHUM (900Mhz)" + +#: +msgid "NONE (1200Mhz)" +msgstr "NENHUM (1200Mhz)" + +#: +msgid "NONE" +msgstr "NENHUM" + +#. NEW SETTINGS ORGANIZATION +#: +msgid "UPDATES" +msgstr "ATUALIZAร‡ร•ES" + +#: +msgid "THEME INSTALLER" +msgstr "INSTALADOR DE TEMAS" + +#: +msgid "SELECT THEME TO INSTALL" +msgstr "SELECIONE O TEMA PARA INSTALAR" + +#: +msgid "AUTO UPDATES" +msgstr "ATUALIZAร‡ร•ES AUTOMรTICAS" + +#: +msgid "START UPDATE" +msgstr "INICIAR ATUALIZAร‡ร•ES" + +#: +msgid "KODI SETTINGS" +msgstr "OPร‡ร•ES DO KODI" + +#: +msgid "ENABLE KODI" +msgstr "HABILITAR KODI" + +#: +msgid "KODI AT START" +msgstr "AUTO INICIAR KODI" + +#: +msgid "START KODI WITH X" +msgstr "INICIAR KODI COM X" + +#: +msgid "SECURITY" +msgstr "SEGURANร‡A" + +#: +msgid "ENFORCE SECURITY" +msgstr "FORTALECER A SEGURANร‡A" + +#: +msgid "ROOT PASSWORD" +msgstr "SENHA ROOT" + +#: +msgid "THE SYSTEM WILL NOW REBOOT" +msgstr "O SISTEMA REINICIARร AGORA" + +#: +msgid "GAMES SETTINGS" +msgstr "OPร‡ร•ES DE JOGOS" + +#: +msgid "GAME RATIO" +msgstr "FORMATO DE JOGO" + +#: +msgid "SMOOTH GAMES" +msgstr "JOGOS SUAVES" + +#: +msgid "REWIND" +msgstr "REBOBINAR" + +#: +msgid "AUTO SAVE/LOAD" +msgstr "AUTO SALVAR/CARREGAR" + +#: +msgid "SHADERS SET" +msgstr "CONJUNTO DE SOMBRAS" + +#: +msgid "SCANLINES" +msgstr "SCANLINES" + +#: +msgid "RETRO" +msgstr "RETRO" + +#: +msgid "RETROACHIEVEMENTS SETTINGS" +msgstr "AJUSTAR RECOMPENSAS" + +#: +msgid "RETROACHIEVEMENTS" +msgstr "RECOMPENSAS" + +#: +msgid "HARDCORE MODE" +msgstr "MODO HARDCORE" + +#: +msgid "USERNAME" +msgstr "NOME DE USUARIO" + +#: +msgid "PASSWORD" +msgstr "SENHA" + +#: +msgid "ADVANCED" +msgstr "AVANร‡ADO" + +#: +msgid "REALLY UPDATE GAMES LISTS ?" +msgstr "ยฟREALMENTE QUER ATUALIZAR A LISTA DE JOGOS?" + +#: +msgid "UPDATE GAMES LISTS" +msgstr "ATUALIZAR LISTA DE JOGOS" + +#: +msgid "CONTROLLERS SETTINGS" +msgstr "CONFIGURAR CONTROLES" + +#: +msgid "UI SETTINGS" +msgstr "CONFIGURAR INTERFACE" + +#: +msgid "OVERSCAN" +msgstr "OVERSCAN" + +#: +msgid "LAUNCH SCREENSAVER" +msgstr "INICIAR SCREENSAVER" + +#: +msgid "SCREENSAVER SETTINGS" +msgstr "AJUSTAR SCREENSAVER" + +#: +msgid "SCREENSAVER AFTER" +msgstr "SCREENSAVER DEPOIS DE:" + +#: +msgid "TRANSITION STYLE" +msgstr "ESTILO DE TRANSIร‡รƒO" + +#: +msgid "SCREENSAVER BEHAVIOR" +msgstr "COMPORTAMENTO DE TELA" + +#: +msgid "dim" +msgstr "escura" + +#: +msgid "black" +msgstr "preta" + +#: +msgid "random video" +msgstr "vรญdeo aleatรณrio" + +#: +msgid "slideshow" +msgstr "apresentaรงรฃo de slides" + +#: +msgid "VIDEO SCREENSAVER SETTINGS" +msgstr "CONFIGURAร‡ร•ES DO PROTETOR DE TELA" + +#: +msgid "VIDEO SCREENSAVER" +msgstr "PROTETOR DE TELA EM VรDEO" + +#: +msgid "SWAP VIDEO AFTER (SECS)" +msgstr "TROQUE O VรDEO DEPOIS DE (Segundos)" + +#: +msgid "SHOW GAME INFO ON SCREENSAVER" +msgstr "MOSTRAR INFORMAร‡ร•ES DO JOGO" + +#: +msgid "SHOW GAME INFO" +msgstr "MOSTRAR INFORMAร‡ร•ES DO JOGO" + +#: +msgid "USE MARQUEE AS GAME INFO" +msgstr "UTILIZAR MARQUEE COMO INFORMAร‡ร•ES DO JOGO" + +#: +msgid "STRETCH VIDEO ON SCREENSAVER" +msgstr "ESTICAR VรDEO DO PROTETOR DE TELA" + +#: +msgid "SLIDESHOW SCREENSAVER SETTINGS" +msgstr "CONFIGURAร‡ร•ES DA APRESENTAร‡รƒO DE SLIDES DE TELA" + +#: +msgid "SLIDESHOW SCREENSAVER" +msgstr "APRESENTAร‡รƒO DE SLIDES DE TELA" + +#: +msgid "SWAP IMAGE AFTER (SECS)" +msgstr "TROQUE A IMAGEM APร“S DE (SEGUNDOS)" + +#: +msgid "STRETCH IMAGES" +msgstr "ESTICAR IMAGENS" + +#: +msgid "USE CUSTOM IMAGES" +msgstr "UTILIZAR IMAGENS PERSONALIZADAS" + +#: +msgid "CUSTOM IMAGE DIR" +msgstr "DIRETร“RIO DE IMAGEM PERSONALIZADO" + +#: +msgid "CUSTOM IMAGE DIR RECURSIVE" +msgstr "DIRETร“RIO RECURSIVO DE IMAGEM PERSONALIZADO" + +#: +msgid "CUSTOM IMAGE FILTER" +msgstr "FILTRO DE IMAGEM PERSONALIZADO" + +#: +msgid "SCREENSAVER CONTROLS" +msgstr "CONTROLAR PROTETOR DE TELA" + +#: +msgid "SHOW FRAMERATE" +msgstr "MOSTRAR TAXA DE QUADROS" + +#: +msgid "THREADED LOADING" +msgstr "LOADING EM LINHA" + +#: +msgid "COMPLETE QUIT MENU" +msgstr "MENU DE SAรDA COMPLETO" + +#: +msgid "LOG LEVEL" +msgstr "NรVEL DE LOG" + +#: +msgid "enabled" +msgstr "ativado" + +#: +msgid "enable" +msgstr "ativado" + +#: +msgid "disabled" +msgstr "desativado" + +#: +msgid "disable" +msgstr "desativado" + +#: +msgid "default" +msgstr "padrรฃo" + +#: +msgid "warning" +msgstr "atenรงรฃo" + +#: +msgid "error" +msgstr "erro" + +#: +msgid "debug" +msgstr "depurar" + +#: +msgid "enhanced" +msgstr "melhorado" + +#: +msgid "ON-SCREEN HELP" +msgstr "AJUDA NA TELA" + +#: +msgid "HIDE WHEN RUNNING GAME" +msgstr "ESCONDER AO EXECUTAR O JOGO" + +#: +msgid "QUICK SYSTEM SELECT" +msgstr "SELEร‡รƒO RAPIDA DE SISTEMA" + +#: +msgid "THEME" +msgstr "SELEร‡รƒO DE TEMAS" + +#: +msgid "SOUND SETTINGS" +msgstr "CONFIGURAR AUDIO" + +#: +msgid "SYSTEM VOLUME" +msgstr "VOLUME DO SISTEMA" + +#: +msgid "FRONTEND MUSIC" +msgstr "MรšSICA DE FUNDO" + +#: +msgid "DISPLAY SONG TITLES" +msgstr "EXIBIR TรTULOS DE MรšSICAS" + +#: +msgid "ONLY PLAY SYSTEM-SPECIFIC MUSIC FOLDER" +msgstr "ATIVAR APENAS PASTA DE MรšSICA ESPECรFICA DO SISTEMA" + +#: +msgid "PLAY THEME MUSICS" +msgstr "ATIVAR MรšSICAS TEMรTICAS" + + +#: +msgid "OUTPUT DEVICE" +msgstr "DISPOSITIVO DE SAIDA" + +#: +msgid "HDMI" +msgstr "HDMI" + +#: +msgid "JACK" +msgstr "JACK" + +#: +msgid "AUTO" +msgstr "AUTO" + +#: +msgid "NETWORK SETTINGS" +msgstr "OPร‡ร•ES DE RED" + +#: +msgid "CONNECTED" +msgstr "CONECTADO" + +#: +msgid "NOT CONNECTED" +msgstr "NรƒO CONECTADO" + +#: +msgid "STATUS" +msgstr "ESTADO" + +#: +msgid "IP ADDRESS" +msgstr "ENDEREร‡O โ€‹โ€‹DE IP" + +#: +msgid "HOSTNAME" +msgstr "NOME DO HOST" + +#: +msgid "ENABLE WIFI" +msgstr "ATIVAR WIFI" + +#: +msgid "WIFI SSID" +msgstr "SSID WIFI" + +#: +msgid "WIFI KEY" +msgstr "SENHA DO WIFI" + +#: +msgid "WIFI ENABLED" +msgstr "WIFI HABILITADO" + +#: +msgid "WIFI CONFIGURATION ERROR" +msgstr "ERRO DE CONFIGURAร‡รƒO DO WIFI" + +#: +msgid "SCRAPER" +msgstr "SCRAPER DE JOGOS" + +#: +msgid "DOWNLOADS AND UPDATES" +msgstr "DOWNLOADS E ATUALIZAร‡ร•ES" + +#: +msgid "SCRAPE FROM" +msgstr "SERVIDOR SCRAPE" + +#: +msgid "IMAGE SOURCE" +msgstr "TIPO DE IMAGEM" + +#: +msgid "BOX SOURCE" +msgstr "TIPO DE BOX" + +#: +msgid "LOGO SOURCE" +msgstr "TIPO DE LOGO" + +#: +msgid "SCRAPE RATINGS" +msgstr "CLASSIFICAร‡ร•ES DE SCRAPER" + +#: +msgid "SCRAPE VIDEOS" +msgstr "VรDEOS SCRAPE" + +#: +msgid "SCRAPE NOW" +msgstr "SCRAPE AGORA"" + +#: +msgid "QUIT" +msgstr "SAIR" + +#: +msgid "REALLY QUIT?" +msgstr "QUER REALMENTE SAIR?" + +#: +msgid "REALLY RESTART?" +msgstr "QUER REALMENTE REINICIAR?" + +#: +msgid "REALLY SHUTDOWN?" +msgstr "QUER REALMENTE DESLIGAR?" + +#: +msgid "QUIT EMULATIONSTATION" +msgstr "SAIR DO RETRORAT" + +#: +msgid "RESTART SYSTEM" +msgstr "REINICIAR WINDOWS" + +#: +msgid "SHUTDOWN SYSTEM" +msgstr "DESLIGAR WINDOWS" + +#: +msgid "Emulator" +msgstr "Emulador" + +#: +msgid "Core" +msgstr "Nรบcleo" + +#: +msgid "YOU ARE GOING TO CONFIGURE A CONTROLLER. IF YOU HAVE ONLY ONE JOYSTICK, CONFIGURE THE DIRECTIONS KEYS AND SKIP JOYSTICK CONFIG BY HOLDING A BUTTON. IF YOU DO NOT HAVE A SPECIAL KEY FOR HOTKEY, CHOOSE THE SELECT BUTTON. SKIP ALL BUTTONS YOU DO NOT HAVE BY HOLDING A KEY. BUTTONS NAMES ARE BASED ON THE SNES CONTROLLER." +msgstr "CONFIGURAR UM CONTROLE, SE VOCรŠ TIVER Sร“ UM JOYSTICK OU OUTRO CONTROLADOR, CONFIGURE APENAS OS BOTร•ES QUE VOCรŠ TEM NO SEU CONTROLADOR OU CONTROLE. SE VOCรŠ NรƒO TIVER UMA CHAVE ESPECIAL PARA O HOTKEY, USE O SELECT E IGNORE OS BOTร•ES QUE NรƒO ESTรƒO NO SEU CONTROLE. VOCรŠ NรƒO PODE USAR UM BOTรƒO CONFIGURADO NOVAMENTE. O NOME DOS BOTร•ES SรƒO BASEADOS NO CONTROLE DE SUPER NINTENDO." + +#. GUIMENU +#: +msgid "CONFIGURE A CONTROLLER" +msgstr "CONFIGURAR UM CONTROLE" + +#. Bluetooth +#: +msgid "CONTROLLER PAIRED" +msgstr "CONTROLE PAREADO" + +#: +msgid "UNABLE TO PAIR CONTROLLER" +msgstr "NรƒO FOI POSSIVEL PAREAR ESSE CONTROLE" + +#: +msgid "AN ERROR OCCURED" +msgstr "OCORREU UM ERRO" + +#: +msgid "NO CONTROLLERS FOUND" +msgstr "CONTROLE NรƒO ENCONTRADO" + +#: +msgid "PAIR A BLUETOOTH CONTROLLER" +msgstr "PAREAR CONTROLE BLUETOOTH" + +#: +msgid "CONTROLLERS LINKS HAVE BEEN DELETED." +msgstr "CONTROLES PAREADOS APAGADOS." + +#: +msgid "FORGET BLUETOOTH CONTROLLERS" +msgstr "ESQUECER CONTROLES BLUETOOTH" + +#: +msgid "INPUT P%i" +msgstr "ENTRADA %i" + +#: +msgid "CHOOSE" +msgstr "ESCOLHER" + +#: +msgid "SELECT" +msgstr "SELECIONAR" + +#: +msgid "OPTIONS" +msgstr "OPร‡ร•ES" + +#: +msgid "JUMP TO LETTER" +msgstr "SALTAR PARA LETRA O NรšMERO" + +#: +msgid "SORT GAMES BY" +msgstr "ORDENAR JOGOS POR:" + +#. FAVORITES +#: +msgid "FAVORITES ONLY" +msgstr "SOMENTE FAVORITOS" + +#: +msgid "EDIT THIS GAME'S METADATA" +msgstr "EDITAR METADATOS PARA ESTE JOGO" + +#: +msgid "SCRAPE THESE GAMES" +msgstr "SCRAPER PARA ESTES JOGOS" + +#: +msgid "All Games" +msgstr "TODOS OS JOGOS" + +#. MISSING SCRAPPER TRANSLATIONS +#: +msgid "Only missing image" +msgstr "SOMENTE FALTANDO IMAGEM" + +#: +msgid "FILTER" +msgstr "FILTRO" + +#: +msgid "ONLY MISSING MEDIAS" +msgstr "SOMENTE MรDIAS PERDIDAS" + +#: +msgid "SCRAPE THESE SYSTEMS" +msgstr "SCRAPER PARA ESTES SISTEMAS" + +#: +msgid "SYSTEMS" +msgstr "SISTEMAS" + +#: +msgid "USER DECIDES ON CONFLICTS" +msgstr "ESCOLHER EM CONFLITOS" + +#: +msgid "START" +msgstr "INICIAR" + +#: +msgid "WARNING: SOME OF YOUR SELECTED SYSTEMS DO NOT HAVE A PLATFORM SET. RESULTS MAY BE EVEN MORE INACCURATE THAN USUAL!\n" +"CONTINUE ANYWAY?" +msgstr "AVISO: ALGUNS DOS SEUS SISTEMAS SELECIONADOS NรƒO TรŠM UM CONJUNTO DE PLATAFORMA. OS RESULTADOS PODEM SER AINDA MAIS IMPRECISOS QUE O NORMAL! \ N " +"CONTINUE DE QUALQUER MANEIRA?" + +#: +msgid "NO GAMES FIT THAT CRITERIA." +msgstr "NENHUM JOGO COM ESTES CRITร‰RIOS." + +#: +msgid "REALLY UPDATE?" +msgstr "ยฟATUALIZAR SISTEMA?" + +#: +msgid "NETWORK CONNECTION NEEDED" +msgstr "CONEXรƒO DE REDE NECESSรRIA" + +#: +msgid "UPDATE DOWNLOADED, THE SYSTEM WILL NOW REBOOT" +msgstr "ATUALIZAร‡รƒO COMPLETA, O SISTEMA REINICIARร AGORA" + +#: +msgid "UPDATE FAILED, THE SYSTEM WILL NOW REBOOT" +msgstr "ATUALIZAร‡รƒO FALHOU, O SISTEMA REINICIARร AGORA" + +#: +msgid "NO UPDATE AVAILABLE" +msgstr "NENHUMA ATUALIZAร‡รƒO DISPONรVEL" + +#: +msgid "enter emulator" +msgstr "Entrar no emulador" + +#: +msgid "enter core" +msgstr "Entrar no nรบcleo" + +#: +msgid "Ratio" +msgstr "Ratio" + +#: +msgid "enter ratio" +msgstr "Entrar ratio" + +#: +msgid "Name" +msgstr "Nome" + +#: +msgid "enter game name" +msgstr "digite o nome do jogo" + +#: +msgid "Description" +msgstr "Descriรงรฃo" + +#: +msgid "enter description" +msgstr "insira a descriรงรฃo" + +#: +msgid "Image" +msgstr "Imagem" + +#: +msgid "enter path to image" +msgstr "insira o caminho para a imagem" + +#: +msgid "Thumbnail" +msgstr "Miniatura" + +#: +msgid "enter path to thumbnail" +msgstr "insira o caminho para a miniatura" + +#: +msgid "enter rating" +msgstr "insira a classificaรงรฃo" + +#: +msgid "Release date" +msgstr "Data de lanรงamento" + +#: +msgid "enter release date" +msgstr "insira a data de lanรงamento" + +#: +msgid "enter game developer" +msgstr "entrar em desenvolvedor de jogos" + +#: +msgid "enter game publisher" +msgstr "entrar na editora de jogos" + +#: +msgid "enter game genre" +msgstr "entrar no gรชnero de jogo" + +#: +msgid "enter number of players" +msgstr "insira o nรบmero de jogadores" + +#: +msgid "Favorite" +msgstr "Favorito" + +#: +msgid "aรฑadir a favorito" +msgstr "adicionar aos favoritos" + +#: +msgid "Region" +msgstr "Regiรฃo" + +#: +msgid "enter region" +msgstr "entrar na regiรฃo" + +#: +msgid "Romtype" +msgstr "Tipo de rom" + +#: +msgid "enter romtype" +msgstr "entrar em tipo de rom" + +#: +msgid "Hidden" +msgstr "Oculto" + +#: +msgid "HIDDEN" +msgstr "OCULTO" + +#: +msgid "NAME" +msgstr "NOME" + +#: +msgid "EMULATOR" +msgstr "EMULADOR" + +#: +msgid "FAVORITE" +msgstr "FAVORITO" + +#: +msgid "set hidden" +msgstr "conjunto oculto" + +#: +msgid "Play count" +msgstr "Contagem de jogos" + +#: +msgid "enter number of times played" +msgstr "insira o nรบmero de vezes que jogou" + +#: +msgid "Last played" +msgstr "รšltimo jogado" + +#: +msgid "enter last played date" +msgstr "insira a รบltima data reproduzida" + +#: +msgid "%i GAME AVAILABLE" +msgstr "%i ITEM DISPONรVEL" + +#: +msgid "%i GAMES AVAILABLE" +msgstr "%i ITENS DISPONรVEIS" + +#: +msgid "%i FAVORITES" +msgid_plural "%i FAVORITES" +msgstr[0] "%i FAVORITO" +msgstr[1] "%i FAVORITOS" + +#: +msgid "SCROLL" +msgstr "ROLAGEM" + +#: +msgid "LAUNCH" +msgstr "INICIAR" + +#: +msgid "Times played" +msgstr "Tempo jogado" + +#: +msgid "MENU" +msgstr "MENU" + +#: +msgid "FILENAME, ASCENDING" +msgstr "NOME DO ARQUIVO, ASCENDENTE" + +#: +msgid "FILENAME, DESCENDING" +msgstr "NOME DO ARQUIVO, DESCENDENTE" + +#: +msgid "RATING, ASCENDING" +msgstr "CLASSIFICAร‡รƒO, ASCENDENTE" + +#: +msgid "RATING, DESCENDING" +msgstr "CLASSIFICAร‡รƒO, DESCENDENTE" + +#: +msgid "TIMES PLAYED, ASCENDING" +msgstr "TEMPO JOGADO, ASCENDENTE" + +#: +msgid "TIMES PLAYED, DESCENDING" +msgstr "TEMPO JOGADO, DESCENDENTE" + +#: +msgid "LAST PLAYED, ASCENDING" +msgstr "รšLTIMO JOGO, ASCENDENTE" + +#: +msgid "LAST PLAYED, DESCENDING" +msgstr "รšLTIMO JOGO, DESCENDENTE" + +#: +msgid "WORKING..." +msgstr "TRABALHANDO..." + +#: +msgid "CHANGE" +msgstr "MUDAR" + +#: +msgid "start & end" +msgstr "inรญcio e fim" + +#: +msgid "always" +msgstr "sempre" + +#: +msgid "never" +msgstr "nunca" + +#: +msgid "just now" +msgstr "agora" + +#: +msgid "%i sec ago" +msgid_plural "%i secs ago" +msgstr[0] "hace %i s" +msgstr[1] "hace %i s" + +#: +msgid "%i min ago" +msgid_plural "%i mins ago" +msgstr[0] "hace %i min" +msgstr[1] "hace %i min" + +#: +msgid "%i hour ago" +msgid_plural "%i hours ago" +msgstr[0] "hace %i h" +msgstr[1] "hace %i h" + +#: +msgid "%i day ago" +msgid_plural "%i days ago" +msgstr[0] "hace %i dรญa" +msgstr[1] "hace %i dรญas" + +#: +msgid "unknown" +msgstr "desconhecido" + +#: +msgid "SELECT ALL" +msgstr "SELECIONAR TUDO" + +#: +msgid "SELECT NONE" +msgstr "NENHUM SELECIONADO" + +#: +msgid "%i SELECTED" +msgid_plural "%i SELECTED" +msgstr[0] "%i SELECIONADO" +msgstr[1] "%i SELECIONADOS" + +#: +msgid "UP" +msgstr "CIMA" + +#: +msgid "DOWN" +msgstr "BAIXO" + +#: +msgid "LEFT" +msgstr "ESQUERDA" + +#: +msgid "RIGHT" +msgstr "DIREITA" + +#: +msgid "JOYSTICK 1 UP" +msgstr "JOYSTICK 1 CIMA" + +#: +msgid "JOYSTICK 1 LEFT" +msgstr "JOYSTICK 1 ESQUERDA" + +#: +msgid "JOYSTICK 2 UP" +msgstr "JOYSTICK 2 CIMA" + +#: +msgid "JOYSTICK 2 LEFT" +msgstr "JOYSTICK 2 ESQUERDA" + +#: +msgid "PAGE UP" +msgstr "L1 (PรGINA SEGUINTE)" + +#: +msgid "PAGE DOWN" +msgstr "R1 (PรGINA ANTERIOR)" + +#: +msgid "HOTKEY" +msgstr "HOTKEY" + +#: +msgid "CONFIGURING" +msgstr "CONFIGURAร‡รƒO" + +#: +msgid "KEYBOARD" +msgstr "TECLADO" + +#: +msgid "GAMEPAD %i" +msgstr "JOYSTICK %i" + +#. Config controllers missing translation +#: +msgid "PRESS ANYTHING" +msgstr "PRESSIONE QUALQUER BOTรƒO" + +#: +msgid "ALREADY TAKEN" +msgstr "Jร ESTร EM USO" + +#: +msgid "DISCARD CHANGES" +msgstr "DESCARTAR MUDANร‡AS" + +#: +msgid "WELCOME" +msgstr "BEM VINDO" + +#: +msgid "CONFIGURE INPUT" +msgstr "CONFIGURAR CONTROLES" + +#: +msgid "%i GAMEPAD DETECTED" +msgid_plural "%i GAMEPADS DETECTED" +msgstr[0] "%i CONTROLE DETECTADO" +msgstr[1] "%i CONTROLES DETECTADOS" + +#: +msgid "NO GAMEPADS DETECTED" +msgstr "NENHUM CONTROLE DETECTADO" + +#: +msgid "HOLD A BUTTON ON YOUR DEVICE TO CONFIGURE IT." +msgstr "SEGURE UM BOTรƒO NO SEU DISPOSITIVO PARA CONFIGURAR." + +#: +msgid "PRESS F4 TO QUIT AT ANY TIME." +msgstr "PRESSIONE F4 PARA SAIR A QUALQUER MOMENTO." + +#: +msgid "PRESS ESC OR THE HOTKEY TO CANCEL." +msgstr "PRESSIONE ESC OU O HOTKEY PARA CANCELAR." + +#: +msgid "DO YOU WANT TO START KODI MEDIA CENTER ?" +msgstr "QUER INICIAR O KODI MEDIA CENTER ?" + +#: +msgid "LOADING..." +msgstr "CARREGANDO ..." + +#: +msgid "PLEASE WAIT..." +msgstr "POR FAVOR ESPERE ..." + +#: +msgid "REALLY SHUTDOWN WITHOUT SAVING METADATAS?" +msgstr "ยฟ VOCรŠ REALMENTE QUER DESLIGAR SEM SALVAR MATADATA ?" + +#: +msgid "FAST SHUTDOWN SYSTEM" +msgstr "DESLIGAMENTE RรPIDO DO SISTEMA" + +#: +msgid "WE CAN'T FIND ANY SYSTEMS!\n" +"CHECK THAT YOUR PATHS ARE CORRECT IN THE SYSTEMS CONFIGURATION FILE, AND YOUR GAME DIRECTORY HAS AT LEAST ONE GAME WITH THE CORRECT EXTENSION.\n" +"\n" +"VISIT EMULATIONSTATION.ORG FOR MORE INFORMATION." +msgstr "ยกNENHUM SISTEMA ENCONTRADO!\n" +"VERIFIQUE SE OS SEUS CAMINHOS ESTรƒO CORRETOS NO ARQUIVO DE CONFIGURAร‡รƒO DOS SISTEMAS E SEU DIRETร“RIO DE JOGOS TEM PELO MENOS UM JOGO COM A EXTENSรƒO CORRETA.\n" +"SE INSCREVE NO MEU CANAL DO YOUTUBE PARA FORTALECER ( JTulho*LTDA )." + +#: +msgid "ON SCREEN KEYBOARD" +msgstr "TECLADO NA TELA" + +#: +msgid "SHIFTS FOR UPPER,LOWER, AND SPECIAL" +msgstr "PRESSIONE SHIFT PARA MAIรšSCULAS" + +#: +msgid "SPACE" +msgstr "ESPAร‡O" + +#: +msgid "DELETE A CHAR" +msgstr "APAGAR UM CARACTER" + +#: +msgid "SHIFT" +msgstr "MAIรšSCULAS" + +#: +msgid "STOP EDITING" +msgstr "PARE A EDIร‡รƒO" + +#: +msgid "MOVE CURSOR" +msgstr "MOVER O CURSOR" + +#: +msgid "EDIT" +msgstr "EDITAR" + +#: +msgid "ACCEPT RESULT" +msgstr "ACEITAR RESULTADO" + +#: +msgid "FILENAME" +msgstr "NOME DO ARQUIVO" + +#: +msgid "RATING" +msgstr "AVALIAร‡รƒO" + +#: +msgid "TIEMPO JUGADO" +msgstr "TEMPO JOGADO" + +#: +msgid "LAST PLAYED" +msgstr "รšLTIMO JOGADO" + +#: +msgid "NUMBER OF PLAYERS" +msgstr "NรšMERO DE JOGADORES" + +#: +msgid "DEVELOPER" +msgstr "DESENVOLVEDORA" + +#: +msgid "GENRE" +msgstr "Gร‰NERO" + +#: +msgid "SHOW HIDDEN" +msgstr "MOSTRAR OCULTADO" + +#: +msgid "EXTREM (1400Mhz)" +msgstr "EXTREMO (1400MHz)" + +#: +msgid "TURBO (1350Mhz)" +msgstr "TURBO (1350MHz)" + +#: +msgid "HIGH (1300Mhz)" +msgstr "ALTO (1300MHz)" + +#: +msgid "TURBO AND EXTREM OVERCLOCK PRESETS MAY CAUSE SYSTEM UNSTABILITIES, SO USE THEM AT YOUR OWN RISK.\n" +"IF YOU CONTINUE, THE SYSTEM WILL REBOOT NOW." +msgstr "TURBO E EXTREMO, DEFINIร‡รƒO DE OVERCLOCK PODEM PROVOCAR INCERTEZAS DO SISTEMA, PORTANTO UTILIZE-OS POR SUA CONTA E RISCO. \ N " +"SE VOCรŠ CONTINUAR, O SISTEMA REINICIARร AGORA." + +#: +msgid "%i GAME HIDDEN" +msgid_plural "%i GAMES HIDDEN" +msgstr[0] "%i JOGO OCULTO" +msgstr[1] "%i JOGOS OCULTOS" + +#: +msgid "Start kodi media player." +msgstr "Iniciar KODI" + +#: +msgid "Select the language for your recalbox, select an external drive to store your games and configurations, check your current version and the free space on your drive" +msgstr "Selecione o idioma para o seu recalbox, selecione uma unidade externa para armazenar seus jogos e configuraรงรตes, verifique sua versรฃo atual e o espaรงo livre em sua unidade" + +#: +msgid "Configure games display, ratio, filters (shaders), auto save and load and retroachievement account." +msgstr "Configurar exibiรงรฃo de jogos, taxa, filtros (shaders), auto salvar e carregar e conta retroachievement." + +#: +msgid "The game ratio is the ratio between image width and image height. Use AUTO to let the emulator choose the original game ratio, that will give you the best retrogaming experience." +msgstr "A razรฃo do jogo รฉ a razรฃo entre a largura da imagem e a altura da imagem. Use AUTO para permitir que o emulador escolha a taxa de jogo original, que lhe darรก a melhor experiรชncia de retrogaming." + +#: +msgid "Smooth the game image. This option makes the image smoother, using bilinear filtering." +msgstr "Suavilize a imagem do jogo. Esta opรงรฃo torna a imagem mais suave, usando filtragem bilinear." + +#: +msgid "This option allows you to rewind the game if you get killed by a monster, or if you make any other mistake. Use the HOTKEY + LEFT command within the game to rewind." +msgstr "Esta opรงรฃo permite-lhe rebobinar o jogo se for morto por um monstro ou se cometer outro erro. Use o comando HOTKEY + LEFT no jogo para retroceder." + +#: +msgid "Auto save the state when you quit a game, and auto load last saved state when you start a game." +msgstr "Auto salvar o estado quando vocรช sair de um jogo e carregar automaticamente o รบltimo estado salvo quando vocรช iniciar um jogo." + +#: +msgid "Integer scaling is scaling by a factor of a whole number, such as 2x, 3x, 4x, etc. This option scales the image up to the greatest integer scale below the set resolution. So for instance, if you set your fullscreen resolution to 1920x1080 and enable integer scaling, it will only scale a 320x240 image up to 1280x960, and leave black borders all around. This is to maintain a 1:1 pixel ratio with the original source image, so that pixels are not unevenly duplicated." +msgstr "A escala inteira รฉ dimensionada por um fator de um nรบmero inteiro, como 2x, 3x, 4x, etc. Essa opรงรฃo dimensiona a imagem atรฉ a maior escala inteira abaixo da resoluรงรฃo definida. Por exemplo, se vocรช definir a resoluรงรฃo de tela cheia como 1920x1080 e ativar o dimensionamento de nรบmeros inteiros, ela sรณ dimensionarรก uma imagem de 320x240 atรฉ 1280x960 e deixarรก as bordas pretas ao redor. Isso รฉ para manter uma proporรงรฃo de pixel 1: 1 com a imagem original, para que os pixels nรฃo sejam duplicados de forma desigual." + +#: +msgid "Shaders are like filters for the game rendering. You can select a shader set here, which is a collection of shaders selected for each system. You can also change the shader within the game with HOTKEY + L2 or HOTKEY + R2." +msgstr "Shaders sรฃo como filtros para a renderizaรงรฃo do jogo. Vocรช pode selecionar um shader set aqui, que รฉ uma coleรงรฃo de shaders selecionados para cada sistema. Vocรช tambรฉm pode alterar o shader dentro do jogo com HOTKEY + L2 ou HOTKEY + R2." + +#: +msgid "Enable or disable RetroAchievements in games." +msgstr "Habilitar ou desabilitar RetroAchievements em jogos." + +#: +msgid "Hardcore mode disables *all* savestate and rewind functions within the emulator: you will not be able to save and reload at any time. You will have to complete the game and get the achievements first time, just like on the original console. In reward for this, you will earn both the standard and the hardcore achievement, in effect earning double points! A regular game worth 400 points, is now worth 800 if you complete it on hardcore! For example: if you complete the game for 400 points, you then have the opportunity to earn another 400 on hardcore." +msgstr "O modo Hardcore desativa as funรงรตes * all * savestate e rewind dentro do emulador: vocรช nรฃo poderรก salvar e recarregar a qualquer momento. Vocรช terรก que completar o jogo e obter as conquistas pela primeira vez, assim como no console original. Em recompensa por isso, vocรช vai ganhar tanto a conquista padrรฃo quanto a hardcore, ganhando o dobro de pontos! Um jogo regular no valor de 400 pontos, agora vale 800 se vocรช o completar no hardcore! Por exemplo: se vocรช completar o jogo por 400 pontos, vocรช terรก a oportunidade de ganhar outros 400 no hardcore." + +#: +msgid "The website retroachievements.org proposes challenges/achievements/trophies on platforms like NES, SNES, GB, GBC, GBA, Genesis/Megadrive, TurboGrafx16/PCEngine and more! Create your account on retroachievements.org and start your quest for achievements!" +msgstr "O website retroachievements.org propรตe desafios / conquistas / trofรฉus em plataformas como NES, SNES, GB, GBC, GBA, Gรชnesis / Megadrive, TurboGrafx16 / PCEngine e mais! Crie sua conta no retroachievements.org e comece sua busca por conquistas!" + +#: +msgid "Add and configure up to 5 controllers." +msgstr "Adicione e configure atรฉ 5 controles." + +#: +msgid "Start the screensaver after N minutes." +msgstr "Inicie o protetor de tela apรณs N minutos." + +#: +msgid "Set the screensaver behavior. DIM will reduce the screen light, and BLACK will turn the screen black." +msgstr "Definir o comportamento do protetor de tela. O DIM reduzirรก a luz da tela e o preto tornarรก a tela preta." + +#: +msgid "Shows a help at the bottom of the screen which displays commands you can use." +msgstr "Mostra uma ajuda na parte inferior da tela que exibe os comandos que vocรช pode usar." + +#: +msgid "When enabled, you can switch between systems while browsing a gamelist by pressing LEFT or RIGHT." +msgstr "Quando ativado, vocรช pode alternar entre sistemas enquanto navega em um gamelista pressionando ESQUERDA ou DIREITA." + +#: +msgid "Updates the gamelists, if you added games since the last boot." +msgstr "Atualiza os gamelistas, se vocรช adicionou jogos desde a รบltima inicializaรงรฃo." + +#: +msgid "Set the volume of the sound output for the frontend and the games." +msgstr "Defina o volume da saรญda de som para o frontend e os jogos." + +#: +msgid "Get informations and visual for your games. The scraper downloads metadata and visuals for your games from different servers and enhances the user experience in EmulationStation completely." +msgstr "Obtenha informaรงรตes e visuais para seus jogos. O raspador faz o download de metadados e imagens para seus jogos de diferentes servidores e aprimora a experiรชncia do usuรกrio no EmulationStation completamente." + +#: +msgid "Select a server to scrape from. The SCREENSCRAPER server is recommended and is based on www.screenscraper.fr and scrapes game data in your language, if available." +msgstr "Selecione um servidor para raspar. O servidor SCREENSCRAPER รฉ recomendado e รฉ baseado em www.screenscraper.fr e raspa os dados do jogo em seu idioma, se disponรญvel." + +#: +msgid "Begin the scrape process with the configuration shown below." +msgstr "Comece o processo de raspar com a configuraรงรฃo mostrada abaixo." + +#: +msgid "Scrape and display game ratings." +msgstr "Raspe e exiba classificaรงรตes de jogos." + +#: +msgid "Advanced settings. Please make sure you really know what you're doing, before changing any values in this menu." +msgstr "Configuraรงรตes avanรงadas. Por favor, certifique-se de que vocรช realmente sabe o que estรก fazendo, antes de alterar qualquer valor neste menu." + +#: +msgid "Overclock your board to increase the performance.\n" +"Overclock settings are tested and validated by the community. Keep in mind that overclocking your board can void your warranty." +msgstr "Overclock sua placa para aumentar o desempenho.\n" +"As configuraรงรตes de overclock sรฃo testadas e validadas pela comunidade. Lembre-se de que o overclock da sua placa pode anular sua garantia." + +#: +msgid "Only show games contained in the gamelist.xml file (located in your roms directories).\n" +"This option highly speeds up boot time, but new games will not be detected." +msgstr "Apenas mostre jogos contidos no arquivo gamelist.xml (localizado em seus diretรณrios de roms).\n" +"Esta opรงรฃo acelera bastante o tempo de inicializaรงรฃo, mas novos jogos nรฃo serรฃo detectados." + +#: +msgid "This option allows you to set the selected system to fixed mode. With this option activated, the user cannot access other systems." +msgstr "Esta opรงรฃo permite que vocรช configure o sistema selecionado no modo fixo.\n" +"Com esta opรงรฃo ativada, o usuรกrio nรฃo pode acessar outros sistemas." + +#: +msgid "Always display the basic gamelist view, even if you have scraped your games." +msgstr "Sempre exiba a visรฃo bรกsica do gamelist, mesmo que vocรช tenha raspado seus jogos." + +#: +msgid "Enable or disable Kodi, customize the Kodi startup, enable the X button to start Kodi" +msgstr "Ativar ou desativar o Kodi, personalizar a inicializaรงรฃo do Kodi, ativar o botรฃo X para iniciar o Kodi." + +#: +msgid "Enable or disable Kodi. If kodi is disabled, you won't be able to start it with the X button, or start it automatically at boot. The menu entry will be removed as well." +msgstr "Ativar ou desativar o Kodi. Se o kodi estiver desativado, vocรช nรฃo poderรก iniciรก-lo com o botรฃo X ou iniciรก-lo automaticamente na inicializaรงรฃo. A entrada do menu tambรฉm serรก removida." + +#: +msgid "Use the X button to start Kodi." +msgstr "Use o botรฃo X para iniciar o Kodi." + +#: +msgid "Automatically start into Kodi on boot." +msgstr "Iniciar automaticamente no Kodi na inicializaรงรฃo." + +#: +msgid "Show the framerate in EmulationStation and in game." +msgstr "Mostrar o taxa de quadros no EmulationStation e no jogo." + +#: +msgid "Select which emulator to use when you start a game for this system." +msgstr "Selecione qual emulador usar quando vocรช iniciar um jogo para este sistema." + +#: +msgid "Select which core to use for the selected emulator. For example, the LIBRETRO emulator has many cores to run Super Nintendo games. The default core you choose here can also be overridden in game specific settings." +msgstr "Selecione qual nรบcleo usar para o emulador selecionado. Por exemplo, o emulador LIBRETRO possui muitos nรบcleos para executar os jogos do Super Nintendo. O nรบcleo padrรฃo que vocรช escolhe aqui tambรฉm pode ser substituรญdo nas configuraรงรตes especรญficas do jogo." + +#: +msgid "USE COMPOSED VISUALS" +msgstr "USE VISUAIS COMPOSTOS" + +#: +msgid "CHECK UPDATES" +msgstr "VERIFICAR ATUALIZAร‡ร•ES" + +#: +msgid "UPDATE TYPE" +msgstr "TIPO DE ACTUALIZAร‡รƒO" + +#: +msgid "INTEGER SCALE (PIXEL PERFECT)" +msgstr "TELA INTEIRA (PIXEL PERFEITO)" + +#: +msgid "ADVANCED SETTINGS" +msgstr "CONFIGURAร‡ร•ES AVANร‡ADAS" + +#: +msgid "BOOT SETTINGS" +msgstr "PARรMETROS DE INICIO" + +#: +msgid "GAMELIST ONLY" +msgstr "SOMENTE LISTA JOGO" + +#: +msgid "START ON SYSTEM" +msgstr "INICIAR EM SISTEMA" + +#: +msgid "BOOT ON GAMELIST" +msgstr "COMECE NA LISTA DE JOGOS" + +#: +msgid "HIDE SYSTEM VIEW" +msgstr "OCULTAR VISUALIZAร‡รƒO DO SISTEMA" + +#: +msgid "EMULATOR ADVANCED CONFIGURATION" +msgstr "CONFIGURAร‡รƒO AVANร‡ADA DO EMULADOR" + +#: +msgid "ADVANCED EMULATOR CONFIGURATION" +msgstr "CONFIGURAร‡รƒO AVANร‡ADA DO EMULADOR" + +#: +msgid "HELP" +msgstr "AJUDA" + +#: +msgid "THE SYSTEM IS UP TO DATE" +msgstr "O SISTEMA ESTร ATUALIZADO" + +#: +msgid "FORCE BASIC GAMELIST VIEW" +msgstr "FORCE A VISTA DO JOGO BรSICO " + +#: +msgid "DOWNLOADED" +msgstr "BAIXANDO" + +#: +msgid "UPDATE VERSION:" +msgstr "VERSรƒO ATUALIZADA:" + +#: +msgid "UPDATE CHANGELOG:" +msgstr "ATUALIZAR MUDANร‡A:" + +#: +msgid "MORE DETAILS" +msgstr "MAIS DETALHES" + +#: +msgid "CAROUSEL TRANSITIONS" +msgstr "TRANSIร‡ร•ES DE CARROSSEL" + +#: +msgid "ENABLE FILTERS" +msgstr "ATIVAR FILTROS" + +#: +msgid "THEME CONFIGURATION" +msgstr "CONFIGURAร‡ร•ES DE TEMA" + +#: +msgid "THEME COLORSET" +msgstr "COR DO TEMA" + +#: +msgid "THEME ICONSET" +msgstr "รCONES DO TEMA" + +#: +msgid "THEME MENU" +msgstr "MENรš TEMA" + +#: +msgid "THEME SYSTEMVIEW" +msgstr "TEMA DE SISTEMA" + +#: +msgid "THEME GAMELISTVIEW" +msgstr "TEMA DE LISTA DE JOGOS" + +#: +msgid "THEME REGION" +msgstr "REGIรƒO DO TEMA" + +#: +msgid "THIS THEME HAS NO OPTION" +msgstr "ESTE TEMA NรƒO TEM OPร‡ร•ES" + +#: +msgid "MANUAL INPUT" +msgstr "ENTRADA MANUAL" + +#: +msgid "AN ERROR OCCURED - DOWNLOADED" +msgstr "OCORREU UM ERRO - BAIXADO" + +#: +msgid "START KODI" +msgstr "INICIAR KODI" + +#: +msgid "Muestra la actual versiรณn de actualizaciรณn disponible." +msgstr "Mostra a versรฃo de atualizaรงรฃo atual disponรญvel." + +#: +msgid "Shows the current available update changelog." +msgstr "Mostra o atual log de alteraรงรตes de atualizaรงรฃo disponรญvel." + +#: +msgid "Configurar un controlador asociado. Su controlador debe estar asociado / enchufado antes." +msgstr "Configure um controlador associado. Seu controlador deve estar associado / conectado antes." + +#: +msgid "Choose if carousel will be animated or not during transitions" +msgstr "Escolha se o carrossel serรก animado ou nรฃo durante as transiรงรตes." + +#: +msgid "Select the type of transition that occurs when you start a game. INSTANT will do nothing, FADE will fade to dark, and SLIDE will zoom on the game cover (or name if there is no scrape information)" +msgstr "Selecione o tipo de transiรงรฃo que ocorre quando vocรช inicia um jogo. INSTANT nรฃo farรก nada, o FADE desaparecerรก no escuro e o SLIDE ampliarรก a capa do jogo (ou o nome se nรฃo houver informaรงรตes de scraper)." + +#: +msgid "Select exisiting colorset options for this theme." +msgstr "Selecione as opรงรตes de cores existentes para este tema." + +#: +msgid "Select exisiting iconset options for this theme." +msgstr "Selecione as opรงรตes existentes do conjunto de รญcones para este tema." + +#: +msgid "Select exisiting menu style options for this theme." +msgstr "Selecione as opรงรตes de estilo de menu existentes para este tema." + +#: +msgid "Select exisiting system view options for this theme." +msgstr "Selecione as opรงรตes existentes de visualizaรงรฃo do sistema para este tema." + +#: +msgid "Select exisiting gamelist view options for this theme." +msgstr "Selecione as opรงรตes de exibiรงรฃo existentes da lista de jogos para este tema." + +#: +msgid "Configure theme options if available." +msgstr "Configurar opรงรตes de tema, se disponรญveis." + +#: +msgid "Select Region of logos, pictures for system that are different for some countries. E.g. Megadrive in EU / Genesis in US" +msgstr "Selecione Regiรฃo de logotipos, imagens para sistema diferentes para alguns paรญses. Por exemplo, E.g. Megadrive em EU / Genesis em US" + +#: +msgid "Type the name of your SSID if it is hidden or not listed" +msgstr "Digite o nome do seu SSID se ele estiver oculto ou nรฃo listado" + +#: +msgid "Select a letter and the listing will go directly on the first game starting with this letter." +msgstr "Selecione uma carta e a listagem irรก diretamente no primeiro jogo comeรงando com esta carta." + +#: +msgid "Select the way the game list is sortered (alphabetically, by notation...)." +msgstr "Selecione o modo como a lista de jogos รฉ filtrada (alfabeticamente, por notaรงรฃo ...)." + +#: +msgid "Switch between seing or not only the favorites games. To add a game in the favorite list, select the game and toggle its state using 'Y'." +msgstr "Alterne entre ver ou nรฃo apenas os jogos favoritos. Para adicionar um jogo na lista de favoritos, selecione o jogo e alterne seu estado usando 'Y'." + +#: +msgid "Switch between seing or not the hidden games. To hide a game, edit its data and select 'Hide'." +msgstr "Alterne entre ver ou nรฃo os jogos escondidos. Para ocultar um jogo, edite seus dados e selecione "Ocultar"." + +#: +msgid "This option display a menu which allows to change game data and many others options." +msgstr "Esta opรงรฃo exibe um menu que permite alterar dados do jogo e muitas outras opรงรตes." + +#: +msgid "AVAILABLE UPDATE" +msgstr "ATUALIZAร‡รƒO DISPONรVEL" + +#: +msgid "UPDATE CHANGELOG" +msgstr "ATUALIZAR CAMBIOS" + +#: +msgid "CLOCK IN MENU" +msgstr "ASSISTIR NO MENU" + +#: +msgid "Now playing" +msgstr "Jogando agora" + +#: +msgid "DEFAULT (%1%)" +msgstr "PADRรƒO (%1%)" + +#: +msgid "INPUT REQUIRED" +msgstr "ENTRADA NECESSรRIA" + +#: +msgid "(skipped)" +msgstr "(saltado)" + +#: +msgid "UP/DOWN TO SKIP" +msgstr "CIMA / BAIXO PARA SALTAR" + +#: +msgid "A TO UNSET" +msgstr "A PARA CANCELAR" + +#: +msgid "DOWN TO SKIP AND KEEP [%1%]" +msgstr "BAIXO PARA SALTAR E MANTER [%1%]" + +#: +msgid "UP/DOWN TO SKIP AND KEEP [%1%]" +msgstr "CIMA /BAIXO PARA SALTAR E MANTER [%1%]" + +#: +msgid "Set duration of help popups, 0 means no popup." +msgstr "Definir a duraรงรฃo dos janelas emergentes de ajuda, 0 significa que nรฃo hรก popup." + +#: +msgid "HELP POPUP DURATION" +msgstr "AJUDA DA DURAร‡รƒO DO POPUP" + +#: +msgid "Set duration of music popups, 0 means no popup." +msgstr "Definir duraรงรฃo de popups de mรบsica, 0 significa que nรฃo hรก popup." + +#: +msgid "MUSIC POPUP DURATION" +msgstr "DURAร‡รƒO POPUP MรšSICA" + +#: +msgid "POPUP SETTINGS" +msgstr "CONFIGURAR POPUP" + +#: +msgid "POPUP POSITION" +msgstr "POSIร‡รƒO DE POPUP" + +#: +msgid "Select the position of popups on screen." +msgstr "Selecione a posiรงรฃo dos popups na tela." + +#: +msgid "Set position and duration of popups." +msgstr "Definir a posiรงรฃo e duraรงรฃo dos popups." + +#: +msgid "TOP/RIGHT" +msgstr "CIMA/DIREITA" + +#: +msgid "INFERIOR/DERECHA" +msgstr "BAIXO/DIREITA" + +#: +msgid "BOTTOM/LEFT" +msgstr "BAIXO/ESQUERDA" + +#: +msgid "TOP/LEFT" +msgstr "CIMA/ESQUERDA" + +#: +msgid "SHOW FOLDERS CONTENT" +msgstr "MOSTRAR O CONTEรšDO DA PASTA" + +#: +msgid "Switch between seeing the folders structure and seeing all games in a flatten top level." +msgstr "Alterne entre ver a estrutura das pastas e ver todos os jogos em um nรญvel superior nivelado." + +#: +msgid "NETPLAY" +msgstr "JOGO EM REDE" + +#: +msgid "NETPLAY SETTINGS" +msgstr "OPร‡ร•ES DE JOGO EM REDE" + +#: +msgid "NETPLAY LOBBY" +msgstr "LOBBY DE JOGO EM REDE" + +#: +msgid "Enable or disable Netplay in games." +msgstr "Ativar ou desativar jogo em rede em jogos." + +#: +msgid "PORT" +msgstr "PORTA" + +#: +msgid "NICKNAME" +msgstr "APELIDO" + +#: +msgid "RELAY SERVER" +msgstr "SERVIDOR DE RETRANSMISSรƒO" + +#: +msgid "Enable or disable connections throught relay servers." +msgstr "Ativar ou desativar conexรตes atravรฉs de servidores de retransmissรฃo." + +#: +msgid "KODI/NETPLAY" +msgstr "KODI/JUEGO EM REDE" + +#: +msgid "NO GAMES OR NO CONNECTION" +msgstr "NENHUM JOGO OU NENHUMA CONEXรƒO" + +#: +msgid "HASH NOW" +msgstr "MANTENHA HASH" + +#: +msgid "HASH THESE SYSTEMS" +msgstr "CRIE HASH DESTES SISTEMAS" + +#: +msgid "Add hash of roms in your gamelists to have more accurate results in Netplay." +msgstr "Adicione um hash de roms em seus gamelistas para ter resultados mais precisos em jogos em rede." + +#: +msgid "HASH ROMS" +msgstr "HASH DE ROMS" + +#: +msgid "Only missing hashs" +msgstr "Apenas faltando hashs" + +#: +msgid "USERNAME" +msgstr "NOME DE USUรRIO" + +#: +msgid "Country" +msgstr "Paรญs" + +#: +msgid "Latency" +msgstr "Latencia" + +#: +msgid "Host arch." +msgstr "Arco do host" + +#: +msgid "Core ver." +msgstr "Ver. de nรบcleo" + +#: +msgid "RA ver." +msgstr "Ver. de RA" + +#: +msgid "Can join" +msgstr "Pode juntar" + +#: +msgid "Rom and core match" +msgstr "Match de rom e nรบcleo" + +#: +msgid "Rom found" +msgstr "Rom encontrado" + +#: +msgid "No rom match" +msgstr "Nรฃo รก match de rom" + +#: +msgid "Match" +msgstr "Match" + +#: +msgid "No Match" +msgstr "Nรฃo รก Match" + +#: +msgid "Rom file" +msgstr "Arquivo rom" + +#: +msgid "Rom hash" +msgstr "Hash da rom" + +#: +msgid "THIS COULD TAKE A WHILE, CONFIRM?" +msgstr "ISSO PODE DEMORAR UM POUCO, CONFIRMAR?" + +#: +msgid "bueno" +msgstr "bom" + +#: +msgid "bad" +msgstr "ruim" + +#: +msgid "medium" +msgstr "mรฉdio" + +#: +msgid "NETPLAY POPUP DURATION" +msgstr "DURAร‡รƒO DO POPUP NO JOGO EM REDE" + +#: +msgid "Set duration of netplay popups, 0 means no popup." +msgstr "Definir a duraรงรฃo dos pop-ups de netplay, 0 significa que nรฃo hรก pop-ups." + +#: +msgid "Player" +msgstr "Jogador" + +#: +msgid "Game" +msgstr "Jogo" + +#: +msgid "ยกJuega en lรญnea en juegos que se ejecutan en Retroarch como NES, SNES, FBA, Genesis / Megadrive y mรกs!" +msgstr "Jogue online em jogos que rodam em Retroarch como NES, SNES, FBA, Genesis / Megadrive e muito mais!" + +#: +msgid "Rom, hash and core match" +msgstr "Rom, hash e match de nรบcleo" + +#: +msgid "No core match" +msgstr "Nรฃo รก match de nรบcleo" + +#: +Add a clock in the main menu. +Adicione um relรณgio no menu principal + +msgid "SHOW CLOCK" +msgstr "ATIVAR RELร“GIO" + +#: +msgid "UPGRADING" +msgstr "ATUALIZANDO" + +#: +msgid "PREPARING" +msgstr "PREPARANDO" + +#: +msgid "Starting UI" +msgstr "Iniciando interface" + +#: +msgid "VERIFYING" +msgstr "VERIFICANDO" + +#: +msgid "EMPTY LIST" +msgstr "LISTA VAZIA" + + +#: Retroarch ratio +msgid "Auto" +msgstr "Automรกtico" + +msgid "Square pixel" +msgstr "Pรญxel Quadrado" + +msgid "Square pixel" +msgstr "Pixel quadrado" + +msgid "Retroarch Config" +msgstr "Configuraรงรฃo do Retroarch'" + +msgid "Retroarch Custom" +msgstr "Customizaรงรฃo do Retroarch'" + +msgid "Core provided" +msgstr "Nรบcleo proporcionado" + +msgid "Do not set" +msgstr "Nรฃo definir" diff --git a/resources/locale/es/emulationstation2.po b/resources/locale/es/emulationstation2.po new file mode 100644 index 0000000000..0d3cdbcc9b --- /dev/null +++ b/resources/locale/es/emulationstation2.po @@ -0,0 +1,1946 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: recalbox-emulationstation\n" +"Language: es\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: +msgid "Loading..." +msgstr "Cargando..." + +#: +msgid "Preloading UI" +msgstr "Iniciando..." + +#: +msgid "GRID SIZE" +msgstr "TAMAร‘O DE CUADRรCULA" + +#: +msgid "DEFAULT GRID SIZE" +msgstr "TAMAร‘O POR DEFECTO CUADRรCULA" + +#: +msgid "UI MODE" +msgstr "MODO INTERFAZ" + +#: +msgid "PRELOAD UI" +msgstr "PRECARGA UI" + +#: +msgid "VSYNC" +msgstr "VSYNC" + +#: +msgid "DISPLAY FAVORITES FIRST IN GAMELIST" +msgstr "MOSTRAR JUEGOS FAVORITOS PRIMERO" + +#: +msgid "OPTIMIZE IMAGES VRAM USE" +msgstr "OPTIMIZAR USO VRMAM CON IMรGENES" + +#: +msgid "Full" +msgstr "Completo" + +#: +msgid "Kiosk" +msgstr "Kiosco" + +#: +msgid "Kid" +msgstr "Niรฑo" + +#: +msgstr "GAME COLLECTION SETTINGS" +msgstr "OPCIONES LISTAS PERSONALIZADAS" + +#: +msgstr "CANCEL" +msgstr "CANCELAR" + +#: +msgid "automatic" +msgstr "automรกtico" + +#: +msgid "basic" +msgstr "bรกsico" + +#: +msgid "detailed" +msgstr "detallado" + +#: +msgid "grid" +msgstr "cuadrรญcula" + +#: +msgid "fade" +msgstr "desvanecerse" + +#: +msgid "instant" +msgstr "instantรกneo" + +#: +msgid "slide" +msgstr "animado" + +#: +msgid "gridex" +msgstr "cuadrรญcula detallada" + +#: +msgid "ENABLE NAVIGATION SOUNDS" +msgstr "ACTIVAR SONIDOS DE NAVEGACIร“N" + +#: +msgid "ENABLE VIDEO AUDIO" +msgstr "ACTIVAR AUDIO DE VรDEO" + +#: +msgid "APPLY FILTER" +msgstr "APLICAR FILTRO" + +#: +msgid "AUTOMATIC GAME COLLECTIONS" +msgstr "COLECCIONES DE JUEGOS AUTOMรTICAS" + +#: +msgid "SELECT COLLECTIONS" +msgstr "SELECCIONAR COLECCIONES" + +#: +msgid "CUSTOM GAME COLLECTIONS" +msgstr "COLECCIONES PERSONALIZADAS" + +#: +msgid "CREATE NEW CUSTOM COLLECTION FROM THEME" +msgstr "CREAR UNA COLECCIร“N DESDE SISTEMAS" + +#: +msgid "GAME COLLECTION SETTINGS" +msgstr "OPCIONES LISTAS DE JUEGO" + +#: +msgid "SELECT THEME FOLDER" +msgstr "SELECCIONAR CARPETA" + +#: +msgid "CREATE NEW CUSTOM COLLECTION" +msgstr "CREAR UNA COLECCIร“N PERSONALIZADA" + +#: +msgid "New Collection Name" +msgstr "Nombre nueva colecciรณn" + +#: +msgid "Loading..." +msgstr "Cargando ..." + +#: +msgid "GROUP UNTHEMED CUSTOM COLLECTIONS" +msgstr "AGRUPAR COLECCIONES SIN SISTEMA" + +#: +msgid "SORT CUSTOM COLLECTIONS AND SYSTEMS" +msgstr "ORGANIZAR COLECCIONES Y SISTEMAS PERSONALIZADOS" + +#: +msgid "FINISH EDITING COLLECTION" +msgstr "FINALIZAR EDICIร“N" + +#: +msgid "PARSE GAMESLISTS ONLY" +msgstr "SOLO LISTA XML DE JUEGOS" + +#: +msgid "POWER SAVER MODES" +msgstr "MODOS AHORRO DE ENERGรA" + +#: +msgid "SAVE METADATA ON EXIT" +msgstr "GUARDAR METADATOS AL SALIR" + +#: +msgid "SHOW HIDDEN FILES" +msgstr "MOSTRAR ARCHIVOS OCULTOS" + +#: +msgid "VRAM LIMIT" +msgstr "LIMITE MAX VRAM" + +#: +msgid "RESET ALL FILTERS" +msgstr "RESTABLECER TODOS LOS FILTROS" + +#: +msgid "GAMELIST VIEW STYLE" +msgstr "MODO VISTA, LISTA DE JUEGOS" + +#: +msgid "GAME LAUNCH TRANSITION" +msgstr "TRANSICIร“N AL LANZAR JUEGO" + +#: +msgid "RANDOM" +msgstr "ALEATORIO" + +#: +msgid "Rating" +msgstr "Calificaciรณn" + +#: +msgid "Released" +msgstr "Publicado" + +#: +msgid "Developer" +msgstr "Desarrollador" + +#: +msgid "Publisher" +msgstr "Editor" + +#: +msgid "Genre" +msgstr "Gรฉnero" + +#: +msgid "Players" +msgstr "Jugadores" + +#: +msgid "NO GAMES FOUND - SKIP" +msgstr "NO SE ENCUENTRAN JUEGOS - SALTAR" + +#: +msgid "RETRY" +msgstr "REINTENTAR" + +#: +msgid "SKIP" +msgstr "SALTAR" + +#: +msgid "SEARCH FOR" +msgstr "BUSCAR POR:" + +#: +msgid "SEARCH" +msgstr "BUSCAR" + +#: +msgid "SCRAPING IN PROGRESS" +msgstr "ESCANEADO EN CURSO" + +#: +msgid "SYSTEM" +msgstr "SISTEMA" + +#: +msgid "subtitle text" +msgstr "texto del subtรญtulo" + +#: +msgid "INPUT" +msgstr "ENTRADA" + +#: +msgid "search" +msgstr "bรบsqueda" + +#: +msgid "STOP" +msgstr "DETENER" + +#: +msgid "stop (progress saved)" +msgstr "detener (progreso guardado)" + +#: +msgid "GAME %i OF %i" +msgstr "JUEGO %i DE %i" + +#: +msgid "WE CAN'T FIND ANY SYSTEMS!\n" +"CHECK THAT YOUR PATHS ARE CORRECT IN THE SYSTEMS CONFIGURATION FILE, AND YOUR GAME DIRECTORY HAS AT LEAST ONE GAME WITH THE CORRECT EXTENSION.\n" +"\n" +"" +msgstr "ยกNO HEMOS ENCONTRADO NINGรšN SISTEMA!\n" +""COMPRUEBE QUE SUS RUTAS SON CORRECTAS EN EL ARCHIVO DE CONFIGURACIร“N DE SISTEMAS, Y SU DIRECTORIO DE JUEGOS TIENE AL MENOS UN JUEGO CON LA EXTENSIร“N CORRECTA.\n" +"\n" +"" + +#: +msgid "%i i ยกJUEGO ESCANEADO EXITOSAMENTE!" +msgid_plural "%i ยกJUEGOS EXITOSAMENTE ESCANEADOS!" +msgstr[0] "%i ยกJUEGO ESCANEADO!" +msgstr[1] "%i ยกJUEGOS ESCANEADOS!" + +#: +msgid "%i i ยกJUEGO SALTADO!" +msgid_plural "%i GAMES SKIPPED." +msgstr[0] "%i ยกJUEGO SALTADO!" +msgstr[1] "%i ยกJUEGOS SALTADOS!" + +#: +msgid "OK" +msgstr "OK" + +#: +msgid "EDIT METADATA" +msgstr "EDITAR LOS METADATOS" + +#: +msgid "SCRAPE" +msgstr "ESCANEAR" + +#: +msgid "SAVE" +msgstr "GUARDAR" + +#: +msgid "THIS WILL DELETE A FILE!\n" +"ARE YOU SURE?" +msgstr "ยกESTO BORRARร UN ARCHIVO! \n" +"ยฟESTรS SEGURO?" + +#: +msgid "YES" +msgstr "SI" + +#: +msgid "NO" +msgstr "NO" + +#: +msgid "DELETE" +msgstr "BORRAR" + +#: +msgid "SAVE CHANGES?" +msgstr "ยฟGUARDAR CAMBIOS?" + +#: +msgid "BACK" +msgstr "VOLVER" + +#: +msgid "CLOSE" +msgstr "CERRAR" + +#: +msgid "MAIN MENU" +msgstr "MENรš PRINCIPAL" + +#: +msgid "KODI MEDIA CENTER" +msgstr "KODI MEDIA CENTER" + +#: +msgid "SYSTEM SETTINGS" +msgstr "OPCIONES DE SISTEMA" + +#: +msgid "VERSION" +msgstr "VERSIร“N" + +#: +msgid "DISK USAGE" +msgstr "ESPACIO EN DISCO" + +#: +msgid "STORAGE DEVICE" +msgstr "DISPOSITIVO DE ALMACENAMIENTO" + +#: +msgid "LANGUAGE" +msgstr "IDIOMA" + +#: +msgid "OVERCLOCK" +msgstr "OVERCLOCK" + +#: +msgid "EXTREM (1100Mhz)" +msgstr "EXTREMO (1100Mhz)" + +#: +msgid "TURBO (1000Mhz)" +msgstr "TURBO (1000Mhz)" + +#: +msgid "HIGH (950Mhz)" +msgstr "ALTO (950Mhz)" + +#: +msgid "NONE (700Mhz)" +msgstr "NINGUNO (900Mhz)" + +#: +msgid "TURBO (1050Mhz)+" +msgstr "TURBO (1050Mhz)+" + +#: +msgid "HIGH (1050Mhz)" +msgstr "ALTO (1050Mhz)" + +#: +msgid "NONE (900Mhz)" +msgstr "NINGUNO (900Mhz)" + +#: +msgid "NONE (1200Mhz)" +msgstr "NINGUNO (1200Mhz)" + +#: +msgid "NONE" +msgstr "NINGUNO" + +#. NEW SETTINGS ORGANIZATION +#: +msgid "UPDATES" +msgstr "ACTUALIZACIONES" + +#: +msgid "AUTO UPDATES" +msgstr "ACTUALIZACIONES AUTOMรTICAS" + +#: +msgid "START UPDATE" +msgstr "INICIAR ACTUALIZACIร“N" + +#: +msgid "KODI SETTINGS" +msgstr "OPCIONES KODI" + +#: +msgid "ENABLE KODI" +msgstr "HABILITAR KODI" + +#: +msgid "KODI AT START" +msgstr "AUTO INICIAR KODI" + +#: +msgid "START KODI WITH X" +msgstr "COMENZAR KODI CON X" + +#: +msgid "SECURITY" +msgstr "SEGURIDAD" + +#: +msgid "ENFORCE SECURITY" +msgstr "REFORZAR LA SEGURIDAD" + +#: +msgid "ROOT PASSWORD" +msgstr "CONTRASEร‘A DE ROOT" + +#: +msgid "THE SYSTEM WILL NOW REBOOT" +msgstr "EL SISTEMA SE REINICIARร" + +#: +msgid "GAMES SETTINGS" +msgstr "OPCIONES DE JUEGOS" + +#: +msgid "GAME RATIO" +msgstr "FORMATO DE JUEGO" + +#: +msgid "SMOOTH GAMES" +msgstr "JUEGOS SUAVES" + +#: +msgid "REWIND" +msgstr "RREBOBINAR" + +#: +msgid "AUTO SAVE/LOAD" +msgstr "AUTO GUARDAR/CARGAR" + +#: +msgid "SHADERS SET" +msgstr "CONJUNTO DE SHADERS" + +#: +msgid "SCANLINES" +msgstr "SCANLINES" + +#: +msgid "RETRO" +msgstr "RETRO" + +#: +msgid "RETROACHIEVEMENTS SETTINGS" +msgstr "AJUSTES DE RETROACHEIVMENTS" + +#: +msgid "RETROACHIEVEMENTS" +msgstr "RETROACHIEVEMENTS" + +#: +msgid "HARDCORE MODE" +msgstr "MODO HARDCORE" + +#: +msgid "USERNAME" +msgstr "NOMBRE DE USUARIO" + +#: +msgid "PASSWORD" +msgstr "CONTRASEร‘A" + +#: +msgid "ADVANCED" +msgstr "AVANZADO" + +#: +msgid "REALLY UPDATE GAMES LISTS ?" +msgstr "ยฟSEGURO QUE QUIERE ACTUALIZAR LAS LISTAS DE JUEGOS?" + +#: +msgid "UPDATE GAMES LISTS" +msgstr "ACTUALIZAR LISTAS DE JUEGOS" + +#: +msgid "CONTROLLERS SETTINGS" +msgstr "AJUSTE MANDOS" + +#: +msgid "UI SETTINGS" +msgstr "OPCIONES DE INTERFAZ" + +#: +msgid "OVERSCAN" +msgstr "OVERSCAN" + +#: +msgid "LAUNCH SCREENSAVER" +msgstr "SALVAPANTALLAS" + +#: +msgid "SCREENSAVER SETTINGS" +msgstr "AJUSTES SALVAPANTALLAS" + +#: +msgid "SCREENSAVER AFTER" +msgstr "SALVAPANTALLAS DESPUร‰S DE:" + +#: +msgid "TRANSITION STYLE" +msgstr "ESTILO DE TRANSICIร“N" + +#: +msgid "SCREENSAVER BEHAVIOR" +msgstr "COMPORTAMIENTO DE SALVAPANTALLAS" + +#: +msgid "SHOW FRAMERATE" +msgstr "MOSTRAR TASA DE IMรGENES" + +#: +msgid "ON-SCREEN HELP" +msgstr "AYUDA EN PANTALLA" + +#: +msgid "HIDE WHEN RUNNING GAME" +msgstr "OCULTAR AL LANZAR JUEGO" + +#: +msgid "QUICK SYSTEM SELECT" +msgstr "SELECCIร“N RAPIDA DE SISTEMA" + +#: +msgid "THEME" +msgstr "TEMAS" + +#: +msgid "SOUND SETTINGS" +msgstr "OPCIONES DE SONIDO" + +#: +msgid "SYSTEM VOLUME" +msgstr "VOLUMEN DEL SISTEMA" + +#: +msgid "FRONTEND MUSIC" +msgstr "MรšSICA DE FONDO" + +#: +msgid "OUTPUT DEVICE" +msgstr "DISPOSITIVO DE SALIDA" + +#: +msgid "HDMI" +msgstr "HDMI" + +#: +msgid "JACK" +msgstr "JACK" + +#: +msgid "AUTO" +msgstr "AUTO" + +#: +msgid "NETWORK SETTINGS" +msgstr "OPCIONES DE RED" + +#: +msgid "CONNECTED" +msgstr "CONECTADO" + +#: +msgid "NOT CONNECTED" +msgstr "NO CONECTADO" + +#: +msgid "STATUS" +msgstr "ESTADO" + +#: +msgid "IP ADDRESS" +msgstr "DIRECCIร“N IP" + +#: +msgid "HOSTNAME" +msgstr "NOMBRE HOST" + +#: +msgid "ENABLE WIFI" +msgstr "HABILITAR WIFI" + +#: +msgid "WIFI SSID" +msgstr "SSID WIFI" + +#: +msgid "WIFI KEY" +msgstr "CLAVE WIFI" + +#: +msgid "WIFI ENABLED" +msgstr "WIFI HABILITADO" + +#: +msgid "WIFI CONFIGURATION ERROR" +msgstr "ERROR DE CONFIGURACIร“N WIFI" + +#: +msgid "SCRAPER" +msgstr "ESCANEADOR" + +#: +msgid "SCRAPE FROM" +msgstr "ESCANEAR DESDE:" + +#: +msgid "SCRAPE RATINGS" +msgstr "ESCANEAR CALIFICACIONES" + +#: +msgid "SCRAPE NOW" +msgstr "ESCANEAR AHORA" + +#: +msgid "QUIT" +msgstr "SALIR" + +#: +msgid "REALLY RESTART?" +msgstr "ยฟSEGURO DE REINICIAR?" + +#: +msgid "RESTART SYSTEM" +msgstr "REINICIAR SISTEMA" + +#: +msgid "REALLY SHUTDOWN?" +msgstr "ยฟSEGURO DE APAGAR?" + +#: +msgid "SHUTDOWN SYSTEM" +msgstr "SAPAGAR EL SISTEMA" + +#: +msgid "Emulator" +msgstr "Emulador" + +#: +msgid "Core" +msgstr "Nรบcleo" + +#: +msgid "YOU ARE GOING TO CONFIGURE A CONTROLLER. IF YOU HAVE ONLY ONE JOYSTICK, CONFIGURE THE DIRECTIONS KEYS AND SKIP JOYSTICK CONFIG BY HOLDING A BUTTON. IF YOU DO NOT HAVE A SPECIAL KEY FOR HOTKEY, CHOOSE THE SELECT BUTTON. SKIP ALL BUTTONS YOU DO NOT HAVE BY HOLDING A KEY. BUTTONS NAMES ARE BASED ON THE SNES CONTROLLER." +msgstr "USTED VA A CONFIGURAR UN MANDO, SI TIENE Sร“LO UN JOYSTICK, CONFIGURE LAS TECLAS DE DIRECCIร“N Y SรLTESE LA CONFIGURACIร“N DEL JOYSTICK MANTENIENDO APRETADO UN BOTร“N. SI NO TIENE UN BOTร“N ESPECIAL PARA HOTKEY, USE SELECT E IGNORE LOS BOTONES QUE NO HAYA EN SU MANDO. NO PUEDE USAR OTRA VEZ UN BOTร“N YA CONFIGURADO MANTENIENDO APRETADO UN BOTร“N. LOS NOMBRES DE LOS BOTONES ESTรN BASADOS EN EL MANDO DE SUPER NINTENDO." + +#. GUIMENU +#: +msgid "CONFIGURE A CONTROLLER" +msgstr "CONFIGURAR UN MANDO" + +#. Bluetooth +#: +msgid "CONTROLLER PAIRED" +msgstr "MANDO EMPAREJADO" + +#: +msgid "UNABLE TO PAIR CONTROLLER" +msgstr "NO SE PUEDE EMPAREJAR EL MANDO" + +#: +msgid "AN ERROR OCCURED" +msgstr "OCURRIร“ UN ERROR" + +#: +msgid "NO CONTROLLERS FOUND" +msgstr "NO SE ENCUENTRA MANDO" + +#: +msgid "PAIR A BLUETOOTH CONTROLLER" +msgstr "EMPAREJAR MANDO BLUETOOTH" + +#: +msgid "CONTROLLERS LINKS HAVE BEEN DELETED." +msgstr "SE HA BORRADO EL EMPAREJAMIENTO DEL MANDO." + +#: +msgid "FORGET BLUETOOTH CONTROLLERS" +msgstr "OLVIDAR MANDOS BLUETOOTH" + +#: +msgid "INPUT P%i" +msgstr "ENTRADA %i" + +#: +msgid "CHOOSE" +msgstr "ELEGIR" + +#: +msgid "SELECT" +msgstr "SELECCIONAR" + +#: +msgid "OPTIONS" +msgstr "OPCIONES" + +#: +msgid "JUMP TO LETTER" +msgstr "IR A LA LETRA O NรšMERO" + +#: +msgid "SORT GAMES BY" +msgstr "ORDENAR JUEGOS POR:" + +#. FAVORITES +#: +msgid "FAVORITES ONLY" +msgstr "SOLO FAVORITOS" + +#: +msgid "EDIT THIS GAME'S METADATA" +msgstr "EDITAR METADATOS DE ESTE JUEGO" + +#: +msgid "SCRAPE THESE GAMES" +msgstr "ESCANEAR ESTOS JUEGOS" + +#: +msgid "All Games" +msgstr "Todos los juegos" + +#. MISSING SCRAPPER TRANSLATIONS +#: +msgid "Only missing image" +msgstr "Sรณlo imagenes faltantes" + +#: +msgid "FILTER" +msgstr "FILTRO" + +#: +msgid "SCRAPE THESE SYSTEMS" +msgstr "ESCANEAR ESTOS SISTEMAS" + +#: +msgid "SYSTEMS" +msgstr "SISTEMAS" + +#: +msgid "USER DECIDES ON CONFLICTS" +msgstr "ELIGIR SI HAY CONFLICTO" + +#: +msgid "START" +msgstr "INICIAR" + +#: +msgid "WARNING: SOME OF YOUR SELECTED SYSTEMS DO NOT HAVE A PLATFORM SET. RESULTS MAY BE EVEN MORE INACCURATE THAN USUAL!\n" +"CONTINUE ANYWAY?" +msgstr "ADVERTENCIA: ALGUNOS DE SUS SISTEMAS SELECCIONADOS NO TIENEN UNA PLATAFORMA.!LOS RESULTADOS PUEDEN SER ERRONEOS!\n" +"ยฟCONTINUAR IGUALMENTE?" + +#: +msgid "NO GAMES FIT THAT CRITERIA." +msgstr "SIN JUEGOS CON ESOS CRITERIOS." + +#: +msgid "REALLY UPDATE?" +msgstr "ยฟACTUALIZAR EL SISTEMA?" + +#: +msgid "NETWORK CONNECTION NEEDED" +msgstr "SE NECESITA CONEXIร“N DE RED" + +#: +msgid "UPDATE DOWNLOADED, THE SYSTEM WILL NOW REBOOT" +msgstr "ACTUALIZACIร“N DESCARGADA, EL SISTEMA SE REINICIARร" + +#: +msgid "UPDATE FAILED, THE SYSTEM WILL NOW REBOOT" +msgstr "FALLO ACTUALIZAR, EL SISTEMA SE REINICIARร" + +#: +msgid "NO UPDATE AVAILABLE" +msgstr "NO HAY ACTUALIZACIร“N DISPONIBLE" + +#: +msgid "enter emulator" +msgstr "introducir emulador" + +#: +msgid "enter core" +msgstr "introducir nรบcleo" + +#: +msgid "Ratio" +msgstr "Ratio" + +#: +msgid "enter ratio" +msgstr "introducir ratio" + +#: +msgid "Name" +msgstr "Nombre" + +#: +msgid "enter game name" +msgstr "introducir nombre de juego" + +#: +msgid "Description" +msgstr "Descripciรณn" + +#: +msgid "enter description" +msgstr "introducir descripciรณn" + +#: +msgid "Image" +msgstr "Imagen" + +#: +msgid "enter path to image" +msgstr "introducir ruta de la imagen" + +#: +msgid "Thumbnail" +msgstr "Miniatura" + +#: +msgid "enter path to thumbnail" +msgstr "introducir ruta de la miniatura" + +#: +msgid "enter rating" +msgstr "introducir calificaciรณn" + +#: +msgid "Release date" +msgstr "Fecha de lanzamiento" + +#: +msgid "enter release date" +msgstr "introducir fecha de lanzamiento" + +#: +msgid "enter game developer" +msgstr "introducir desarrollador del juego" + +#: +msgid "enter game publisher" +msgstr "introducir el editor del juego" + +#: +msgid "enter game genre" +msgstr "introducir el gรฉnero de juego" + +#: +msgid "enter number of players" +msgstr "introducir el nรบmero de jugadores" + +#: +msgid "Favorite" +msgstr "Favorito" + +#: +msgid "entrer le favori" +msgstr "aรฑadir a favoritos" + +#: +msgid "Region" +msgstr "Regiรณn" + +#: +msgid "enter region" +msgstr "introducir regiรณn" + +#: +msgid "Romtype" +msgstr "Tipo de rom" + +#: +msgid "enter romtype" +msgstr "introducir tipo de rom" + +#: +msgid "Hidden" +msgstr "Oculto" + +#: +msgid "HIDDEN" +msgstr "OCULTO" + +#: +msgid "NAME" +msgstr "NOMBRE" + +#: +msgid "EMULATOR" +msgstr "EMULADOR" + +#: +msgid "FAVORITE" +msgstr "FAVORITO" + +#: +msgid "set hidden" +msgstr "marcar como oculto" + +#: +msgid "Play count" +msgstr "veces jugado" + +#: +msgid "enter number of times played" +msgstr "introducir el nรบmero de veces jugado" + +#: +msgid "Last played" +msgstr "รšltima vez jugado" + +#: +msgid "enter last played date" +msgstr "introducir la รบltima vez que jugรณ al juego" + +#: +msgid "GAMES AVAILABLE" +msgstr "JUEGOS DISPONIBLES" + +#: +msgid "%i FAVORITE" +msgid_plural "%i FAVORITES" +msgstr[0] "%i FAVORITO" +msgstr[1] "%i FAVORITOS" + +#: +msgid "SCROLL" +msgstr "SCROLL" + +#: +msgid "LAUNCH" +msgstr "LANZAR" + +#: +msgid "Times played" +msgstr "Veces jugado" + +#: +msgid "MENU" +msgstr "MENรš" + +#: +msgid "FILENAME, ASCENDING" +msgstr "NOMBRE ARCHIVO, ASCENDENTE" + +#: +msgid "FILENAME, DESCENDING" +msgstr "NOMBRE ARCHIVO, DESCENDENTE" + +#: +msgid "RATING, ASCENDING" +msgstr "CALIFICACIร“N, ASCENDENTE" + +#: +msgid "RATING, DESCENDING" +msgstr "CALIFICACIร“N, DESCENDENTE" + +#: +msgid "TIMES PLAYED, ASCENDING" +msgstr "VECES JUGADO, ASCENDENTE" + +#: +msgid "TIMES PLAYED, DESCENDING" +msgstr "VECES JUGADO, DESCENDENTE" + +#: +msgid "LAST PLAYED, ASCENDING" +msgstr "รšLTIMA VEZ JUGADO, ASCENDENTE" + +#: +msgid "LAST PLAYED, DESCENDING" +msgstr "รšLTIMA VEZ JUGADO, DESCENDENTE" + +#: +msgid "WORKING..." +msgstr "EN CURSO..." + +#: +msgid "CHANGE" +msgstr "CAMBIAR" + +#: +msgid "never" +msgstr "nunca" + +#: +msgid "just now" +msgstr "ahora" + +#: +msgid "%i sec ago" +msgid_plural "%i secs ago" +msgstr[0] "hace %i segundos" +msgstr[1] "hace %i segundos" + +#: +msgid "%i min ago" +msgid_plural "%i mins ago" +msgstr[0] "hace %i min" +msgstr[1] "hace %i min" + +#: +msgid "%i hour ago" +msgid_plural "%i hours ago" +msgstr[0] "hace %i horas" +msgstr[1] "hace %i horas" + +#: +msgid "%i day ago" +msgid_plural "%i days ago" +msgstr[0] "hace %i dรญa" +msgstr[1] "hace %i dรญas" + +#: +msgid "unknown" +msgstr "desconocido" + +#: +msgid "SELECT ALL" +msgstr "SELECCIONAR TODO" + +#: +msgid "SELECT NONE" +msgstr "SELECCIONAR NINGUNO" + +#: +msgid "%i SELECTED" +msgid_plural "%i SELECTED" +msgstr[0] "%i SELECCIONADO" +msgstr[1] "%i SELECCIONADOS" + +#: +msgid "UP" +msgstr "ARRIBA" + +#: +msgid "DOWN" +msgstr "ABAJO" + +#: +msgid "LEFT" +msgstr "IZQUIERDA" + +#: +msgid "RIGHT" +msgstr "DERECHA" + +#: +msgid "JOYSTICK 1 UP" +msgstr "JOYSTICK 1 ARRIBA" + +#: +msgid "JOYSTICK 1 LEFT" +msgstr "JOYSTICK 1 IZQUIERDA" + +#: +msgid "JOYSTICK 2 UP" +msgstr "JOYSTICK 2 ARRIBA" + +#: +msgid "JOYSTICK 2 LEFT" +msgstr "JOYSTICK 2 IZQUIERDA" + +#: +msgid "PAGE UP" +msgstr "L1 (PรGINA SIGUIENTE)" + +#: +msgid "PAGE DOWN" +msgstr "R1 (PรGINA ANTERIOR)" + +#: +msgid "HOTKEY" +msgstr "HOTKEY" + +#: +msgid "CONFIGURING" +msgstr "CONFIGURACIร“N" + +#: +msgid "KEYBOARD" +msgstr "TECLADO" + +#: +msgid "GAMEPAD %i" +msgstr "MANDO %i" + +#. Config controllers missing translation +#: +msgid "PRESS ANYTHING" +msgstr "PRESIONE CUALQUIER BOTร“N" + +#: +msgid "ALREADY TAKEN" +msgstr "YA ESTร EN USO" + +#: +msgid "DISCARD CHANGES" +msgstr "DESECHAR LOS CAMBIOS" + +#: +msgid "WELCOME" +msgstr "BIENVENIDO" + +#: +msgid "CONFIGURE INPUT" +msgstr "CONFIGURAR MANDO" + +#: +msgid "%i GAMEPAD DETECTED" +msgid_plural "%i GAMEPADS DETECTED" +msgstr[0] "%i MANDO DETECTADO" +msgstr[1] "%i MANDOS DETECTADOS" + +#: +msgid "NO GAMEPADS DETECTED" +msgstr "NINGรšN MANDO DETECTADO" + +#: +msgid "HOLD A BUTTON ON YOUR DEVICE TO CONFIGURE IT." +msgstr "MANTENGA UN BOTร“N PULSADO PARA CONFIGURAR EL MANDO" + +#: +msgid "PRESS F4 TO QUIT AT ANY TIME." +msgstr "PULSE F4 PARA SALIR EN CUALQUIER MOMENTO" + +#: +msgid "PRESS ESC OR THE HOTKEY TO CANCEL." +msgstr "PULSE ESC O BOTร“N HOTKEY PARA CANCELAR" + +#: +msgid "DO YOU WANT TO START KODI MEDIA CENTER ?" +msgstr "ยฟQUIERES INICIAR KODI MEDIA CENTER?" + +#: +msgid "LOADING..." +msgstr "CARGANDO..." + +#: +msgid "PLEASE WAIT..." +msgstr "POR FAVOR ESPERE..." + +#: +msgid "REALLY SHUTDOWN WITHOUT SAVING METADATAS?" +msgstr "ยฟREALMENTE QUIERE APAGAR SIN GUARDAR LOS METADATOS?" + +#: +msgid "FAST SHUTDOWN SYSTEM" +msgstr "SISTEMA DE APAGADO RรPIDO" + +#: +msgid "WE CAN'T FIND ANY SYSTEMS!\n" +"CHECK THAT YOUR PATHS ARE CORRECT IN THE SYSTEMS CONFIGURATION FILE, AND YOUR GAME DIRECTORY HAS AT LEAST ONE GAME WITH THE CORRECT EXTENSION.\n" +"\n" +"VISIT EMULATIONSTATION.ORG FOR MORE INFORMATION." +msgstr "ยกNO PODEMOS ENCONTRAR SISTEMAS!\n" +"COMPRUEBE QUE LA RUTA ES CORRECTA EN EL ARCHIVO DE CONFIGURACIร“N DE SISTEMAS, Y SU DIRECTORIO DE JUEGOS TIENE MรNIMO UN JUEGO CON EXTENSIร“N CORRECTA.\n" +"VISITA EMULATIONSTATION.ORG PARA MรS INFORMACIร“N." + +#: +msgid "ON SCREEN KEYBOARD" +msgstr "TECLADO DE PANTALLA" + +#: +msgid "SHIFTS FOR UPPER,LOWER, AND SPECIAL" +msgstr "PULSE SHIFT PARA MAYรšSCULAS" + +#: +msgid "SPACE" +msgstr "ESPACIO" + +#: +msgid "DELETE A CHAR" +msgstr "BORRAR UN CARACTER" + +#: +msgid "SHIFT" +msgstr "MAYรšSCULAS" + +#: +msgid "STOP EDITING" +msgstr "DEJAR DE EDITAR" + +#: +msgid "MOVE CURSOR" +msgstr "MOVER EL CURSOR" + +#: +msgid "EDIT" +msgstr "EDITAR" + +#: +msgid "ACCEPT RESULT" +msgstr "ACEPTAR EL RESULTADO" + +#: +msgid "FILENAME" +msgstr "NOMBRE DE ARCHIVO" + +#: +msgid "RATING" +msgstr "VOTOS" + +#: +msgid "PARTIES JOUร‰ES" +msgstr "VECES JUGADO" + +#: +msgid "LAST PLAYED" +msgstr "รšLTIMA VEZ JUGADO" + +#: +msgid "NUMBER OF PLAYERS" +msgstr "NรšMERO DE JUGADORES" + +#: +msgid "DEVELOPER" +msgstr "DESARROLLADOR" + +#: +msgid "GENRE" +msgstr "Gร‰NERO" + +#: +msgid "SHOW HIDDEN" +msgstr "MOSTRAR OCULTO" + +#: +msgid "EXTREM (1400Mhz)" +msgstr "EXTREMO (1400MHz)" + +#: +msgid "TURBO (1350Mhz)" +msgstr "TURBO (1350MHz)" + +#: +msgid "HIGH (1300Mhz)" +msgstr "ALTO (1300MHz)" + +#: +msgid "TURBO AND EXTREM OVERCLOCK PRESETS MAY CAUSE SYSTEM UNSTABILITIES, SO USE THEM AT YOUR OWN RISK.\n" +"IF YOU CONTINUE, THE SYSTEM WILL REBOOT NOW." +msgstr "TURBO Y EXTREMO PUEDEN CAUSAR INESTABILIDAD DEL SISTEMA POR SOBRECARGA, ASร QUE UTILIZALOS BAJO SU PROPIO RIESGO.\n" +"SI CONTINUAS, EL SISTEMA REINICIARร AHORA." + +#: +msgid "%i GAME HIDDEN" +msgid_plural "%i GAMES HIDDEN" +msgstr[0] "%i JUEGO OCULTO" +msgstr[1] "%i JUEGOS OCULTOS" + +#: +msgid "Start kodi media player." +msgstr "Iniciar Kodi media player" + +#: +msgid "Select the language for your recalbox, select an external drive to store your games and configurations, check your current version and the free space on your drive" +msgstr "Seleccione el idioma para su ES, seleccione una unidad externa para almacenar sus juegos y configuraciones, verifique su versiรณn actual y el espacio libre en su unidad" + +#: +msgid "Configure games display, ratio, filters (shaders), auto save and load and retroachievement account." +msgstr "Configurar la pantalla de juegos, la proporciรณn, los filtros (shaders), guardar automรกticamente y cargar la cuenta de retroachievement." + +#: +msgid "The game ratio is the ratio between image width and image height. Use AUTO to let the emulator choose the original game ratio, that will give you the best retrogaming experience." +msgstr "La proporciรณn del juego es la proporciรณn entre el ancho de la imagen y la altura de la imagen. Use AUTO para que el emulador elija la proporciรณn del juego original, que le darรก la mejor experiencia de retrogaming." + +#: +msgid "Smooth the game image. This option makes the image smoother, using bilinear filtering." +msgstr "Suavizar la imagen del juego. Esta opciรณn hace que la imagen sea mรกs suave, usando un filtro bilineal." + +#: +msgid "This option allows you to rewind the game if you get killed by a monster, or if you make any other mistake. Use the HOTKEY + LEFT command within the game to rewind." +msgstr "Esta opciรณn le permite rebobinar el juego si un monstruo lo mata, o si comete algรบn otro error. Use el comando HOTKEY + IZQUIERDA dentro del juego para rebobinar." + +#: +msgid "Auto save the state when you quit a game, and auto load last saved state when you start a game." +msgstr "Guardar automรกticamente el estado al salir de un juego, y cargar automรกticamente el รบltimo estado guardado al iniciar un juego." + +#: +msgid "Integer scaling is scaling by a factor of a whole number, such as 2x, 3x, 4x, etc. This option scales the image up to the greatest integer scale below the set resolution. So for instance, if you set your fullscreen resolution to 1920x1080 and enable integer scaling, it will only scale a 320x240 image up to 1280x960, and leave black borders all around. This is to maintain a 1:1 pixel ratio with the original source image, so that pixels are not unevenly duplicated." +msgstr "La escala de enteros es escalar por un factor de un nรบmero entero, como 2x, 3x, 4x, etc. Esta opciรณn escala la imagen a la escala de enteros mรกs grande por debajo de la resoluciรณn establecida. Por ejemplo, si configura la resoluciรณn de pantalla completa "Para 1920x1080 y habilitar el escalado de enteros, solo escalarรก una imagen de 320x240 hasta 1280x960, y dejarรก bordes negros alrededor. Esto es para mantener una proporciรณn de pรญxeles de 1:1 con la imagen de origen original, para que los pรญxeles no se dupliquen de manera desigual." + +#: +msgid "Shaders are like filters for the game rendering. You can select a shader set here, which is a collection of shaders selected for each system. You can also change the shader within the game with HOTKEY + L2 or HOTKEY + R2." +msgstr "Los shaders son como filtros para la renderizaciรณn del juego. Puede seleccionar un conjunto de shaders aquรญ, hay una colecciรณn de shaders para seleccionar en cada sistema. Tambiรฉn puede cambiar el shader dentro del juego con HOTKEY + L2 o HOTKEY + R2" + +#: +msgid "Enable or disable RetroAchievements in games." +msgstr "Habilitar o deshabilitar RetroAchievements en juegos." + +#: +msgid "Hardcore mode disables *all* savestate and rewind functions within the emulator: you will not be able to save and reload at any time. You will have to complete the game and get the achievements first time, just like on the original console. In reward for this, you will earn both the standard and the hardcore achievement, in effect earning double points! A regular game worth 400 points, is now worth 800 if you complete it on hardcore! For example: if you complete the game for 400 points, you then have the opportunity to earn another 400 on hardcore." +msgstr "El modo Hardcore desactiva *todas las funciones* de savestate y rebobinar dentro del emulador: no podrรก guardar ni recargar en ningรบn momento. Deberรก completar el juego y obtener los logros por primera vez, al igual que en la consola original. ยกEn recompensa por esto, obtendrรกs el logro estรกndar y el logro hardcore, en efecto obteniendo el doble de puntos! ยกUn juego normal que vale 400 puntos vale ahora 800 si lo completas en hardcore!\n" +"Por ejemplo: si completas el juego por 400 puntos, entonces tienes la oportunidad de ganar otros 400 en el hardcore." + +#: +msgid "The website retroachievements.org proposes challenges/achievements/trophies on platforms like NES, SNES, GB, GBC, GBA, Genesis/Megadrive, TurboGrafx16/PCEngine and more! Create your account on retroachievements.org and start your quest for achievements!" +msgstr "ยกEl sitio web retroachievements.org propone desafรญos / logros / trofeos en plataformas como NES, SNES, GB, GBC, GBA, Genesis / Megadrive, TurboGrafx16 / PCEngine y mรกs! ยกCrea tu cuenta en retroachievements.org y comienza tu bรบsqueda de logros! " + +#: +msgid "Add and configure up to 5 controllers." +msgstr "Agregar y configurar hasta 5 mandos." + +#: +msgid "Start the screensaver after N minutes." +msgstr "Iniciar el salvapantallas despuรฉs de N minutos." + +#: +msgid "Set the screensaver behavior. DIM will reduce the screen light, and BLACK will turn the screen black." +msgstr "Establece el comportamiento del salvapantallas. DIM reducirรก la luz de la pantalla, y NEGRO volverรก la pantalla a negro." + +#: +msgid "Shows a help at the bottom of the screen which displays commands you can use." +msgstr "Muestra una ayuda en la parte inferior de la pantalla que muestra los comandos que puede usar." + +#: +msgid "When enabled, you can switch between systems while browsing a gamelist by pressing LEFT or RIGHT." +msgstr "Cuando estรก habilitado, puede cambiar entre sistemas mientras navega por una lista de juegos presionando IZQUIERDA o DERECHA." + +#: +msgid "Updates the gamelists, if you added games since the last boot." +msgstr "Actualiza las listas de juegos, si agregรณ juegos desde el รบltimo arranque." + +#: +msgid "Set the volume of the sound output for the frontend and the games." +msgstr "Establecer el volumen de la salida de sonido para la interfaz y los juegos." + +#: +msgid "Get informations and visual for your games. The scraper downloads metadata and visuals for your games from different servers and enhances the user experience in EmulationStation completely." +msgstr "Obtenga informaciรณn e imรกgenes para sus juegos. El escaneadorr descarga metadatos e imรกgenes para sus juegos desde diferentes servidores mejorando la experiencia del usuario en EmulationStation por completo." + +#: +msgid "Select a server to scrape from. The SCREENSCRAPER server is recommended and is based on www.screenscraper.fr and scrapes game data in your language, if available." +msgstr "Seleccione un servidor para escanear. Se recomienda el servidor SCREENSCRAPER y se basa en www.screenscraper.fr y escanea los datos del juego en su idioma, si estรกn disponibles." + +#: +msgid "Begin the scrape process with the configuration shown below." +msgstr "Comience el proceso de escaneado con la configuraciรณn que se muestra a continuaciรณn." + +#: +msgid "Scrape and display game ratings." +msgstr "Escanear y mostrar clasificaciones de juegos." + +#: +msgid "Advanced settings. Please make sure you really know what you're doing, before changing any values in this menu." +msgstr "Configuraciรณn avanzada. Asegรบrese de que realmente sabe lo que estรก haciendo, antes de cambiar cualquier valor en este menรบ." + +#: +msgid "Overclock your board to increase the performance.\n" +"Overclock settings are tested and validated by the community. Keep in mind that overclocking your board can void your warranty." +msgstr "Overclockee su placa para aumentar el rendimiento.\ n" +"La comunidad ha probado y validado las configuraciones de overclock.\ n" +"Tenga en cuenta que el overclocking de su placa puede anular su garantรญa." + +#: +msgid "Only show games contained in the gamelist.xml file (located in your roms directories).\n" +"This option highly speeds up boot time, but new games will not be detected." +msgstr "Solo muestra los juegos contenidos en el archivo gamelist.xml (ubicado en sus directorios roms).\n" +"Esta opciรณn acelera mucho el tiempo de arranque, pero no se detectarรกn nuevos juegos." + +#: +msgid "This option allows you to set the selected system to fixed mode. With this option activated, the user cannot access other systems." +msgstr "Esta opciรณn le permite configurar el sistema seleccionado en modo fijo.\n" +"Con esta opciรณn activada, el usuario no puede acceder a otros sistemas." + +#: +msgid "Always display the basic gamelist view, even if you have scraped your games." +msgstr "Mostrar siempre la vista bรกsica de la lista de juegos, incluso si ha escaneado sus juegos." + +#: +msgid "Enable or disable Kodi, customize the Kodi startup, enable the X button to start Kodi" +msgstr "Habilitar o deshabilitar Kodi, personalizar el inicio de Kodi, habilitar el botรณn X para iniciar Kodi." + +#: +msgid "Enable or disable Kodi. If kodi is disabled, you won't be able to start it with the X button, or start it automatically at boot. The menu entry will be removed as well." +msgstr "Habilitar o deshabilitar Kodi. Si Kodi estรก deshabilitado, no podrรก iniciarlo con el botรณn X, o iniciarlo automรกticamente en el arranque. La entrada del menรบ tambiรฉn se eliminarรก." + +#: +msgid "Use the X button to start Kodi." +msgstr "Usa el botรณn X para iniciar Kodi." + +#: +msgid "Automatically start into Kodi on boot." +msgstr "Iniciar automรกticamente Kodi en el arranque." + +#: +msgid "Show the framerate in EmulationStation and in game." +msgstr "Mostrar tasa de imรกgenes en EmulationStation en el juego." + +#: +msgid "Select which emulator to use when you start a game for this system." +msgstr "Seleccione el emulador que inicie juego para este sistema." + +#: +msgid "Select which core to use for the selected emulator. For example, the LIBRETRO emulator has many cores to run Super Nintendo games. The default core you choose here can also be overridden in game specific settings." +msgstr "Seleccione quรฉ nรบcleo usarรก para el emulador seleccionado. Por ejemplo, el emulador LIBRETRO tiene muchos nรบcleos para ejecutar juegos de Super Nintendo. El nรบcleo predeterminado que elija aquรญ tambiรฉn se puede anular en la configuraciรณn especรญfica del juego." + +#: +msgid "USE COMPOSED VISUALS" +msgstr "UTILIZAR VISUALES COMPUESTOS" + +#: +msgid "CHECK UPDATES" +msgstr "REVISAR ACTUALIZACIONES" + +#: +msgid "UPDATE TYPE" +msgstr "TIPO DE ACTUALIZACIร“N" + +#: +msgid "INTEGER SCALE (PIXEL PERFECT)" +msgstr "ESCALA DE ENTEROS (PIXEL PERFECTO)" + +#: +msgid "ADVANCED SETTINGS" +msgstr "AJUSTES AVANZADOS" + +#: +msgid "BOOT SETTINGS" +msgstr "PARรMETROS DE INICIO" + +#: +msgid "GAMELIST ONLY" +msgstr "Sร“LO LISTA JUEGO" + +#: +msgid "START ON SYSTEM" +msgstr "COMENZAR EN EL SISTEMA" + +#: +msgid "BOOT ON GAMELIST" +msgstr "EMPEZAR EN LA LISTA DE JUEGOS" + +#: +msgid "HIDE SYSTEM VIEW" +msgstr "OCULTAR VISTA DE SISTEMAS" + +#: +msgid "EMULATOR ADVANCED CONFIGURATION" +msgstr "CONFIGURACIร“N AVANZADA EMULADOR" + +#: +msgid "ADVANCED EMULATOR CONFIGURATION" +msgstr "CONFIGURACIร“N AVANZADA EMULADOR" + +#: +msgid "HELP" +msgstr "AYUDA" + +#: +msgid "THE SYSTEM IS UP TO DATE" +msgstr "EL SISTEMA ESTA ACTUALIZADO" + +#: +msgid "FORCE BASIC GAMELIST VIEW" +msgstr "FORZAR LA VISTA BรSICA DE JUEGO" + +#: +msgid "DOWNLOADED" +msgstr "DESCARGADO" + +#: +msgid "UPDATE VERSION:" +msgstr "VERSIร“N ACTUALIZADA:" + +#: +msgid "UPDATE CHANGELOG:" +msgstr "ACTUALIZAR REGISTRO DE CAMBIOS:" + +#: +msgid "MORE DETAILS" +msgstr "MรS DETALLES" + +#: +msgid "CAROUSEL TRANSITIONS" +msgstr "TRANSICIONES DE CARRUSEL" + +#: +msgid "ENABLE FILTERS" +msgstr "ACTIVAR FILTROS" + +#: +msgid "THEME CONFIGURATION" +msgstr "CONFIGURACIร“N DE TEMA" + +#: +msgid "THEME COLORSET" +msgstr "COLORSET TEMA" + +#: +msgid "THEME ICONSET" +msgstr "ICONSET TEMA" + +#: +msgid "THEME MENU" +msgstr "MENรš TEMA" + +#: +msgid "THEME SYSTEMVIEW" +msgstr "TEMA DE SISTEMA" + +#: +msgid "THEME GAMELISTVIEW" +msgstr "TEMA DE LISTA DE JUEGOS" + +#: +msgid "THEME REGION" +msgstr "REGIร“N DE TEMA" + +#: +msgid "THIS THEME HAS NO OPTION" +msgstr "ESTE TEMA NO TIENE OPCIONES" + +#: +msgid "MANUAL INPUT" +msgstr "ENTRADA MANUAL" + +#: +msgid "AN ERROR OCCURED - DOWNLOADED" +msgstr "HA OCURRIDO UN ERROR - DESCARGADO" + +#: +msgid "START KODI" +msgstr "COMENZAR KODI" + +#: +msgid "Montre les versions disponibles." +msgstr "Muestra la actual versiรณn de actualizaciรณn disponible." + +#: +msgid "Shows the current available update changelog." +msgstr "Muestra el registro de cambios actual." + +#: +msgid "Configurer une manette appairรฉe. Votre manette doit รชtre appairรฉe / branchรฉe auparavant." +msgstr "Configurar un mando asociado. Su mando debe estar asociado / enchufado previamente." + +#: +msgid "Choose if carousel will be animated or not during transitions" +msgstr "Elija si el carrusel serรก animado o no durante las transiciones." + +#: +msgid "Select the type of transition that occurs when you start a game. INSTANT will do nothing, FADE will fade to dark, and SLIDE will zoom on the game cover (or name if there is no scrape information)" +msgstr "Seleccione el tipo de transiciรณn que se producirรก cuando inicie un juego. INSTANTANEO no harรก nada, DESVANECER se desvanecerรก paulatinammente y ANIMADO harรก zoom en la cubierta del juego (o nombre si no hay informaciรณn de escaneado)." + +#: +msgid "Select exisiting colorset options for this theme." +msgstr "Seleccione las opciones de conjuntos de colores existentes para este tema" + +#: +msgid "Select exisiting iconset options for this theme." +msgstr "Seleccione las opciones de conjuntos de iconos existentes para este tema" + +#: +msgid "Select exisiting menu style options for this theme." +msgstr "Seleccione las opciones de estilo de menรบ existentes para este tema" + +#: +msgid "Select exisiting system view options for this theme." +msgstr "Seleccione las opciones de vista del sistema existentes para este tema" + +#: +msgid "Select exisiting gamelist view options for this theme." +msgstr "Seleccione las opciones de vista de lista de juegos existentes para este tema" + +#: +msgid "Configure theme options if available." +msgstr "Configurar opciones de tema si estรกn disponibles" + +#: +msgid "Select Region of logos, pictures for system that are different for some countries. E.g. Megadrive in EU / Genesis in US" +msgstr "Seleccionar la regiรณn de logotipos y las imรกgenes para sistemas que son diferentes para algunos paรญses. Por ejemplo, Megadrive en EU / Genesis en EE.UU." + +#: +msgid "Type the name of your SSID if it is hidden or not listed" +msgstr "Escriba el nombre de su SSID si estรก oculto o no aparece en la lista" + +#: +msgid "Select a letter and the listing will go directly on the first game starting with this letter." +msgstr "Seleccione una letra y el listado irรก directamente en el primer juego que comience con esta letra." + +#: +msgid "Select the way the game list is sortered (alphabetically, by notation...)." +msgstr "Seleccione la forma en que se clasifica la lista de juegos (alfabรฉticamente, por notaciรณn ...)." + +#: +msgid "Switch between seing or not only the favorites games. To add a game in the favorite list, select the game and toggle its state using 'Y'." +msgstr "Cambia entre ver o no sรณlo los juegos favoritos. Para agregar un juego en la lista de favoritos, selecciona el juego y cambia su estado usando 'Y'." + +#: +msgid "Switch between seing or not the hidden games. To hide a game, edit its data and select 'Hide'." +msgstr "Cambia entre ver o no los juegos ocultos. Para ocultar un juego, edita sus datos y selecciona 'Ocultar'." + +#: +msgid "This option display a menu which allows to change game data and many others options." +msgstr "Esta opciรณn muestra un menรบ que permite cambiar los datos del juego y muchas otras opciones." + +#: +msgid "AVAILABLE UPDATE" +msgstr "ACTUALIZACIร“N DISPONIBLE" + +#: +msgid "UPDATE CHANGELOG" +msgstr "ACTUALIZAR REGISTRO DE CAMBIOS" + +#: +msgid "CLOCK IN MENU" +msgstr "RELOJ EN EL MENรš" + +#: +msgid "Now playing" +msgstr "Jugando" + +#: +msgid "DEFAULT (%1%)" +msgstr "POR DEFECTO (%1%)" + +#: +msgid "INPUT REQUIRED" +msgstr "SE REQUIERE ENTRADA" + +#: +msgid "(skipped)" +msgstr "(saltado)" + +#: +msgid "UP/DOWN TO SKIP" +msgstr "ARRIBA / ABAJO PARA SALTAR" + +#: +msgid "A TO UNSET" +msgstr "A PARA CANCELAR" + +#: +msgid "DOWN TO SKIP AND KEEP [%1%]" +msgstr "ABAJO PARA SALTAR Y MANTENER [%1%]" + +#: +msgid "UP/DOWN TO SKIP AND KEEP [%1%]" +msgstr "ARRIBA / ABAJO PARA SALTAR Y MANTENER [%1%]" + +#: +msgid "Set duration of help popups, 0 means no popup." +msgstr "Establecer duraciรณn de ventanas emergentes de ayuda, 0 significa que no hay ventanas emergentes." + +#: +msgid "HELP POPUP DURATION" +msgstr "DURACIร“N VENTANA EMERGENTE DE AYUDA" + +#: +msgid "Set duration of music popups, 0 means no popup." +msgstr "Establecer duraciรณn de ventanas emergentes de mรบsica, 0 significa que no hay ventanas emergentes." + +#: +msgid "MUSIC POPUP DURATION" +msgstr "DURACIร“N VENTANA EMERGENTE DE MรšSICA" + +#: +msgid "POPUP SETTINGS" +msgstr "AJUSTES DE VENTANA EMERGENTES" + +#: +msgid "POPUP POSITION" +msgstr "POSICIร“N DE VENTANA EMERGENTES" + +#: +msgid "Select the position of popups on screen." +msgstr "Seleccione la posiciรณn de las ventanas emergentes en la pantalla" + +#: +msgid "Set position and duration of popups." +msgstr "Establecer posiciรณn y duraciรณn de las ventanas emergentes" + +#: +msgid "TOP/RIGHT" +msgstr "ARRIBA/DERECHA" + +#: +msgid "BAS/DROITE" +msgstr "INFERIOR/DERECHA" + +#: +msgid "BOTTOM/LEFT" +msgstr "ABAJO/IZQUIERDA" + +#: +msgid "TOP/LEFT" +msgstr "ARRIBA/IZQUIERDA" + +#: +msgid "SHOW FOLDERS CONTENT" +msgstr "MOSTRAR CONTENIDO DE CARPETAS" + +#: +msgid "Switch between seeing the folders structure and seeing all games in a flatten top level." +msgstr "Cambia entre ver la estructura de las carpetas y ver todos los juegos en un nivel superior." + +#: +msgid "NETPLAY" +msgstr "JUEGO EN RED" + +#: +msgid "NETPLAY SETTINGS" +msgstr "OPCIONES DE JUEGO EN RED" + +#: +msgid "NETPLAY LOBBY" +msgstr "LOBBY DE JUEGO EN RED" + +#: +msgid "Enable or disable Netplay in games." +msgstr "Habilitar o deshabilitar juego en red." + +#: +msgid "PORT" +msgstr "PUERTO" + +#: +msgid "NICKNAME" +msgstr "APODO" + +#: +msgid "RELAY SERVER" +msgstr "SERVIDOR DE RETRANSMISIร“N" + +#: +msgid "Enable or disable connections throught relay servers." +msgstr "Habilitar o deshabilitar conexiones a travรฉs de servidores de retransmisiรณn." + +#: +msgid "KODI/NETPLAY" +msgstr "KODI/JUEGO EN RED" + +#: +msgid "NO GAMES OR NO CONNECTION" +msgstr "NO HAY JUEGOS O SIN CONEXIร“N" + +#: +msgid "HASH NOW" +msgstr "MANTENER HASH" + +#: +msgid "HASH THESE SYSTEMS" +msgstr "CREAR HASH DE ESTOS SISTEMAS" + +#: +msgid "Add hash of roms in your gamelists to have more accurate results in Netplay." +msgstr Agrega hash de roms en tus listas de juego para tener resultados mรกs precisos en juego en red." + +#: +msgid "HASH ROMS" +msgstr "HASH DE ROMS" + +#: +msgid "Only missing hashs" +msgstr "Sรณlo faltan hashs" + +#: +msgid "Username" +msgstr "Nombre de usuario" + +#: +msgid "Country" +msgstr "Paรญs" + +#: +msgid "Latency" +msgstr "Latencia" + +#: +msgid "Host arch." +msgstr "Arco del host" + +#: +msgid "Core ver." +msgstr "Ver. de nรบcleo" + +#: +msgid "RA ver." +msgstr "Ver. de RA" + +#: +msgid "Can join" +msgstr "Jugable" + +#: +msgid "Rom and core match" +msgstr "Coincidencia de rom y nรบcleo" + +#: +msgid "Rom found" +msgstr "Se encontrรณ rom" + +#: +msgid "No rom match" +msgstr "No hay coincidencia de rom" + +#: +msgid "Match" +msgstr "Coincide" + +#: +msgid "No Match" +msgstr "No hay coincidencia." + +#: +msgid "Rom file" +msgstr "Archivo rom" + +#: +msgid "Rom hash" +msgstr "Hash de la rom" + +#: +msgid "THIS COULD TAKE A WHILE, CONFIRM?" +msgstr "ESTO PODRรA TOMAR UN TIEMPO, ยฟCONFIRMAR?" + +#: +msgid "bueno" +msgstr "bon" + +#: +msgid "bad" +msgstr "malO" + +#: +msgid "medium" +msgstr "medio" + +#: +msgid "NETPLAY POPUP DURATION" +msgstr "DURACIร“N DE LA VENTANA EMER. JUEGO EN RED" + +#: +msgid "Set duration of netplay popups, 0 means no popup." +msgstr "Establecer duraciรณn de las ventanas emergentes de netplay, 0 significa que no hay ventanas emergentes." + +#: +msgid "Player" +msgstr "Jugador" + +#: +msgid "Game" +msgstr "Juego" + +#: +msgid "ยกJuega en lรญnea en juegos que se ejecutan en Retroarch como NES, SNES, FBA, Genesis / Megadrive y mรกs!" +msgstr "Jouez en ligne ร  des jeux tournant sous Retroarch comme NES, SNES, FBA, Megadrive et bien plus!" + +#: +msgid "Rom, hash and core match" +msgstr "Rom, hash y coincidencia de nรบcleo" + +#: +msgid "No core match" +msgstr "No hay coincidencia de nรบcleo" + +#: +msgid "Add a clock in the main menu." +msgstr "Agregar un reloj en el menรบ principal" + +#: +msgid "UPGRADING" +msgstr "ACTUALIZANDO" + +#: +msgid "PREPARING" +msgstr "PREPARANDO" + +#: +msgid "Starting UI" +msgstr "Inicializaciรณn interfaz" + +#: +msgid "VERIFYING" +msgstr "VERIFICANDO" + +#: +msgid "EMPTY LIST" +msgstr "LISTA VACรA" + + +#: Retroarch ratio +msgid "Auto" +msgstr "Automรกtico" + +msgid "Square pixel" +msgstr "Pรญxel cuadrado" + +msgid "Square pixel" +msgstr "Pixel carrรฉ" + +msgid "Retroarch Config" +msgstr "Retroarch Configuraciรณn'" + +msgid "Retroarch Custom" +msgstr "Retroarch Customizaciรณn'" + +msgid "Core provided" +msgstr "Nรบcleo proporcionado" + +msgid "Do not set" +msgstr "No establecer" diff --git a/resources/locale/fr/emulationstation2.po b/resources/locale/fr/emulationstation2.po new file mode 100644 index 0000000000..2de0a7c5f5 --- /dev/null +++ b/resources/locale/fr/emulationstation2.po @@ -0,0 +1,2272 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: emulationstation\n" +"Language: fr\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: +msgid "EMULATOR SETTINGS" +msgstr "PARAMรˆTRES DES EMULATEURS" + +#: +msgid "LOG LEVEL" +msgstr "NIVEAU DE LOG" + +#: +msgid "LOWER MUSIC WHEN PLAYING VIDEO" +msgstr "BAISSER LA MUSIQUE EN LECTURE VIDEO" + +#: +msgid "PLAY THEME MUSICS" +msgstr "JOUER LES MUSIQUES DES THEMES" + +#: +msgid "DISPLAY SONG TITLES" +msgstr "AFFICHER LES NOMS DES MUSIQUES" + +#: +msgid "SHOW FAVORITES ON TOP" +msgstr "AFFICHER LES FAVORIS EN PREMIER" + +#: +msgid "PREFERED IMAGE SOURCE" +msgstr "SOURCE D'IMAGE PREFERร‰E" + +#: +msgid "PREFERED THUMBNAIL SOURCE" +msgstr "SOURCE DE MINIATURE PRร‰Fร‰Rร‰E" + +#: +msgid "SELECT THEME TO INSTALL" +msgstr "CHOIX DU THEME A INSTALLER" + +#: +msgid "'%s' ADDED TO DOWNLOAD QUEUE" +msgstr "'%s' SERA PROCHAINEMENT TELร‰CHARGร‰" + +#: +msgid "'%s' ADDED TO DOWNLOAD QUEUE" +msgstr "'%s' SERA PROCHAINEMENT TELร‰CHARGร‰" + +#: +msgid "SCRAPE MARQUEE" +msgstr "SCRAPER LES LOGOS DE JEU" + +#, c-format +msgid "%i SELECTED" +msgid_plural "%i SELECTED" +msgstr[0] "%i Sร‰LECTIONNร‰" +msgstr[1] "%i Sร‰LECTIONNร‰S" + +#: +msgid "VIDEO SCREENSAVER SETTINGS" +msgstr "PARAMรˆTRES ร‰CRAN DE VEILLE VIDร‰O" + +#: +msgid "SLIDESHOW SCREENSAVER SETTINGS" +msgstr "PARAMรˆTRES ร‰CRAN DE VEILLE SLIDESHOW" + +#: +msgid "SCREENSAVER CONTROLS" +msgstr "CONTRร”LE DE l'ร‰CONOMISEUR D'ร‰CRAN" + +#: +msgid "All Games" +msgstr "Tous les jeux" + +#: +msgid "DOWNLOADING" +msgstr "Tร‰Lร‰CHARGEMENT" + +#: +msgid "GROUPED SYSTEMS" +msgstr "SYSTรˆMES ร€ FUSIONNER" + +#: +msgid "THEME INSTALLED SUCCESSFULLY" +msgstr "THEME INSTALLร‰" + +#: +msgid "Downloading" +msgstr "Telechargement" + +#: +msgid "Extracting" +msgstr "Extraction" + +#: +msgid "Only missing medias" +msgstr "Seulement les mรฉdias manquants" + +#: +msgid "always" +msgstr "toujours" + +#: +msgid "start & end" +msgstr "dรฉbut et fin" + +#: +msgid "never" +msgstr "jamais" + +#: +msgid "SCRAPE VIDEOS" +msgstr "SCRAPER LES VIDร‰OS" + +#: +msgid "RESET GAMELIST CUSTOMISATIONS" +msgstr "Rร‰INITIALISER LES PERSONNALISATIONS PAR SYSTรˆME" + +#: +msgid "RESET" +msgstr "Rร‰INITIALISER" + +#: +msgid "SHOW FOLDERS" +msgstr "AFFICHER LES DOSSIERS" + +#: +msgid "ARE YOU SURE YOU WANT TO CONFIGURE INPUT?" +msgstr "VOULEZ-VOUS MODIFIER LA CONFIGURATION DES MANETTES ?" + +#: +msgid "disabled" +msgstr "dรฉsactivรฉ" + +#: +msgid "default" +msgstr "par dรฉfaut" + +#: +msgid "warning" +msgstr "avertissement" + +#: +msgid "error" +msgstr "erreur" + +#: +msgid "SWAP IMAGE AFTER (SECS)" +msgstr "CHANGER L'IMAGE APRรˆS (SECS)" + +#: +msgid "SHOW GAME NAME" +msgstr "AFFICHER LE NOM DU JEU" + +#: +msgid "STRETCH IMAGES" +msgstr "ร‰TIRER LES IMAGES" + +#: +msgid "USE CUSTOM IMAGES" +msgstr "UTILISER DES IMAGES PERSONNALISร‰ES" + +#: +msgid "CUSTOM IMAGE DIR" +msgstr "Rร‰PERTOIRE D'IMAGES PERSONNALISร‰ES" + +#: +msgid "CUSTOM IMAGE DIR RECURSIVE" +msgstr "RECHERCHER Rร‰CURSIVEMENT DANS LE Rร‰PERTOIRE" + +#: +msgid "CUSTOM IMAGE FILTER" +msgstr "FILTRE SUR LES IMAGES" + +#: +msgid "SWAP VIDEO AFTER (SECS)" +msgstr "CHANGER LA VIDร‰O APRรˆS (SECS)" + +#: +msgid "USE OMX PLAYER FOR SCREENSAVER" +msgstr "UTILISER OMX POUR L'ร‰CRAN DE VEILLE" + +#: +msgid "SHOW GAME INFO" +msgstr "AFFICHER LES INFORMATIONS DU JEU" + +#: +msgid "SHOW GAME INFO ON SCREENSAVER" +msgstr "AFFICHER LES INFORMATIONS DU JEU" + +#: +msgid "USE MARQUEE AS GAME INFO" +msgstr "AFFICHER LE LOGO DU JEU" + +#: +msgid "STRETCH VIDEO ON SCREENSAVER" +msgstr "ร‰TIRER LA VIDร‰O DE L'ร‰CRAN DE VEILLE" + +#: +msgid "This collection is empty" +msgstr "Cette collection est vide" + +#: +msgid "None" +msgstr "Aucun" + +#: +msgid "Various" +msgstr "Multiple" + +#: +msgid "among other titles." +msgstr "d'autres jeux" + +#: +msgid "This collection contains" +msgstr "Cette collection contient" + +#: +msgid "games, including" +msgstr "jeux, incluant " + +#: +msgid "Loading..." +msgstr "Chargement en cours" + +#: +msgid "Preloading UI" +msgstr "Chargement de l'interface" + +#: +msgid "ADD/REMOVE GAMES TO THIS GAME COLLECTION" +msgstr "Ajouter/Retirer des jeux ร  cette collection" + +#: +msgid "FINISH EDITING" +msgstr "Terminer l'รฉdition" + +#: +msgid "GRID SIZE" +msgstr "TAILLE DE LA GRILLE" + +#: +msgid "DEFAULT GRID SIZE" +msgstr "TAILLE DE LA GRILLE PAR Dร‰FAUT" + +#: +msgid "UI MODE" +msgstr "TYPE D'INTERFACE" + +#: +msgid "PRELOAD UI" +msgstr "PRECHARGEMENT DE L'INTERFACE" + +#: +msgid "VSYNC" +msgstr "SYNCHRONISATION VERTICALE" + +#: +msgid "DISPLAY FAVORITES FIRST IN GAMELIST" +msgstr "AFFICHER LES FAVORIS EN PREMIER" + +#: +msgid "SHOW SYSTEM NAME IN COLLECTIONS" +msgstr "AFFICHER LE NOM DU SYSTรˆME DANS LES COLLECTIONS" + +#: +msgid "OPTIMIZE IMAGES VRAM USE" +msgstr "OPTIMISER LE BESOIN Mร‰MOIRE DES IMAGES" + +#: +msgid "Full" +msgstr "Complet" + +#: +msgid "Kiosk" +msgstr "Kiosque" + +#: +msgid "Kid" +msgstr "Enfant" + +#: +msgid "GAME COLLECTION SETTINGS" +msgstr "LISTES PERSONNALISร‰ES" + +#: +msgid "CANCEL" +msgstr "ANNULER" + +#: +msgid "automatic" +msgstr "automatique" + +#: +msgid "basic" +msgstr "basique" + +#: +msgid "detailed" +msgstr "dรฉtaillรฉe" + +#: +msgid "grid" +msgstr "grille" + +#: +msgid "fade" +msgstr "fondu" + +#: +msgid "instant" +msgstr "instantanรฉe" + +#: +msgid "slide" +msgstr "animรฉe" + +#: +msgid "gridex" +msgstr "grille dรฉtaillรฉe" + +#: +msgid "ENABLE NAVIGATION SOUNDS" +msgstr "SONS LORS DE LA NAVIGATION" + +#: +msgid "ENABLE VIDEO AUDIO" +msgstr "SON DES VIDEOS" + +#: +msgid "APPLY FILTER" +msgstr "FILTER LES JEUX" + +#: +msgid "AUTOMATIC GAME COLLECTIONS" +msgstr "COLLECTIONS AUTOMATIQUES" + +#: +msgid "SELECT COLLECTIONS" +msgstr "Sร‰LECTIONNER LES COLLECTIONS" + +#: +msgid "CUSTOM GAME COLLECTIONS" +msgstr "COLLECTIONS PERSONNALISEES" + +#: +msgid "CREATE NEW CUSTOM COLLECTION FROM THEME" +msgstr "CRร‰ER UNE COLLECTION A PARTIR D'UN THEME" + +#: +msgid "GAME COLLECTION SETTINGS" +msgstr "PARAMรˆTRES DES LISTES DE JEUX" + +#: +msgid "SELECT THEME FOLDER" +msgstr "Sร‰LECTIONNEZ LE DOSSIER" + +#: +msgid "CREATE NEW CUSTOM COLLECTION" +msgstr "CREER UNE COLLECTION PERSONNALISร‰E" + +#: +msgid "New Collection Name" +msgstr "Nom de la collection" + +#: +msgid "Loading..." +msgstr "Chargement en cours..." + +#: +msgid "GROUP UNTHEMED CUSTOM COLLECTIONS" +msgstr "GROUPER LES COLLECTIONS SANS THEME" + +#: +msgid "SORT CUSTOM COLLECTIONS AND SYSTEMS" +msgstr "TRIER LES COLLECTIONS ET LES SYSTรˆMES" + +#: +msgid "FINISH EDITING COLLECTION" +msgstr "TERMINER L'ร‰DITION DE LA COLLECTION" + +#: +msgid "PARSE GAMESLISTS ONLY" +msgstr "LISTES DE JEUX XML UNIQUEMENT" + +#: +msgid "POWER SAVER MODES" +msgstr "MODE D'ECONOMIE D'ENERGIE" + +#: +msgid "SAVE METADATA ON EXIT" +msgstr "SAUVER LES DONNร‰ES EN QUITTANT" + +#: +msgid "SHOW HIDDEN FILES" +msgstr "AFFICHER LES FICHIERS CACHร‰S" + +#: +msgid "VRAM LIMIT" +msgstr "Mร‰MOIRE VIDร‰O MAX" + +#: +msgid "RESET ALL FILTERS" +msgstr "Rร‰INITIALISER TOUS LES FILTRES" + +#: +msgid "GAMELIST VIEW STYLE" +msgstr "STYLE D'AFFICHAGE" + +#: +msgid "GAME LAUNCH TRANSITION" +msgstr "TRANSITION AU LANCEMENT D'UN JEU" + +#: +msgid "THREADED LOADING" +msgstr "Chargement multitรขche" + +#: +msgid "RANDOM" +msgstr "ALร‰ATOIRE" + +#: +msgid "Rating" +msgstr "Note" + +#: +msgid "Released" +msgstr "Sortie" + +#: +msgid "Developer" +msgstr "Dรฉveloppeur" + +#: +msgid "Publisher" +msgstr "ร‰diteur" + +#: +msgid "Genre" +msgstr "Genre" + +#: +msgid "Players" +msgstr "Joueurs" + +#: +msgid "NO GAMES FOUND - SKIP" +msgstr "AUCUN JEU CORRESPONDANT - PASSER" + +#: +msgid "RETRY" +msgstr "RECOMMENCER" + +#: +msgid "SKIP" +msgstr "PASSER" + +#: +msgid "SEARCH FOR" +msgstr "RECHERCHER" + +#: +msgid "SEARCH" +msgstr "RECHERCHER" + +#: +msgid "SCRAPING IN PROGRESS" +msgstr "SCRAPING EN COURS" + +#: +msgid "SYSTEM" +msgstr "SYSTรˆME" + +#: +msgid "subtitle text" +msgstr "sous-titre" + +#: +msgid "INPUT" +msgstr "ร‰DITER" + +#: +msgid "search" +msgstr "rechercher" + +#: +msgid "STOP" +msgstr "ARRรŠTER" + +#: +msgid "stop (progress saved)" +msgstr "stop (progression sauvegardรฉe)" + +#: +msgid "GAME %i OF %i" +msgstr "JEU %i SUR %i" + +#: +msgid "WE CAN'T FIND ANY SYSTEMS!\n" +"CHECK THAT YOUR PATHS ARE CORRECT IN THE SYSTEMS CONFIGURATION FILE, AND YOUR GAME DIRECTORY HAS AT LEAST ONE GAME WITH THE CORRECT EXTENSION.\n" +"\n" +"VISIT EMULATIONSTATION.ORG FOR MORE INFORMATION." +msgstr "AUCUN JEU N'A ร‰Tร‰ TROUVร‰ SUR LE SYSTรˆME !\n" +"VEUILLEZ AJOUTER DES JEUX EN SUIVANT LES INSTRUCTIONS DE LA NOTICE.\n" +"\n" +"RENDEZ-VOUS SUR EMULATIONSTATION.ORG POUR PLUS D'INFORMATIONS" + +#: +msgid "%i GAME SUCCESSFULLY SCRAPED!" +msgid_plural "%i GAMES SUCCESSFULLY SCRAPED!" +msgstr[0] "%i JEU SCRAPPร‰ !" +msgstr[1] "%i JEUX SCRAPPร‰S !" + +#: +msgid "%i GAME SKIPPED." +msgid_plural "%i GAMES SKIPPED." +msgstr[0] "%i JEU PASSร‰." +msgstr[1] "%i JEUX PASSร‰S." + +#: +msgid "OK" +msgstr "OK" + +#: +msgid "EDIT METADATA" +msgstr "ร‰DITER LES DONNร‰ES" + +#: +msgid "SCRAPE" +msgstr "SCRAPPER" + +#: +msgid "SAVE" +msgstr "SAUVEGARDER" + +#: +msgid "THIS WILL DELETE A FILE!\n" +"ARE YOU SURE?" +msgstr "CELA VA EFFACER LE FICHIER\n" +"รŠTES-VOUS SUR ?" + +#: +msgid "YES" +msgstr "OUI" + +#: +msgid "NO" +msgstr "NON" + +#: +msgid "DELETE" +msgstr "SUPPRIMER" + +#: +msgid "SAVE CHANGES?" +msgstr "SAUVEGARDER ?" + +#: +msgid "BACK" +msgstr "RETOUR" + +#: +msgid "go back" +msgstr "RETOUR" + +#: +msgid "CLOSE" +msgstr "FERMER" + +#: +msgid "MAIN MENU" +msgstr "MENU PRINCIPAL" + +#: +msgid "KODI MEDIA CENTER" +msgstr "KODI MEDIA CENTER" + +#: +msgid "SYSTEM SETTINGS" +msgstr "OPTIONS SYSTรˆME" + +#: +msgid "VERSION" +msgstr "VERSION" + +#: +msgid "DISK USAGE" +msgstr "ESPACE DISQUE" + +#: +msgid "STORAGE DEVICE" +msgstr "Mร‰DIA DE STOCKAGE" + +#: +msgid "LANGUAGE" +msgstr "LANGUE" + +#: +msgid "OVERCLOCK" +msgstr "OVERCLOCK" + +#: +msgid "EXTREM (1100Mhz)" +msgstr "EXTREM (1100Mhz)" + +#: +msgid "TURBO (1000Mhz)" +msgstr "TURBO (1000Mhz)" + +#: +msgid "HIGH (950Mhz)" +msgstr "HIGH (950Mhz)" + +#: +msgid "NONE (700Mhz)" +msgstr "AUCUN (700Mhz)" + +#: +msgid "TURBO (1050Mhz)+" +msgstr "TURBO (1050Mhz)+" + +#: +msgid "HIGH (1050Mhz)" +msgstr "HIGH (1050Mhz)" + +#: +msgid "NONE (900Mhz)" +msgstr "AUCUN (900Mhz)" + +#: +msgid "NONE (1200Mhz)" +msgstr "AUCUN (1200Mhz)" + +#: +msgid "NONE" +msgstr "AUCUN" + +#. NEW SETTINGS ORGANIZATION +#: +msgid "UPDATES" +msgstr "MISES ร€ JOUR" + +#: +msgid "AUTO UPDATES" +msgstr "MISES ร€ JOUR AUTO" + +#: +msgid "START UPDATE" +msgstr "LANCER LA MISE ร€ JOUR" + +#: +msgid "KODI SETTINGS" +msgstr "OPTIONS KODI" + +#: +msgid "ENABLE KODI" +msgstr "ACTIVER KODI" + +#: +msgid "KODI AT START" +msgstr "AUTO Dร‰MARRAGE DE KODI" + +#: +msgid "START KODI WITH X" +msgstr "X POUR LANCER KODI" + +#: +msgid "SECURITY" +msgstr "Sร‰CURITร‰" + +#: +msgid "ENFORCE SECURITY" +msgstr "RENFORCER LA Sร‰CURITร‰" + +#: +msgid "ROOT PASSWORD" +msgstr "MOT DE PASSE ROOT" + +#: +msgid "THE SYSTEM WILL NOW REBOOT" +msgstr "LE SYSTรˆME VA REDร‰MARRER" + +#: +msgid "GAMES SETTINGS" +msgstr "OPTIONS DES JEUX" + +#: +msgid "GAME RATIO" +msgstr "FORMAT JEUX" + +#: +msgid "SMOOTH GAMES" +msgstr "LISSER LES JEUX" + +#: +msgid "REWIND" +msgstr "REMBOBINAGE" + +#: +msgid "AUTO SAVE/LOAD" +msgstr "SAUVEGARDE/CHARGEMENT AUTOMATIQUE" + +#: +msgid "SHADERS SET" +msgstr "SHADERS SET" + +#: +msgid "SCANLINES" +msgstr "SCANLINES" + +#: +msgid "RETRO" +msgstr "RETRO" + +#: +msgid "RETROACHIEVEMENTS SETTINGS" +msgstr "OPTIONS DE RETROACHIEVEMENTS" + +#: +msgid "RETROACHIEVEMENTS" +msgstr "RETROACHIEVEMENTS" + +#: +msgid "HARDCORE MODE" +msgstr "MODE HARDCORE" + +#: +msgid "USERNAME" +msgstr "NOM UTILISATEUR" + +#: +msgid "PASSWORD" +msgstr "MOT DE PASSE" + +#: +msgid "ADVANCED" +msgstr "AVANCร‰ES" + +#: +msgid "REALLY UPDATE GAMES LISTS ?" +msgstr "CONFIRMER LA MISE ร€ JOUR DES LISTES DES JEUX ?" + +#: +msgid "UPDATE GAMES LISTS" +msgstr "METTRE ร€ JOUR LES LISTES DES JEUX" + +#: +msgid "CONTROLLERS SETTINGS" +msgstr "OPTIONS MANETTES" + +#: +msgid "UI SETTINGS" +msgstr "OPTIONS DE L'INTERFACE" + +#: +msgid "OVERSCAN" +msgstr "OVERSCAN" + +#: +msgid "LAUNCH SCREENSAVER" +msgstr "ร‰CRAN DE VEILLE" + +#: +msgid "SCREENSAVER SETTINGS" +msgstr "ร‰CRAN DE VEILLE" + +#: +msgid "SCREENSAVER AFTER" +msgstr "ร‰CRAN DE VEILLE APRรˆS" + +#: +msgid "TRANSITION STYLE" +msgstr "STYLE DE LA TRANSITION" + +#: +msgid "SCREENSAVER BEHAVIOR" +msgstr "TYPE D'ร‰CRAN DE VEILLE" + +#: +msgid "random video" +msgstr "vidรฉo" + +#: +msgid "slideshow" +msgstr "slideshow" + +#: +msgid "dim" +msgstr "grisรฉ" + +#: +msgid "black" +msgstr "noir" + +#: +msgid "SHOW FRAMERATE" +msgstr "AFFICHER LE FRAMERATE" + +#: +msgid "ON-SCREEN HELP" +msgstr "AFFICHAGE DE L'AIDE" + +#: +msgid "HIDE WHEN RUNNING GAME" +msgstr "MASQUER AU LANCEMENT D'UN JEU" + +#: +msgid "QUICK SYSTEM SELECT" +msgstr "Sร‰LECTION RAPIDE DU SYSTรˆME" + +#: +msgid "THEME SET" +msgstr "THรˆME" + +#: +msgid "SOUND SETTINGS" +msgstr "OPTIONS DU SON" + +#: +msgid "SYSTEM VOLUME" +msgstr "VOLUME DU SYSTรˆME" + +#: +msgid "FRONTEND MUSIC" +msgstr "MUSIQUE DE FOND" + +#: +msgid "ONLY PLAY SYSTEM-SPECIFIC MUSIC FOLDER" +msgstr "MUSIQUES DES SYSTรˆMES UNIQUEMENT" + +#: +msgid "FILTER GAMES BY TEXT" +msgstr "RECHERCHE TEXTUELLE" + +#: +msgid "OUTPUT DEVICE" +msgstr "SORTIE AUDIO" + +#: +msgid "HDMI" +msgstr "HDMI" + +#: +msgid "JACK" +msgstr "JACK" + +#: +msgid "AUTO" +msgstr "AUTO" + +#: +msgid "NETWORK SETTINGS" +msgstr "OPTIONS Rร‰SEAU" + +#: +msgid "CONNECTED" +msgstr "CONNECTร‰" + +#: +msgid "NOT CONNECTED" +msgstr "Dร‰CONNECTร‰" + +#: +msgid "STATUS" +msgstr "STATUT" + +#: +msgid "IP ADDRESS" +msgstr "ADRESSE IP" + +#: +msgid "HOSTNAME" +msgstr "NOM SUR LE Rร‰SEAU" + +#: +msgid "ENABLE WIFI" +msgstr "ACTIVER LE WIFI" + +#: +msgid "WIFI SSID" +msgstr "SSID WIFI" + +#: +msgid "WIFI KEY" +msgstr "CLร‰ WIFI" + +#: +msgid "WIFI ENABLED" +msgstr "WIFI ACTIVร‰" + +#: +msgid "WIFI CONFIGURATION ERROR" +msgstr "ERREUR DE CONFIGURATION WIFI" + +#: +msgid "SCRAPER" +msgstr "SCRAPPEUR" + +#: +msgid "SCRAPE FROM" +msgstr "SCRAPPER DEPUIS" + +#: +msgid "SCRAPE RATINGS" +msgstr "SCRAPPER LES NOTES" + +#: +msgid "SCRAPE NOW" +msgstr "SCRAPPER MAINTENANT" + +#: +msgid "QUIT" +msgstr "QUITTER" + +#: +msgid "QUIT EMULATIONSTATION" +msgstr "QUITTER EMULATIONSTATION" + +#: +msgid "RESTART EMULATIONSTATION" +msgstr "REDEMARRER EMULATIONSTATION" + +#: +msgid "REALLY QUIT?" +msgstr "ETES-VOUS SUR DE VOULOIR QUITTER ?" + +#: +msgid "REALLY RESTART?" +msgstr "ETES-VOUS SUR DE VOULOIR REDร‰MARRER ?" + +#: +msgid "RESTART SYSTEM" +msgstr "REDร‰MARRER" + +#: +msgid "REALLY SHUTDOWN?" +msgstr "ETES-VOUS SUR DE VOULOIR ร‰TEINDRE ?" + +#: +msgid "SHUTDOWN SYSTEM" +msgstr "ร‰TEINDRE" + +#: +msgid "Emulator" +msgstr "ร‰mulateur" + +#: +msgid "Core" +msgstr "Core" + +#: +msgid "YOU ARE GOING TO CONFIGURE A CONTROLLER. IF YOU HAVE ONLY ONE JOYSTICK, CONFIGURE THE DIRECTIONS KEYS AND SKIP JOYSTICK CONFIG BY HOLDING A BUTTON. IF YOU DO NOT HAVE A SPECIAL KEY FOR HOTKEY, CHOOSE THE SELECT BUTTON. SKIP ALL BUTTONS YOU DO NOT HAVE BY HOLDING A KEY. BUTTONS NAMES ARE BASED ON THE SNES CONTROLLER." +msgstr "VOUS ALLEZ CONFIGURER UNE MANETTE. SI VOUS N'AVEZ QU'UN JOYSTICK OU JUSTE UNE CROIX DIRECTIONNELLE, CONFIGUREZ SEULEMENT LES DIRECTIONS ET PASSEZ LES JOYSTICKS EN RESTANT APPUYร‰ SUR UN BOUTON. SI VOUS N'AVEZ PAS DE BOUTON SPECIAL POUR HOTKEY, UTILISEZ SELECT. IGNOREZ UN BOUTON QUE VOTRE MANETTE N'A PAS EN RESTANT APPUYร‰ SUR UN BOUTON Dร‰Jร€ CONFIGURร‰. LES NOMS DES BOUTONS SONT BASร‰S SUR LA SUPER NINTENDO" + +#. GUIMENU +#: +msgid "CONFIGURE A CONTROLLER" +msgstr "CONFIGURER UNE MANETTE" + +#. Bluetooth +#: +msgid "CONTROLLER PAIRED" +msgstr "MANETTE ASSOCIร‰E" + +#: +msgid "UNABLE TO PAIR CONTROLLER" +msgstr "IMPOSSIBLE D'ASSOCIER LA MANETTE" + +#: +msgid "AN ERROR OCCURED" +msgstr "UNE ERREUR EST SURVENUE" + +#: +msgid "NO CONTROLLERS FOUND" +msgstr "AUCUNE MANETTE Dร‰TECTร‰E" + +#: +msgid "PAIR A BLUETOOTH CONTROLLER" +msgstr "ASSOCIER UNE MANETTE BLUETOOTH" + +#: +msgid "CONTROLLERS LINKS HAVE BEEN DELETED." +msgstr "LES LIENS VERS LES MANETTES ONT ร‰Tร‰ SUPPRIMร‰S." + +#: +msgid "FORGET BLUETOOTH CONTROLLERS" +msgstr "OUBLIER LES MANETTES BLUETOOTH" + +#: +msgid "INPUT P%i" +msgstr "JOUEUR %i" + +#: +msgid "CHOOSE" +msgstr "CHOISIR" + +#: +msgid "SELECT" +msgstr "Sร‰LECTIONNER" + +#: +msgid "OPTIONS" +msgstr "OPTIONS" + +#: +msgid "JUMP TO..." +msgstr "ALLER ร€..." + +#: +msgid "SORT GAMES BY" +msgstr "TRIER LES JEUX PAR" + +#. FAVORITES +#: +msgid "FAVORITES ONLY" +msgstr "FAVORIS SEULEMENT" + +#: +msgid "EDIT THIS GAME'S METADATA" +msgstr "ร‰DITER LES DONNร‰ES DU JEU" + +#: +msgid "SCRAPE THESE GAMES" +msgstr "SCRAPPER CES JEUX" + +#: +msgid "All Games" +msgstr "Tous les jeux" + +#. MISSING SCRAPPER TRANSLATIONS +#: +msgid "Only missing image" +msgstr "Seulement images manquantes" + +#: +msgid "FILTER" +msgstr "FILTRE" + +#: +msgid "SCRAPE THESE SYSTEMS" +msgstr "SCRAPPER CES SYSTรˆMES" + +#: +msgid "SYSTEMS" +msgstr "SYSTรˆMES" + +#: +msgid "USER DECIDES ON CONFLICTS" +msgstr "CHOISIR SI CONFLIT" + +#: +msgid "START" +msgstr "Dร‰MARRER" + +#: +msgid "WARNING: SOME OF YOUR SELECTED SYSTEMS DO NOT HAVE A PLATFORM SET. RESULTS MAY BE EVEN MORE INACCURATE THAN USUAL!\n" +"CONTINUE ANYWAY?" +msgstr "ATTENTION: CERTAINS DE VOS SYSTรˆMES N'ONT PAS DE PLATEFORME. LES Rร‰SULTATS POURRAIENT รŠTRE INATTENDUS!\n" +"CONTINUER QUAND MรŠME?" + +#: +msgid "NO GAMES FIT THAT CRITERIA." +msgstr "AUCUN JEU NE CORRESPOND." + +#: +msgid "REALLY UPDATE?" +msgstr "METTRE ร€ JOUR LE SYSTรˆME ?" + +#: +msgid "NETWORK CONNECTION NEEDED" +msgstr "VEUILLEZ BRANCHER UN Cร‚BLE Rร‰SEAU" + +#: +msgid "UPDATE DOWNLOADED, THE SYSTEM WILL NOW REBOOT" +msgstr "MISE ร€ JOUR Tร‰Lร‰CHARGร‰E, REDร‰MARRAGE EN COURS" + +#: +msgid "UPDATE FAILED, THE SYSTEM WILL NOW REBOOT" +msgstr "ร‰CHEC DE LA MISE ร€ JOUR" + +#: +msgid "START UPDATE" +msgstr "TELร‰CHARGER LA MISE ร€ JOUR" + +#: +msgid "APPLY UPDATE" +msgstr "APPLIQUER LA MISE ร€ JOUR" + +#: +msgid "DOWNLOADS AND UPDATES" +msgstr "TELร‰CHARGEMENTS ET MISES ร€ JOUR" + +#: +msgid "THEME INSTALLER" +msgstr "TELร‰CHARGER DES THEMES" + +#: +msgid "IMAGE SOURCE" +msgstr "TYPE D'IMAGE" + +#: +msgid "BOX SOURCE" +msgstr "TYPE DE BOITE" + +#: +msgid "LOGO SOURCE" +msgstr "TYPE DE LOGO" + +#: +msgid "SHOW FILENAMES IN LISTS" +msgstr "AFFICHER LES NOMS DE FICHIER" + +#: +msgid "SHOW CLOCK" +msgstr "AFFICHER L'HEURE" + +#: +msgid "NO UPDATE AVAILABLE" +msgstr "AUCUNE MISE ร€ JOUR DISPONIBLE" + +#: +msgid "enter emulator" +msgstr "entrer l'รฉmulateur" + +#: +msgid "enter core" +msgstr "entrer le core" + +#: +msgid "Ratio" +msgstr "Ratio" + +#: +msgid "enter ratio" +msgstr "entrer le ratio" + +#: +msgid "Name" +msgstr "Nom" + +#: +msgid "enter game name" +msgstr "entrer le nom du jeu" + +#: +msgid "Description" +msgstr "Description" + +#: +msgid "enter description" +msgstr "entrer la description" + +#: +msgid "Image" +msgstr "Image" + +#: +msgid "enter path to image" +msgstr "entrer le chemin vers l'image" + +#: +msgid "Thumbnail" +msgstr "Miniature" + +#: +msgid "enter path to thumbnail" +msgstr "entrer le chemin vers la miniature" + +#: +msgid "enter rating" +msgstr "entrer les votes" + +#: +msgid "Release date" +msgstr "Date de sortie" + +#: +msgid "enter release date" +msgstr "entrer la date de sortie" + +#: +msgid "enter game developer" +msgstr "entrer le dรฉveloppeur du jeu" + +#: +msgid "enter game publisher" +msgstr "entrer l'รฉditeur du jeu" + +#: +msgid "enter game genre" +msgstr "entrer le genre du jeu" + +#: +msgid "enter number of players" +msgstr "entrer le nombre de joueurs" + +#: +msgid "Favorite" +msgstr "Favori" + +#: +msgid "enter favorite" +msgstr "entrer le favori" + +#: +msgid "Region" +msgstr "Rรฉgion" + +#: +msgid "enter region" +msgstr "entrer la rรฉgion" + +#: +msgid "Romtype" +msgstr "Type de rom" + +#: +msgid "enter romtype" +msgstr "entre le type de rom" + +#: +msgid "Hidden" +msgstr "Invisible" + +#: +msgid "HIDDEN" +msgstr "CACHร‰" + +#: +msgid "NAME" +msgstr "NOM" + +#: +msgid "EMULATOR" +msgstr "EMULATEUR" + +#: +msgid "FAVORITE" +msgstr "FAVORI" + +#: +msgid "set hidden" +msgstr "Mettre cachรฉ" + +#: +msgid "Play count" +msgstr "Nb parties" + +#: +msgid "enter number of times played" +msgstr "entrer le nombre de parties" + +#: +msgid "Last played" +msgstr "Dernier jeu" + +#: +msgid "enter last played date" +msgstr "entrer la date de la derniรจre partie" + +#: +msgid "GAMES AVAILABLE" +msgstr "JEUX DISPONIBLES" + +#: +msgid "%i GAME AVAILABLE" +msgid_plural "%i GAMES AVAILABLE" +msgstr[0] "%i JEU DISPONIBLE" +msgstr[1] "%i JEUX DISPONIBLES" + +#: +msgid "%i FAVORITE" +msgid_plural "%i FAVORITES" +msgstr[0] "%i FAVORI" +msgstr[1] "%i FAVORIS" + +#: +msgid "%i GAME SUCCESSFULLY SCRAPED!" +msgid_plural "%i GAMES SUCCESSFULLY SCRAPED!" +msgstr[0] "%i JEU SCRAPPร‰ !" +msgstr[1] "%i JEUX SCRAPPร‰S !" + +#: +msgid "SCROLL" +msgstr "SCROLLER" + +#: +msgid "LAUNCH" +msgstr "LANCER" + +#: +msgid "Times played" +msgstr "Nb parties" + +#: +msgid "MENU" +msgstr "MENU" + +#: +msgid "FILENAME, ASCENDING" +msgstr "FICHIER, ASCENDANT" + +#: +msgid "FILENAME, DESCENDING" +msgstr "FICHIER, DESCENDANT" + +#: +msgid "RATING, ASCENDING" +msgstr "VOTES, ASCENDANT" + +#: +msgid "RATING, DESCENDING" +msgstr "VOTES, DESCENDANT" + +#: +msgid "TIMES PLAYED, ASCENDING" +msgstr "PARTIES JOUร‰ES, ASCENDANT" + +#: +msgid "TIMES PLAYED, DESCENDING" +msgstr "PARTIES JOUร‰ES, DESCENDANT" + +#: +msgid "LAST PLAYED, ASCENDING" +msgstr "DERNIรˆRE PARTIE, ASCENDANT" + +#: +msgid "LAST PLAYED, DESCENDING" +msgstr "DERNIรˆRE PARTIE, DESCENDANT" + +#: +msgid "WORKING..." +msgstr "EN COURS..." + +#: +msgid "CHANGE" +msgstr "CHANGER" + +#: +msgid "never" +msgstr "jamais" + +#: +msgid "just now" +msgstr "ร  l'instant" + +#: +msgid "%i sec ago" +msgid_plural "%i secs ago" +msgstr[0] "il y a %i sec" +msgstr[1] "il y a %i secs" + +#: +msgid "%i min ago" +msgid_plural "%i mins ago" +msgstr[0] "il y a %i min" +msgstr[1] "il y a %i mins" + +#: +msgid "%i hour ago" +msgid_plural "%i hours ago" +msgstr[0] "il y a %i heure" +msgstr[1] "il y a %i heures" + +#: +msgid "%i day ago" +msgid_plural "%i days ago" +msgstr[0] "il y a %i jour" +msgstr[1] "il y a %i jours" + +#: +msgid "unknown" +msgstr "inconnu" + +#: +msgid "SELECT ALL" +msgstr "TOUT Sร‰LECTIONNER" + +#: +msgid "SELECT NONE" +msgstr "AUCUN" + +#: +msgid "%i SELECTED" +msgid_plural "%i SELECTED" +msgstr[0] "%i Sร‰LECTIONNร‰" +msgstr[1] "%i Sร‰LECTIONNร‰S" + +#: +msgid "UP" +msgstr "HAUT" + +#: +msgid "DOWN" +msgstr "BAS" + +#: +msgid "LEFT" +msgstr "GAUCHE" + +#: +msgid "RIGHT" +msgstr "DROITE" + +#: +msgid "JOYSTICK 1 UP" +msgstr "JOYSTICK 1 HAUT" + +#: +msgid "JOYSTICK 1 LEFT" +msgstr "JOYSTICK 1 GAUCHE" + +#: +msgid "JOYSTICK 2 UP" +msgstr "JOYSTICK 2 HAUT" + +#: +msgid "JOYSTICK 2 LEFT" +msgstr "JOYSTICK 2 GAUCHE" + +#: +msgid "PAGE UP" +msgstr "L1 (PAGE PRECEDENTE)" + +#: +msgid "PAGE DOWN" +msgstr "R1 (PAGE SUIVANTE)" + +#: +msgid "HOTKEY" +msgstr "HOTKEY" + +#: +msgid "CONFIGURING" +msgstr "CONFIGURATION" + +#: +msgid "KEYBOARD" +msgstr "CLAVIER" + +#: +msgid "GAMEPAD %i" +msgstr "JOYSTICK %i" + +#. Config controllers missing translation +#: +msgid "PRESS ANYTHING" +msgstr "APPUYEZ SUR UN BOUTON" + +#: +msgid "ALREADY TAKEN" +msgstr "Dร‰Jร€ PRIS" + +#: +msgid "DISCARD CHANGES" +msgstr "ANNULER LES MODIFICATIONS" + +#: +msgid "WELCOME" +msgstr "BIENVENUE" + +#: +msgid "CONFIGURE INPUT" +msgstr "CONFIGURER LES MANETTES" + +#: +msgid "%i GAMEPAD DETECTED" +msgid_plural "%i GAMEPADS DETECTED" +msgstr[0] "%i MANETTE Dร‰TECTร‰E" +msgstr[1] "%i MANETTES Dร‰TECTร‰ES" + +#: +msgid "NO GAMEPADS DETECTED" +msgstr "AUCUNE MANETTE Dร‰TECTร‰E" + +#: +msgid "HOLD A BUTTON ON YOUR DEVICE TO CONFIGURE IT." +msgstr "MAINTENEZ UN BOUTON SUR LA MANETTE POUR LA CONFIGURER" + +#: +msgid "PRESS F4 TO QUIT AT ANY TIME." +msgstr "APPUYEZ SUR F4 POUR QUITTER ร€ TOUT MOMENT" + +#: +msgid "PRESS ESC OR THE HOTKEY TO CANCEL." +msgstr "APPUYEZ SUR ECHAP OU LA TOUCHE HOTKEY POUR ANNULER" + +#: +msgid "DO YOU WANT TO START KODI MEDIA CENTER ?" +msgstr "VOULEZ-VOUS LANCER LE MEDIA CENTER KODI ?" + +#: +msgid "LOADING..." +msgstr "CHARGEMENT..." + +#: +msgid "PLEASE WAIT..." +msgstr "VEUILLEZ PATIENTER..." + +#: +msgid "REALLY SHUTDOWN WITHOUT SAVING METADATAS?" +msgstr "VOULEZ-VOUS VRAIMENT ร‰TEINDRE SANS SAUVEGARDER LES METADATAS ?" + +#: +msgid "FAST SHUTDOWN SYSTEM" +msgstr "EXTINCTION RAPIDE" + +#: +msgid "WE CAN'T FIND ANY SYSTEMS!\n" +"CHECK THAT YOUR PATHS ARE CORRECT IN THE SYSTEMS CONFIGURATION FILE, AND YOUR GAME DIRECTORY HAS AT LEAST ONE GAME WITH THE CORRECT EXTENSION.\n" +"\n" +"VISIT EMULATIONSTATION.ORG FOR MORE INFORMATION." +msgstr "Aucun systรจme trouvรฉ !\n" +"\n" +"Visitez le site emulationstation.org pour plus d'infos !" + +#: +msgid "ON SCREEN KEYBOARD" +msgstr "CLAVIER VIRTUEL" + +#: +msgid "SHIFTS FOR UPPER,LOWER, AND SPECIAL" +msgstr "APPUYEZ SUR SHIFT POUR OBTENIR LES MAJUSCULES" + +#: +msgid "SPACE" +msgstr "ESPACE" + +#: +msgid "DELETE A CHAR" +msgstr "EFFACER UN CARACTERE" + +#: +msgid "SHIFT" +msgstr "MAJUSCULE" + +#: +msgid "STOP EDITING" +msgstr "ARRETER L'EDITION" + +#: +msgid "MOVE CURSOR" +msgstr "BOUGER LE CURSEUR" + +#: +msgid "EDIT" +msgstr "EDITER" + +#: +msgid "ACCEPT RESULT" +msgstr "ACCEPTER LE RESULTAT" + +#: +msgid "FILENAME" +msgstr "FICHIER" + +#: +msgid "RATING" +msgstr "VOTES" + +#: +msgid "TIMES PLAYED" +msgstr "PARTIES JOUร‰ES" + +#: +msgid "LAST PLAYED" +msgstr "DERNIรˆRE PARTIE" + +#: +msgid "NUMBER OF PLAYERS" +msgstr "NOMBRE DE JOUEURS" + +#: +msgid "DEVELOPER" +msgstr "Dร‰VELOPPEUR" + +#: +msgid "GENRE" +msgstr "GENRE" + +#: +msgid "SHOW HIDDEN" +msgstr "AFFICHER LES FICHIERS CACHร‰S" + +#: +msgid "EXTREM (1400Mhz)" +msgstr "EXTRรŠME (1400MHz)" + +#: +msgid "TURBO (1350Mhz)" +msgstr "TURBO (1350MHz)" + +#: +msgid "HIGH (1300Mhz)" +msgstr "HIGH (1300MHz)" + +#: +msgid "TURBO AND EXTREM OVERCLOCK PRESETS MAY CAUSE SYSTEM UNSTABILITIES, SO USE THEM AT YOUR OWN RISK.\n" +"IF YOU CONTINUE, THE SYSTEM WILL REBOOT NOW." +msgstr "LES PRร‰Rร‰GLAGES TURBO ET EXTRรŠME PEUVENT PROVOQUER DES INSTABILITร‰S DU SYSTรˆME, UTILISEZ-LES ร€ VOS PROPRES RISQUES.\n" +"SI VOUS CONTINUEZ, LE SYSTรˆME REDร‰MARRERA MAINTENANT." + +#: +msgid "%i GAME HIDDEN" +msgid_plural "%i GAMES HIDDEN" +msgstr[0] "%i JEU CACHร‰" +msgstr[1] "%i JEUX CACHร‰S" + +#: +msgid "Start kodi media player." +msgstr "Dรฉmarrer le media player Kodi" + +#: +msgid "Show how much space is used on your SHARE partition, located either on the SDCARD or on an external drive. The information shows how much GB are used and how much GB your storage has overall (example 13GB/26GB)." +msgstr "Montre l'espace utilisรฉ sur la partition SHARE situรฉe sur la carte SD ou le lecteur externe. L'information montre combien de Go sont utilisรฉs sur la capacitรฉ totale du support (exemple 13Go/26Go)." + +#: +msgid "Configure games display, ratio, filters (shaders), auto save and load and retroachievement account." +msgstr "Configurez l'affichage des jeux, le ratio, les filtres (shaders), sauvgarde/chargement auto et votre compte retroachievement." + +#: +msgid "The game ratio is the ratio between image width and image height. Use AUTO to let the emulator choose the original game ratio, that will give you the best retrogaming experience." +msgstr "Le ratio est le rapport entre la largeur et la hauteur de l'image. Utilisez AUTO pour laisser l'รฉmulateur choisir le ratio original du jeu, cela vous procurera la meilleur expรฉrience de retrogaming." + +#: +msgid "Smooth the game image. This option makes the image smoother, using bilinear filtering." +msgstr "Lisser l'image du jeu. Cette option rend l'image plus lisse en utilisant un filtre bi-linรฉaire." + +#: +msgid "This option allows you to rewind the game if you get killed by a monster, or if you make any other mistake. Use the HOTKEY + LEFT command within the game to rewind." +msgstr "Cette option vous permet de rembobiner le jeu si vous รชtes tuรฉ par un monstre ou si vous avez fait une erreur. Utilisez la commande HOTKEY + GAUCHE dans le jeu pour rembobiner." + +#: +msgid "Auto save the state when you quit a game, and auto load last saved state when you start a game." +msgstr "Sauvegardez automatiquement l'รฉtat actuel quand vous quittez un jeu et lancez automatiquement la derniรจre sauvegarde quand vous lancez un jeu." + +#: +msgid "Integer scaling is scaling by a factor of a whole number, such as 2x, 3x, 4x, etc. This option scales the image up to the greatest integer scale below the set resolution. So for instance, if you set your fullscreen resolution to 1920x1080 and enable integer scaling, it will only scale a 320x240 image up to 1280x960, and leave black borders all around. This is to maintain a 1:1 pixel ratio with the original source image, so that pixels are not unevenly duplicated." +msgstr "Integer scale est une รฉchelle par facteur de nombre entier comme 2x, 3x, 4x, etc. Cette option met ร  l'รฉchelle l'image jusqu'au plus grand facteur entier en dessous de la rรฉsolution. Par exemple,avec une rรฉsolution de 1920x1080 et integer scale activรฉ, une image de 320x240 sera mis ร  l'รฉchelle jusqu'ร  1280x960 en laissant des bords noirs tout autour. Ceci pour maintenir un ratio de pixels de 1:1 avec le format d'image original pour que les pixels ne soit pas inรฉgalement dupliquรฉs." + +#: +msgid "Shaders are like filters for the game rendering. You can select a shader set here, which is a collection of shaders selected for each system. You can also change the shader within the game with HOTKEY + L2 or HOTKEY + R2." +msgstr "Les \"shaders\" sont des filtres pour le rendu du jeu. Vous pouvez choisir un \"shader set\" ici qui est une collection de \"shaders\" sรฉlectionnรฉs pour chaque systรจme. Vous pouvez aussi changer de \"shader\" en cours de jeu avec HOTKEY + L2 ou HOTKEY + R2" + +#: +msgid "Enable or disable RetroAchievements in games." +msgstr "Activez ou dรฉsactivez RetroAchievements dans les jeux." + +#: +msgid "Hardcore mode disables *all* savestate and rewind functions within the emulator: you will not be able to save and reload at any time. You will have to complete the game and get the achievements first time, just like on the original console. In reward for this, you will earn both the standard and the hardcore achievement, in effect earning double points! A regular game worth 400 points, is now worth 800 if you complete it on hardcore! For example: if you complete the game for 400 points, you then have the opportunity to earn another 400 on hardcore." +msgstr "Le mode Hardcore dรฉsactive *toutes* les fonctions rembobinage et sauvegarde dans l'รฉmulateur: vous ne pourrez pas sauvegarder et charger n'importe quand. Vous devrez finir le jeu et dรฉbloquer les succรจs du premier coup, comme sur la console originale. En rรฉcompense, vous gagnerez ร  la fois les succรจs hardcore et standard, doublant ainsi vos points! Un jeu normal valant 400 points vaudra dรฉsormais 800 points si vous le finissez en hardcore!\n" +"Par exemple: si vous finissez un jeu pour 40 points, vous avez ensuite la possibilitรฉ d'obtenir 400 points de plus en hardcore." + +#: +msgid "The website retroachievements.org proposes challenges/achievements/trophies on platforms like NES, SNES, GB, GBC, GBA, Genesis/Megadrive, TurboGrafx16/PCEngine and more! Create your account on retroachievements.org and start your quest for achievements!" +msgstr "Le site web retroachievements.org propose des challenges/succรจs/trophรฉes sur les systรจme comme NES, SNES, GB, GBC, GBA, Megadrive, TurboGrafx16/PCEngine et plus! Crรฉez un compte sur retroachievements.org et lancer vous dans la quรชte de succรจs!" + +#: +msgid "Add and configure up to 5 controllers." +msgstr "Ajoutez et configurez jusqu'ร  5 manettes." + +#: +msgid "Start the screensaver after N minutes." +msgstr "Dรฉmarrer l'รฉconomiseur d'รฉcran aprรจs N minutes." + +#: +msgid "Set the screensaver behavior. DIM will reduce the screen light, and BLACK will turn the screen black." +msgstr "Choisissez le comportement de l'รฉconomiseur d'รฉcran. DIM rรฉduira la luminositรฉ de l'รฉcran, BLACK rendra l'รฉcran noir." + +#: +msgid "Shows a help at the bottom of the screen which displays commands you can use." +msgstr "Montre l'aide en bas de l'รฉcran qui vous indique les commandes que vous pouvez utiliser." + +#: +msgid "When enabled, you can switch between systems while browsing a gamelist by pressing LEFT or RIGHT." +msgstr "Si activรฉ, vous pouvez changer de systรจme tout en parcourant les listes de jeux en appuyant sur GAUCHE ou DROITE." + +#: +msgid "Updates the gamelists, if you added games since the last boot." +msgstr "Mettez ร  jour les listes de jeux si vous avez ajoutรฉ des jeux depuis le dernier dรฉmarrage." + +#: +msgid "Set the volume of the sound output for the frontend and the games." +msgstr "Rรฉglez le volume de la sortie audio pour EmulationStation et les jeux." + +#: +msgid "Get informations and visual for your games. The scraper downloads metadata and visuals for your games from different servers and enhances the user experience in EmulationStation completely." +msgstr "Rรฉcupรฉrez des informations et visuels pour vos jeux. Le scraper tรฉlรฉcharge les metadatas et images pour vos jeux depuis diffรฉrents serveurs et amรฉliore complรจtement l'expรฉrience d'EmulationStation." + +#: +msgid "Select a server to scrape from. The SCREENSCRAPER server is recommended and is based on www.screenscraper.fr and scrapes game data in your language, if available." +msgstr "Sรฉlectionnez un serveur depuis lequel scraper. Le serveur SCREENSCRAPER est recommandรฉ et est basรฉ sur www.screenscraper.fr. Il scrape les donnรฉes de jeu dans votre langue, si disponible." + +#: +msgid "Begin the scrape process with the configuration shown below." +msgstr "Dรฉmarrez le processus de scrape avec la configuration ci-dessous." + +#: +msgid "Scrape and display game ratings." +msgstr "Scrape et affiche la notation des jeux." + +#: +msgid "Advanced settings. Please make sure you really know what you're doing, before changing any values in this menu." +msgstr "Options avancรฉes. Assurez-vous de savoir ce que vous faรฎtes avant de changer les valeurs dans ce menu." + +#: +msgid "Overclock your board to increase the performance.\n" +"Overclock settings are tested and validated by the community. Keep in mind that overclocking your board can void your warranty." +msgstr "Overclockez votre carte pour augmenter les performances.\n" +"Les paramรจtres d'overclocking sont testรฉs et validรฉs par la communautรฉ.\n" +"Gardez ร  l'esprit qu'overclocker votre carte peut annuler votre garantie." + +#: +msgid "Only show games contained in the gamelist.xml file (located in your roms directories).\n" +"This option highly speeds up boot time, but new games will not be detected." +msgstr "Montrer seulement les jeux prรฉsent dans le fichier gamelist.xml (situรฉ dans le dossier des roms).\n" +"Cette option accรฉlรจre grandement le temps de dรฉmarrage mais les nouveaux jeux ne seront pas dรฉtectรฉs." + +#: +msgid "This option allows you to set the selected system to fixed mode. With this option activated, the user cannot access other systems." +msgstr "Cette option vous permet de mettre le systรจme sรฉlectionnรฉ en mode fixe.\n" +"Avec cette option activรฉ, l'utilisateur ne peut pas accรฉder aux autres systรจmes." + +#: +msgid "Always display the basic gamelist view, even if you have scraped your games." +msgstr "Toujours afficher la vue liste de jeux basique, mรชme si vous avez scrappรฉ vos jeux." + +#: +msgid "Enable or disable Kodi, customize the Kodi startup, enable the X button to start Kodi" +msgstr "Activez ou dรฉsactivez Kodi, configurez le dรฉmarrage de Kodi, activez le bouton X pour dรฉmarrer Kodi." + +#: +msgid "Enable or disable Kodi. If kodi is disabled, you won't be able to start it with the X button, or start it automatically at boot. The menu entry will be removed as well." +msgstr "Activez ou dรฉsactivez Kodi. Si Kodi est dรฉsactivรฉ, vous ne pourrez pas le lancer avec le bouton X ou le lancer automatiquement au dรฉmarrage. L'option du menu sera aussi retirรฉe." + +#: +msgid "Use the X button to start Kodi." +msgstr "Utilisez le bouton X pour dรฉmarrer Kodi." + +#: +msgid "Automatically start into Kodi on boot." +msgstr "Lancer directement Kodi au dรฉmarrage." + +#: +msgid "Show the framerate in EmulationStation and in game." +msgstr "Affichez le taux de rafraรฎchissement dans EmulationStation et dans les jeux." + +#: +msgid "Select which emulator to use when you start a game for this system." +msgstr "Selectionnez quel รฉmulateur utiliser quand vous lancer un jeu pour ce systรจme." + +#: +msgid "Select which core to use for the selected emulator. For example, the LIBRETRO emulator has many cores to run Super Nintendo games. The default core you choose here can also be overridden in game specific settings." +msgstr "Selectionnez quel core utiliser pour l'รฉmulateur sรฉlectionnรฉ. Par exemple, l'รฉmulateur LIBRETRO a plusieurs core pour les jeux Super Nintendo. Le core par dรฉfaut que vous choisissez peut aussi รชtre outrepassรฉ par les rรฉglages spรฉcifiques par jeu." + +#: +msgid "USE COMPOSED VISUALS" +msgstr "UTILISER DES IMAGES COMPOSร‰ES" + +#: +msgid "CHECK UPDATES" +msgstr "Vร‰RIFIER LES MISES ร€ JOUR" + +#: +msgid "UPDATE TYPE" +msgstr "TYPE DE MISE ร€ JOUR" + +#: +msgid "INTEGER SCALE (PIXEL PERFECT)" +msgstr "INTEGER SCALE (PIXEL PERFECT)" + +#: +msgid "ADVANCED SETTINGS" +msgstr "PARAMรˆTRES AVANCร‰S" + +#: +msgid "BOOT SETTINGS" +msgstr "PARAMรˆTRES DE Dร‰MARRAGE" + +#: +msgid "GAMELIST ONLY" +msgstr "LISTE DE JEUX UNIQUEMENT" + +#: +msgid "START ON SYSTEM" +msgstr "Dร‰MARRER SUR UN SYSTรˆME" + +#: +msgid "BOOT ON GAMELIST" +msgstr "Dร‰MARRER SUR LA LISTE DE JEUX" + +#: +msgid "HIDE SYSTEM VIEW" +msgstr "CACHER LA VUE SYSTรˆME" + +#: +msgid "EMULATOR ADVANCED CONFIGURATION" +msgstr "CONFIGURATION AVANCร‰E DE L'ร‰MULATEUR" + +#: +msgid "ADVANCED EMULATOR CONFIGURATION" +msgstr "CONFIGURATION AVANCร‰E DE L'ร‰MULATEUR" + +#: +msgid "HELP" +msgstr "AIDE" + +#: +msgid "THE SYSTEM IS UP TO DATE" +msgstr "LE SYSTรˆME EST ร€ JOUR" + +#: +msgid "FORCE BASIC GAMELIST VIEW" +msgstr "FORCER LA VUE LISTE DE JEUX BASIQUE" + +#: +msgid "DOWNLOADED" +msgstr "Tร‰Lร‰CHARGร‰" + +#: +msgid "UPDATE VERSION:" +msgstr "VERSION MISE ยญร€ JOUR:" + +#: +msgid "UPDATE CHANGELOG:" +msgstr "CHANGELOG DE LA MISE ร€ JOUR:" + +#: +msgid "MORE DETAILS" +msgstr "PLUS DE Dร‰TAILS" + +#: +msgid "CAROUSEL TRANSITIONS" +msgstr "ANIMATION DU BANDEAU" + +#: +msgid "ENABLE FILTERS" +msgstr "ACTIVER LES FILTRES" + +#: +msgid "THEME CONFIGURATION" +msgstr "CONFIGURATION DU THรˆME" + +#: +msgid "THEME COLORSET" +msgstr "COLORSET DU THรˆME" + +#: +msgid "THEME ICONSET" +msgstr "ICONSET DU THรˆME" + +#: +msgid "THEME MENU" +msgstr "MENU DU THรˆME" + +#: +msgid "THEME SYSTEMVIEW" +msgstr "SYSTEMVIEW DU THรˆME" + +#: +msgid "THEME GAMELISTVIEW" +msgstr "GAMELISTVIEW DU THรˆME" + +#: +msgid "THEME REGION" +msgstr "Rร‰GION DU THรˆME" + +#: +msgid "THIS THEME HAS NO OPTION" +msgstr "CE THรˆME N'A PAS D'OPTIONS" + +#: +msgid "MANUAL INPUT" +msgstr "SAISIE MANUELLE" + +#: +msgid "AN ERROR OCCURED - DOWNLOADED" +msgstr "UNE ERREUR EST SURVENUE - Tร‰Lร‰CHARGร‰" + +#: +msgid "START KODI" +msgstr "Dร‰MARRER KODI" + +#: +msgid "Shows the current available update version." +msgstr "Montre les versions disponibles." + +#: +msgid "Shows the current available update changelog." +msgstr "Montre le changelog de la mise ร  jour disponible." + +#: +msgid "Configure an associated controller. Your controller has to be associated / plugged before." +msgstr "Configurer une manette appairรฉe. Votre manette doit รชtre appairรฉe / branchรฉe auparavant." + +#: +msgid "Choose if carousel will be animated or not during transitions" +msgstr "Choisissez si le bandeau sera animรฉ ou non durant les transitions." + +#: +msgid "Select the type of transition that occurs when you start a game. INSTANT will do nothing, FADE will fade to dark, and SLIDE will zoom on the game cover (or name if there is no scrape information)" +msgstr "Sรฉlectionnez le type de transition qui se produit lorsque vous lancez un jeu. FADE fera un fondu au noir et SLIDE zoomera sur l'image du jeu (ou le nom si il n'y a pas de metadatas)." + +#: +msgid "Select exisiting colorset options for this theme." +msgstr "Sรฉlectionnez un colorset disponible pour votre thรจme" + +#: +msgid "Select exisiting iconset options for this theme." +msgstr "Sรฉlectionnez un iconset disponible pour votre thรจme" + +#: +msgid "Select exisiting menu style options for this theme." +msgstr "Sรฉlectionnez un style de menu disponible pour votre thรจme" + +#: +msgid "Select exisiting system view options for this theme." +msgstr "Sรฉlectionnez une vue systรจme disponible pour votre thรจme" + +#: +msgid "Select exisiting gamelist view options for this theme." +msgstr "Sรฉlectionnez une vue gamelist disponible pour votre thรจme" + +#: +msgid "Configure theme options if available." +msgstr "Configurez les options du thรจme si disponibles" + +#: +msgid "Select Region of logos, pictures for system that are different for some countries. E.g. Megadrive in EU / Genesis in US" +msgstr "Sรฉlectionnez une rรฉgion des logos et images pour les systรจmes qui sont diffรฉrents selon les pays. Ex: Megadrive en Europe / Genesis aux USA" + +#: +msgid "Type the name of your SSID if it is hidden or not listed" +msgstr "Saisissez le nom de votre SSID si il n'est pas dans la liste ou cachรฉ" + +#: +msgid "Select a letter and the listing will go directly on the first game starting with this letter." +msgstr "Sรฉlectionnez une lettre et le listing ira directement sur le 1er jeu commenรงant par cette lettre." + +#: +msgid "Select the way the game list is sortered (alphabetically, by notation...)." +msgstr "Sรฉlectionnez la faรงon dont la liste de jeux est triรฉe (alphabรฉtique, par note...)." + +#: +msgid "Switch between seing or not only the favorites games. To add a game in the favorite list, select the game and toggle its state using 'Y'." +msgstr "Changer entre rendre visible ou non les jeux favoris. Pour ajouter un jeu dans les favoris, sรฉlectionnez le jeu et changer son statut en utilisant 'Y'." + +#: +msgid "Switch between seing or not the hidden games. To hide a game, edit its data and select 'Hide'." +msgstr "Changer entre rendre visible ou non les jeux cachรฉs. pour cacher un jeu, รฉditez ses donnรฉes et sรฉlectionnez 'Cacher'." + +#: +msgid "This option display a menu which allows to change game data and many others options." +msgstr "Cette option affiche un menu qui permet de changer les donnรฉes du jeu et de nombreuses autres options." + +#: +msgid "AVAILABLE UPDATE" +msgstr "MISE ร€ JOUR DISPONIBLE" + +#: +msgid "UPDATE CHANGELOG" +msgstr "CHANGELOG DE LA MISE ร€ JOUR" + +#: +msgid "CLOCK IN MENU" +msgstr "HORLOGE DANS LE MENU" + +#: +msgid "Now playing" +msgstr "Lecture en cours" + +#: +msgid "DEFAULT (%1%)" +msgstr "Dร‰FAUT (%1%)" + +#: +msgid "INPUT REQUIRED" +msgstr "ENTRร‰E Nร‰CESSAIRE" + +#: +msgid "(skipped)" +msgstr "(passรฉ)" + +#: +msgid "UP/DOWN TO SKIP" +msgstr "HAUT/BAS POUR PASSER" + +#: +msgid "A TO UNSET" +msgstr "A POUR ANNULER" + +#: +msgid "DOWN TO SKIP AND KEEP [%1%]" +msgstr "BAS POUR PASSER ET GARDER [%1%]" + +#: +msgid "UP/DOWN TO SKIP AND KEEP [%1%]" +msgstr "HAUT/BAS POUR PASSER ET GARDER [%1%]" + +#: +msgid "Set duration of help popups, 0 means no popup." +msgstr "Rรฉgler la durรฉe des popups d'aide, 0 pour dรฉsactiver." + +#: +msgid "HELP POPUP DURATION" +msgstr "DURร‰E DES POPUPS D'AIDE" + +#: +msgid "Set duration of music popups, 0 means no popup." +msgstr "Rรฉgler la durรฉe des popups de musique, 0 pour dรฉsactiver." + +#: +msgid "MUSIC POPUP DURATION" +msgstr "DURร‰E DES POPUPS DE MUSIQUE" + +#: +msgid "POPUP SETTINGS" +msgstr "Rร‰GLAGE DES POPUPS" + +#: +msgid "POPUP POSITION" +msgstr "POSITION DES POPUPS" + +#: +msgid "Select the position of popups on screen." +msgstr "Choisissez la position des popups sur l'รฉcran" + +#: +msgid "Set position and duration of popups." +msgstr "Rรฉglez la position et la durรฉe des popups" + +#: +msgid "TOP/RIGHT" +msgstr "HAUT/DROITE" + +#: +msgid "BOTTOM/RIGHT" +msgstr "BAS/DROITE" + +#: +msgid "BOTTOM/LEFT" +msgstr "BAS/GAUCHE" + +#: +msgid "TOP/LEFT" +msgstr "HAUT/GAUCHE" + +#: +msgid "SHOW FOLDERS CONTENT" +msgstr "MONTRER LE CONTENU DES DOSSIERS" + +#: +msgid "Switch between seeing the folders structure and seeing all games in a flatten top level." +msgstr "Basculer entre l'affichage classique de rรฉpertoires et l'affichage de tous les jeux en une seule liste." + +#: +msgid "NETPLAY" +msgstr "NETPLAY" + +#: +msgid "NETPLAY SETTINGS" +msgstr "OPTIONS NETPLAY" + +#: +msgid "NETPLAY LOBBY" +msgstr "LOBBY NETPLAY" + +#: +msgid "Enable or disable Netplay in games." +msgstr "Activer ou dรฉsactiver Netplay dans les jeux." + +#: +msgid "PORT" +msgstr "PORT" + +#: +msgid "NICKNAME" +msgstr "SURNOM" + +#: +msgid "RELAY SERVER" +msgstr "SERVEUR DE RELAI" + +#: +msgid "Enable or disable connections throught relay servers." +msgstr "Activer ou dรฉsactiver les connexions au travers de serveurs de relai." + +#: +msgid "KODI/NETPLAY" +msgstr "KODI/NETPLAY" + +#: +msgid "NO GAMES OR NO CONNECTION" +msgstr "PAS DE JEUX OU PAS DE CONNEXION" + +#: +msgid "HASH NOW" +msgstr "HASHER MAINTENANT" + +#: +msgid "HASH THESE SYSTEMS" +msgstr "HASHER CES SYSTรˆMES" + +#: +msgid "Add hash of roms in your gamelists to have more accurate results in Netplay." +msgstr "Ajoutez les hashs des roms dans vos gamelists pour avoir un rรฉsultat plus prรฉcis en Netplay." + +#: +msgid "HASH ROMS" +msgstr "HASHER LES ROMS" + +#: +msgid "Only missing hashs" +msgstr "Seulement hashs manquants" + +#: +msgid "Username" +msgstr "Nom" + +#: +msgid "Country" +msgstr "Pays" + +#: +msgid "Latency" +msgstr "Latence" + +#: +msgid "Host arch." +msgstr "Arch. de l'hรดte" + +#: +msgid "Core ver." +msgstr "Ver. du core" + +#: +msgid "RA ver." +msgstr "Ver. de RA" + +#: +msgid "Can join" +msgstr "Joignable" + +#: +msgid "Rom and core match" +msgstr "Corres. rom et core" + +#: +msgid "Rom found" +msgstr "Rom trouvรฉe" + +#: +msgid "No rom match" +msgstr "Pas de corres. de rom" + +#: +msgid "Match" +msgstr "Correspondance" + +#: +msgid "No Match" +msgstr "Pas de corres." + +#: +msgid "Rom file" +msgstr "Fichier rom" + +#: +msgid "Rom hash" +msgstr "Hash de la rom" + +#: +msgid "THIS COULD TAKE A WHILE, CONFIRM?" +msgstr "CELA PEUX PRENDRE LONGTEMPS, CONFIRMER?" + +#: +msgid "good" +msgstr "bon" + +#: +msgid "bad" +msgstr "mauvais" + +#: +msgid "medium" +msgstr "moyen" + +#: +msgid "NETPLAY POPUP DURATION" +msgstr "DURร‰E DES POPUPS NETPLAY" + +#: +msgid "Set duration of netplay popups, 0 means no popup." +msgstr "Rรฉgler la durรฉe des popups Netplay, 0 pour dรฉsactiver." + +#: +msgid "Player" +msgstr "Joueur" + +#: +msgid "Game" +msgstr "Jeu" + +#: +msgid "Play online on games running through Retroarch like NES, SNES, FBA, Genesis/Megadrive and more!" +msgstr "Jouez en ligne ร  des jeux tournant sous Retroarch comme NES, SNES, FBA, Megadrive et bien plus!" + +#: +msgid "Rom, hash and core match" +msgstr "Corres. rom, hash et core" + +#: +msgid "No core match" +msgstr "Pas de corres. du core" + +#: +msgid "Add a clock in the main menu." +msgstr "Ajouter une horloge dans le menu principal" + +#: +msgid "UPGRADING" +msgstr "Mise ร  jour" + +#: +msgid "PREPARING" +msgstr "Prรฉparation" + +#: +msgid "Starting UI" +msgstr "Initialisation de l'interface" + +#: +msgid "VERIFYING" +msgstr "Verification" + +#: +msgid "EMPTY LIST" +msgstr "Liste vide" + +#: +msgid "VISIBLE SYSTEMS" +msgstr "SYSTรˆMES A AFFICHER" + +#: +msgid "COMPLETE QUIT MENU" +msgstr "AFFICHER LE MENU QUITTER COMPLET" + +#: Retroarch ratio +msgid "Auto" +msgstr "Automatique" + +msgid "Square pixel" +msgstr "Pixel carrรฉ" + +msgid "Retroarch Config" +msgstr "Retroarch mode 'Config'" + +msgid "Retroarch Custom" +msgstr "Retroarch mode 'Custom'" + +msgid "Core provided" +msgstr "Fourni par le Core" + +msgid "Do not set" +msgstr "Ne pas toucher" \ No newline at end of file diff --git a/resources/locale/ko/emulationstation2.po b/resources/locale/ko/emulationstation2.po new file mode 100644 index 0000000000..55f96b2599 --- /dev/null +++ b/resources/locale/ko/emulationstation2.po @@ -0,0 +1,1768 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: emulationstation\n" +"Language: ko\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +msgid "EMULATOR SETTINGS" +msgstr "์—๋ฎฌ๋ ˆ์ดํ„ฐ ์„ค์ •" + +msgid "LOG LEVEL" +msgstr "๋กœ๊ทธ ๋ ˆ๋ฒจ" + +msgid "PLAY THEME MUSICS" +msgstr "ํ…Œ๋งˆ ์Œ์•… ์žฌ์ƒ" + +msgid "DISPLAY SONG TITLES" +msgstr "๋…ธ๋ž˜ ๊ณก๋ชฉ ํ‘œ์‹œ" + +msgid "SHOW FAVORITES ON TOP" +msgstr "์ƒ๋‹จ์— ์ฆ๊ฒจ์ฐพ๊ธฐ ํ‘œ์‹œ" + +msgid "PREFERED IMAGE SOURCE" +msgstr "์„ ํ˜ธ ์ด๋ฏธ์ง€ ์†Œ์Šค" + +msgid "PREFERED THUMBNAIL SOURCE" +msgstr "์„ ํ˜ธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์†Œ์Šค" + +msgid "SCRAPE MARQUEE" +msgstr "๋งˆํ‚ค ์Šคํฌ๋žฉ" + +msgid "%i SELECTED" +msgid_plural "%i SELECTED" +msgstr[0] "%i ์„ ํƒ๋จ" +msgstr[1] "%i ์„ ํƒ๋จ" +msgid "VIDEO SCREENSAVER SETTINGS" +msgstr "๋™์˜์ƒ ํ™”๋ฉด๋ณดํ˜ธ๊ธฐ ์„ค์ •" + +msgid "SLIDESHOW SCREENSAVER SETTINGS" +msgstr "์Šฌ๋ผ์ด๋“œ์‡ผ ํ™”๋ฉด๋ณดํ˜ธ๊ธฐ ์„ค์ •" + +msgid "SCREENSAVER CONTROLS" +msgstr "ํ™”๋ฉด๋ณดํ˜ธ๊ธฐ ์ œ์–ด" + +msgid "All Games" +msgstr "์ „์ฒด ๊ฒŒ์ž„" + +msgid "Only missing medias" +msgstr "๋ˆ„๋ฝ ๋œ ๋ฏธ๋””์–ด ๋งŒ" + +msgid "always" +msgstr "ํ•ญ์ƒ" + +msgid "start & end" +msgstr "์‹œ์ž‘ ๋ฐ ์ข…๋ฃŒ์‹œ" + +msgid "never" +msgstr "์‚ฌ์šฉ์•ˆํ•จ" + +msgid "SCRAPE VIDEOS" +msgstr "๋™์˜์ƒ ์Šคํฌ๋žฉ" + +msgid "RESET GAMELIST CUSTOMISATIONS" +msgstr "๊ฒŒ์ž„๋ชฉ๋ก ์‚ฌ์šฉ์ž ์„ค์ • ์ดˆ๊ธฐํ™”" + +msgid "RESET" +msgstr "์ดˆ๊ธฐํ™”" + +msgid "SHOW FOLDERS" +msgstr "ํด๋” ํ‘œ์‹œ" + +msgid "ARE YOU SURE YOU WANT TO CONFIGURE INPUT?" +msgstr "์ž…๋ ฅ ์„ค์ •์„ ์ง„ํ–‰ํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?" + +msgid "disabled" +msgstr "๋น„ํ™œ์„ฑ" + +msgid "DEFAULT" +msgstr "๊ธฐ๋ณธ๊ฐ’" + +msgid "warning" +msgstr "๊ฒฝ๊ณ " + +msgid "error" +msgstr "์˜ค๋ฅ˜" + +msgid "SWAP IMAGE AFTER (SECS)" +msgstr "๋‹ค์Œ ์‹œ๊ฐ„ ํ›„ ์ด๋ฏธ์ง€ ๋ณ€๊ฒฝ (์ดˆ)" + +msgid "SHOW GAME NAME" +msgstr "๊ฒŒ์ž„ ์ด๋ฆ„ ํ‘œ์‹œ" + +msgid "STRETCH IMAGES" +msgstr "์ด๋ฏธ์ง€ ๋Š˜๋ฆฌ๊ธฐ" + +msgid "USE CUSTOM IMAGES" +msgstr "์‚ฌ์šฉ์ž ์ด๋ฏธ์ง€ ์‚ฌ์šฉ" + +msgid "CUSTOM IMAGE DIR" +msgstr "์‚ฌ์šฉ์ž ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ" +"" + +msgid "CUSTOM IMAGE DIR RECURSIVE" +msgstr "์‚ฌ์šฉ์ž ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ ์ˆœํ™˜" + +msgid "CUSTOM IMAGE FILTER" +msgstr "์‚ฌ์šฉ์ž ์ด๋ฏธ์ง€ ํ•„ํ„ฐ" + +msgid "SWAP VIDEO AFTER (SECS)" +msgstr "๋‹ค์Œ ์‹œ๊ฐ„ ํ›„ ๋™์˜์ƒ ๋ณ€๊ฒฝ (์ดˆ)" + +msgid "USE OMX PLAYER FOR SCREENSAVER" +msgstr "ํ™”๋ฉด๋ณดํ˜ธ๊ธฐ์— OMX ํ”Œ๋ ˆ์ด์–ด ์‚ฌ์šฉ" + +msgid "SHOW GAME INFO" +msgstr "๊ฒŒ์ž„ ์ •๋ณด ํ‘œ์‹œ" + +msgid "SHOW GAME INFO ON SCREENSAVER" +msgstr "ํ™”๋ฉด๋ณดํ˜ธ๊ธฐ์— ๊ฒŒ์ž„ ์ •๋ณด ํ‘œ์‹œ" + +msgid "USE MARQUEE AS GAME INFO" +msgstr "๊ฒŒ์ž„ ์ •๋ณด์— ๋งˆํ‚ค ์‚ฌ์šฉ" + +msgid "STRETCH VIDEO ON SCREENSAVER" +msgstr "ํ™”๋ฉด๋ณดํ˜ธ๊ธฐ ๋™์˜์ƒ ํฌ๊ธฐ ํ™•์žฅ" + +msgid "This collection is empty" +msgstr "ํ˜„์žฌ ์ปฌ๋ ‰์…˜์€ ๋น„์–ด ์žˆ์Šต๋‹ˆ๋‹ค" + +msgid "None" +msgstr "์—†์Œ" + +msgid "Various" +msgstr "๋‹ค์–‘" + +msgid "among other titles." +msgstr "ํฌํ•จํ•œ ์—ฌ๋Ÿฌ ํƒ€์ดํ‹€์ด ์žˆ์Šต๋‹ˆ๋‹ค." + +msgid "This collection contains" +msgstr "ํ˜„์žฌ ์ปฌ๋ ‰์…˜์—๋Š” " + +msgid "games, including" +msgstr "๊ฐœ์˜ ๊ฒŒ์ž„์ด ์žˆ์œผ๋ฉฐ " + +msgid "Loading..." +msgstr "๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘..." + +msgid "Preloading UI" +msgstr "UI ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘" + +msgid "ADD/REMOVE GAMES TO THIS GAME COLLECTION" +msgstr "ํ˜„์žฌ ๊ฒŒ์ž„ ์ปฌ๋ ‰์…˜์— ๊ฒŒ์ž„ ์ถ”๊ฐ€/์‚ญ์ œ" + +msgid "FINISH EDITING" +msgstr "ํŽธ์ง‘ ์ข…๋ฃŒ" + +msgid "GRID SIZE" +msgstr "๊ทธ๋ฆฌ๋“œ ํฌ๊ธฐ" + +msgid "DEFAULT GRID SIZE" +msgstr "๊ธฐ๋ณธ ๊ทธ๋ฆฌ๋“œ ํฌ๊ธฐ" + +msgid "UI MODE" +msgstr "UI ๋ชจ๋“œ" + +msgid "PRELOAD UI" +msgstr "UI ๋ฏธ๋ฆฌ์ฝ๊ธฐ" + +msgid "VSYNC" +msgstr "VSYNC" + +msgid "DISPLAY FAVORITES FIRST IN GAMELIST" +msgstr "๊ฒŒ์ž„๋ก๋ก ์•ž์ชฝ์— ์ฆ๊ฒจ์ฐพ๊ธฐ ํ‘œ์‹œ" + +msgid "SHOW SYSTEM NAME IN COLLECTIONS" +msgstr "์ปฌ๋ ‰์…˜์— ์‹œ์Šคํ…œ ์ด๋ฆ„ ํ‘œ์‹œ" + +msgid "OPTIMIZE IMAGES VRAM USE" +msgstr "์ด๋ฏธ์ง€ VRAM ์‚ฌ์šฉ ์ตœ์ ํ™”" + +msgid "Full" +msgstr "์ „์ฒด" + +msgid "Kiosk" +msgstr "ํ‚ค์˜ค์Šคํฌ" + +msgid "Kid" +msgstr "์•„๋™์šฉ" + +msgid "GAME COLLECTION SETTINGS" +msgstr "๊ฒŒ์ž„ ์ปฌ๋ ‰์…˜ ์„ค์ •" + +msgid "CANCEL" +msgstr "์ทจ์†Œ" + +msgid "automatic" +msgstr "์ž๋™" + +msgid "basic" +msgstr "๊ธฐ๋ณธ" + +msgid "detailed" +msgstr "์ƒ์„ธ" + +msgid "grid" +msgstr "๊ทธ๋ฆฌ๋“œ" + +msgid "fade" +msgstr "์–ด๋‘ก๊ฒŒ" + +msgid "instant" +msgstr "์ฆ‰์‹œ" + +msgid "slide" +msgstr "์Šฌ๋ผ์ด๋“œ" + +msgid "gridex" +msgstr "๊ทธ๋ฆฌ๋ฑ์Šค" + +msgid "ENABLE NAVIGATION SOUNDS" +msgstr "์ด๋™ ํšจ๊ณผ์Œ ์‚ฌ์šฉ" + +msgid "ENABLE VIDEO AUDIO" +msgstr "๋™์˜์ƒ ์˜ค๋””์˜ค ์‚ฌ์šฉ" + +msgid "APPLY FILTER" +msgstr "ํ•„ํ„ฐ ์ ์šฉ" + +msgid "AUTOMATIC GAME COLLECTIONS" +msgstr "์ž๋™ ๊ฒŒ์ž„ ์ปฌ๋ ‰์…˜" + +msgid "SELECT COLLECTIONS" +msgstr "์ปฌ๋ ‰์…˜ ์„ ํƒ" + +msgid "CUSTOM GAME COLLECTIONS" +msgstr "์‚ฌ์šฉ์ž ๊ฒŒ์ž„ ์ปฌ๋ ‰์…˜" + +msgid "CREATE NEW CUSTOM COLLECTION FROM THEME" +msgstr "ํ…Œ๋งˆ์—์„œ ์ƒˆ ์‚ฌ์šฉ์ž ์ปฌ๋ ‰์…˜ ๋งŒ๋“ค๊ธฐ" + +msgid "SELECT THEME FOLDER" +msgstr "ํ…Œ๋งˆ ํด๋” ์„ ํƒ" + +msgid "CREATE NEW CUSTOM COLLECTION" +msgstr "์ƒˆ ์‚ฌ์šฉ์ž ์ปฌ๋ ‰์…˜ ์ƒ์„ฑ" + +msgid "New Collection Name" +msgstr "์ƒˆ ์ปฌ๋ ‰์…˜ ์ด๋ฆ„" + +msgid "GROUP UNTHEMED CUSTOM COLLECTIONS" +msgstr "ํ…Œ๋งˆ๊ฐ€ ์—†๋Š” ์‚ฌ์šฉ์ž ์ปฌ๋ ‰์…˜ ๋ฌถ๊ธฐ" + +msgid "SORT CUSTOM COLLECTIONS AND SYSTEMS" +msgstr "์‚ฌ์šฉ์ž ์ปฌ๋ ‰์…˜ ๋ฐ ์‹œ์Šคํ…œ ์ •๋ ฌ" + +msgid "FINISH EDITING COLLECTION" +msgstr "์ปฌ๋ ‰์…˜ ํŽธ์ง‘ ์ข…๋ฃŒ" + +msgid "PARSE GAMESLISTS ONLY" +msgstr "๊ฒŒ์ž„๋ชฉ๋ก ํ•ญ๋ชฉ๋งŒ ํ‘œ์‹œ" + +msgid "POWER SAVER MODES" +msgstr "์ ˆ์ „ ๋ชจ๋“œ" + +msgid "SAVE METADATA ON EXIT" +msgstr "์ข…๋ฃŒ์‹œ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ €์žฅ" + +msgid "SHOW HIDDEN FILES" +msgstr "์ˆจ๊น€ํŒŒ์ผ ํ‘œ์‹œ" + +msgid "VRAM LIMIT" +msgstr "VRAM ์ œํ•œ" + +msgid "RESET ALL FILTERS" +msgstr "์ „์ฒด ํ•„ํ„ฐ ์ดˆ๊ธฐํ™”" + +msgid "GAMELIST VIEW STYLE" +msgstr "๊ฒŒ์ž„๋ชฉ๋ก ๋ณด๊ธฐ ์Šคํƒ€์ผ" + +msgid "GAME LAUNCH TRANSITION" +msgstr "๊ฒŒ์ž„ ์‹คํ–‰ ์ „ํ™˜ํšจ๊ณผ" + +msgid "THREADED LOADING" +msgstr "์Šค๋ ˆ๋“œ๋œ ๋กœ๋”ฉ" + +msgid "RANDOM" +msgstr "๋ฌด์ž‘์œ„" + +msgid "RATING" +msgstr "ํ‰์ " + +msgid "RELEASED" +msgstr "์ถœ์‹œ" + +msgid "DEVELOPER" +msgstr "๊ฐœ๋ฐœ์‚ฌ" + +msgid "PUBLISHER" +msgstr "ํŒ๋งค์‚ฌ" + +msgid "GENRE" +msgstr "์žฅ๋ฅด" + +msgid "PLAYERS" +msgstr "ํ”Œ๋ ˆ์ด์–ด" + +msgid "NO GAMES FOUND - SKIP" +msgstr "๊ฒŒ์ž„์„ ์ฐพ์„ ์ˆ˜ ์—†์Œ - ๊ฑด๋„ˆ๋›ฐ๊ธฐ" + +msgid "RETRY" +msgstr "๋‹ค์‹œ ์‹œ๋„" + +msgid "SKIP" +msgstr "๊ฑด๋„ˆ๋›ฐ๊ธฐ" + +msgid "SEARCH FOR" +msgstr "๊ฒ€์ƒ‰" + +msgid "SEARCH" +msgstr "๊ฒ€์ƒ‰" + +msgid "SCRAPING IN PROGRESS" +msgstr "์Šคํฌ๋žฉ ์ง„ํ–‰์ค‘" + +msgid "SYSTEM" +msgstr "์‹œ์Šคํ…œ" + +msgid "subtitle text" +msgstr "์ž๋ง‰ ํ…์ŠคํŠธ" + +msgid "INPUT" +msgstr "์ž…๋ ฅ" + +msgid "STOP" +msgstr "์ค‘์ง€" + +msgid "stop (progress saved)" +msgstr "์ค‘์ง€ (์ง„ํ–‰์ƒํ™ฉ ์ €์žฅ๋จ)" + +msgid "GAME %i OF %i" +msgstr "๊ฒŒ์ž„ %i / %i" + +msgid "WE CAN'T FIND ANY SYSTEMS!\n" +"CHECK THAT YOUR PATHS ARE CORRECT IN THE SYSTEMS CONFIGURATION FILE, AND YOUR GAME DIRECTORY HAS AT LEAST ONE GAME WITH THE CORRECT EXTENSION.\n" +"\n" +"VISIT EMULATIONSTATION.ORG FOR MORE INFORMATION." +msgid "์‹œ์Šคํ…œ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค!\n" +"์‹œ์Šคํ…œ ์„ค์ • ํŒŒ์ผ์—์„œ ๊ฒฝ๋กœ๊ฐ€ ์˜ฌ๋ฐ”๋ฅธ์ง€ ํ™•์ธํ•˜๊ณ  ํด๋”์— ์ตœ์†Œ ํ•œ ๊ฐœ ์ด์ƒ์˜ ์˜ฌ๋ฐ”๋ฅธ ํ™•์žฅ์ž๋ฅผ ๊ฐ€์ง„ ๊ฒŒ์ž„์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•˜์‹ญ์‹œ์˜ค.\n" +"\n" +"์ž์„ธํ•œ ์ •๋ณด๋Š” EMULATIONSTATION.ORG์—์„œ ํ™•์ธํ•˜์„ธ์š”." + +msgid "%i GAME SUCCESSFULLY SCRAPED!" +msgid_plural "%i GAMES SUCCESSFULLY SCRAPED!" +msgstr[0] "%i ๊ฒŒ์ž„ ์Šคํฌ๋žฉ๋จ!" +msgstr[1] "%i ๊ฒŒ์ž„ ์Šคํฌ๋žฉ๋จ!" + +msgid "%i GAME SKIPPED." +msgid_plural "%i GAMES SKIPPED." +msgstr[0] "%i ๊ฒŒ์ž„ ๊ฑด๋„ˆ๋œ€." +msgstr[1] "%i ๊ฒŒ์ž„ ๊ฑด๋„ˆ๋œ€." + +msgid "OK" +msgstr "ํ™•์ธ" + +msgid "EDIT METADATA" +msgstr "๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ˆ˜์ •" + +msgid "SCRAPE" +msgstr "์Šคํฌ๋žฉ" + +msgid "SAVE" +msgstr "์ €์žฅ" + +msgid "THIS WILL DELETE A FILE!\n" +"ARE YOU SURE?" +msgstr "ํŒŒ์ผ์„ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค! \n" +"ํ™•์‹คํ•ฉ๋‹ˆ๊นŒ?" + +msgid "YES" +msgstr "์˜ˆ" + +msgid "NO" +msgstr "์•„๋‹ˆ์˜ค" + +msgid "DELETE" +msgstr "์‚ญ์ œ" + +msgid "SAVE CHANGES ?" +msgstr "๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ์ €์žฅํ•ฉ๋‹ˆ๊นŒ?" + +msgid "BACK" +msgstr "์ด์ „" + +msgid "CLOSE" +msgstr "๋‹ซ๊ธฐ" + +msgid "MAIN MENU" +msgstr "๋ฉ”์ธ ๋ฉ”๋‰ด" + +msgid "KODI MEDIA CENTER" +msgstr "KODI ๋ฏธ๋””์–ด ์„ผํ„ฐ" + +msgid "SYSTEM SETTINGS" +msgstr "ํ™˜๊ฒฝ ์„ค์ •" + +msgid "VERSION" +msgstr "๋ฒ„์ „" + +msgid "DISK USAGE" +msgstr "๋””์Šคํฌ ์‚ฌ์šฉ๋Ÿ‰" + +msgid "STORAGE DEVICE" +msgstr "์ €์žฅ ์žฅ์น˜" + +msgid "LANGUAGE" +msgstr "์–ธ์–ด" + +msgid "OVERCLOCK" +msgstr "์˜ค๋ฒ„ ํด๋Ÿญ" + +msgid "EXTREM (1100Mhz)" +msgstr "๊ทนํ•œ (1100MHz)" + +msgid "TURBO (1000Mhz)" +msgstr "ํ„ฐ๋ณด (1000Mhz)" + +msgid "HIGH (950Mhz)" +msgstr "๋†’์Œ (950Mhz)" + +msgid "NONE (700Mhz)" +msgstr "์—†์Œ (700Mhz)" + +msgid "TURBO (1050Mhz)+" +msgstr "ํ„ฐ๋ณด (1050Mhz)+" + +msgid "HIGH (1050Mhz)" +msgstr "๋†’์Œ (1050Mhz)" + +msgid "NONE (900Mhz)" +msgstr "์—†์Œ (900Mhz)" + +msgid "NONE (1200Mhz)" +msgstr "์—†์Œ (1200Mhz)" + +msgid "UPDATES" +msgstr "์—…๋ฐ์ดํŠธ" + +msgid "AUTO UPDATES" +msgstr "์ž๋™ ์—…๋ฐ์ดํŠธ" + +msgid "START UPDATE" +msgstr "์—…๋ฐ์ดํŠธ ์‹œ์ž‘" + +msgid "KODI SETTINGS" +msgstr "KODI ์„ค์ •" + +msgid "ENABLE KODI" +msgstr "KODI ํ™œ์„ฑํ™”" + +msgid "KODI AT START" +msgstr "์‹œ์ž‘์‹œ KODI ์‹คํ–‰" + +msgid "START KODI WITH X" +msgstr "์‹œ์ž‘์‹œ X๋ฒ„ํŠผ์œผ๋กœ KODI ์‹คํ–‰" + +msgid "SECURITY" +msgstr "๋ณด์•ˆ" + +msgid "ENFORCE SECURITY" +msgstr "๋ณด์•ˆ ๊ฐ•ํ™”" + +msgid "ROOT PASSWORD" +msgstr "๋ฃจํŠธ ๋น„๋ฐ€๋ฒˆํ˜ธ" + +msgid "THE SYSTEM WILL NOW REBOOT" +msgstr "์ด์ œ ์‹œ์Šคํ…œ์ด ๋‹ค์‹œ ๋ถ€ํŒ…๋ฉ๋‹ˆ๋‹ค" + +msgid "GAMES SETTINGS" +msgstr "๊ฒŒ์ž„ ์„ค์ •" + +msgid "GAME RATIO" +msgstr "๊ฒŒ์ž„ ํ™”๋ฉด ๋น„์œจ" + +msgid "SMOOTH GAMES" +msgstr "๊ฒŒ์ž„ ํ™”๋ฉด ๋ถ€๋“œ๋Ÿฝ๊ฒŒ" + +msgid "REWIND" +msgstr "๋˜๊ฐ๊ธฐ" + +msgid "AUTO SAVE/LOAD" +msgstr "์ž๋™ ์ €์žฅ/๋ถˆ๋Ÿฌ์˜ค๊ธฐ" + +msgid "SHADERS SET" +msgstr "์‰์ด๋”" + +msgid "SCANLINES" +msgstr "์Šค์บ”๋ผ์ธ" + +msgid "RETRO" +msgstr "๋ ˆํŠธ๋กœ" + +msgid "RETROACHIEVEMENTS SETTINGS" +msgstr "RETROACHIEVEMENTS ์„ค์ •" + +msgid "RETROACHIEVEMENTS" +msgstr "RETROACHIEVEMENTS" + +msgid "HARDCORE MODE" +msgstr "ํ•˜๋“œ ์ฝ”์–ด ๋ชจ๋“œ" + +msgid "USERNAME" +msgstr "์‚ฌ์šฉ์ž๋ช…" + +msgid "PASSWORD" +msgstr "์•”ํ˜ธ" + +msgid "ADVANCED" +msgstr "๊ณ ๊ธ‰" + +msgid "REALLY UPDATE GAMES LISTS ?" +msgstr "๊ฒŒ์ž„ ๋ชฉ๋ก์„ ์—…๋ฐ์ดํŠธ ํ•ฉ๋‹ˆ๊นŒ?" + +msgid "UPDATE GAMES LISTS" +msgstr "๊ฒŒ์ž„ ๋ชฉ๋ก ์—…๋ฐ์ดํŠธ" + +msgid "CONTROLLERS SETTINGS" +msgstr "์ปจํŠธ๋กค๋Ÿฌ ์„ค์ •" + +msgid "UI SETTINGS" +msgstr "UI ์„ค์ •" + +msgid "OVERSCAN" +msgstr "์˜ค๋ฒ„ ์Šค์บ”" + +msgid "LAUNCH SCREENSAVER" +msgstr "ํ™”๋ฉด๋ณดํ˜ธ๊ธฐ ์‹คํ–‰" + +msgid "SCREENSAVER SETTINGS" +msgstr "ํ™”๋ฉด๋ณดํ˜ธ๊ธฐ ์„ค์ •" + +msgid "SCREENSAVER AFTER" +msgstr "ํ™”๋ฉด๋ณดํ˜ธ๊ธฐ ์‹คํ–‰์‹œ๊ฐ„" + +msgid "TRANSITION STYLE" +msgstr "์ „ํ™˜ ์Šคํƒ€์ผ" + +msgid "SCREENSAVER BEHAVIOR" +msgstr "ํ™”๋ฉด๋ณดํ˜ธ๊ธฐ ๋ฐฉ์‹" + +msgid "random video" +msgstr "๋ฌด์ž‘์œ„ ๋™์˜์ƒ" + +msgid "slideshow" +msgstr "์Šฌ๋ผ์ด๋“œ์‡ผ" + +msgid "dim" +msgstr "ํ๋ฆฌ๊ฒŒ" + +msgid "black" +msgstr "๊ฒ€์€ ํ™”๋ฉด" + +msgid "SHOW FRAMERATE" +msgstr "FPS ํ‘œ์‹œ" + +msgid "ON-SCREEN HELP" +msgstr "์˜จ์Šคํฌ๋ฆฐ ๋„์›€๋ง" + +msgid "HIDE WHEN RUNNING GAME" +msgstr "๊ฒŒ์ž„ ์‹คํ–‰์‹œ ์ˆจ๊น€" + +msgid "QUICK SYSTEM SELECT" +msgstr "๋น ๋ฅธ ์‹œ์Šคํ…œ ์ „ํ™˜" + +msgid "THEME SET" +msgstr "ํ…Œ๋งˆ" + +msgid "SOUND SETTINGS" +msgstr "์‚ฌ์šด๋“œ ์„ค์ •" + +msgid "SYSTEM VOLUME" +msgstr "์‹œ์Šคํ…œ ๋ณผ๋ฅจ" + +msgid "FRONTEND MUSIC" +msgstr "ํ”„๋ก ํŠธ์—”๋“œ ์Œ์•…" + +msgid "ONLY PLAY SYSTEM-SPECIFIC MUSIC FOLDER" +msgstr "์‹œ์Šคํ…œ ํŠน์ • ์Œ์•… ํด๋” ๋งŒ ์žฌ์ƒ" + +msgid "FILTER GAMES BY TEXT" +msgstr "๋ฌธ์ž๋กœ ๊ฒŒ์ž„ ํ•„ํ„ฐ" + +msgid "OUTPUT DEVICE" +msgstr "์ถœ๋ ฅ ์žฅ์น˜" + +msgid "HDMI" +msgstr "HDMI" + +msgid "JACK" +msgstr "์˜ค๋””์˜ค์žญ" + +msgid "AUTO" +msgstr "์ž๋™" + +msgid "NETWORK SETTINGS" +msgstr "๋„คํŠธ์›Œํฌ ์„ค์ •" + +msgid "CONNECTED" +msgstr "์—ฐ๊ฒฐ๋จ" + +msgid "NOT CONNECTED" +msgstr "์—ฐ๊ฒฐ๋˜์ง€ ์•Š์Œ" + +msgid "STATUS" +msgstr "์ƒํƒœ" + +msgid "IP ADDRESS" +msgstr "IP ์ฃผ์†Œ" + +msgid "HOSTNAME" +msgstr "ํ˜ธ์ŠคํŠธ ์ด๋ฆ„" + +msgid "ENABLE WIFI" +msgstr "WIFI ์‚ฌ์šฉ" + +msgid "WIFI SSID" +msgstr "WIFI SSID" + +msgid "WIFI KEY" +msgstr "WIFI ํ‚ค" + +msgid "WIFI ENABLED" +msgstr "WIFI ํ™œ์„ฑ์ค‘" + +msgid "WIFI CONFIGURATION ERROR" +msgstr "WIFI ๊ตฌ์„ฑ ์˜ค๋ฅ˜" + +msgid "SCRAPER" +msgstr "์Šคํฌ๋ž˜ํผ" + +msgid "SCRAPE FROM" +msgstr "์Šคํฌ๋žฉ " + +msgid "SCRAPE RATINGS" +msgstr "ํ‰์  ์Šคํฌ๋žฉ" + +msgid "SCRAPE NOW" +msgstr "์ง€๊ธˆ ์Šคํฌ๋žฉ" + +msgid "QUIT" +msgstr "์ข…๋ฃŒ" + +msgid "QUIT EMULATIONSTATION" +msgstr "์—๋ฎฌ๋ ˆ์ด์…˜์Šคํ…Œ์ด์…˜ ๋‚˜๊ฐ€๊ธฐ" + +msgid "RESTART EMULATIONSTATION" +msgstr "์—๋ฎฌ๋ ˆ์ด์…˜์Šคํ…Œ์ด์…˜ ์žฌ์‹œ์ž‘" + +msgid "REALLY QUIT?" +msgstr "์ •๋ง๋กœ ๋‚˜๊ฐ‘๋‹ˆ๊นŒ?" + +msgid "REALLY RESTART?" +msgstr "์ •๋ง ๋‹ค์‹œ ์‹œ์ž‘ํ•ฉ๋‹ˆ๊นŒ?" + +msgid "RESTART SYSTEM" +msgstr "์‹œ์Šคํ…œ ์žฌ์‹œ์ž‘" + +msgid "REALLY SHUTDOWN?" +msgstr "์ •๋ง๋กœ ์ข…๋ฃŒํ•ฉ๋‹ˆ๊นŒ?" + +msgid "SHUTDOWN SYSTEM" +msgstr "์‹œ์Šคํ…œ ์ข…๋ฃŒ" + +msgid "EMULATOR" +msgstr "์—๋ฎฌ๋ ˆ์ดํ„ฐ" + +msgid "CORE" +msgstr "์ฝ”์–ด" + +msgid "YOU ARE GOING TO CONFIGURE A CONTROLLER. IF YOU HAVE ONLY ONE JOYSTICK, CONFIGURE THE DIRECTIONS KEYS AND SKIP JOYSTICK CONFIG BY HOLDING A BUTTON. IF YOU DO NOT HAVE A SPECIAL KEY FOR HOTKEY, CHOOSE THE SELECT BUTTON. SKIP ALL BUTTONS YOU DO NOT HAVE BY HOLDING A KEY. BUTTONS NAMES ARE BASED ON THE SNES CONTROLLER." +msgstr "์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ๊ตฌ์„ฑํ•˜๋ ค๊ณ ํ•ฉ๋‹ˆ๋‹ค. ํ•˜๋‚˜์˜ ์กฐ์ด์Šคํ‹ฑ ๋งŒ์žˆ๋Š” ๊ฒฝ์šฐ ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ ๋ฐฉํ–ฅํ‚ค์™€ ๊ฑด๋„ˆ ๋›ฐ๊ธฐ ์กฐ์ด์Šคํ‹ฑ ๊ตฌ์„ฑ์„ ๊ตฌ์„ฑํ•˜์‹ญ์‹œ์˜ค. ํ•ซํ‚ค ์šฉ ํŠน์ˆ˜ ํ‚ค๊ฐ€์—†๋Š” ๊ฒฝ์šฐ, ์„ ํƒ ๋ฒ„ํŠผ์„ ์„ ํƒํ•˜์‹ญ์‹œ์˜ค. ์—ด์‡ ๋ฅผ ์žก๊ณ  ์žˆ์ง€ ์•Š์€ ๋ชจ๋“  ๋‹จ์ถ”๋ฅผ ๊ฑด๋„ˆ ๋›ฐ์‹ญ์‹œ์˜ค. ๋ฒ„ํŠผ ์ด๋ฆ„์€ SNES ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœํ•ฉ๋‹ˆ๋‹ค." + +msgid "CONFIGURE A CONTROLLER" +msgstr "์ปจํŠธ๋กค๋Ÿฌ ๊ตฌ์„ฑ" + +msgid "CONTROLLER PAIRED" +msgstr "์ปจํŠธ๋กค๋Ÿฌ๊ฐ€ ํŽ˜์–ด๋ง๋จ" + +msgid "UNABLE TO PAIR CONTROLLER" +msgstr "์ปจํŠธ๋กค๋Ÿฌ ํŽ˜์–ด๋ง ๋ถˆ๊ฐ€" + +msgid "AN ERROR OCCURED" +msgstr "์˜ค๋ฅ˜ ๋ฐœ์ƒ" + +msgid "NO CONTROLLERS FOUND" +msgstr "์ปจํŠธ๋กค๋Ÿฌ ์—†์Œ" + +msgid "PAIR A BLUETOOTH CONTROLLER" +msgstr "๋ธ”๋ฃจํˆฌ์Šค ์ปจํŠธ๋กค๋Ÿฌ ํŽ˜์–ด๋ง" + +msgid "CONTROLLERS LINKS HAVE BEEN DELETED." +msgstr "์ปจํŠธ๋กค๋Ÿฌ ์—ฐ๊ฒฐ์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค." + +msgid "FORGET BLUETOOTH CONTROLLERS" +msgstr "๋ธ”๋ฃจํˆฌ์Šค ์ปจํŠธ๋กค๋Ÿฌ ์‚ญ์ œ" + +msgid "INPUT P%i" +msgstr "์ž…๋ ฅ P%i" + +msgid "CHOOSE" +msgstr "์ด๋™" + +msgid "SELECT" +msgstr "์„ ํƒ" + +msgid "OPTIONS" +msgstr "์˜ต์…˜" + +msgid "JUMP TO..." +msgstr "์ด๋™..." + +msgid "SORT GAMES BY" +msgstr "๊ฒŒ์ž„ ์ •๋ ฌ" + +msgid "FAVORITES ONLY" +msgstr "์ฆ๊ฒจ์ฐพ๊ธฐ๋งŒ" + +msgid "EDIT THIS GAME'S METADATA" +msgstr "ํ˜„์žฌ ๊ฒŒ์ž„์˜ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ˆ˜์ •" + +msgid "SCRAPE THESE GAMES" +msgstr "๋‹ค์Œ ๊ฒŒ์ž„ ์Šคํฌ๋žฉ" + +msgid "Only missing image" +msgstr "์ด๋ฏธ์ง€๊ฐ€ ์—†๋Š” ๊ฒŒ์ž„" + +msgid "FILTER" +msgstr "ํ•„ํ„ฐ" + +msgid "SCRAPE THESE SYSTEMS" +msgstr "๋‹ค์Œ ์‹œ์Šคํ…œ ์Šคํฌ๋žฉ" + +msgid "SYSTEMS" +msgstr "์‹œ์Šคํ…œ" + +msgid "USER DECIDES ON CONFLICTS" +msgstr "์ถฉ๋Œ์‹œ ์‚ฌ์šฉ์ž๊ฐ€ ์„ ํƒ" + +msgid "START" +msgstr "์‹œ์ž‘" + +msgid "WARNING: SOME OF YOUR SELECTED SYSTEMS DO NOT HAVE A PLATFORM SET. RESULTS MAY BE EVEN MORE INACCURATE THAN USUAL!\n" +"CONTINUE ANYWAY?" +msgid "๊ฒฝ๊ณ : ์„ ํƒ๋œ ์‹œ์Šคํ…œ์ค‘ ์ผ๋ถ€๊ฐ€ ํ”Œ๋žซํผ ์„ธํŠธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๊ฒฐ๊ณผ๊ฐ€ ์ •ํ™•ํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค!\n" +"๊ณ„์†ํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?" + +msgid "NO GAMES FIT THAT CRITERIA." +msgstr "์กฐ๊ฑด์— ๋งž๋Š” ๊ฒŒ์ž„์ด ์—†์Šต๋‹ˆ๋‹ค." + +msgid "REALLY UPDATE?" +msgstr "์—…๋ฐ์ดํŠธ ํ•ฉ๋‹ˆ๊นŒ?" + +msgid "NETWORK CONNECTION NEEDED" +msgstr "๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ํ•„์š”" + +msgid "UPDATE DOWNLOADED, THE SYSTEM WILL NOW REBOOT" +msgstr "์—…๋ฐ์ดํŠธ๊ฐ€ ๋‹ค์šด๋กœ๋“œ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์‹œ์Šคํ…œ์„ ๋‹ค์‹œ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค" + +msgid "UPDATE FAILED, THE SYSTEM WILL NOW REBOOT" +msgstr "์—…๋ฐ์ดํŠธ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ์‹œ์Šคํ…œ์„ ๋‹ค์‹œ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค" + +msgid "NO UPDATE AVAILABLE" +msgstr "์ด์šฉ ๊ฐ€๋Šฅํ•œ ์—…๋ฐ์ดํŠธ ์—†์Œ" + +msgid "enter emulator" +msgstr "์—๋ฎฌ๋ ˆ์ดํ„ฐ ์ž…๋ ฅ" + +msgid "enter core" +msgstr "์ฝ”์–ด ์ž…๋ ฅ" + +msgid "RATIO" +msgstr "๋น„์œจ" + +msgid "enter ratio" +msgstr "๋น„์œจ ์ž…๋ ฅ" + +msgid "NAME" +msgstr "์ด๋ฆ„" + +msgid "enter game name" +msgstr "๊ฒŒ์ž„ ์ด๋ฆ„ ์ž…๋ ฅ" + +msgid "DESCRIPTION" +msgstr "์„ค๋ช…" + +msgid "enter description" +msgstr "์„ค๋ช… ์ž…๋ ฅ" + +msgid "IMAGE" +msgstr "์ด๋ฏธ์ง€" + +msgid "enter path to image" +msgstr "์ด๋ฏธ์ง€ ๊ฒฝ๋กœ ์ž…๋ ฅ" + +msgid "THUMBNAIL" +msgstr "๋ฏธ๋ฆฌ๋ณด๊ธฐ" + +msgid "enter path to thumbnail" +msgstr "๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ ์ž…๋ ฅ" + +msgid "enter rating" +msgstr "ํ‰์  ์ž…๋ ฅ" + +msgid "RELEASE DATE" +msgstr "๋ฐœ๋งค์ผ" + +msgid "enter release date" +msgstr "๋ฐœ๋งค์ผ ์ž…๋ ฅ" + +msgid "enter game developer" +msgstr "๊ฐœ๋ฐœ์‚ฌ ์ž…๋ ฅ" + +msgid "enter game publisher" +msgstr "ํŒ๋งค์‚ฌ ์ž…๋ ฅ" + +msgid "enter game genre" +msgstr "์žฅ๋ฅด ์ž…๋ ฅ" + +msgid "enter number of players" +msgstr "ํ”Œ๋ ˆ์ด์–ด ์ˆ˜ ์ž…๋ ฅ" + +msgid "enter favorite" +msgstr "์ฆ๊ฒจ์ฐพ๊ธฐ ์ž…๋ ฅ" + +msgid "Region" +msgstr "์ง€์—ญ" + +msgid "enter region" +msgstr "์ง€์—ญ ์ž…๋ ฅ" + +msgid "Romtype" +msgstr "๋กฌ ํƒ€์ž…" + +msgid "enter romtype" +msgstr "๋กฌ ํƒ€์ž… ์ž…๋ ฅ" + +msgid "HIDDEN" +msgstr "์ˆจ๊น€" + +msgid "set hidden" +msgstr "์ˆจ๊น€ ์„ค์ •" + +msgid "Play count" +msgstr "์‹คํ–‰ ํšŸ์ˆ˜" + +msgid "enter number of times played" +msgstr "์‹คํ–‰ ํšŸ์ˆ˜ ์ž…๋ ฅ" + +msgid "Last played" +msgstr "์ตœ๊ทผ ์‹คํ–‰" + +msgid "enter last played date" +msgstr "์ตœ๊ทผ ์‹คํ–‰ ์ผ์ž ์ž…๋ ฅ" + +msgid "GAMES AVAILABLE" +msgid "๊ฒŒ์ž„ ์ด์šฉ๊ฐ€๋Šฅ" + +msgid "%i GAME AVAILABLE" +msgid_plural "%i GAMES AVAILABLE" +msgstr[0] "%i๊ฐœ์˜ ๊ฒŒ์ž„ ์ด์šฉ๊ฐ€๋Šฅ" +msgstr[1] "%i๊ฐœ์˜ ๊ฒŒ์ž„ ์ด์šฉ๊ฐ€๋Šฅ" + +msgid "%i FAVORITE" +msgid_plural "%i FAVORITES" +msgstr[0] "%i๊ฐœ์˜ ์ฆ๊ฒจ์ฐพ๊ธฐ" +msgstr[0] "%i๊ฐœ์˜ ์ฆ๊ฒจ์ฐพ๊ธฐ" + +msgid "SCROLL" +msgstr "์Šคํฌ๋กค" + +msgid "LAUNCH" +msgstr "์‹คํ–‰" + +msgid "Times played" +msgstr "์‹คํ–‰ ํšŸ์ˆ˜" + +msgid "MENU" +msgstr "๋ฉ”๋‰ด" + +msgid "FILENAME, ASCENDING" +msgstr "ํŒŒ์ผ๋ช… ์˜ค๋ฆ„์ฐจ์ˆœ" + +msgid "FILENAME, DESCENDING" +msgstr "ํŒŒ์ผ๋ช… ๋‚ด๋ฆผ์ฐจ์ˆœ" + +msgid "RATING, ASCENDING" +msgstr "ํ‰์  ์˜ค๋ฆ„์ฐจ์ˆœ" + +msgid "RATING, DESCENDING" +msgstr "ํ‰์  ๋‚ด๋ฆผ์ฐจ์ˆœ" + +msgid "TIMES PLAYED, ASCENDING" +msgstr "์‹คํ–‰ ํšŸ์ˆ˜ ์˜ค๋ฆ„์ฐจ์ˆœ" + +msgid "TIMES PLAYED, DESCENDING" +msgstr "์‹คํ–‰ ํšŸ์ˆ˜ ๋‚ด๋ฆผ์ฐจ์ˆœ" + +msgid "LAST PLAYED, ASCENDING" +msgstr "์ตœ๊ทผ ์‹คํ–‰ ์˜ค๋ฆ„์ฐจ์ˆœ" + +msgid "LAST PLAYED, DESCENDING" +msgstr "์ตœ๊ทผ ์‹คํ–‰ ๋‚ด๋ฆผ์ฐจ์ˆœ" + +msgid "WORKING..." +msgstr "์ž‘์—… ์ค‘..." + +msgid "CHANGE" +msgstr "๋ณ€๊ฒฝ" + +msgid "just now" +msgid "๋ฐฉ๊ธˆ" + +msgid "%i sec ago" +msgid_plural "%i secs ago" +msgstr[0] "%i์ดˆ ์ „" +msgstr[1] "%i์ดˆ ์ „" + +msgid "%i min ago" +msgid_plural "%i mins ago" +msgstr[0] "%i๋ถ„ ์ „" +msgstr[1] "%i๋ถ„ ์ „" + +msgid "%i hour ago" +msgid_plural "%i hours ago" +msgstr[0] "%i์‹œ๊ฐ„ ์ „" +msgstr[1] "%i์‹œ๊ฐ„ ์ „" + +msgid "%i day ago" +msgid_plural "%i days ago" +msgstr[0] "%i์ผ ์ „" +msgstr[1] "%i์ผ ์ „" + +msgid "unknown" +msgstr "์•Œ์ˆ˜์—†์Œ" + +msgid "SELECT ALL" +msgstr "๋ชจ๋‘ ์„ ํƒ" + +msgid "SELECT NONE" +msgstr "์„ ํƒ ํ•ด์ œ" + +msgid "UP" +msgstr "์œ„" + +msgid "DOWN" +msgstr "์•„๋ž˜" + +msgid "LEFT" +msgstr "์ขŒ" + +msgid "RIGHT" +msgstr "์šฐ" + +msgid "JOYSTICK 1 UP" +msgstr "์กฐ์ด์Šคํ‹ฑ 1 ์œ„" + +msgid "JOYSTICK 1 LEFT" +msgstr "์กฐ์ด์Šคํ‹ฑ 1 ์ขŒ" + +msgid "JOYSTICK 2 UP" +msgstr "์กฐ์ด์Šคํ‹ฑ 2 ์œ„" + +msgid "JOYSTICK 2 LEFT" +msgstr "์กฐ์ด์Šคํ‹ฑ 2 ์ขŒ" + +msgid "PAGE UP" +msgstr "ํŽ˜์ด์ง€ ์œ„๋กœ" + +msgid "PAGE DOWN" +msgstr "ํŽ˜์ด์ง€ ์•„๋ž˜๋กœ" + +msgid "HOTKEY" +msgstr "ํ•ซํ‚ค" + +msgid "CONFIGURING" +msgstr "์„ค์ •์ค‘" + +msgid "KEYBOARD" +msgstr "ํ‚ค๋ณด๋“œ" + +msgid "GAMEPAD %i" +msgstr "๊ฒŒ์ž„ํŒจ๋“œ %i" + +msgid "PRESS ANYTHING" +msgstr "์•„๋ฌด ๋ฒ„ํŠผ์ด๋‚˜ ๋ˆ„๋ฅด์„ธ์š”" + +msgid "ALREADY TAKEN" +msgstr "์ด๋ฏธ ์‚ฌ์šฉ์ค‘์ž…๋‹ˆ๋‹ค" + +msgid "DISCARD CHANGES" +msgstr "๋ณ€๊ฒฝ ์ทจ์†Œ" + +msgid "WELCOME" +msgstr "ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค" + +msgid "CONFIGURE INPUT" +msgstr "์ž…๋ ฅ ์„ค์ •" + +msgid "%i GAMEPAD DETECTED" +msgid_plural "%i GAMEPADS DETECTED" +msgstr[0] "%i๊ฐœ์˜ ๊ฒŒ์ž„ํŒจ๋“œ ํƒ์ง€๋จ" +msgstr[1] "%i๊ฐœ์˜ ๊ฒŒ์ž„ํŒจ๋“œ ํƒ์ง€๋จ" + +msgid "NO GAMEPADS DETECTED" +msgstr "๊ฒ€์ƒ‰๋œ ๊ฒŒ์ž„ํŒจ๋“œ ์—†์Œ" + +msgid "HOLD A BUTTON ON YOUR DEVICE TO CONFIGURE IT." +msgstr "์„ค์ • ํ•˜๋ ค๋ฉด ์žฅ์น˜์˜ ๋ฒ„ํŠผ์„ ๊ธธ๊ฒŒ ๋ˆ„๋ฅด์„ธ์š”." + +msgid "PRESS F4 TO QUIT AT ANY TIME." +msgstr "F4 ํ‚ค๋ฅผ ๋ˆ„๋ฅด๋ฉด ์–ธ์ œ๋“  ์ข…๋ฃŒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค." + +msgid "PRESS ESC OR THE HOTKEY TO CANCEL." +msgstr "์ทจ์†Œํ•˜๋ ค๋ฉด ESCํ‚ค๋‚˜ ํ•ซํ‚ค๋ฅผ ๋ˆ„๋ฅด์„ธ์š”." + +msgid "DO YOU WANT TO START KODI MEDIA CENTER ?" +msgstr "KODI ๋ฏธ๋””์–ด ์„ผํ„ฐ๋ฅผ ์‹œ์ž‘ ํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?" + +msgid "LOADING..." +msgstr "๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘..." + +msgid "PLEASE WAIT..." +msgstr "๊ธฐ๋‹ค๋ ค์ฃผ์‹ญ์‹œ์˜ค..." + +msgid "REALLY SHUTDOWN WITHOUT SAVING METADATAS?" +msgstr "๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜์ง€ ์•Š๊ณ  ์™„์ „ํžˆ ์ข…๋ฃŒ ํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?" + +msgid "FAST SHUTDOWN SYSTEM" +msgstr "๋น ๋ฅธ ์‹œ์Šคํ…œ ์ข…๋ฃŒ" + +msgid "ON SCREEN KEYBOARD" +msgstr "์˜จ์Šคํฌ๋ฆฐ ํ‚ค๋ณด๋“œ" + +msgid "SHIFTS FOR UPPER,LOWER, AND SPECIAL" +msgstr "์‹œํ”„ํŠธํ‚ค๋กœ ๋Œ€์†Œ๋ฌธ์ž ๋ฐ ํŠน์ˆ˜๋ฌธ์ž ์ „ํ™˜" + +msgid "SPACE" +msgstr "์ŠคํŽ˜์ด์Šค" + +msgid "DELETE A CHAR" +msgstr "๋ฌธ์ž ์‚ญ์ œ" + +msgid "SHIFT" +msgstr "์‹œํ”„ํŠธ" + +msgid "STOP EDITING" +msgstr "์ˆ˜์ • ์ข…๋ฃŒ" + +msgid "MOVE CURSOR" +msgstr "์ปค์„œ ์ด๋™" + +msgid "EDIT" +msgstr "์ˆ˜์ •" + +msgid "ACCEPT RESULT" +msgstr "๊ฒฐ๊ณผ ํ™•์ธ" + +msgid "FILENAME" +msgstr "ํŒŒ์ผ๋ช…" + +msgid "TIMES PLAYED" +msgstr "์‹คํ–‰ ํšŸ์ˆ˜" + +msgid "LAST PLAYED" +msgstr "์ตœ๊ทผ ์‹คํ–‰" + +msgid "NUMBER OF PLAYERS" +msgstr "ํ”Œ๋ ˆ์ด์–ด ์ˆ˜" + +msgid "SHOW HIDDEN" +msgstr "์ˆจ๊น€ ํ‘œ์‹œ" + +msgid "EXTREM (1400Mhz)" +msgstr "๊ทนํ•œ (1400Mhz)" + +msgid "TURBO (1350Mhz)" +msgstr "ํ„ฐ๋ณด (1350Mhz)" + +msgid "HIGH (1300Mhz)" +msgstr "๋†’์Œ (1300Mhz)" + +msgid "TURBO AND EXTREM OVERCLOCK PRESETS MAY CAUSE SYSTEM UNSTABILITIES, SO USE THEM AT YOUR OWN RISK.\n" +"IF YOU CONTINUE, THE SYSTEM WILL REBOOT NOW." +msgstr "ํ„ฐ๋ณด ๋ฐ ๊ทนํ•œ ์˜ค๋ฒ„ ํด๋Ÿญ ์„ค์ •์€ ์‹œ์Šคํ…œ์„ ๋ถˆ์•ˆ์ •ํ•˜๊ฒŒ ํ•  ์ˆ˜ ์žˆ์œผ๋‹ˆ ๋ณธ์ธ ์ฑ…์ž„ํ•˜์— ์‚ฌ์šฉํ•˜์‹ญ์‹œ์˜ค.\n" +"๊ณ„์†ํ•˜๋ฉด ์‹œ์Šคํ…œ์„ ๋‹ค์‹œ ์‹œ์ž‘ ํ•ฉ๋‹ˆ๋‹ค." + +msgid "%i GAME HIDDEN" +msgid_plural "%i GAMES HIDDEN" +msgstr[0] "%i ๊ฒŒ์ž„ ์ˆจ๊น€" +msgstr[1] "%i ๊ฒŒ์ž„ ์ˆจ๊น€" + +msgid "Start kodi media player." +msgstr "kodi ๋ฏธ๋””์–ด ํ”Œ๋ ˆ์ด์–ด๋ฅผ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค." + +msgid "Show how much space is used on your SHARE partition, located either on the SDCARD or on an external drive. The information shows how much GB are used and how much GB your storage has overall (example 13GB/26GB)." +msgid "SDCARD๋‚˜ ์™ธ์žฅ๋“œ๋ผ์ด๋ธŒ์— ๊ณต์œ  ํŒŒํ‹ฐ์…˜์ด ์‚ฌ์šฉ๋˜๋Š” ์šฉ๋Ÿ‰์„ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. ์ •๋ณด๋Š” ์‚ฌ์šฉ๋œ ์šฉ๋Ÿ‰(GB)๊ณผ ์ „์ฒด ์ €์žฅ์†Œ ์šฉ๋Ÿ‰(GB)์œผ๋กœ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. (์˜ˆ 13GB/26GB)" + +msgid "Configure games display, ratio, filters (shaders), auto save and load and retroachievement account." +msgstr "๊ฒŒ์ž„ ํ™”๋ฉด, ๋น„์œจ, ํ•„ํ„ฐ (์‰์ด๋”), ์ž๋™ ์ €์žฅ/๋ถˆ๋Ÿฌ์˜ค๊ธฐ ๋ฐ retroachievement ๊ณ„์ •์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค." + +msgid "The game ratio is the ratio between image width and image height. Use AUTO to let the emulator choose the original game ratio, that will give you the best retrogaming experience." +msgstr "๊ฒŒ์ž„ ๋น„์œจ์€ ์ด๋ฏธ์ง€ ๋„ˆ๋น„์™€ ๋†’์ด ์‚ฌ์ด์˜ ๋น„์œจ์ž…๋‹ˆ๋‹ค. ์—๋ฎฌ๋ ˆ์ดํ„ฐ๊ฐ€ ์›๋ณธ ๊ฒŒ์ž„ ๋น„์œจ์„ ์„ ํƒํ•˜๊ฒŒ ํ•ด์„œ ์ตœ์ƒ์˜ ๋ ˆํŠธ๋กœ ๊ฒŒ์ž„ ๊ฒฝํ—˜์„ ์ œ๊ณต๋ฐ›์œผ๋ ค๋ฉด ์ž๋™ ์„ค์ •์„ ์‚ฌ์šฉํ•˜์‹ญ์‹œ์˜ค." + +msgid "Smooth the game image. This option makes the image smoother, using bilinear filtering." +msgstr "๊ฒŒ์ž„ ์ด๋ฏธ์ง€๋ฅผ ๋ถ€๋“œ๋Ÿฝ๊ฒŒํ•ฉ๋‹ˆ๋‹ค. ์ด ์˜ต์…˜์€ ์ด์ค‘ ์„ ํ˜• ํ•„ํ„ฐ๋ง์„ ์‚ฌ์šฉํ•˜์—ฌ ์ด๋ฏธ์ง€๋ฅผ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ๋งŒ๋“ญ๋‹ˆ๋‹ค." + +msgid "This option allows you to rewind the game if you get killed by a monster, or if you make any other mistake. Use the HOTKEY + LEFT command within the game to rewind." +msgstr "์ด ์˜ต์…˜์„ ์‚ฌ์šฉํ•˜๋ฉด ์ ์—๊ฒŒ ์ฃฝ๊ฑฐ๋‚˜ ์‹ค์ˆ˜ ํ–ˆ์„ ๋•Œ ๊ฒŒ์ž„์„ ๋’ค๋กœ ๋˜๊ฐ๊ฒŒ ํ•ด์ค๋‹ˆ๋‹ค. ๊ฒŒ์ž„ ๋‚ด์—์„œ ํ•ซํ‚ค + ์ขŒ ๋ฅผ ์ž…๋ ฅํ•˜์‹ญ์‹œ์˜ค" + +msgid "Auto save the state when you quit a game, and auto load last saved state when you start a game." +msgstr "๊ฒŒ์ž„์„ ์ข…๋ฃŒ ํ•  ๋•Œ ์ƒํƒœ๋ฅผ ์ž๋™ ์ €์žฅํ•˜๊ณ  ๊ฒŒ์ž„์„ ์‹œ์ž‘ํ•  ๋•Œ ๋งˆ์ง€๋ง‰์œผ๋กœ ์ €์žฅ๋œ ์ƒํƒœ๋ฅผ ์ž๋™์œผ๋กœ ๋ถˆ๋Ÿฌ์˜ต๋‹ˆ๋‹ค." + +msgid "Integer scaling is scaling by a factor of a whole number, such as 2x, 3x, 4x, etc. This option scales the image up to the greatest integer scale below the set resolution. So for instance, if you set your fullscreen resolution to 1920x1080 and enable integer scaling, it will only scale a 320x240 image up to 1280x960, and leave black borders all around. This is to maintain a 1:1 pixel ratio with the original source image, so that pixels are not unevenly duplicated." +msgid "์ •์ˆ˜๋‹จ์œ„ ํ™•๋Œ€๋Š” 2x, 3x, 4x ๋“ฑ ์ •์ˆ˜ ๋‹จ์œ„๋กœ์˜ ํ™•๋Œ€๋ฅผ ๋งํ•ฉ๋‹ˆ๋‹ค. ์ด ์˜ต์…˜์€ ์„ค์ •๋œ ํ•ด์ƒ๋„ ์•ˆ์—์„œ ๊ฐ€์žฅ ํฐ ์ •์ˆ˜ ๋‹จ์œ„ ์ด๋ฏธ์ง€๋ฅผ ํ™•๋Œ€ํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด ํ™”๋ฉด ํ•ด์ƒ๋„๊ฐ€ 1920x1080์ด๊ณ  ์ •์ˆ˜๋‹จ์œ„ ํ™•๋Œ€๊ฐ€ ์ผœ์ ธ์žˆ์œผ๋ฉด 320x240 ํ™”๋ฉด์€ 1280x960๋กœ ํ™•๋Œ€๋˜๊ณ  ๋‚˜๋จธ์ง€ ๋นˆ ๊ณต๊ฐ„์€ ๊ฒ€์€ ๊ณต๋งฅ์œผ๋กœ ๋‚จ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์ด ์„ค์ •์œผ๋กœ ์›๋ณธ ์†Œ์Šค ์ด๋ฏธ์ง€์˜ 1:1ํ”ฝ์…€ ๋น„์œจ์„ ์œ ์ง€ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค." + +msgid "Shaders are like filters for the game rendering. You can select a shader set here, which is a collection of shaders selected for each system. You can also change the shader within the game with HOTKEY + L2 or HOTKEY + R2." +msgstr "์…ฐ์ด๋”๋Š” ๊ฒŒ์ž„ ๋ Œ๋”๋ง์„์œ„ํ•œ ํ•„ํ„ฐ์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์—์„œ ๊ฐ๊ฐ์˜ ์‹œ์Šคํ…œ์— ๋Œ€ํ•œ ์‰์ด๋” ๋ชจ์Œ์„ ์„ ํƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•ซํ‚ค + L2 ๋˜๋Š” ํ•ซํ‚ค + R2๋ฅผ ์‚ฌ์šฉํ•ด ๊ฒŒ์ž„ ๋‚ด์—์„œ๋„ ์…ฐ์ด๋”๋ฅผ ๋ณ€๊ฒฝํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค." + +msgid "Enable or disable RetroAchievements in games." +msgid "๊ฒŒ์ž„์—์„œ RetroAchievements ๋ฅผ ํ™œ์„ฑํ™” ๋˜๋Š” ๋น„ํ™œ์„ฑํ™”ํ•ฉ๋‹ˆ๋‹ค." + +msgid "Hardcore mode disables *all* savestate and rewind functions within the emulator: you will not be able to save and reload at any time. You will have to complete the game and get the achievements first time, just like on the original console. In reward for this, you will earn both the standard and the hardcore achievement, in effect earning double points! A regular game worth 400 points, is now worth 800 if you complete it on hardcore! For example: if you complete the game for 400 points, you then have the opportunity to earn another 400 on hardcore." +msgid "ํ•˜๋“œ์ฝ”์–ด ๋ชจ๋“œ ์„ค์ •์‹œ ๋ชจ๋“  ์ƒํƒœ์ €์žฅ ๋ฐ ๋˜๊ฐ๊ธฐ ๊ธฐ๋Šฅ์ด ๋น„ํ™œ์„ฑ๋ฉ๋‹ˆ๋‹ค : ์•„๋ฌด๋•Œ๋‚˜ ๋ถˆ๋Ÿฌ์˜ค๊ณ  ์ €์žฅํ•˜์ง€ ๋ชปํ•˜๊ฒŒ ๋˜๋ฉฐ ์˜ค๋ฆฌ์ง€๋„ ์ฝ˜์†”์—์„œ ์ฒ˜๋Ÿผ ํ•œ ๋ฒˆ์— ๊ฒŒ์ž„์„ ๋๋‚ด์•ผ ๋„์ „๊ณผ์ œ๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ณด์ƒ์œผ๋กœ ์ผ๋ฐ˜ ๋ฐ ํ•˜๋“œ์ฝ”์–ด ๋„์ „๊ณผ์ œ๋ฅผ ๋™์‹œ์— ์–ป๊ฒŒ ๋˜๊ณ  ๋‘๋ฐฐ์˜ ํฌ์ดํŠธ๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค! + +msgid "The website retroachievements.org proposes challenges/achievements/trophies on platforms like NES, SNES, GB, GBC, GBA, Genesis/Megadrive, TurboGrafx16/PCEngine and more! Create your account on retroachievements.org and start your quest for achievements!" +msgid "retroachievements.org์—์„œ NES, SNES, GB, GBC, GBA, ๋ฉ”๊ฐ€๋“œ๋ผ์ด๋ธŒ, PC์—”์ง„ ๋“ฑ ํ”Œ๋žซํผ์— ๋Œ€ํ•œ ๋„์ „๊ณผ์ œ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. retroachievements.org์—์„œ ๊ณ„์ •์„ ์ƒ์„ฑํ•˜๊ณ  ๋„์ „๊ณผ์ œ์— ๋„์ „ํ•ด ๋ณด์„ธ์š”!" + +msgid "Add and configure up to 5 controllers." +msgstr "์ตœ๋Œ€ 5 ๊ฐœ์˜ ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ  ๊ตฌ์„ฑํ•˜์‹ญ์‹œ์˜ค." + +msgid "Start the screensaver after N minutes." +msgstr "N๋ถ„ ํ›„ ํ™”๋ฉด ๋ณดํ˜ธ๊ธฐ๋ฅผ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค." + +msgid "Set the screensaver behavior. DIM will reduce the screen light, and BLACK will turn the screen black." +msgstr "ํ™”๋ฉด๋ณดํ˜ธ๊ธฐ ๋™์ž‘์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. ์–ด๋‘ก๊ฒŒ๋Š” ํ™”๋ฉด ์กฐ๋ช…์„ ์–ด๋‘ก๊ฒŒํ•˜๊ณ  ๊ฒ€์€ํ™”๋ฉด์€ ํ™”๋ฉด์„ ๊ฒ€๊ฒŒ ๋งŒ๋“ญ๋‹ˆ๋‹ค." + +msgid "Shows a help at the bottom of the screen which displays commands you can use." +msgstr "ํ™”๋ฉด ํ•˜๋‹จ์— ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ช…๋ น์„ ๋‚˜์˜จ ๋„์›€๋ง์ด ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค." + +msgid "When enabled, you can switch between systems while browsing a gamelist by pressing LEFT or RIGHT." +msgstr "ํ™œ์„ฑํ™”ํ•˜๋ฉด ๊ฒŒ์ž„ ๋ชฉ๋ก์„ ํƒ์ƒ‰ํ•˜๋Š” ๋™์•ˆ ์ขŒ/์šฐ ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ ์‹œ์Šคํ…œ๊ฐ„ ์ „ํ™˜์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค." + +msgid "Updates the gamelists, if you added games since the last boot." +msgstr "๋งˆ์ง€๋ง‰ ๋ถ€ํŒ… ์ดํ›„ ๊ฒŒ์ž„์„ ์ถ”๊ฐ€ ํ•œ ๊ฒฝ์šฐ ๊ฒŒ์ž„ ๋ชฉ๋ก์„ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค." + +msgid "Set the volume of the sound output for the frontend and the games." +msgstr "ํ”„๋ก ํŠธ์—”๋“œ ๋ฐ ๊ฒŒ์ž„์˜ ์‚ฌ์šด๋“œ ์ถœ๋ ฅ ๋ณผ๋ฅจ์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค." + +msgid "Get informations and visual for your games. The scraper downloads metadata and visuals for your games from different servers and enhances the user experience in EmulationStation completely." +msgstr "๊ฒŒ์ž„ ์ •๋ณด์™€ ์ด๋ฏธ์ง€๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. ์Šคํฌ๋ž˜ํผ๋Š” ๋‹ค๋ฅธ ์„œ๋ฒ„์—์„œ ๊ฒŒ์ž„์— ๋Œ€ํ•œ ๋ฉ”ํƒ€ ๋ฐ์ดํ„ฐ์™€ ์‹œ๊ฐ ์ž๋ฃŒ๋ฅผ ๋‹ค์šด๋กœ๋“œํ•˜๊ณ  EmulationStation์˜ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ํ–ฅ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค." + +msgid "Select a server to scrape from. The SCREENSCRAPER server is recommended and is based on www.screenscraper.fr and scrapes game data in your language, if available." +msgstr "์Šคํฌ๋žฉ ํ•  ์„œ๋ฒ„๋ฅผ ์„ ํƒํ•˜์‹ญ์‹œ์˜ค. SCREENSCRAPER ์„œ๋ฒ„๋ฅผ ๊ถŒ์žฅํ•˜๋ฉฐ www.screenscraper.fr์„ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ฒŒ์ž„ ๋ฐ์ดํ„ฐ๋ฅผ ์Šคํฌ๋žฉํ•ฉ๋‹ˆ๋‹ค." + +msgid "Begin the scrape process with the configuration shown below." +msgstr "์•„๋ž˜ ํ‘œ์‹œ๋œ ๊ตฌ์„ฑ์œผ๋กœ ์Šคํฌ๋žฉ ์ž‘์—…์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค." + +msgid "Scrape and display game ratings." +msgstr "๊ฒŒ์ž„ ํ‰์ ์„ ๊ฐ€์ ธ์˜ค๊ณ  ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค." + +msgid "Advanced settings. Please make sure you really know what you're doing, before changing any values in this menu." +msgstr "๊ณ ๊ธ‰ ์„ค์ •์ž…๋‹ˆ๋‹ค. ๋ฉ”๋‰ด์—์„œ ๊ฐ’์„ ๋ณ€๊ฒฝํ•˜๊ธฐ ์ „์— ๋ฌด์—‡์„ ์ˆ˜์ •ํ•˜๋Š”์ง€ ์•Œ๊ณ  ์žˆ๋Š”์ง€ ํ™•์ธํ•˜์‹ญ์‹œ์˜ค." + +msgid "Overclock your board to increase the performance.\n" +msgstr "์„ฑ๋Šฅ์„ ๋†’์ด๋ ค๋ฉด ์˜ค๋ฒ„ ํด๋กœํ‚นํ•˜์‹ญ์‹œ์˜ค. \n" + +msgid "Only show games contained in the gamelist.xml file (located in your roms directories).\n" +msgstr "gamelist.xml ์— ํฌํ•จ๋œ ๊ฒŒ์ž„๋งŒ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค\n" + +msgid "This option allows you to set the selected system to fixed mode. With this option activated, the user cannot access other systems." +msgstr "์ด ์˜ต์…˜์„ ์‚ฌ์šฉํ•˜๋ฉด ์„ ํƒํ•œ ์‹œ์Šคํ…œ์„ ๊ณ ์ • ๋ชจ๋“œ๋กœ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ™œ์„ฑํ™”ํ•˜๋ฉด ์‚ฌ์šฉ์ž๋Š” ๋‹ค๋ฅธ ์‹œ์Šคํ…œ์— ์•ก์„ธ์Šค ํ•  ์ˆ˜ ์—†๊ฒŒ๋ฉ๋‹ˆ๋‹ค." + +msgid "Always display the basic gamelist view, even if you have scraped your games." +msgstr "๊ฒŒ์ž„ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜จ ๊ฒฝ์šฐ์—๋„ ํ•ญ์ƒ ๊ธฐ๋ณธ ๊ฒŒ์ž„ ๋ชฉ๋ก๋ณด๊ธฐ๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค." + +msgid "Enable or disable Kodi, customize the Kodi startup, enable the X button to start Kodi" +msgstr "Kodi ํ™œ์„ฑํ™” ๋˜๋Š” ๋น„ํ™œ์„ฑํ™”, Kodi ์‹œ์ž‘ ์‚ฌ์šฉ์ž ์ •์˜, X ๋ฒ„ํŠผ์œผ๋กœ Kodi ์‹œ์ž‘" + +msgid "Enable or disable Kodi. If kodi is disabled, you won't be able to start it with the X button, or start it automatically at boot. The menu entry will be removed as well." +msgstr "Kodi๋ฅผ ํ™œ์„ฑํ™” ๋˜๋Š” ๋น„ํ™œ์„ฑํ™”ํ•ฉ๋‹ˆ๋‹ค. kodi๊ฐ€ ๋น„ํ™œ์„ฑํ™”๋˜์–ด ์žˆ์œผ๋ฉด X ๋ฒ„ํŠผ์œผ๋กœ ์‹œ์ž‘ํ•˜๊ฑฐ๋‚˜ ๋ถ€ํŒ…์‹œ ์ž๋™์œผ๋กœ ์‹œ์ž‘ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋ฉ”๋‰ด ํ•ญ๋ชฉ๋„ ์ œ๊ฑฐ๋ฉ๋‹ˆ๋‹ค." + +msgid "Use the X button to start Kodi." +msgstr "X ๋ฒ„ํŠผ์„ ์‚ฌ์šฉํ•˜์—ฌ Kodi๋ฅผ ์‹œ์ž‘ํ•˜์‹ญ์‹œ์˜ค." + +msgid "Automatically start into Kodi on boot." +msgstr "๋ถ€ํŒ…์‹œ Kodi๋ฅผ ์ž๋™์œผ๋กœ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค." + +msgid "Show the framerate in EmulationStation and in game." +msgstr "EmulationStation ๋ฐ ๊ฒŒ์ž„์—์„œ ํ”„๋ ˆ์ž„๋ ˆ์ดํŠธ๋ฅผ ํ‘œ์‹œํ•˜์‹ญ์‹œ์˜ค." + +msgid "Select which emulator to use when you start a game for this system." +msgstr "ํ˜„์žฌ ์‹œ์Šคํ…œ์—์„œ ๊ฒŒ์ž„์„ ์‹œ์ž‘ํ•  ๋•Œ ์‚ฌ์šฉํ•  ์—๋ฎฌ๋ ˆ์ดํ„ฐ๋ฅผ ์„ ํƒํ•˜์‹ญ์‹œ์˜ค." + +msgid "Select which core to use for the selected emulator. For example, the LIBRETRO emulator has many cores to run Super Nintendo games. The default core you choose here can also be overridden in game specific settings." +msgstr "์„ ํƒํ•œ ์—๋ฎฌ๋ ˆ์ดํ„ฐ์— ์‚ฌ์šฉํ•  ์ฝ”์–ด๋ฅผ ์„ ํƒํ•˜์‹ญ์‹œ์˜ค. ์˜ˆ๋ฅผ ๋“ค์–ด, LIBRETRO ์—๋ฎฌ๋ ˆ์ดํ„ฐ์—๋Š” Super Nintendo ๊ฒŒ์ž„์„ ์‹คํ–‰ํ•˜๊ธฐ ์œ„ํ•œ ๋งŽ์€ ์ฝ”์–ด๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ธฐ๋ณธ ์ฝ”์–ด๋Š” ๊ฒŒ์ž„ ๋ณ„ ์„ค์ •์—์„œ ์žฌ์ •์˜ ๋  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค." + +msgid "USE COMPOSED VISUALS" +msgstr "์กฐํ•ฉ๋œ ์ด๋ฏธ์ง€๋ฅผ ์‚ฌ์šฉ" + +msgid "CHECK UPDATES" +msgstr "์—…๋ฐ์ดํŠธ ํ™•์ธ" + +msgid "UPDATE TYPE" +msgstr "์—…๋ฐ์ดํŠธ ์œ ํ˜•" + +msgid "INTEGER SCALE (PIXEL PERFECT)" +msgstr "์ •์ˆ˜๋‹จ์œ„ ํ™•๋Œ€" + +msgid "ADVANCED SETTINGS" +msgstr "๊ณ ๊ธ‰ ์„ค์ •" + +msgid "BOOT SETTINGS" +msgstr "๋ถ€ํŠธ ์„ค์ •" + +msgid "GAMELIST ONLY" +msgid "๊ฒŒ์ž„๋ชฉ๋ก ๋งŒ" + +msgid "START ON SYSTEM" +msgstr "์‹œ์ž‘ ์‹œ์Šคํ…œ" + +msgid "BOOT ON GAMELIST" +msgid "๊ฒŒ์ž„๋ชฉ๋ก์œผ๋กœ ์‹œ์ž‘" + +msgid "HIDE SYSTEM VIEW" +msgstr "์‹œ์Šคํ…œ ๋ณด๊ธฐ ์ˆจ๊น€" + +msgid "EMULATOR ADVANCED CONFIGURATION" +msgstr "๊ณ ๊ธ‰ ์—๋ฎฌ๋ ˆ์ดํ„ฐ ์„ค์ •" + +msgid "ADVANCED EMULATOR CONFIGURATION" +msgstr "๊ณ ๊ธ‰ ์—๋ฎฌ๋ ˆ์ดํ„ฐ ์„ค์ •" + +msgid "HELP" +msgstr "๋„์›€๋ง" + +msgid "THE SYSTEM IS UP TO DATE" +msgstr "์‹œ์Šคํ…œ์ด ์ตœ์‹ ์ž…๋‹ˆ๋‹ค" + +msgid "FORCE BASIC GAMELIST VIEW" +msgstr "๊ฐ•์ œ๋กœ ๊ธฐ๋ณธ ๊ฒŒ์ž„๋ชฉ๋ก ์„ค์ •" + +msgid "DOWNLOADED" +msgstr "๋‹ค์šด๋กœ๋“œ๋จ" + +msgid "UPDATE VERSION:" +msgstr "์—…๋ฐ์ดํŠธ ๋ฒ„์ „:" + +msgid "UPDATE CHANGELOG:" +msgstr "์—…๋ฐ์ดํŠธ ๋ณ€๊ฒฝ ๋กœ๊ทธ:" + +msgid "MORE DETAILS" +msgstr "์„ธ๋ถ€ ์ •๋ณด" + +msgid "CAROUSEL TRANSITIONS" +msgstr "ํšŒ์ „๋ฉ”๋‰ด ์ „ํ™˜ ํšจ๊ณผ" + +msgid "ENABLE FILTERS" +msgstr "ํ•„ํ„ฐ ์‚ฌ์šฉ" + +msgid "THEME CONFIGURATION" +msgstr "ํ…Œ๋งˆ ๊ตฌ์„ฑ" + +msgid "THEME COLORSET" +msgstr "ํ…Œ๋งˆ ์ƒ‰์ƒ" + +msgid "THEME ICONSET" +msgstr "ํ…Œ๋งˆ ์•„์ด์ฝ˜" + +msgid "THEME MENU" +msgstr "ํ…Œ๋งˆ ๋ฉ”๋‰ด" + +msgid "THEME SYSTEMVIEW" +msgstr "ํ…Œ๋งˆ ์‹œ์Šคํ…œ๋ณด๊ธฐ" + +msgid "THEME GAMELISTVIEW" +msgstr "ํ…Œ๋งˆ ๊ฒŒ์ž„๋ชฉ๋ก" + +msgid "THEME REGION" +msgstr "ํ…Œ๋งˆ ์ง€์—ญ" + +msgid "THIS THEME HAS NO OPTION" +msgstr "ํ˜„์žฌ ํ…Œ๋งˆ์—๋Š” ์˜ต์…˜์ด ์—†์Šต๋‹ˆ๋‹ค" + +msgid "MANUAL INPUT" +msgstr "์ˆ˜๋™ ์ž…๋ ฅ" + +msgid "AN ERROR OCCURED - DOWNLOADED" +msgstr "์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค-๋‹ค์šด๋กœ๋“œ" + +msgid "START KODI" +msgstr "KODI ์‹œ์ž‘" + +msgid "Shows the current available update version." +msgstr "ํ˜„์žฌ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์—…๋ฐ์ดํŠธ ๋ฒ„์ „์„ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค." + +msgid "Shows the current available update changelog." +msgstr "ํ˜„์žฌ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์—…๋ฐ์ดํŠธ ๋ณ€๊ฒฝ ๋กœ๊ทธ๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค." + +msgid "Configure an associated controller. Your controller has to be associated / plugged before." +msgstr "์—ฐ๊ฒฐ๋œ ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ๋จผ์ € ์—ฐ๊ฒฐํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค." + +msgid "Choose if carousel will be animated or not during transitions" +msgstr "์ „ํ™˜ํ•˜๋Š” ๋™์•ˆ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ ์šฉํ• ์ง€ ์—ฌ๋ถ€๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค" + +msgid "Select the type of transition that occurs when you start a game. INSTANT will do nothing, FADE will fade to dark, and SLIDE will zoom on the game cover (or name if there is no scrape information)" +msgstr "๊ฒŒ์ž„ ์‹œ์ž‘ ์‹œ ์ ์šฉํ•  ์ „ํ™˜ ์œ ํ˜•์„ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค. ์ฆ‰์‹œ๋Š” ์•„๋ฌด ๊ฒƒ๋„ํ•˜์ง€ ์•Š๊ณ  ์–ด๋‘ก๊ฒŒ๋Š” ์–ด๋‘์›Œ์ง€๊ณ  ์Šฌ๋ผ์ด๋“œ๋Š” ๊ฒŒ์ž„ ์ด๋ฏธ์ง€๋ฅผ ํ™•๋Œ€์ปค๋ฒ„์—์„œ ํ™•๋Œ€๋ฉ๋‹ˆ๋‹ค(์ด๋ฏธ์ง€๊ฐ€ ์—†์„ ๊ฒฝ์šฐ ์ด๋ฆ„์ด ํ™•๋Œ€)." + +msgid "Select exisiting colorset options for this theme." +msgstr "ํ…Œ๋งˆ์— ๋Œ€ํ•œ ์ƒ‰์ƒ ์„ธํŠธ ์˜ต์…˜์„ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค." + +msgid "Select exisiting iconset options for this theme." +msgstr "ํ…Œ๋งˆ์— ๋Œ€ํ•œ ์•„์ด์ฝ˜ ์„ธํŠธ ์˜ต์…˜์„ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค." + +msgid "Select exisiting menu style options for this theme." +msgstr "ํ…Œ๋งˆ์— ๋Œ€ํ•œ ๋ฉ”๋‰ด ์Šคํƒ€์ผ ์˜ต์…˜์„ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค." + +msgid "Select exisiting system view options for this theme." +msgstr "ํ…Œ๋งˆ์— ๋Œ€ํ•œ ์‹œ์Šคํ…œ ๋ณด๊ธฐ ์˜ต์…˜์„ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค." + +msgid "Select exisiting gamelist view options for this theme." +msgstr "ํ…Œ๋งˆ์— ๋Œ€ํ•œ ๊ฒŒ์ž„๋ชฉ๋ก ๋ณด๊ธฐ ์˜ต์…˜์„ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค." + +msgid "Configure theme options if available." +msgstr "์ด์šฉ๊ฐ€๋Šฅํ•œ ํ…Œ๋งˆ ์˜ต์…˜์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค." + +msgid "Select Region of logos, pictures for system that are different for some countries. E.g. Megadrive in EU / Genesis in US" +msgid "๊ตญ๊ฐ€๋ณ„๋กœ ๋‹ค๋ฅธ ์‹œ์Šคํ…œ์˜ ๋กœ๊ณ , ์‚ฌ์ง„์— ๋Œ€ํ•œ ์ง€์—ญ ์„ค์ •์„ ํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ: ์•„์‹œ์•„ ์œ ๋Ÿฝ ๋ฉ”๊ฐ€๋“œ๋ผ์ด๋ธŒ / ๋ถ๋ฏธ ์ œ๋„ค์‹œ์Šค" + +msgid "Type the name of your SSID if it is hidden or not listed" +msgstr "SSID๊ฐ€ ์ˆจ๊ฒจ์ ธ ์žˆ๊ฑฐ๋‚˜ ๋ชฉ๋ก์— ์—†์„ ๊ฒฝ์šฐ SSID๋ฅผ ์ง์ ‘ ์ž…๋ ฅํ•ฉ๋‹ˆ๋‹ค." + +msgid "Select a letter and the listing will go directly on the first game starting with this letter." +msgstr "๋ฌธ์ž๋ฅผ ์„ ํƒํ•˜๋ฉด์ด ๋ฌธ์ž๋กœ ์‹œ์ž‘ํ•˜๋Š” ์ฒซ ๋ฒˆ์งธ ๊ฒŒ์ž„์—์„œ ๋ชฉ๋ก์ด ๋ฐ”๋กœ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค." + +msgid "Select the way the game list is sortered (alphabetically, by notation...)." +msgstr "๊ฒŒ์ž„๋ชฉ๋ก์„ ์ •๋ ฌํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์„ ํƒํ•˜์‹ญ์‹œ์˜ค(์•ŒํŒŒ๋ฒณ์ˆœ ๋“ฑ...)" + +msgid "Switch between seing or not only the favorites games. To add a game in the favorite list, select the game and toggle its state using 'Y'." +msgstr "์ฆ๊ฒจ์ฐพ๊ธฐ ๊ฒŒ์ž„ ํ‘œ์‹œ๋ฅผ ์ „ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์ฆ๊ฒจ์ฐพ๊ธฐ ๋ชฉ๋ก์— ๊ฒŒ์ž„์„ ์ถ”๊ฐ€ํ•˜๋ ค๋ฉด ๊ฒŒ์ž„์„ ์„ ํƒํ•˜๊ณ  'Y'๋ฅผ ๋ˆ„๋ฅด์‹ญ์‹œ์˜ค." + +msgid "Switch between seing or not the hidden games. To hide a game, edit its data and select 'Hide'." +msgstr "์ˆจ๊ฒจ์ง„ ๊ฒŒ์ž„ ํ‘œ์‹œ๋ฅผ ์ „ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๊ฒŒ์ž„์„ ์ˆจ๊ธฐ๋ ค๋ฉด ํ•ด๋‹น ๋ฐ์ดํ„ฐ๋ฅผ ํŽธ์ง‘ํ•˜๊ณ  '์ˆจ๊ธฐ๊ธฐ'๋ฅผ ์„ ํƒํ•˜์‹ญ์‹œ์˜ค." + +msgid "This option display a menu which allows to change game data and many others options." +msgstr "์ด ์˜ต์…˜์€ ๊ฒŒ์ž„ ๋ฐ์ดํ„ฐ ๋ฐ ๊ธฐํƒ€ ์—ฌ๋Ÿฌ ์˜ต์…˜์„ ๋ณ€๊ฒฝํ•  ์ˆ˜์žˆ๋Š” ๋ฉ”๋‰ด๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค." + +msgid "AVAILABLE UPDATE" +msgstr "์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์—…๋ฐ์ดํŠธ" + +msgid "UPDATE CHANGELOG" +msgstr "๋ณ€๊ฒฝ์‚ฌํ•ญ ์—…๋ฐ์ดํŠธ" + +msgid "CLOCK IN MENU" +msgstr "๋ฉ”๋‰ด์— ์‹œ๊ณ„ ํ‘œ์‹œ" + +msgid "Now playing" +msgstr "์‹คํ–‰ ์ค‘" + +msgid "DEFAULT (%1%)" +msgstr "๊ธฐ๋ณธ (%1%)" + +msgid "INPUT REQUIRED" +msgstr "์ž…๋ ฅ ํ•„์š”" + +msgid "(skipped)" +msgstr "(๊ฑด๋„ˆ๋œ€)" + +msgid "UP/DOWN TO SKIP" +msgstr "๊ฑด๋„ˆ๋›ฐ๋ ค๋ฉด ์œ„/์•„๋ž˜" + +msgid "A TO UNSET" +msgstr "A๋ฒ„ํŠผ์œผ๋กœ ์„ค์ •ํ•ด์ œ" + +msgid "DOWN TO SKIP AND KEEP [%1%]" +msgstr "๊ฑด๋„ˆ ๋›ฐ๋ ค๋ฉด ์•„๋ž˜ [%1%] ์œ ์ง€" + +msgid "UP/DOWN TO SKIP AND KEEP [%1%]" +msgstr "๊ฑด๋„ˆ ๋›ฐ๋ ค๋ฉด ์œ„/์•„๋ž˜ [%1%] ์œ ์ง€" + +msgid "Set duration of help popups, 0 means no popup." +msgstr "๋„์›€๋ง ํ‘œ์‹œ ์‹œ๊ฐ„์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. 0์€ ํ‘œ์‹œ ์•ˆํ•จ." + +msgid "HELP POPUP DURATION" +msgstr "๋„์›€๋ง ํ‘œ์‹œ ์‹œ๊ฐ„" + +msgid "Set duration of music popups, 0 means no popup." +msgstr "์Œ์•… ํ‘œ์‹œ ์‹œ๊ฐ„์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. 0์€ ํ‘œ์‹œ ์•ˆํ•จ." + +msgid "MUSIC POPUP DURATION" +msgstr "์Œ์•… ํ‘œ์‹œ ์‹œ๊ฐ„" + +msgid "POPUP SETTINGS" +msgstr "ํŒ์—… ์•Œ๋žŒ ์„ค์ •" + +msgid "POPUP POSITION" +msgstr "ํŒ์—… ์•Œ๋žŒ ์œ„์น˜" + +msgid "Select the position of popups on screen." +msgstr "ํ™”๋ฉด์—์„œ ํŒ์—… ์•Œ๋žŒ ์œ„์น˜๋ฅผ ์„ ํƒํ•˜์‹ญ์‹œ์˜ค." + +msgid "Set position and duration of popups." +msgstr "ํŒ์—… ์•Œ๋žŒ์˜ ์œ„์น˜์™€ ์ง€์† ์‹œ๊ฐ„์„ ์„ค์ •ํ•˜์‹ญ์‹œ์˜ค." + +msgid "TOP/RIGHT" +msgstr "์ƒ๋‹จ / ์˜ค๋ฅธ์ชฝ" + +msgid "BOTTOM/RIGHT" +msgstr "ํ•˜๋‹จ / ์˜ค๋ฅธ์ชฝ" + +msgid "BOTTOM/LEFT" +msgstr "ํ•˜๋‹จ / ์™ผ์ชฝ" + +msgid "TOP/LEFT" +msgstr "์ƒ๋‹จ / ์™ผ์ชฝ" + +msgid "SHOW FOLDERS CONTENT" +msgstr "ํด๋” ๋‚ด์šฉ ํ‘œ์‹œ" + +msgid "Switch between seeing the folders structure and seeing all games in a flatten top level." +msgstr "ํด๋” ๊ตฌ์กฐ ๋ณด๊ธฐ์™€ ์ „์ฒด ๊ฒŒ์ž„ ๋ณด๊ธฐ ์‚ฌ์ด๋ฅผ ์ „ํ™˜ํ•ฉ๋‹ˆ๋‹ค." + +msgid "NETPLAY" +msgstr "๋„ทํ”Œ๋ ˆ์ด" + +msgid "NETPLAY SETTINGS" +msgstr "๋„คํŠธ์›Œํฌ ์„ค์ •" + +msgid "NETPLAY LOBBY" +msgstr "๋„ทํ”Œ๋ ˆ์ด ๋กœ๋น„" + +msgid "Enable or disable Netplay in games." +msgstr "๊ฒŒ์ž„์—์„œ ๋„ทํ”Œ๋ ˆ์ด๋ฅผ ํ™œ์„ฑํ™” ๋˜๋Š” ๋น„ํ™œ์„ฑํ™”ํ•ฉ๋‹ˆ๋‹ค." + +msgid "PORT" +msgstr "ํฌํŠธ" + +msgid "NICKNAME" +msgstr "๋‹‰๋„ค์ž„" + +msgid "RELAY SERVER" +msgstr "์ค‘๊ณ„ ์„œ๋ฒ„" + +msgid "Enable or disable connections throught relay servers." +msgstr "์ค‘๊ณ„ ์„œ๋ฒ„๋ฅผ ํ†ตํ•œ ์—ฐ๊ฒฐ์„ ํ™œ์„ฑํ™” ๋˜๋Š” ๋น„ํ™œ์„ฑํ™”ํ•ฉ๋‹ˆ๋‹ค." + +msgid "KODI/NETPLAY" +msgstr "KODI/๋„ทํ”Œ๋ ˆ์ด" + +msgid "NO GAMES OR NO CONNECTION" +msgstr "๊ฒŒ์ž„์ด๋‚˜ ์—ฐ๊ฒฐ์ด ์—†์Šต๋‹ˆ๋‹ค" + +msgid "HASH NOW" +msgstr "ํ•ด์‹œ ํ™•์ธ" + +msgid "HASH THESE SYSTEMS" +msgstr "๋‹ค์Œ ์‹œ์Šคํ…œ ํ•ด์‹œ" + +msgid "Add hash of roms in your gamelists to have more accurate results in Netplay." +msgstr "๋„ทํ”Œ๋ ˆ์ด์—์„œ ๋ณด๋‹ค ์ •ํ™•ํ•œ ๊ฒฐ๊ณผ๋ฅผ ์–ป์œผ๋ ค๋ฉด ๊ฒŒ์ž„ ๋ชฉ๋ก์— ๋กฌ ํ•ด์‹œ๋ฅผ ์ถ”๊ฐ€ํ•˜์‹ญ์‹œ์˜ค." + +msgid "HASH ROMS" +msgstr "๋กฌ ํ•ด์‹œ" + +msgid "Only missing hashs" +msgstr "์—†๋Š” ํ•ด์‹œ๋งŒ" + +msgid "Username" +msgstr "์‚ฌ์šฉ์ž๋ช…" + +msgid "Country" +msgstr "๊ตญ๊ฐ€" + +msgid "Latency" +msgstr "์ง€์—ฐ ์‹œ๊ฐ„" + +msgid "Host arch." +msgstr "ํ˜ธ์ŠคํŠธ" + +msgid "Core ver." +msgstr "์ฝ”์–ด ๋ฒ„์ „" + +msgid "RA ver." +msgstr "๋ ˆํŠธ๋กœ์•„ํฌ ๋ฒ„์ „" + +msgid "Can join" +msgstr "์กฐ์ธ ๊ฐ€๋Šฅ" + +msgid "Rom and core match" +msgstr "๋กฌ ๋ฐ ์ฝ”์–ด ๋งค์น˜" + +msgid "Rom found" +msgstr "๋กฌ ๋ฐœ๊ฒฌ" + +msgid "No rom match" +msgstr "๋กฌ ์ผ์น˜ ์—†์Œ" + +msgid "Match" +msgstr "์ผ์น˜" + +msgid "No Match" +msgstr "์ผ์น˜ ์•ˆํ•จ" + +msgid "Rom file" +msgstr "๋กฌ ํŒŒ์ผ" + +msgid "Rom hash" +msgstr "๋กฌ ํ•ด์‹œ" + +msgid "THIS COULD TAKE A WHILE, CONFIRM?" +msgstr "์‹œ๊ฐ„์ด ๊ฑธ๋ฆด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค, ์ง„ํ–‰ ํ•ฉ๋‹ˆ๊นŒ?" + +msgid "good" +msgstr "์ข‹์Œ" + +msgid "bad" +msgstr "๋‚˜์จ" + +msgid "medium" +msgstr "์ค‘๊ฐ„" + +msgid "NETPLAY POPUP DURATION" +msgstr "๋„ทํ”Œ๋ ˆ์ด ํŒ์—… ์ง€์† ์‹œ๊ฐ„" + +msgid "Set duration of netplay popups, 0 means no popup." +msgstr "๋„ทํ”Œ๋ ˆ์ด ํŒ์—… ์ง€์† ์‹œ๊ฐ„์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. 0์€ ํŒ์—… ์—†์Œ." + +msgid "Player" +msgstr "ํ”Œ๋ ˆ์ด์–ด" + +msgid "Game" +msgstr "๊ฒŒ์ž„" + +msgid "Play online on games running through Retroarch like NES, SNES, FBA, Genesis/Megadrive and more!" +msgstr "NES, SNES, FBA, ๋ฉ”๊ฐ€๋“œ๋ผ์ด๋ธŒ๋“ฑ Retroarch๋ฅผ ํ†ตํ•ด ์‹คํ–‰๋˜๋Š” ๊ฒŒ์ž„์„ ์˜จ๋ผ์ธ์œผ๋กœ ํ”Œ๋ ˆ์ดํ•˜์‹ญ์‹œ์˜ค!" + +msgid "Rom, hash and core match" +msgstr "๋กฌ, ํ•ด์‹œ ๋ฐ ์ฝ”์–ด ์ผ์น˜" + +msgid "No core match" +msgstr "์ฝ”์–ด ์ผ์ง€ ์•ˆํ•จ" + +msgid "Add a clock in the main menu." +msgstr "๋ฉ”์ธ ๋ฉ”๋‰ด์— ์‹œ๊ณ„๋ฅผ ์ถ”๊ฐ€" + +msgid "UPGRADING" +msgstr "์—…๊ทธ๋ ˆ์ด๋“œ ์ค‘" + +msgid "PREPARING" +msgstr "์ค€๋น„ ์ค‘" + +msgid "Starting UI" +msgstr "UI ์‹œ์ž‘ ์ค‘" + +msgid "VERIFYING" +msgstr "ํ™•์ธ ์ค‘" + +msgid "EMPTY LIST" +msgstr "๋ชฉ๋ก ๋น„์šฐ๊ธฐ" + +msgid "COMPLETE QUIT MENU" +msgstr "์ „์ฒด ์ข…๋ฃŒ ๋ฉ”๋‰ด" + +msgid "Auto" +msgstr "์ž๋™" + +msgid "Square pixel" +msgstr "์ •์‚ฌ๊ฐํ˜• ํ”ฝ์…€" + +msgid "Retroarch Config" +msgstr "๋ ˆํŠธ๋กœ์•„ํฌ ๊ตฌ์„ฑ" + +msgid "Retroarch Custom" +msgstr "๋ ˆํŠธ๋กœ์•„ํฌ ์‚ฌ์šฉ์ž์„ค์ •" + +msgid "Core provided" +msgstr "์ฝ”์–ด ์ œ๊ณต" + +msgid "Do not set" +msgstr "์„ค์ •ํ•˜์ง€ ๋งˆ์‹ญ์‹œ์˜ค" + +msgid "FAVORITES" +msgstr "์ฆ๊ฒจ์ฐพ๊ธฐ" + +msgid "menu" +msgstr "๋ฉ”๋‰ด" + +msgid "SHOW CLOCK" +msgstr "์‹œ๊ณ„ ํ‘œ์‹œ" + +msgid "MODIFIER" +msgstr "๋ณ€๊ฒฝ" + +msgid "SELECTIONNER" +msgstr "์„ ํƒ" + +msgid "NONE" +msgstr "์—†์Œ" + +msgid "PRESS ESC TO CANCEL." +msgstr "์ทจ์†Œํ•˜๋ ค๋ฉด ESCํ‚ค๋ฅผ ๋ˆ„๋ฅด์„ธ์š”." + +msgctxt "button" +msgid "D-PAD UP" +msgstr "D-ํŒจ๋“œ ์œ„" + +msgctxt "button" +msgid "D-PAD DOWN" +msgstr "D-ํŒจ๋“œ ์•„๋ž˜" + +msgctxt "button" +msgid "D-PAD LEFT" +msgstr "D-ํŒจ๋“œ ์ขŒ" + +msgctxt "button" +msgid "D-PAD RIGHT" +msgstr "D-ํŒจ๋“œ ์šฐ" + +msgctxt "button" +msgid "START" +msgstr "์Šคํƒ€ํŠธ" + +msgctxt "button" +msgid "SELECT" +msgstr "์…€๋ ‰ํŠธ" + +msgctxt "button" +msgid "BUTTON A / EAST" +msgstr "A๋ฒ„ํŠผ" + +msgctxt "button" +msgid "BUTTON B / SOUTH" +msgstr "B ๋ฒ„ํŠผ" + +msgctxt "button" +msgid "BUTTON X / NORTH" +msgstr "X ๋ฒ„ํŠผ" + +msgctxt "button" +msgid "BUTTON Y / WEST" +msgstr "Y ๋ฒ„ํŠผ" + +msgctxt "button" +msgid "LEFT SHOULDER" +msgstr "์™ผ์ชฝ ์ˆ„๋”" + +msgctxt "button" +msgid "RIGHT SHOULDER" +msgstr "์˜ค๋ฅธ์ชฝ ์ˆ„๋”" + +msgctxt "button" +msgid "LEFT TRIGGER" +msgstr "์™ผ์ชฝ ํŠธ๋ฆฌ๊ฑฐ" + +msgctxt "button" +msgid "RIGHT TRIGGER" +msgstr "์˜ค๋ฅธ์ชฝ ํŠธ๋ฆฌ๊ฑฐ" + +msgctxt "button" +msgid "LEFT THUMB" +msgstr "์™ผ์ชฝ ์—„์ง€๋ฒ„ํŠผ" + +msgctxt "button" +msgid "RIGHT THUMB" +msgstr "์˜ค๋ฅธ์ชฝ ์—„์ง€๋ฒ„ํŠผ" + +msgctxt "button" +msgid "LEFT ANALOG UP" +msgstr "์™ผ์ชฝ ์•„๋‚ ๋กœ๊ทธ ์œ„" + +msgctxt "button" +msgid "LEFT ANALOG DOWN" +msgstr "์™ผ์ชฝ ์•„๋‚ ๋กœ๊ทธ ์•„๋ž˜" + +msgctxt "button" +msgid "LEFT ANALOG LEFT" +msgstr "์™ผ์ชฝ ์•„๋‚ ๋กœ๊ทธ ์ขŒ" + +msgctxt "button" +msgid "LEFT ANALOG RIGHT" +msgstr "์™ผ์ชฝ ์•„๋‚ ๋กœ๊ทธ ์šฐ" + +msgctxt "button" +msgid "RIGHT ANALOG UP" +msgstr "์˜ค๋ฅธ์ชฝ ์•„๋‚ ๋กœ๊ทธ ์œ„" + +msgctxt "button" +msgid "RIGHT ANALOG DOWN" +msgstr "์˜ค๋ฅธ์ชฝ ์•„๋‚ ๋กœ๊ทธ ์•„๋ž˜" + +msgctxt "button" +msgid "RIGHT ANALOG LEFT" +msgstr "์˜ค๋ฅธ์ชฝ ์•„๋‚ ๋กœ๊ทธ ์ขŒ" + +msgctxt "button" +msgid "RIGHT ANALOG RIGHT" +msgstr "์˜ค๋ฅธ์ชฝ ์•„๋‚ ๋กœ๊ทธ ์šฐ" + +msgctxt "button" +msgid "HOTKEY ENABLE" +msgstr "ํ•ซํ‚ค" + +msgid "CONFIxGURING" +msgstr "์„ค์ •์ค‘" + +msgid "KEYBOARD" +msgstr "ํ‚ค๋ณด๋“œ" + +msgid "GAMEPAD %i" +msgstr "๊ฒŒ์ž„ํŒจ๋“œ %i" + +msgid "HOLD ANY BUTTON TO SKIP" +msgstr "๊ฑด๋„ˆ๋›ฐ๋ ค๋ฉด ๋ฒ„ํŠผ์„ ๊ธธ๊ฒŒ ๋ˆ„๋ฅด์„ธ์š”" + +msgid "-NOT DEFINED-" +msgstr "-์ •์˜ ์•ˆ๋จ-" + +msgid "" +"YOU DIDN'T CHOOSE A HOTKEY ENABLE BUTTON. THIS IS REQUIRED FOR EXITING GAMES " +"WITH A CONTROLLER. DO YOU WANT TO USE THE SELECT BUTTON DEFAULT ? PLEASE " +"ANSWER YES TO USE SELECT OR NO TO NOT SET A HOTKEY ENABLE BUTTON." +msgstr "" +"ํ•ซํ‚ค ์„ค์ •์„ ์„ ํƒํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ์ด์šฉํ•ด ๊ฒŒ์ž„ ์ข…๋ฃŒ๋ฅผ ํ•˜๋ ค๋ฉด ๋ฐ˜๋“œ์‹œ ํ•„์š”ํ•œ ์„ค์ •์ž…๋‹ˆ๋‹ค. " +"์…€๋ ‰ํŠธ ๋ฒ„ํŠผ์„ ๊ธฐ๋ณธ ํ•ซํ‚ค๋กœ ์‚ฌ์šฉํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?" +"์˜ˆ๋ฅผ ์„ ํƒํ•˜๋ฉด ์…€๋ ‰ํŠธ ๋ฒ„ํŠผ์œผ๋กœ ์„ค์ •๋˜๊ณ  ์•„๋‹ˆ์˜ค๋ฅผ ์„ ํƒํ•˜๋ฉด ํ•ซํ‚ค๋ฅผ ์„ค์ •ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค." + +msgid "HOLD FOR %iS TO SKIP" +msgid_plural "HOLD FOR %iS TO SKIP" +msgstr[0] "๊ฑด๋„ˆ ๋›ฐ๋ ค๋ฉด %i ์ดˆ ๋™์•ˆ ๋ˆ„๋ฅด์„ธ์š”" +msgstr[1] "๊ฑด๋„ˆ ๋›ฐ๋ ค๋ฉด %i ์ดˆ ๋™์•ˆ ๋ˆ„๋ฅด์„ธ์š”" + +msgid "PRESS ANYTHING" +msgstr "์•„๋ฌด ๋ฒ„ํŠผ์ด๋‚˜ ๋ˆ„๋ฅด์„ธ์š”" + +msgid "ALREADY TAKEN" +msgstr "์ด๋ฏธ ์‚ฌ์šฉ์ค‘์ž…๋‹ˆ๋‹ค" + +msgid "THEME" +msgstr "ํ…Œ๋งˆ" + +msgid "ALL GAMES" +msgstr "์ „์ฒด ๊ฒŒ์ž„" + +msgid "MARQUEE" +msgstr "๋งˆํ‚ค" + +msgid "VIDEO" +msgstr "๋™์˜์ƒ" + +msgid "KIDGAME" +msgstr "์•„๋™์šฉ" + +msgid "FAVORITE" +msgstr "์ฆ๊ฒจ์ฐพ๊ธฐ" + +msgid "PLAYERS" +msgstr "ํ”Œ๋ ˆ์ด์–ด" diff --git a/resources/off.svg b/resources/off.svg index 5dacfb38a4..5a293cd401 100644 --- a/resources/off.svg +++ b/resources/off.svg @@ -1,9 +1,59 @@ - - - - - - - - + + + + + + image/svg+xml + + + + + + + + diff --git a/resources/on.svg b/resources/on.svg index ab4d5d9d5a..726d7bf46e 100644 --- a/resources/on.svg +++ b/resources/on.svg @@ -1,5 +1,54 @@ - - - - + + + + + + image/svg+xml + + + + + + + diff --git a/resources/option_arrow.svg b/resources/option_arrow.svg index 3027a4bc9b..3587e74823 100644 --- a/resources/option_arrow.svg +++ b/resources/option_arrow.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/resources/question.svg b/resources/question.svg new file mode 100644 index 0000000000..39a090afdf --- /dev/null +++ b/resources/question.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/search.svg b/resources/search.svg new file mode 100644 index 0000000000..c712b43b07 --- /dev/null +++ b/resources/search.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/resources/slider_knob.svg b/resources/slider_knob.svg index 6600ecb6c3..1a0a9b59bc 100644 --- a/resources/slider_knob.svg +++ b/resources/slider_knob.svg @@ -1,4 +1,7 @@ - - - +๏ปฟ + + + Layer 1 + + diff --git a/resources/splash.svg b/resources/splash.svg index b15cdec4e2..eb5f0b9f7f 100644 --- a/resources/splash.svg +++ b/resources/splash.svg @@ -1,55 +1,32 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file +๏ปฟ + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + Layer 1 + + + + + diff --git a/resources/star_filled.svg b/resources/star_filled.svg index 0be43f7911..808a40955f 100644 --- a/resources/star_filled.svg +++ b/resources/star_filled.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/resources/star_unfilled.svg b/resources/star_unfilled.svg index 9d3a2f7370..c59a17c61c 100644 --- a/resources/star_unfilled.svg +++ b/resources/star_unfilled.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/resources/warning.svg b/resources/warning.svg new file mode 100644 index 0000000000..9cf70aa4b0 --- /dev/null +++ b/resources/warning.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +