Commit 666ab01d authored by Jordan Petridis's avatar Jordan Petridis 🌱

Merge branch 'appmenu' into 'master'

Appmenu

See merge request World/hammond!33
parents 74f8e744 ffbab013
Pipeline #13246 passed with stages
in 70 minutes and 23 seconds
......@@ -257,12 +257,12 @@ Tobias Bernard
</object>
</child>
<child>
<object class="GtkMenuButton" id="menu_toggle">
<object class="GtkMenuButton" id="menu_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="valign">center</property>
<property name="popover">menu_popover</property>
<property name="use_popover">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
......@@ -281,125 +281,4 @@ Tobias Bernard
</packing>
</child>
</object>
<object class="GtkPopover" id="menu_popover">
<property name="can_focus">False</property>
<property name="relative_to">menu_toggle</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<property name="margin_top">10</property>
<property name="margin_bottom">10</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkModelButton">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="text" translatable="yes">Preferences</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkModelButton" id="update_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="text" translatable="yes">Check for new episodes</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkModelButton" id="import">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="text" translatable="yes">Import Shows</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkModelButton" id="export">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="text" translatable="yes">Export Shows</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkSeparator">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkModelButton" id="about">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="text" translatable="yes">About</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">6</property>
</packing>
</child>
<child>
<object class="GtkModelButton">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="text" translatable="yes">Help</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">7</property>
</packing>
</child>
<child>
<object class="GtkModelButton">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="text" translatable="yes">Keyboard Shortcuts</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">8</property>
</packing>
</child>
</object>
</child>
</object>
</interface>
</interface>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<object class="GtkShortcutsWindow" id="help_overlay">
<property name="modal">True</property>
<child>
<object class="GtkShortcutsSection">
<property name="section-name">shortcuts</property>
<property name="max-height">12</property>
<property name="visible">True</property>
<child>
<object class="GtkShortcutsGroup">
<property name="title" translatable="yes">General</property>
<property name="visible">True</property>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">True</property>
<property name="accelerator">&lt;primary&gt;r</property>
<property name="title" translatable="yes" context="shortcut window">Check for new episodes</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">True</property>
<property name="accelerator">&lt;primary&gt;q</property>
<property name="title" translatable="yes" context="shortcut window">Quit the application</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</interface>
\ No newline at end of file
<?xml version="1.0"?>
<interface>
<!-- interface-requires gtk+ 3.0 -->
<menu id="menu">
<section>
<item>
<attribute name="label" translatable="yes">_Check for new episodes</attribute>
<attribute name="action">app.refresh</attribute>
<attribute name="accel">&lt;primary&gt;r</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Import Shows</attribute>
<attribute name="action">app.import</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Export Shows</attribute>
<attribute name="action">app.export</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">_Preferences</attribute>
<attribute name="action">app.preferences</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">_Keyboard Shortcuts</attribute>
<attribute name="action">win.show-help-overlay</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Help</attribute>
<attribute name="action">app.help</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_About</attribute>
<attribute name="action">app.about</attribute>
</item>
</section>
</menu>
</interface>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/org/gnome/hammond/">
<gresource prefix="/org/gnome/Hammond/">
<file compressed="true" preprocess="xml-stripblanks">gtk/episode_widget.ui</file>
<file compressed="true" preprocess="xml-stripblanks">gtk/show_widget.ui</file>
<file compressed="true" preprocess="xml-stripblanks">gtk/empty_view.ui</file>
......@@ -11,6 +11,8 @@
<file compressed="true" preprocess="xml-stripblanks">gtk/shows_child.ui</file>
<file compressed="true" preprocess="xml-stripblanks">gtk/headerbar.ui</file>
<file compressed="true" preprocess="xml-stripblanks">gtk/inapp_notif.ui</file>
<file compressed="true" preprocess="xml-stripblanks">gtk/menus.ui</file>
<file compressed="true" preprocess="xml-stripblanks">gtk/help-overlay.ui</file>
<file compressed="true">gtk/style.css</file>
</gresource>
</gresources>
#![allow(new_without_default)]
use gio::{ApplicationExt, ApplicationExtManual, ApplicationFlags, Settings, SettingsExt};
use gio::{
ActionMapExt, ApplicationExt, ApplicationExtManual, ApplicationFlags, Settings, SettingsExt,
SimpleAction, SimpleActionExt,
};
use glib;
use gtk;
use gtk::prelude::*;
......@@ -13,12 +16,14 @@ use headerbar::Header;
use settings::{self, WindowGeometry};
use stacks::{Content, PopulatedState};
use utils;
use widgets::{mark_all_notif, remove_show_notif};
use widgets::{about_dialog, mark_all_notif, remove_show_notif};
use std::rc::Rc;
use std::sync::mpsc::{channel, Receiver, Sender};
use std::sync::mpsc::{channel, Sender};
use std::sync::Arc;
use std::cell::RefCell;
#[derive(Debug, Clone)]
pub enum Action {
RefreshAllViews,
......@@ -41,12 +46,6 @@ pub enum Action {
#[derive(Debug)]
pub struct App {
app_instance: gtk::Application,
window: gtk::Window,
overlay: gtk::Overlay,
header: Rc<Header>,
content: Rc<Content>,
receiver: Receiver<Action>,
sender: Sender<Action>,
settings: Settings,
}
......@@ -63,62 +62,178 @@ impl App {
let cleanup_date = settings::get_cleanup_date(&settings);
utils::cleanup(cleanup_date);
// Create the main window
let window = gtk::Window::new(gtk::WindowType::Toplevel);
window.set_title("Hammond");
window.connect_delete_event(clone!(application, settings, window => move |_, _| {
WindowGeometry::from_window(&window).write(&settings);
application.quit();
Inhibit(false)
application.connect_startup(clone!(settings => move |app| {
let (sender, receiver) = channel();
let receiver = Rc::new(RefCell::new(receiver));
let refresh = SimpleAction::new("refresh", None);
refresh.connect_activate(clone!(sender => move |_, _| {
gtk::idle_add(clone!(sender => move || {
let s: Option<Vec<_>> = None;
utils::refresh(s, sender.clone());
glib::Continue(false)
}));
}));
app.add_action(&refresh);
app.set_accels_for_action("app.refresh", &["<primary>r"]);
let import = SimpleAction::new("import", None);
import.connect_activate(clone!(sender, app => move |_, _| {
let window = app.get_active_window().expect("Failed to get active window");
utils::on_import_clicked(&window, &sender);
}));
app.add_action(&import);
let about = SimpleAction::new("about", None);
about.connect_activate(clone!(app => move |_, _| {
let window = app.get_active_window().expect("Failed to get active window");
about_dialog(&window);
}));
app.add_action(&about);
let quit = SimpleAction::new("quit", None);
quit.connect_activate(clone!(app => move |_, _| app.quit()));
app.add_action(&quit);
app.set_accels_for_action("app.quit", &["<primary>q"]);
app.set_accels_for_action("win.menu", &["F10"]);
app.connect_activate(clone!(sender, settings, receiver => move |app| {
// Get the current window (if any)
if let Some(window) = app.get_active_window() {
// Already open, just raise the window
window.present();
} else {
// Time to open one!
// Create the main window
let window = gtk::ApplicationWindow::new(&app);
window.set_title("Hammond");
window.connect_delete_event(clone!(app, settings => move |window, _| {
WindowGeometry::from_window(&window).write(&settings);
app.quit();
Inhibit(false)
}));
// Create a content instance
let content =
Rc::new(Content::new(sender.clone()).expect(
"Content Initialization failed."));
// Create the headerbar
let header = Rc::new(Header::new(&content, &window, &sender));
let menu = SimpleAction::new("menu", None);
menu.connect_activate(clone!(header => move |_, _| {
header.open_menu();
}));
window.add_action(&menu);
// Add the content main stack to the overlay.
let overlay = gtk::Overlay::new();
overlay.add(&content.get_stack());
// Add the overlay to the main window
window.add(&overlay);
WindowGeometry::from_settings(&settings).apply(&window);
App::setup_timed_callbacks(&sender, &settings);
window.show_all();
window.activate();
gtk::timeout_add(50, clone!(sender, receiver => move || {
// Uses receiver, content, header, sender, overlay
let act = receiver.borrow().try_recv();
//let act: Result<Action, RecvError> = Ok(Action::RefreshAllViews);
match act {
Ok(Action::RefreshAllViews) => content.update(),
Ok(Action::RefreshShowsView) => content.update_shows_view(),
Ok(Action::RefreshWidgetIfSame(id)) =>
content.update_widget_if_same(id),
Ok(Action::RefreshEpisodesView) => content.update_home(),
Ok(Action::RefreshEpisodesViewBGR) =>
content.update_home_if_background(),
Ok(Action::ReplaceWidget(pd)) => {
let shows = content.get_shows();
let mut pop = shows.borrow().populated();
pop.borrow_mut()
.replace_widget(pd.clone())
.map_err(|err| error!("Failed to update ShowWidget: {}", err))
.map_err(|_|
error!("Failed ot update ShowWidget {}", pd.title()))
.ok();
}
Ok(Action::ShowWidgetAnimated) => {
let shows = content.get_shows();
let mut pop = shows.borrow().populated();
pop.borrow_mut().switch_visible(
PopulatedState::Widget,
gtk::StackTransitionType::SlideLeft,
);
}
Ok(Action::ShowShowsAnimated) => {
let shows = content.get_shows();
let mut pop = shows.borrow().populated();
pop.borrow_mut()
.switch_visible(PopulatedState::View,
gtk::StackTransitionType::SlideRight);
}
Ok(Action::HeaderBarShowTile(title)) =>
header.switch_to_back(&title),
Ok(Action::HeaderBarNormal) => header.switch_to_normal(),
Ok(Action::HeaderBarShowUpdateIndicator) =>
header.show_update_notification(),
Ok(Action::HeaderBarHideUpdateIndicator) =>
header.hide_update_notification(),
Ok(Action::MarkAllPlayerNotification(pd)) => {
let notif = mark_all_notif(pd, &sender);
notif.show(&overlay);
}
Ok(Action::RemoveShow(pd)) => {
let notif = remove_show_notif(pd, sender.clone());
notif.show(&overlay);
}
Ok(Action::ErrorNotification(err)) => {
error!("An error notification was triggered: {}", err);
let callback = || glib::Continue(false);
let notif = InAppNotification::new(&err, callback,
|| {}, UndoState::Hidden);
notif.show(&overlay);
}
Err(_) => (),
}
Continue(true)
}));
}
}));
}));
let (sender, receiver) = channel();
// Create a content instance
let content =
Rc::new(Content::new(sender.clone()).expect("Content Initialization failed."));
// Create the headerbar
let header = Rc::new(Header::new(&content, &window, &sender));
// Add the content main stack to the overlay.
let overlay = gtk::Overlay::new();
overlay.add(&content.get_stack());
// Add the overlay to the main window
window.add(&overlay);
App {
app_instance: application,
window,
overlay,
header,
content,
receiver,
sender,
settings,
}
}
fn setup_timed_callbacks(&self) {
self.setup_dark_theme();
self.setup_refresh_on_startup();
self.setup_auto_refresh();
fn setup_timed_callbacks(sender: &Sender<Action>, settings: &Settings) {
App::setup_dark_theme(&sender, settings);
App::setup_refresh_on_startup(&sender, settings);
App::setup_auto_refresh(&sender, settings);
}
fn setup_dark_theme(&self) {
let settings = gtk::Settings::get_default().unwrap();
let enabled = self.settings.get_boolean("dark-theme");
fn setup_dark_theme(_sender: &Sender<Action>, settings: &Settings) {
let gtk_settings = gtk::Settings::get_default().unwrap();
let enabled = settings.get_boolean("dark-theme");
settings.set_property_gtk_application_prefer_dark_theme(enabled);
gtk_settings.set_property_gtk_application_prefer_dark_theme(enabled);
}
fn setup_refresh_on_startup(&self) {
fn setup_refresh_on_startup(sender: &Sender<Action>, settings: &Settings) {
// Update the feeds right after the Application is initialized.
if self.settings.get_boolean("refresh-on-startup") {
let sender = self.sender.clone();
let sender = sender.clone();
if settings.get_boolean("refresh-on-startup") {
info!("Refresh on startup.");
// The ui loads async, after initialization
// so we need to delay this a bit so it won't block
......@@ -131,12 +246,12 @@ impl App {
}
}
fn setup_auto_refresh(&self) {
let refresh_interval = settings::get_refresh_interval(&self.settings).num_seconds() as u32;
let sender = self.sender.clone();
fn setup_auto_refresh(sender: &Sender<Action>, settings: &Settings) {
let refresh_interval = settings::get_refresh_interval(&settings).num_seconds() as u32;
info!("Auto-refresh every {:?} seconds.", refresh_interval);
let sender = sender.clone();
gtk::timeout_add_seconds(refresh_interval, move || {
let s: Option<Vec<_>> = None;
utils::refresh(s, sender.clone());
......@@ -146,81 +261,6 @@ impl App {
}
pub fn run(self) {
WindowGeometry::from_settings(&self.settings).apply(&self.window);
let window = self.window.clone();
self.app_instance.connect_startup(move |app| {
build_ui(&window, app);
});
self.setup_timed_callbacks();
let content = self.content;
let headerbar = self.header;
let sender = self.sender;
let overlay = self.overlay;
let receiver = self.receiver;
gtk::timeout_add(50, move || {
match receiver.try_recv() {
Ok(Action::RefreshAllViews) => content.update(),
Ok(Action::RefreshShowsView) => content.update_shows_view(),
Ok(Action::RefreshWidgetIfSame(id)) => content.update_widget_if_same(id),
Ok(Action::RefreshEpisodesView) => content.update_home(),
Ok(Action::RefreshEpisodesViewBGR) => content.update_home_if_background(),
Ok(Action::ReplaceWidget(pd)) => {
let shows = content.get_shows();
let mut pop = shows.borrow().populated();
pop.borrow_mut()
.replace_widget(pd.clone())
.map_err(|err| error!("Failed to update ShowWidget: {}", err))
.map_err(|_| error!("Failed ot update ShowWidget {}", pd.title()))
.ok();
}
Ok(Action::ShowWidgetAnimated) => {
let shows = content.get_shows();
let mut pop = shows.borrow().populated();
pop.borrow_mut().switch_visible(
PopulatedState::Widget,
gtk::StackTransitionType::SlideLeft,
);
}
Ok(Action::ShowShowsAnimated) => {
let shows = content.get_shows();
let mut pop = shows.borrow().populated();
pop.borrow_mut()
.switch_visible(PopulatedState::View, gtk::StackTransitionType::SlideRight);
}
Ok(Action::HeaderBarShowTile(title)) => headerbar.switch_to_back(&title),
Ok(Action::HeaderBarNormal) => headerbar.switch_to_normal(),
Ok(Action::HeaderBarShowUpdateIndicator) => headerbar.show_update_notification(),
Ok(Action::HeaderBarHideUpdateIndicator) => headerbar.hide_update_notification(),
Ok(Action::MarkAllPlayerNotification(pd)) => {
let notif = mark_all_notif(pd, &sender);
notif.show(&overlay);
}
Ok(Action::RemoveShow(pd)) => {
let notif = remove_show_notif(pd, sender.clone());
notif.show(&overlay);
}
Ok(Action::ErrorNotification(err)) => {
error!("An error notification was triggered: {}", err);
let callback = || glib::Continue(false);
let notif = InAppNotification::new(&err, callback, || {}, UndoState::Hidden);
notif.show(&overlay);
}
Err(_) => (),
}
Continue(true)
});
ApplicationExtManual::run(&self.app_instance, &[]);
}
}
fn build_ui(window: &gtk::Window, app: &gtk::Application) {
window.set_application(app);
window.show_all();
window.activate();
app.connect_activate(move |_| ());
}
......@@ -21,7 +21,7 @@ pub struct InAppNotification {
impl Default for InAppNotification {
fn default() -> Self {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/inapp_notif.ui");
let builder = gtk::Builder::new_from_resource("/org/gnome/Hammond/gtk/inapp_notif.ui");
let revealer: gtk::Revealer = builder.get_object("revealer").unwrap();
let text: gtk::Label = builder.get_object("text").unwrap();
......
use gio::MenuModel;
use glib;
use gtk;
use gtk::prelude::*;
use failure::Error;
use failure::ResultExt;
use rayon;
use url::Url;
use hammond_data::{dbqueries, opml, Source};
use hammond_data::{dbqueries, Source};
use std::sync::mpsc::Sender;
use app::Action;
use stacks::Content;
use utils::{self, itunes_to_rss, refresh};
use utils::{itunes_to_rss, refresh};
#[derive(Debug, Clone)]
// TODO: split this into smaller
......@@ -23,31 +23,28 @@ pub struct Header {
switch: gtk::StackSwitcher,
back: gtk::Button,
show_title: gtk::Label,
about: gtk::ModelButton,
import: gtk::ModelButton,
export: gtk::ModelButton,
update_button: gtk::ModelButton,
update_box: gtk::Box,
update_label: gtk::Label,
update_spinner: gtk::Spinner,
menu_button: gtk::MenuButton,
app_menu: MenuModel,
}
impl Default for Header {
fn default() -> Header {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/headerbar.ui");
let builder = gtk::Builder::new_from_resource("/org/gnome/Hammond/gtk/headerbar.ui");
let header = builder.get_object("headerbar").unwrap();
let add_toggle = builder.get_object("add_toggle").unwrap();
let switch = builder.get_object("switch").unwrap();
let back = builder.get_object("back").unwrap();
let show_title = builder.get_object("show_title").unwrap();
let import = builder.get_object("import").unwrap();
let export = builder.get_object("export").unwrap();
let update_button = builder.get_object("update_button").unwrap();
let update_box = builder.get_object("update_notification").unwrap();
let update_label = builder.get_object("update_label").unwrap();
let update_spinner = builder.get_object("update_spinner").unwrap();
let about = builder.get_object("about").unwrap();
let menu_button = builder.get_object("menu_button").unwrap();
let menus = gtk::Builder::new_from_resource("/org/gnome/Hammond/gtk/menus.ui");
let app_menu = menus.get_object("menu").unwrap();
Header {
container: header,
......@@ -55,27 +52,34 @@ impl Default for Header {
switch,
back,
show_title,
about,
import,
export,
update_button,
update_box,
update_label,
update_spinner,
menu_button,
app_menu,
}
}
}
// TODO: Refactor components into smaller state machines
impl Header {
pub fn new(content: &Content, window: &gtk::Window, sender: &Sender<Action>) -> Header {
pub fn new(
content: &Content,
window: &gtk::ApplicationWindow,
sender: &Sender<Action>,
) -> Header {
let h = Header::default();
h.init(content, window, &sender);
h
}
pub fn init(&self, content: &Content, window: &gtk::Window, sender: &Sender<Action>) {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/headerbar.ui");
pub fn init(
&self,
content: &Content,
window: &gtk::ApplicationWindow,
sender: &Sender<Action>,
) {
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();
......@@ -98,22 +102,6 @@ impl Header {
self.add_toggle.set_popover(&add_popover);
self.update_button
.connect_clicked(clone!(sender => move |_| {