diff --git a/corrscope/cli.py b/corrscope/cli.py index 6ea7d98..84c2d61 100644 --- a/corrscope/cli.py +++ b/corrscope/cli.py @@ -2,16 +2,16 @@ import datetime import sys from itertools import count from pathlib import Path -from typing import Optional, List, Tuple, Union, Iterator, cast +from typing import Optional, List, Tuple, Union, Iterator, cast, TypeVar import click import corrscope from corrscope.channel import ChannelConfig from corrscope.config import yaml -from corrscope.settings.paths import MissingFFmpegError -from corrscope.outputs import IOutputConfig, FFplayOutputConfig, FFmpegOutputConfig from corrscope.corrscope import default_config, CorrScope, Config, Arguments +from corrscope.outputs import IOutputConfig, FFplayOutputConfig +from corrscope.settings.paths import MissingFFmpegError Folder = click.Path(exists=True, file_okay=False) @@ -43,20 +43,39 @@ VIDEO_NAME = ".mp4" DEFAULT_NAME = corrscope.app_name -def get_name(audio_file: Union[None, str, Path]) -> str: - # Write file to current working dir, not audio dir. - if audio_file: - name = Path(audio_file).stem +def _get_file_name(cfg_path: Optional[Path], cfg: Config, ext: str) -> str: + """ + Returns a file path with extension (but no dir). + Defaults to "corrscope.ext". + """ + name = get_file_stem(cfg_path, cfg, default=DEFAULT_NAME) + return name + ext + + +T = TypeVar("T") + + +def get_file_stem(cfg_path: Optional[Path], cfg: Config, default: T) -> Union[str, T]: + """ + Returns a "name" (no dir or extension) for saving file or video. + Defaults to `default`. + + Used by GUI as well. + """ + if cfg_path: + # Current file was saved. + file_path_or_name = Path(cfg_path) + + # Current file was never saved. + # Master audio and all channels may/not be absolute paths. + elif cfg.master_audio: + file_path_or_name = Path(cfg.master_audio) + elif len(cfg.channels) > 0: + file_path_or_name = Path(cfg.channels[0].wav_path) else: - name = DEFAULT_NAME - return name + return default - -def get_path(audio_file: Union[None, str, Path], ext: str) -> Path: - name = get_name(audio_file) - - # Add extension - return Path(name).with_suffix(ext) + return file_path_or_name.stem PROFILE_DUMP_NAME = "cprofile" @@ -202,8 +221,9 @@ def main( assert False, cfg_or_path if write: - write_path = get_path(audio, YAML_NAME) - yaml.dump(cfg, write_path) + # `corrscope file.yaml -w` should write back to same path. + write_path = _get_file_name(cfg_path, cfg, ext=YAML_NAME) + yaml.dump(cfg, Path(write_path)) outputs = [] # type: List[IOutputConfig] @@ -211,8 +231,8 @@ def main( outputs.append(FFplayOutputConfig()) if render: - video_path = get_path(cfg_path or audio, VIDEO_NAME) - outputs.append(cfg.get_ffmpeg_cfg(str(video_path))) + video_path = _get_file_name(cfg_path, cfg, ext=VIDEO_NAME) + outputs.append(cfg.get_ffmpeg_cfg(video_path)) if outputs: arg = Arguments(cfg_dir=cfg_dir, outputs=outputs) diff --git a/corrscope/gui/__init__.py b/corrscope/gui/__init__.py index a11e50c..7f5e5fc 100644 --- a/corrscope/gui/__init__.py +++ b/corrscope/gui/__init__.py @@ -102,6 +102,31 @@ class MainWindow(qw.QMainWindow, Ui_MainWindow): Opening a document: - load_cfg_from_path + + ## Dialog Directory/Filename Generation + + Save-dialog dir is persistent state, saved across program runs. + Most recent of: + - Any open/save dialog (unless separate_render_dir is True). + - self.pref.file_dir_ref, .set() + - Load YAML from CLI. + - load_cfg_from_path(cfg_path) sets `self.pref.file_dir`. + - Load .wav files from CLI. + - if isinstance(cfg_or_path, Config): + - save_dir = self.compute_save_dir(self.cfg) + - self.pref.file_dir = save_dir (if not empty) + + Render-dialog dir is persistent state, = most recent render-save dialog. + - self.pref.render_dir, .set() + + Save/render-dialog filename (no dir) is computed on demand, NOT persistent state. + - (Currently loaded config path, or master audio, or channel 0) + ext. + - Otherwise empty string. + - self.get_save_filename() calls cli.get_file_stem(). + + CLI filename is the same, + but defaults to "corrscope.{yaml, mp4}" instead of empty string. + - cli._get_file_name() calls cli.get_file_stem(). """ def __init__(self, cfg_or_path: Union[Config, Path]): @@ -145,6 +170,9 @@ class MainWindow(qw.QMainWindow, Ui_MainWindow): # Bind config to UI. if isinstance(cfg_or_path, Config): self.load_cfg(cfg_or_path, None) + save_dir = self.compute_save_dir(self.cfg) + if save_dir: + self.pref.file_dir = save_dir elif isinstance(cfg_or_path, Path): self.load_cfg_from_path(cfg_or_path) else: @@ -217,7 +245,7 @@ class MainWindow(qw.QMainWindow, Ui_MainWindow): if not self.prompt_save(): return name = get_open_file_name( - self.pref.file_dir_ref, self, "Open config", ["YAML files (*.yaml)"] + self, "Open config", self.pref.file_dir_ref, ["YAML files (*.yaml)"] ) if name: cfg_path = Path(name) @@ -301,7 +329,7 @@ class MainWindow(qw.QMainWindow, Ui_MainWindow): def on_master_audio_browse(self): name = get_open_file_name( - self.pref.file_dir_ref, self, "Open master audio file", FILTER_WAV_FILES + self, "Open master audio file", self.pref.file_dir_ref, FILTER_WAV_FILES ) if name: master_audio = "master_audio" @@ -317,7 +345,7 @@ class MainWindow(qw.QMainWindow, Ui_MainWindow): def on_channel_add(self): wavs = get_open_file_list( - self.pref.file_dir_ref, self, "Add audio channels", FILTER_WAV_FILES + self, "Add audio channels", self.pref.file_dir_ref, FILTER_WAV_FILES ) if wavs: self.channel_view.append_channels(wavs) @@ -341,14 +369,17 @@ class MainWindow(qw.QMainWindow, Ui_MainWindow): """ :return: False if user cancels save action. """ - cfg_path_default = self.file_stem + cli.YAML_NAME + # Name and extension (no folder). + cfg_filename = self.get_save_filename(cli.YAML_NAME) + + # Folder is obtained from self.pref.file_dir_ref. filters = ["YAML files (*.yaml)", "All files (*)"] path = get_save_file_path( - self.pref.file_dir_ref, self, "Save As", - cfg_path_default, + self.pref.file_dir_ref, + cfg_filename, filters, cli.YAML_NAME, ) @@ -377,14 +408,16 @@ class MainWindow(qw.QMainWindow, Ui_MainWindow): qw.QMessageBox.critical(self, "Error", error_msg) return - video_path = self.file_stem + cli.VIDEO_NAME + # Name and extension (no folder). + video_filename = self.get_save_filename(cli.VIDEO_NAME) filters = ["MP4 files (*.mp4)", "All files (*)"] # Points to either `file_dir` or `render_dir`. - ref = self.pref.render_dir_ref + # Folder is obtained from `dir_ref`. + dir_ref = self.pref.render_dir_ref path = get_save_file_path( - ref, self, "Render to Video", video_path, filters, cli.VIDEO_NAME + self, "Render to Video", dir_ref, video_filename, filters, cli.VIDEO_NAME ) if path: name = str(path) @@ -460,10 +493,34 @@ class MainWindow(qw.QMainWindow, Ui_MainWindow): return self._cfg_path.name return self.UNTITLED - @property - def file_stem(self) -> str: - """ Returns a "config name" with no dots or slashes. """ - return cli.get_name(self._cfg_path or self.cfg.master_audio) + def get_save_filename(self, suffix: str) -> str: + """ + If file name can be guessed, return "filename.suffix" (no dir). + Otherwise return "". + + Used for saving file or video. + """ + stem = cli.get_file_stem(self._cfg_path, self.cfg, default="") + if stem: + return stem + suffix + else: + return "" + + @staticmethod + def compute_save_dir(cfg: Config) -> Optional[str]: + """Computes a "save directory" when constructing a config from CLI wav files.""" + if cfg.master_audio: + file_path = cfg.master_audio + elif len(cfg.channels) > 0: + file_path = cfg.channels[0].wav_path + else: + return None + + # If file_path is "file.wav", we want to return "." . + # os.path.dirname("file.wav") == "" + # Path("file.wav").parent..str == "." + dir = Path(file_path).parent + return str(dir) @property def cfg(self): diff --git a/corrscope/gui/history_file_dlg.py b/corrscope/gui/history_file_dlg.py index 617f500..4d47005 100644 --- a/corrscope/gui/history_file_dlg.py +++ b/corrscope/gui/history_file_dlg.py @@ -20,14 +20,17 @@ class FileName: def _get_hist_name( func: Callable[..., Tuple[str, str]], - history_dir: _gp.Ref[_gp.GlobalPrefs], parent: qw.QWidget, title: str, + history_dir: _gp.Ref[_gp.GlobalPrefs], default_name: Optional[str], filters: List[str], ) -> Optional[FileName]: - """Get file name, defaulting to history folder. If user accepts, update history.""" - + """ + Get file name. + Default folder is history folder, and `default_name`.folder is discarded. + If user accepts, update history. + """ # Get recently used dir. dir_or_file: str = history_dir.get() if default_name: @@ -56,13 +59,13 @@ def _get_hist_name( def get_open_file_name( - history_dir: _gp.Ref[_gp.GlobalPrefs], parent: qw.QWidget, title: str, + history_dir: _gp.Ref[_gp.GlobalPrefs], filters: List[str], ) -> Optional[str]: name = _get_hist_name( - qw.QFileDialog.getOpenFileName, history_dir, parent, title, None, filters + qw.QFileDialog.getOpenFileName, parent, title, history_dir, None, filters ) if name: assert name.file_name is not None @@ -71,13 +74,13 @@ def get_open_file_name( def get_open_file_list( - history_dir: _gp.Ref[_gp.GlobalPrefs], parent: qw.QWidget, title: str, + history_dir: _gp.Ref[_gp.GlobalPrefs], filters: List[str], ) -> Optional[List[str]]: name = _get_hist_name( - qw.QFileDialog.getOpenFileNames, history_dir, parent, title, None, filters + qw.QFileDialog.getOpenFileNames, parent, title, history_dir, None, filters ) if name: assert name.file_list is not None @@ -87,18 +90,18 @@ def get_open_file_list( # Returns Path for legacy reasons. Others return str. def get_save_file_path( - history_dir: _gp.Ref[_gp.GlobalPrefs], parent: qw.QWidget, title: str, + history_dir: _gp.Ref[_gp.GlobalPrefs], default_name: str, filters: List[str], default_suffix: str, ) -> Optional[Path]: name = _get_hist_name( qw.QFileDialog.getSaveFileName, - history_dir, parent, title, + history_dir, default_name, filters, ) diff --git a/tests/test_cli.py b/tests/test_cli.py index 1bd2b7c..a1b4f57 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -142,7 +142,7 @@ def test_load_yaml_another_dir(mocker, Popen): # Issue: this test does not use cli.main() to compute output path. # Possible solution: Call cli.main() via Click runner. - output = FFmpegOutputConfig(cli.get_path(cfg.master_audio, cli.VIDEO_NAME)) + output = FFmpegOutputConfig(cli._get_file_name(None, cfg, cli.VIDEO_NAME)) corr = CorrScope(cfg, Arguments(subdir, [output])) corr.play()