From e34dcf137288c76a5d20effdcd9b41f54d75526f Mon Sep 17 00:00:00 2001 From: Josh Megnauth Date: Mon, 24 Feb 2025 00:47:32 -0500 Subject: [PATCH] Respect military time config from time applet Closes: #775 --- src/app.rs | 40 +++++++++++------ src/config.rs | 22 +++++++++- src/dialog.rs | 9 ++-- src/tab.rs | 118 ++++++++++++++++++++++++++++++-------------------- 4 files changed, 125 insertions(+), 64 deletions(-) diff --git a/src/app.rs b/src/app.rs index 1aca306..1460398 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,6 +1,7 @@ // Copyright 2023 System76 // SPDX-License-Identifier: GPL-3.0-only +use cosmic::iced::mouse::Event::CursorMoved; #[cfg(feature = "wayland")] use cosmic::iced::{ event::wayland::{Event as WaylandEvent, OutputEvent, OverlapNotifyEvent}, @@ -55,7 +56,6 @@ use std::{ sync::{Arc, Mutex}, time::{self, Instant}, }; -use cosmic::iced::mouse::Event::CursorMoved; use tokio::sync::mpsc; use trash::TrashItem; #[cfg(feature = "wayland")] @@ -552,7 +552,7 @@ pub struct App { tab_dnd_hover: Option<(Entity, Instant)>, nav_drag_id: DragId, tab_drag_id: DragId, - auto_scroll_speed: Option + auto_scroll_speed: Option, } impl App { @@ -1458,9 +1458,14 @@ impl App { let mut children = Vec::with_capacity(1); let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); + let military_time = self.config.tab.military_time; match kind { PreviewKind::Custom(PreviewItem(item)) => { - children.push(item.preview_view(Some(&self.mime_app_cache), IconSizes::default())); + children.push(item.preview_view( + Some(&self.mime_app_cache), + IconSizes::default(), + military_time, + )); } PreviewKind::Location(location) => { if let Some(tab) = self.tab_model.data::(entity) { @@ -1470,6 +1475,7 @@ impl App { children.push(item.preview_view( Some(&self.mime_app_cache), tab.config.icon_sizes, + military_time, )); // Only show one property view to avoid issues like hangs when generating // preview images on thousands of files @@ -1487,6 +1493,7 @@ impl App { children.push(item.preview_view( Some(&self.mime_app_cache), tab.config.icon_sizes, + military_time, )); // Only show one property view to avoid issues like hangs when generating // preview images on thousands of files @@ -1498,6 +1505,7 @@ impl App { children.push(item.preview_view( Some(&self.mime_app_cache), tab.config.icon_sizes, + military_time, )); } } @@ -1934,7 +1942,10 @@ impl Application for App { } Message::CursorMoved(pos) => { let entity = self.tab_model.active(); - return self.update(Message::TabMessage(Some(entity), tab::Message::CursorMoved(pos))); + return self.update(Message::TabMessage( + Some(entity), + tab::Message::CursorMoved(pos), + )); } Message::Cut(entity_opt) => { let paths = self.selected_paths(entity_opt); @@ -2800,7 +2811,10 @@ impl Application for App { } Message::ScrollTab(scroll_speed) => { let entity = self.tab_model.active(); - return self.update(Message::TabMessage(Some(entity), tab::Message::ScrollTab((scroll_speed as f32) / 10.0))); + return self.update(Message::TabMessage( + Some(entity), + tab::Message::ScrollTab((scroll_speed as f32) / 10.0), + )); } Message::SearchActivate => { return if self.search_get().is_none() { @@ -2944,11 +2958,9 @@ impl Application for App { // further resolution isn't necessary if let Some(scroll_speed_float) = scroll_speed { self.auto_scroll_speed = Some((scroll_speed_float * 10.0) as i16); - } - else { + } else { self.auto_scroll_speed = None; } - } tab::Command::ChangeLocation(tab_title, tab_path, selection_paths) => { self.activate_nav_model_location(&tab_path); @@ -4160,15 +4172,16 @@ impl Application for App { apply_to_all, tx, } => { + let military_time = self.config.tab.military_time; let dialog = widget::dialog() .title(fl!("replace-title", filename = to.name.as_str())) .body(fl!("replace-warning-operation")) .control( - to.replace_view(fl!("original-file"), IconSizes::default()) + to.replace_view(fl!("original-file"), IconSizes::default(), military_time) .map(|x| Message::TabMessage(None, x)), ) .control( - from.replace_view(fl!("replace-with"), IconSizes::default()) + from.replace_view(fl!("replace-with"), IconSizes::default(), military_time) .map(|x| Message::TabMessage(None, x)), ) .primary_action(widget::button::suggested(fl!("replace")).on_press( @@ -4561,7 +4574,7 @@ impl Application for App { } _ => None, } - }, + } Event::Mouse(CursorMoved { position: pos }) => Some(Message::CursorMoved(pos)), _ => None, }), @@ -4735,8 +4748,9 @@ impl Application for App { if let Some(scroll_speed) = self.auto_scroll_speed { subscriptions.push( - iced::time::every(time::Duration::from_millis(10)).with(scroll_speed) - .map(|(scroll_speed, _)| Message::ScrollTab(scroll_speed)) + iced::time::every(time::Duration::from_millis(10)) + .with(scroll_speed) + .map(|(scroll_speed, _)| Message::ScrollTab(scroll_speed)), ); } diff --git a/src/config.rs b/src/config.rs index 9b6aa0c..e071a2b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,7 +3,7 @@ use std::{any::TypeId, num::NonZeroU16, path::PathBuf}; use cosmic::{ - cosmic_config::{self, cosmic_config_derive::CosmicConfigEntry, CosmicConfigEntry}, + cosmic_config::{self, cosmic_config_derive::CosmicConfigEntry, ConfigGet, CosmicConfigEntry}, iced::Subscription, theme, Application, }; @@ -197,6 +197,10 @@ pub struct TabConfig { pub show_hidden: bool, /// Icon zoom pub icon_sizes: IconSizes, + #[serde(skip, default = "military_time_enabled")] + /// 24 hour clock; this is neither serialized nor deserialized because we use the user's global + /// preference rather than save it + pub military_time: bool, } impl Default for TabConfig { @@ -206,6 +210,22 @@ impl Default for TabConfig { folders_first: true, show_hidden: false, icon_sizes: IconSizes::default(), + military_time: military_time_enabled(), + } + } +} + +/// Return whether the user enabled military time via the Time applet. +fn military_time_enabled() -> bool { + // Borrowed from COSMIC Greeter + match cosmic_config::Config::new("com.system76.CosmicAppletTime", 1) { + Ok(config_handler) => config_handler.get("military_time").unwrap_or_default(), + Err(err) => { + log::error!( + "failed to create CosmicAppletTime config handler: {:?}", + err + ); + false } } } diff --git a/src/dialog.rs b/src/dialog.rs index dc10905..b78930c 100644 --- a/src/dialog.rs +++ b/src/dialog.rs @@ -472,16 +472,17 @@ impl App { } fn preview<'a>(&'a self, kind: &'a PreviewKind) -> Element<'a, tab::Message> { + let military_time = self.tab.config.military_time; let mut children = Vec::with_capacity(1); match kind { PreviewKind::Custom(PreviewItem(item)) => { - children.push(item.preview_view(None, IconSizes::default())); + children.push(item.preview_view(None, IconSizes::default(), military_time)); } PreviewKind::Location(location) => { if let Some(items) = self.tab.items_opt() { for item in items.iter() { if item.location_opt.as_ref() == Some(location) { - children.push(item.preview_view(None, self.tab.config.icon_sizes)); + children.push(item.preview_view(None, self.tab.config.icon_sizes, military_time)); // Only show one property view to avoid issues like hangs when generating // preview images on thousands of files break; @@ -493,7 +494,7 @@ impl App { if let Some(items) = self.tab.items_opt() { for item in items.iter() { if item.selected { - children.push(item.preview_view(None, self.tab.config.icon_sizes)); + children.push(item.preview_view(None, self.tab.config.icon_sizes, military_time)); // Only show one property view to avoid issues like hangs when generating // preview images on thousands of files break; @@ -501,7 +502,7 @@ impl App { } if children.is_empty() { if let Some(item) = &self.tab.parent_item_opt { - children.push(item.preview_view(None, self.tab.config.icon_sizes)); + children.push(item.preview_view(None, self.tab.config.icon_sizes, military_time)); } } } diff --git a/src/tab.rs b/src/tab.rs index 15f6e94..4d55140 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -87,7 +87,9 @@ const DRAG_SCROLL_DISTANCE: f32 = 15.0; //TODO: adjust for locales? const DATE_TIME_FORMAT: &str = "%b %-d, %-Y, %-I:%M %p"; +const DATE_TIME_FORMAT_MILITARY: &str = "%b %-d, %-Y, %-H:%M %p"; const TIME_FORMAT: &str = "%-I:%M %p"; +const TIME_FORMAT_MILITARY: &str = "%-H:%M %p"; static SPECIAL_DIRS: Lazy> = Lazy::new(|| { let mut special_dirs = HashMap::new(); if let Some(dir) = dirs::document_dir() { @@ -380,10 +382,13 @@ fn format_permissions(metadata: &Metadata, owner: PermissionOwner) -> String { } } -struct FormatTime(SystemTime); +struct FormatTime { + pub time: SystemTime, + pub military_time: bool, +} impl FormatTime { - fn from_secs(secs: i64) -> Option { + fn from_secs(secs: i64, military_time: bool) -> Option { // This looks convoluted because we need to ensure the units match up let secs: u64 = secs.try_into().ok()?; let now = SystemTime::now(); @@ -393,31 +398,51 @@ impl FormatTime { .ok() .and_then(|now_secs| now_secs.checked_sub(secs)) .map(Duration::from_secs)?; - now.checked_add(filetime_diff).map(FormatTime) + now.checked_add(filetime_diff).map(|time| Self { + time, + military_time, + }) } } impl Display for FormatTime { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let date_time = chrono::DateTime::::from(self.0); + let date_time = chrono::DateTime::::from(self.time); let now = chrono::Local::now(); if date_time.date_naive() == now.date_naive() { write!( f, "{}, {}", fl!("today"), - date_time.format_localized(TIME_FORMAT, *LANGUAGE_CHRONO) + date_time.format_localized( + if self.military_time { + TIME_FORMAT_MILITARY + } else { + TIME_FORMAT + }, + *LANGUAGE_CHRONO + ) ) } else { date_time - .format_localized(DATE_TIME_FORMAT, *LANGUAGE_CHRONO) + .format_localized( + if self.military_time { + DATE_TIME_FORMAT_MILITARY + } else { + DATE_TIME_FORMAT + }, + *LANGUAGE_CHRONO, + ) .fmt(f) } } } -fn format_time(time: SystemTime) -> FormatTime { - FormatTime(time) +const fn format_time(time: SystemTime, military_time: bool) -> FormatTime { + FormatTime { + time, + military_time, + } } #[cfg(not(target_os = "windows"))] @@ -1497,6 +1522,7 @@ impl Item { &'a self, mime_app_cache_opt: Option<&'a mime_app::MimeAppCache>, sizes: IconSizes, + military_time: bool, ) -> Element<'a, Message> { let cosmic_theme::Spacing { space_xxxs, @@ -1558,21 +1584,21 @@ impl Item { if let Ok(time) = metadata.created() { details = details.push(widget::text::body(fl!( "item-created", - created = format_time(time).to_string() + created = format_time(time, military_time).to_string() ))); } if let Ok(time) = metadata.modified() { details = details.push(widget::text::body(fl!( "item-modified", - modified = format_time(time).to_string() + modified = format_time(time, military_time).to_string() ))); } if let Ok(time) = metadata.accessed() { details = details.push(widget::text::body(fl!( "item-accessed", - accessed = format_time(time).to_string() + accessed = format_time(time, military_time).to_string() ))); } @@ -1638,7 +1664,12 @@ impl Item { column.into() } - pub fn replace_view(&self, heading: String, sizes: IconSizes) -> Element<'_, Message> { + pub fn replace_view( + &self, + heading: String, + sizes: IconSizes, + military_time: bool, + ) -> Element<'_, Message> { let cosmic_theme::Spacing { space_xxxs, .. } = theme::active().cosmic().spacing; let mut row = widget::row().spacing(space_xxxs); @@ -1662,7 +1693,7 @@ impl Item { if let Ok(time) = metadata.modified() { column = column.push(widget::text::body(format!( "Last modified: {}", - format_time(time) + format_time(time, military_time) ))); } } @@ -1873,7 +1904,7 @@ impl Tab { viewport_rect: None, virtual_cursor_offset: None, last_scroll_position: None, - last_scroll_offset: None + last_scroll_offset: None, } } @@ -2228,7 +2259,7 @@ impl Tab { // if our mouse is above the scrollable viewport, we want to scroll up let drag_start_point = Point { x: viewport.x, - y: viewport.y + y: viewport.y, }; // diff_y should be NEGATIVE here when close to y=0 (above the MouseArea) // and positive when below the viewport @@ -2241,39 +2272,30 @@ impl Tab { 0.0 }; - - commands.push(Command::AutoScroll(Some(scroll_y))); - } - else { + } else { if let Some(last_scroll_offset) = self.last_scroll_offset { if let Some(last_scroll_position) = self.last_scroll_position { self.virtual_cursor_offset = Some(Point { x: 0.0, - y: (pos.y - last_scroll_position.y + last_scroll_offset.y) + y: (pos.y - last_scroll_position.y + + last_scroll_offset.y), }); } - - } - else { + } else { if let Some(last_scroll_position) = self.last_scroll_position { self.virtual_cursor_offset = Some(Point { x: 0.0, - y: (pos.y - last_scroll_position.y) - }); - } - else { - self.virtual_cursor_offset = Some(Point { - x: 0.0, - y: 0.0 + y: (pos.y - last_scroll_position.y), }); + } else { + self.virtual_cursor_offset = Some(Point { x: 0.0, y: 0.0 }); } } commands.push(Command::AutoScroll(None)); } - } - else { + } else { // reset our virtual cursor offset when we're back in bounds self.virtual_cursor_offset = None; self.last_scroll_position = Some(pos); @@ -2292,13 +2314,11 @@ impl Tab { x: viewport.x - scroll_pos.x, y: viewport.y - scroll_pos.y, width: viewport.width, - height: viewport.height + height: viewport.height, }); - } - else { + } else { self.viewport_rect = Some(viewport); } - } Message::DragEnd(_) => { self.clicked = None; @@ -2947,7 +2967,7 @@ impl Tab { Message::ScrollTab(scroll_speed) => { let mut new_offset = Point { x: 0.0, - y: scroll_speed + y: scroll_speed, }; if let Some(virtual_cursor_offset) = self.virtual_cursor_offset { @@ -2961,17 +2981,20 @@ impl Tab { if let Some(global_cursor_position) = self.global_cursor_position { new_offset.x = global_cursor_position.x - last_scroll_position.x; } - } self.virtual_cursor_offset = Some(new_offset); self.last_scroll_offset = Some(new_offset); commands.push(Command::Iced( - scrollable::scroll_by(self.scrollable_id.clone(), AbsoluteOffset { - x: 0.0, - y: scroll_speed - }).into(), + scrollable::scroll_by( + self.scrollable_id.clone(), + AbsoluteOffset { + x: 0.0, + y: scroll_speed, + }, + ) + .into(), )); } Message::ScrollToFocus => { @@ -4311,14 +4334,17 @@ impl Tab { y += 1; } + let military_time = self.config.military_time; let modified_text = match &item.metadata { ItemMetadata::Path { metadata, .. } => match metadata.modified() { - Ok(time) => format_time(time).to_string(), + Ok(time) => format_time(time, military_time).to_string(), Err(_) => String::new(), }, - ItemMetadata::Trash { entry, .. } => FormatTime::from_secs(entry.time_deleted) - .map(|t| t.to_string()) - .unwrap_or_default(), + ItemMetadata::Trash { entry, .. } => { + FormatTime::from_secs(entry.time_deleted, military_time) + .map(|t| t.to_string()) + .unwrap_or_default() + } _ => String::new(), };