Working non-state machine stack implementation.

Removed the stack state-machines. It was confusing trying to both
implement statemachines and re-design the stack architecture at the same time.
This commit is contained in:
Jordan Petridis 2017-12-12 16:01:19 +02:00
parent 211b36dfa3
commit 01310ee7fa
No known key found for this signature in database
GPG Key ID: CEABAD9F5683B9A6
6 changed files with 146 additions and 312 deletions

View File

@ -1,318 +1,167 @@
use gtk;
use gtk::prelude::*;
// use hammond_data::Podcast;
use hammond_data::Podcast;
use hammond_data::dbqueries;
use widgets::podcast::PodcastWidget;
use views::podcasts::PopulatedView;
use views::empty::EmptyView;
use widgets::podcast::PodcastWidget;
use std::rc::Rc;
#[derive(Debug, Clone)]
pub struct Content {
pub stack: gtk::Stack,
shows: ShowStateWrapper,
episodes: EpisodeStateWrapper,
shows: Rc<ShowStack>,
episodes: Rc<EpisodeStack>,
}
impl Content {
pub fn new() -> Content {
pub fn new() -> Rc<Content> {
let stack = gtk::Stack::new();
let shows = ShowStateWrapper::new();
let episodes = EpisodeStateWrapper::new();
let shows = ShowStack::new();
let episodes = EpisodeStack::new();
let shows_stack = shows.get_stack();
let ep_stack = episodes.get_stack();
stack.add_titled(&episodes.stack, "episodes", "Episodes");
stack.add_titled(&shows.stack, "shows", "Shows");
stack.add_titled(&ep_stack, "episodes", "Episodes");
stack.add_titled(&shows_stack, "shows", "Shows");
Content {
Rc::new(Content {
stack,
shows,
episodes,
}
})
}
pub fn update(&mut self) {
self.shows = self.shows.clone().update();
// FIXME: like above
pub fn update(&self) {
self.shows.update();
self.episodes.update();
}
}
// pub fn on_podcasts_child_activate(stack: &gtk::Stack, pd: &Podcast) {
// update_widget(stack, pd);
// stack.set_visible_child_full("widget", gtk::StackTransitionType::SlideLeft);
// }
// FIXME: Rename and remove aliases
type ShowsPopulated = PopulatedView;
type ShowsEmpty = EmptyView;
type EpisodesPopulated = PodcastWidget;
type EpisodesEmpty = EmptyView;
#[derive(Debug, Clone)]
struct Populated;
#[derive(Debug, Clone)]
struct Empty;
// struct Shows;
// struct Episodes;
// Thats probably too overengineered
// struct StackStateMachine<S, T> {
// shows: ShowsMachine<S>,
// episodes: EpisodesMachine<S>,
// stack: gtk::Stack,
// state: T,
// }
#[derive(Debug, Clone)]
struct ShowsMachine<S> {
populated: ShowsPopulated,
empty: ShowsEmpty,
stack: gtk::Stack,
state: S,
pub struct ShowStack {
pub stack: gtk::Stack,
}
impl<S> ShowsMachine<S> {
fn new(state: S) -> ShowsMachine<S> {
impl ShowStack {
fn new() -> Rc<ShowStack> {
let stack = gtk::Stack::new();
let pop = ShowsPopulated::new_initialized();
let show = Rc::new(ShowStack { stack });
let pop = PopulatedView::new_initialized(show.clone());
let widget = PodcastWidget::new();
let empty = EmptyView::new();
stack.add_named(&pop.container, "populated");
stack.add_named(&empty.container, "empty");
ShowsMachine {
empty,
populated: pop,
stack,
state,
show.stack.add_named(&pop.container, "podcasts");
show.stack.add_named(&widget.container, "widget");
show.stack.add_named(&empty.container, "empty");
if pop.is_empty() {
show.stack.set_visible_child_name("empty")
} else {
show.stack.set_visible_child_name("podcasts")
}
show
}
fn update(&mut self) {
// fn is_empty(&self) -> bool {
// self.podcasts.is_empty()
// }
pub fn update(&self) {
self.update_podcasts();
self.update_widget();
}
pub fn update_podcasts(&self) {
let vis = self.stack.get_visible_child_name().unwrap();
let old = self.stack.get_child_by_name("populated").unwrap();
let old = self.stack.get_child_by_name("podcasts").unwrap();
let pop = PopulatedView::new();
pop.init(Rc::new(self.clone()));
self.stack.remove(&old);
self.stack.add_named(&pop.container, "podcasts");
let pop = ShowsPopulated::new_initialized();
self.populated = pop;
self.stack.add_named(&self.populated.container, "populated");
self.stack.set_visible_child_name(&vis);
if pop.is_empty() {
self.stack.set_visible_child_name("empty");
} else if vis != "empty" {
self.stack.set_visible_child_name(&vis);
} else {
self.stack.set_visible_child_name("podcasts");
}
old.destroy();
}
}
#[derive(Debug, Clone)]
struct EpisodesMachine<S> {
populated: EpisodesPopulated,
empty: EpisodesEmpty,
stack: gtk::Stack,
state: S,
}
pub fn replace_widget(&self, pd: &Podcast) {
let old = self.stack.get_child_by_name("widget").unwrap();
let new = PodcastWidget::new_initialized(Rc::new(self.clone()), pd);
impl<S> EpisodesMachine<S> {
// FIXME:
fn update(&mut self) {
self.stack.remove(&old);
self.stack.add_named(&new.container, "widget");
}
pub fn update_widget(&self) {
let vis = self.stack.get_visible_child_name().unwrap();
let old = self.stack.get_child_by_name("populated").unwrap();
let old = self.stack.get_child_by_name("widget").unwrap();
let id = WidgetExt::get_name(&old).unwrap();
if id == "GtkBox" {
return;
}
let pd = dbqueries::get_podcast_from_id(id.parse::<i32>().unwrap()).unwrap();
let pdw = EpisodesPopulated::new_initialized(&self.stack, &pd);
self.populated = pdw;
self.stack.remove(&old);
self.stack.add_named(&self.populated.container, "populated");
self.stack.set_visible_child_name(&vis);
}
}
// impl Into<StackStateMachine<Populated, Shows>> for StackStateMachine<Populated, Episodes> {
// fn into(self) -> StackStateMachine<Populated, Shows> {
// self.stack.set_visible_child_name("shows");
// StackStateMachine {
// shows: self.shows,
// episodes: self.episodes,
// stack: self.stack,
// state: Shows {},
// }
// }
// }
// impl Into<StackStateMachine<Populated, Episodes>> for StackStateMachine<Populated, Shows> {
// fn into(self) -> StackStateMachine<Populated, Episodes> {
// self.stack.set_visible_child_name("episodes");
// StackStateMachine {
// shows: self.shows,
// episodes: self.episodes,
// stack: self.stack,
// state: Episodes {},
// }
// }
// }
// TODO: Impl <From> instead of <Into>
impl Into<ShowsMachine<Populated>> for ShowsMachine<Empty> {
fn into(self) -> ShowsMachine<Populated> {
self.stack.set_visible_child_name("populated");
ShowsMachine {
populated: self.populated,
empty: self.empty,
stack: self.stack,
state: Populated {},
let pd = dbqueries::get_podcast_from_id(id.parse::<i32>().unwrap());
if let Ok(pd) = pd {
self.replace_widget(&pd);
self.stack.set_visible_child_name(&vis);
old.destroy();
}
}
}
impl Into<ShowsMachine<Empty>> for ShowsMachine<Populated> {
fn into(self) -> ShowsMachine<Empty> {
self.stack.set_visible_child_name("empty");
ShowsMachine {
populated: self.populated,
empty: self.empty,
stack: self.stack,
state: Empty {},
}
pub fn switch_podcasts_animated(&self) {
self.stack
.set_visible_child_full("podcasts", gtk::StackTransitionType::SlideRight);
}
}
impl Into<EpisodesMachine<Populated>> for EpisodesMachine<Empty> {
fn into(self) -> EpisodesMachine<Populated> {
self.stack.set_visible_child_name("populated");
EpisodesMachine {
populated: self.populated,
empty: self.empty,
stack: self.stack,
state: Populated {},
}
pub fn switch_widget_animated(&self) {
self.stack
.set_visible_child_full("widget", gtk::StackTransitionType::SlideLeft)
}
}
impl Into<EpisodesMachine<Empty>> for EpisodesMachine<Populated> {
fn into(self) -> EpisodesMachine<Empty> {
self.stack.set_visible_child_name("empty");
EpisodesMachine {
populated: self.populated,
empty: self.empty,
stack: self.stack,
state: Empty {},
}
}
}
// enum StackStateWrapper<S> {
// Shows(StackStateMachine<S, Shows>),
// Episodes(StackStateMachine<S, Episodes>),
// }
#[derive(Debug, Clone)]
enum ShowStateWrapper {
Populated(ShowsMachine<Populated>),
Empty(ShowsMachine<Empty>),
}
#[derive(Debug, Clone)]
enum EpisodeStateWrapper {
Populated(EpisodesMachine<Populated>),
Empty(EpisodesMachine<Empty>),
struct RecentEpisodes;
#[derive(Debug, Clone)]
struct EpisodeStack {
// populated: RecentEpisodes,
// empty: EmptyView,
stack: gtk::Stack,
}
// impl <S>StackStateWrapper<S> {
// fn switch(mut self) -> Self {
// match self {
// StackStateWrapper::Shows(val) => StackStateWrapper::Episodes(val.into()),
// StackStateWrapper::Episodes(val) => StackStateWrapper::Shows(val.into())
// }
// }
// }
impl ShowStateWrapper {
fn new() -> Self {
let machine = ShowsMachine::new(Populated {});
if machine.populated.flowbox.get_children().is_empty() {
machine.stack.set_visible_child_name("empty");
ShowStateWrapper::Empty(machine.into())
} else {
machine.stack.set_visible_child_name("populated");
ShowStateWrapper::Populated(machine)
}
}
fn update(mut self) -> Self {
match self {
ShowStateWrapper::Populated(ref mut val) => val.update(),
ShowStateWrapper::Empty(ref mut val) => val.update(),
}
if self.is_empty() {
match self {
ShowStateWrapper::Populated(val) => ShowStateWrapper::Empty(val.into()),
_ => self,
}
} else {
match self {
ShowStateWrapper::Empty(val) => ShowStateWrapper::Populated(val.into()),
_ => self,
}
}
}
fn get_stack(&self) -> gtk::Stack {
match *self {
ShowStateWrapper::Populated(ref val) => val.stack.clone(),
ShowStateWrapper::Empty(ref val) => val.stack.clone(),
}
}
fn is_empty(&self) -> bool {
match *self {
ShowStateWrapper::Populated(ref val) => val.populated.flowbox.get_children().is_empty(),
ShowStateWrapper::Empty(ref val) => val.populated.flowbox.get_children().is_empty(),
}
}
}
impl EpisodeStateWrapper {
// FIXME:
fn new() -> Self {
let pop = PodcastWidget::new();
impl EpisodeStack {
fn new() -> Rc<EpisodeStack> {
let _pop = RecentEpisodes {};
let empty = EmptyView::new();
let stack = gtk::Stack::new();
stack.add_named(&pop.container, "populated");
// stack.add_named(&pop.container, "populated");
stack.add_named(&empty.container, "empty");
// FIXME:
stack.set_visible_child_name("empty");
EpisodeStateWrapper::Empty(EpisodesMachine {
empty,
populated: pop,
Rc::new(EpisodeStack {
// empty,
// populated: pop,
stack,
state: Empty {},
})
}
fn update(&mut self) {
match *self {
EpisodeStateWrapper::Populated(ref mut val) => val.update(),
EpisodeStateWrapper::Empty(ref mut val) => val.update(),
}
}
fn get_stack(&self) -> gtk::Stack {
match *self {
EpisodeStateWrapper::Populated(ref val) => val.stack.clone(),
EpisodeStateWrapper::Empty(ref val) => val.stack.clone(),
}
fn update(&self) {
// unimplemented!()
}
}

View File

@ -4,7 +4,7 @@ use gtk::prelude::*;
use hammond_data::Source;
use hammond_data::utils::url_cleaner;
use std::sync::{Arc, Mutex};
use std::rc::Rc;
use utils;
use content::Content;
@ -36,23 +36,19 @@ impl Header {
}
}
pub fn new_initialized(content: Arc<Mutex<Content>>) -> Header {
pub fn new_initialized(content: Rc<Content>) -> Header {
let header = Header::new();
header.init(content);
header
}
fn init(&self, content: Arc<Mutex<Content>>) {
fn init(&self, content: Rc<Content>) {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/headerbar.ui");
let add_popover: gtk::Popover = builder.get_object("add-popover").unwrap();
let new_url: gtk::Entry = builder.get_object("new-url").unwrap();
let add_button: gtk::Button = builder.get_object("add-button").unwrap();
{
let cont = content.lock().unwrap();
self.switch.set_stack(&cont.stack);
}
self.switch.set_stack(&content.stack);
new_url.connect_changed(move |url| {
println!("{:?}", url.get_text());
@ -68,14 +64,13 @@ impl Header {
self.add_toggle.set_popover(&add_popover);
// FIXME: There appears to be a memmory leak here.
let cont = content.clone();
self.refresh.connect_clicked(move |_| {
utils::refresh_feed(cont.clone(), None, None);
utils::refresh_feed(content.clone(), None, None);
});
}
}
fn on_add_bttn_clicked(content: Arc<Mutex<Content>>, entry: &gtk::Entry) {
fn on_add_bttn_clicked(content: Rc<Content>, entry: &gtk::Entry) {
let url = entry.get_text().unwrap_or_default();
let url = url_cleaner(&url);
let source = Source::from_url(&url);

View File

@ -1,3 +1,5 @@
#![cfg_attr(feature = "cargo-clippy", allow(clone_on_ref_ptr))]
extern crate gdk;
extern crate gdk_pixbuf;
extern crate gio;
@ -22,8 +24,6 @@ use hammond_data::utils::checkup;
use gtk::prelude::*;
use gio::{ActionMapExt, ApplicationExt, MenuExt, SimpleActionExt};
use std::sync::{Arc, Mutex};
// http://gtk-rs.org/tuto/closures
#[macro_export]
macro_rules! clone {
@ -72,7 +72,6 @@ fn build_ui(app: &gtk::Application) {
// let ct = content::Content::new_initialized();
let ct = content::Content::new();
let stack = ct.stack.clone();
let ct = Arc::new(Mutex::new(ct));
window.add(&stack);
window.connect_delete_event(|w, _| {

View File

@ -1,5 +1,4 @@
use glib;
use gtk;
use gdk_pixbuf::Pixbuf;
use hammond_data::feed;
@ -9,14 +8,12 @@ use hammond_downloader::downloader;
use std::{thread, time};
use std::cell::RefCell;
use std::sync::mpsc::{channel, Receiver};
use std::borrow::Cow;
use content::Content;
use regex::Regex;
use std::sync::{Arc, Mutex};
use std::rc::Rc;
type Foo = RefCell<Option<(Arc<Mutex<Content>>, Receiver<bool>)>>;
type Foo = RefCell<Option<(Rc<Content>, Receiver<bool>)>>;
// Create a thread local storage that will store the arguments to be transfered.
thread_local!(static GLOBAL: Foo = RefCell::new(None));
@ -25,13 +22,13 @@ thread_local!(static GLOBAL: Foo = RefCell::new(None));
/// If `source` is None, Fetches all the `Source` entries in the database and updates them.
/// `delay` represents the desired time in seconds for the thread to sleep before executing.
/// When It's done,it queues up a `podcast_view` refresh.
pub fn refresh_feed(content: Arc<Mutex<Content>>, source: Option<Vec<Source>>, delay: Option<u64>) {
pub fn refresh_feed(content: Rc<Content>, source: Option<Vec<Source>>, delay: Option<u64>) {
// Create a async channel.
let (sender, receiver) = channel();
// Pass the desired arguments into the Local Thread Storage.
GLOBAL.with(clone!(content => move |global| {
*global.borrow_mut() = Some((content, receiver));
*global.borrow_mut() = Some((content.clone(), receiver));
}));
thread::spawn(move || {
@ -61,7 +58,6 @@ fn refresh_podcasts_view() -> glib::Continue {
GLOBAL.with(|global| {
if let Some((ref content, ref reciever)) = *global.borrow() {
if reciever.try_recv().is_ok() {
let mut content = content.lock().unwrap();
content.update();
}
}
@ -74,18 +70,6 @@ pub fn get_pixbuf_from_path(pd: &Podcast) -> Option<Pixbuf> {
Pixbuf::new_from_file_at_scale(&img_path, 256, 256, true).ok()
}
#[allow(dead_code)]
// WIP: parse html to markup
pub fn html_to_markup(s: &mut str) -> Cow<str> {
s.trim();
s.replace('&', "&amp;");
s.replace('<', "&lt;");
s.replace('>', "&gt;");
let re = Regex::new("(?P<url>https?://[^\\s&,)(\"]+(&\\w=[\\w._-]?)*(#[\\w._-]+)?)").unwrap();
re.replace_all(s, "<a href=\"$url\">$url</a>")
}
#[cfg(test)]
mod tests {
use hammond_data::Source;

View File

@ -7,8 +7,9 @@ use hammond_data::dbqueries;
use hammond_data::Podcast;
use utils::get_pixbuf_from_path;
use content::ShowStack;
// use content;
use std::rc::Rc;
#[derive(Debug, Clone)]
pub struct PopulatedView {
@ -42,25 +43,29 @@ impl PopulatedView {
}
#[allow(dead_code)]
pub fn new_initialized() -> PopulatedView {
pub fn new_initialized(show: Rc<ShowStack>) -> PopulatedView {
let pop = PopulatedView::new();
pop.init();
pop.init(show);
pop
}
pub fn init(&self) {
// pub fn init(&self, stack: &gtk::Stack) {
// use gtk::WidgetExt;
pub fn init(&self, show: Rc<ShowStack>) {
use gtk::WidgetExt;
// // TODO: handle unwraps.
// self.flowbox
// .connect_child_activated(clone!(stack => move |_, child| {
// // This is such an ugly hack...
// // let id = child.get_name().unwrap().parse::<i32>().unwrap();
// let id = WidgetExt::get_name(child).unwrap().parse::<i32>().unwrap();
// let parent = dbqueries::get_podcast_from_id(id).unwrap();
// on_flowbox_child_activate(&stack, &parent);
// }));
// TODO: handle unwraps.
// Note: flowbox_activation always adds "widnget" into the stack and switch to it,
// TODO: implement back button.
// so back button should always remove "widget" and destroy it.
let show = show.clone();
self.flowbox
.connect_child_activated(clone!(show => move |_, child| {
// This is such an ugly hack...
let id = WidgetExt::get_name(child).unwrap().parse::<i32>().unwrap();
let pd = dbqueries::get_podcast_from_id(id).unwrap();
show.replace_widget(&pd);
show.switch_widget_animated();
}));
// Populate the flowbox with the Podcasts.
self.populate_flowbox();
}
@ -76,6 +81,10 @@ impl PopulatedView {
self.flowbox.show_all();
}
}
pub fn is_empty(&self) -> bool {
self.flowbox.get_children().is_empty()
}
}
impl PodcastChild {
@ -139,7 +148,3 @@ impl PodcastChild {
}
}
}
// fn on_flowbox_child_activate(stack: &gtk::Stack, parent: &Podcast) {
// content::on_podcasts_child_activate(stack, parent)
// }

View File

@ -10,7 +10,8 @@ use hammond_downloader::downloader;
use widgets::episode::episodes_listbox;
use utils::get_pixbuf_from_path;
// use content;
use content::ShowStack;
use std::rc::Rc;
#[derive(Debug, Clone)]
pub struct PodcastWidget {
@ -47,18 +48,18 @@ impl PodcastWidget {
}
}
pub fn new_initialized(stack: &gtk::Stack, pd: &Podcast) -> PodcastWidget {
pub fn new_initialized(shows: Rc<ShowStack>, pd: &Podcast) -> PodcastWidget {
let pdw = PodcastWidget::new();
pdw.init(stack, pd);
pdw.init(shows, pd);
pdw
}
pub fn init(&self, stack: &gtk::Stack, pd: &Podcast) {
pub fn init(&self, shows: Rc<ShowStack>, pd: &Podcast) {
WidgetExt::set_name(&self.container, &pd.id().to_string());
// TODO: should spawn a thread to avoid locking the UI probably.
self.unsub.connect_clicked(clone!(stack, pd => move |bttn| {
on_unsub_button_clicked(&stack, &pd, bttn);
self.unsub.connect_clicked(clone!(shows, pd => move |bttn| {
on_unsub_button_clicked(shows.clone(), &pd, bttn);
}));
self.title.set_text(pd.title());
@ -77,8 +78,8 @@ impl PodcastWidget {
self.cover.set_from_pixbuf(&i);
}
self.played.connect_clicked(clone!(stack, pd => move |_| {
on_played_button_clicked(&stack, &pd);
self.played.connect_clicked(clone!(shows, pd => move |_| {
on_played_button_clicked(shows.clone(), &pd);
}));
self.show_played_button(pd);
@ -95,7 +96,7 @@ impl PodcastWidget {
}
}
fn on_unsub_button_clicked(stack: &gtk::Stack, pd: &Podcast, unsub_button: &gtk::Button) {
fn on_unsub_button_clicked(shows: Rc<ShowStack>, pd: &Podcast, unsub_button: &gtk::Button) {
let res = dbqueries::remove_feed(pd);
if res.is_ok() {
info!("{} was removed succesfully.", pd.title());
@ -111,11 +112,12 @@ fn on_unsub_button_clicked(stack: &gtk::Stack, pd: &Podcast, unsub_button: &gtk:
}
};
}
// content::update_podcasts(stack);
shows.switch_podcasts_animated();
shows.update_podcasts();
}
fn on_played_button_clicked(stack: &gtk::Stack, pd: &Podcast) {
fn on_played_button_clicked(shows: Rc<ShowStack>, pd: &Podcast) {
let _ = dbqueries::update_none_to_played_now(pd);
// content::update_widget_preserve_vis(stack, pd);
shows.update_widget();
}