Subclassing, De-Spagettification, and List Models
Table of Contents
Prior to the UI rewrite, which made a plethora of improvements to the user-facing side of Recordbox, there were a few significant issues pertaining to code quality that I wanted to address. One was that a lot of the code for the library lists was either redundant or messy. Another was that the various functions that handled most the primary actions for the library were quite verbose and somewhat redundant with each other. The last issue – which had been a long-standing source of frustration by this point ― was the fact that the code for the play queue was a raging dumpster fire of unmitigated disaster.
Since the UI update, all of these issues have been resolved, and what they have in common is that they were all resolved with list models.
In this post, (which is hopefully coherent, but if anyone is reading this shortly after it was posted it hasn’t been edited much yet) I talk about these issues.
A Bit About List Models #
GListModel
is an interface, which are similar to Rust’s traits in that they define a set of methods that an object implements without being an object that is subclassed by others. There is no way to create an instance of a GListModel
, only objects which implement it. One example is the base implementation provided by GTK, GListStore
.
A ListModel is exactly what it says on the tin: a list. Or more specifically, GListModel
is an interface that objects can use so other objects can interface with them like a list.
The Interface #
The GListModel
interface defines three methods that must be implemented by objects:
item_type()
returns the type of the object stored in the list.item(i)
returns the object at positioni
.n_items()
returns the number of objects in the list.
To display elements in a list widget (specifically a GtkListView
) you need a GListModel
that stores the objects that will be displayed. The GtkListView
widget takes an implementation of a GListModel
and generates widgets for the items in the list as needed.
In Recordbox, this means that all the artists (and generes) in the library are stored in a GListStore
(A basic GListModel
implementation provided by GTK) for the artist (and genre) list, and all the albums are stored in another ListStore for the album list.
Or at least, they were. Until I realized I could make a MusicItem into a ListModel.
It’s List Models All The Way Down #
Another crucial component of a ListModel is the items-changed
signal. Whenever the list changes, if items are added, removed, or rearranged, this signal is emitted, with the parameters position
, removed
, and added
. Signals are really a list of closures that a GObject calls when it does something. To ‘connect’ to a signal, you provide a closure that will be called every time the signal is emitted
. In the case of items-changed
, the closure takes the aforementioned parameters which declare where the change happened, how many items were removed at that location, and how many were added respectively.
This allows widgets like list views to update the view when the list changes, removing and adding new rows as needed. In general, it allows for the state of the list to be closely followed and specific actions taken whenever the list changes. It also allows for ListModels to be composed on top of other ListModels.
For example, a FilterListModel
provided by GTK takes another list model and filters out items according to the GtkFilter
it is given. Whenever the model it was given emits items-changed
, the filter model updates in response, and any items in the base model that do not pass the filter will not appear in the filter model. The value returned by n_items
on the filter model will be the n_items
from the base model, minus all filtered items, and if the item returned by item(i)
on the base model was filtered out by the filter model for some i
, calling item(i)
on the filter model will not return that item.
The longer I work on Recordbox, the more I have come to realize how many things can be implemented by simply using this strategy of making a ListModel
in a ListModel
(in a ListModel
, etc…) And nowhere was that more obvious than with the various issues which are discussed below.
The Problems With The Library #
While I had been spending quite a lot of effort on cleaning up the code and simplifying things as much as possible leading up to and during the UI rewrite, following the release the library in Recordbox had two significant quality issues remaining. The reason neither could be resolved was due to my inability to figure out how to subclass custom GObject
types in gtk-rs
.
The Subclassing Problem #
Suffice to say, Rust is not an object-oriented language, while GTK itself is object-oriented. GTK, and other libraries based on Glib
like GStreamer,
use an in-house object model, called GObject
. Every component of a GTK application ― the widgets, window, data structures, are descendants of the base GObject
class. All widgets are descendants of GtkWidget
, itself a descendant of GObject
. To write pretty much any kind of program in GTK, you need to have some way to represent GTK’s object orientation.
The team behind gtk-rs
, the rust bindings for GTK, diby implementing a system that represents the object-orientated aspects of GTK in Rust and allows developers to define custom GObjects
in Rust that inherit from GObject
, GtkWidget
, GtkBox
, etc. gtk-rs
provides several macros that simplify this process for developers, but the challenge starts to arise when you want to subclass one of your own custom objects. For the built-in GTK objects, all the necessary boilerplate is written and provided as part of gtk-rs
. For custom objects, all the boilerplate must be written yourself, as there is no simple derive macro you can declare on your object struct to make it subclassable. And when I was porting to Rust I couldn’t really figure out how to do that.
This lead to a few issues. Back in the Python version, I had one main list class which both the artist list and the album list inherited from. Internally, I also represent the artists, albums, tracks, etc. in Recordbox’s data structures as GObjects
: ArtistItem
, AlbumItem
, and so on. Each is a GObject with relevant properties like the title, track list if it’s an AlbumItem
, etc. In the Python version, these all inherited from one type: LibraryItem
. LibraryItems had the properties title
, subtitle
, cover
and thumbnail
, which were the information needed to generate a row widget in the library lists. ArtistItems would overwrite the subtitle property to return the number of albums they had, AlbumItems the number of tracks, total length, and release date, and so on.
When I was doing the port, I wasn’t able to figure out how to do this in Rust, because I couldn’t figure out the specifics of making the LibraryItem
subclassible. Suffice to say, this was due to a combination of gtk-rs
only having one simple example of how to do this and my own impatience.
Redundant Lists #
Since I couldn’t figure out how to subclass custom types, I had to resort to two independent list types for the album and artist/genre lists. This was the more significant issue that resulted from my inability to subclass things, the lack of a parent LibraryItem
for the various music item types was less of an issue at the time, but it became a blocker for the solution to the second problem that emerged with the library following the UI update.
Now, after the UI update was done, and after going back to figuring out how to make a GObject
defined in Rust subclassible with a fresh perspective, I was able to crack it and develop a MusicItem
object which all the other music types inherited from. Unlike with the Python version of Recordbox however, I did not go back to having a parent list type that the library’s lists inherited from, and was able to instead combine the two lists into a single list type that interacted on MusicItems
only. Rather than have the different behaviour needed for the artists/genres and albums handled by different list classes, the list behaved (mostly) the same for both, and then the external code which interacted with each list handled the album/artist/genre specific behaviour as needed. This was also possible due to the changes to the various action handling functions in the main window and elsewhere which will be mentioned a later section, as they were all made to accept and operate on generic MusicItem
objects as well, so unique handling of the different music types was largely not needed, as the list could just spit out MusicItem
objects in all it’s signal callbacks and such.
This change was greatly needed, and a relief to finally be able to implement, as now I know for sure that the code for the primary and secondary lists in the library is the same, and there’s that much less boilerplate left in the codebase.
However, it also had implications for another dilemma. One I had only recently introduced, and for which solving it was the other main motivation for figuring out out to get a MusicItem
working again.
Better Grouping #
So in the UI update, one of the new features that I added was an option that groups the album list by an additional category, which you can see here:
With this grouping the album list would function as a tree. As seen in the screenshot they are grouped by artists, so the top-level items are artists which can be expanded to show albums.
This is done with a TreeListModel
, which takes on construction a list model and a closure that can produce ListModels
for items in the model. The items in the models returned from the closure are shown as children of the item the model was obtained from. So to make the tree work, I needed the albums to be contained in a ListModel according to the artist they needed to be grouped under.
With the old version before the MusicItem
was added, the albums were just in a flat, ungrouped list, and when the grouping was enabled a function would run which ran through the list of albums, sorting them into categories according to artist using hash maps, and then creating an intermediate GroupItem
to function as the list model.
This was similar to how the queue works, where their is a QueueItem
that tracks and albums are put in before being addeed to the queue. The queue does it that way for more reasons than just needing to make a tree through, there is extra functionality needed in the queue that makes having a QueueItem
useful.
The GroupItem
in the library was only needed because every item in a list model must have the same type, so an AlbumItem
and a TrackItem
cannot be in the same list directly. If, when returning a list model for an ArtistItem
in the closure for the TreeListModel
, a model containing AlbumItem
is returned, an unrecoverable error occurs and the program panics. So the GroupItem
existed just to serve as a placeholder for the artist/genre at the top-level and to wrap the albums to prevent panics.
With the MusicItem
working this changed, as now ArtistItems and AlbumItems shared this common parent, so they could be put in the same list model simply by upcasting them to a MusicItem
first. And the MusicItem
could implement the GListModel
interface. So the existence of the GroupItem was no longer necessary, as it could be absorbed into the MusicItem
design. And this further lead to a great deal of simplification in how the grouping was implemented.
Grouping By Default #
Previously, albums were loaded into the album list completely independently of the artists or genres. Tracks were the only music item type that were contained within another, being stored in the track list of their albums. With these changes, I instead made it so that when the application starts the library loads a list of ArtistItems and GenreItems like before, but after loading the AlbumItems it goes on to do the grouping step that was previously only done when enabling the grouping in the list. So the albums are loaded into the library, categorized by artist and genre, and stored in the artists and genres. The library went from one list that stores either artists or genres and one that stores albums, to one list that stores genres that contain albums, and one list that stores artists that contain albums.
So now to initialize the library view for artists, the secondary list on the left is given the list of artists and set to not display a trees so the albums contained in the artist will not be shown in the list, and the album list on the right is given the genre list.
The album list is set to use a TreeListModel
, which means it puts the list model it is given into one and then uses the tree model to generate the list widgets. Since it is given the genre list, it shows genres as the top-level items, and the closure to generate the list models for children returns the album list stored in that genre (with a FilterListModel
on top to filter out albums that don’t belong to the selected artist in the secondary list) and boom, we have the grouping from before. But what if the grouping is turned off?
Enter the FlattenListModel
.
The FlattenListModel #
A FlattenListModel
is a list model implementation provided by GTK that takes a list model containing list models and concatenates all the list models into a single list. When given a list of artists, (or genres) the FlattenListModel
then behaves like a concatenated list of the albums contained in each artist. So when grouping is off, the library just replaces the base model that the album list has with a FlattenListModel
containing the original base model. And since all music types inherit from MusicItem
now, which implements GListModel
, the FlattenListModel
produces a flat list of the albums in the original list, from which the TreeListModel
doesn’t find any child models, so it doesn’t make any expanders and the grouping is gone. To switch between grouped and ungrouped, all library needs to do now is change whether or not the FlattenListModel
is present.
With this, all of the code needed to sort and group the albums by artist or genre every time the option was enabled could be ripped out, which was great because it both removed a large source of potential bugs, and reduced the amount of album-specific code in the single remaining list type in the library. Since the grouping was now determined by whether the library gave the album list a flattened or flattened model, the actual list itself no longer needed to concern itself with the grouping at all. It just needed a tree
property―which would wrap the provided list model it was given in a TreeListModel
if true
or use the provided model directly if false
―along with some extra code to make sure it traversed the tree correctly if it was enabled.
Cleaning Up The Window Actions #
Another opportunity for cleanup arose in the functions that handled the callbacks for all the various actions that could be triggered by the library, by simplifying them to only operate on the new MusicItem
type that I now had.
Basically, GTK has “actions”, which are similar to the signals I mentioned before, except for a few key differences. Signals are emitted by a widget or object to indicate that some event happened. Other widgets connect closures to the signal that get executed when it is emitted. Actions, on the other hand, can be activated by widgets, keyboard shortcuts, or even over DBus, and trigger an event.
In Recordbox, the main window class had a lot of actions for each of the various, well, actions that could be done in the application. Things like playing the selected albums in the album list, or just appending them to the queue, etc, and the according actions for tracks and such. None of the code was particularly bad per-se, other than the fact that there was a separate action for each music item that could be added to the queue. So there was a play-track
and a play-album
and a play-disc
action. (which plays only the tracks on the specified disc on the album, if the album had discnumber
tags) For the append actions it was the same, and so on. There was also a set of actions for the artists/genres, which would add the entire contents of the current album list to the queue. (or play them, etc)
Again, this wasn’t particularity bad in and of itself, as there were a lot of actions that needed to be handled. It just bothered me because it was a lot of verbosity in the main window and because I had ended up with some messy handler functions that all related actions would call into with a bunch of boolean flags. track_handler
for example, handled every track related action, and took a bunch of boolean arguments depending on what the caller was. So play-track
would call something like track_handler(1, true, false)
, append track_handler(3, false, true)
, or something to that effect.
The immediate problem with this, as should be quite clear, is: what exactly is any of this doing? This is why I wanted to change this. In the section of window.rs
where the actions were declared, there was effectively a list of at least 8 actions, all calling these handler functions, so without the surrounding code, you basically just saw:
self.track_handler(i, true, false);
self.track_handler(i, false, true);
self.track_handler(i, false, false);
self.disc_handler(i, true, false);
// ... etc, etc.
Which wasn’t very obvious what was going on, especially because the actions aren’t passed the actual objects they act on. The actions instead triggered the window to get the selected albums, tracks, etc. from the library and then handle them according to which action was called. The “handler” functions then came about because the alternative of repeatedly writing the same code to do this retrieval in the callback for every action just made the code verbose and annoying in a different way.
So what changed? Well everything was a MusicItem
now. I could have done the change before actually, the existance of the MusicItem
wasn’t necessary because I could have just upcasted everything to GObjects
, but the idea only came to me after solving the previous problems with the library. In short, instead of all the different actions in the window class, the window was left with pretty much one single function to handle everything. This function just took a list model of MusicItems and a boolean flag to indicate whether to play them or just add them to the queue. Since it already had the music, it could just pass them directly to the queue without needing the extra steps of retrieving them from the library.
Then, instead of using actions, I changed the library and album view to emit a signal whenever some selection of music items were activated, with these music items provided as the parameter to the signal. These signals were then all connected to that single function in the window.
The actions still existed though, they were just defined in the library and album view widgets now. But unlike when they were in the window, their callbacks now had access to the internals of the widgets they were defined in, and didn’t have to look in multiple places for the right objects to return.
Before, if one of the album actions was activated in the window, the callback in the window code would first check if an album was set on the album view (which means the user has opened it and the activation came from there, as it hides the library when it is open, preventing activation from there) and uses that album if it is set, otherwise checking the library.
Now, the album view has it’s own action that gets activated instead, so it just gets the album it is displaying and emits its items-activated
signal with a list model containing the album and a boolean indicating whether it’s to be played or not. This then calls the window’s callback function, which doesn’t need to know anything other than the fact that it has something that needs to be put in the queue and maybe played. The library does the same, depending on which action is activated, it either collects all the albums it is displaying or just the selected ones, and emits items-activated
with that collection. And unlike with the previous version, where every type had to be handled differently, if the user activates the action to play a selection of tracks in the currently displayed album, the album view can just collect the selected tracks and emit the same items-activated
signal, and again, the callback for the signal doesn’t care, it just puts them straight into the queue. At that point, the queue does then downcast the music items because it needs to handle the tracks and albums differently, but that’s fine because it is the endpoint. The fewer different codepaths data can travel through to get from A to B, the better. And now there’s only one.
The Dumpster Fire of A Queue #
And last but definitely not least, the biggest pile of spaghetti I had been stuck with since the port to Rust―and which had still been causing enumerable headaches well before the port, and even as far back as the initial implementation―was finally untangled. That mess being, of course, the play queue.
Now all of the problems with the queue I did to myself. I had ended up with a queue that could hold either tracks or albums, thereby needing to function as a tree; could be edited by adding, deleting, and re-arranging elements; had undo/redo capabilities; could be shuffled in-place without loosing the original order so it could be restored when shuffle was removed; and could shuffle either the top-level items or everything in the queue, which meant first flattening the albums into their track list and then shuffling the queue.
No wonder the code was a massive unreadable disaster.
I mean, it mostly worked, which was good. But the reason I considered it the worst part of the codebase was that every single one of the features I mentioned above were entangled together to the point that figuring out what certain functions were doing was difficult, and making any sort of changes the code was extraordinarily frustrating.
A Clear Example #
To demonstrate just how disastrous the code for the queue really was, here is the splice
function, which was how anything could be removed or added to the queue:
fn splice(
&self,
mut position: u32,
mut n_removals: u32,
additions: &[QueueItem],
// Rebuild is used to determine if the queue should be rebuilt after the splice,
// in cases where multiple splices are being performed in a row, it is more efficient
// to rebuild the queue once at the end, rather than after each splice.
rebuild: bool,
) -> Vec<QueueItem> {
let items = self.imp().items.borrow().clone();
let to_check = additions.iter().cloned();
let mut additions = to_check.clone();
let shuffle_swaps = n_removals < items.len() as u32 && additions.len() > 0;
if !items.is_empty() && n_removals != 0 && self.shuffle() == ShuffleMode::Off {
// run through the additions and check if they are the same as the ones
// already in the model up to the first non-matching item...
for addition in to_check {
if let Some(item) = items.get(position as usize) {
if item.same_as(&addition) {
position += 1;
n_removals -= 1;
additions.next();
if n_removals == 0 {
break;
}
continue;
}
}
break;
}
}
// ... Then if there are no more removals or additions, return because the additions
// were identical to the items already in the model, and no change is needed.
if 0 == n_removals && n_removals == additions.len() as u32 {
self.emit_by_name::<()>("queue-changed", &[]);
// Still record what is effectively a no-op, so if the current
// index changes after the splice it can be undone. (Overwriting
// the queue with the same contents resets the position to the start)
self.record_change(
QueueOperation::Splice(position, 0, vec![]),
self.real_current_index(),
self.active_shuffle_map(),
true,
);
return Vec::new();
}
let mut additions_len = additions.len() as u32;
// Actually perform the splice operation, making sure to borrow mutably in a
// smaller scope so it is released before items_changed is called.
let removed: Vec<QueueItem> = {
let mut items = self.imp().items.borrow_mut();
let removed = items.splice(
position as usize..(position + n_removals) as usize,
additions.clone(),
);
removed.collect()
};
// record the operation so it can be undone, this means
// taking the items that are going to be removed, and the length
// of the additions, so that the undo operation can be performed.
if self.imp().recording.get() {
self.record_change(
QueueOperation::Splice(position, additions_len, removed.clone()),
self.real_current_index(),
self.active_shuffle_map(),
true,
);
}
if rebuild {
self.rebuild_queue(None);
// Any additions to the queue when shuffle is on reshuffles the queue, when
// rebuild is true. Otherwise, this splice is probably being used internally for
// an undo/redo operation, and the shuffle map is being restored from the backup.
if self.shuffle() != ShuffleMode::Off && additions_len > 0 {
self.reshuffle(shuffle_swaps);
}
// self.reshuffle() will have already emitted queue-changed when shuffle is albums
if self.shuffle() != ShuffleMode::Albums {
self.emit_by_name::<()>("queue-changed", &[]);
}
}
// The shuffle map needs to be rebuilt if shuffle albums is enabled and items are removed,
// otherwise emiting items-changed will cause GTK to crash. If shuffle:tracks is on, the
// removals must be handled by the caller.
if self.shuffle() == ShuffleMode::Albums && !removed.is_empty() && additions_len == 0 {
let indices_to_remove: Vec<u32> = (position..(position + n_removals)).collect();
self.update_shuffle_maps(&indices_to_remove);
}
// When tracks is on, the number of removals and additions needs to be converted to
// the number of removals/additions in the queue, not the items list.
if self.shuffle() == ShuffleMode::Tracks {
n_removals = removed
.iter()
.map(|i| i.track_list().map_or(1, |tl| tl.n_items()))
.sum();
additions_len = additions
.map(|i| i.track_list().map_or(1, |tl| tl.n_items()))
.sum();
}
self.items_changed(position, n_removals, additions_len);
if rebuild {
self.update_current_highlight();
}
removed
}
This is, frankly, ridiculous. Now it didn’t start out like that of course, more that complexity slowly creeped in over time as more things needed to be added. In a simpler version, the only part of this entire wall of code would be this:1
fn splice(&self, mut position: u32, mut n_removals: u32, additions: &[QueueItem]) -> Vec<QueueItem>
let additions_len = additions.len();
let removed: Vec<QueueItem> = {
let mut items = self.imp().items.borrow_mut();
let removed = items.splice(
position as usize..(position + n_removals) as usize,
additions,
);
removed.collect()
};
self.items_changed(position, n_removals, additions_len)
removed
}
Clearly, something needed to be changed. I had put such a change off for two update cycles by this point, so during the UI update I decided to finally commit to fixing the mess.
Partitioning and Layering #
What I was finally able to settle on, after a few initial attempts where I couldn’t quite figure how how to translate shuffle into the new structure, was to partition out different functionality, like editing, shuffling, and indexing, into separate list models that operated on top of another list model. These list models could be composed on top of each other so each core function of the queue was in its own ’layer’, making it much easier to make future changes and maintain as each layer can be changed without immediately breaking other functionality.
For example, the splice function above became much more like the second example, as it is now part of the BaseQueueModel
, which handles editing the queue and the majority of the undo/redo mechanism. (A tiny amount needs to be also handled in the shuffle model and the indexer, but I’ll get to that)
The New Structure #
The original QueueModel
was one enormous blob. In the refactor, I was able to split it into four separate objects:
- The
QueueBaseModel
, which sits as the bottom list model and stores the actual items. It is the only model that allows for adding and removing items, and tracks changes for the undo/redo actions. - The
QueueShuffleModel
, which sits above theQueueBaseModel
and implements the shuffle-related functionality. It connects to theitems-changed
signal of the base model and updates itself accordingly. It also can internally swap in aFlattenListModel
which flattens out any albums into their track lists for the full shuffle mode. It also tracks undo/redo for operations which rearrange the queue, as when shuffle is enabled the order of items is determined at the shuffle layer - The
QueueIndexer
is just a regular Ruststruct
, not a list model or even aGObject
. It encapsulates the functionality for tracking the current position in the queue and moving it appropriately when the queue changes, as well as saving positions as needed for undo/redo. I decided to create this to enforce that the current index in the queue was only able to be updated in the specific ways allowed by the public functions on theQueueIndexer
. - The new
QueueModel
stores all three of the previous objects. It’sListModel
functions are passed through directly to theQueueShuffleModel
, so from the outside it behaves the same. It contains all the code needed to coordinate between the other components and handle anything that can’t be done through signal bindings. None of the three previous objects are exposed outside of theQueueModel
; the only interface other objects can interact with is theQueueModel
.
The Troubles With Shuffle #
I mentioned earlier that I had put off refactoring the queue a few times. The reason for this was that I was having trouble coming up with a method for shuffling the queue that would work for the QueueShuffleModel
. Without the QueueShuffleModel
, any refactor would not offer significant improvements over the old queue, because the shuffle functionality was the code that was the most tangled up in all the other code. If it wasn’t changed, most of the complexity would still be there, and I didn’t want to leave a half-finished refactor floating around in the code.
The old method I was using to shuffle was to essentially generate a permutation of the queue. When shuffle was enabled, a Vec
was created with the indexes of the elements in the queue in a random order. Then, the item(i)
method and any other ways of accessing items in the queue looked up the index in that Vec
and used that index to then get the item in the queue. When new items were added, additional indexes were added and the Vec
reshuffled. When any were removed, however, the specific indexes of the removed items had to be removed and then all the indexes greater then them decremented to ensure there were no gaps.
This was not possible to translate to a list model approach. It’s been a while since the update, and I don’t remember specifics, but one problem was the fact that QueueShuffleModel
only knows when the base model changes through the items-changed
signal. When that signal is emitted, any removed items are already gone from the base model, but to correctly update the permutation the shuffle model needs access to these removed items. A larger issue was interactions with undo/redo. When an undo or redo operation removes items from the queue, and a subsequent operation adds the same items back, they need to be in the same order. This would require either saving the entire permutation before any change to the queue, or again, some way of accessing items after they had been removed from the base model in order to record their positions.
To solve these issues, I eventually ended up using a GtkSortListModel
and a hash map to do the shuffling. A SortListModel
in GTK is exactly what you would expect, it sorts the list model it is given. It does the sort according to a GtkSorter
that it is provided, which in my case was a GtkCustomSorter
that uses a closure function which takes in two items and returns an ordering for it. The hash map uses the queue items as keys and floats as the values, In order to determine the ordering of two items, the closure first checks the hash map for existing values for each item to compare. If it doesn’t find one for either item, it randomly generates a non-zero float and stores that, using it as the sort key.
To reshuffle the list, all that needs to be done is clear the hash map and trigger a re-sort on the sort model. More importantly, however, is that this solves the problem of maintaining order across undo/redo operations. If an item is removed from the base model, it is not removed from the hash map, meaning the next time it is added to the queue it will be sorted to the same position and order is preserved. For the cases where order is not supposed to be preserved, (like when adding brand new items to the queue or overwriting it) all that needs to be done is to clear the hash map.
The Fire is Out #
The result of all this is that the most glaringly bad (in my opinion at least) code in the project is now significantly improved to the point where I don’t expect to be touching it again for the foreseeable future. The queue is working much better than it used to, and that is a win. There are still some small problems, I think the removed-selected
function doesn’t quite work correctly still, (if you delete a lot of items some don’t get removed for some reason) but fixing that in the old version of the queue would have been unpleasant, to say the least. Regardless, this allows me to shift my focus to other things, like adding some new features and fixing up the remaining edge cases with the database.
What’s Next #
Future plans for Recordbox are to fix the issues with the database/library. There have been several issues opened recently about tracks and albums being grouped incorrectly that need to be addressed. After the queue, the library and database management code has been the next most difficult part to work on, but for a different reason. While the queue was bad because the code was a mess, the library is tricky because of the sheer amount of ways music can be organized and the enumerable metadata permutations. I’d like to update the database to handle as many cases as possible so as to support as many different users’ libraries as possible.
Another big addition in the works is synchronized lyrics. Most of the core functionality is up and running right now, all that’s left is the fine details and the rest of the editing functionality. Need to update the saving mechanism too.
All of this is planned to come in 0.11.0
, and then on to 1.0.0
. More improvements and features are on the way!
-
The reason the actual splice is done in a smaller scope is so that the mutable reference is dropped before the
items-changed
signal is emitted, otherwise it would cause a panic. ↩︎