Commit 69df2fdb authored by Felix Häcker's avatar Felix Häcker

Use gio::ListStore instead of IndexMap in StationModel, create StationObject...

Use gio::ListStore instead of IndexMap in StationModel, create StationObject (gobject subclass) for proper gtk::ListBox integration using bind_model()
parent a6775491
Pipeline #66428 failed with stages
in 44 seconds
......@@ -1348,7 +1348,7 @@ dependencies = [
"log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
"url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
......@@ -1376,7 +1376,7 @@ dependencies = [
"protoc-rust 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
......@@ -1483,7 +1483,7 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.38"
version = "1.0.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
......@@ -1502,7 +1502,6 @@ dependencies = [
"gstreamer 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gstreamer-pbutils 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gtk 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
"indexmap 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libhandy 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
"mdns 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
......@@ -1513,6 +1512,7 @@ dependencies = [
"rusqlite 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)",
"rust_cast 0.14.0 (registry+https://github.com/rust-lang/crates.io-index)",
"rustio 0.0.1 (git+https://gitlab.gnome.org/haecker-felix/Rustio.git)",
"serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)",
"stopwatch 0.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
"uuid 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
......@@ -2071,7 +2071,7 @@ dependencies = [
"checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
"checksum serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)" = "9f301d728f2b94c9a7691c90f07b0b4e8a4517181d9461be94c04bddeb4bd850"
"checksum serde_derive 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)" = "beed18e6f5175aef3ba670e57c60ef3b1b74d250d962a26604bff4c80e970dd4"
"checksum serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)" = "27dce848e7467aa0e2fcaf0a413641499c0b745452aaca1194d24dedde9e13c9"
"checksum serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)" = "5a23aa71d4a4d43fdbfaac00eff68ba8a06a51759a89ac3304323e800c4dd40d"
"checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8"
"checksum smallvec 0.6.9 (registry+https://github.com/rust-lang/crates.io-index)" = "c4488ae950c49d403731982257768f48fada354a5203fe81f9bb6f43ca9002be"
"checksum stable_deref_trait 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8"
......
......@@ -8,8 +8,8 @@ edition = "2018"
[dependencies]
gtk = { version="0.6.0", features=["v3_22"] }
libhandy = { version="0.3.0", features=["v0_0_7"] }
gio = "0.6.0"
glib = "0.7.0"
glib = { version="0.7.0", features=["subclassing"] }
gio = { version="0.6.0", features=["v2_46"] }
gdk = "0.10.0"
gstreamer = "0.13.0"
gstreamer-pbutils = "0.13.0"
......@@ -18,7 +18,6 @@ mpris-player = { git="https://gitlab.gnome.org/World/Rust/mpris-player" }
log = "0.4"
pretty_env_logger = "0.3.0"
rusqlite = "0.13"
indexmap = "1.0"
quick-error = "1.2.2"
restson = "0.4"
stopwatch = "0.0.7"
......@@ -26,3 +25,4 @@ uuid = { version = "0.7", features = ["v4"] }
chrono = "0.4.6"
rust_cast = "0.14.0"
mdns = "0.3.1"
serde_json = "1.0.39"
{
"app-id" : "de.haeckerfelix.ShortwaveDevel",
"runtime" : "org.gnome.Platform",
"runtime-version" : "master",
"runtime-version" : "3.30",
"sdk" : "org.gnome.Sdk",
"sdk-extensions" : [
"org.freedesktop.Sdk.Extension.rust-stable"
......
......@@ -13,6 +13,7 @@ use std::thread;
use crate::app::Action;
use crate::config;
use crate::station_model::StationModel;
use crate::station_model::{Order, Sorting};
use crate::widgets::station_listbox::StationListBox;
use crate::widgets::station_row::ContentType;
......@@ -24,7 +25,7 @@ static SQL_INIT_COLLECTIONS: &str = " CREATE TABLE \"collections\" ('collection_
pub struct Library {
pub widget: gtk::Box,
station_listbox: RefCell<StationListBox>,
library_model: RefCell<StationModel>,
db_path: PathBuf,
builder: gtk::Builder,
......@@ -36,8 +37,11 @@ impl Library {
let builder = gtk::Builder::new_from_resource("/de/haeckerfelix/Shortwave/gtk/library.ui");
let widget: gtk::Box = builder.get_object("library").unwrap();
let content_box: gtk::Box = builder.get_object("content_box").unwrap();
let station_listbox = RefCell::new(StationListBox::new(sender.clone(), ContentType::Library));
content_box.add(&station_listbox.borrow().widget);
let library_model = RefCell::new(StationModel::new());
let station_listbox = StationListBox::new(sender.clone(), ContentType::Library);
station_listbox.bind_model(&library_model.borrow());
content_box.add(&station_listbox.widget);
let db_path = Self::get_database_path("shortwave.db").expect("Could not open database path...");
......@@ -48,7 +52,7 @@ impl Library {
let library = Self {
widget,
station_listbox,
library_model,
db_path,
builder,
sender,
......@@ -63,19 +67,23 @@ impl Library {
pub fn add_stations(&self, stations: Vec<Station>) {
debug!("Add {} station(s)", stations.len());
self.station_listbox.borrow_mut().add_stations(stations);
for station in stations {
self.library_model.borrow_mut().add_station(station.clone());
}
self.update_visible_page();
}
pub fn remove_stations(&self, stations: Vec<Station>) {
debug!("Remove {} station(s)", stations.len());
self.station_listbox.borrow_mut().remove_stations(stations);
for station in stations {
self.library_model.borrow_mut().remove_station(station.clone());
}
self.update_visible_page();
}
pub fn write_data(&self) {
debug!("Write library data to disk...");
Self::write_stations_to_db(&self.db_path, self.station_listbox.borrow().get_stations()).expect("Could not write stations to database.");
Self::write_stations_to_db(&self.db_path, self.library_model.borrow().clone()).expect("Could not write stations to database.");
}
pub fn import_from_path(&self, path: &PathBuf) -> Result<(), LibraryError> {
......@@ -100,12 +108,12 @@ impl Library {
}
pub fn export_to_path(&self, path: &PathBuf) -> Result<(), LibraryError> {
Self::write_stations_to_db(&path, self.station_listbox.borrow().get_stations()).expect("Could not export database.");
Self::write_stations_to_db(&path, self.library_model.borrow().clone()).expect("Could not export database.");
Ok(())
}
pub fn set_sorting(&self, sorting: Sorting, order: Order) {
self.station_listbox.borrow_mut().set_sorting(sorting, order);
self.library_model.borrow_mut().set_sorting(sorting, order);
}
fn read_stations_from_db(path: &PathBuf) -> Result<Vec<Station>, LibraryError> {
......@@ -131,7 +139,7 @@ impl Library {
Ok(result)
}
fn write_stations_to_db(path: &PathBuf, stations: Vec<Station>) -> Result<(), LibraryError> {
fn write_stations_to_db(path: &PathBuf, stations: StationModel) -> Result<(), LibraryError> {
let tmpdb = Self::get_database_path("tmp.db")?;
info!("Delete previous database data...");
......@@ -186,7 +194,7 @@ impl Library {
}
fn update_visible_page(&self) {
if self.station_listbox.borrow().get_stations().len() != 0 {
if self.library_model.borrow().len() != 0 {
self.set_visible_page("content");
} else {
self.set_visible_page("empty");
......@@ -224,7 +232,3 @@ quick_error! {
}
}
}
// TODO
// Lösung: Das Model/station_listbox einfach öffentlich (statisch) für alle module verfügbar machen
// ordentliches station model implementieren
......@@ -5,6 +5,9 @@ extern crate pretty_env_logger;
#[macro_use]
extern crate quick_error;
#[macro_use]
extern crate glib;
mod app;
mod config;
mod library;
......@@ -13,6 +16,7 @@ mod search;
mod song;
mod static_resource;
mod station_model;
mod station_object;
mod widgets;
mod window;
......
......@@ -5,12 +5,13 @@ use rustio::{Client, StationSearch};
use std::cell::RefCell;
use crate::app::Action;
use crate::station_model::StationModel;
use crate::widgets::station_listbox::StationListBox;
use crate::widgets::station_row::ContentType;
pub struct Search {
pub widget: gtk::Box,
station_listbox: RefCell<StationListBox>,
result_model: RefCell<StationModel>,
builder: gtk::Builder,
sender: Sender<Action>,
......@@ -21,13 +22,15 @@ impl Search {
let builder = gtk::Builder::new_from_resource("/de/haeckerfelix/Shortwave/gtk/search.ui");
let widget: gtk::Box = builder.get_object("search").unwrap();
let result_model = RefCell::new(StationModel::new());
let results_box: gtk::Box = builder.get_object("results_box").unwrap();
let station_listbox = RefCell::new(StationListBox::new(sender.clone(), ContentType::Other));
results_box.add(&station_listbox.borrow().widget);
let station_listbox = StationListBox::new(sender.clone(), ContentType::Other);
station_listbox.bind_model(&result_model.borrow());
results_box.add(&station_listbox.widget);
let search = Self {
widget,
station_listbox,
result_model,
builder,
sender,
};
......@@ -42,8 +45,10 @@ impl Search {
let mut client = Client::new("http://www.radio-browser.info");
let result = client.search(data).unwrap();
self.station_listbox.borrow_mut().clear();
self.station_listbox.borrow_mut().add_stations(result);
self.result_model.borrow_mut().clear();
for station in result {
self.result_model.borrow_mut().add_station(station);
}
}
fn setup_signals(&self) {
......
use indexmap::map::Entry;
use indexmap::IndexMap;
use gio::prelude::ListStoreExtManual;
use gio::prelude::*;
use glib::prelude::*;
use rustio::Station;
use crate::station_object::StationObject;
#[derive(Clone, Debug)]
pub enum Sorting {
Name,
......@@ -13,7 +16,7 @@ pub enum Sorting {
Bitrate,
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, PartialEq)]
pub enum Order {
Ascending,
Descending,
......@@ -21,104 +24,97 @@ pub enum Order {
#[derive(Clone, Debug)]
pub struct StationModel {
map: IndexMap<u32, Station>,
pub model: gio::ListStore,
sorting: Sorting,
order: Order,
iter_id: u32,
}
impl StationModel {
pub fn new() -> Self {
let map: IndexMap<u32, Station> = IndexMap::new();
let model = gio::ListStore::new(StationObject::static_type());
let sorting = Sorting::Name;
let order = Order::Ascending;
Self { map, sorting, order }
}
let iter_id = 0;
pub fn export_vec(&self) -> Vec<Station> {
let mut result = Vec::new();
for (_id, station) in self.map.clone() {
result.insert(0, station);
}
result
Self { model, sorting, order, iter_id }
}
pub fn add_station(&mut self, station: Station) -> Option<usize> {
let mut index = None;
if !self.contains(&station) {
let id = station.id.parse::<u32>().unwrap();
self.map.insert(id.clone(), station);
self.sort();
index = match self.map.entry(id) {
Entry::Occupied(e) => Some(e.index()),
_ => None,
};
}
index
pub fn add_station(&mut self, station: Station) {
let object = StationObject::new(station.clone());
let sorting = self.sorting.clone();
let order = self.order.clone();
self.model.insert_sorted(&object, move |a, b| Self::station_cmp(a, b, sorting.clone(), order.clone()));
}
pub fn remove_station(&mut self, station: Station) -> Option<usize> {
let mut index = None;
if self.contains(&station) {
let id = station.id.parse::<u32>().unwrap();
index = Some(self.map.swap_remove_full(&id).unwrap().0);
self.sort();
pub fn remove_station(&mut self, station: Station) {
for i in 0..self.model.get_n_items() {
let gobject = self.model.get_object(i).unwrap();
let station_object = gobject.downcast_ref::<StationObject>().expect("StationObject is of wrong type");
let s = station_object.to_station();
if s == station {
self.model.remove(i);
break;
}
}
index
}
pub fn contains(&self, station: &Station) -> bool {
let id = station.id.parse::<u32>().unwrap();
self.map.contains_key(&id)
pub fn set_sorting(&mut self, sorting: Sorting, order: Order) {
self.sorting = sorting.clone();
self.order = order.clone();
self.model.sort(move |a, b| Self::station_cmp(a, b, sorting.clone(), order.clone()));
}
pub fn set_sorting(&mut self, sorting: Sorting, order: Order) {
self.sorting = sorting;
self.order = order;
pub fn clear(&mut self) {
self.model.remove_all();
}
pub fn sort(&mut self) {
let order = self.order.clone();
let sorting = self.sorting.clone();
pub fn len(&self) -> u32 {
self.model.get_n_items()
}
self.map.sort_by(move |_, b, _, d| {
let station_a: Station;
let station_b: Station;
match order {
Order::Ascending => {
station_a = b.clone();
station_b = d.clone();
}
Order::Descending => {
station_b = b.clone();
station_a = d.clone();
}
}
fn station_cmp(a: &gio::Object, b: &gio::Object, sorting: Sorting, order: Order) -> std::cmp::Ordering {
let mut station_a: Station = a.downcast_ref::<StationObject>().unwrap().to_station();
let mut station_b: Station = b.downcast_ref::<StationObject>().unwrap().to_station();
match sorting {
Sorting::Name => station_a.name.cmp(&station_b.name),
Sorting::Language => station_a.language.cmp(&station_b.language),
Sorting::Country => station_a.country.cmp(&station_b.country),
Sorting::State => station_a.state.cmp(&station_b.state),
Sorting::Codec => station_a.codec.cmp(&station_b.codec),
Sorting::Votes => station_a.votes.parse::<i32>().unwrap().cmp(&station_b.votes.parse::<i32>().unwrap()),
Sorting::Bitrate => station_a.bitrate.parse::<i32>().unwrap().cmp(&station_b.bitrate.parse::<i32>().unwrap()),
}
});
}
if order == Order::Descending {
let tmp = station_a;
station_a = station_b;
station_b = tmp;
}
pub fn clear(&mut self) {
self.map.clear();
match sorting {
Sorting::Name => station_a.name.cmp(&station_b.name),
Sorting::Language => station_a.language.cmp(&station_b.language),
Sorting::Country => station_a.country.cmp(&station_b.country),
Sorting::State => station_a.state.cmp(&station_b.state),
Sorting::Codec => station_a.codec.cmp(&station_b.codec),
Sorting::Votes => station_a.votes.parse::<i32>().unwrap().cmp(&station_b.votes.parse::<i32>().unwrap()),
Sorting::Bitrate => station_a.bitrate.parse::<i32>().unwrap().cmp(&station_b.bitrate.parse::<i32>().unwrap()),
}
}
}
impl IntoIterator for StationModel {
type Item = (u32, Station);
type IntoIter = ::indexmap::map::IntoIter<u32, Station>;
impl Iterator for StationModel {
type Item = Station;
fn next(&mut self) -> Option<Station> {
let max = self.len();
let mut result = None;
fn into_iter(self) -> Self::IntoIter {
self.map.into_iter()
if self.iter_id < max {
let gobject = self.model.get_object(self.iter_id).unwrap();
let station_object = gobject.downcast_ref::<StationObject>().expect("StationObject is of wrong type");
result = Some(station_object.to_station());
self.iter_id = self.iter_id + 1;
}
result
}
}
// StationObject is a GObject subclass, which we need to carry the rustio::Station struct.
// With this we can use gtk::ListBox bind_model() properly.
//
// For more details, you should look at this gtk-rs example:
// https://github.com/gtk-rs/examples/blob/master/src/bin/listbox_model.rs
use super::*;
use gtk::prelude::*;
use rustio::Station;
use glib::subclass;
use glib::subclass::prelude::*;
use glib::translate::*;
mod imp {
use super::*;
use std::cell::RefCell;
pub struct StationObject {
data: RefCell<Option<String>>,
}
static PROPERTIES: [subclass::Property; 1] = [subclass::Property("data", |name| {
glib::ParamSpec::string(
name,
"Data",
"Data",
None, // Default value
glib::ParamFlags::READWRITE,
)
})];
impl ObjectSubclass for StationObject {
const NAME: &'static str = "StationObject";
type ParentType = glib::Object;
type Instance = subclass::simple::InstanceStruct<Self>;
type Class = subclass::simple::ClassStruct<Self>;
glib_object_subclass!();
fn class_init(klass: &mut Self::Class) {
klass.install_properties(&PROPERTIES);
}
fn new() -> Self {
Self { data: RefCell::new(None) }
}
}
impl ObjectImpl for StationObject {
glib_object_impl!();
fn set_property(&self, _obj: &glib::Object, id: usize, value: &glib::Value) {
let prop = &PROPERTIES[id];
match *prop {
subclass::Property("data", ..) => {
let data = value.get();
self.data.replace(data);
}
_ => unimplemented!(),
}
}
fn get_property(&self, _obj: &glib::Object, id: usize) -> Result<glib::Value, ()> {
let prop = &PROPERTIES[id];
match *prop {
subclass::Property("data", ..) => Ok(self.data.borrow().to_value()),
_ => unimplemented!(),
}
}
}
}
glib_wrapper! {
pub struct StationObject(Object<subclass::simple::InstanceStruct<imp::StationObject>, subclass::simple::ClassStruct<imp::StationObject>, StationObjectClass>);
match fn {
get_type => || imp::StationObject::get_type().to_glib(),
}
}
impl StationObject {
pub fn new(station: Station) -> StationObject {
glib::Object::new(Self::static_type(), &[("data", &serde_json::to_string(&station).unwrap())])
.unwrap()
.downcast()
.unwrap()
}
pub fn to_station(&self) -> Station {
let data = self.get_property("data").unwrap().get::<String>().unwrap();
serde_json::from_str(&data).unwrap()
}
}
......@@ -14,7 +14,7 @@ impl SongRow {
pub fn new(song: Song) -> Self {
let widget = ActionRow::new();
widget.set_title(&song.title);
widget.set_subtitle(&Self::format_duration(song.duration.elapsed().as_secs())); //TODO: Display time correctly
widget.set_subtitle(&Self::format_duration(song.duration.elapsed().as_secs()));
widget.set_icon_name("");
let save_button = gtk::Button::new();
......
use glib::Sender;
use gtk::prelude::*;
use libhandy::{Column, ColumnExt};
use rustio::Station;
use crate::app::Action;
use crate::station_model::{Order, Sorting, StationModel};
use crate::station_model::StationModel;
use crate::station_object::StationObject;
use crate::widgets::station_row::{ContentType, StationRow};
pub struct StationListBox {
pub widget: gtk::Box,
listbox: gtk::ListBox,
station_model: StationModel,
content_type: ContentType,
sender: Sender<Action>,
......@@ -21,7 +20,6 @@ impl StationListBox {
let builder = gtk::Builder::new_from_resource("/de/haeckerfelix/Shortwave/gtk/station_listbox.ui");
let widget: gtk::Box = builder.get_object("station_listbox").unwrap();
let listbox: gtk::ListBox = builder.get_object("listbox").unwrap();
let station_model = StationModel::new();
// Setup HdyColumn
let column = Column::new();
......@@ -35,66 +33,18 @@ impl StationListBox {
Self {
widget,
listbox,
station_model,
content_type,
sender,
}
}
pub fn add_stations(&mut self, stations: Vec<Station>) {
for station in stations {
match self.station_model.add_station(station.clone()) {
Some(index) => {
let row = StationRow::new(self.sender.clone(), station, self.content_type.clone());
self.listbox.insert(&row.widget, index as i32);
}
None => (),
}
}
}
pub fn remove_stations(&mut self, stations: Vec<Station>) {
for station in stations {
match self.station_model.remove_station(station) {
Some(index) => {
let row = self.listbox.get_row_at_index(index as i32).unwrap();
self.listbox.remove(&row);
}
None => (),
}
}
}
pub fn get_stations(&self) -> Vec<Station> {
self.station_model.export_vec()
}
pub fn set_sorting(&mut self, sorting: Sorting, order: Order) {
self.station_model.set_sorting(sorting, order);
self.station_model.sort();
self.refresh();
}
fn refresh(&self) {
// remove all rows
for widget in self.listbox.get_children() {
widget.destroy();
}
// sort
for (_, station) in self.station_model.clone() {
let row = StationRow::new(self.sender.clone(), station, self.content_type.clone());
self.listbox.add(&row.widget);
}
}
pub fn clear(&mut self) {
// remove all rows
for widget in self.listbox.get_children() {
widget.destroy();
}
pub fn bind_model(&self, model: &StationModel) {
let sender = self.sender.clone();
let content_type = self.content_type.clone();
// clear station_model
self.station_model.clear();
self.listbox.bind_model(&model.model, move |station| {
let row = StationRow::new(sender.clone(), station.downcast_ref::<StationObject>().unwrap().to_station(), content_type.clone());
row.widget.upcast::<gtk::Widget>()
});
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment