Use a custom GtkApplication instead of GtkApplication direct

This commit is contained in:
Zander Brown 2019-06-30 16:33:49 +01:00 committed by Jordan Petridis
parent c8a194cf32
commit f47413686c
No known key found for this signature in database
GPG Key ID: E8523968931763BE
3 changed files with 452 additions and 335 deletions

View File

@ -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: &gtk::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",
&gtk_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",
&gtk_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"));

View File

@ -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
View 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
}
}