Skip to content

Commit 01b067e

Browse files
committed
Add keyboard controls for more player actions
1 parent 57de03e commit 01b067e

File tree

5 files changed

+208
-116
lines changed

5 files changed

+208
-116
lines changed

docs/help/keyboard-controls.md

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,29 @@
11
# Keyboard controls
22
(On Mac, use `cmd` instead of `ctrl`.)
33

4-
* Main screen:
5-
* `space`: play/pause all or selection
6-
* `m`: mute/unmute all or selection
7-
* `tab`: select next grid/player
8-
* `shift-tab`: select previous grid/player
9-
* `escape`: deselect grid/player
10-
* Modal screens:
11-
* `tab`: select next text field
12-
* `shift-tab`: select previous text field
13-
* `escape`: close modal
14-
* `ctrl-z`: undo in current text field
15-
* `ctrl-y`, `ctrl-shift-z`: redo in current text field
4+
## Main screen
5+
| action | shortcut | uses selection |
6+
|-----------------------------|-------------------|----------------|
7+
| select next grid/player | tab | |
8+
| select previous grid/player | shift+tab | |
9+
| deselect grid/player | escape | |
10+
| play/pause | space | yes |
11+
| close grid/player | backspace, delete | yes |
12+
| add player in selected grid | N | yes |
13+
| mute/unmute | M | yes |
14+
| refresh | R | yes |
15+
| jump to random position | J | yes |
16+
| toggle synchronization | L | |
17+
| open playlist | ctrl+O | |
18+
| save playlist | ctrl+S | |
19+
| save playlist as new file | ctrl-shift+S | |
20+
| reset playlist | ctrl+N | |
21+
22+
## Modal screens
23+
| action | shortcut |
24+
|----------------------------|----------------------|
25+
| close modal | escape |
26+
| select next text field | tab |
27+
| select previous text field | shift+tab |
28+
| undo in current text field | ctrl+Z |
29+
| redo in current text field | ctrl-shift+Z, ctrl-Y |

src/gui/app.rs

Lines changed: 127 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -291,63 +291,46 @@ impl App {
291291
}
292292
}
293293

