Skip to content
This repository has been archived by the owner on Mar 4, 2024. It is now read-only.

Add proc macro for binding methods to action maps #266

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ jobs:
- run: cargo run --release -- --no-manual-traits ../glib-macros
working-directory: checker
if: matrix.rust == 'nightly' && matrix.conf.name == 'glib'
- run: cargo run --release -- --no-manual-traits ../gio-macros
working-directory: checker
if: matrix.rust == 'nightly' && matrix.conf.name == 'gio'
- run: cargo run --release -- --no-manual-traits ../gtk3-macros
working-directory: checker
if: matrix.rust == 'nightly' && matrix.conf.name == 'glib'
Expand Down Expand Up @@ -135,6 +138,11 @@ jobs:
run: xvfb-run --auto-servernum cargo test --manifest-path glib-macros/Cargo.toml
- name: "glib-macros: build"
run: cargo build --manifest-path glib-macros/Cargo.toml
# gio-macros
- name: "gio-macros: tests"
run: cargo test --manifest-path gio-macros/Cargo.toml
- name: "gio-macros: build"
run: cargo build --manifest-path gio-macros/Cargo.toml
# examples
- name: "examples"
run: cargo build --manifest-path examples/Cargo.toml --bins --examples --all-features
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ members = [
"gdkx11/sys",
"gio",
"gio/sys",
"gio-macros",
"glib",
"glib/gobject-sys",
"glib/sys",
Expand Down
3 changes: 3 additions & 0 deletions examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ path = "basics/main.rs"
name = "builder_pattern"
path = "builder_pattern/main.rs"

[[bin]]
name = "actions"

[[bin]]
name = "cairo_png"
path = "cairo_png/main.rs"
Expand Down
326 changes: 326 additions & 0 deletions examples/src/bin/actions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,326 @@
//! # Actions Sample
//!
//! This sample demonstrates how to use `gio::actions` macro.

use gtk::prelude::*;
use gtk::{gio, glib};

#[derive(glib::Downgrade)]
pub struct ExampleApplication(gtk::Application);

impl ExampleApplication {
pub fn new() -> Self {
let app = Self(
gtk::ApplicationBuilder::new()
.application_id("com.github.gtk-rs.examples.actions")
.build(),
);
app.0.connect_startup(glib::clone!(@weak app => move |_app|
app.startup()
));
app.0.connect_activate(glib::clone!(@weak app => move |_app|
app.activate()
));
app
}

fn startup(&self) {
// This line creates actions (`gio::SimpleAction`),
// binds them to handlers, defined in the next `impl` block
// and adds them to a `self.0`.
self.register_actions(&self.0);
self.add_accelerators();
self.build_system_menu();
}

fn add_accelerators(&self) {
self.0.set_accels_for_action("app.about", &["F1"]);
self.0.set_accels_for_action("app.quit", &["<Primary>Q"]);
}

fn activate(&self) {
if let Some(window) = self.0.active_window() {
window.show_all();
} else {
let window = ExampleWindow::new(&self.0);
window.0.show_all();
}
}

fn build_system_menu(&self) {
let menu = gio::Menu::new();
menu.append(Some("Quit"), Some("app.quit"));
self.0.set_app_menu(Some(&menu));

let menu_bar = gio::Menu::new();
menu_bar.append_submenu(Some("_Actions"), &{
let menu = gio::Menu::new();
menu.append(Some("Example Action"), Some("win.example_action"));
menu.append(Some("Full screen"), Some("win.fullscreen"));
menu.append(Some("Quit"), Some("app.quit"));
menu
});
menu_bar.append_submenu(Some("_Group #1"), &{
let menu = gio::Menu::new();
menu.append(Some("Example Action"), Some("group1.example_action"));
menu
});
menu_bar.append_submenu(Some("_Group #2"), &{
let menu = gio::Menu::new();
menu.append(Some("Example Action"), Some("group2.example_action"));
menu
});
menu_bar.append_submenu(Some("?"), &{
let menu = gio::Menu::new();
menu.append(Some("About"), Some("app.about"));
menu
});
self.0.set_menubar(Some(&menu_bar));
}
}

impl std::default::Default for ExampleApplication {
fn default() -> Self {
Self::new()
}
}

#[gio::actions]
impl ExampleApplication {
fn about(&self) {
let window = self.0.active_window();
let dialog = gtk::AboutDialogBuilder::new()
.website_label("gtk-rs")
.website("http://gtk-rs.org")
.authors(vec!["Gtk-rs developers".into()])
.title("About!")
.build();
dialog.set_transient_for(window.as_ref());
dialog.show_all();
}

fn quit(&self) {
// Close all windows
for window in self.0.windows() {
window.close();
}
}
}

#[derive(glib::Downgrade)]
pub struct ExampleWindow(gtk::ApplicationWindow);

impl ExampleWindow {
pub fn new(app: &gtk::Application) -> Self {
let window = Self(
gtk::ApplicationWindowBuilder::new()
.application(app)
.title("System menu bar")
.window_position(gtk::WindowPosition::Center)
.default_width(350)
.default_height(70)
.build(),
);

let grid = gtk::GridBuilder::new()
.margin(10)
.row_spacing(10)
.column_spacing(10)
.build();
window.0.add(&grid);

let label = gtk::LabelBuilder::new()
.label("Nothing happened yet")
.halign(gtk::Align::Start)
.build();
grid.attach(&label, 0, 0, 2, 1);

let example = gtk::ButtonBuilder::new()
.label("Example Action")
.action_name("win.example_action")
.build();
grid.attach(&example, 0, 1, 1, 1);

let label = gtk::LabelBuilder::new()
.label("Toggle full screen mode")
.halign(gtk::Align::Start)
.build();
grid.attach(&label, 0, 2, 1, 1);

let switch = gtk::Switch::new();
switch.set_action_name(Some("win.fullscreen"));
grid.attach(&switch, 1, 2, 1, 1);

let colors = RadioGroupBuilder::new("win.pick_color")
.add("Red", "#FF0000")
.add("Green", "#00FF00")
.add("Blue", "#0000FF")
.build();
grid.attach(&colors, 0, 3, 1, 1);

// This line creates actions (`gio::SimpleAction`),
// binds them to handlers, defined in the next `impl` block
// and adds them to a `window.0`.
window.register_actions(&window.0);

// Get "volume" action from the window. It exists after an invocation
// of `register_action` method, which created all actions and added them
// to our window.
let volume_action = window.0.lookup_action("volume").unwrap();

let volume = gtk::ScaleBuilder::new()
.orientation(gtk::Orientation::Horizontal)
.adjustment(&gtk::Adjustment::new(0.0, -2.0, 12.0, 0.1, 0.0, 0.0))
.build();
volume.connect_change_value(
glib::clone!(@weak window, @weak volume_action => @default-return Inhibit(false), move |_scale, _scroll_type, value| {
volume_action.activate(Some(&value.to_variant()));
Inhibit(false)
}),
);
grid.attach(&volume, 0, 4, 1, 1);

let volume_label = gtk::LabelBuilder::new()
.label("Volume: 0.0")
.halign(gtk::Align::End)
.build();
grid.attach(&volume_label, 1, 4, 1, 1);

volume_action.connect_property_state_notify(
glib::clone!(@weak volume_label => move |action| {
let value = action.state().and_then(|variant| variant.get::<f64>()).unwrap();
volume_label.set_label(&format!("Volume: {:.2}", value));
}),
);

// Additionally, we can create own action groups and add them into our window.
// All actions in this group have a prefix "group1".
let action_group1 = gio::SimpleActionGroup::new();
window.register_actions_group1(&action_group1);
window.0.insert_action_group("group1", Some(&action_group1));

// And all actions in this group have a prefix "group2".
let action_group2 = gio::SimpleActionGroup::new();
window.register_actions_group2(&action_group2);
window.0.insert_action_group("group2", Some(&action_group2));

window
}

fn show_message(&self, message: &str) {
let dialog = gtk::MessageDialogBuilder::new()
.transient_for(&self.0)
.modal(true)
.message_type(gtk::MessageType::Info)
.text(message)
.buttons(gtk::ButtonsType::Ok)
.build();

dialog.show_all();
dialog.run();
dialog.close();
}
}

#[gio::actions]
impl ExampleWindow {
fn example_action(&self) {
self.show_message("Example Action is activated!");
}

#[action(stateful)]
fn fullscreen(&self, was_active: bool) -> Option<bool> {
let is_active = !was_active; // New state
if is_active {
self.0.fullscreen();
} else {
self.0.unfullscreen();
}
// Update state of the action by returning new value
Some(is_active)
}

// This action has both a state and a parameter.
#[action(stateful)]
fn pick_color(&self, previous_state: String, color_parameter: String) -> Option<String> {
if previous_state == color_parameter {
// We do not want to change the state to the same value. So, returning `None`.
None
} else {
// Color is different, so let's update the state.
Some(color_parameter)
}
}

#[action(change_state)]
fn volume(&self, value: f64) -> Option<f64> {
if value >= 0.0 && value <= 10.0 {
// accept
Some(value)
} else {
// reject value
None
}
}
}

// This `impl` block also contains actions, but its register method is
// renamed by a following annotation to `register_actions_group1` not to
// collide with a block above.
#[gio::actions(register_fn = "register_actions_group1")]
impl ExampleWindow {
// We can rename an action, so its detailed name becomes `group1.example_action`.
#[action(name = "example_action")]
fn group1_example_action(&self) {
self.show_message("Group #1 Example Action is activated!");
}
}

#[gio::actions(register_fn = "register_actions_group2")]
impl ExampleWindow {
// We can rename an action, so its detailed name becomes `group2.example_action`.
#[action(name = "example_action")]
fn group2_example_action(&self) {
self.show_message("Group #2 Example Action is activated!");
}
}

struct RadioGroupBuilder {
grid: gtk::Grid,
count: i32,
last: Option<gtk::RadioButton>,
action: String,
}

impl RadioGroupBuilder {
pub fn new(action: &str) -> Self {
Self {
grid: gtk::GridBuilder::new().row_spacing(10).build(),
count: 0,
last: None,
action: action.into(),
}
}

pub fn add(mut self, label: &str, parameter: &str) -> Self {
let radio = gtk::RadioButtonBuilder::new()
.label(label)
.action_name(&self.action)
.action_target(&parameter.to_variant())
.build();
radio.join_group(self.last.as_ref());
self.grid.attach(&radio, 0, self.count, 1, 1);
self.last = Some(radio);
self.count += 1;
self
}

pub fn build(self) -> gtk::Widget {
self.grid.upcast()
}
}

fn main() {
let application = ExampleApplication::new();
application.0.run();
}
1 change: 1 addition & 0 deletions gio-macros/COPYRIGHT
24 changes: 24 additions & 0 deletions gio-macros/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[package]
name = "gio-macros"
documentation = "https://gtk-rs.org/docs/gio-macros/"
homepage = "https://gtk-rs.org/"
authors = ["The Gtk-rs Project Developers"]
description = "Rust bindings for the Gio library, proc macros crate"
version = "0.13.0"
keywords = ["glib", "gtk-rs", "gnome", "GUI"]
repository = "https://github.com/gtk-rs/gtk-rs"
license = "MIT"
edition = "2018"

[dependencies]
syn = { version = "1", features = ["full"] }
quote = "1"
proc-macro2 = "1"
darling = "0.12"

[lib]
proc-macro = true

[dev-dependencies]
glib = { path = "../glib" }
gio = { path = "../gio" }
1 change: 1 addition & 0 deletions gio-macros/LICENSE
Loading