User dict (#286)

* refactor dict-pane

* refactor

* add user_dict adding feature

* clippy --fix

* cargo fmt

* clippy fix

* optimize imports

* user dictionary editing menu

* open dict

* show surface

* add row

* implement delete button

* refacor

* save user dict

* clippy fix
This commit is contained in:
Tokuhiro Matsuno
2023-02-12 00:06:09 +09:00
committed by GitHub
parent ca3520eea9
commit 9e227a25b0
10 changed files with 598 additions and 186 deletions

16
Cargo.lock generated
View File

@ -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",

View File

@ -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"]

View File

@ -1,18 +1,55 @@
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<Mutex<Config>>) -> anyhow::Result<ScrolledWindow> {
// TODO ここは TreeView 使った方がすっきり書けるはずだが、僕の GTK+ 力が引くすぎて対応できていない。
// 誰かすっきり使い易くしてほしい。
fn add_row(grid: &Grid, dict_config: &DictConfig, config: &Arc<Mutex<Config>>, i: usize) {
let scroll = ScrolledWindow::new();
let parent_grid = Grid::builder().column_spacing(10).build();
let grid = Grid::builder().column_spacing(10).build();
let dicts = config.lock().unwrap().engine.dicts.clone();
for (i, dict_config) in dicts.iter().enumerate() {
add_row(&grid, dict_config, &config.clone(), i);
}
parent_grid.attach(&grid, 0, 0, 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<Mutex<Config>>, i: usize) {
grid.attach(
&Label::builder()
.xalign(0_f32)
@ -112,24 +149,10 @@ pub fn build_dict_pane(config: Arc<Mutex<Config>>) -> anyhow::Result<ScrolledWin
};
grid.attach(&delete_btn, 4, i as i32, 1, 1);
}
}
}
let scroll = ScrolledWindow::new();
let parent_grid = Grid::builder().column_spacing(10).build();
let grid = Grid::builder().column_spacing(10).build();
let dicts = config.lock().unwrap().engine.dicts.clone();
for (i, dict_config) in dicts.iter().enumerate() {
add_row(&grid, dict_config, &config.clone(), i);
}
parent_grid.attach(&grid, 0, 0, 1, 1);
{
let add_btn = {
let add_btn = Button::with_label("Add");
fn build_add_system_dict_btn(config: Arc<Mutex<Config>>, grid: Grid) -> Button {
let add_btn = Button::with_label("システム辞書の追加");
let config = config;
let grid = grid;
add_btn.connect_clicked(move |_| {
@ -183,9 +206,97 @@ pub fn build_dict_pane(config: Arc<Mutex<Config>>) -> anyhow::Result<ScrolledWin
dialog.show();
});
add_btn
};
parent_grid.attach(&add_btn, 0, 1, 1, 1);
}
scroll.set_child(Some(&parent_grid));
Ok(scroll)
}
fn build_add_user_dict_btn(dict_list_grid: Grid, config: Arc<Mutex<Config>>) -> 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<PathBuf> {
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)
}

20
akaza-dict/Cargo.toml Normal file
View File

@ -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"

View File

@ -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<String> = env::args().collect();
open_userdict_window(&args[1])?;
Ok(())
}

176
akaza-dict/src/conf.rs Normal file
View File

@ -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<String> = Vec::new();
app.run_with_args(v.as_slice());
Ok(())
}
fn connect_activate(
app: &Application,
_config: Arc<Mutex<Config>>,
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::<Vec<_>>()
})
.collect::<Vec<_>>();
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<String, Vec<String>> = 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)])
}

1
akaza-dict/src/lib.rs Normal file
View File

@ -0,0 +1 @@
pub mod conf;

View File

@ -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"

View File

@ -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),
}
}
}

View File

@ -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<Self> {
let (input_mode_prop, prop_list, prop_dict) = Self::init_props(initial_input_mode);
pub fn new(initial_input_mode: InputMode, config: Config) -> Result<Self> {
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<String, *mut IBusProperty>,
) {
)> {
unsafe {
let prop_list =
g_object_ref_sink(ibus_prop_list_new() as gpointer) as *mut IBusPropList;
@ -90,10 +93,71 @@ 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)?;
// 設定ファイルを開くというやつ
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,
Path::new(&dict.path)
.file_name()
.unwrap()
.to_string_lossy()
.to_ibus_text(),
"\0".as_ptr() as *const gchar,
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;
// 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<Vec<DictConfig>> {
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::<Vec<_>>();
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,
@ -106,9 +170,6 @@ impl PropController {
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)
}
}
/// input_mode の切り替え時に実行される処理