Pegasus theme development general
-
@cyperghost done a lot with it so far, mostly resizing various elements. Reduced grid columns, removed a lot of the animations and behaviors to speed it up, then built a settings menu (which still needs UI improving, but since its eventually going to be handled by the frontend I hesitate to put more time on that), changed the way background art is being handled, same for grid art, figuring out how to get the art assets to all be found and work, lot of trial error, and added pico8 svg and openbor svg logos. Next on my list is Favorites, Last Played, few more cosmetic changes, test megadrive/pce16 -> genesis/tg16 swapping, and if I really want to go wild, see about adding alphabet listview that pops out from the side and jumps to (closest) matching index in gridview, and see if we have on screen keyboard support and add it if so. (Not sure if either of these will happen).
-
Trying my hand at the fake collections, and it seems to be working beautifully! :D below was the code of how I added it, and I'm able to see the two new collections, and switch to all collections and the grid displays all the proper games, the only thing I'm worried about now, is, if the user toggles the favorites bool for the game, will it still work, and if not, how I will fix it, but I have ideas, maybe. maybe not lol. Edit: so yeah, as I expected, the gameData.favorite is a problem, but also another issue with doing it this way is I get a bunch of errors when trying to access game.assets (screenshot, boxFront, etc) saying
/GameDetails.qml:287: TypeError: Cannot read property 'boxFront' of undefined
I guess because it's not a true "game" from api.collections but rather just a standard array. Hmmmm. So I guess I need to redo this, following the way that @fluffypillow showed, using it as a model data specifically, and I'll just need to use the proxy filter index translating methods when filling or swapping thecurrentGame
object. (if the current collection being displayed comes from one of the proxy sort filters). I guess I'm starting to understand the difference between an Object, a Model, and an Array.FocusScope { SortFilterProxyModel { id: favoriteGames sourceModel: api.allGames filters: ValueFilter { roleName: "favorite" value: true } } property var favoritesCollection: { return { name: "Favorite Games", shortName: "favorites", games: favoriteGames, } } SortFilterProxyModel { id: lastPlayedGames sourceModel: api.allGames sorters: RoleSorter { roleName: "lastPlayed" } } property var lastPlayedCollection: { return { name: "Last Played", shortName: "lastplayed", games: lastPlayedGames, } } //form a collection which contains our favorites, last played, and all real collections. property var dynamicCollections: [favoritesCollection, lastPlayedCollection, ...api.collections.toVarArray()] ... ////////////////////////// // Collection switching // function modulo(a,n) { return (a % n + n) % n; } property int collectionIndex: 0 property var currentCollection: dynamicCollections[collectionIndex] function nextCollection () { jumpToCollection(collectionIndex + 1); } function prevCollection() { jumpToCollection(collectionIndex - 1); } function jumpToCollection(idx) { api.memory.set('gameCollIndex' + collectionIndex, currentGameIndex); // save game index of current collection collectionIndex = modulo(idx, (api.collections.count + 2)); // new collection index currentGameIndex = api.memory.get('gameCollIndex' + collectionIndex) || 0; // restore game index for newly selected collection } // End collection switching // ////////////////////////////// //////////////////// // Game switching // property int currentGameIndex: 0 readonly property var currentGame: currentCollection.games.get(currentGameIndex) function changeGameIndex (idx) { currentGameIndex = idx if (collectionIndex && idx) { api.memory.set('gameIndex' + collectionIndex, idx); } } // End game switching // ////////////////////////
-
I'm stuck and could really use some help on:
SortFilterProxyModel { id: lastPlayedGames sourceModel: api.allGames sorters: RoleSorter { roleName: "lastPlayed" enabled: true } }
This is giving me a collection of games that does not seem right. It's just a list of api.allGames and it's not being sorted, they are all just alphabetical. I can't understand why that is.
-
@SinisterSpatula not sure if it will help or not, but I've been playing around a bit with the sort/filter code to get a better handle on it. I've been using the "Flixnet" theme from @fluffypillow's repository as my base as theme as I had used that tutorial to get started.
The only differences I see in my code is I flipped the sort order since by default the most recently played games were being added to the end of the list. I also did not include the "enabled" flag but doubt that would make a difference.
// Recently Played custom collection SortFilterProxyModel { id: recentGames sourceModel: api.allGames sorters: RoleSorter { roleName: "lastPlayed" sortOrder: Qt.DescendingOrder } }
Another tweak I made was to limit the list to the last 20 games games since the api.allGames collection can be quite large.
I am not sure if there is a way to layer filters inside of one proxy so that I could filter the last 20 results based on the sorted list, so what I ended up doing was chaining a second proxy that filters the sorted list above. It works fine, but it does make the launch command a bit more tricky since you have to traverse 2 proxies to get back to the original source model.
A larger snippet of the code is below, but I have the WIP theme on github too. I haven't broken the theme out into it's own repo as I'm still just experimenting a bit but feel free to have a look and see if it helps you with any points you might be stuck on, as the favorites and recently played collections do work on this theme with these edits.
// Recently Played custom collection SortFilterProxyModel { id: recentGames sourceModel: api.allGames sorters: RoleSorter { roleName: "lastPlayed" sortOrder: Qt.DescendingOrder } } // Apply a second proxy to only show the most recent 20 games - not sure if this can be consolidated into 1 proxy? SortFilterProxyModel { id: filteredRecentGames sourceModel: recentGames filters: IndexFilter { maximumIndex: maxRecentGames } } property var recentCollection: { return { name: "Recently Played", games: filteredRecentGames } } property var allCollections: [favCollection, recentCollection, ...api.collections.toVarArray()]
-
@msheehan79 thank you so much! This is exactly what I needed and extremely helpful. I wondered about that too, how to limit the number of items, fantastic! I just added an alphabet listview that pops out when the filter button is pressed and I plan to use the logic from holding the Alt key and pressing letters, should work perfectly. Man this is shaping up to exceed beyond what I imagined. Once I get that working plus lastplayed, I think it will be perfect.
Yes I think we might be able to combine multiple sorters and filters in the same proxy, I've seen that done before and should be as simple as putting both right in there. I think the order might be important, like you would want to sort decending and then chop. Versus chopping then sorting.
edit: I tried combining the sort & chop and it did not work, it seems we do have to double proxy.
-
@SinisterSpatula
Great to see the progress! Scrolling looks awesome - on a Pi Zero, that's amazing! Can't wait to test on my CM3+ module.Do share a link when you think it's ready for test!
-
Wow nice progress :) It is possible to use multiple filters, but not for chopping unfortunately: only games that pass all filters (no matter the order) will be present in the output. This is because filters operate on the source model: IndexFilter works on the source index, so combining that with favorites means "games that have less than <some max> index in the source model AND games that are favorites", which might indeed have unexpected results. A workaround could be using another proxy, yes, or if it's getting hard to use, perhaps manually creating and filling a JavaScript array of games could also work. (I think the proxy model does not have a
toVarArray
function sadly, but if it does, that could make things easier.) -
This code is pretty wild, but it's working :D I'm so freaking happy right now. The last item I need to finish is the Alpha-jumping menu. Then just fixing any other bugs I can find and figure out.
//////////////////// // Game switching // property int currentGameIndex: 0 readonly property var currentGame: (collectionIndex >= 2) ? currentCollection.games.get(currentGameIndex) : api.allGames.get(findCurrentGameFromProxy(currentGameIndex, collectionIndex)) function findCurrentGameFromProxy (idx, collidx) { if (collidx == 0) { return favoriteGames.mapToSource(idx); } if (collidx == 1) { return lastPlayedFilter.mapToSource((lastPlayedGames.mapToSource(idx))); } return; } function changeGameIndex (idx) { currentGameIndex = idx if (collectionIndex && idx) { api.memory.set('gameIndex' + collectionIndex, idx); } } // End game switching // ////////////////////////
-
@SinisterSpatula tip: for currentGame it's not necessary to track back to the original source model; it doesn't matter whether you
get()
a game from a proxy model or the Api, the games themselves are all the same thing in both places. Ie. you can justfavoriteGames.get(someIndex)
and it should work fine. -
@fluffypillow I tried using the lastPlayedGames.get(index) and favoriteGames.get(index) but something strange is happening when I do that. It's like the item it returns is not a "real game item" or something. When other code tries to access stuff it should be able to (currentGame.assets) it says it's null or a type error or something. If I fetch the game all the way back at the source it works fine.
-
Man, I'm stuck on this again :(
I'm trying to make a function call to the grid from my AlphaMenu.qml, and I'm not understanding the proper way to reference it:
Keys.onPressed: { if (api.keys.isAccept(event)) { event.accepted = true; gamegrid.grid.jumpToMyLetter(lettersList[alphaList.currentIndex]); closeMenu(); return; } }
GameGrid.qml:
FocusScope { id: root GridView { id: grid function jumpToMyLetter (inputletter) { event.accepted = true; var jumpletter = inputletter.toLowerCase(); var match = false; for (var idx = 0; idx < model.count; idx++) { // search title starting-with pattern var lowTitle = model.get(idx).title.toLowerCase(); if (lowTitle.indexOf(jumpletter) == 0) { currentIndex = idx; match = true; break; } } if (!match) { // no match - try to search title containing pattern for (var idx = 0; idx < model.count; idx++) { var lowTitle = model.get(idx).title.toLowerCase(); if (lowTitle.indexOf(jumpletter) != -1) { currentIndex = idx; break; } } } }
It looks so pretty! I just need it to work. I'm so thankful that the game title seeking code existed, I would never have figured that out on my own. Maybe this needs to be a signal and signal handler instead? I'm trying hard to understand and do the right thing.
edit: Just got it working! I'm sure this is a noobish way of doing it, but it worked:
On gamegrid.qml, under the root id:function jumpTheGrid (letter) { grid.jumpToMyLetter(letter); }
still on gamegrid.qml, under the Gridview grid id:
function jumpToMyLetter (letter) { //actual letter jumping code }
On alphamenu.qml:
Keys.onPressed: { if (event.isAutoRepeat) return; if (api.keys.isAccept(event)) { event.accepted = true; gamegrid.jumpTheGrid(lettersList[alphaList.currentIndex]); closeMenu(); return; } }
So now, the last bit that needs fixing is a bunch of Type errors and reference errors when my theme tries to access the assets of the current game when sometimes the currentGame.assets is null. I just need to probably add a check for null at the beginning of these code blocks. Tomorrow I'll work on making a video to show it off.
-
Video Walkthrough and annoucement post here: https://retropie.org.uk/forum/topic/23682/theme-gpios-based-on-gameos-pegasus-front-end-theme-modified-for-retroflag-gpi-case
-
@fluffypillow @PlayingKarrde I want to update my modded theme and the guide I wrote for it to use the proper artwork folder names, and proper way to scrape, so that it can co-exist with Emulation Station if possible. Is there any official documentation or knowledge you can share about the correct way to name the folders for artwork? Specifically: Box art, Screenshot, Cartridge, Wheel (transparent), steam tile, and fan art? I think I have all the correct
gameData.assets.'type'
for each type of asset in the code and it works well, but when it came to the frontend searching for art when there exists a gamelist.xml or metadata.pegasus.txt it had problems. If pegasus hunts for the art on it's own, these folders worked:Folder names: box2dfront, fanart, screenshot, videos, wheel, steamgrid, box2dback
[located in roms/systemname/media/] (I could not find the proper foldername for cartridges for use with this method so I was using box2dback for that purpose.- If I used gamelist.xml containing art path's it would break and not find some of the art.
- If I used metadata.pegasus.txt either from the web conversion tool, or from skyscraper (linux) it would also break and not find some art.
Hoping I can get confirmation on the exact, correct, way to name the folders. I personally plan to only use Pegasus (but it would be nice to find this out for other users who want to use both frontends and swap.
-
@SinisterSpatula The tricky thing here is that gameOS requires certain media types that the default EmulationStation scraper doesn't grab (for example videos). If you were to use Skyscraper (which is possible from within Retropie) then I believe it should work as the creator of Skyscraper recently added support for Pegasus I believe.
However I haven't tested this so it's just speculation on my part.
-
@SinisterSpatula the fields of
assets
are listed here: https://pegasus-frontend.org/docs/themes/api/#assets, and here are the directories checked for assets: https://pastebin.com/KubBUcg1. In addition, if Skraper support is enabled, here are some more directories that are checked: https://pastebin.com/Thnkgz59. -
Thanks guys, great info. What is the proper way to handle the swap between Megadrive and Genesis? Looks like the themes support both, but how is Pegasus choosing which one it shows? I tried changing it in the usual way and it didn't seem to have any effect.
-
@SinisterSpatula the default theme just shows the logo based on the collection's
shortname
(if there is one for it). There isn't any hardcoded game console information inside Pegasus. -
Okay, I guess the issue I'm running into, and if I understand correctly, is that Pegasus is pulling in the "shortname" for megadrive from
/etc/emulationstation/es_systems.cfg
and whatever is set as<name>megadrive</name>
. ES/retropie is wanting to keep the name always as megadrive so it knows where to grab configs, launching image, and other info from, and as far as themes are concerned, they should follow what is set in<theme>megadrive</theme>
(same applies for pcengine). So, unless I also rename the folder in/opt/retropie/configs/megadrive
to genesis, (which is going against the current standard) I don't see how to get it to use "genesis" instead. I can work around this in the code of the theme, but wanted to know if there was already a more proper way that I was not aware of yet. So I guess I was hoping, that pegasus was already taking into consideration what is written in<theme>
as well, and not just<name>
.Edit: Maybe I'm wrong about all of this? I just noticed Pegasus is saying "Sega Mega Drive" for the name in platforms menu, but that's not in es_systems.cfg.
For now I'm adding a setting to switch megadrive/genesis, and changing what it does based on that. (changing the source.svg image it uses, and text that it displays.) One bad side effect of this, is that the platform list will show the TurboGrafx-16 in the same spot as pcengine so the alphabetical sorting is not accurate.
-
@SinisterSpatula Yes, it uses
name
for shortname andfullname
for name.theme
is not used, however I thinkplatform
could be used for this task and I can add support for that. The theme is set to just load "shortname +.svg
" by default, but you can add any other logic as well. -
@fluffypillow
platform
can be multi-valued ines_systems.cfg
, though I don't think this is currently used by any system.
Contributions to the project are always appreciated, so if you would like to support us with a donation you can do so here.
Hosting provided by Mythic-Beasts. See the Hosting Information page for more information.