294-
fn generate_player_event_in_selection<T>(
294+
fn generate_event_in_selection(
295295
&mut self,
296-
message: impl FnOnce(T) -> player::Event,
297-
from_app: impl FnOnce(&mut Self) -> T,
298-
from_grid: impl FnOnce(&Grid) -> T,
299-
from_player: impl FnOnce(&Player) -> T,
300-
) {
296+
from_app: impl FnOnce(&Self) -> Option<Message>,
297+
from_grid: impl FnOnce(grid::Id, &Grid) -> Option<PaneEvent>,
298+
from_player: impl FnOnce(&Player) -> Option<player::Event>,
299+
) -> Task<Message> {
300+
self.generate_event_in_selection_maybe(from_app, from_grid, from_player)
301+
.unwrap_or_else(Task::none)
302+
}
303+
304+
fn generate_event_in_selection_maybe(
305+
&mut self,
306+
from_app: impl FnOnce(&Self) -> Option<Message>,
307+
from_grid: impl FnOnce(grid::Id, &Grid) -> Option<PaneEvent>,
308+
from_player: impl FnOnce(&Player) -> Option<player::Event>,
309+
) -> Option<Task<Message>> {
301310
match self.selection.pair() {
302311
Some((grid_id, player_id)) => {
303-
if let Some(grid) = self.grids.get_mut(grid_id) {
304-
match player_id {
305-
Some(player_id) => {
306-
if let Some(value) = grid.player(player_id).map(from_player) {
307-
let update = grid.update_player(
308-
player_id,
309-
message(value),
310-
&mut self.media,
311-
&self.config.playback,
312-
);
313-
if let Some(update) = update {
314-
self.handle_grid_update(update, grid_id);
315-
}
316-
}
317-
}
318-
None => {
319-
let event = message(from_grid(grid));
320-
grid.update_all_players(event.clone(), &mut self.media, &self.config.playback);
321-
self.synchronize_players(grid_id, None, event);
322-
}
312+
let grid = self.grids.get_mut(grid_id)?;
313+
match player_id {
314+
Some(player_id) => {
315+
let player = grid.player(player_id)?;
316+
let event = from_player(player)?;
317+
Some(self.update(Message::Player {
318+
grid_id,
319+
player_id,
320+
event,
321+
}))
322+
}
323+
None => {
324+
let event = from_grid(grid_id, grid)?;
325+
Some(self.update(Message::Pane { event }))
323326
}
324327
}
325328
}
326-
None => match message(from_app(self)) {
327-
player::Event::SetPause(paused) => {
328-
self.set_paused(paused);
329-
}
330-
player::Event::SetLoop(_) => {}
331-
player::Event::SetMute(muted) => {
332-
self.set_muted(muted);
333-
}
334-
player::Event::SetVolume(_) => {}
335-
player::Event::Seek(_) => {}
336-
player::Event::SeekRelative(_) => {}
337-
player::Event::SeekStop => {}
338-
player::Event::SeekRandom => {}
339-
player::Event::EndOfStream => {}
340-
player::Event::NewFrame => {}
341-
player::Event::MouseEnter => {}
342-
player::Event::MouseExit => {}
343-
player::Event::Refresh => {}
344-
player::Event::Close => {}
345-
player::Event::WindowFocused => {}
346-
player::Event::WindowUnfocused => {}
347-
},
329+
None => {
330+
let message = from_app(self)?;
331+
Some(self.update(message))
332+
}
348333
}
349-
350-
self.update_playback();
351334
}
352335

353336
fn set_muted(&mut self, muted: bool) {
@@ -567,12 +550,27 @@ impl App {
567550
let mut out = vec![];
568551

569552
for (grid_id, grid) in self.grids.iter() {
570-
out.push((*grid_id, None));
571553
let player_ids = grid.player_ids();
572-
if player_ids.len() > 1 {
573-
for player_id in player_ids {
574-
out.push((*grid_id, Some(player_id)));
554+
if player_ids.len() != 1 {
555+
out.push((*grid_id, None));
556+
}
557+
for player_id in player_ids {
558+
out.push((*grid_id, Some(player_id)));
559+
}
560+
}
561+
562+
out
563+
}
564+
565+
fn selectables_in_grid(&self) -> Vec<(grid::Id, player::Id)> {
566+
let mut out = vec![];
567+
568+
for (grid_id, grid) in self.grids.iter() {
569+
if self.selection.is_grid_selected(*grid_id) {
570+
for player_id in grid.player_ids() {
571+
out.push((*grid_id, player_id));
575572
}
573+
break;
576574
}
577575
}
578576

@@ -594,6 +592,7 @@ impl App {
594592
grid::Update::PlayerClosed => {
595593
self.playlist_dirty = true;
596594
self.update_playback();
595+
self.selection.ensure_valid_in_grid(self.selectables_in_grid());
597596

598597
if let Some(grid) = self.grids.get(grid_id) {
599598
if grid.is_idle() {
@@ -779,7 +778,7 @@ impl App {
779778
Task::none()
780779
}
781780
Message::KeyboardEvent(event) => {
782-
use iced::keyboard::{self, key, Key};
781+
use iced::keyboard::{self, key, Key, Modifiers};
783782

784783
match event {
785784
keyboard::Event::KeyPressed { key, modifiers, .. } => match key {
@@ -801,36 +800,88 @@ impl App {
801800
} else if !self.dragged_files.is_empty() {
802801
self.dragged_files.clear();
803802
} else if self.selection.is_any_selected() {
804-
self.selection.deselect();
803+
self.selection.clear();
805804
}
806805
Task::none()
807806
}
808807
Key::Named(key::Named::Space) => {
809808
if self.modals.is_empty() {
810-
self.generate_player_event_in_selection(
811-
player::Event::SetPause,
812-
|app| !app.config.playback.paused,
813-
|grid| !grid.all_paused().unwrap_or_default(),
814-
|player| !player.is_paused().unwrap_or_default(),
815-
);
809+
self.generate_event_in_selection(
810+
|app| Some(Message::SetPause(!app.config.playback.paused)),
811+
|grid_id, grid| {
812+
Some(PaneEvent::SetPause {
813+
grid_id,
814+
paused: !grid.all_paused().unwrap_or_default(),
815+
})
816+
},
817+
|player| Some(player::Event::SetPause(!player.is_paused().unwrap_or_default())),
818+
)
819+
} else {
820+
Task::none()
821+
}
822+
}
823+
Key::Named(key::Named::Backspace | key::Named::Delete) => {
824+
if self.modals.is_empty() {
825+
self.generate_event_in_selection(
826+
|_| None,
827+
|grid_id, _| Some(PaneEvent::Close { grid_id }),
828+
|_| Some(player::Event::Close),
829+
)
830+
} else {
831+
Task::none()
816832
}
817-
Task::none()
818833
}
819834
Key::Character(c) => {
835+
let command = modifiers == Modifiers::COMMAND;
836+
let command_shift = modifiers == Modifiers::COMMAND | Modifiers::SHIFT;
837+
820838
if self.modals.is_empty() {
821839
match c.as_str() {
822-
"M" | "m" => {
823-
self.generate_player_event_in_selection(
824-
player::Event::SetMute,
825-
|app| !app.config.playback.muted,
826-
|grid| !grid.all_muted().unwrap_or_default(),
827-
|player| !player.is_muted().unwrap_or_default(),
828-
);
840+
"J" | "j" => self.generate_event_in_selection(
841+
|_| {
842+
Some(Message::AllPlayers {
843+
event: player::Event::SeekRandom,
844+
})
845+
},
846+
|grid_id, _| Some(PaneEvent::SeekRandom { grid_id }),
847+
|_| Some(player::Event::SeekRandom),
848+
),
849+
"L" | "l" => {
850+
self.update(Message::SetSynchronized(!self.config.playback.synchronized))
829851
}
830-
_ => {}
852+
"M" | "m" => self.generate_event_in_selection(
853+
|app| Some(Message::SetMute(!app.config.playback.muted)),
854+
|grid_id, grid| {
855+
Some(PaneEvent::SetMute {
856+
grid_id,
857+
muted: !grid.all_muted().unwrap_or_default(),
858+
})
859+
},
860+
|player| Some(player::Event::SetMute(!player.is_muted().unwrap_or_default())),
861+
),
862+
"N" | "n" if modifiers.is_empty() => {
863+
if let Some((grid_id, _)) = self.selection.pair() {
864+
self.update(Message::Pane {
865+
event: PaneEvent::AddPlayer { grid_id },
866+
})
867+
} else {
868+
Task::none()
869+
}
870+
}
871+
"N" | "n" if command => self.update(Message::PlaylistReset { force: false }),
872+
"O" | "o" if command => self.update(Message::PlaylistSelect { force: false }),
873+
"R" | "r" => self.generate_event_in_selection(
874+
|_| Some(Message::Refresh),
875+
|grid_id, _| Some(PaneEvent::Refresh { grid_id }),
876+
|_| Some(player::Event::Refresh),
877+
),
878+
"S" | "s" if command => self.update(Message::PlaylistSave),
879+
"S" | "s" if command_shift => self.update(Message::PlaylistSaveAs),
880+
_ => Task::none(),
831881
}
882+
} else {
883+
Task::none()
832884
}
833-
Task::none()
834885
}
835886
_ => Task::none(),
836887
},
@@ -1071,6 +1122,7 @@ impl App {
10711122
self.playlist_dirty = true;
10721123
self.grids.close(grid_id);
10731124
self.update_playback();
1125+
self.selection.clear();
10741126
}
10751127
PaneEvent::AddPlayer { grid_id } => {
10761128
self.playlist_dirty = true;
@@ -1410,16 +1462,17 @@ impl App {
14101462
.push(Container::new(center_controls).center(Length::Fill));
14111463

14121464
let grids = PaneGrid::new(&self.grids, |grid_id, grid, _maximized| {
1465+
let selected = self.selection.is_grid_only_selected(grid_id);
14131466
pane_grid::Content::new(
14141467
Container::new(grid.view(
14151468
grid_id,
1416-
self.selection.is_grid_selected(grid_id),
1469+
selected,
14171470
self.selection.player_for_grid(grid_id),
14181471
obscured,
14191472
dragging_file,
14201473
))
14211474
.padding(5)
1422-
.class(style::Container::PlayerGroup),
1475+
.class(style::Container::PlayerGroup { selected }),
14231476
)
14241477
.title_bar({
14251478
let mut bar = pane_grid::TitleBar::new(" ")

src/gui/common.rs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,10 @@ impl Selection {
300300
}
301301

302302
pub fn is_grid_selected(&self, grid: grid::Id) -> bool {
303+
self.grid == Some(grid)
304+
}
305+
306+
pub fn is_grid_only_selected(&self, grid: grid::Id) -> bool {
303307
self.grid == Some(grid) && self.player.is_none()
304308
}
305309

@@ -315,7 +319,7 @@ impl Selection {
315319
}
316320
}
317321

318-
pub fn deselect(&mut self) {
322+
pub fn clear(&mut self) {
319323
self.grid = None;
320324
self.player = None;
321325
}
@@ -358,4 +362,28 @@ impl Selection {
358362
}
359363
}
360364
}
365+
366+
pub fn ensure_valid_in_grid(&mut self, available: Vec<(grid::Id, player::Id)>) {
367+
let known = self
368+
.grid
369+
.and_then(|grid_id| {
370+
available
371+
.iter()
372+
.position(|(g, p)| *g == grid_id && Some(*p) == self.player)
373+
})
374+
.is_some();
375+
376+
if !known {
377+
match available.last().copied() {
378+
Some((grid, player)) => {
379+
self.grid = Some(grid);
380+
self.player = Some(player);
381+
}
382+
None => {
383+
self.grid = None;
384+
self.player = None;
385+
}
386+
}
387+
}
388+
}
361389
}

0 commit comments

Comments
 (0)