mirror of
https://github.com/mii443/akaza.git
synced 2025-08-22 14:55:31 +00:00
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:
16
Cargo.lock
generated
16
Cargo.lock
generated
@ -58,6 +58,21 @@ dependencies = [
|
|||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "akaza-dict"
|
||||||
|
version = "0.1.7"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"encoding_rs",
|
||||||
|
"env_logger",
|
||||||
|
"gtk4",
|
||||||
|
"libakaza",
|
||||||
|
"log",
|
||||||
|
"serde",
|
||||||
|
"serde_yaml",
|
||||||
|
"xdg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "android_system_properties"
|
name = "android_system_properties"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@ -963,6 +978,7 @@ name = "ibus-akaza"
|
|||||||
version = "0.1.7"
|
version = "0.1.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"akaza-conf",
|
"akaza-conf",
|
||||||
|
"akaza-dict",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"cc",
|
"cc",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
[workspace]
|
[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"]
|
||||||
|
@ -1,119 +1,27 @@
|
|||||||
use gtk4::prelude::{
|
use std::path::PathBuf;
|
||||||
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::sync::{Arc, Mutex};
|
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> {
|
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) {
|
|
||||||
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 scroll = ScrolledWindow::new();
|
||||||
|
|
||||||
let parent_grid = Grid::builder().column_spacing(10).build();
|
let parent_grid = Grid::builder().column_spacing(10).build();
|
||||||
@ -128,64 +36,267 @@ pub fn build_dict_pane(config: Arc<Mutex<Config>>) -> anyhow::Result<ScrolledWin
|
|||||||
parent_grid.attach(&grid, 0, 0, 1, 1);
|
parent_grid.attach(&grid, 0, 0, 1, 1);
|
||||||
|
|
||||||
{
|
{
|
||||||
let add_btn = {
|
let add_system_dict_btn = build_add_system_dict_btn(config.clone(), grid.clone());
|
||||||
let add_btn = Button::with_label("Add");
|
parent_grid.attach(&add_system_dict_btn, 0, 1, 1, 1);
|
||||||
let config = config;
|
}
|
||||||
let grid = grid;
|
{
|
||||||
add_btn.connect_clicked(move |_| {
|
let add_user_dict_btn = build_add_user_dict_btn(grid, config);
|
||||||
let dialog = FileChooserDialog::new(
|
parent_grid.attach(&add_user_dict_btn, 0, 2, 1, 1);
|
||||||
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
|
|
||||||
};
|
|
||||||
parent_grid.attach(&add_btn, 0, 1, 1, 1);
|
|
||||||
}
|
}
|
||||||
scroll.set_child(Some(&parent_grid));
|
scroll.set_child(Some(&parent_grid));
|
||||||
Ok(scroll)
|
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)
|
||||||
|
.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<Mutex<Config>>, 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<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
20
akaza-dict/Cargo.toml
Normal 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"
|
17
akaza-dict/src/bin/akaza-dict.rs
Normal file
17
akaza-dict/src/bin/akaza-dict.rs
Normal 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
176
akaza-dict/src/conf.rs
Normal 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
1
akaza-dict/src/lib.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod conf;
|
@ -13,6 +13,7 @@ log = "0.4.17"
|
|||||||
libakaza = { path = "../libakaza" }
|
libakaza = { path = "../libakaza" }
|
||||||
ibus-sys = { path = "../ibus-sys" }
|
ibus-sys = { path = "../ibus-sys" }
|
||||||
akaza-conf = { path = "../akaza-conf" }
|
akaza-conf = { path = "../akaza-conf" }
|
||||||
|
akaza-dict = { path = "../akaza-dict" }
|
||||||
env_logger = "0.10.0"
|
env_logger = "0.10.0"
|
||||||
clap = { version = "4.1.1", features = ["derive"] }
|
clap = { version = "4.1.1", features = ["derive"] }
|
||||||
clap-verbosity-flag = "2.0.0"
|
clap-verbosity-flag = "2.0.0"
|
||||||
|
@ -2,9 +2,10 @@ use std::collections::HashMap;
|
|||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use kelp::{h2z, hira2kata, z2h, ConvOption};
|
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_conf::conf::open_configuration_window;
|
||||||
|
use akaza_dict::conf::open_userdict_window;
|
||||||
use ibus_sys::core::{
|
use ibus_sys::core::{
|
||||||
IBusModifierType_IBUS_CONTROL_MASK, IBusModifierType_IBUS_HYPER_MASK,
|
IBusModifierType_IBUS_CONTROL_MASK, IBusModifierType_IBUS_HYPER_MASK,
|
||||||
IBusModifierType_IBUS_META_MASK, IBusModifierType_IBUS_MOD1_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),
|
current_state: CurrentState::new(input_mode, config.live_conversion, romkan, engine),
|
||||||
command_map: ibus_akaza_commands_map(),
|
command_map: ibus_akaza_commands_map(),
|
||||||
keymap: IBusKeyMap::new(keymap)?,
|
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_name: String,
|
||||||
prop_state: guint,
|
prop_state: guint,
|
||||||
) {
|
) {
|
||||||
debug!("do_property_activate: {}, {}", prop_name, prop_state);
|
info!("do_property_activate: {}, {}", prop_name, prop_state);
|
||||||
if prop_name == "PrefPane" {
|
if prop_name == "PrefPane" {
|
||||||
match open_configuration_window() {
|
match open_configuration_window() {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
@ -86,6 +87,14 @@ impl AkazaContext {
|
|||||||
&& prop_name.starts_with("InputMode.")
|
&& prop_name.starts_with("InputMode.")
|
||||||
{
|
{
|
||||||
self.input_mode_activate(engine, prop_name, prop_state);
|
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),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
@ -13,6 +14,7 @@ use ibus_sys::property::{
|
|||||||
IBusProperty,
|
IBusProperty,
|
||||||
};
|
};
|
||||||
use ibus_sys::text::{IBusText, StringExt};
|
use ibus_sys::text::{IBusText, StringExt};
|
||||||
|
use libakaza::config::{Config, DictConfig};
|
||||||
|
|
||||||
use crate::input_mode::{get_all_input_modes, InputMode};
|
use crate::input_mode::{get_all_input_modes, InputMode};
|
||||||
|
|
||||||
@ -25,8 +27,8 @@ pub struct PropController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl PropController {
|
impl PropController {
|
||||||
pub fn new(initial_input_mode: InputMode) -> Result<Self> {
|
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);
|
let (input_mode_prop, prop_list, prop_dict) = Self::init_props(initial_input_mode, config)?;
|
||||||
|
|
||||||
Ok(PropController {
|
Ok(PropController {
|
||||||
prop_list,
|
prop_list,
|
||||||
@ -47,11 +49,12 @@ impl PropController {
|
|||||||
/// * `initial_input_mode`: 初期状態の input_mode
|
/// * `initial_input_mode`: 初期状態の input_mode
|
||||||
fn init_props(
|
fn init_props(
|
||||||
initial_input_mode: InputMode,
|
initial_input_mode: InputMode,
|
||||||
) -> (
|
config: Config,
|
||||||
|
) -> Result<(
|
||||||
*mut IBusProperty,
|
*mut IBusProperty,
|
||||||
*mut IBusPropList,
|
*mut IBusPropList,
|
||||||
HashMap<String, *mut IBusProperty>,
|
HashMap<String, *mut IBusProperty>,
|
||||||
) {
|
)> {
|
||||||
unsafe {
|
unsafe {
|
||||||
let prop_list =
|
let prop_list =
|
||||||
g_object_ref_sink(ibus_prop_list_new() as gpointer) as *mut IBusPropList;
|
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);
|
prop_map.insert(input_mode.prop_name.to_string(), prop);
|
||||||
ibus_prop_list_append(props, prop);
|
ibus_prop_list_append(props, prop);
|
||||||
}
|
}
|
||||||
|
|
||||||
ibus_property_set_sub_props(input_mode_prop, props);
|
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(
|
Self::build_preference_menu(prop_list);
|
||||||
"PrefPane\0".as_ptr() as *const gchar,
|
|
||||||
|
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,
|
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,
|
"\0".as_ptr() as *const gchar,
|
||||||
"Preference".to_ibus_text(),
|
std::ptr::null_mut() as *mut IBusText,
|
||||||
to_gboolean(true),
|
to_gboolean(true),
|
||||||
to_gboolean(true),
|
to_gboolean(true),
|
||||||
IBusPropState_PROP_STATE_UNCHECKED,
|
IBusPropState_PROP_STATE_UNCHECKED,
|
||||||
std::ptr::null_mut() as *mut IBusPropList,
|
std::ptr::null_mut() as *mut IBusPropList,
|
||||||
) as gpointer) as *mut IBusProperty;
|
) as gpointer) as *mut IBusProperty;
|
||||||
ibus_prop_list_append(prop_list, preference_prop);
|
// prop_map.insert(input_mode.prop_name.to_string(), prop);
|
||||||
|
ibus_prop_list_append(props, prop);
|
||||||
(input_mode_prop, prop_list, prop_map)
|
|
||||||
}
|
}
|
||||||
|
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,
|
||||||
|
"設定".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 の切り替え時に実行される処理
|
/// input_mode の切り替え時に実行される処理
|
||||||
|
Reference in New Issue
Block a user