From f47413686cf9d5dce5703e743b5e60de37525a42 Mon Sep 17 00:00:00 2001 From: Zander Brown Date: Sun, 30 Jun 2019 16:33:49 +0100 Subject: [PATCH] Use a custom GtkApplication instead of GtkApplication direct --- podcasts-gtk/src/app.rs | 560 +++++++++++++++---------------------- podcasts-gtk/src/main.rs | 5 +- podcasts-gtk/src/window.rs | 222 +++++++++++++++ 3 files changed, 452 insertions(+), 335 deletions(-) create mode 100644 podcasts-gtk/src/window.rs diff --git a/podcasts-gtk/src/app.rs b/podcasts-gtk/src/app.rs index 219686b..c985c3a 100644 --- a/podcasts-gtk/src/app.rs +++ b/podcasts-gtk/src/app.rs @@ -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(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>, + settings: RefCell>, +} + +impl ObjectSubclass for PdApplicationPrivate { + const NAME: &'static str = "PdApplication"; + type ParentType = gtk::Application; + type Instance = subclass::simple::InstanceStruct; + type Class = subclass::simple::ClassStruct; + + 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::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::().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::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::().expect("How?"); + app.setup_timed_callbacks(); + } +} + +impl gtk::subclass::application::GtkApplicationImpl for PdApplicationPrivate {} + +glib_wrapper! { + pub struct PdApplication(Object, subclass::simple::ClassStruct, 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, - headerbar: Rc
, - player: player::PlayerWrapper, - updater: RefCell>, - sender: Sender, - receiver: Receiver, -} +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::() + .expect("Congrats, you have won a prize for triggering an impossible outcome"); -impl App { - pub(crate) fn new(application: >k::Application) -> Rc { - 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) { - 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> = 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> = 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> = None; - utils::refresh(s, sender.clone()); - glib::Continue(false) - })); - })); - self.instance.set_accels_for_action("win.refresh", &["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", &["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 = 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 = 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 = 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 = 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::().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::().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::().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::().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")); diff --git a/podcasts-gtk/src/main.rs b/podcasts-gtk/src/main.rs index 628f112..3f45655 100644 --- a/podcasts-gtk/src/main.rs +++ b/podcasts-gtk/src/main.rs @@ -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] diff --git a/podcasts-gtk/src/window.rs b/podcasts-gtk/src/window.rs new file mode 100644 index 0000000..481464c --- /dev/null +++ b/podcasts-gtk/src/window.rs @@ -0,0 +1,222 @@ +// window.rs +// +// Copyright 2019 Jordan Petridis +// +// 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 . +// +// 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(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, + pub(crate) headerbar: Rc
, + pub(crate) player: player::PlayerWrapper, + pub(crate) updater: RefCell>, + pub(crate) sender: Sender, + pub(crate) receiver: Receiver, +} + +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::(); + 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> = 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> = 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> = None; + utils::refresh(s, sender.clone()); + glib::Continue(false) + })); + })); + self.app.set_accels_for_action("win.refresh", &["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", &["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 + } +}