use fuse_rust::Fuse;
use gdk::{keys, EventKey};
use gio::prelude::*;
use gladis::Gladis;
use gtk::prelude::*;
use gtk::{Dialog, Entry, TreePath, TreeView, Window};
use once_cell::unsync::OnceCell;
use shrinkwraprs::Shrinkwrap;
use crate::{script::Script, scripts::ScriptMap};
use glib::Type;
use std::{
collections::HashMap,
rc::Rc,
sync::{Arc, RwLock},
};
const ICON_COLUMN: u32 = 0;
const TEXT_COLUMN: u32 = 1;
const NAME_COLUMN: u32 = 2;
const SCORE_COLUMN: u32 = 3;
const VISIBLE_COLUMN: u32 = 4;
const COLUMNS: [u32; 5] = [
ICON_COLUMN,
TEXT_COLUMN,
NAME_COLUMN,
SCORE_COLUMN,
VISIBLE_COLUMN,
];
const COLUMN_TYPES: [Type; 5] = [
Type::String,
Type::String,
Type::String,
Type::F64,
Type::Bool,
];
const DIALOG_WIDTH: i32 = 300;
const ICON_COLUMN_PADDING: i32 = 8;
const ICON_COLUMN_WIDTH: i32 = ICON_COLUMN_PADDING + 32 + ICON_COLUMN_PADDING; const TEXT_COLUMN_WIDTH: i32 = DIALOG_WIDTH - ICON_COLUMN_WIDTH;
#[derive(Shrinkwrap, Gladis)]
pub struct CommandPalleteDialogWidgets {
#[shrinkwrap(main_field)]
dialog: Dialog,
dialog_tree_view: TreeView,
search_bar: Entry,
}
#[derive(Shrinkwrap)]
pub struct CommandPalleteDialog {
#[shrinkwrap(main_field)]
widgets: CommandPalleteDialogWidgets,
scripts: Arc<RwLock<ScriptMap>>,
selected_script: Rc<OnceCell<String>>,
}
impl CommandPalleteDialog {
pub(crate) fn new<P: IsA<Window>>(window: &P, scripts: Arc<RwLock<ScriptMap>>) -> Self {
let widgets =
CommandPalleteDialogWidgets::from_resource("/fyi/zoey/Boop-GTK/command-pallete.glade")
.unwrap_or_else(|e| panic!("failed to load command-pallete.glade: {}", e));
let command_pallete_dialog = CommandPalleteDialog {
widgets,
scripts: scripts.clone(),
selected_script: Rc::new(OnceCell::new()),
};
command_pallete_dialog.set_transient_for(Some(window));
{
let store = gtk::ListStore::new(&COLUMN_TYPES);
store.set_sort_column_id(
gtk::SortColumn::Index(SCORE_COLUMN),
gtk::SortType::Descending,
);
let filtered_store = gtk::TreeModelFilter::new(&store, None);
filtered_store.set_visible_column(VISIBLE_COLUMN as i32);
{
let renderer = gtk::CellRendererPixbuf::new();
renderer.set_padding(ICON_COLUMN_PADDING, ICON_COLUMN_PADDING);
renderer.set_property_stock_size(gtk::IconSize::Dnd);
let column = gtk::TreeViewColumn::new();
column.pack_start(&renderer, false);
column.add_attribute(&renderer, "icon-name", ICON_COLUMN as i32);
command_pallete_dialog
.dialog_tree_view
.append_column(&column);
}
{
let renderer = gtk::CellRendererText::new();
renderer.set_property_wrap_mode(pango::WrapMode::Word);
renderer.set_property_wrap_width(TEXT_COLUMN_WIDTH - 8);
let column = gtk::TreeViewColumn::new();
column.pack_start(&renderer, true);
column.set_sizing(gtk::TreeViewColumnSizing::Autosize);
column.set_max_width(TEXT_COLUMN_WIDTH);
column.add_attribute(&renderer, "markup", TEXT_COLUMN as i32);
command_pallete_dialog
.dialog_tree_view
.append_column(&column);
}
#[cfg(debug_assertions)]
{
for c in &[NAME_COLUMN, SCORE_COLUMN] {
let renderer = gtk::CellRendererText::new();
let column = gtk::TreeViewColumn::new();
column.pack_start(&renderer, false);
column.add_attribute(&renderer, "markup", *c as i32);
command_pallete_dialog
.dialog_tree_view
.append_column(&column);
}
}
for (index, (name, script)) in scripts
.read()
.expect("scripts lock is poisoned")
.0
.iter()
.enumerate()
{
let mut icon_name = script.metadata.icon.to_lowercase();
icon_name.insert_str(0, "boop-gtk-");
icon_name.push_str("-symbolic");
let entry_text = format!(
"<b>{}</b>\n<span size=\"smaller\">{}</span>",
script.metadata.name.to_string(),
script.metadata.description.to_string()
);
let values: [&dyn ToValue; 5] =
[&icon_name, &entry_text, &name, &(-(index as i64)), &true];
store.set(&store.append(), &COLUMNS, &values);
}
command_pallete_dialog
.dialog_tree_view
.set_model(Some(&filtered_store));
}
command_pallete_dialog.dialog_tree_view.set_cursor(
&TreePath::new_first(),
gtk::NONE_TREE_VIEW_COLUMN,
false,
);
command_pallete_dialog.register_handlers();
command_pallete_dialog
}
pub(crate) fn get_selected(&self) -> Option<&String> {
self.selected_script.get()
}
fn register_handlers(&self) {
{
let lb = self.dialog_tree_view.clone();
let dialog = self.dialog.clone();
let selected = self.selected_script.clone();
self.dialog.connect_key_press_event(move |_, k| {
CommandPalleteDialog::on_key_press(k, &lb, &dialog, &selected)
});
}
{
let lb = self.dialog_tree_view.clone();
let scripts = self.scripts.clone();
self.search_bar.connect_changed(move |s| {
CommandPalleteDialog::on_changed(s, &lb, scripts.clone())
});
}
{
let dialog = self.dialog.clone();
let selected = self.selected_script.clone();
self.dialog_tree_view
.connect_row_activated(move |tv, _, _| {
CommandPalleteDialog::on_click(tv, &dialog, &selected)
});
}
}
fn on_key_press(
key: &EventKey,
dialog_tree_view: &TreeView,
dialog: &Dialog,
selected: &OnceCell<String>,
) -> Inhibit {
let model: gtk::TreeModelFilter = dialog_tree_view.get_model().unwrap().downcast().unwrap();
let result_count: i32 = model.iter_n_children(None);
let key = key.get_keyval();
if key == keys::constants::Up || key == keys::constants::Down {
if let (Some(mut path), _) = dialog_tree_view.get_cursor() {
let index: i32 = path.get_indices()[0];
match key {
keys::constants::Up => {
if index == 0 {
path = TreePath::from_indicesv(&[result_count - 1]);
} else {
path.prev();
}
}
keys::constants::Down => {
if index >= result_count - 1 {
path = TreePath::new_first();
} else {
path.next();
}
}
_ => (),
};
dialog_tree_view.set_cursor(&path, gtk::NONE_TREE_VIEW_COLUMN, false);
}
return Inhibit(true);
} else if key == keys::constants::Return {
CommandPalleteDialog::on_click(dialog_tree_view, dialog, selected);
} else if key == keys::constants::Escape {
dialog.close();
}
Inhibit(false)
}
fn on_click(dialog_tree_view: &TreeView, dialog: &Dialog, selected: &OnceCell<String>) {
let model: gtk::TreeModelFilter = dialog_tree_view.get_model().unwrap().downcast().unwrap();
if let (Some(path), _) = dialog_tree_view.get_cursor() {
let value = model.get_value(
&model
.get_iter(&path)
.unwrap_or_else(|| panic!("failed to get iter for path: {:?}", path)),
NAME_COLUMN as i32,
);
let value_string = value
.downcast::<String>()
.expect("cannot downcast value to String")
.get();
if let Some(v) = value_string {
debug!("v: {}", v);
selected.set(v).unwrap();
debug!("selected: {:?}", selected.get());
}
dialog.response(gtk::ResponseType::Accept);
}
}
fn on_changed(searchbar: &Entry, dialog_tree_view: &TreeView, scripts: Arc<RwLock<ScriptMap>>) {
let filter_store: gtk::TreeModelFilter =
dialog_tree_view.get_model().unwrap().downcast().unwrap();
let store: gtk::ListStore = filter_store.get_model().unwrap().downcast().unwrap();
store.set_unsorted();
let searchbar_text = searchbar.get_text().to_owned();
let script_count = store.iter_n_children(None);
let scripts_ref = scripts.read().expect("scripts lock is poisoned");
let script_vec = scripts_ref.0.values().collect::<Vec<&Script>>();
if searchbar_text.is_empty() {
let script_order: HashMap<String, usize> = scripts_ref
.0
.iter()
.enumerate()
.map(|(idx, (name, _))| (name.clone(), idx))
.collect();
for i in 0..script_count {
let mut path = gtk::TreePath::new();
path.append_index(i);
let iter = store
.get_iter(&path)
.unwrap_or_else(|| panic!("failed to get iter for path: {:?}", path));
let script_name: String = store
.get_value(&iter, NAME_COLUMN as i32)
.get()
.unwrap()
.unwrap();
let score = script_order[&script_name] as f64;
let visible = true;
let values: [&dyn ToValue; 2] = [&score, &visible];
store.set(&iter, &[SCORE_COLUMN, VISIBLE_COLUMN], &values);
}
} else {
let results: HashMap<String, f64> = Fuse::default()
.search_text_in_fuse_list(&searchbar_text, &*script_vec)
.into_iter()
.map(|result| (script_vec[result.index].metadata.name.clone(), result.score))
.collect();
for i in 0..script_count {
let mut path = gtk::TreePath::new();
path.append_index(i);
let iter = store
.get_iter(&path)
.unwrap_or_else(|| panic!("failed to get iter for path: {:?}", path));
let script_name: String = store
.get_value(&iter, NAME_COLUMN as i32)
.get()
.unwrap()
.unwrap();
let score = *results.get(&script_name).unwrap_or(&0.0);
let visible = results.contains_key(&script_name);
let values: [&dyn ToValue; 2] = [&score, &visible];
store.set(&iter, &[SCORE_COLUMN, VISIBLE_COLUMN], &values);
}
}
store.set_sort_column_id(
gtk::SortColumn::Index(SCORE_COLUMN),
gtk::SortType::Ascending,
);
dialog_tree_view.set_cursor(&TreePath::new_first(), gtk::NONE_TREE_VIEW_COLUMN, false);
}
}