@@ -16,9 +16,9 @@ use crate::analytics;
1616use crate :: computed:: { ComputedChannel , ComputedChannelLibrary , FormulaEditorState } ;
1717use crate :: parsers:: { Aim , EcuMaster , EcuType , Haltech , Link , Parseable , RomRaider , Speeduino } ;
1818use crate :: state:: {
19- ActiveTool , CacheKey , FontScale , LoadResult , LoadedFile , LoadingState , ScatterPlotConfig ,
20- ScatterPlotState , SelectedChannel , Tab , ToastType , CHART_COLORS , COLORBLIND_COLORS ,
21- MAX_CHANNELS ,
19+ ActivePanel , ActiveTool , CacheKey , FontScale , LoadResult , LoadedFile , LoadingState ,
20+ ScatterPlotConfig , ScatterPlotState , SelectedChannel , Tab , ToastType , CHART_COLORS ,
21+ COLORBLIND_COLORS , MAX_CHANNELS ,
2222} ;
2323use crate :: units:: UnitPreferences ;
2424use crate :: updater:: { DownloadResult , UpdateCheckResult , UpdateState } ;
@@ -92,6 +92,9 @@ pub struct UltraLogApp {
9292 // === Tool/View Selection ===
9393 /// Currently active tool/view
9494 pub ( crate ) active_tool : ActiveTool ,
95+ // === Panel Selection ===
96+ /// Currently active side panel (activity bar selection)
97+ pub ( crate ) active_panel : ActivePanel ,
9598 // === Tab Management ===
9699 /// Open tabs (one per log file being viewed)
97100 pub ( crate ) tabs : Vec < Tab > ,
@@ -161,6 +164,7 @@ impl Default for UltraLogApp {
161164 norm_editor_custom_source : String :: new ( ) ,
162165 norm_editor_custom_target : String :: new ( ) ,
163166 active_tool : ActiveTool :: default ( ) ,
167+ active_panel : ActivePanel :: default ( ) ,
164168 tabs : Vec :: new ( ) ,
165169 active_tab : None ,
166170 update_state : UpdateState :: default ( ) ,
@@ -1233,18 +1237,78 @@ impl UltraLogApp {
12331237
12341238 /// Handle keyboard shortcuts
12351239 fn handle_keyboard_shortcuts ( & mut self , ctx : & egui:: Context ) {
1236- // Only handle shortcuts when we have data loaded
1237- if self . files . is_empty ( ) || self . get_selected_channels ( ) . is_empty ( ) {
1238- return ;
1239- }
1240-
12411240 // Don't handle shortcuts when a text field or other widget has keyboard focus
12421241 if ctx. memory ( |m| m. focused ( ) . is_some ( ) ) {
12431242 return ;
12441243 }
12451244
1246- // Spacebar to toggle play/pause
12471245 ctx. input ( |i| {
1246+ let cmd = i. modifiers . command ;
1247+ let shift = i. modifiers . shift ;
1248+
1249+ // ⌘O - Open file
1250+ if cmd && i. key_pressed ( egui:: Key :: O ) {
1251+ if let Some ( path) = rfd:: FileDialog :: new ( )
1252+ . add_filter ( "Log Files" , crate :: state:: SUPPORTED_EXTENSIONS )
1253+ . pick_file ( )
1254+ {
1255+ self . start_loading_file ( path) ;
1256+ }
1257+ return ;
1258+ }
1259+
1260+ // ⌘W - Close current tab
1261+ if cmd && i. key_pressed ( egui:: Key :: W ) {
1262+ if let Some ( tab_idx) = self . active_tab {
1263+ self . close_tab ( tab_idx) ;
1264+ }
1265+ return ;
1266+ }
1267+
1268+ // ⌘, - Open Settings panel
1269+ if cmd && i. key_pressed ( egui:: Key :: Comma ) {
1270+ self . active_panel = crate :: state:: ActivePanel :: Settings ;
1271+ return ;
1272+ }
1273+
1274+ // ⌘1/2/3 - Switch tool modes
1275+ if cmd && !shift {
1276+ if i. key_pressed ( egui:: Key :: Num1 ) {
1277+ self . active_tool = crate :: state:: ActiveTool :: LogViewer ;
1278+ return ;
1279+ }
1280+ if i. key_pressed ( egui:: Key :: Num2 ) {
1281+ self . active_tool = crate :: state:: ActiveTool :: ScatterPlot ;
1282+ return ;
1283+ }
1284+ if i. key_pressed ( egui:: Key :: Num3 ) {
1285+ self . active_tool = crate :: state:: ActiveTool :: Histogram ;
1286+ return ;
1287+ }
1288+ }
1289+
1290+ // ⌘⇧F/C/T - Switch panels
1291+ if cmd && shift {
1292+ if i. key_pressed ( egui:: Key :: F ) {
1293+ self . active_panel = crate :: state:: ActivePanel :: Files ;
1294+ return ;
1295+ }
1296+ if i. key_pressed ( egui:: Key :: C ) {
1297+ self . active_panel = crate :: state:: ActivePanel :: Channels ;
1298+ return ;
1299+ }
1300+ if i. key_pressed ( egui:: Key :: T ) {
1301+ self . active_panel = crate :: state:: ActivePanel :: Tools ;
1302+ return ;
1303+ }
1304+ }
1305+
1306+ // Playback shortcuts (require file loaded with channels selected)
1307+ if self . files . is_empty ( ) || self . get_selected_channels ( ) . is_empty ( ) {
1308+ return ;
1309+ }
1310+
1311+ // Spacebar to toggle play/pause
12481312 if i. key_pressed ( egui:: Key :: Space ) {
12491313 self . is_playing = !self . is_playing ;
12501314 if self . is_playing {
@@ -1347,41 +1411,38 @@ impl eframe::App for UltraLogApp {
13471411 self . render_tool_switcher ( ui) ;
13481412 } ) ;
13491413
1350- // Panel background color (matches drop zone card)
1414+ // Panel background color
13511415 let panel_bg = egui:: Color32 :: from_rgb ( 45 , 45 , 45 ) ;
13521416 let panel_frame = egui:: Frame :: NONE
13531417 . fill ( panel_bg)
13541418 . inner_margin ( egui:: Margin :: symmetric ( 10 , 10 ) ) ;
13551419
1356- // Left sidebar panel (always visible)
1357- egui:: SidePanel :: left ( "files_panel" )
1358- . default_width ( 200.0 )
1420+ // Activity bar (far left, narrow icon strip)
1421+ let activity_bar_bg = egui:: Color32 :: from_rgb ( 35 , 35 , 35 ) ;
1422+ let activity_bar_frame = egui:: Frame :: NONE
1423+ . fill ( activity_bar_bg)
1424+ . inner_margin ( egui:: Margin :: symmetric ( 4 , 8 ) ) ;
1425+
1426+ egui:: SidePanel :: left ( "activity_bar" )
1427+ . exact_width ( crate :: ui:: activity_bar:: ACTIVITY_BAR_WIDTH )
1428+ . resizable ( false )
1429+ . frame ( activity_bar_frame)
1430+ . show ( ctx, |ui| {
1431+ self . render_activity_bar ( ui) ;
1432+ } ) ;
1433+
1434+ // Side panel (context-sensitive based on activity bar selection)
1435+ egui:: SidePanel :: left ( "side_panel" )
1436+ . default_width ( crate :: ui:: side_panel:: SIDE_PANEL_WIDTH )
1437+ . min_width ( crate :: ui:: side_panel:: SIDE_PANEL_MIN_WIDTH )
13591438 . resizable ( true )
13601439 . frame ( panel_frame)
13611440 . show ( ctx, |ui| {
1362- self . render_sidebar ( ui) ;
1441+ self . render_side_panel ( ui) ;
13631442 } ) ;
13641443
1365- // Right panel for channel selection (only in Log Viewer mode)
1366- if self . active_tool == ActiveTool :: LogViewer {
1367- egui:: SidePanel :: right ( "channels_panel" )
1368- . default_width ( 300.0 )
1369- . min_width ( 200.0 )
1370- . resizable ( true )
1371- . frame ( panel_frame)
1372- . show ( ctx, |ui| {
1373- self . render_channel_selection ( ui) ;
1374- } ) ;
1375- }
1376-
1377- // Bottom panel for timeline scrubber (Log Viewer and Histogram modes)
1378- let show_timeline = match self . active_tool {
1379- ActiveTool :: LogViewer => {
1380- self . get_time_range ( ) . is_some ( ) && !self . get_selected_channels ( ) . is_empty ( )
1381- }
1382- ActiveTool :: Histogram => self . get_time_range ( ) . is_some ( ) ,
1383- ActiveTool :: ScatterPlot => false ,
1384- } ;
1444+ // Bottom panel for timeline scrubber (always visible when file loaded)
1445+ let show_timeline = self . get_time_range ( ) . is_some ( ) ;
13851446
13861447 if show_timeline {
13871448 egui:: TopBottomPanel :: bottom ( "timeline_panel" )
0 commit comments