Pegasus theme development general
-
@fluffypillow I've been going through your theme tutorials to try and learn a bit about QML and theme design - one of the features I was trying to take a stab at was a way to incorporate a "Recently Played" collection into a theme so that it would appear as another collection in the list. That is one of the features of ES2 that I really liked and I think would be a nice addition to themes on Pegasus.
The challenge seems to be that in a ListView or PathView you can only have one model, which in most themes would be api.collections. But in order to have a recently played that spans all collections, I would need to use api.allGames as the source for the filter proxy.
So I guess the question I have is do you have any suggestions for how one might go about incorporating 2 different models into a theme in some fashion? I'm still learning about QML so I might have missed something obvious. Still going to keep trying things and looking into it here but thought I'd bounce it off of you in case you have any tips.
Thanks!
-
@msheehan79 actually that's a really interesting question! A View operates over a list of items, which in practice either an item model you can access from the Api, or a plain JavaScript array. In fact, you could actually do this:
model: [ api.collections.get(0), api.collections.get(1), api.collections.get(2) ] // etc.
So the simplest way to add a "fake" collection would be setting the model to an array of collections, and put our custom one into it too. Now if we have a filter like this:
SortFilterProxyModel { id: filteredGames sourceModel: api.allGames filters: ValueFilter { roleName: "favorite" value: true } }
, a tempting first idea might be to put this
filteredGames
into the array... However, a filter overapi.allGames
produces a list of games, not a collection (it doesn't have a name, for example), so we should turn the list into one somehow. Themes can't "officially" create new collections, however this is JavaScript, so as long as it looks like a collections, ie. has the same properties as a collection, then it can be used as one just fine. If we then create a new object like this somewhere:// There are various ways to do this, this is just an example property var newCollection: { return { name: "My Little Favorites", games: filteredGames, // feel free to add things like `shortName`, etc. } }
, we can then include it in an array together with the other collections, and it will be treated just like the others:
model: [ newCollection, api.collections.get(0), api.collections.get(1) ] // etc.
And indeed it should work as expected:
https://snipboard.io/DhJj8L.jpg
Now obviously nobody wants to fill that array manually. Since it's a plain JS array, you could fill in a
for
loop or something. Item models also have a not really documented function calledtoVarArray
which just returns the list of items as a plain array. For the nicest code however, we can use the fact that modern JavaScript constructs like the spread operator are also supported, and combine all that into this nice line:model: [ newCollection, ...api.collections.toVarArray() ]
Example code repo here.
-
@fluffypillow Nice! This is very helpful indeed. Hope to get some time in the next day or two to take another crack at it.
I'm still figuring out exactly where in the code I can leverage JavaScript, so knowing I can use it in the model declaration for example is really useful info for me. Up until now I had pretty much just used it inside of functions called on keyPress events.
-
Hi @AndersHP you can definitely give it a try. I got a little frustrated with the project because I'm not versed enough on QML and finding it extremely difficult to understand lol. I haven't done anything for about a week, and I'm just now starting to regain motivation to try any more with it. I came back to see some really neat stuff about making a fake collection, and that is definitely one of the pieces of the puzzle that was confusing the hell out of me. My only issue is, whenever I start adding the sort proxy filter, things with the gameOS theme start behaving erratically. Once the model changes, the gridview disappears for a moment, but then if you start pressing navigating buttons, it reappears and everything is fine. I just couldn't figure out how I'm supposed to do it so that this doesn't cause it to bug out like that. My other problem is the performance of the grid scrolling. And I think this is down to all the fancy stuff in the theme, slowing it down. I feel like I want to just start over on a very basic code, with none of the fancy elements. I want the grid to flow and scroll as smooth as possible, and I'm thinking the best way is a grid view that only wants to show cartridge art (and maybe screenshot as fallback) but no scaling the currently selected tile, no changing font color to gray, or changing opacity, no animations at all, just the scrolling, and maybe a border around the current selection. Feel like I might start over on a clean code, just keeping the platform menu, and game details screens, and cutting back all the bling on the gridview. Then, trying to implement the fake collections of "Last Played" and "Favorites". If I could get to that point, I would be so happy with it. :D But man, it's a challenge, because I really don't know anything about javascript and QML and everything I try to do is really guesswork and trial and error. Someone who actually has skills at this could do it an 30 minutes, what is taking me weeks LOL.
-
@SinisterSpatula Have you already taken out the video player in the GridItem? That is most likely the heaviest piece in terms of performance. I haven't tried the theme on a pi zero (or even a pi3 for that matter) but removing all the code for that should be the first step. After that I would look at removing the changing of the background art on each item update call.
Other than that I don't think there's anything super crazy going on that should cause a lot of performance issues.
-
@PlayingKarrde Yeah, the video stuff on grid view was taken out very early on, and it still chugs at times. I think it might actually be the the grid tile elements have too much going on as well. I think I need to strip out even the loading spinners, probably, anything that is asking it to use CPU cycles pretty much. I think the logic needs to be purely, grab and display art, show text on top (or not) and do it as fast as you can. The zero is really sensitive to multitasking since it only has it's single core.
-
Much better, I think I'm happy with the performance of the grid scrolling now. Had to remove a lot to get it this fast. I still wish it could scroll smoothly, like if I could set the maximum velocity to something slower to give it a chance to load elements before they come into view. If I set the maximum flick velocity it will work for mouse interaction, but it completely ignores key navigation which the gamepad counts as. If I could slow that down just a tad, I think it would scroll even smoother, but seems impossible.
-
Well crap. I thought I would be smart and tie my background art changes to onMovementEnded for the gridview. This worked out great, if the grid is flicked with the mouse. But does nothing if it's moved by key navigation (d-pad). So the only way to do this is with a timer? I have to reset the timer every time the grid is being scrolled? That seems so inefficient, and with the pi zero I'm trying to be as efficient as possible. :(
-
Holy moly!!! I'm glad I tried the timer, and I messed it up initially because it lead to a major discovery. It stopped a function from getting called (every time the index was changing), and the performance went through the roof! So now I moved that function to the Timer trigger, and it's a dream come true. The grid is now scrolling with the performance I was dreaming of:
-
@SinisterSpatula looks great - what do you have done?
-
@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.
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.