diff --git a/Cargo.lock b/Cargo.lock index 22316b6..440adc7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -58,6 +58,21 @@ dependencies = [ "walkdir", ] +[[package]] +name = "akaza-dict" +version = "0.1.7" +dependencies = [ + "anyhow", + "encoding_rs", + "env_logger", + "gtk4", + "libakaza", + "log", + "serde", + "serde_yaml", + "xdg", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -963,6 +978,7 @@ name = "ibus-akaza" version = "0.1.7" dependencies = [ "akaza-conf", + "akaza-dict", "anyhow", "cc", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 606a490..9d1002e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,2 +1,2 @@ [workspace] -members = ["libakaza", "marisa-sys", "ibus-akaza", "ibus-sys", "akaza-data", "akaza-conf"] +members = ["libakaza", "marisa-sys", "ibus-akaza", "ibus-sys", "akaza-data", "akaza-conf", "akaza-dict"] diff --git a/akaza-conf/src/pane/dict_pane.rs b/akaza-conf/src/pane/dict_pane.rs index 2205cf1..6a47de3 100644 --- a/akaza-conf/src/pane/dict_pane.rs +++ b/akaza-conf/src/pane/dict_pane.rs @@ -1,119 +1,27 @@ -use gtk4::prelude::{ - ButtonExt, ComboBoxExt, DialogExt, FileChooserExt, FileExt, GridExt, GtkWindowExt, WidgetExt, -}; -use gtk4::{ - Button, ComboBoxText, FileChooserAction, FileChooserDialog, Grid, Label, ResponseType, - ScrolledWindow, Window, -}; -use libakaza::config::{Config, DictConfig, DictEncoding, DictType, DictUsage}; -use log::info; +use std::path::PathBuf; use std::sync::{Arc, Mutex}; +use gtk4::builders::MessageDialogBuilder; +use gtk4::prelude::ButtonExt; +use gtk4::prelude::ComboBoxExt; +use gtk4::prelude::DialogExt; +use gtk4::prelude::EntryBufferExt; +use gtk4::prelude::EntryBufferExtManual; +use gtk4::prelude::FileChooserExt; +use gtk4::prelude::FileExt; +use gtk4::prelude::GridExt; +use gtk4::prelude::GtkWindowExt; +use gtk4::prelude::WidgetExt; +use gtk4::{ + Button, ComboBoxText, FileChooserAction, FileChooserDialog, Grid, Label, MessageType, + ResponseType, ScrolledWindow, Text, TextBuffer, TextView, Window, +}; +use log::info; + +use libakaza::config::{Config, DictConfig, DictEncoding, DictType, DictUsage}; +use libakaza::dict::skk::write::write_skk_dict; + pub fn build_dict_pane(config: Arc>) -> anyhow::Result { - // TODO ここは TreeView 使った方がすっきり書けるはずだが、僕の GTK+ 力が引くすぎて対応できていない。 - // 誰かすっきり使い易くしてほしい。 - fn add_row(grid: &Grid, dict_config: &DictConfig, config: &Arc>, i: usize) { - grid.attach( - &Label::builder() - .xalign(0_f32) - .label(dict_config.path.as_str()) - .build(), - 0, - i as i32, - 1, - 1, - ); - - { - let cbt = ComboBoxText::builder().build(); - for usage in vec![ - DictUsage::Normal, - DictUsage::SingleTerm, - DictUsage::Disabled, - ] { - cbt.append(Some(usage.as_str()), usage.text_jp()); - } - cbt.set_active_id(Some(dict_config.usage.as_str())); - { - let config = config.clone(); - let path = dict_config.path.clone(); - cbt.connect_changed(move |f| { - if let Some(id) = f.active_id() { - let mut config = config.lock().unwrap(); - for mut dict in &mut config.engine.dicts { - if dict.path == path { - dict.usage = DictUsage::from(&id).unwrap(); - return; - } - } - config.engine.dicts.push(DictConfig { - dict_type: DictType::SKK, - encoding: DictEncoding::EucJp, - path: path.to_string(), - usage: DictUsage::from(&id).unwrap(), - }) - } - }); - } - grid.attach(&cbt, 1, i as i32, 1, 1); - } - - grid.attach( - &Label::new(Some(dict_config.dict_type.as_str())), - 2, - i as i32, - 1, - 1, - ); - { - let cbt = ComboBoxText::builder().build(); - for encoding in vec![DictEncoding::EucJp, DictEncoding::Utf8] { - cbt.append( - Some(&encoding.to_string()), - encoding.as_str().replace('_', "-").as_str(), - ); - } - cbt.set_active_id(Some(dict_config.encoding.as_str())); - { - let config = config.clone(); - let path = dict_config.path.clone(); - cbt.connect_changed(move |f| { - if let Some(id) = f.active_id() { - let mut config = config.lock().unwrap(); - for mut dict in &mut config.engine.dicts { - if dict.path == path { - dict.encoding = DictEncoding::from(&id).unwrap(); - break; - } - } - } - }); - } - grid.attach(&cbt, 3, i as i32, 1, 1); - } - - { - let delete_btn = { - let path = dict_config.path.clone(); - let config = config.clone(); - let delete_btn = Button::with_label("削除"); - let grid = grid.clone(); - delete_btn.connect_clicked(move |_| { - let mut config = config.lock().unwrap(); - for (i, dict) in &mut config.engine.dicts.iter().enumerate() { - if dict.path == path { - config.engine.dicts.remove(i); - grid.remove_row(i as i32); - break; - } - } - }); - delete_btn - }; - grid.attach(&delete_btn, 4, i as i32, 1, 1); - } - } - let scroll = ScrolledWindow::new(); let parent_grid = Grid::builder().column_spacing(10).build(); @@ -128,64 +36,267 @@ pub fn build_dict_pane(config: Arc>) -> anyhow::Result, - FileChooserAction::Open, - &[ - ("開く", ResponseType::Accept), - ("キャンセル", ResponseType::None), - ], - ); - let config = config.clone(); - let grid = grid.clone(); - dialog.connect_response(move |dialog, resp| match resp { - ResponseType::Accept => { - let file = dialog.file().unwrap(); - let path = file.path().unwrap(); - - info!("File: {:?}", path); - let dict_config = &DictConfig { - path: path.to_string_lossy().to_string(), - encoding: DictEncoding::Utf8, - usage: DictUsage::Normal, - dict_type: DictType::SKK, - }; - config - .lock() - .unwrap() - .engine - .dicts - .push(dict_config.clone()); - add_row( - &grid, - dict_config, - &config.clone(), - config.lock().unwrap().engine.dicts.len(), - ); - dialog.close(); - } - ResponseType::Close - | ResponseType::Reject - | ResponseType::Yes - | ResponseType::No - | ResponseType::None - | ResponseType::DeleteEvent => { - dialog.close(); - } - _ => {} - }); - dialog.show(); - }); - add_btn - }; - parent_grid.attach(&add_btn, 0, 1, 1, 1); + let add_system_dict_btn = build_add_system_dict_btn(config.clone(), grid.clone()); + parent_grid.attach(&add_system_dict_btn, 0, 1, 1, 1); + } + { + let add_user_dict_btn = build_add_user_dict_btn(grid, config); + parent_grid.attach(&add_user_dict_btn, 0, 2, 1, 1); } scroll.set_child(Some(&parent_grid)); Ok(scroll) } + +// TODO ここは TreeView 使った方がすっきり書けるはずだが、僕の GTK+ 力が引くすぎて対応できていない。 +// 誰かすっきり使い易くしてほしい。 +fn add_row(grid: &Grid, dict_config: &DictConfig, config: &Arc>, i: usize) { + grid.attach( + &Label::builder() + .xalign(0_f32) + .label(dict_config.path.as_str()) + .build(), + 0, + i as i32, + 1, + 1, + ); + + { + let cbt = ComboBoxText::builder().build(); + for usage in vec![ + DictUsage::Normal, + DictUsage::SingleTerm, + DictUsage::Disabled, + ] { + cbt.append(Some(usage.as_str()), usage.text_jp()); + } + cbt.set_active_id(Some(dict_config.usage.as_str())); + { + let config = config.clone(); + let path = dict_config.path.clone(); + cbt.connect_changed(move |f| { + if let Some(id) = f.active_id() { + let mut config = config.lock().unwrap(); + for mut dict in &mut config.engine.dicts { + if dict.path == path { + dict.usage = DictUsage::from(&id).unwrap(); + return; + } + } + config.engine.dicts.push(DictConfig { + dict_type: DictType::SKK, + encoding: DictEncoding::EucJp, + path: path.to_string(), + usage: DictUsage::from(&id).unwrap(), + }) + } + }); + } + grid.attach(&cbt, 1, i as i32, 1, 1); + } + + grid.attach( + &Label::new(Some(dict_config.dict_type.as_str())), + 2, + i as i32, + 1, + 1, + ); + { + let cbt = ComboBoxText::builder().build(); + for encoding in vec![DictEncoding::EucJp, DictEncoding::Utf8] { + cbt.append( + Some(&encoding.to_string()), + encoding.as_str().replace('_', "-").as_str(), + ); + } + cbt.set_active_id(Some(dict_config.encoding.as_str())); + { + let config = config.clone(); + let path = dict_config.path.clone(); + cbt.connect_changed(move |f| { + if let Some(id) = f.active_id() { + let mut config = config.lock().unwrap(); + for mut dict in &mut config.engine.dicts { + if dict.path == path { + dict.encoding = DictEncoding::from(&id).unwrap(); + break; + } + } + } + }); + } + grid.attach(&cbt, 3, i as i32, 1, 1); + } + + { + let delete_btn = { + let path = dict_config.path.clone(); + let config = config.clone(); + let delete_btn = Button::with_label("削除"); + let grid = grid.clone(); + delete_btn.connect_clicked(move |_| { + let mut config = config.lock().unwrap(); + for (i, dict) in &mut config.engine.dicts.iter().enumerate() { + if dict.path == path { + config.engine.dicts.remove(i); + grid.remove_row(i as i32); + break; + } + } + }); + delete_btn + }; + grid.attach(&delete_btn, 4, i as i32, 1, 1); + } +} + +fn build_add_system_dict_btn(config: Arc>, grid: Grid) -> Button { + let add_btn = Button::with_label("システム辞書の追加"); + let config = config; + let grid = grid; + add_btn.connect_clicked(move |_| { + let dialog = FileChooserDialog::new( + Some("辞書の選択"), + None::<&Window>, + FileChooserAction::Open, + &[ + ("開く", ResponseType::Accept), + ("キャンセル", ResponseType::None), + ], + ); + let config = config.clone(); + let grid = grid.clone(); + dialog.connect_response(move |dialog, resp| match resp { + ResponseType::Accept => { + let file = dialog.file().unwrap(); + let path = file.path().unwrap(); + + info!("File: {:?}", path); + let dict_config = &DictConfig { + path: path.to_string_lossy().to_string(), + encoding: DictEncoding::Utf8, + usage: DictUsage::Normal, + dict_type: DictType::SKK, + }; + config + .lock() + .unwrap() + .engine + .dicts + .push(dict_config.clone()); + add_row( + &grid, + dict_config, + &config.clone(), + config.lock().unwrap().engine.dicts.len(), + ); + dialog.close(); + } + ResponseType::Close + | ResponseType::Reject + | ResponseType::Yes + | ResponseType::No + | ResponseType::None + | ResponseType::DeleteEvent => { + dialog.close(); + } + _ => {} + }); + dialog.show(); + }); + add_btn +} + +fn build_add_user_dict_btn(dict_list_grid: Grid, config: Arc>) -> Button { + let add_btn = Button::with_label("ユーザー辞書の追加"); + let config = config; + let dict_list_grid = dict_list_grid; + add_btn.connect_clicked(move |_| { + let window = Window::builder() + .title("ユーザー辞書の追加") + .default_width(300) + .default_height(100) + .destroy_with_parent(true) + .modal(true) + .build(); + + let grid = Grid::builder().build(); + + let label = TextView::builder() + .buffer(&TextBuffer::builder().text("辞書名: ").build()) + .build(); + grid.attach(&label, 0, 0, 1, 1); + + let text = Text::builder().build(); + grid.attach(&text, 1, 0, 2, 1); + + let ok_btn = { + let window = window.clone(); + let ok_btn = Button::with_label("OK"); + let text = text.clone(); + let config = config.clone(); + let dict_list_grid = dict_list_grid.clone(); + ok_btn.set_sensitive(false); + ok_btn.connect_clicked(move |_| match create_user_dict(&text.buffer().text()) { + Ok(path) => { + let dict_config = DictConfig { + path: path.to_string_lossy().to_string(), + encoding: DictEncoding::Utf8, + dict_type: DictType::SKK, + usage: DictUsage::Normal, + }; + let mut locked_conf = config.lock().unwrap(); + add_row( + &dict_list_grid, + &dict_config, + &config, + locked_conf.engine.dicts.len(), + ); + locked_conf.engine.dicts.push(dict_config); + window.close(); + } + Err(err) => { + let dialog = MessageDialogBuilder::new() + .message_type(MessageType::Error) + .text(&format!("Error: {err}")) + .build(); + dialog.show(); + } + }); + grid.attach(&ok_btn, 1, 1, 1, 1); + ok_btn + }; + + { + let window = window.clone(); + let cancel_btn = Button::with_label("Cancel"); + cancel_btn.connect_clicked(move |_| { + window.close(); + }); + grid.attach(&cancel_btn, 2, 1, 1, 1); + } + + // 辞書名を入力していない場合は OK ボタンを押せない。 + text.buffer().connect_length_notify(move |t| { + ok_btn.set_sensitive(!t.text().is_empty()); + }); + + window.set_child(Some(&grid)); + window.show(); + }); + add_btn +} + +fn create_user_dict(name: &str) -> anyhow::Result { + let base = xdg::BaseDirectories::with_prefix("akaza")?; + + let userdictdir = base.create_data_directory("userdict")?; + let path = userdictdir.join(name); + if !path.as_path().exists() { + // ファイルがなければカラの SKK 辞書をつくっておく。 + write_skk_dict(&path.to_string_lossy(), vec![])?; + } + + Ok(path) +} diff --git a/akaza-dict/Cargo.toml b/akaza-dict/Cargo.toml new file mode 100644 index 0000000..a6054bd --- /dev/null +++ b/akaza-dict/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "akaza-dict" +version = "0.1.7" +edition = "2021" +license = "MIT" + +[dependencies] +gtk4 = "0.5.5" +xdg = "2.4.1" +log = "0.4.17" +env_logger = "0.10.0" +libakaza = { path = "../libakaza" } +anyhow = "1.0.68" +serde = "1.0.152" +serde_yaml = "0.9.17" +encoding_rs = "0.8.31" + +[[bin]] +name = "akaza-dict" +path = "src/bin/akaza-dict.rs" diff --git a/akaza-dict/src/bin/akaza-dict.rs b/akaza-dict/src/bin/akaza-dict.rs new file mode 100644 index 0000000..74feb5a --- /dev/null +++ b/akaza-dict/src/bin/akaza-dict.rs @@ -0,0 +1,17 @@ +use anyhow::Result; +use log::LevelFilter; + +use akaza_dict::conf::open_userdict_window; + +use std::env; + +/// デバッグ用 +fn main() -> Result<()> { + let _ = env_logger::builder() + .filter_level(LevelFilter::Info) + .try_init(); + + let args: Vec = env::args().collect(); + open_userdict_window(&args[1])?; + Ok(()) +} diff --git a/akaza-dict/src/conf.rs b/akaza-dict/src/conf.rs new file mode 100644 index 0000000..23b948f --- /dev/null +++ b/akaza-dict/src/conf.rs @@ -0,0 +1,176 @@ +use std::collections::HashMap; +use std::fs; +use std::path::Path; +use std::sync::{Arc, Mutex}; + +use anyhow::Result; +use encoding_rs::UTF_8; +use gtk::glib::signal::Inhibit; +use gtk::prelude::*; +use gtk::{Application, ApplicationWindow, Button, ListStore}; +use gtk4 as gtk; +use gtk4::builders::MessageDialogBuilder; +use gtk4::gio::ApplicationFlags; +use gtk4::glib::Type; + +use gtk4::{CellRendererText, Grid, MessageType, TreeView, TreeViewColumn}; +use log::{info, trace}; + +use libakaza::config::Config; +use libakaza::dict::skk::read::read_skkdict; +use libakaza::dict::skk::write::write_skk_dict; + +pub fn open_userdict_window(user_dict_path: &str) -> Result<()> { + let config = Arc::new(Mutex::new(Config::load()?)); + let app = Application::new(Some("com.github.akaza.config"), ApplicationFlags::empty()); + + let user_dict_path = user_dict_path.to_string(); + app.connect_activate(move |app| { + connect_activate(app, config.clone(), &user_dict_path).unwrap(); + }); + + let v: Vec = Vec::new(); + app.run_with_args(v.as_slice()); + Ok(()) +} + +fn connect_activate( + app: &Application, + _config: Arc>, + user_dict_path: &str, +) -> Result<()> { + let window = ApplicationWindow::builder() + .application(app) + .default_width(520) + .default_height(500) + .title("Akaza の設定") + .build(); + + let grid = Grid::builder().build(); + + info!("Loading skk dict from {user_dict_path}"); + let dict = read_skkdict(Path::new(user_dict_path), UTF_8)?; + let dict = dict + .iter() + .flat_map(|(yomi, surfaces)| { + surfaces + .iter() + .map(|surface| (yomi.to_string(), surface.to_string())) + .collect::>() + }) + .collect::>(); + + let list_store = ListStore::new(&[Type::STRING, Type::STRING]); + for (yomi, surface) in dict { + list_store.set(&list_store.append(), &[(0, &yomi), (1, &surface)]); + } + // list_store.set(&list_store.append(), &[(0, &"world".to_string())]); + let tree_view = TreeView::builder().model(&list_store).build(); + { + let tree_view_column = build_tree_view_column("読み", 0, list_store.clone()); + tree_view.append_column(&tree_view_column); + } + { + let tree_view_column = build_tree_view_column("表記", 1, list_store.clone()); + tree_view.append_column(&tree_view_column); + } + // https://gitlab.gnome.org/GNOME/gtk/-/issues/3561 + grid.attach(&tree_view, 0, 0, 6, 1); + + // TODO このへんは Menu にしたい。gtk4-rs で menu を使う方法が分からん。 + let add_button = Button::with_label("追加"); + { + let list_store = list_store.clone(); + add_button.connect_clicked(move |_| { + info!("Add new row..."); + list_store.set(&list_store.append(), &[(0, &""), (1, &"")]); + }); + } + grid.attach(&add_button, 4, 1, 1, 1); + + { + let delete_btn = Button::with_label("削除"); + let list_store = list_store.clone(); + let tree_view = tree_view; + delete_btn.connect_clicked(move |_| { + let selection = tree_view.selection(); + let Some((_, tree_iter)) = selection.selected() else { + return; + }; + list_store.remove(&tree_iter); + }); + grid.attach(&delete_btn, 5, 1, 1, 1); + } + + { + let save_btn = Button::with_label("保存"); + let user_dict_path = user_dict_path.to_string(); + save_btn.connect_clicked(move |_| { + let Some(iter) = list_store.iter_first() else { + return; + }; + + let mut dict: HashMap> = HashMap::new(); + + loop { + let yomi: String = list_store.get(&iter, 0); + let surface: String = list_store.get(&iter, 1); + info!("Got: {}, {}", yomi, surface); + + dict.entry(yomi).or_insert_with(Vec::new).push(surface); + + if !list_store.iter_next(&iter) { + break; + } + } + + if let Err(err) = write_skk_dict(&(user_dict_path.to_string() + ".tmp"), vec![dict]) { + let dialog = MessageDialogBuilder::new() + .message_type(MessageType::Error) + .text(&format!("Error: {err}")) + .build(); + dialog.show(); + } + info!("Renaming file"); + if let Err(err) = fs::rename(user_dict_path.to_string() + ".tmp", &user_dict_path) { + let dialog = MessageDialogBuilder::new() + .message_type(MessageType::Error) + .text(&format!("Error: {err}")) + .build(); + dialog.show(); + } + }); + grid.attach(&save_btn, 6, 1, 1, 1); + } + + window.set_child(Some(&grid)); + + window.connect_close_request(move |window| { + if let Some(application) = window.application() { + application.remove_window(window); + } + Inhibit(false) + }); + + window.show(); + Ok(()) +} + +fn build_tree_view_column(title: &str, column: u32, list_store: ListStore) -> TreeViewColumn { + let cell_renderer = CellRendererText::builder() + .editable(true) + .xpad(20) + .ypad(20) + .build(); + cell_renderer.connect_edited(move |_cell_renderer, _treepath, _str| { + trace!("{:?}, {:?}", _treepath, _str); + if _str.is_empty() { + return; + } + let Some(iter) = list_store.iter(&_treepath) else { + return; + }; + list_store.set_value(&iter, column, &_str.to_value()); + }); + TreeViewColumn::with_attributes(title, &cell_renderer, &[("text", column as i32)]) +} diff --git a/akaza-dict/src/lib.rs b/akaza-dict/src/lib.rs new file mode 100644 index 0000000..1f3b828 --- /dev/null +++ b/akaza-dict/src/lib.rs @@ -0,0 +1 @@ +pub mod conf; diff --git a/ibus-akaza/Cargo.toml b/ibus-akaza/Cargo.toml index 020da90..a52be3c 100644 --- a/ibus-akaza/Cargo.toml +++ b/ibus-akaza/Cargo.toml @@ -13,6 +13,7 @@ log = "0.4.17" libakaza = { path = "../libakaza" } ibus-sys = { path = "../ibus-sys" } akaza-conf = { path = "../akaza-conf" } +akaza-dict = { path = "../akaza-dict" } env_logger = "0.10.0" clap = { version = "4.1.1", features = ["derive"] } clap-verbosity-flag = "2.0.0" diff --git a/ibus-akaza/src/context.rs b/ibus-akaza/src/context.rs index abcd1e5..da7af95 100644 --- a/ibus-akaza/src/context.rs +++ b/ibus-akaza/src/context.rs @@ -2,9 +2,10 @@ use std::collections::HashMap; use anyhow::Result; use kelp::{h2z, hira2kata, z2h, ConvOption}; -use log::{debug, error, info, trace, warn}; +use log::{error, info, trace, warn}; use akaza_conf::conf::open_configuration_window; +use akaza_dict::conf::open_userdict_window; use ibus_sys::core::{ IBusModifierType_IBUS_CONTROL_MASK, IBusModifierType_IBUS_HYPER_MASK, IBusModifierType_IBUS_META_MASK, IBusModifierType_IBUS_MOD1_MASK, @@ -65,7 +66,7 @@ impl AkazaContext { current_state: CurrentState::new(input_mode, config.live_conversion, romkan, engine), command_map: ibus_akaza_commands_map(), keymap: IBusKeyMap::new(keymap)?, - prop_controller: PropController::new(input_mode)?, + prop_controller: PropController::new(input_mode, config)?, }) } @@ -76,7 +77,7 @@ impl AkazaContext { prop_name: String, prop_state: guint, ) { - debug!("do_property_activate: {}, {}", prop_name, prop_state); + info!("do_property_activate: {}, {}", prop_name, prop_state); if prop_name == "PrefPane" { match open_configuration_window() { Ok(_) => {} @@ -86,6 +87,14 @@ impl AkazaContext { && prop_name.starts_with("InputMode.") { self.input_mode_activate(engine, prop_name, prop_state); + } else if prop_name.starts_with("UserDict.") { + let dict_path = prop_name.replace("UserDict.", ""); + info!("Edit the {}", dict_path); + + match open_userdict_window(&dict_path) { + Ok(_) => {} + Err(e) => error!("Err: {}", e), + } } } diff --git a/ibus-akaza/src/ui/prop_controller.rs b/ibus-akaza/src/ui/prop_controller.rs index 0dfa83d..0b13952 100644 --- a/ibus-akaza/src/ui/prop_controller.rs +++ b/ibus-akaza/src/ui/prop_controller.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::path::Path; use anyhow::Result; @@ -13,6 +14,7 @@ use ibus_sys::property::{ IBusProperty, }; use ibus_sys::text::{IBusText, StringExt}; +use libakaza::config::{Config, DictConfig}; use crate::input_mode::{get_all_input_modes, InputMode}; @@ -25,8 +27,8 @@ pub struct PropController { } impl PropController { - pub fn new(initial_input_mode: InputMode) -> Result { - let (input_mode_prop, prop_list, prop_dict) = Self::init_props(initial_input_mode); + pub fn new(initial_input_mode: InputMode, config: Config) -> Result { + let (input_mode_prop, prop_list, prop_dict) = Self::init_props(initial_input_mode, config)?; Ok(PropController { prop_list, @@ -47,11 +49,12 @@ impl PropController { /// * `initial_input_mode`: 初期状態の input_mode fn init_props( initial_input_mode: InputMode, - ) -> ( + config: Config, + ) -> Result<( *mut IBusProperty, *mut IBusPropList, HashMap, - ) { + )> { unsafe { let prop_list = g_object_ref_sink(ibus_prop_list_new() as gpointer) as *mut IBusPropList; @@ -90,25 +93,83 @@ impl PropController { prop_map.insert(input_mode.prop_name.to_string(), prop); ibus_prop_list_append(props, prop); } - ibus_property_set_sub_props(input_mode_prop, props); + // ユーザー辞書 + Self::build_user_dict(prop_list, config)?; + // 設定ファイルを開くというやつ - let preference_prop = g_object_ref_sink(ibus_property_new( - "PrefPane\0".as_ptr() as *const gchar, + Self::build_preference_menu(prop_list); + + Ok((input_mode_prop, prop_list, prop_map)) + } + } + + unsafe fn build_user_dict(prop_list: *mut IBusPropList, config: Config) -> Result<()> { + let user_dict_prop = g_object_ref_sink(ibus_property_new( + "UserDict\0".as_ptr() as *const gchar, + IBusPropType_PROP_TYPE_MENU, + "ユーザー辞書".to_ibus_text(), + "\0".as_ptr() as *const gchar, + "User dict".to_ibus_text(), + to_gboolean(true), + to_gboolean(true), + IBusPropState_PROP_STATE_UNCHECKED, + std::ptr::null_mut() as *mut IBusPropList, + ) as gpointer) as *mut IBusProperty; + ibus_prop_list_append(prop_list, user_dict_prop); + + let props = g_object_ref_sink(ibus_prop_list_new() as gpointer) as *mut IBusPropList; + for dict in Self::find_user_dicts(config)? { + let prop = g_object_ref_sink(ibus_property_new( + ("UserDict.".to_string() + dict.path.as_str() + "\0").as_ptr() as *const gchar, IBusPropType_PROP_TYPE_MENU, - "設定".to_ibus_text(), + Path::new(&dict.path) + .file_name() + .unwrap() + .to_string_lossy() + .to_ibus_text(), "\0".as_ptr() as *const gchar, - "Preference".to_ibus_text(), + std::ptr::null_mut() as *mut IBusText, to_gboolean(true), to_gboolean(true), IBusPropState_PROP_STATE_UNCHECKED, std::ptr::null_mut() as *mut IBusPropList, ) as gpointer) as *mut IBusProperty; - ibus_prop_list_append(prop_list, preference_prop); - - (input_mode_prop, prop_list, prop_map) + // prop_map.insert(input_mode.prop_name.to_string(), prop); + ibus_prop_list_append(props, prop); } + ibus_property_set_sub_props(user_dict_prop, props); + Ok(()) + } + + fn find_user_dicts(config: Config) -> anyhow::Result> { + let dir = xdg::BaseDirectories::with_prefix("akaza")?; + let dir = dir.create_data_directory("userdict")?; + let dicts = config + .engine + .dicts + .iter() + .filter(|f| f.path.contains(&dir.to_string_lossy().to_string())) + .cloned() + .collect::>(); + + Ok(dicts) + } + + unsafe fn build_preference_menu(prop_list: *mut IBusPropList) { + let preference_prop = g_object_ref_sink(ibus_property_new( + "PrefPane\0".as_ptr() as *const gchar, + IBusPropType_PROP_TYPE_MENU, + "設定".to_ibus_text(), + "\0".as_ptr() as *const gchar, + "Preference".to_ibus_text(), + to_gboolean(true), + to_gboolean(true), + IBusPropState_PROP_STATE_UNCHECKED, + std::ptr::null_mut() as *mut IBusPropList, + ) as gpointer) as *mut IBusProperty; + ibus_prop_list_append(prop_list, preference_prop); } /// input_mode の切り替え時に実行される処理