Merge branch 'hints' into 'master'
layout: Take into account text purpose again Closes #277 See merge request Librem5/squeekboard!448
This commit is contained in:
307
src/data.rs
307
src/data.rs
@ -69,8 +69,12 @@ pub mod c {
|
|||||||
let overlay_str = as_str(&overlay)
|
let overlay_str = as_str(&overlay)
|
||||||
.expect("Bad overlay name")
|
.expect("Bad overlay name")
|
||||||
.expect("Empty overlay name");
|
.expect("Empty overlay name");
|
||||||
|
let overlay_str = match overlay_str {
|
||||||
|
"" => None,
|
||||||
|
other => Some(other),
|
||||||
|
};
|
||||||
|
|
||||||
let (kind, layout) = load_layout_data_with_fallback(&name, type_, variant, &overlay_str);
|
let (kind, layout) = load_layout_data_with_fallback(&name, type_, variant, overlay_str);
|
||||||
let layout = ::layout::Layout::new(layout, kind);
|
let layout = ::layout::Layout::new(layout, kind);
|
||||||
Box::into_raw(Box::new(layout))
|
Box::into_raw(Box::new(layout))
|
||||||
}
|
}
|
||||||
@ -113,97 +117,156 @@ impl fmt::Display for DataSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type LayoutSource = (ArrangementKind, DataSource);
|
/* All functions in this family carry around ArrangementKind,
|
||||||
|
* because it's not guaranteed to be preserved,
|
||||||
|
* and the resulting layout needs to know which version was loaded.
|
||||||
|
* See `squeek_layout_get_kind`.
|
||||||
|
* Possible TODO: since this is used only in styling,
|
||||||
|
* and makes the below code nastier than needed, maybe it should go.
|
||||||
|
*/
|
||||||
|
|
||||||
/// Lists possible sources, with 0 as the most preferred one
|
/// Returns ordered names treating `name` as the base name,
|
||||||
/// Trying order: native lang of the right kind, native base,
|
/// ignoring any `+` inside.
|
||||||
/// fallback lang of the right kind, fallback base
|
fn _get_arrangement_names(name: &str, arrangement: ArrangementKind)
|
||||||
/// The `purpose` argument is not ContentPurpose,
|
-> Vec<(ArrangementKind, String)>
|
||||||
/// but rather ContentPurpose and overlay in one.
|
{
|
||||||
fn list_layout_sources(
|
let name_with_arrangement = match arrangement {
|
||||||
name: &str,
|
ArrangementKind::Base => name.into(),
|
||||||
kind: ArrangementKind,
|
ArrangementKind::Wide => format!("{}_wide", name),
|
||||||
purpose: &str,
|
|
||||||
keyboards_path: Option<PathBuf>,
|
|
||||||
) -> Vec<LayoutSource> {
|
|
||||||
// Just a simplification of often called code.
|
|
||||||
let add_by_name = |
|
|
||||||
mut ret: Vec<LayoutSource>,
|
|
||||||
purpose: &str,
|
|
||||||
name: &str,
|
|
||||||
kind: &ArrangementKind,
|
|
||||||
| -> Vec<LayoutSource> {
|
|
||||||
let name = if purpose == "" { name.into() }
|
|
||||||
else { format!("{}/{}", purpose, name) };
|
|
||||||
|
|
||||||
if let Some(path) = keyboards_path.clone() {
|
|
||||||
ret.push((
|
|
||||||
kind.clone(),
|
|
||||||
DataSource::File(
|
|
||||||
path.join(name.clone())
|
|
||||||
.with_extension("yaml")
|
|
||||||
)
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
ret.push((
|
|
||||||
kind.clone(),
|
|
||||||
DataSource::Resource(name)
|
|
||||||
));
|
|
||||||
ret
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Another grouping.
|
let mut ret = Vec::new();
|
||||||
let add_by_kind = |ret, purpose: &str, name: &str, kind| {
|
if name_with_arrangement != name {
|
||||||
let ret = match kind {
|
ret.push((arrangement, name_with_arrangement));
|
||||||
&ArrangementKind::Base => ret,
|
|
||||||
kind => add_by_name(
|
|
||||||
ret,
|
|
||||||
purpose,
|
|
||||||
&name_with_arrangement(name.into(), kind),
|
|
||||||
kind,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
add_by_name(ret, purpose, name, &ArrangementKind::Base)
|
|
||||||
};
|
|
||||||
|
|
||||||
fn name_with_arrangement(
|
|
||||||
name: String,
|
|
||||||
kind: &ArrangementKind,
|
|
||||||
) -> String {
|
|
||||||
match kind {
|
|
||||||
ArrangementKind::Base => name,
|
|
||||||
ArrangementKind::Wide => name + "_wide",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
ret.push((ArrangementKind::Base, name.into()));
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
let ret = Vec::new();
|
/// Returns names accounting for any `+` in the `name`,
|
||||||
|
/// including the fallback to the default layout.
|
||||||
// Name as given takes priority.
|
fn get_preferred_names(name: &str, kind: ArrangementKind)
|
||||||
let ret = add_by_kind(ret, purpose, name, &kind);
|
-> Vec<(ArrangementKind, String)>
|
||||||
|
{
|
||||||
// Then try non-alternative name if applicable (`us` for `us+colemak`).
|
let mut ret = _get_arrangement_names(name, kind);
|
||||||
let ret = {
|
|
||||||
|
let base_name_preferences = {
|
||||||
let mut parts = name.splitn(2, '+');
|
let mut parts = name.splitn(2, '+');
|
||||||
match parts.next() {
|
match parts.next() {
|
||||||
Some(base) => {
|
Some(base) => {
|
||||||
// The name is already equal to base, so it was already added.
|
// The name is already equal to base, so nothing to add
|
||||||
if base == name { ret }
|
if base == name {
|
||||||
else {
|
vec![]
|
||||||
add_by_kind(ret, purpose, base, &kind)
|
} else {
|
||||||
|
_get_arrangement_names(base, kind)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// The layout's base name starts with a "+". Weird but OK.
|
// The layout's base name starts with a "+". Weird but OK.
|
||||||
None => {
|
None => {
|
||||||
log_print!(logging::Level::Surprise, "Base layout name is empty: {}", name);
|
log_print!(logging::Level::Surprise, "Base layout name is empty: {}", name);
|
||||||
ret
|
vec![]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ret.extend(base_name_preferences.into_iter());
|
||||||
|
let fallback_names = _get_arrangement_names(FALLBACK_LAYOUT_NAME, kind);
|
||||||
|
ret.extend(fallback_names.into_iter());
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
add_by_kind(ret, purpose, FALLBACK_LAYOUT_NAME.into(), &kind)
|
/// Includes the subdirectory before the forward slash.
|
||||||
|
type LayoutPath = String;
|
||||||
|
|
||||||
|
// This is only used inside iter_fallbacks_with_meta.
|
||||||
|
// Placed at the top scope
|
||||||
|
// because `use LayoutPurpose::*;`
|
||||||
|
// complains about "not in scope" otherwise.
|
||||||
|
// This seems to be a Rust 2015 edition problem.
|
||||||
|
/// Helper for determining where to look up the layout.
|
||||||
|
enum LayoutPurpose<'a> {
|
||||||
|
Default,
|
||||||
|
Special(&'a str),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the directory string
|
||||||
|
/// where the layout should be looked up, including the slash.
|
||||||
|
fn get_directory_string(
|
||||||
|
content_purpose: ContentPurpose,
|
||||||
|
overlay: Option<&str>) -> String
|
||||||
|
{
|
||||||
|
use self::LayoutPurpose::*;
|
||||||
|
|
||||||
|
let layout_purpose = match overlay {
|
||||||
|
None => match content_purpose {
|
||||||
|
ContentPurpose::Number => Special("number"),
|
||||||
|
ContentPurpose::Digits => Special("number"),
|
||||||
|
ContentPurpose::Phone => Special("number"),
|
||||||
|
ContentPurpose::Terminal => Special("terminal"),
|
||||||
|
_ => Default,
|
||||||
|
},
|
||||||
|
Some(overlay) => Special(overlay),
|
||||||
|
};
|
||||||
|
|
||||||
|
// For intuitiveness,
|
||||||
|
// default purpose layouts are stored in the root directory,
|
||||||
|
// as they correspond to typical text
|
||||||
|
// and are seen the most often.
|
||||||
|
match layout_purpose {
|
||||||
|
Default => "".into(),
|
||||||
|
Special(purpose) => format!("{}/", purpose),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an iterator over all fallback paths.
|
||||||
|
fn to_layout_paths(
|
||||||
|
name_fallbacks: Vec<(ArrangementKind, String)>,
|
||||||
|
content_purpose: ContentPurpose,
|
||||||
|
overlay: Option<&str>,
|
||||||
|
) -> impl Iterator<Item=(ArrangementKind, LayoutPath)> {
|
||||||
|
let prepend_directory = get_directory_string(content_purpose, overlay);
|
||||||
|
|
||||||
|
name_fallbacks.into_iter()
|
||||||
|
.map(move |(arrangement, name)|
|
||||||
|
(arrangement, format!("{}{}", prepend_directory, name))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type LayoutSource = (ArrangementKind, DataSource);
|
||||||
|
|
||||||
|
fn to_layout_sources(
|
||||||
|
layout_paths: impl Iterator<Item=(ArrangementKind, LayoutPath)>,
|
||||||
|
filesystem_path: Option<PathBuf>,
|
||||||
|
) -> impl Iterator<Item=LayoutSource> {
|
||||||
|
layout_paths.flat_map(move |(arrangement, layout_path)| {
|
||||||
|
let mut sources = Vec::new();
|
||||||
|
if let Some(path) = &filesystem_path {
|
||||||
|
sources.push((
|
||||||
|
arrangement,
|
||||||
|
DataSource::File(
|
||||||
|
path.join(&layout_path)
|
||||||
|
.with_extension("yaml")
|
||||||
|
)
|
||||||
|
));
|
||||||
|
};
|
||||||
|
sources.push((arrangement, DataSource::Resource(layout_path.clone())));
|
||||||
|
sources.into_iter()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns possible sources, with first as the most preferred one.
|
||||||
|
/// Trying order: native lang of the right kind, native base,
|
||||||
|
/// fallback lang of the right kind, fallback base
|
||||||
|
fn iter_layout_sources(
|
||||||
|
name: &str,
|
||||||
|
arrangement: ArrangementKind,
|
||||||
|
purpose: ContentPurpose,
|
||||||
|
ui_overlay: Option<&str>,
|
||||||
|
layout_storage: Option<PathBuf>,
|
||||||
|
) -> impl Iterator<Item=LayoutSource> {
|
||||||
|
let names = get_preferred_names(name, arrangement);
|
||||||
|
let paths = to_layout_paths(names, purpose, ui_overlay);
|
||||||
|
to_layout_sources(paths, layout_storage)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_layout_data(source: DataSource)
|
fn load_layout_data(source: DataSource)
|
||||||
@ -231,7 +294,7 @@ fn load_layout_data_with_fallback(
|
|||||||
name: &str,
|
name: &str,
|
||||||
kind: ArrangementKind,
|
kind: ArrangementKind,
|
||||||
purpose: ContentPurpose,
|
purpose: ContentPurpose,
|
||||||
overlay: &str,
|
overlay: Option<&str>,
|
||||||
) -> (ArrangementKind, ::layout::LayoutData) {
|
) -> (ArrangementKind, ::layout::LayoutData) {
|
||||||
|
|
||||||
// Build the path to the right keyboard layout subdirectory
|
// Build the path to the right keyboard layout subdirectory
|
||||||
@ -239,13 +302,7 @@ fn load_layout_data_with_fallback(
|
|||||||
.map(PathBuf::from)
|
.map(PathBuf::from)
|
||||||
.or_else(|| xdg::data_path("squeekboard/keyboards"));
|
.or_else(|| xdg::data_path("squeekboard/keyboards"));
|
||||||
|
|
||||||
log_print!(
|
for (kind, source) in iter_layout_sources(&name, kind, purpose, overlay, path) {
|
||||||
logging::Level::Debug,
|
|
||||||
"load_layout_data_with_fallback() -> name:{}, purpose:{:?}, overlay:{}, layout_name:{}",
|
|
||||||
name, purpose, overlay, &name
|
|
||||||
);
|
|
||||||
|
|
||||||
for (kind, source) in list_layout_sources(&name, kind, overlay, path) {
|
|
||||||
let layout = load_layout_data(source.clone());
|
let layout = load_layout_data(source.clone());
|
||||||
match layout {
|
match layout {
|
||||||
Err(e) => match (e, source) {
|
Err(e) => match (e, source) {
|
||||||
@ -977,11 +1034,11 @@ mod tests {
|
|||||||
|
|
||||||
/// First fallback should be to builtin, not to FALLBACK_LAYOUT_NAME
|
/// First fallback should be to builtin, not to FALLBACK_LAYOUT_NAME
|
||||||
#[test]
|
#[test]
|
||||||
fn fallbacks_order() {
|
fn test_fallback_basic_builtin() {
|
||||||
let sources = list_layout_sources("nb", ArrangementKind::Base, "", None);
|
let sources = iter_layout_sources("nb", ArrangementKind::Base, ContentPurpose::Normal, None, None);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
sources,
|
sources.collect::<Vec<_>>(),
|
||||||
vec!(
|
vec!(
|
||||||
(ArrangementKind::Base, DataSource::Resource("nb".into())),
|
(ArrangementKind::Base, DataSource::Resource("nb".into())),
|
||||||
(
|
(
|
||||||
@ -991,14 +1048,36 @@ mod tests {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prefer loading from file system before builtin.
|
||||||
|
#[test]
|
||||||
|
fn test_preferences_order_path() {
|
||||||
|
let sources = iter_layout_sources("nb", ArrangementKind::Base, ContentPurpose::Normal, None, Some(".".into()));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
sources.collect::<Vec<_>>(),
|
||||||
|
vec!(
|
||||||
|
(ArrangementKind::Base, DataSource::File("./nb.yaml".into())),
|
||||||
|
(ArrangementKind::Base, DataSource::Resource("nb".into())),
|
||||||
|
(
|
||||||
|
ArrangementKind::Base,
|
||||||
|
DataSource::File("./us.yaml".into())
|
||||||
|
),
|
||||||
|
(
|
||||||
|
ArrangementKind::Base,
|
||||||
|
DataSource::Resource("us".into())
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// If layout contains a "+", it should reach for what's in front of it too.
|
/// If layout contains a "+", it should reach for what's in front of it too.
|
||||||
#[test]
|
#[test]
|
||||||
fn fallbacks_order_base() {
|
fn test_preferences_order_base() {
|
||||||
let sources = list_layout_sources("nb+aliens", ArrangementKind::Base, "", None);
|
let sources = iter_layout_sources("nb+aliens", ArrangementKind::Base, ContentPurpose::Normal, None, None);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
sources,
|
sources.collect::<Vec<_>>(),
|
||||||
vec!(
|
vec!(
|
||||||
(ArrangementKind::Base, DataSource::Resource("nb+aliens".into())),
|
(ArrangementKind::Base, DataSource::Resource("nb+aliens".into())),
|
||||||
(ArrangementKind::Base, DataSource::Resource("nb".into())),
|
(ArrangementKind::Base, DataSource::Resource("nb".into())),
|
||||||
@ -1011,22 +1090,58 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn fallbacks_terminal_order_base() {
|
fn test_preferences_order_arrangement() {
|
||||||
let sources = list_layout_sources("nb+aliens", ArrangementKind::Base, "terminal", None);
|
let sources = iter_layout_sources("nb", ArrangementKind::Wide, ContentPurpose::Normal, None, None);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
sources,
|
sources.collect::<Vec<_>>(),
|
||||||
vec!(
|
vec!(
|
||||||
(ArrangementKind::Base, DataSource::Resource("terminal/nb+aliens".into())),
|
(ArrangementKind::Wide, DataSource::Resource("nb_wide".into())),
|
||||||
(ArrangementKind::Base, DataSource::Resource("terminal/nb".into())),
|
(ArrangementKind::Base, DataSource::Resource("nb".into())),
|
||||||
|
(
|
||||||
|
ArrangementKind::Wide,
|
||||||
|
DataSource::Resource("us_wide".into())
|
||||||
|
),
|
||||||
(
|
(
|
||||||
ArrangementKind::Base,
|
ArrangementKind::Base,
|
||||||
DataSource::Resource(format!("terminal/{}", FALLBACK_LAYOUT_NAME))
|
DataSource::Resource("us".into())
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_preferences_order_overlay() {
|
||||||
|
let sources = iter_layout_sources("nb", ArrangementKind::Base, ContentPurpose::Normal, Some("terminal"), None);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
sources.collect::<Vec<_>>(),
|
||||||
|
vec!(
|
||||||
|
(ArrangementKind::Base, DataSource::Resource("terminal/nb".into())),
|
||||||
|
(
|
||||||
|
ArrangementKind::Base,
|
||||||
|
DataSource::Resource("terminal/us".into())
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_preferences_order_hint() {
|
||||||
|
let sources = iter_layout_sources("nb", ArrangementKind::Base, ContentPurpose::Terminal, None, None);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
sources.collect::<Vec<_>>(),
|
||||||
|
vec!(
|
||||||
|
(ArrangementKind::Base, DataSource::Resource("terminal/nb".into())),
|
||||||
|
(
|
||||||
|
ArrangementKind::Base,
|
||||||
|
DataSource::Resource("terminal/us".into())
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn unicode_keysym() {
|
fn unicode_keysym() {
|
||||||
let keysym = xkb::keysym_from_name(
|
let keysym = xkb::keysym_from_name(
|
||||||
|
|||||||
@ -600,7 +600,7 @@ impl View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The physical characteristic of layout for the purpose of styling
|
/// The physical characteristic of layout for the purpose of styling
|
||||||
#[derive(Clone, PartialEq, Debug)]
|
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||||
pub enum ArrangementKind {
|
pub enum ArrangementKind {
|
||||||
Base = 0,
|
Base = 0,
|
||||||
Wide = 1,
|
Wide = 1,
|
||||||
|
|||||||
Reference in New Issue
Block a user