Use a custom GtkApplication instead of GtkApplication direct
This commit is contained in:
parent
c8a194cf32
commit
f47413686c
@ -20,46 +20,110 @@
|
||||
|
||||
#![allow(new_without_default)]
|
||||
|
||||
use gio::{self, prelude::*, ActionMapExt, SettingsExt};
|
||||
use glib::{self, Variant};
|
||||
use glib::subclass;
|
||||
use glib::subclass::prelude::*;
|
||||
use glib::translate::*;
|
||||
use glib::{glib_object_impl, glib_object_subclass, glib_object_wrapper, glib_wrapper};
|
||||
|
||||
use gio::subclass::application::ApplicationImplExt;
|
||||
use gio::{self, prelude::*, ActionMapExt, ApplicationFlags, SettingsExt};
|
||||
|
||||
use gtk;
|
||||
use gtk::prelude::*;
|
||||
|
||||
use gettextrs::{bindtextdomain, setlocale, textdomain, LocaleCategory};
|
||||
|
||||
use crossbeam_channel::{unbounded, Receiver, Sender};
|
||||
use crossbeam_channel::Receiver;
|
||||
use fragile::Fragile;
|
||||
use podcasts_data::Show;
|
||||
|
||||
use crate::headerbar::Header;
|
||||
use crate::settings::{self, WindowGeometry};
|
||||
use crate::stacks::{Content, PopulatedState};
|
||||
use crate::settings;
|
||||
use crate::stacks::PopulatedState;
|
||||
use crate::utils;
|
||||
use crate::widgets::about_dialog;
|
||||
use crate::widgets::appnotif::{InAppNotification, SpinnerState, State};
|
||||
use crate::widgets::player;
|
||||
use crate::widgets::show_menu::{mark_all_notif, remove_show_notif, ShowMenu};
|
||||
use crate::window::MainWindow;
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::env;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config::{APP_ID, LOCALEDIR};
|
||||
use crate::i18n::i18n;
|
||||
|
||||
/// Creates an action named `name` in the action map `T with the handler `F`
|
||||
fn action<T, F>(thing: &T, name: &str, action: F)
|
||||
where
|
||||
T: ActionMapExt,
|
||||
for<'r, 's> F: Fn(&'r gio::SimpleAction, Option<&Variant>) + 'static,
|
||||
{
|
||||
// Create a stateless, parameterless action
|
||||
let act = gio::SimpleAction::new(name, None);
|
||||
// Connect the handler
|
||||
act.connect_activate(action);
|
||||
// Add it to the map
|
||||
thing.add_action(&act);
|
||||
pub struct PdApplicationPrivate {
|
||||
window: RefCell<Option<MainWindow>>,
|
||||
settings: RefCell<Option<gio::Settings>>,
|
||||
}
|
||||
|
||||
impl ObjectSubclass for PdApplicationPrivate {
|
||||
const NAME: &'static str = "PdApplication";
|
||||
type ParentType = gtk::Application;
|
||||
type Instance = subclass::simple::InstanceStruct<Self>;
|
||||
type Class = subclass::simple::ClassStruct<Self>;
|
||||
|
||||
glib_object_subclass!();
|
||||
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
window: RefCell::new(None),
|
||||
settings: RefCell::new(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for PdApplicationPrivate {
|
||||
glib_object_impl!();
|
||||
}
|
||||
|
||||
impl gio::subclass::prelude::ApplicationImpl for PdApplicationPrivate {
|
||||
fn activate(&self, app: &gio::Application) {
|
||||
debug!("GtkApplication<PdApplication>::activate");
|
||||
|
||||
if let Some(ref window) = *self.window.borrow() {
|
||||
// Ideally Gtk4/GtkBuilder make this irrelvent
|
||||
window.show_all();
|
||||
window.present();
|
||||
info!("Window presented");
|
||||
return;
|
||||
}
|
||||
|
||||
let app = app.clone().downcast::<PdApplication>().expect("How?");
|
||||
let window = MainWindow::new(&app);
|
||||
window.setup_gactions();
|
||||
window.show_all();
|
||||
window.present();
|
||||
self.window.replace(Some(window));
|
||||
// Setup the Action channel
|
||||
gtk::timeout_add(25, clone!(app => move || app.setup_action_channel()));
|
||||
}
|
||||
|
||||
fn startup(&self, app: &gio::Application) {
|
||||
debug!("GtkApplication<PdApplication>::startup");
|
||||
|
||||
self.parent_startup(app);
|
||||
|
||||
let settings = gio::Settings::new("org.gnome.Podcasts");
|
||||
|
||||
let cleanup_date = settings::get_cleanup_date(&settings);
|
||||
// Garbage collect watched episodes from the disk
|
||||
utils::cleanup(cleanup_date);
|
||||
|
||||
self.settings.replace(Some(settings));
|
||||
|
||||
let app = app.clone().downcast::<PdApplication>().expect("How?");
|
||||
app.setup_timed_callbacks();
|
||||
}
|
||||
}
|
||||
|
||||
impl gtk::subclass::application::GtkApplicationImpl for PdApplicationPrivate {}
|
||||
|
||||
glib_wrapper! {
|
||||
pub struct PdApplication(Object<subclass::simple::InstanceStruct<PdApplicationPrivate>, subclass::simple::ClassStruct<PdApplicationPrivate>, PdApplicationClass>) @extends gio::Application, gtk::Application;
|
||||
|
||||
match fn {
|
||||
get_type => || PdApplicationPrivate::get_type().to_glib(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@ -85,312 +149,168 @@ pub(crate) enum Action {
|
||||
RaiseWindow,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct App {
|
||||
instance: gtk::Application,
|
||||
window: gtk::ApplicationWindow,
|
||||
overlay: gtk::Overlay,
|
||||
settings: gio::Settings,
|
||||
content: Rc<Content>,
|
||||
headerbar: Rc<Header>,
|
||||
player: player::PlayerWrapper,
|
||||
updater: RefCell<Option<InAppNotification>>,
|
||||
sender: Sender<Action>,
|
||||
receiver: Receiver<Action>,
|
||||
}
|
||||
impl PdApplication {
|
||||
pub(crate) fn new() -> Self {
|
||||
let application = glib::Object::new(
|
||||
PdApplication::static_type(),
|
||||
&[
|
||||
("application-id", &Some(APP_ID)),
|
||||
("flags", &ApplicationFlags::empty()),
|
||||
],
|
||||
)
|
||||
.expect("Application initialization failed...")
|
||||
.downcast::<PdApplication>()
|
||||
.expect("Congrats, you have won a prize for triggering an impossible outcome");
|
||||
|
||||
impl App {
|
||||
pub(crate) fn new(application: >k::Application) -> Rc<Self> {
|
||||
let settings = gio::Settings::new("org.gnome.Podcasts");
|
||||
application.set_resource_base_path(Some("/org/gnome/Podcasts"));
|
||||
|
||||
let (sender, receiver) = unbounded();
|
||||
|
||||
let window = gtk::ApplicationWindow::new(application);
|
||||
window.set_title(&i18n("Podcasts"));
|
||||
if APP_ID.ends_with("Devel") {
|
||||
window.get_style_context().add_class("devel");
|
||||
}
|
||||
|
||||
let weak_s = settings.downgrade();
|
||||
let weak_app = application.downgrade();
|
||||
window.connect_delete_event(move |window, _| {
|
||||
let app = match weak_app.upgrade() {
|
||||
Some(a) => a,
|
||||
None => return Inhibit(false),
|
||||
};
|
||||
|
||||
let settings = match weak_s.upgrade() {
|
||||
Some(s) => s,
|
||||
None => return Inhibit(false),
|
||||
};
|
||||
|
||||
info!("Saving window position");
|
||||
WindowGeometry::from_window(&window).write(&settings);
|
||||
|
||||
info!("Application is exiting");
|
||||
app.quit();
|
||||
Inhibit(false)
|
||||
});
|
||||
|
||||
// Create a content instance
|
||||
let content = Content::new(&sender).expect("Content initialization failed.");
|
||||
|
||||
// Create the headerbar
|
||||
let header = Header::new(&content, &sender);
|
||||
// Add the Headerbar to the window.
|
||||
window.set_titlebar(Some(&header.container));
|
||||
|
||||
// Add the content main stack to the overlay.
|
||||
let overlay = gtk::Overlay::new();
|
||||
overlay.add(&content.get_stack());
|
||||
|
||||
let wrap = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
||||
// Add the overlay to the main Box
|
||||
wrap.add(&overlay);
|
||||
|
||||
let player = player::PlayerWrapper::new(&sender);
|
||||
// Add the player to the main Box
|
||||
wrap.add(&player.action_bar);
|
||||
|
||||
let updater = RefCell::new(None);
|
||||
|
||||
window.add(&wrap);
|
||||
|
||||
let app = App {
|
||||
instance: application.clone(),
|
||||
window,
|
||||
settings,
|
||||
overlay,
|
||||
headerbar: header,
|
||||
content,
|
||||
player,
|
||||
updater,
|
||||
sender,
|
||||
receiver,
|
||||
};
|
||||
|
||||
Rc::new(app)
|
||||
}
|
||||
|
||||
fn init(app: &Rc<Self>) {
|
||||
let cleanup_date = settings::get_cleanup_date(&app.settings);
|
||||
// Garbage collect watched episodes from the disk
|
||||
utils::cleanup(cleanup_date);
|
||||
|
||||
app.setup_gactions();
|
||||
app.setup_timed_callbacks();
|
||||
|
||||
// Retrieve the previous window position and size.
|
||||
WindowGeometry::from_settings(&app.settings).apply(&app.window);
|
||||
|
||||
// Setup the Action channel
|
||||
gtk::timeout_add(25, clone!(app => move || app.setup_action_channel()));
|
||||
application
|
||||
}
|
||||
|
||||
fn setup_timed_callbacks(&self) {
|
||||
self.setup_dark_theme();
|
||||
self.setup_refresh_on_startup();
|
||||
self.setup_auto_refresh();
|
||||
}
|
||||
|
||||
fn setup_dark_theme(&self) {
|
||||
let gtk_settings = gtk::Settings::get_default().unwrap();
|
||||
self.settings.bind(
|
||||
"dark-theme",
|
||||
>k_settings,
|
||||
"gtk-application-prefer-dark-theme",
|
||||
gio::SettingsBindFlags::DEFAULT,
|
||||
);
|
||||
}
|
||||
|
||||
fn setup_refresh_on_startup(&self) {
|
||||
// Update the feeds right after the Application is initialized.
|
||||
let sender = self.sender.clone();
|
||||
if self.settings.get_boolean("refresh-on-startup") {
|
||||
info!("Refresh on startup.");
|
||||
let s: Option<Vec<_>> = None;
|
||||
utils::refresh(s, sender.clone());
|
||||
let data = PdApplicationPrivate::from_instance(self);
|
||||
if let Some(ref settings) = *data.settings.borrow() {
|
||||
let gtk_settings = gtk::Settings::get_default().unwrap();
|
||||
settings.bind(
|
||||
"dark-theme",
|
||||
>k_settings,
|
||||
"gtk-application-prefer-dark-theme",
|
||||
gio::SettingsBindFlags::DEFAULT,
|
||||
);
|
||||
} else {
|
||||
debug_assert!(false, "Well how'd you manage that?");
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_auto_refresh(&self) {
|
||||
let refresh_interval = settings::get_refresh_interval(&self.settings).num_seconds() as u32;
|
||||
info!("Auto-refresh every {:?} seconds.", refresh_interval);
|
||||
|
||||
let sender = self.sender.clone();
|
||||
gtk::timeout_add_seconds(refresh_interval, move || {
|
||||
let s: Option<Vec<_>> = None;
|
||||
utils::refresh(s, sender.clone());
|
||||
|
||||
glib::Continue(true)
|
||||
});
|
||||
}
|
||||
|
||||
/// Define the `GAction`s.
|
||||
///
|
||||
/// Used in menus and the keyboard shortcuts dialog.
|
||||
#[cfg_attr(rustfmt, rustfmt_skip)]
|
||||
fn setup_gactions(&self) {
|
||||
let sender = &self.sender;
|
||||
let weak_win = self.window.downgrade();
|
||||
|
||||
// Create the `refresh` action.
|
||||
//
|
||||
// This will trigger a refresh of all the shows in the database.
|
||||
action(&self.window, "refresh", clone!(sender => move |_, _| {
|
||||
gtk::idle_add(clone!(sender => move || {
|
||||
let s: Option<Vec<_>> = None;
|
||||
utils::refresh(s, sender.clone());
|
||||
glib::Continue(false)
|
||||
}));
|
||||
}));
|
||||
self.instance.set_accels_for_action("win.refresh", &["<primary>r"]);
|
||||
|
||||
// Create the `OPML` import action
|
||||
action(&self.window, "import", clone!(sender, weak_win => move |_, _| {
|
||||
weak_win.upgrade().map(|win| utils::on_import_clicked(&win, &sender));
|
||||
}));
|
||||
|
||||
action(&self.window, "export", clone!(sender, weak_win => move |_, _| {
|
||||
weak_win.upgrade().map(|win| utils::on_export_clicked(&win, &sender));
|
||||
}));
|
||||
|
||||
// Create the action that shows a `gtk::AboutDialog`
|
||||
action(&self.window, "about", clone!(weak_win => move |_, _| {
|
||||
weak_win.upgrade().map(|win| about_dialog(&win));
|
||||
}));
|
||||
|
||||
// Create the quit action
|
||||
let weak_instance = self.instance.downgrade();
|
||||
action(&self.window, "quit", move |_, _| {
|
||||
weak_instance.upgrade().map(|app| app.quit());
|
||||
});
|
||||
self.instance.set_accels_for_action("win.quit", &["<primary>q"]);
|
||||
|
||||
// Create the menu action
|
||||
let header = Rc::downgrade(&self.headerbar);
|
||||
action(&self.window, "menu", move |_, _| {
|
||||
header.upgrade().map(|h| h.open_menu());
|
||||
});
|
||||
// Bind the hamburger menu button to `F10`
|
||||
self.instance.set_accels_for_action("win.menu", &["F10"]);
|
||||
}
|
||||
|
||||
fn setup_action_channel(&self) -> glib::Continue {
|
||||
use crossbeam_channel::TryRecvError;
|
||||
let data = PdApplicationPrivate::from_instance(self);
|
||||
|
||||
let action = match self.receiver.try_recv() {
|
||||
Ok(a) => a,
|
||||
Err(TryRecvError::Empty) => return glib::Continue(true),
|
||||
Err(TryRecvError::Disconnected) => {
|
||||
unreachable!("How the hell was the action channel dropped.")
|
||||
}
|
||||
};
|
||||
if let Some(ref window) = *data.window.borrow() {
|
||||
let action = match window.receiver.try_recv() {
|
||||
Ok(a) => a,
|
||||
Err(TryRecvError::Empty) => return glib::Continue(true),
|
||||
Err(TryRecvError::Disconnected) => {
|
||||
unreachable!("How the hell was the action channel dropped.")
|
||||
}
|
||||
};
|
||||
|
||||
trace!("Incoming channel action: {:?}", action);
|
||||
match action {
|
||||
Action::RefreshAllViews => self.content.update(),
|
||||
Action::RefreshShowsView => self.content.update_shows_view(),
|
||||
Action::RefreshWidgetIfSame(id) => self.content.update_widget_if_same(id),
|
||||
Action::RefreshEpisodesView => self.content.update_home(),
|
||||
Action::RefreshEpisodesViewBGR => self.content.update_home_if_background(),
|
||||
Action::ReplaceWidget(pd) => {
|
||||
let shows = self.content.get_shows();
|
||||
let pop = shows.borrow().populated();
|
||||
pop.borrow_mut()
|
||||
.replace_widget(pd.clone())
|
||||
.map_err(|err| error!("Failed to update ShowWidget: {}", err))
|
||||
.map_err(|_| error!("Failed to update ShowWidget {}", pd.title()))
|
||||
.ok();
|
||||
}
|
||||
Action::ShowWidgetAnimated => {
|
||||
let shows = self.content.get_shows();
|
||||
let pop = shows.borrow().populated();
|
||||
pop.borrow_mut()
|
||||
.switch_visible(PopulatedState::Widget, gtk::StackTransitionType::SlideLeft);
|
||||
}
|
||||
Action::ShowShowsAnimated => {
|
||||
let shows = self.content.get_shows();
|
||||
let pop = shows.borrow().populated();
|
||||
pop.borrow_mut()
|
||||
.switch_visible(PopulatedState::View, gtk::StackTransitionType::SlideRight);
|
||||
}
|
||||
Action::HeaderBarShowTile(title) => self.headerbar.switch_to_back(&title),
|
||||
Action::HeaderBarNormal => self.headerbar.switch_to_normal(),
|
||||
Action::MarkAllPlayerNotification(pd) => {
|
||||
let notif = mark_all_notif(pd, &self.sender);
|
||||
notif.show(&self.overlay);
|
||||
}
|
||||
Action::RemoveShow(pd) => {
|
||||
let notif = remove_show_notif(pd, self.sender.clone());
|
||||
notif.show(&self.overlay);
|
||||
}
|
||||
Action::ErrorNotification(err) => {
|
||||
error!("An error notification was triggered: {}", err);
|
||||
let callback = |revealer: gtk::Revealer| {
|
||||
revealer.set_reveal_child(false);
|
||||
glib::Continue(false)
|
||||
};
|
||||
let undo_cb: Option<fn()> = None;
|
||||
let notif = InAppNotification::new(&err, 6000, callback, undo_cb);
|
||||
notif.show(&self.overlay);
|
||||
}
|
||||
Action::ShowUpdateNotif(receiver) => {
|
||||
let sender = self.sender.clone();
|
||||
let callback = move |revealer: gtk::Revealer| match receiver.try_recv() {
|
||||
Err(TryRecvError::Empty) => glib::Continue(true),
|
||||
Err(TryRecvError::Disconnected) => glib::Continue(false),
|
||||
Ok(_) => {
|
||||
trace!("Incoming channel action: {:?}", action);
|
||||
match action {
|
||||
Action::RefreshAllViews => window.content.update(),
|
||||
Action::RefreshShowsView => window.content.update_shows_view(),
|
||||
Action::RefreshWidgetIfSame(id) => window.content.update_widget_if_same(id),
|
||||
Action::RefreshEpisodesView => window.content.update_home(),
|
||||
Action::RefreshEpisodesViewBGR => window.content.update_home_if_background(),
|
||||
Action::ReplaceWidget(pd) => {
|
||||
let shows = window.content.get_shows();
|
||||
let pop = shows.borrow().populated();
|
||||
pop.borrow_mut()
|
||||
.replace_widget(pd.clone())
|
||||
.map_err(|err| error!("Failed to update ShowWidget: {}", err))
|
||||
.map_err(|_| error!("Failed to update ShowWidget {}", pd.title()))
|
||||
.ok();
|
||||
}
|
||||
Action::ShowWidgetAnimated => {
|
||||
let shows = window.content.get_shows();
|
||||
let pop = shows.borrow().populated();
|
||||
pop.borrow_mut().switch_visible(
|
||||
PopulatedState::Widget,
|
||||
gtk::StackTransitionType::SlideLeft,
|
||||
);
|
||||
}
|
||||
Action::ShowShowsAnimated => {
|
||||
let shows = window.content.get_shows();
|
||||
let pop = shows.borrow().populated();
|
||||
pop.borrow_mut()
|
||||
.switch_visible(PopulatedState::View, gtk::StackTransitionType::SlideRight);
|
||||
}
|
||||
Action::HeaderBarShowTile(title) => window.headerbar.switch_to_back(&title),
|
||||
Action::HeaderBarNormal => window.headerbar.switch_to_normal(),
|
||||
Action::MarkAllPlayerNotification(pd) => {
|
||||
let notif = mark_all_notif(pd, &window.sender);
|
||||
notif.show(&window.overlay);
|
||||
}
|
||||
Action::RemoveShow(pd) => {
|
||||
let notif = remove_show_notif(pd, window.sender.clone());
|
||||
notif.show(&window.overlay);
|
||||
}
|
||||
Action::ErrorNotification(err) => {
|
||||
error!("An error notification was triggered: {}", err);
|
||||
let callback = |revealer: gtk::Revealer| {
|
||||
revealer.set_reveal_child(false);
|
||||
sender
|
||||
.send(Action::RefreshAllViews)
|
||||
.expect("Action channel blew up somehow");
|
||||
glib::Continue(false)
|
||||
}
|
||||
};
|
||||
let txt = i18n("Fetching new episodes");
|
||||
let undo_cb: Option<fn()> = None;
|
||||
let updater = InAppNotification::new(&txt, 250, callback, undo_cb);
|
||||
updater.set_close_state(State::Hidden);
|
||||
updater.set_spinner_state(SpinnerState::Active);
|
||||
};
|
||||
let undo_cb: Option<fn()> = None;
|
||||
let notif = InAppNotification::new(&err, 6000, callback, undo_cb);
|
||||
notif.show(&window.overlay);
|
||||
}
|
||||
Action::ShowUpdateNotif(receiver) => {
|
||||
let sender = window.sender.clone();
|
||||
let callback = move |revealer: gtk::Revealer| match receiver.try_recv() {
|
||||
Err(TryRecvError::Empty) => glib::Continue(true),
|
||||
Err(TryRecvError::Disconnected) => glib::Continue(false),
|
||||
Ok(_) => {
|
||||
revealer.set_reveal_child(false);
|
||||
sender
|
||||
.send(Action::RefreshAllViews)
|
||||
.expect("Action channel blew up somehow");
|
||||
glib::Continue(false)
|
||||
}
|
||||
};
|
||||
let txt = i18n("Fetching new episodes");
|
||||
let undo_cb: Option<fn()> = None;
|
||||
let updater = InAppNotification::new(&txt, 250, callback, undo_cb);
|
||||
updater.set_close_state(State::Hidden);
|
||||
updater.set_spinner_state(SpinnerState::Active);
|
||||
|
||||
let old = self.updater.replace(Some(updater));
|
||||
old.map(|i| i.destroy());
|
||||
self.updater
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.map(|i| i.show(&self.overlay));
|
||||
}
|
||||
Action::InitEpisode(rowid) => {
|
||||
let res = self.player.initialize_episode(rowid);
|
||||
debug_assert!(res.is_ok());
|
||||
}
|
||||
Action::InitShowMenu(s) => {
|
||||
let menu = &s.get().container;
|
||||
self.headerbar.set_secondary_menu(menu);
|
||||
}
|
||||
Action::EmptyState => {
|
||||
self.window
|
||||
.lookup_action("refresh")
|
||||
.and_then(|action| action.downcast::<gio::SimpleAction>().ok())
|
||||
// Disable refresh action
|
||||
.map(|action| action.set_enabled(false));
|
||||
let old = window.updater.replace(Some(updater));
|
||||
old.map(|i| i.destroy());
|
||||
window
|
||||
.updater
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.map(|i| i.show(&window.overlay));
|
||||
}
|
||||
Action::InitEpisode(rowid) => {
|
||||
let res = window.player.initialize_episode(rowid);
|
||||
debug_assert!(res.is_ok());
|
||||
}
|
||||
Action::InitShowMenu(s) => {
|
||||
let menu = &s.get().container;
|
||||
window.headerbar.set_secondary_menu(menu);
|
||||
}
|
||||
Action::EmptyState => {
|
||||
window
|
||||
.window
|
||||
.lookup_action("refresh")
|
||||
.and_then(|action| action.downcast::<gio::SimpleAction>().ok())
|
||||
// Disable refresh action
|
||||
.map(|action| action.set_enabled(false));
|
||||
|
||||
self.headerbar.switch.set_sensitive(false);
|
||||
self.content.switch_to_empty_views();
|
||||
}
|
||||
Action::PopulatedState => {
|
||||
self.window
|
||||
.lookup_action("refresh")
|
||||
.and_then(|action| action.downcast::<gio::SimpleAction>().ok())
|
||||
// Enable refresh action
|
||||
.map(|action| action.set_enabled(true));
|
||||
window.headerbar.switch.set_sensitive(false);
|
||||
window.content.switch_to_empty_views();
|
||||
}
|
||||
Action::PopulatedState => {
|
||||
window
|
||||
.window
|
||||
.lookup_action("refresh")
|
||||
.and_then(|action| action.downcast::<gio::SimpleAction>().ok())
|
||||
// Enable refresh action
|
||||
.map(|action| action.set_enabled(true));
|
||||
|
||||
self.headerbar.switch.set_sensitive(true);
|
||||
self.content.switch_to_populated();
|
||||
}
|
||||
Action::RaiseWindow => self.window.present(),
|
||||
};
|
||||
window.headerbar.switch.set_sensitive(true);
|
||||
window.content.switch_to_populated();
|
||||
}
|
||||
Action::RaiseWindow => window.window.present(),
|
||||
};
|
||||
} else {
|
||||
debug_assert!(false, "Huh that's odd then");
|
||||
}
|
||||
|
||||
glib::Continue(true)
|
||||
}
|
||||
@ -401,33 +321,7 @@ impl App {
|
||||
bindtextdomain("gnome-podcasts", LOCALEDIR);
|
||||
textdomain("gnome-podcasts");
|
||||
|
||||
let application = gtk::Application::new(Some(APP_ID), gio::ApplicationFlags::empty())
|
||||
.expect("Application initialization failed...");
|
||||
application.set_resource_base_path(Some("/org/gnome/Podcasts"));
|
||||
|
||||
let weak_app = application.downgrade();
|
||||
application.connect_startup(move |_| {
|
||||
info!("GApplication::startup");
|
||||
weak_app.upgrade().map(|application| {
|
||||
let app = Self::new(&application);
|
||||
Self::init(&app);
|
||||
|
||||
let weak = Rc::downgrade(&app);
|
||||
application.connect_activate(move |_| {
|
||||
info!("GApplication::activate");
|
||||
if let Some(app) = weak.upgrade() {
|
||||
// Ideally Gtk4/GtkBuilder make this irrelvent
|
||||
app.window.show_all();
|
||||
app.window.present();
|
||||
info!("Window presented");
|
||||
} else {
|
||||
debug_assert!(false, "I hate computers");
|
||||
}
|
||||
});
|
||||
|
||||
info!("Init complete");
|
||||
});
|
||||
});
|
||||
let application = Self::new();
|
||||
|
||||
// Weird magic I copy-pasted that sets the Application Name in the Shell.
|
||||
glib::set_application_name(&i18n("Podcasts"));
|
||||
|
||||
@ -85,6 +85,7 @@ mod widgets;
|
||||
mod app;
|
||||
mod config;
|
||||
mod headerbar;
|
||||
mod window;
|
||||
|
||||
mod manager;
|
||||
mod settings;
|
||||
@ -93,7 +94,7 @@ mod utils;
|
||||
|
||||
mod i18n;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::app::PdApplication;
|
||||
|
||||
#[cfg(test)]
|
||||
fn init_gtk_tests() -> Result<(), failure::Error> {
|
||||
@ -126,7 +127,7 @@ fn main() {
|
||||
600,
|
||||
);
|
||||
|
||||
App::run();
|
||||
PdApplication::run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
222
podcasts-gtk/src/window.rs
Normal file
222
podcasts-gtk/src/window.rs
Normal file
@ -0,0 +1,222 @@
|
||||
// window.rs
|
||||
//
|
||||
// Copyright 2019 Jordan Petridis <jpetridis@gnome.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
use glib;
|
||||
use glib::Variant;
|
||||
|
||||
use gio::{self, prelude::*, ActionMapExt, SettingsExt};
|
||||
|
||||
use gtk;
|
||||
use gtk::prelude::*;
|
||||
|
||||
use crossbeam_channel::{unbounded, Receiver, Sender};
|
||||
|
||||
use crate::app::{Action, PdApplication};
|
||||
use crate::headerbar::Header;
|
||||
use crate::settings::{self, WindowGeometry};
|
||||
use crate::stacks::Content;
|
||||
use crate::utils;
|
||||
use crate::widgets::about_dialog;
|
||||
use crate::widgets::appnotif::InAppNotification;
|
||||
use crate::widgets::player;
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::ops::Deref;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::config::APP_ID;
|
||||
use crate::i18n::i18n;
|
||||
|
||||
/// Creates an action named `name` in the action map `T with the handler `F`
|
||||
fn action<T, F>(thing: &T, name: &str, action: F)
|
||||
where
|
||||
T: ActionMapExt,
|
||||
for<'r, 's> F: Fn(&'r gio::SimpleAction, Option<&Variant>) + 'static,
|
||||
{
|
||||
// Create a stateless, parameterless action
|
||||
let act = gio::SimpleAction::new(name, None);
|
||||
// Connect the handler
|
||||
act.connect_activate(action);
|
||||
// Add it to the map
|
||||
thing.add_action(&act);
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MainWindow {
|
||||
app: PdApplication,
|
||||
pub(crate) window: gtk::ApplicationWindow,
|
||||
pub(crate) overlay: gtk::Overlay,
|
||||
pub(crate) content: Rc<Content>,
|
||||
pub(crate) headerbar: Rc<Header>,
|
||||
pub(crate) player: player::PlayerWrapper,
|
||||
pub(crate) updater: RefCell<Option<InAppNotification>>,
|
||||
pub(crate) sender: Sender<Action>,
|
||||
pub(crate) receiver: Receiver<Action>,
|
||||
}
|
||||
|
||||
impl MainWindow {
|
||||
pub fn new(app: &PdApplication) -> Self {
|
||||
let settings = gio::Settings::new("org.gnome.Podcasts");
|
||||
|
||||
let (sender, receiver) = unbounded();
|
||||
|
||||
let window = gtk::ApplicationWindow::new(app);
|
||||
window.set_title(&i18n("Podcasts"));
|
||||
if APP_ID.ends_with("Devel") {
|
||||
window.get_style_context().add_class("devel");
|
||||
}
|
||||
|
||||
let weak_s = settings.downgrade();
|
||||
let weak_app = app.downgrade();
|
||||
window.connect_delete_event(move |window, _| {
|
||||
let app = match weak_app.upgrade() {
|
||||
Some(a) => a,
|
||||
None => return Inhibit(false),
|
||||
};
|
||||
|
||||
let settings = match weak_s.upgrade() {
|
||||
Some(s) => s,
|
||||
None => return Inhibit(false),
|
||||
};
|
||||
|
||||
info!("Saving window position");
|
||||
WindowGeometry::from_window(&window).write(&settings);
|
||||
|
||||
info!("Application is exiting");
|
||||
let app = app.clone().upcast::<gio::Application>();
|
||||
app.quit();
|
||||
Inhibit(false)
|
||||
});
|
||||
|
||||
// Create a content instance
|
||||
let content = Content::new(&sender).expect("Content initialization failed.");
|
||||
|
||||
// Create the headerbar
|
||||
let header = Header::new(&content, &sender);
|
||||
// Add the Headerbar to the window.
|
||||
window.set_titlebar(Some(&header.container));
|
||||
|
||||
// Add the content main stack to the overlay.
|
||||
let overlay = gtk::Overlay::new();
|
||||
overlay.add(&content.get_stack());
|
||||
|
||||
let wrap = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
||||
// Add the overlay to the main Box
|
||||
wrap.add(&overlay);
|
||||
|
||||
let player = player::PlayerWrapper::new(&sender);
|
||||
// Add the player to the main Box
|
||||
wrap.add(&player.action_bar);
|
||||
|
||||
let updater = RefCell::new(None);
|
||||
|
||||
window.add(&wrap);
|
||||
|
||||
// Retrieve the previous window position and size.
|
||||
WindowGeometry::from_settings(&settings).apply(&window);
|
||||
|
||||
// Update the feeds right after the Window is initialized.
|
||||
if settings.get_boolean("refresh-on-startup") {
|
||||
info!("Refresh on startup.");
|
||||
let s: Option<Vec<_>> = None;
|
||||
utils::refresh(s, sender.clone());
|
||||
}
|
||||
|
||||
let refresh_interval = settings::get_refresh_interval(&settings).num_seconds() as u32;
|
||||
info!("Auto-refresh every {:?} seconds.", refresh_interval);
|
||||
|
||||
let r_sender = sender.clone();
|
||||
gtk::timeout_add_seconds(refresh_interval, move || {
|
||||
let s: Option<Vec<_>> = None;
|
||||
utils::refresh(s, r_sender.clone());
|
||||
|
||||
glib::Continue(true)
|
||||
});
|
||||
|
||||
Self {
|
||||
app: app.clone(),
|
||||
window,
|
||||
overlay,
|
||||
headerbar: header,
|
||||
content,
|
||||
player,
|
||||
updater,
|
||||
sender,
|
||||
receiver,
|
||||
}
|
||||
}
|
||||
|
||||
/// Define the `GAction`s.
|
||||
///
|
||||
/// Used in menus and the keyboard shortcuts dialog.
|
||||
#[cfg_attr(rustfmt, rustfmt_skip)]
|
||||
pub fn setup_gactions(&self) {
|
||||
let sender = &self.sender;
|
||||
let weak_win = self.window.downgrade();
|
||||
|
||||
// Create the `refresh` action.
|
||||
//
|
||||
// This will trigger a refresh of all the shows in the database.
|
||||
action(&self.window, "refresh", clone!(sender => move |_, _| {
|
||||
gtk::idle_add(clone!(sender => move || {
|
||||
let s: Option<Vec<_>> = None;
|
||||
utils::refresh(s, sender.clone());
|
||||
glib::Continue(false)
|
||||
}));
|
||||
}));
|
||||
self.app.set_accels_for_action("win.refresh", &["<primary>r"]);
|
||||
|
||||
// Create the `OPML` import action
|
||||
action(&self.window, "import", clone!(sender, weak_win => move |_, _| {
|
||||
weak_win.upgrade().map(|win| utils::on_import_clicked(&win, &sender));
|
||||
}));
|
||||
|
||||
action(&self.window, "export", clone!(sender, weak_win => move |_, _| {
|
||||
weak_win.upgrade().map(|win| utils::on_export_clicked(&win, &sender));
|
||||
}));
|
||||
|
||||
// Create the action that shows a `gtk::AboutDialog`
|
||||
action(&self.window, "about", clone!(weak_win => move |_, _| {
|
||||
weak_win.upgrade().map(|win| about_dialog(&win));
|
||||
}));
|
||||
|
||||
// Create the quit action
|
||||
let weak_instance = self.app.downgrade();
|
||||
action(&self.window, "quit", move |_, _| {
|
||||
weak_instance.upgrade().map(|app| app.quit());
|
||||
});
|
||||
self.app.set_accels_for_action("win.quit", &["<primary>q"]);
|
||||
|
||||
// Create the menu action
|
||||
let header = Rc::downgrade(&self.headerbar);
|
||||
action(&self.window, "menu", move |_, _| {
|
||||
header.upgrade().map(|h| h.open_menu());
|
||||
});
|
||||
// Bind the hamburger menu button to `F10`
|
||||
self.app.set_accels_for_action("win.menu", &["F10"]);
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for MainWindow {
|
||||
type Target = gtk::ApplicationWindow;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.window
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user