diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..ad1ff17 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,238 @@ +// app.rs +// +// Copyright 2021 Stéphane Cerveau +// +// 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-only +use gtk::cairo::Context; +use gtk::glib; +use gtk::prelude::*; +use gtk::{ + AboutDialog, AccelFlags, AccelGroup, ApplicationWindow, Builder, Button, DrawingArea, EventBox, + FileChooserDialog, MenuItem, ResponseType, Viewport, WindowPosition, +}; +use std::cell::RefCell; +use std::rc::{Rc, Weak}; +use std::{error, ops}; + +use crate::pipeline::Pipeline; +use crate::pluginlistwindow; + +#[derive(Debug)] +pub struct GPSAppInner { + pub window: gtk::ApplicationWindow, + pub builder: Builder, + pipeline: Pipeline, +} + +// This represents our main application window. +#[derive(Debug, Clone)] +pub struct GPSApp(Rc); + +// Deref into the contained struct to make usage a bit more ergonomic +impl ops::Deref for GPSApp { + type Target = GPSAppInner; + + fn deref(&self) -> &GPSAppInner { + &*self.0 + } +} + +// Weak reference to our application struct +// +// Weak references are important to prevent reference cycles. Reference cycles are cases where +// struct A references directly or indirectly struct B, and struct B references struct A again +// while both are using reference counting. +pub struct GPSAppWeak(Weak); +impl GPSAppWeak { + // Upgrade to a strong reference if it still exists + pub fn upgrade(&self) -> Option { + self.0.upgrade().map(GPSApp) + } +} + +#[derive(Debug, Clone, Default)] +struct Element { + name: String, + position: (f64, f64), + size: (f64, f64), +} +fn draw_elements(elements: &Vec, c: &Context) { + for element in elements { + c.rectangle(element.position.0, element.position.1, 80.0, 45.0); + c.fill().expect("Can not draw into context"); + } +} + +impl GPSApp { + fn new(application: >k::Application) -> anyhow::Result> { + let glade_src = include_str!("gps.ui"); + let builder = Builder::from_string(glade_src); + let window: ApplicationWindow = builder.object("mainwindow").expect("Couldn't get window"); + window.set_application(Some(application)); + window.set_title("GstPipelineStudio"); + window.set_position(WindowPosition::Center); + window.set_size_request(800, 600); + let pipeline = Pipeline::new().expect("Unable to initialize the pipeline"); + + let app = GPSApp(Rc::new(GPSAppInner { + window, + builder, + pipeline, + })); + Ok(app) + } + pub fn on_startup(application: >k::Application) { + // Create application and error out if that fails for whatever reason + let app = match GPSApp::new(application) { + Ok(app) => app, + Err(_err) => { + /* utils::show_error_dialog( + true, + format!("Error creating application: {}", err).as_str(), + ); */ + return; + } + }; + + // When the application is activated show the UI. This happens when the first process is + // started, and in the first process whenever a second process is started + let app_weak = app.downgrade(); + application.connect_activate(move |_| { + let app = upgrade_weak!(app_weak); + app.build_ui(); + }); + + let app_container = RefCell::new(Some(app)); + application.connect_shutdown(move |_| { + let app = app_container + .borrow_mut() + .take() + .expect("Shutdown called multiple times"); + app.drop(); + }); + } + + pub fn build_ui(&self) { + let drawing_area = DrawingArea::new(); + let view_port: Viewport = self + .builder + .object("drawing_area") + .expect("Couldn't get window"); + let event_box = EventBox::new(); + event_box.add(&drawing_area); + view_port.add(&event_box); + let elements: Rc>> = Rc::new(RefCell::new(vec![])); + let e_clone = elements.clone(); + drawing_area.connect_draw(move |w, c| { + println!("w: {} c:{} e: {:?}", w, c, e_clone); + draw_elements(&e_clone.borrow().to_vec(), c); + gtk::Inhibit(false) + }); + + event_box.connect_button_release_event(move |_w, evt| { + let mut elements = elements.borrow_mut(); + let mut element: Element = Default::default(); + element.position.0 = evt.position().0; + element.position.1 = evt.position().1; + elements.push(element); + drawing_area.queue_draw(); + gtk::Inhibit(false) + }); + let window = &self.window; + + window.show_all(); + + let quit: MenuItem = self + .builder + .object("menu-quit") + .expect("Couldn't get window"); + let about: MenuItem = self + .builder + .object("menu-about") + .expect("Couldn't get window"); + let about_dialog: AboutDialog = self + .builder + .object("dialog-about") + .expect("Couldn't get window"); + about.connect_activate(move |_| { + about_dialog.connect_delete_event(|dialog, _| { + dialog.hide(); + gtk::Inhibit(true) + }); + + about_dialog.show_all(); + }); + + // Create a dialog to select GStreamer elements. + let add_button: Button = self + .builder + .object("button-add-plugin") + .expect("Couldn't get app_button"); + let app_weak = self.downgrade(); + add_button.connect_clicked(glib::clone!(@weak window => move |_| { + let app = upgrade_weak!(app_weak); + let elements = Pipeline::elements_list().expect("Unable to obtain element's list"); + pluginlistwindow::build_plugin_list(&app, &elements); + })); + // Create a dialog to open a file + let open_button: Button = self + .builder + .object("button-open-file") + .expect("Couldn't get app_button"); + let open_dialog: FileChooserDialog = self + .builder + .object("dialog-open-file") + .expect("Couldn't get window"); + open_button.connect_clicked(glib::clone!(@weak window => move |_| { + // entry.set_text("Clicked!"); + open_dialog.connect_response(|dialog, _| dialog.close()); + open_dialog.add_buttons(&[ + ("Open", ResponseType::Ok), + ("Cancel", ResponseType::Cancel) + ]); + open_dialog.set_select_multiple(true); + open_dialog.connect_response(|open_dialog, response| { + if response == ResponseType::Ok { + let files = open_dialog.filenames(); + println!("Files: {:?}", files); + } + open_dialog.close(); + }); + open_dialog.show_all(); + })); + + let accel_group = AccelGroup::new(); + window.add_accel_group(&accel_group); + + quit.connect_activate(glib::clone!(@weak window => move |_| { + window.close(); + })); + + // `Primary` is `Ctrl` on Windows and Linux, and `command` on macOS + // It isn't available directly through `gdk::ModifierType`, since it has + // different values on different platforms. + let (key, modifier) = gtk::accelerator_parse("Q"); + quit.add_accelerator("activate", &accel_group, key, modifier, AccelFlags::VISIBLE); + } + + // Downgrade to a weak reference + pub fn downgrade(&self) -> GPSAppWeak { + GPSAppWeak(Rc::downgrade(&self.0)) + } + + // Called when the application shuts down. We drop our app struct here + fn drop(self) {} +} diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 0000000..ca8bc11 --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,14 @@ +// Macro for upgrading a weak reference or returning the given value +// +// This works for glib/gtk objects as well as anything else providing an upgrade method +macro_rules! upgrade_weak { + ($x:ident, $r:expr) => {{ + match $x.upgrade() { + Some(o) => o, + None => return $r, + } + }}; + ($x:ident) => { + upgrade_weak!($x, ()) + }; +} diff --git a/src/main.rs b/src/main.rs index fd8d49a..70c9da0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,12 +17,15 @@ // along with this program. If not, see . // // SPDX-License-Identifier: GPL-3.0-only -mod mainwindow; +#[macro_use] +mod macros; +mod app; mod pipeline; mod pluginlistwindow; use gtk::prelude::*; +use crate::app::GPSApp; fn main() { // gio::resources_register_include!("compiled.gresource").unwrap(); @@ -30,8 +33,9 @@ fn main() { Some("com.github.gtk-rs.examples.menu_bar"), Default::default(), ); - - application.connect_activate(mainwindow::build_ui); + application.connect_startup(|application| { + GPSApp::on_startup(application); + }); application.run(); } diff --git a/src/mainwindow.rs b/src/mainwindow.rs deleted file mode 100644 index 9107fe5..0000000 --- a/src/mainwindow.rs +++ /dev/null @@ -1,116 +0,0 @@ -use gtk::cairo::Context; -use gtk::glib; -use gtk::prelude::*; -use gtk::{ - AboutDialog, AccelFlags, AccelGroup, ApplicationWindow, Builder, Button, Dialog, DrawingArea, - EventBox, FileChooserDialog, MenuItem, ResponseType, Viewport, WindowPosition, -}; -use std::cell::RefCell; -use std::rc::Rc; - -use crate::pipeline::Pipeline; -use crate::pluginlistwindow; -#[derive(Debug, Clone, Default)] -struct Element { - name: String, - position: (f64, f64), - size: (f64, f64), -} - -fn draw_elements(elements: &Vec, c: &Context) { - for element in elements { - c.rectangle(element.position.0, element.position.1, 80.0, 45.0); - c.fill().expect("Can not draw into context"); - } -} - -pub fn build_ui(application: >k::Application) { - let glade_src = include_str!("gps.ui"); - let builder = Builder::from_string(glade_src); - - let window: ApplicationWindow = builder.object("mainwindow").expect("Couldn't get window"); - window.set_application(Some(application)); - - window.set_title("GstPipelineStudio"); - window.set_position(WindowPosition::Center); - window.set_size_request(800, 600); - - let drawing_area = DrawingArea::new(); - let view_port: Viewport = builder.object("drawing_area").expect("Couldn't get window"); - let event_box = EventBox::new(); - event_box.add(&drawing_area); - view_port.add(&event_box); - let elements: Rc>> = Rc::new(RefCell::new(vec![])); - let e_clone = elements.clone(); - drawing_area.connect_draw(move |w, c| { - println!("w: {} c:{} e: {:?}", w, c, e_clone); - draw_elements(&e_clone.borrow().to_vec(), c); - gtk::Inhibit(false) - }); - - event_box.connect_button_release_event(move |_w, evt| { - let mut elements = elements.borrow_mut(); - let mut element: Element = Default::default(); - element.position.0 = evt.position().0; - element.position.1 = evt.position().1; - elements.push(element); - drawing_area.queue_draw(); - gtk::Inhibit(false) - }); - window.show_all(); - - let quit: MenuItem = builder.object("menu-quit").expect("Couldn't get window"); - let about: MenuItem = builder.object("menu-about").expect("Couldn't get window"); - let about_dialog: AboutDialog = builder.object("dialog-about").expect("Couldn't get window"); - about.connect_activate(move |_| { - about_dialog.show(); - }); - // Create a dialog to select GStreamer elements. - let add_button: Button = builder - .object("button-add-plugin") - .expect("Couldn't get app_button"); - - add_button.connect_clicked(glib::clone!(@weak window => move |_| { - // entry.set_text("Clicked!"); - let pipeline = Pipeline::new(); - let elements = Pipeline::get_elements_list().expect("cocuou"); - pluginlistwindow::build_plugin_list(&window, &elements); - })); - // Create a dialog to open a file - let open_button: Button = builder - .object("button-open-file") - .expect("Couldn't get app_button"); - let open_dialog: FileChooserDialog = builder - .object("dialog-open-file") - .expect("Couldn't get window"); - open_button.connect_clicked(glib::clone!(@weak window => move |_| { - // entry.set_text("Clicked!"); - open_dialog.connect_response(|dialog, _| dialog.close()); - open_dialog.add_buttons(&[ - ("Open", ResponseType::Ok), - ("Cancel", ResponseType::Cancel) - ]); - open_dialog.set_select_multiple(true); - open_dialog.connect_response(|open_dialog, response| { - if response == ResponseType::Ok { - let files = open_dialog.filenames(); - println!("Files: {:?}", files); - } - open_dialog.close(); - }); - open_dialog.show_all(); - })); - - let accel_group = AccelGroup::new(); - window.add_accel_group(&accel_group); - - quit.connect_activate(glib::clone!(@weak window => move |_| { - window.close(); - })); - - // `Primary` is `Ctrl` on Windows and Linux, and `command` on macOS - // It isn't available directly through `gdk::ModifierType`, since it has - // different values on different platforms. - let (key, modifier) = gtk::accelerator_parse("Q"); - quit.add_accelerator("activate", &accel_group, key, modifier, AccelFlags::VISIBLE); -} diff --git a/src/pipeline.rs b/src/pipeline.rs index daaa67d..c23bfcd 100644 --- a/src/pipeline.rs +++ b/src/pipeline.rs @@ -37,6 +37,7 @@ impl Default for ElementInfo { } } +#[derive(Debug)] pub struct Pipeline { initialized: bool, } diff --git a/src/pluginlistwindow.rs b/src/pluginlistwindow.rs index 32eaaee..7f3784b 100644 --- a/src/pluginlistwindow.rs +++ b/src/pluginlistwindow.rs @@ -1,3 +1,4 @@ +use crate::app::GPSApp; use crate::pipeline::ElementInfo; use gtk::{ glib::{self, clone}, @@ -6,8 +7,7 @@ use gtk::{ }; use gtk::{ - ApplicationWindow, CellRendererText, Dialog, Label, ListStore, Orientation, TreeView, - TreeViewColumn, WindowPosition, + CellRendererText, Label, ListStore, Orientation, TreeView, TreeViewColumn, WindowPosition, }; fn create_and_fill_model(elements: &Vec) -> ListStore { @@ -45,10 +45,10 @@ fn create_and_setup_view() -> TreeView { tree } -pub fn build_plugin_list(window: &ApplicationWindow, elements: &Vec) { +pub fn build_plugin_list(app: &GPSApp, elements: &Vec) { let dialog = gtk::Dialog::with_buttons( Some("Edit Item"), - Some(window), + Some(&app.window), gtk::DialogFlags::MODAL, &[("Close", ResponseType::Close)], );