diff --git a/Cargo.lock b/Cargo.lock
index bdafe9af..2a70c6c0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -954,6 +954,8 @@ dependencies = [
"libadwaita",
"libpulse-binding",
"libpulse-glib-binding",
+ "num-rational",
+ "num-traits",
"once_cell",
"serde",
"serde_yaml",
@@ -1114,6 +1116,17 @@ dependencies = [
"winapi",
]
+[[package]]
+name = "num-bigint"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
[[package]]
name = "num-derive"
version = "0.3.3"
@@ -1141,6 +1154,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0"
dependencies = [
"autocfg",
+ "num-bigint",
"num-integer",
"num-traits",
]
diff --git a/Cargo.toml b/Cargo.toml
index d5fb0c3f..695ecb27 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -21,6 +21,8 @@ gst = { package = "gstreamer", version = "0.22" }
gst-plugin-gif = "0.12"
gst-plugin-gtk4 = { version = "0.12", features = ["gtk_v4_14"] }
gtk = { package = "gtk4", version = "0.8", features = ["v4_14"] }
+num-rational = "0.4"
+num-traits = "0.2"
once_cell = "1.19.0"
pulse = { package = "libpulse-binding", version = "2.26.0" }
pulse_glib = { package = "libpulse-glib-binding", version = "2.25.1" }
diff --git a/data/io.github.seadve.Kooha.gschema.xml.in b/data/io.github.seadve.Kooha.gschema.xml.in
index 972b2df8..52b04acb 100644
--- a/data/io.github.seadve.Kooha.gschema.xml.in
+++ b/data/io.github.seadve.Kooha.gschema.xml.in
@@ -26,8 +26,8 @@
b""
-
- 30
+
+ (30, 1)
""
diff --git a/data/resources/ui/preferences-dialog.ui b/data/resources/ui/preferences-dialog.ui
index eaee9e5c..6d91b289 100644
--- a/data/resources/ui/preferences-dialog.ui
+++ b/data/resources/ui/preferences-dialog.ui
@@ -54,35 +54,8 @@
-
diff --git a/src/area_selector/mod.rs b/src/area_selector/mod.rs
index 0cf35927..f4ff2169 100644
--- a/src/area_selector/mod.rs
+++ b/src/area_selector/mod.rs
@@ -14,9 +14,14 @@ use std::{cell::RefCell, os::unix::prelude::RawFd};
pub use self::view_port::Selection;
use self::view_port::ViewPort;
-use crate::{application::Application, cancelled::Cancelled, pipeline, screencast_session::Stream};
+use crate::{
+ application::Application,
+ cancelled::Cancelled,
+ pipeline::{self, Framerate},
+ screencast_session::Stream,
+};
-const PREVIEW_FRAMERATE: u32 = 60;
+const PREVIEW_FRAMERATE: Framerate = Framerate::new_raw(60, 1);
const WINDOW_TO_MONITOR_SCALE_FACTOR: f64 = 0.4;
// We can't get header bar height before the window is presented, so we assume "46" as the default.
diff --git a/src/pipeline.rs b/src/pipeline.rs
index dbe71241..0da4347b 100644
--- a/src/pipeline.rs
+++ b/src/pipeline.rs
@@ -1,6 +1,7 @@
use anyhow::{bail, Context, Ok, Result};
use gst::prelude::*;
use gtk::graphene::Rect;
+use num_rational::Rational32;
use std::{
os::unix::io::RawFd,
@@ -12,11 +13,13 @@ use crate::{area_selector::SelectAreaData, profile::Profile, screencast_session:
const AUDIO_SAMPLE_RATE: i32 = 48_000;
const AUDIO_N_CHANNELS: i32 = 1;
+pub type Framerate = Rational32;
+
#[derive(Debug)]
#[must_use]
pub struct PipelineBuilder {
file_path: PathBuf,
- framerate: u32,
+ framerate: Framerate,
profile: Profile,
fd: RawFd,
streams: Vec,
@@ -28,7 +31,7 @@ pub struct PipelineBuilder {
impl PipelineBuilder {
pub fn new(
file_path: &Path,
- framerate: u32,
+ framerate: Framerate,
profile: Profile,
fd: RawFd,
streams: Vec,
@@ -63,7 +66,7 @@ impl PipelineBuilder {
pub fn build(&self) -> Result {
tracing::debug!(
file_path = %self.file_path.display(),
- framerate = self.framerate,
+ framerate = ?self.framerate,
profile = ?self.profile.id(),
stream_len = self.streams.len(),
streams = ?self.streams,
@@ -226,13 +229,13 @@ fn make_videocrop(data: &SelectAreaData) -> Result {
pub fn make_pipewiresrc_bin(
fd: RawFd,
streams: &[Stream],
- framerate: u32,
+ framerate: Framerate,
select_area_data: Option<&SelectAreaData>,
) -> Result {
let bin = gst::Bin::builder().name("kooha-pipewiresrc-bin").build();
let videorate_caps = gst::Caps::builder("video/x-raw")
- .field("framerate", gst::Fraction::new(framerate as i32, 1))
+ .field("framerate", gst::Fraction::from(framerate))
.build();
let src_element = match streams {
diff --git a/src/preferences_dialog.rs b/src/preferences_dialog.rs
index 2a7380e1..62f1e362 100644
--- a/src/preferences_dialog.rs
+++ b/src/preferences_dialog.rs
@@ -1,19 +1,103 @@
-use std::path::Path;
+use std::{fmt, path::Path};
use adw::{prelude::*, subclass::prelude::*};
use gettextrs::gettext;
use gtk::{
gio,
- glib::{self, clone, closure, BoxedAnyObject},
+ glib::{self, clone, closure, translate::FromGlib, BoxedAnyObject},
};
+use num_traits::Signed;
-use crate::{item_row::ItemRow, profile::Profile, settings::Settings, IS_EXPERIMENTAL_MODE};
+use crate::{
+ item_row::ItemRow, pipeline::Framerate, profile::Profile, settings::Settings,
+ IS_EXPERIMENTAL_MODE,
+};
+
+const ROW_SELECTED_ITEM_NOTIFY_HANDLER_ID_KEY: &str = "kooha-row-selected-item-notify-handler-id";
+const SETTINGS_PROFILE_CHANGED_HANDLER_ID_KEY: &str = "kooha-settings-profile-changed-handler-id";
/// Used to represent "none" profile in the profiles model
type NoneProfile = BoxedAnyObject;
-const PROFILE_ROW_SELECTED_ITEM_NOTIFY_HANDLER_ID_KEY: &str =
- "kooha-profile-row-selected-item-notify-handler-id";
+#[derive(Debug, Clone, Copy, PartialEq, Eq, glib::Enum)]
+#[enum_type(name = "KoohaFramerateOption")]
+pub enum FramerateOption {
+ _10,
+ _20,
+ _23_976,
+ _24,
+ _25,
+ _29_97,
+ _30,
+ _48,
+ _50,
+ _59_94,
+ _60,
+}
+
+impl FramerateOption {
+ /// Returns the closest `FramerateOption` to the given `Framerate`.
+ pub fn from_framerate(framerate: Framerate) -> Self {
+ let all = [
+ Self::_10,
+ Self::_20,
+ Self::_23_976,
+ Self::_24,
+ Self::_25,
+ Self::_29_97,
+ Self::_30,
+ Self::_48,
+ Self::_50,
+ Self::_59_94,
+ Self::_60,
+ ];
+
+ *all.iter()
+ .min_by(|a, b| {
+ (a.to_framerate() - framerate)
+ .abs()
+ .cmp(&(b.to_framerate() - framerate).abs())
+ })
+ .unwrap()
+ }
+
+ /// Converts a `FramerateOption` to a `Framerate`.
+ pub fn to_framerate(self) -> Framerate {
+ let (numer, denom) = match self {
+ Self::_10 => (10, 1),
+ Self::_20 => (20, 1),
+ Self::_23_976 => (24_000, 1001),
+ Self::_24 => (24, 1),
+ Self::_25 => (25, 1),
+ Self::_29_97 => (30_000, 1001),
+ Self::_30 => (30, 1),
+ Self::_48 => (48, 1),
+ Self::_50 => (50, 1),
+ Self::_59_94 => (60_000, 1001),
+ Self::_60 => (60, 1),
+ };
+ Framerate::new(numer, denom)
+ }
+}
+
+impl fmt::Display for FramerateOption {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let name = match self {
+ Self::_10 => "10",
+ Self::_20 => "20",
+ Self::_23_976 => "23.976",
+ Self::_24 => "24 NTSC",
+ Self::_25 => "25 PAL",
+ Self::_29_97 => "29.97",
+ Self::_30 => "30",
+ Self::_48 => "48",
+ Self::_50 => "50 PAL",
+ Self::_59_94 => "59.94",
+ Self::_60 => "60",
+ };
+ f.write_str(name)
+ }
+}
mod imp {
use std::cell::OnceCell;
@@ -28,13 +112,11 @@ mod imp {
#[property(get, set, construct_only)]
pub(super) settings: OnceCell,
- #[template_child]
- pub(super) framerate_button: TemplateChild,
- #[template_child]
- pub(super) framerate_warning: TemplateChild,
#[template_child]
pub(super) profile_row: TemplateChild,
#[template_child]
+ pub(super) framerate_row: TemplateChild,
+ #[template_child]
pub(super) delay_button: TemplateChild,
#[template_child]
pub(super) file_chooser_button_content: TemplateChild,
@@ -83,14 +165,67 @@ mod imp {
let obj = self.obj();
let settings = obj.settings();
- let active_profile = settings.profile();
- self.profile_row
- .set_factory(Some(&profile_row_factory(&self.profile_row)));
+ self.framerate_row.set_factory(Some(&row_factory(
+ &self.framerate_row,
+ &gettext("This frame rate may cause performance issues on the selected format."),
+ clone!(@strong settings => move |list_item| {
+ let item = list_item.item().unwrap();
+ let item_row = list_item.child().unwrap().downcast::().unwrap();
+
+ let enum_list_item = item.downcast_ref::().unwrap();
+ let framerate_option = unsafe { FramerateOption::from_glib(enum_list_item.value()) };
+ item_row.set_title(framerate_option.to_string());
+
+ unsafe {
+ list_item.set_data(
+ SETTINGS_PROFILE_CHANGED_HANDLER_ID_KEY,
+ settings.connect_profile_changed(
+ clone!(@weak list_item => move |settings| {
+ update_item_row_shows_warning_icon(settings, &list_item);
+ }),
+ ),
+ );
+ }
+
+ update_item_row_shows_warning_icon(&settings, list_item);
+ }),
+ clone!(@strong settings => move |list_item| {
+ unsafe {
+ let handler_id = list_item
+ .steal_data(SETTINGS_PROFILE_CHANGED_HANDLER_ID_KEY)
+ .unwrap();
+ settings.disconnect(handler_id);
+ }
+ }),
+ )));
+ self.framerate_row.set_model(Some(&adw::EnumListModel::new(
+ FramerateOption::static_type(),
+ )));
+
+ self.profile_row.set_factory(Some(&row_factory(
+ &self.profile_row,
+ &gettext(gettext("This format is experimental and unsupported.")),
+ |list_item| {
+ let item = list_item.item().unwrap();
+ let item_row = list_item.child().unwrap().downcast::().unwrap();
+
+ let profile = profile_from_obj(&item);
+ item_row.set_title(
+ profile
+ .map_or_else(|| gettext("None"), |profile| profile.name().to_string()),
+ );
+ item_row.set_shows_warning_icon(
+ profile.is_some_and(|profile| profile.is_experimental()),
+ );
+ },
+ |_| {},
+ )));
let profiles = Profile::all()
.inspect_err(|err| tracing::error!("Failed to load profiles: {:?}", err))
.unwrap_or_default();
let profiles_model = gio::ListStore::new::();
+ let active_profile = settings.profile();
if active_profile.is_none() {
profiles_model.append(&NoneProfile::new(()));
}
@@ -116,12 +251,8 @@ mod imp {
.bind_record_delay(&self.delay_button.get(), "value")
.build();
- settings
- .bind_video_framerate(&self.framerate_button.get(), "value")
- .build();
-
- settings.connect_video_framerate_changed(clone!(@weak obj => move |_| {
- obj.update_framerate_warning();
+ settings.connect_framerate_changed(clone!(@weak obj => move |_| {
+ obj.update_framerate_row();
}));
settings.connect_saving_location_changed(clone!(@weak obj => move |_| {
@@ -130,15 +261,14 @@ mod imp {
settings.connect_profile_changed(clone!(@weak obj => move |_| {
obj.update_profile_row();
- obj.update_framerate_warning();
}));
obj.update_file_chooser_button();
- obj.update_framerate_warning();
obj.update_profile_row();
+ obj.update_framerate_row();
- // Load last active profile first in `update_profile_row` before
- // connecting to the signal to avoid unnecessary updates.
+ // Load last active value first in `update_*_row` before connecting to
+ // the signal to avoid unnecessary updates.
self.profile_row
.connect_selected_item_notify(clone!(@weak obj => move |row| {
if let Some(item) = row.selected_item() {
@@ -146,6 +276,14 @@ mod imp {
obj.settings().set_profile(profile);
}
}));
+ self.framerate_row
+ .connect_selected_item_notify(clone!(@weak obj => move |row| {
+ if let Some(item) = row.selected_item() {
+ let enum_list_item = item.downcast_ref::().unwrap();
+ let framerate_option = unsafe { FramerateOption::from_glib(enum_list_item.value()) };
+ obj.settings().set_framerate(framerate_option.to_framerate());
+ }
+ }));
}
}
@@ -175,10 +313,11 @@ impl PreferencesDialog {
}
fn update_profile_row(&self) {
+ let imp = self.imp();
+
let settings = self.settings();
let active_profile = settings.profile();
- let imp = self.imp();
let position = imp
.profile_row
.model()
@@ -191,7 +330,6 @@ impl PreferencesDialog {
_ => false,
},
);
-
if let Some(position) = position {
imp.profile_row.set_selected(position as u32);
} else {
@@ -202,77 +340,91 @@ impl PreferencesDialog {
}
}
- fn update_framerate_warning(&self) {
+ fn update_framerate_row(&self) {
let imp = self.imp();
+
let settings = self.settings();
+ let framerate_option = FramerateOption::from_framerate(settings.framerate());
- imp.framerate_warning.set_visible(
- settings.profile().is_some_and(|profile| {
- settings.video_framerate() > profile.suggested_max_framerate()
- }),
- );
+ let position = imp
+ .framerate_row
+ .model()
+ .unwrap()
+ .into_iter()
+ .position(|item| {
+ let item = item.unwrap();
+ let enum_list_item = item.downcast::().unwrap();
+ enum_list_item.value() == framerate_option as i32
+ });
+ if let Some(position) = position {
+ imp.framerate_row.set_selected(position as u32);
+ } else {
+ tracing::error!(
+ "Active framerate `{:?}` was not found on framerate model",
+ framerate_option
+ );
+ }
}
}
-fn profile_row_factory(profile_row: &adw::ComboRow) -> gtk::SignalListItemFactory {
+fn row_factory(
+ row: &adw::ComboRow,
+ warning_tooltip_text: &str,
+ bind_cb: impl Fn(>k::ListItem) + 'static,
+ unbind_cb: impl Fn(>k::ListItem) + 'static,
+) -> gtk::SignalListItemFactory {
let factory = gtk::SignalListItemFactory::new();
- factory.connect_setup(clone!(@weak profile_row => move |_, list_item| {
+ let warning_tooltip_text = warning_tooltip_text.to_string();
+ factory.connect_setup(clone!(@weak row => move |_, list_item| {
let list_item = list_item.downcast_ref::().unwrap();
let item_row = ItemRow::new();
- item_row.set_warning_tooltip_text(gettext("This format is experimental and unsupported."));
+ item_row.set_warning_tooltip_text(warning_tooltip_text.as_str());
list_item.set_child(Some(&item_row));
}));
- factory.connect_bind(clone!(@weak profile_row => move |_, list_item| {
+ factory.connect_bind(clone!(@weak row => move |_, list_item| {
let list_item = list_item.downcast_ref::().unwrap();
let item_row = list_item.child().unwrap().downcast::().unwrap();
- let item = list_item.item().unwrap();
- let profile = profile_from_obj(&item);
-
- item_row.set_shows_warning_icon(profile.is_some_and(|profile| profile.is_experimental()));
- item_row.set_title(
- profile.map_or_else(|| gettext("None"), |profile| profile.name().to_string()),
- );
-
// Only show the selected icon when it is inside the given row's popover. This assumes that
// the parent of the given row is not a popover, so we can tell which is which.
if item_row.ancestor(gtk::Popover::static_type()).is_some() {
- debug_assert!(profile_row.ancestor(gtk::Popover::static_type()).is_none());
+ debug_assert!(row.ancestor(gtk::Popover::static_type()).is_none());
item_row.set_shows_selected_icon(true);
unsafe {
list_item.set_data(
- PROFILE_ROW_SELECTED_ITEM_NOTIFY_HANDLER_ID_KEY,
- profile_row.connect_selected_item_notify(
- clone!(@weak list_item => move |profile_row| {
- update_item_row_is_selected(profile_row, &list_item);
- }),
- ),
+ ROW_SELECTED_ITEM_NOTIFY_HANDLER_ID_KEY,
+ row.connect_selected_item_notify(clone!(@weak list_item => move |row| {
+ update_item_row_is_selected(row, &list_item);
+ })),
);
}
- update_item_row_is_selected(&profile_row, list_item);
+ update_item_row_is_selected(&row, list_item);
} else {
item_row.set_shows_selected_icon(false);
}
+
+ bind_cb(list_item);
}));
- factory.connect_unbind(clone!(@weak profile_row => move |_, list_item| {
+ factory.connect_unbind(clone!(@weak row => move |_, list_item| {
let list_item = list_item.downcast_ref::().unwrap();
unsafe {
- if let Some(handler_id) =
- list_item.steal_data(PROFILE_ROW_SELECTED_ITEM_NOTIFY_HANDLER_ID_KEY)
+ if let Some(handler_id) = list_item.steal_data(ROW_SELECTED_ITEM_NOTIFY_HANDLER_ID_KEY)
{
- profile_row.disconnect(handler_id);
+ row.disconnect(handler_id);
}
}
+
+ unbind_cb(list_item);
}));
factory
@@ -284,6 +436,19 @@ fn update_item_row_is_selected(row: &adw::ComboRow, list_item: >k::ListItem) {
item_row.set_is_selected(row.selected_item() == list_item.item());
}
+fn update_item_row_shows_warning_icon(settings: &Settings, list_item: >k::ListItem) {
+ let item_row = list_item.child().unwrap().downcast::().unwrap();
+ let item = list_item.item().unwrap();
+
+ let enum_list_item = item.downcast_ref::().unwrap();
+
+ let framerate_option = unsafe { FramerateOption::from_glib(enum_list_item.value()) };
+
+ item_row.set_shows_warning_icon(settings.profile().is_some_and(|profile| {
+ framerate_option.to_framerate() > profile.suggested_max_framerate()
+ }));
+}
+
/// Returns `Some` if the object is a `Profile`, otherwise `None`, if the object is a `NoneProfile`.
fn profile_from_obj(obj: &glib::Object) -> Option<&Profile> {
if let Some(profile) = obj.downcast_ref::() {
@@ -315,3 +480,64 @@ fn display_path(path: &Path) -> String {
path_display
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn framerate_option() {
+ assert_eq!(
+ FramerateOption::from_framerate(Framerate::from_integer(5)),
+ FramerateOption::_10
+ );
+ assert_eq!(
+ FramerateOption::from_framerate(Framerate::from_integer(10)),
+ FramerateOption::_10
+ );
+ assert_eq!(
+ FramerateOption::from_framerate(Framerate::from_integer(20)),
+ FramerateOption::_20
+ );
+ assert_eq!(
+ FramerateOption::from_framerate(Framerate::approximate_float(23.976).unwrap()),
+ FramerateOption::_23_976
+ );
+ assert_eq!(
+ FramerateOption::from_framerate(Framerate::from_integer(24)),
+ FramerateOption::_24
+ );
+ assert_eq!(
+ FramerateOption::from_framerate(Framerate::from_integer(25)),
+ FramerateOption::_25
+ );
+ assert_eq!(
+ FramerateOption::from_framerate(Framerate::approximate_float(29.97).unwrap()),
+ FramerateOption::_29_97
+ );
+ assert_eq!(
+ FramerateOption::from_framerate(Framerate::from_integer(30)),
+ FramerateOption::_30
+ );
+ assert_eq!(
+ FramerateOption::from_framerate(Framerate::from_integer(48)),
+ FramerateOption::_48
+ );
+ assert_eq!(
+ FramerateOption::from_framerate(Framerate::from_integer(50)),
+ FramerateOption::_50
+ );
+ assert_eq!(
+ FramerateOption::from_framerate(Framerate::approximate_float(59.94).unwrap()),
+ FramerateOption::_59_94
+ );
+ assert_eq!(
+ FramerateOption::from_framerate(Framerate::from_integer(60)),
+ FramerateOption::_60
+ );
+ assert_eq!(
+ FramerateOption::from_framerate(Framerate::from_integer(120)),
+ FramerateOption::_60
+ );
+ }
+}
diff --git a/src/profile.rs b/src/profile.rs
index 65193d71..add2c322 100644
--- a/src/profile.rs
+++ b/src/profile.rs
@@ -7,7 +7,9 @@ use gtk::{
use once_cell::sync::OnceCell as OnceLock;
use serde::Deserialize;
-const DEFAULT_SUGGESTED_MAX_FRAMERATE: u32 = 60;
+use crate::pipeline::Framerate;
+
+const DEFAULT_SUGGESTED_MAX_FRAMERATE: Framerate = Framerate::new_raw(30, 1);
const MAX_THREAD_COUNT: u32 = 64;
#[derive(Debug, Deserialize)]
@@ -23,7 +25,7 @@ struct ProfileData {
is_experimental: bool,
name: String,
#[serde(rename = "suggested-max-fps")]
- suggested_max_framerate: Option,
+ suggested_max_framerate: Option,
#[serde(rename = "extension")]
file_extension: String,
#[serde(rename = "videoenc")]
@@ -113,10 +115,11 @@ impl Profile {
self.data().audioenc_bin_str.is_some()
}
- pub fn suggested_max_framerate(&self) -> u32 {
- self.data()
- .suggested_max_framerate
- .unwrap_or(DEFAULT_SUGGESTED_MAX_FRAMERATE)
+ pub fn suggested_max_framerate(&self) -> Framerate {
+ self.data().suggested_max_framerate.map_or_else(
+ || DEFAULT_SUGGESTED_MAX_FRAMERATE,
+ |raw| Framerate::approximate_float(raw).unwrap(),
+ )
}
pub fn is_experimental(&self) -> bool {
@@ -268,7 +271,10 @@ mod tests {
assert!(!profile.name().is_empty());
assert!(!profile.file_extension().is_empty());
- assert_ne!(profile.suggested_max_framerate(), 0);
+ assert_ne!(
+ profile.suggested_max_framerate(),
+ Framerate::from_integer(0)
+ );
assert!(
unique.insert(profile.id().to_string()),
diff --git a/src/recording.rs b/src/recording.rs
index ef8379b1..c5120121 100644
--- a/src/recording.rs
+++ b/src/recording.rs
@@ -198,7 +198,7 @@ impl Recording {
let file_path = new_recording_path(&settings.saving_location(), profile.file_extension());
let mut pipeline_builder = PipelineBuilder::new(
&file_path,
- settings.video_framerate(),
+ settings.framerate(),
profile.clone(),
fd,
streams.clone(),
diff --git a/src/settings.rs b/src/settings.rs
index 56bdfa6d..3bc0e988 100644
--- a/src/settings.rs
+++ b/src/settings.rs
@@ -9,6 +9,7 @@ use gtk::{gio, glib};
use crate::{
area_selector::{Selection, SelectionContext},
config::APP_ID,
+ pipeline::Framerate,
profile::Profile,
};
@@ -20,6 +21,7 @@ use crate::{
ret_type = "SelectionContext"
)]
#[gen_settings_skip(key_name = "saving-location")]
+#[gen_settings_skip(key_name = "framerate")]
#[gen_settings_skip(key_name = "record-delay")]
#[gen_settings_skip(key_name = "profile-id")]
pub struct Settings;
@@ -84,6 +86,26 @@ impl Settings {
})
}
+ pub fn framerate(&self) -> Framerate {
+ self.0.get::<(i32, i32)>("framerate").into()
+ }
+
+ pub fn set_framerate(&self, framerate: Framerate) {
+ self.0
+ .set("framerate", (framerate.numer(), framerate.denom()))
+ .unwrap();
+ }
+
+ pub fn connect_framerate_changed(
+ &self,
+ f: impl Fn(&Self) + 'static,
+ ) -> gio::glib::SignalHandlerId {
+ self.0
+ .connect_changed(Some("framerate"), move |settings, _| {
+ f(&Self(settings.clone()));
+ })
+ }
+
pub fn record_delay(&self) -> Duration {
Duration::from_secs(self.0.get::("record-delay") as u64)
}
diff --git a/src/window/mod.rs b/src/window/mod.rs
index a97b5aa9..a94955ea 100644
--- a/src/window/mod.rs
+++ b/src/window/mod.rs
@@ -18,6 +18,7 @@ use crate::{
config::PROFILE,
format_time,
help::Help,
+ preferences_dialog::FramerateOption,
recording::{NoProfileError, Recording, RecordingState},
settings::CaptureMode,
Application,
@@ -517,10 +518,10 @@ impl Window {
let profile_text = settings
.profile()
.map_or_else(|| gettext("None"), |profile| profile.name().to_string());
- let fps_text = settings.video_framerate().to_string();
+ let framerate_option = FramerateOption::from_framerate(settings.framerate());
imp.title
- .set_subtitle(&format!("{} • {} FPS", profile_text, fps_text));
+ .set_subtitle(&format!("{} • {}", profile_text, framerate_option));
}
fn update_audio_actions(&self) {
@@ -559,7 +560,7 @@ impl Window {
obj.update_subtitle_label();
}));
- settings.connect_video_framerate_changed(clone!(@weak self as obj => move |_| {
+ settings.connect_framerate_changed(clone!(@weak self as obj => move |_| {
obj.update_subtitle_label();
}));