gio/
settings.rs

1// Take a look at the license at the top of the repository in the LICENSE file.
2
3use glib::{BoolError, StrV, Variant, prelude::*, translate::*};
4
5use crate::{Settings, SettingsBindFlags, ffi, prelude::*};
6
7#[must_use = "The builder must be built to be used"]
8pub struct BindingBuilder<'a> {
9    settings: &'a Settings,
10    key: &'a str,
11    object: &'a glib::Object,
12    property: &'a str,
13    flags: SettingsBindFlags,
14    #[allow(clippy::type_complexity)]
15    get_mapping: Option<Box<dyn Fn(&glib::Variant, glib::Type) -> Option<glib::Value>>>,
16    #[allow(clippy::type_complexity)]
17    set_mapping: Option<Box<dyn Fn(&glib::Value, glib::VariantType) -> Option<glib::Variant>>>,
18}
19
20impl BindingBuilder<'_> {
21    pub fn flags(mut self, flags: SettingsBindFlags) -> Self {
22        self.flags = flags;
23        self
24    }
25
26    // rustdoc-stripper-ignore-next
27    /// Set the binding flags to [`GET`][crate::SettingsBindFlags::GET].
28    pub fn get(mut self) -> Self {
29        self.flags |= SettingsBindFlags::GET;
30        self
31    }
32
33    // rustdoc-stripper-ignore-next
34    /// Set the binding flags to [`SET`][crate::SettingsBindFlags::SET].
35    pub fn set(mut self) -> Self {
36        self.flags |= SettingsBindFlags::SET;
37        self
38    }
39
40    // rustdoc-stripper-ignore-next
41    /// Unsets the default [`GET`][crate::SettingsBindFlags::GET] flag.
42    pub fn set_only(mut self) -> Self {
43        self.flags = (self.flags - SettingsBindFlags::GET) | SettingsBindFlags::SET;
44        self
45    }
46
47    // rustdoc-stripper-ignore-next
48    /// Unsets the default [`SET`][crate::SettingsBindFlags::SET] flag.
49    pub fn get_only(mut self) -> Self {
50        self.flags = (self.flags - SettingsBindFlags::SET) | SettingsBindFlags::GET;
51        self
52    }
53
54    // rustdoc-stripper-ignore-next
55    /// Set the binding flags to [`NO_SENSITIVITY`][crate::SettingsBindFlags::NO_SENSITIVITY].
56    pub fn no_sensitivity(mut self) -> Self {
57        self.flags |= SettingsBindFlags::NO_SENSITIVITY;
58        self
59    }
60
61    // rustdoc-stripper-ignore-next
62    /// Set the binding flags to [`GET_NO_CHANGES`][crate::SettingsBindFlags::GET_NO_CHANGES].
63    pub fn get_no_changes(mut self) -> Self {
64        self.flags |= SettingsBindFlags::GET_NO_CHANGES;
65        self
66    }
67
68    // rustdoc-stripper-ignore-next
69    /// Set the binding flags to [`INVERT_BOOLEAN`][crate::SettingsBindFlags::INVERT_BOOLEAN].
70    pub fn invert_boolean(mut self) -> Self {
71        self.flags |= SettingsBindFlags::INVERT_BOOLEAN;
72        self
73    }
74
75    #[doc(alias = "get_mapping")]
76    pub fn mapping<F: Fn(&glib::Variant, glib::Type) -> Option<glib::Value> + 'static>(
77        mut self,
78        f: F,
79    ) -> Self {
80        self.get_mapping = Some(Box::new(f));
81        self
82    }
83
84    pub fn set_mapping<
85        F: Fn(&glib::Value, glib::VariantType) -> Option<glib::Variant> + 'static,
86    >(
87        mut self,
88        f: F,
89    ) -> Self {
90        self.set_mapping = Some(Box::new(f));
91        self
92    }
93
94    pub fn build(self) {
95        type Mappings = (
96            Option<Box<dyn Fn(&glib::Variant, glib::Type) -> Option<glib::Value>>>,
97            Option<Box<dyn Fn(&glib::Value, glib::VariantType) -> Option<glib::Variant>>>,
98        );
99        unsafe extern "C" fn bind_with_mapping_get_trampoline(
100            value: *mut glib::gobject_ffi::GValue,
101            variant: *mut glib::ffi::GVariant,
102            user_data: glib::ffi::gpointer,
103        ) -> glib::ffi::gboolean {
104            unsafe {
105                let user_data = &*(user_data as *const Mappings);
106                let f = user_data.0.as_ref().unwrap();
107                let value = &mut *(value as *mut glib::Value);
108                match f(&from_glib_borrow(variant), value.type_()) {
109                    Some(v) => {
110                        *value = v;
111                        true
112                    }
113                    _ => false,
114                }
115                .into_glib()
116            }
117        }
118        unsafe extern "C" fn bind_with_mapping_set_trampoline(
119            value: *const glib::gobject_ffi::GValue,
120            variant_type: *const glib::ffi::GVariantType,
121            user_data: glib::ffi::gpointer,
122        ) -> *mut glib::ffi::GVariant {
123            unsafe {
124                let user_data = &*(user_data as *const Mappings);
125                let f = user_data.1.as_ref().unwrap();
126                let value = &*(value as *const glib::Value);
127                f(value, from_glib_none(variant_type)).into_glib_ptr()
128            }
129        }
130        unsafe extern "C" fn destroy_closure(ptr: *mut libc::c_void) {
131            unsafe {
132                let _ = Box::<Mappings>::from_raw(ptr as *mut _);
133            }
134        }
135
136        if self.get_mapping.is_none() && self.set_mapping.is_none() {
137            unsafe {
138                ffi::g_settings_bind(
139                    self.settings.to_glib_none().0,
140                    self.key.to_glib_none().0,
141                    self.object.to_glib_none().0,
142                    self.property.to_glib_none().0,
143                    self.flags.into_glib(),
144                );
145            }
146        } else {
147            let get_trampoline: Option<unsafe extern "C" fn(_, _, _) -> _> =
148                if self.get_mapping.is_none() {
149                    None
150                } else {
151                    Some(bind_with_mapping_get_trampoline)
152                };
153            let set_trampoline: Option<unsafe extern "C" fn(_, _, _) -> _> =
154                if self.set_mapping.is_none() {
155                    None
156                } else {
157                    Some(bind_with_mapping_set_trampoline)
158                };
159            let mappings: Mappings = (self.get_mapping, self.set_mapping);
160            unsafe {
161                ffi::g_settings_bind_with_mapping(
162                    self.settings.to_glib_none().0,
163                    self.key.to_glib_none().0,
164                    self.object.to_glib_none().0,
165                    self.property.to_glib_none().0,
166                    self.flags.into_glib(),
167                    get_trampoline,
168                    set_trampoline,
169                    Box::into_raw(Box::new(mappings)) as *mut libc::c_void,
170                    Some(destroy_closure),
171                )
172            }
173        }
174    }
175}
176
177pub trait SettingsExtManual: IsA<Settings> {
178    fn get<U: FromVariant>(&self, key: &str) -> U {
179        let val = self.value(key);
180        FromVariant::from_variant(&val).unwrap_or_else(|| {
181            panic!(
182                "Type mismatch: Expected '{}' got '{}'",
183                U::static_variant_type().as_str(),
184                val.type_()
185            )
186        })
187    }
188
189    fn set(&self, key: &str, value: impl Into<Variant>) -> Result<(), BoolError> {
190        self.set_value(key, &value.into())
191    }
192
193    /// A convenience variant of `Gio::Settings::get()` for string arrays.
194    ///
195    /// It is a programmer error to give a @key that isn’t specified as
196    /// having an `as` type in the schema for @self (see [`glib::VariantType`][crate::glib::VariantType]).
197    /// ## `key`
198    /// the key to get the value for
199    ///
200    /// # Returns
201    ///
202    /// a
203    ///   newly-allocated, `NULL`-terminated array of strings, the value that
204    ///   is stored at @key in @self.
205    #[doc(alias = "g_settings_get_strv")]
206    #[doc(alias = "get_strv")]
207    fn strv(&self, key: &str) -> StrV {
208        unsafe {
209            FromGlibPtrContainer::from_glib_full(ffi::g_settings_get_strv(
210                self.as_ref().to_glib_none().0,
211                key.to_glib_none().0,
212            ))
213        }
214    }
215
216    /// Sets @key in @self to @value.
217    ///
218    /// A convenience variant of `Gio::Settings::set()` for string arrays.  If
219    /// @value is `NULL`, then @key is set to be the empty array.
220    ///
221    /// It is a programmer error to give a @key that isn’t specified as
222    /// having an `as` type in the schema for @self (see [`glib::VariantType`][crate::glib::VariantType]).
223    /// ## `key`
224    /// the key to set the value for
225    /// ## `value`
226    /// the value to set it to
227    ///
228    /// # Returns
229    ///
230    /// true if setting the key succeeded,
231    ///   false if the key was not writable
232    #[doc(alias = "g_settings_set_strv")]
233    fn set_strv(&self, key: &str, value: impl IntoStrV) -> Result<(), glib::error::BoolError> {
234        unsafe {
235            value.run_with_strv(|value| {
236                glib::result_from_gboolean!(
237                    ffi::g_settings_set_strv(
238                        self.as_ref().to_glib_none().0,
239                        key.to_glib_none().0,
240                        value.as_ptr() as *mut _,
241                    ),
242                    "Can't set readonly key"
243                )
244            })
245        }
246    }
247
248    /// Create a binding between the @key in the @self object
249    /// and the property @property of @object.
250    ///
251    /// The binding uses the default GIO mapping functions to map
252    /// between the settings and property values. These functions
253    /// handle booleans, numeric types and string types in a
254    /// straightforward way. Use [`bind_with_mapping()`][Self::bind_with_mapping()] if
255    /// you need a custom mapping, or map between types that are not
256    /// supported by the default mapping functions.
257    ///
258    /// Unless the @flags include [flags@Gio.SettingsBindFlags.NO_SENSITIVITY], this
259    /// function also establishes a binding between the writability of
260    /// @key and the `sensitive` property of @object (if @object has
261    /// a boolean property by that name). See [`SettingsExt::bind_writable()`][crate::prelude::SettingsExt::bind_writable()]
262    /// for more details about writable bindings.
263    ///
264    /// Note that the lifecycle of the binding is tied to @object,
265    /// and that you can have only one binding per object property.
266    /// If you bind the same property twice on the same object, the second
267    /// binding overrides the first one.
268    /// ## `key`
269    /// the key to bind
270    /// ## `object`
271    /// the object with property to bind
272    /// ## `property`
273    /// the name of the property to bind
274    /// ## `flags`
275    /// flags for the binding
276    #[doc(alias = "g_settings_bind")]
277    #[doc(alias = "g_settings_bind_with_mapping")]
278    fn bind<'a, P: IsA<glib::Object>>(
279        &'a self,
280        key: &'a str,
281        object: &'a P,
282        property: &'a str,
283    ) -> BindingBuilder<'a> {
284        BindingBuilder {
285            settings: self.upcast_ref(),
286            key,
287            object: object.upcast_ref(),
288            property,
289            flags: SettingsBindFlags::DEFAULT,
290            get_mapping: None,
291            set_mapping: None,
292        }
293    }
294}
295
296impl<O: IsA<Settings>> SettingsExtManual for O {}
297
298#[cfg(test)]
299mod test {
300    use std::{env::set_var, process::Command, str::from_utf8, sync::Once};
301
302    use super::*;
303
304    static INIT: Once = Once::new();
305
306    fn set_env() {
307        INIT.call_once(|| {
308            let tmp_dir = glib::mkdtemp("gio-rs-test-schemas-XXXXXX").unwrap();
309
310            let output = Command::new("glib-compile-schemas")
311                .args([
312                    &format!("{}/tests", env!("CARGO_MANIFEST_DIR")),
313                    "--targetdir",
314                    tmp_dir.to_str().unwrap(),
315                ])
316                .output()
317                .unwrap();
318
319            if !output.status.success() {
320                println!("Failed to generate GSchema!");
321                println!(
322                    "glib-compile-schemas stdout: {}",
323                    from_utf8(&output.stdout).unwrap()
324                );
325                println!(
326                    "glib-compile-schemas stderr: {}",
327                    from_utf8(&output.stderr).unwrap()
328                );
329                panic!("Can't test without GSchemas!");
330            }
331
332            // TODO: Audit that the environment access only happens in single-threaded code.
333            unsafe { set_var("GSETTINGS_SCHEMA_DIR", tmp_dir) };
334            // TODO: Audit that the environment access only happens in single-threaded code.
335            unsafe { set_var("GSETTINGS_BACKEND", "memory") };
336        });
337    }
338
339    #[test]
340    #[serial_test::serial]
341    fn string_get() {
342        set_env();
343        let settings = Settings::new("com.github.gtk-rs.test");
344        assert_eq!(settings.get::<String>("test-string").as_str(), "Good");
345    }
346
347    #[test]
348    #[serial_test::serial]
349    fn bool_set_get() {
350        set_env();
351        let settings = Settings::new("com.github.gtk-rs.test");
352        settings.set("test-bool", false).unwrap();
353        assert!(!settings.get::<bool>("test-bool"));
354    }
355
356    #[test]
357    #[should_panic]
358    #[serial_test::serial]
359    fn wrong_type() {
360        set_env();
361        let settings = Settings::new("com.github.gtk-rs.test");
362        settings.get::<u8>("test-string");
363    }
364}