mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-29 12:37:51 -06:00
#1290: list the recent libraries read-only
The launch window no longer probes each recent library with the lock check of File > Open Library (which created catalog.lock and wrote catalog.lock.owner into every free library at every launch, including other drives, network shares and sleeping disks). A row is now checked by looking only: the path exists, is a folder, can be listed, holds a LightCraft catalog, and that catalog opens for reading. A library another program has open is found when Open is pressed: the open fails with the lock owner's message, the window stays and greys the row out. New test: listing (and drawing the window) leaves every library folder byte-identical (contents and modification times); it fails with the old probe. Docs (What's New, parity row, control protocol) say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AbegtYaPuwqZ81pGfELsFK
This commit is contained in:
@@ -982,7 +982,7 @@ fn main() -> eframe::Result {
|
|||||||
}
|
}
|
||||||
// the recent libraries to choose from (issue #1290), each checked now
|
// the recent libraries to choose from (issue #1290), each checked now
|
||||||
if launch == Launch::Ask {
|
if launch == Launch::Ask {
|
||||||
problem = Some(LibraryProblem::choose(&app.session, &app.ui.settings));
|
problem = Some(LibraryProblem::choose(&app.ui.settings));
|
||||||
}
|
}
|
||||||
app.alt_asks_for_library = matches!(launch, Launch::Open { alt_asks: true, .. });
|
app.alt_asks_for_library = matches!(launch, Launch::Open { alt_asks: true, .. });
|
||||||
lightcraft_ui_egui::i18n::set_language(app.ui.language);
|
lightcraft_ui_egui::i18n::set_language(app.ui.language);
|
||||||
@@ -1177,7 +1177,7 @@ mod tests {
|
|||||||
let path = d.join("ui.json");
|
let path = d.join("ui.json");
|
||||||
let mut app = LightcraftApp::new(s, Services::default());
|
let mut app = LightcraftApp::new(s, Services::default());
|
||||||
app.ui.settings.recent_libraries = vec!["/gone/a".into(), "/gone/b".into()];
|
app.ui.settings.recent_libraries = vec!["/gone/a".into(), "/gone/b".into()];
|
||||||
app.library_problem = Some(LibraryProblem::choose(&app.session, &app.ui.settings));
|
app.library_problem = Some(LibraryProblem::choose(&app.ui.settings));
|
||||||
let mut w = PrefsWriter { path: Some(path.clone()), ..Default::default() };
|
let mut w = PrefsWriter { path: Some(path.clone()), ..Default::default() };
|
||||||
app.ui.settings.recent_libraries.remove(0);
|
app.ui.settings.recent_libraries.remove(0);
|
||||||
w.save(&app).unwrap();
|
w.save(&app).unwrap();
|
||||||
|
|||||||
@@ -2355,5 +2355,8 @@
|
|||||||
"Open now": "Ouverte en ce moment",
|
"Open now": "Ouverte en ce moment",
|
||||||
"Last opened": "Ouverte le",
|
"Last opened": "Ouverte le",
|
||||||
"Not found: moved, renamed or on a disconnected drive": "Introuvable : déplacée, renommée ou sur un disque déconnecté",
|
"Not found: moved, renamed or on a disconnected drive": "Introuvable : déplacée, renommée ou sur un disque déconnecté",
|
||||||
"There is no LightCraft library in this folder any more": "Ce dossier ne contient plus de bibliothèque LightCraft"
|
"There is no LightCraft library in this folder any more": "Ce dossier ne contient plus de bibliothèque LightCraft",
|
||||||
|
"This is a file, not a library folder": "C’est un fichier, pas un dossier de bibliothèque",
|
||||||
|
"This folder can't be read:": "Ce dossier ne peut pas être lu :",
|
||||||
|
"The library's catalog can't be read:": "Le catalogue de la bibliothèque ne peut pas être lu :"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,21 +98,17 @@ impl LibraryProblem {
|
|||||||
LibraryProblem { path: path.into(), error: error.into(), can_retry: true, ..Default::default() }
|
LibraryProblem { path: path.into(), error: error.into(), can_retry: true, ..Default::default() }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The window listing the recent libraries, each checked now: one that is gone, isn't a
|
/// The window listing the recent libraries, each checked now without writing anything
|
||||||
/// LightCraft library any more, or can't be opened (another program has it, unreadable) is
|
/// ([`unavailable`]): one that is gone, isn't a LightCraft library any more or can't be read
|
||||||
/// greyed out with the reason. The last library opened is selected, or else the first one
|
/// is greyed out with the reason. The last library opened is selected, or else the first one
|
||||||
/// that can be opened.
|
/// that can be opened.
|
||||||
pub fn choose(session: &lightcraft_engine::Session, settings: &AppSettings) -> LibraryProblem {
|
pub fn choose(settings: &AppSettings) -> LibraryProblem {
|
||||||
// as many as File ▸ Open Recent Library keeps (a hand-edited settings file may hold more)
|
// as many as File ▸ Open Recent Library keeps (a hand-edited settings file may hold more)
|
||||||
let libraries: Vec<RecentLibrary> = settings
|
let libraries: Vec<RecentLibrary> = settings
|
||||||
.recent_libraries
|
.recent_libraries
|
||||||
.iter()
|
.iter()
|
||||||
.take(crate::state::RECENT_LIBRARIES)
|
.take(crate::state::RECENT_LIBRARIES)
|
||||||
.map(|path| RecentLibrary {
|
.map(|path| RecentLibrary { path: path.clone(), opened: settings.recent_opened.get(path).cloned(), unavailable: unavailable(path) })
|
||||||
path: path.clone(),
|
|
||||||
opened: settings.recent_opened.get(path).cloned(),
|
|
||||||
unavailable: unavailable(session, path),
|
|
||||||
})
|
|
||||||
.collect();
|
.collect();
|
||||||
let can = |l: &RecentLibrary| l.unavailable.is_none();
|
let can = |l: &RecentLibrary| l.unavailable.is_none();
|
||||||
let selected = libraries
|
let selected = libraries
|
||||||
@@ -143,20 +139,33 @@ impl LibraryProblem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Why the library at `path` can't be opened now (`None`: it can).
|
/// Why the library at `path` can't be opened (`None`: as far as can be told without opening it).
|
||||||
fn unavailable(session: &lightcraft_engine::Session, path: &str) -> Option<String> {
|
///
|
||||||
|
/// Read-only: the folder and its catalog file are looked at, never locked or written (the list
|
||||||
|
/// may name libraries on other drives, network shares or sleeping disks). A library another
|
||||||
|
/// program has open is therefore found when Open is pressed: the open fails and the window says
|
||||||
|
/// who has it.
|
||||||
|
fn unavailable(path: &str) -> Option<String> {
|
||||||
let dir = Path::new(path);
|
let dir = Path::new(path);
|
||||||
if !dir.exists() {
|
if !dir.exists() {
|
||||||
return Some(crate::i18n::tr("Not found: moved, renamed or on a disconnected drive").to_string());
|
return Some(crate::i18n::tr("Not found: moved, renamed or on a disconnected drive").to_string());
|
||||||
}
|
}
|
||||||
// a folder that is there, readable and not held by another program
|
if !dir.is_dir() {
|
||||||
if let Err(e) = session.check_library(dir) {
|
return Some(crate::i18n::tr("This is a file, not a library folder").to_string());
|
||||||
return Some(e.to_string());
|
}
|
||||||
|
if let Err(e) = std::fs::read_dir(dir) {
|
||||||
|
return Some(format!("{} {e}", crate::i18n::tr("This folder can't be read:")));
|
||||||
}
|
}
|
||||||
// opening would make a new, empty library in it
|
// opening would make a new, empty library in it
|
||||||
if !lightcraft_engine::library::is_library(dir) {
|
if !lightcraft_engine::library::is_library(dir) {
|
||||||
return Some(crate::i18n::tr("There is no LightCraft library in this folder any more").to_string());
|
return Some(crate::i18n::tr("There is no LightCraft library in this folder any more").to_string());
|
||||||
}
|
}
|
||||||
|
// the catalog the open reads first: the snapshot, else the log alone (opened to read, not read)
|
||||||
|
let snapshot = dir.join(lightcraft_catalog::journal::SNAPSHOT);
|
||||||
|
let catalog = if snapshot.is_file() { snapshot } else { dir.join(lightcraft_catalog::journal::LOG) };
|
||||||
|
if let Err(e) = std::fs::File::open(&catalog) {
|
||||||
|
return Some(format!("{} {e}", crate::i18n::tr("The library's catalog can't be read:")));
|
||||||
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,7 +266,7 @@ fn alt_at_launch(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||||||
if crate::panels::settings::can_switch_library(app).is_err() {
|
if crate::panels::settings::can_switch_library(app).is_err() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let mut p = LibraryProblem::choose(&app.session, &app.ui.settings);
|
let mut p = LibraryProblem::choose(&app.ui.settings);
|
||||||
if let Some(c) = p.chooser.as_mut() {
|
if let Some(c) = p.chooser.as_mut() {
|
||||||
c.over = app.session.library.as_ref().map(|_| app.session.library_generation());
|
c.over = app.session.library.as_ref().map(|_| app.session.library_generation());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,48 +84,100 @@ fn the_list_checks_each_library_and_selects_the_last_one() {
|
|||||||
let (a, b, held_path) = (make_library(&a), make_library(&b), make_library(&held));
|
let (a, b, held_path) = (make_library(&a), make_library(&b), make_library(&held));
|
||||||
std::fs::create_dir_all(&plain).unwrap();
|
std::fs::create_dir_all(&plain).unwrap();
|
||||||
std::fs::write(plain.join("notes.txt"), b"not a library").unwrap();
|
std::fs::write(plain.join("notes.txt"), b"not a library").unwrap();
|
||||||
let gone = dir.join("Gone").to_string_lossy().into_owned();
|
let file = dir.join("a-file.txt");
|
||||||
let plain = plain.to_string_lossy().into_owned();
|
std::fs::write(&file, b"a file").unwrap();
|
||||||
|
let (gone, plain, file) =
|
||||||
|
(dir.join("Gone").to_string_lossy().into_owned(), plain.to_string_lossy().into_owned(), file.to_string_lossy().into_owned());
|
||||||
|
// another program has it open: listing doesn't take locks, so it is found when opened
|
||||||
let lock = lightcraft_catalog::LibraryLock::acquire(Path::new(&held_path), "another LightCraft").unwrap();
|
let lock = lightcraft_catalog::LibraryLock::acquire(Path::new(&held_path), "another LightCraft").unwrap();
|
||||||
let session = lightcraft_engine::Session::new().with_fs();
|
|
||||||
|
|
||||||
let mut settings = AppSettings {
|
let mut settings = AppSettings {
|
||||||
recent_libraries: vec![gone.clone(), held_path.clone(), a.clone(), plain.clone(), b.clone()],
|
recent_libraries: vec![gone.clone(), held_path.clone(), a.clone(), plain.clone(), file.clone(), b.clone()],
|
||||||
library_path: b.clone(),
|
library_path: b.clone(),
|
||||||
..AppSettings::default()
|
..AppSettings::default()
|
||||||
};
|
};
|
||||||
settings.recent_opened.insert(b.clone(), "2026-10-20T08:30:00".into());
|
settings.recent_opened.insert(b.clone(), "2026-10-20T08:30:00".into());
|
||||||
let p = LibraryProblem::choose(&session, &settings);
|
let p = LibraryProblem::choose(&settings);
|
||||||
assert!(!p.saves_nothing(), "the launch list is not a temporary session: removals are saved");
|
assert!(!p.saves_nothing(), "the launch list is not a temporary session: removals are saved");
|
||||||
let c = p.chooser.as_ref().unwrap();
|
let c = p.chooser.as_ref().unwrap();
|
||||||
let why: Vec<Option<&str>> = c.libraries.iter().map(|l| l.unavailable.as_deref()).collect();
|
let why: Vec<Option<&str>> = c.libraries.iter().map(|l| l.unavailable.as_deref()).collect();
|
||||||
assert!(why[0].unwrap().contains("Not found"), "{why:?}");
|
assert!(why[0].unwrap().contains("Not found"), "{why:?}");
|
||||||
assert!(why[1].unwrap().contains("already open in another LightCraft"), "{why:?}");
|
assert_eq!(why[1], None, "a lock is not probed: {why:?}");
|
||||||
assert_eq!(why[2], None);
|
assert_eq!(why[2], None);
|
||||||
assert!(why[3].unwrap().contains("no LightCraft library"), "{why:?}");
|
assert!(why[3].unwrap().contains("no LightCraft library"), "{why:?}");
|
||||||
assert_eq!(why[4], None);
|
assert!(why[4].unwrap().contains("not a library folder"), "{why:?}");
|
||||||
assert_eq!(c.selected, Some(4), "the last library opened");
|
assert_eq!(why[5], None);
|
||||||
assert_eq!(c.libraries[4].opened.as_deref(), Some("2026-10-20T08:30:00"));
|
assert_eq!(c.selected, Some(5), "the last library opened");
|
||||||
|
assert_eq!(c.libraries[5].opened.as_deref(), Some("2026-10-20T08:30:00"));
|
||||||
assert_eq!(c.libraries[2].opened, None, "no time known");
|
assert_eq!(c.libraries[2].opened, None, "no time known");
|
||||||
let json = p.to_json();
|
let json = p.to_json();
|
||||||
assert_eq!(json["chooser"]["libraries"][1]["available"], false);
|
assert_eq!(json["chooser"]["libraries"][0]["available"], false);
|
||||||
assert_eq!(json["chooser"]["selected"], 4);
|
assert_eq!(json["chooser"]["selected"], 5);
|
||||||
|
|
||||||
// the last library can't be opened: the first one that can is selected
|
// the last library can't be opened: the first one that can is selected
|
||||||
settings.library_path = held_path.clone();
|
settings.library_path = plain.clone();
|
||||||
assert_eq!(LibraryProblem::choose(&session, &settings).chooser.unwrap().selected, Some(2));
|
assert_eq!(LibraryProblem::choose(&settings).chooser.unwrap().selected, Some(1));
|
||||||
// none can: the first row, Open greyed out
|
// none can: the first row, Open greyed out
|
||||||
settings.recent_libraries = vec![gone.clone()];
|
settings.recent_libraries = vec![gone.clone(), file.clone()];
|
||||||
assert_eq!(LibraryProblem::choose(&session, &settings).chooser.unwrap().selected, Some(0));
|
assert_eq!(LibraryProblem::choose(&settings).chooser.unwrap().selected, Some(0));
|
||||||
// an empty list selects nothing
|
// an empty list selects nothing
|
||||||
settings.recent_libraries.clear();
|
settings.recent_libraries.clear();
|
||||||
let c = LibraryProblem::choose(&session, &settings).chooser.unwrap();
|
let c = LibraryProblem::choose(&settings).chooser.unwrap();
|
||||||
assert!(c.libraries.is_empty() && c.selected.is_none());
|
assert!(c.libraries.is_empty() && c.selected.is_none());
|
||||||
assert!(LibraryProblem::new("/x", "e").saves_nothing(), "the failed-library window still saves nothing");
|
assert!(LibraryProblem::new("/x", "e").saves_nothing(), "the failed-library window still saves nothing");
|
||||||
drop(lock);
|
drop(lock);
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Every file under `dir`: relative path → (contents, modification time).
|
||||||
|
fn snapshot(dir: &Path) -> std::collections::BTreeMap<PathBuf, (Vec<u8>, std::time::SystemTime)> {
|
||||||
|
let mut out = std::collections::BTreeMap::new();
|
||||||
|
let mut todo = vec![dir.to_path_buf()];
|
||||||
|
while let Some(d) = todo.pop() {
|
||||||
|
for e in std::fs::read_dir(&d).unwrap() {
|
||||||
|
let p = e.unwrap().path();
|
||||||
|
let m = std::fs::metadata(&p).unwrap();
|
||||||
|
if m.is_dir() {
|
||||||
|
todo.push(p.clone());
|
||||||
|
}
|
||||||
|
let bytes = if m.is_file() { std::fs::read(&p).unwrap() } else { Vec::new() };
|
||||||
|
out.insert(p.strip_prefix(dir).unwrap().to_path_buf(), (bytes, m.modified().unwrap()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Listing the recent libraries (and drawing the window) writes nothing into any of them: no
|
||||||
|
/// lock file, no owner file, nothing modified — free, held by another program or not a library.
|
||||||
|
#[test]
|
||||||
|
fn listing_the_libraries_leaves_every_folder_byte_identical() {
|
||||||
|
let dir = temp("readonly");
|
||||||
|
let (free, fresh, held) = (dir.join("Free"), dir.join("Fresh"), dir.join("Held"));
|
||||||
|
let (free, held_path) = (make_library(&free), make_library(&held));
|
||||||
|
// a library whose lock files were never made (copied over from elsewhere, say)
|
||||||
|
let fresh_path = make_library(&fresh);
|
||||||
|
for f in [lightcraft_catalog::lock::LOCK, lightcraft_catalog::lock::OWNER] {
|
||||||
|
let _ = std::fs::remove_file(fresh.join(f));
|
||||||
|
}
|
||||||
|
std::fs::create_dir_all(dir.join("Plain")).unwrap();
|
||||||
|
let lock = lightcraft_catalog::LibraryLock::acquire(Path::new(&held_path), "another LightCraft").unwrap();
|
||||||
|
let recent = vec![free.clone(), fresh_path.clone(), held_path.clone(), dir.join("Plain").to_string_lossy().into_owned()];
|
||||||
|
let before = snapshot(&dir);
|
||||||
|
// (mtime granularity: a write in the same instant would still change contents or add files)
|
||||||
|
let settings = AppSettings { recent_libraries: recent.clone(), library_path: free.clone(), ..AppSettings::default() };
|
||||||
|
let p = LibraryProblem::choose(&settings);
|
||||||
|
assert_eq!(snapshot(&dir), before, "checking the rows must not create or change any file");
|
||||||
|
assert!(p.chooser.as_ref().unwrap().libraries.iter().take(3).all(|l| l.unavailable.is_none()));
|
||||||
|
let mut h = asking(recent, &free, None);
|
||||||
|
settle(&mut h);
|
||||||
|
assert!(has(&h, "libraryRecent:2"));
|
||||||
|
drop(h);
|
||||||
|
assert_eq!(snapshot(&dir), before, "listing must not create or change any file");
|
||||||
|
assert!(!fresh.join(lightcraft_catalog::lock::LOCK).exists() && !fresh.join(lightcraft_catalog::lock::OWNER).exists());
|
||||||
|
drop(lock);
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
}
|
||||||
|
|
||||||
/// The app as the desktop host starts it with *Ask me*: no library open, the list showing.
|
/// The app as the desktop host starts it with *Ask me*: no library open, the list showing.
|
||||||
fn asking(recent: Vec<String>, last: &str, picked: Option<String>) -> Headless {
|
fn asking(recent: Vec<String>, last: &str, picked: Option<String>) -> Headless {
|
||||||
let session = lightcraft_engine::Session::new().with_fs();
|
let session = lightcraft_engine::Session::new().with_fs();
|
||||||
@@ -145,7 +197,7 @@ fn asking(recent: Vec<String>, last: &str, picked: Option<String>) -> Headless {
|
|||||||
if let Some(p) = picked {
|
if let Some(p) = picked {
|
||||||
app.services.pick_folder = Some(Box::new(move || Some(p.clone())));
|
app.services.pick_folder = Some(Box::new(move || Some(p.clone())));
|
||||||
}
|
}
|
||||||
app.library_problem = Some(LibraryProblem::choose(&app.session, &app.ui.settings));
|
app.library_problem = Some(LibraryProblem::choose(&app.ui.settings));
|
||||||
let mut h = Headless::new(app, [1300.0, 900.0], 1.0);
|
let mut h = Headless::new(app, [1300.0, 900.0], 1.0);
|
||||||
settle(&mut h);
|
settle(&mut h);
|
||||||
h
|
h
|
||||||
@@ -206,7 +258,7 @@ fn return_opens_the_selected_library_and_a_failure_says_why() {
|
|||||||
settle(&mut h);
|
settle(&mut h);
|
||||||
assert!(h.app.session.library.is_none());
|
assert!(h.app.session.library.is_none());
|
||||||
let p = h.app.library_problem.as_ref().unwrap();
|
let p = h.app.library_problem.as_ref().unwrap();
|
||||||
assert!(p.error.contains("already open"), "{}", p.error);
|
assert!(p.error.contains("already open in another LightCraft"), "who has it: {}", p.error);
|
||||||
assert!(p.chooser.as_ref().unwrap().libraries[1].unavailable.is_some(), "greyed out now");
|
assert!(p.chooser.as_ref().unwrap().libraries[1].unavailable.is_some(), "greyed out now");
|
||||||
drop(lock);
|
drop(lock);
|
||||||
// ↑ to Alpha, Return: it opens
|
// ↑ to Alpha, Return: it opens
|
||||||
|
|||||||
@@ -173,7 +173,9 @@ With Settings ▸ General ▸ At launch set to Ask me (`ui.set {"settings": {"at
|
|||||||
default, opens the last library), or Alt/Option held during the first 2 s after the window appears, the desktop app
|
default, opens the last library), or Alt/Option held during the first 2 s after the window appears, the desktop app
|
||||||
shows a window listing the recent libraries (issue #1290). `--library` and `LIGHTCRAFT_LIBRARY` skip it, and so does
|
shows a window listing the recent libraries (issue #1290). `--library` and `LIGHTCRAFT_LIBRARY` skip it, and so does
|
||||||
`--memory`; `lightcraft-cli` never shows it. `ui.inspect` → `libraryProblem.chooser` is `{libraries: [{path,
|
`--memory`; `lightcraft-cli` never shows it. `ui.inspect` → `libraryProblem.chooser` is `{libraries: [{path,
|
||||||
lastOpened, available, reason}], selected, libraryOpen}` (else `null`; `libraryOpen: true` when it was opened over
|
lastOpened, available, reason}], selected, libraryOpen}` (`available` is checked read-only: the folder and its catalog
|
||||||
|
file are looked at, never locked or written, so a library another program has open shows as available and Open
|
||||||
|
reports who has it in `libraryProblem.error`) (else `null`; `libraryOpen: true` when it was opened over
|
||||||
the last library with Alt). Each row is the widget `libraryRecent:<index>`; the buttons are `button:libraryOpen`,
|
the last library with Alt). Each row is the widget `libraryRecent:<index>`; the buttons are `button:libraryOpen`,
|
||||||
`button:libraryOpenAnother` (the folder dialog of `app.openLibrary`), `button:libraryNew` (`app.newLibrary`),
|
`button:libraryOpenAnother` (the folder dialog of `app.openLibrary`), `button:libraryNew` (`app.newLibrary`),
|
||||||
`button:libraryForget` (Remove from List) and `button:libraryQuit`; `ui.key` ArrowUp / ArrowDown move the selection,
|
`button:libraryForget` (Remove from List) and `button:libraryQuit`; `ui.key` ArrowUp / ArrowDown move the selection,
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
+4
-2
@@ -5,8 +5,10 @@
|
|||||||
### Choose which library opens at launch
|
### Choose which library opens at launch
|
||||||
- Settings ▸ General ▸ **At launch**: *Open the last library* (as before) or *Ask me*. With *Ask me*, LightCraft
|
- Settings ▸ General ▸ **At launch**: *Open the last library* (as before) or *Ask me*. With *Ask me*, LightCraft
|
||||||
starts with a window listing your recent libraries: each one's name, the folder it is in and when you last opened
|
starts with a window listing your recent libraries: each one's name, the folder it is in and when you last opened
|
||||||
it. One that is gone (moved, renamed, on a disconnected drive), open in another program or no longer a library is
|
it. One that is gone (moved, renamed, on a disconnected drive), can't be read or no longer holds a library is
|
||||||
greyed out with the reason. The last library is selected; **Open** (or Return, or a double-click) opens it,
|
greyed out with the reason. Making the list only looks: it never locks or writes anything in your libraries, so one
|
||||||
|
that another program has open is reported, with who has it, when you press **Open**, and the window stays. The
|
||||||
|
last library is selected; **Open** (or Return, or a double-click) opens it,
|
||||||
**Open Another…** asks for a folder, **New Library…** makes one, **Quit** (or Esc) quits. ↑ and ↓ move through
|
**Open Another…** asks for a folder, **New Library…** makes one, **Quit** (or Esc) quits. ↑ and ↓ move through
|
||||||
the list.
|
the list.
|
||||||
- Holding Alt (Option on a Mac) as the window appears shows the same list once, over the library that opened: open
|
- Holding Alt (Option on a Mac) as the window appears shows the same list once, over the library that opened: open
|
||||||
|
|||||||
Reference in New Issue
Block a user