mirror of
https://gitlab.com/gabmus/envision.git
synced 2025-08-03 22:58:44 +00:00
feat: profile import/export; small refactor
This commit is contained in:
parent
14689e5358
commit
93e6c3cf9e
2 changed files with 274 additions and 66 deletions
|
@ -1,3 +1,6 @@
|
||||||
|
use std::fs::read_to_string;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
use super::alert::alert;
|
use super::alert::alert;
|
||||||
use super::devices_box::{DevicesBox, DevicesBoxMsg};
|
use super::devices_box::{DevicesBox, DevicesBoxMsg};
|
||||||
use super::install_wivrn_box::{InstallWivrnBox, InstallWivrnBoxInit, InstallWivrnBoxMsg};
|
use super::install_wivrn_box::{InstallWivrnBox, InstallWivrnBoxInit, InstallWivrnBoxMsg};
|
||||||
|
@ -6,9 +9,11 @@ use super::steam_launch_options_box::{SteamLaunchOptionsBox, SteamLaunchOptionsB
|
||||||
use super::steamvr_calibration_box::SteamVrCalibrationBox;
|
use super::steamvr_calibration_box::SteamVrCalibrationBox;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::dependencies::common::dep_pkexec;
|
use crate::dependencies::common::dep_pkexec;
|
||||||
use crate::file_utils::mount_has_nosuid;
|
use crate::file_utils::{get_writer, mount_has_nosuid};
|
||||||
use crate::gpu_profile::{get_amd_gpu_power_profile, GpuPowerProfile};
|
use crate::gpu_profile::{get_amd_gpu_power_profile, GpuPowerProfile};
|
||||||
|
use crate::paths::{get_data_dir, get_home_dir};
|
||||||
use crate::profile::{LighthouseDriver, Profile, XRServiceType};
|
use crate::profile::{LighthouseDriver, Profile, XRServiceType};
|
||||||
|
use crate::stateless_action;
|
||||||
use crate::steamvr_utils::chaperone_info_exists;
|
use crate::steamvr_utils::chaperone_info_exists;
|
||||||
use crate::ui::app::{
|
use crate::ui::app::{
|
||||||
AboutAction, BuildProfileAction, BuildProfileCleanAction, ConfigureWivrnAction,
|
AboutAction, BuildProfileAction, BuildProfileCleanAction, ConfigureWivrnAction,
|
||||||
|
@ -18,9 +23,11 @@ use crate::ui::profile_editor::ProfileEditorInit;
|
||||||
use crate::ui::steamvr_calibration_box::SteamVrCalibrationBoxMsg;
|
use crate::ui::steamvr_calibration_box::SteamVrCalibrationBoxMsg;
|
||||||
use crate::ui::util::{limit_dropdown_width, warning_heading};
|
use crate::ui::util::{limit_dropdown_width, warning_heading};
|
||||||
use crate::xr_devices::XRDevice;
|
use crate::xr_devices::XRDevice;
|
||||||
use gtk::prelude::*;
|
use adw::prelude::*;
|
||||||
|
use gtk::glib::clone;
|
||||||
|
use relm4::actions::{ActionGroupName, RelmAction, RelmActionGroup};
|
||||||
use relm4::adw::{prelude::MessageDialogExt, ResponseAppearance};
|
use relm4::adw::{prelude::MessageDialogExt, ResponseAppearance};
|
||||||
use relm4::prelude::*;
|
use relm4::{new_action_group, new_stateless_action, prelude::*};
|
||||||
use relm4::{ComponentParts, ComponentSender, SimpleComponent};
|
use relm4::{ComponentParts, ComponentSender, SimpleComponent};
|
||||||
|
|
||||||
#[tracker::track]
|
#[tracker::track]
|
||||||
|
@ -48,6 +55,10 @@ pub struct MainView {
|
||||||
steamvr_calibration_box: Controller<SteamVrCalibrationBox>,
|
steamvr_calibration_box: Controller<SteamVrCalibrationBox>,
|
||||||
#[tracker::do_not_track]
|
#[tracker::do_not_track]
|
||||||
root_win: gtk::Window,
|
root_win: gtk::Window,
|
||||||
|
#[tracker::do_not_track]
|
||||||
|
profile_delete_action: gtk::gio::SimpleAction,
|
||||||
|
#[tracker::do_not_track]
|
||||||
|
profile_export_action: gtk::gio::SimpleAction,
|
||||||
xrservice_ready: bool,
|
xrservice_ready: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,6 +80,9 @@ pub enum MainViewMsg {
|
||||||
SaveProfile(Profile),
|
SaveProfile(Profile),
|
||||||
UpdateDevices(Vec<XRDevice>),
|
UpdateDevices(Vec<XRDevice>),
|
||||||
UpdateXrServiceReady(bool),
|
UpdateXrServiceReady(bool),
|
||||||
|
ExportProfile,
|
||||||
|
ImportProfile,
|
||||||
|
OpenProfileEditor(Profile),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -119,12 +133,23 @@ impl SimpleComponent for MainView {
|
||||||
},
|
},
|
||||||
section! {
|
section! {
|
||||||
"_About" => AboutAction,
|
"_About" => AboutAction,
|
||||||
}
|
},
|
||||||
|
},
|
||||||
|
profile_actions_menu: {
|
||||||
|
section! {
|
||||||
|
"_New profile" => ProfileMenuNewAction,
|
||||||
|
"_Edit profile" => ProfileMenuEditAction,
|
||||||
|
"Du_plicate profile" => ProfileMenuDuplicateAction,
|
||||||
|
"_Delete profile" => ProfileMenuDeleteAction,
|
||||||
|
},
|
||||||
|
section! {
|
||||||
|
"_Import profile" => ProfileMenuImportAction,
|
||||||
|
"E_xport profile" => ProfileMenuExportAction,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
// TODO: refactor with adw.toolbarview
|
|
||||||
adw::ToolbarView {
|
adw::ToolbarView {
|
||||||
set_top_bar_style: adw::ToolbarStyle::Flat,
|
set_top_bar_style: adw::ToolbarStyle::Flat,
|
||||||
set_bottom_bar_style: adw::ToolbarStyle::Flat,
|
set_bottom_bar_style: adw::ToolbarStyle::Flat,
|
||||||
|
@ -137,6 +162,7 @@ impl SimpleComponent for MainView {
|
||||||
set_vexpand: false,
|
set_vexpand: false,
|
||||||
pack_end: menu_btn = >k::MenuButton {
|
pack_end: menu_btn = >k::MenuButton {
|
||||||
set_icon_name: "open-menu-symbolic",
|
set_icon_name: "open-menu-symbolic",
|
||||||
|
set_tooltip_text: Some("Menu"),
|
||||||
set_menu_model: Some(&app_menu),
|
set_menu_model: Some(&app_menu),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -390,42 +416,14 @@ impl SimpleComponent for MainView {
|
||||||
},
|
},
|
||||||
connect_realize => move |dd| {
|
connect_realize => move |dd| {
|
||||||
limit_dropdown_width(
|
limit_dropdown_width(
|
||||||
dd, match model.enable_debug_view {
|
dd,
|
||||||
true => 18,
|
);
|
||||||
false => -1,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
gtk::Button {
|
gtk::MenuButton {
|
||||||
set_icon_name: "document-edit-symbolic",
|
set_icon_name: "view-more-symbolic",
|
||||||
set_tooltip_text: Some("Edit Profile"),
|
set_tooltip_text: Some("Menu"),
|
||||||
connect_clicked[sender] => move |_| {
|
set_menu_model: Some(&profile_actions_menu),
|
||||||
sender.input(Self::Input::EditProfile);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
gtk::Button {
|
|
||||||
set_icon_name: "edit-copy-symbolic",
|
|
||||||
set_tooltip_text: Some("Duplicate Profile"),
|
|
||||||
connect_clicked[sender] => move |_| {
|
|
||||||
sender.input(Self::Input::DuplicateProfile);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
gtk::Button {
|
|
||||||
set_icon_name: "list-add-symbolic",
|
|
||||||
set_tooltip_text: Some("Create Profile"),
|
|
||||||
connect_clicked[sender] => move |_| {
|
|
||||||
sender.input(Self::Input::CreateProfile);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
gtk::Button {
|
|
||||||
set_icon_name: "edit-delete-symbolic",
|
|
||||||
add_css_class: "destructive-action",
|
|
||||||
set_tooltip_text: Some("Delete Profile"),
|
|
||||||
#[track = "model.changed(Self::selected_profile())"]
|
|
||||||
set_sensitive: model.selected_profile.editable,
|
|
||||||
connect_clicked[sender] => move |_| {
|
|
||||||
sender.input(Self::Input::DeleteProfile);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -469,16 +467,11 @@ impl SimpleComponent for MainView {
|
||||||
}
|
}
|
||||||
Self::Input::EnableDebugViewChanged(val) => {
|
Self::Input::EnableDebugViewChanged(val) => {
|
||||||
self.set_enable_debug_view(val);
|
self.set_enable_debug_view(val);
|
||||||
limit_dropdown_width(
|
|
||||||
self.profiles_dropdown.as_ref().unwrap(),
|
|
||||||
match val {
|
|
||||||
true => 18,
|
|
||||||
false => -1,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
Self::Input::UpdateSelectedProfile(prof) => {
|
Self::Input::UpdateSelectedProfile(prof) => {
|
||||||
self.set_selected_profile(prof.clone());
|
self.set_selected_profile(prof.clone());
|
||||||
|
self.profile_delete_action.set_enabled(prof.editable);
|
||||||
|
self.profile_export_action.set_enabled(prof.editable);
|
||||||
self.steamvr_calibration_box
|
self.steamvr_calibration_box
|
||||||
.sender()
|
.sender()
|
||||||
.emit(SteamVrCalibrationBoxMsg::SetVisible(
|
.emit(SteamVrCalibrationBoxMsg::SetVisible(
|
||||||
|
@ -511,7 +504,9 @@ impl SimpleComponent for MainView {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.clone()
|
.clone()
|
||||||
.set_selected(index);
|
.set_selected(index);
|
||||||
self.set_selected_profile(self.profiles.get(index as usize).unwrap().clone());
|
sender.input(Self::Input::UpdateSelectedProfile(
|
||||||
|
self.profiles.get(index as usize).unwrap().clone(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
Self::Input::ProfileSelected(position) => {
|
Self::Input::ProfileSelected(position) => {
|
||||||
sender
|
sender
|
||||||
|
@ -522,22 +517,15 @@ impl SimpleComponent for MainView {
|
||||||
}
|
}
|
||||||
Self::Input::EditProfile => {
|
Self::Input::EditProfile => {
|
||||||
if self.selected_profile.editable {
|
if self.selected_profile.editable {
|
||||||
self.create_profile_editor(sender, self.selected_profile.clone());
|
sender.input(Self::Input::OpenProfileEditor(
|
||||||
self.profile_editor
|
self.selected_profile.clone(),
|
||||||
.as_ref()
|
));
|
||||||
.unwrap()
|
|
||||||
.emit(ProfileEditorMsg::Present);
|
|
||||||
} else {
|
} else {
|
||||||
self.profile_not_editable_dialog.present();
|
self.profile_not_editable_dialog.present();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::Input::CreateProfile => {
|
Self::Input::CreateProfile => {
|
||||||
self.create_profile_editor(sender, Profile::default());
|
sender.input(Self::Input::OpenProfileEditor(Profile::default()));
|
||||||
self.profile_editor
|
|
||||||
.as_ref()
|
|
||||||
.unwrap()
|
|
||||||
.sender()
|
|
||||||
.emit(ProfileEditorMsg::Present);
|
|
||||||
}
|
}
|
||||||
Self::Input::DeleteProfile => {
|
Self::Input::DeleteProfile => {
|
||||||
self.profile_delete_confirm_dialog.present();
|
self.profile_delete_confirm_dialog.present();
|
||||||
|
@ -549,12 +537,9 @@ impl SimpleComponent for MainView {
|
||||||
}
|
}
|
||||||
Self::Input::DuplicateProfile => {
|
Self::Input::DuplicateProfile => {
|
||||||
if self.selected_profile.can_be_built {
|
if self.selected_profile.can_be_built {
|
||||||
self.create_profile_editor(sender, self.selected_profile.create_duplicate());
|
sender.input(Self::Input::OpenProfileEditor(
|
||||||
self.profile_editor
|
self.selected_profile.create_duplicate(),
|
||||||
.as_ref()
|
));
|
||||||
.unwrap()
|
|
||||||
.sender()
|
|
||||||
.emit(ProfileEditorMsg::Present);
|
|
||||||
} else {
|
} else {
|
||||||
alert(
|
alert(
|
||||||
"This profile cannot be duplicated",
|
"This profile cannot be duplicated",
|
||||||
|
@ -563,6 +548,112 @@ impl SimpleComponent for MainView {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Self::Input::ExportProfile => {
|
||||||
|
let prof = self.selected_profile.clone();
|
||||||
|
if !prof.editable {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let chooser = gtk::FileDialog::builder()
|
||||||
|
.modal(true)
|
||||||
|
.title("Choose a location for the exported profile")
|
||||||
|
.initial_name(&format!("{}.json", &prof.name))
|
||||||
|
.accept_label("Export")
|
||||||
|
.build();
|
||||||
|
let root_win = self.root_win.clone();
|
||||||
|
chooser.save(
|
||||||
|
Some(&self.root_win),
|
||||||
|
gtk::gio::Cancellable::NONE,
|
||||||
|
move |res| {
|
||||||
|
if let Ok(file) = res {
|
||||||
|
if let Some(path) = file.path() {
|
||||||
|
if let Ok(mut writer) = get_writer(&path) {
|
||||||
|
if let Ok(s) = serde_json::to_string_pretty(&prof) {
|
||||||
|
let prof_id = prof.uuid;
|
||||||
|
let s = s
|
||||||
|
.replace(
|
||||||
|
get_data_dir().to_string_lossy().as_ref(),
|
||||||
|
"@DATADIR@",
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
get_home_dir().to_string_lossy().as_ref(),
|
||||||
|
"@HOMEDIR@",
|
||||||
|
)
|
||||||
|
.replace(&prof_id, "@UUID@");
|
||||||
|
if writer.write_all(s.as_bytes()).is_ok() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
alert("Failed to export profile", None, Some(&root_win));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Self::Input::ImportProfile => {
|
||||||
|
let confirm_dialog = adw::AlertDialog::builder()
|
||||||
|
.heading("Importing profiles is dangerous!")
|
||||||
|
.body_use_markup(true)
|
||||||
|
.body("Importing a profile from an <b>untrusted source</b> is dangerous and could lead to <b>arbitrary code execution</b>.\n\nInspect the profile JSON manually before importing it and make sure it doesn't contain anything suspicious, including references to untrusted forks.")
|
||||||
|
.build();
|
||||||
|
confirm_dialog.add_response("no", "_Cancel");
|
||||||
|
confirm_dialog.add_response("yes", "I understand the risk, continue");
|
||||||
|
confirm_dialog.set_response_appearance("yes", ResponseAppearance::Destructive);
|
||||||
|
|
||||||
|
let root_win = self.root_win.clone();
|
||||||
|
let fd_sender = sender.clone();
|
||||||
|
confirm_dialog.connect_response(None, move |_, res| {
|
||||||
|
if res != "yes" {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let root_win = root_win.clone();
|
||||||
|
let fd_sender = fd_sender.clone();
|
||||||
|
let chooser = gtk::FileDialog::builder()
|
||||||
|
.modal(true)
|
||||||
|
.title("Choose a profile to import")
|
||||||
|
.accept_label("Import")
|
||||||
|
.filters(&{
|
||||||
|
let filter = gtk::gio::ListStore::builder()
|
||||||
|
.item_type(gtk::FileFilter::static_type())
|
||||||
|
.build();
|
||||||
|
filter.append(&{
|
||||||
|
let f = gtk::FileFilter::new();
|
||||||
|
f.add_mime_type("application/json");
|
||||||
|
f
|
||||||
|
});
|
||||||
|
filter
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
chooser.open(
|
||||||
|
Some(&root_win.clone()),
|
||||||
|
gtk::gio::Cancellable::NONE,
|
||||||
|
move |res| {
|
||||||
|
if let Ok(file) = res {
|
||||||
|
if let Some(path) = file.path() {
|
||||||
|
if let Ok(s) = read_to_string(path) {
|
||||||
|
let s = s
|
||||||
|
.replace(
|
||||||
|
"@DATADIR@",
|
||||||
|
get_data_dir().to_string_lossy().as_ref(),
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
"@HOMEDIR@",
|
||||||
|
get_home_dir().to_string_lossy().as_ref(),
|
||||||
|
)
|
||||||
|
.replace("@UUID@", &Profile::new_uuid());
|
||||||
|
if let Ok(nprof) = serde_json::from_str::<Profile>(&s) {
|
||||||
|
fd_sender.input(Self::Input::OpenProfileEditor(nprof));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
alert("Failed to import profile", None, Some(&root_win));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
confirm_dialog.present(Some(&self.root_win));
|
||||||
|
}
|
||||||
Self::Input::UpdateDevices(devs) => {
|
Self::Input::UpdateDevices(devs) => {
|
||||||
self.devices_box
|
self.devices_box
|
||||||
.sender()
|
.sender()
|
||||||
|
@ -571,6 +662,14 @@ impl SimpleComponent for MainView {
|
||||||
Self::Input::UpdateXrServiceReady(ready) => {
|
Self::Input::UpdateXrServiceReady(ready) => {
|
||||||
self.set_xrservice_ready(ready);
|
self.set_xrservice_ready(ready);
|
||||||
}
|
}
|
||||||
|
Self::Input::OpenProfileEditor(profile) => {
|
||||||
|
self.create_profile_editor(sender, profile);
|
||||||
|
self.profile_editor
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.sender()
|
||||||
|
.emit(ProfileEditorMsg::Present);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -633,6 +732,35 @@ impl SimpleComponent for MainView {
|
||||||
init.selected_profile.lighthouse_driver == LighthouseDriver::SteamVR,
|
init.selected_profile.lighthouse_driver == LighthouseDriver::SteamVR,
|
||||||
));
|
));
|
||||||
|
|
||||||
|
let mut actions = RelmActionGroup::<ProfileActionGroup>::new();
|
||||||
|
|
||||||
|
let profile_delete_action = {
|
||||||
|
let action = RelmAction::<ProfileMenuDeleteAction>::new_stateless(clone!(
|
||||||
|
#[strong]
|
||||||
|
sender,
|
||||||
|
move |_| {
|
||||||
|
sender.input(Self::Input::DeleteProfile);
|
||||||
|
}
|
||||||
|
));
|
||||||
|
let ret = action.gio_action().clone();
|
||||||
|
actions.add_action(action);
|
||||||
|
ret.set_enabled(false);
|
||||||
|
ret
|
||||||
|
};
|
||||||
|
let profile_export_action = {
|
||||||
|
let action = RelmAction::<ProfileMenuExportAction>::new_stateless(clone!(
|
||||||
|
#[strong]
|
||||||
|
sender,
|
||||||
|
move |_| {
|
||||||
|
sender.input(Self::Input::ExportProfile);
|
||||||
|
}
|
||||||
|
));
|
||||||
|
let ret = action.gio_action().clone();
|
||||||
|
actions.add_action(action);
|
||||||
|
ret.set_enabled(false);
|
||||||
|
ret
|
||||||
|
};
|
||||||
|
|
||||||
let mut model = Self {
|
let mut model = Self {
|
||||||
xrservice_active: false,
|
xrservice_active: false,
|
||||||
enable_debug_view: init.config.debug_view_enabled,
|
enable_debug_view: init.config.debug_view_enabled,
|
||||||
|
@ -653,12 +781,93 @@ impl SimpleComponent for MainView {
|
||||||
steamvr_calibration_box,
|
steamvr_calibration_box,
|
||||||
profile_editor: None,
|
profile_editor: None,
|
||||||
xrservice_ready: false,
|
xrservice_ready: false,
|
||||||
|
profile_delete_action,
|
||||||
|
profile_export_action,
|
||||||
tracker: 0,
|
tracker: 0,
|
||||||
};
|
};
|
||||||
let widgets = view_output!();
|
let widgets = view_output!();
|
||||||
|
|
||||||
model.profiles_dropdown = Some(widgets.profiles_dropdown.clone());
|
model.profiles_dropdown = Some(widgets.profiles_dropdown.clone());
|
||||||
|
|
||||||
|
stateless_action!(
|
||||||
|
actions,
|
||||||
|
ProfileMenuNewAction,
|
||||||
|
clone!(
|
||||||
|
#[strong]
|
||||||
|
sender,
|
||||||
|
move |_| {
|
||||||
|
sender.input(Self::Input::CreateProfile);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
stateless_action!(
|
||||||
|
actions,
|
||||||
|
ProfileMenuEditAction,
|
||||||
|
clone!(
|
||||||
|
#[strong]
|
||||||
|
sender,
|
||||||
|
move |_| {
|
||||||
|
sender.input(Self::Input::EditProfile);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
stateless_action!(
|
||||||
|
actions,
|
||||||
|
ProfileMenuDuplicateAction,
|
||||||
|
clone!(
|
||||||
|
#[strong]
|
||||||
|
sender,
|
||||||
|
move |_| {
|
||||||
|
sender.input(Self::Input::DuplicateProfile);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
stateless_action!(
|
||||||
|
actions,
|
||||||
|
ProfileMenuImportAction,
|
||||||
|
clone!(
|
||||||
|
#[strong]
|
||||||
|
sender,
|
||||||
|
move |_| {
|
||||||
|
sender.input(Self::Input::ImportProfile);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
root.insert_action_group(ProfileActionGroup::NAME, Some(&actions.into_action_group()));
|
||||||
|
|
||||||
ComponentParts { model, widgets }
|
ComponentParts { model, widgets }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
new_action_group!(ProfileActionGroup, "profile");
|
||||||
|
new_stateless_action!(
|
||||||
|
ProfileMenuNewAction,
|
||||||
|
ProfileActionGroup,
|
||||||
|
"profilemenunewaction"
|
||||||
|
);
|
||||||
|
new_stateless_action!(
|
||||||
|
ProfileMenuEditAction,
|
||||||
|
ProfileActionGroup,
|
||||||
|
"profilemenueditaction"
|
||||||
|
);
|
||||||
|
new_stateless_action!(
|
||||||
|
ProfileMenuDuplicateAction,
|
||||||
|
ProfileActionGroup,
|
||||||
|
"profilemenuduplicateaction"
|
||||||
|
);
|
||||||
|
new_stateless_action!(
|
||||||
|
ProfileMenuDeleteAction,
|
||||||
|
ProfileActionGroup,
|
||||||
|
"profilemenudeleteaction"
|
||||||
|
);
|
||||||
|
new_stateless_action!(
|
||||||
|
ProfileMenuImportAction,
|
||||||
|
ProfileActionGroup,
|
||||||
|
"profilemenuimportaction"
|
||||||
|
);
|
||||||
|
new_stateless_action!(
|
||||||
|
ProfileMenuExportAction,
|
||||||
|
ProfileActionGroup,
|
||||||
|
"profilemenuexportaction"
|
||||||
|
);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use gtk4::{gdk, gio, glib::clone, prelude::*};
|
use gtk4::{gdk, gio, glib::clone, prelude::*};
|
||||||
|
|
||||||
pub fn limit_dropdown_width(dd: >k4::DropDown, chars: i32) {
|
pub fn limit_dropdown_width(dd: >k4::DropDown) {
|
||||||
let mut dd_child = dd
|
let mut dd_child = dd
|
||||||
.first_child()
|
.first_child()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
@ -14,7 +14,6 @@ pub fn limit_dropdown_width(dd: >k4::DropDown, chars: i32) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if let Ok(label) = dd_child.clone().unwrap().downcast::<gtk4::Label>() {
|
if let Ok(label) = dd_child.clone().unwrap().downcast::<gtk4::Label>() {
|
||||||
label.set_max_width_chars(chars);
|
|
||||||
label.set_ellipsize(gtk4::pango::EllipsizeMode::End);
|
label.set_ellipsize(gtk4::pango::EllipsizeMode::End);
|
||||||
}
|
}
|
||||||
let nc = dd_child.unwrap().first_child().clone();
|
let nc = dd_child.unwrap().first_child().clone();
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue