use single click for selection and double click for activation, hide suggestions on escape, replace request trait with struct

This commit is contained in:
yggverse 2025-03-10 21:28:48 +02:00
parent 3e4423eca7
commit 4a2996d3b7
6 changed files with 235 additions and 245 deletions

View file

@ -84,7 +84,8 @@ impl Page {
/// Request `Escape` action for all page components /// Request `Escape` action for all page components
pub fn escape(&self) { pub fn escape(&self) {
self.search.hide() self.search.hide();
self.navigation.escape();
} }
/// Toggle `Find` widget /// Toggle `Find` widget

View file

@ -11,7 +11,7 @@ use bookmark::Bookmark;
use gtk::{ use gtk::{
glib::{GString, Uri}, glib::{GString, Uri},
prelude::{BoxExt, EditableExt, EntryExt, WidgetExt}, prelude::{BoxExt, EditableExt, EntryExt, WidgetExt},
Box, Button, Entry, Orientation, Box, Button, Orientation,
}; };
use history::History; use history::History;
use home::Home; use home::Home;
@ -24,8 +24,7 @@ const MARGIN: i32 = 6;
const SPACING: i32 = 6; const SPACING: i32 = 6;
pub struct Navigation { pub struct Navigation {
profile: Rc<Profile>, request: Rc<Request>,
request: Entry,
pub g_box: Box, pub g_box: Box,
} }
@ -40,10 +39,10 @@ impl Navigation {
) -> Self { ) -> Self {
// Init children components // Init children components
let history = Box::history((window_action, tab_action, item_action)); let history = Box::history((window_action, tab_action, item_action));
let request = Entry::request(item_action, profile); let request = Rc::new(Request::build(item_action, profile));
let reload = Button::reload((window_action, tab_action, item_action), &request); let reload = Button::reload((window_action, tab_action, item_action), &request);
let home = Button::home((window_action, tab_action, item_action), &request); let home = Button::home((window_action, tab_action, item_action), &request);
let bookmark = Button::bookmark(window_action, profile, &request); let bookmark = Button::bookmark(window_action, profile, &request.entry);
// Init main widget // Init main widget
let g_box = Box::builder() let g_box = Box::builder()
@ -57,18 +56,18 @@ impl Navigation {
g_box.append(&home); g_box.append(&home);
g_box.append(&history); g_box.append(&history);
g_box.append(&reload); g_box.append(&reload);
g_box.append(&request); g_box.append(&request.entry);
g_box.append(&bookmark); g_box.append(&bookmark);
Self { Self { request, g_box }
profile: profile.clone(),
request,
g_box,
}
} }
// Actions // Actions
pub fn escape(&self) {
self.request.escape();
}
pub fn clean( pub fn clean(
&self, &self,
transaction: &Transaction, transaction: &Transaction,
@ -106,21 +105,21 @@ impl Navigation {
} }
pub fn grab_focus(&self) -> bool { pub fn grab_focus(&self) -> bool {
self.request.grab_focus() self.request.entry.grab_focus()
} }
pub fn show_identity_dialog(&self) { pub fn show_identity_dialog(&self) {
self.request.show_identity_dialog(&self.profile) self.request.show_identity_dialog()
} }
// Setters // Setters
pub fn set_request(&self, value: &str) { pub fn set_request(&self, value: &str) {
self.request.set_text(value); self.request.entry.set_text(value);
} }
pub fn set_progress_fraction(&self, value: f64) { pub fn set_progress_fraction(&self, value: f64) {
self.request.set_progress_fraction(value); self.request.entry.set_progress_fraction(value);
} }
pub fn to_download(&self) { pub fn to_download(&self) {
@ -134,7 +133,7 @@ impl Navigation {
// Getters // Getters
pub fn request(&self) -> GString { pub fn request(&self) -> GString {
self.request.text() self.request.entry.text()
} }
pub fn home(&self) -> Option<Uri> { pub fn home(&self) -> Option<Uri> {

View file

@ -3,12 +3,15 @@ use crate::app::browser::window::action::Position;
use gtk::{ use gtk::{
gdk::BUTTON_MIDDLE, gdk::BUTTON_MIDDLE,
prelude::{ActionExt, WidgetExt}, prelude::{ActionExt, WidgetExt},
Button, Entry, GestureClick, Button, GestureClick,
}; };
use std::rc::Rc; use std::rc::Rc;
pub trait Home { pub trait Home {
fn home(action: (&Rc<WindowAction>, &Rc<TabAction>, &Rc<ItemAction>), request: &Entry) -> Self; fn home(
action: (&Rc<WindowAction>, &Rc<TabAction>, &Rc<ItemAction>),
request: &Rc<Request>,
) -> Self;
} }
impl Home for Button { impl Home for Button {
@ -18,7 +21,7 @@ impl Home for Button {
&Rc<TabAction>, &Rc<TabAction>,
&Rc<ItemAction>, &Rc<ItemAction>,
), ),
request: &Entry, request: &Rc<Request>,
) -> Self { ) -> Self {
let button = Button::builder() let button = Button::builder()
.action_name(format!("{}.{}", tab_action.id, item_action.home.name())) .action_name(format!("{}.{}", tab_action.id, item_action.home.name()))

View file

@ -3,14 +3,14 @@ use crate::app::browser::window::action::Position;
use gtk::{ use gtk::{
gdk::BUTTON_MIDDLE, gdk::BUTTON_MIDDLE,
prelude::{ActionExt, WidgetExt}, prelude::{ActionExt, WidgetExt},
Button, Entry, GestureClick, Button, GestureClick,
}; };
use std::rc::Rc; use std::rc::Rc;
pub trait Reload { pub trait Reload {
fn reload( fn reload(
action: (&Rc<WindowAction>, &Rc<TabAction>, &Rc<ItemAction>), action: (&Rc<WindowAction>, &Rc<TabAction>, &Rc<ItemAction>),
request: &Entry, request: &Rc<Request>,
) -> Self; ) -> Self;
} }
@ -21,7 +21,7 @@ impl Reload for Button {
&Rc<TabAction>, &Rc<TabAction>,
&Rc<ItemAction>, &Rc<ItemAction>,
), ),
request: &Entry, request: &Rc<Request>,
) -> Self { ) -> Self {
let button = Button::builder() let button = Button::builder()
.action_name(format!("{}.{}", tab_action.id, item_action.reload.name())) .action_name(format!("{}.{}", tab_action.id, item_action.reload.name()))

View file

@ -20,57 +20,17 @@ use suggestion::Suggestion;
const PREFIX_DOWNLOAD: &str = "download:"; const PREFIX_DOWNLOAD: &str = "download:";
const PREFIX_SOURCE: &str = "source:"; const PREFIX_SOURCE: &str = "source:";
pub trait Request { pub struct Request {
// Constructors pub entry: Entry,
suggestion: Rc<Suggestion>,
fn request(item_action: &Rc<ItemAction>, profile: &Rc<Profile>) -> Self; profile: Rc<Profile>,
// Actions
fn clean(
&self,
transaction: &Transaction,
app_browser_window_tab_item_page_navigation_id: &i64,
) -> Result<()>;
fn restore(
&self,
transaction: &Transaction,
app_browser_window_tab_item_page_navigation_id: &i64,
) -> Result<()>;
fn save(
&self,
transaction: &Transaction,
app_browser_window_tab_item_page_navigation_id: &i64,
) -> Result<()>;
fn update_primary_icon(&self, profile: &Profile);
fn update_secondary_icon(&self);
fn show_identity_dialog(&self, profile: &Rc<Profile>);
fn show_search_dialog(&self, profile: &Rc<Profile>);
// Setters
fn to_download(&self);
fn to_source(&self);
// Getters
fn prefix_less(&self) -> GString;
fn download(&self) -> GString;
fn source(&self) -> GString;
fn uri(&self) -> Option<Uri>;
fn home(&self) -> Option<Uri>;
fn is_file(&self) -> bool;
} }
impl Request for Entry { impl Request {
// Constructors // Constructors
/// Build new `Self` /// Build new `Self`
fn request(item_action: &Rc<ItemAction>, profile: &Rc<Profile>) -> Self { pub fn build(item_action: &Rc<ItemAction>, profile: &Rc<Profile>) -> Self {
// Init main widget // Init main widget
let entry = Entry::builder() let entry = Entry::builder()
.placeholder_text("URL or search term...") .placeholder_text("URL or search term...")
@ -79,7 +39,7 @@ impl Request for Entry {
.build(); .build();
// Detect primary icon on construct // Detect primary icon on construct
entry.update_primary_icon(profile); update_primary_icon(&entry, profile);
// Init additional features // Init additional features
let suggestion = Rc::new(Suggestion::build(profile, &entry)); let suggestion = Rc::new(Suggestion::build(profile, &entry));
@ -90,9 +50,9 @@ impl Request for Entry {
move |this, position| match position { move |this, position| match position {
EntryIconPosition::Primary => { EntryIconPosition::Primary => {
if matches!(primary_icon::from(&this.text()), PrimaryIcon::Search { .. }) { if matches!(primary_icon::from(&this.text()), PrimaryIcon::Search { .. }) {
this.show_search_dialog(&profile) show_search_dialog(this, &profile)
} else { } else {
this.show_identity_dialog(&profile) show_identity_dialog(this, &profile)
} }
} }
EntryIconPosition::Secondary => this.emit_activate(), EntryIconPosition::Secondary => this.emit_activate(),
@ -100,7 +60,7 @@ impl Request for Entry {
} }
}); });
entry.connect_has_focus_notify(|this| this.update_secondary_icon()); entry.connect_has_focus_notify(update_secondary_icon);
suggestion suggestion
.signal_handler_id .signal_handler_id
@ -112,11 +72,11 @@ impl Request for Entry {
move |this| { move |this| {
// Update actions // Update actions
item_action.reload.set_enabled(!this.text().is_empty()); item_action.reload.set_enabled(!this.text().is_empty());
item_action.home.set_enabled(this.home().is_some()); item_action.home.set_enabled(home(this).is_some());
// Update icons // Update icons
this.update_primary_icon(&profile); update_primary_icon(this, &profile);
this.update_secondary_icon(); update_secondary_icon(this);
// Show search suggestions // Show search suggestions
if this.focus_child().is_some() { if this.focus_child().is_some() {
@ -156,11 +116,35 @@ impl Request for Entry {
} }
}); });
entry Self {
entry,
suggestion,
profile: profile.clone(),
}
} }
// Actions // Actions
fn clean( pub fn escape(&self) {
self.suggestion.hide()
}
/// Try build home [Uri](https://docs.gtk.org/glib/struct.Uri.html) for `Self`
/// * return `None` if current request already match home or Uri not parsable
pub fn home(&self) -> Option<Uri> {
home(&self.entry)
}
/// Try get current request value as [Uri](https://docs.gtk.org/glib/struct.Uri.html)
/// * `strip_prefix` on parse
pub fn uri(&self) -> Option<Uri> {
uri(&self.entry)
}
pub fn show_identity_dialog(&self) {
show_identity_dialog(&self.entry, &self.profile)
}
pub fn clean(
&self, &self,
transaction: &Transaction, transaction: &Transaction,
app_browser_window_tab_item_page_navigation_id: &i64, app_browser_window_tab_item_page_navigation_id: &i64,
@ -174,7 +158,7 @@ impl Request for Entry {
Ok(()) Ok(())
} }
fn restore( pub fn restore(
&self, &self,
transaction: &Transaction, transaction: &Transaction,
app_browser_window_tab_item_page_navigation_id: &i64, app_browser_window_tab_item_page_navigation_id: &i64,
@ -182,7 +166,7 @@ impl Request for Entry {
for record in database::select(transaction, app_browser_window_tab_item_page_navigation_id)? for record in database::select(transaction, app_browser_window_tab_item_page_navigation_id)?
{ {
if let Some(text) = record.text { if let Some(text) = record.text {
self.set_text(&text); self.entry.set_text(&text);
} }
// Delegate restore action to the item childs // Delegate restore action to the item childs
// nothing yet.. // nothing yet..
@ -190,13 +174,13 @@ impl Request for Entry {
Ok(()) Ok(())
} }
fn save( pub fn save(
&self, &self,
transaction: &Transaction, transaction: &Transaction,
app_browser_window_tab_item_page_navigation_id: &i64, app_browser_window_tab_item_page_navigation_id: &i64,
) -> Result<()> { ) -> Result<()> {
// Keep value in memory until operation complete // Keep value in memory until operation complete
let text = self.text(); let text = self.entry.text();
let _id = database::insert( let _id = database::insert(
transaction, transaction,
app_browser_window_tab_item_page_navigation_id, app_browser_window_tab_item_page_navigation_id,
@ -210,154 +194,32 @@ impl Request for Entry {
Ok(()) Ok(())
} }
fn update_primary_icon(&self, profile: &Profile) {
self.first_child().unwrap().remove_css_class("success"); // @TODO handle
match primary_icon::from(&self.text()) {
PrimaryIcon::Download { name, tooltip } | PrimaryIcon::File { name, tooltip } => {
self.set_primary_icon_activatable(false);
self.set_primary_icon_sensitive(false);
self.set_primary_icon_name(Some(name));
self.set_primary_icon_tooltip_text(Some(tooltip));
}
PrimaryIcon::Gemini { name, tooltip } | PrimaryIcon::Titan { name, tooltip } => {
self.set_primary_icon_activatable(true);
self.set_primary_icon_sensitive(true);
self.set_primary_icon_name(Some(name));
if profile.identity.get(&self.prefix_less()).is_some() {
self.first_child().unwrap().add_css_class("success"); // @TODO handle
self.set_primary_icon_tooltip_text(Some(tooltip.1));
} else {
self.set_primary_icon_tooltip_text(Some(tooltip.0));
}
}
PrimaryIcon::Search { name, tooltip } => {
self.set_primary_icon_activatable(true);
self.set_primary_icon_sensitive(true);
self.set_primary_icon_name(Some(name));
self.set_primary_icon_tooltip_text(Some(tooltip));
}
PrimaryIcon::Source { name, tooltip } => {
self.set_primary_icon_activatable(false);
self.set_primary_icon_sensitive(false);
self.set_primary_icon_name(Some(name));
self.set_primary_icon_tooltip_text(Some(tooltip));
}
}
}
fn update_secondary_icon(&self) {
if !self.text().is_empty() && self.focus_child().is_some_and(|text| text.has_focus()) {
self.set_secondary_icon_name(Some("pan-end-symbolic"));
} else {
self.set_secondary_icon_name(None);
self.select_region(0, 0);
}
}
/// Present Identity [AlertDialog](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.AlertDialog.html) for `Self`
fn show_identity_dialog(&self, profile: &Rc<Profile>) {
// connect identity traits
use identity::{Common, Unsupported};
if let Some(uri) = self.uri() {
if ["gemini", "titan"].contains(&uri.scheme().as_str()) {
return AlertDialog::common(
profile,
&uri,
&Rc::new({
let profile = profile.clone();
let this = self.clone();
move |is_reload| {
this.update_primary_icon(&profile);
if is_reload {
this.emit_activate();
}
}
}),
)
.present(Some(self));
}
}
AlertDialog::unsupported().present(Some(self));
}
/// Present Search providers [AlertDialog](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.AlertDialog.html) for `Self`
fn show_search_dialog(&self, profile: &Rc<Profile>) {
use search::Search;
AlertDialog::search(profile).present(Some(self))
}
// Setters // Setters
fn to_download(&self) { pub fn to_download(&self) {
self.set_text(&self.download()); self.entry.set_text(&self.download());
} }
fn to_source(&self) { pub fn to_source(&self) {
self.set_text(&self.source()); self.entry.set_text(&self.source());
} }
// Getters // Getters
/// Get current request value without system prefix pub fn is_file(&self) -> bool {
/// * the `prefix` is not `scheme` self.entry.text().starts_with("file://")
fn prefix_less(&self) -> GString {
let mut request = self.text();
if let Some(postfix) = request.strip_prefix(PREFIX_SOURCE) {
request = postfix.into()
}
if let Some(postfix) = request.strip_prefix(PREFIX_DOWNLOAD) {
request = postfix.into()
}
request
} }
// Tools
/// Get request value with formatted `download` prefix /// Get request value with formatted `download` prefix
fn download(&self) -> GString { fn download(&self) -> GString {
gformat!("{PREFIX_DOWNLOAD}{}", self.prefix_less()) gformat!("{PREFIX_DOWNLOAD}{}", prefix_less(&self.entry))
} }
/// Get request value with formatted `source` prefix /// Get request value with formatted `source` prefix
fn source(&self) -> GString { fn source(&self) -> GString {
gformat!("{PREFIX_SOURCE}{}", self.prefix_less()) gformat!("{PREFIX_SOURCE}{}", prefix_less(&self.entry))
}
/// Try get current request value as [Uri](https://docs.gtk.org/glib/struct.Uri.html)
/// * `strip_prefix` on parse
fn uri(&self) -> Option<Uri> {
match Uri::parse(&self.prefix_less(), UriFlags::NONE) {
Ok(uri) => Some(uri),
_ => None,
}
}
/// Try build home [Uri](https://docs.gtk.org/glib/struct.Uri.html) for `Self`
/// * return `None` if current request already match home or Uri not parsable
fn home(&self) -> Option<Uri> {
let uri = self.uri()?;
if uri.path().len() > 1 || uri.query().is_some() || uri.fragment().is_some() {
Some(Uri::build(
UriFlags::NONE,
&if uri.scheme() == "titan" {
GString::from("gemini")
} else {
uri.scheme()
},
uri.userinfo().as_deref(),
uri.host().as_deref(),
uri.port(),
"/",
None,
None,
))
} else {
None
}
}
fn is_file(&self) -> bool {
self.text().starts_with("file://")
} }
} }
@ -373,3 +235,127 @@ pub fn migrate(tx: &Transaction) -> Result<()> {
// Success // Success
Ok(()) Ok(())
} }
fn update_primary_icon(entry: &Entry, profile: &Profile) {
entry.first_child().unwrap().remove_css_class("success"); // @TODO handle
match primary_icon::from(&entry.text()) {
PrimaryIcon::Download { name, tooltip } | PrimaryIcon::File { name, tooltip } => {
entry.set_primary_icon_activatable(false);
entry.set_primary_icon_sensitive(false);
entry.set_primary_icon_name(Some(name));
entry.set_primary_icon_tooltip_text(Some(tooltip));
}
PrimaryIcon::Gemini { name, tooltip } | PrimaryIcon::Titan { name, tooltip } => {
entry.set_primary_icon_activatable(true);
entry.set_primary_icon_sensitive(true);
entry.set_primary_icon_name(Some(name));
if profile.identity.get(&prefix_less(entry)).is_some() {
entry.first_child().unwrap().add_css_class("success"); // @TODO handle
entry.set_primary_icon_tooltip_text(Some(tooltip.1));
} else {
entry.set_primary_icon_tooltip_text(Some(tooltip.0));
}
}
PrimaryIcon::Search { name, tooltip } => {
entry.set_primary_icon_activatable(true);
entry.set_primary_icon_sensitive(true);
entry.set_primary_icon_name(Some(name));
entry.set_primary_icon_tooltip_text(Some(tooltip));
}
PrimaryIcon::Source { name, tooltip } => {
entry.set_primary_icon_activatable(false);
entry.set_primary_icon_sensitive(false);
entry.set_primary_icon_name(Some(name));
entry.set_primary_icon_tooltip_text(Some(tooltip));
}
}
}
fn update_secondary_icon(entry: &Entry) {
if !entry.text().is_empty() && entry.focus_child().is_some_and(|text| text.has_focus()) {
entry.set_secondary_icon_name(Some("pan-end-symbolic"));
} else {
entry.set_secondary_icon_name(None);
entry.select_region(0, 0);
}
}
/// Present Identity [AlertDialog](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.AlertDialog.html) for `Self`
fn show_identity_dialog(entry: &Entry, profile: &Rc<Profile>) {
// connect identity traits
use identity::{Common, Unsupported};
if let Some(uri) = uri(entry) {
if ["gemini", "titan"].contains(&uri.scheme().as_str()) {
return AlertDialog::common(
profile,
&uri,
&Rc::new({
let profile = profile.clone();
let entry = entry.clone();
move |is_reload| {
update_primary_icon(&entry, &profile);
if is_reload {
entry.emit_activate();
}
}
}),
)
.present(Some(entry));
}
}
AlertDialog::unsupported().present(Some(entry));
}
/// Present Search providers [AlertDialog](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.AlertDialog.html) for `Self`
fn show_search_dialog(entry: &Entry, profile: &Rc<Profile>) {
use search::Search;
AlertDialog::search(profile).present(Some(entry))
}
/// Get current request value without system prefix
/// * the `prefix` is not `scheme`
fn prefix_less(entry: &Entry) -> GString {
let mut request = entry.text();
if let Some(postfix) = request.strip_prefix(PREFIX_SOURCE) {
request = postfix.into()
}
if let Some(postfix) = request.strip_prefix(PREFIX_DOWNLOAD) {
request = postfix.into()
}
request
}
/// Try get current request value as [Uri](https://docs.gtk.org/glib/struct.Uri.html)
/// * `strip_prefix` on parse
fn uri(entry: &Entry) -> Option<Uri> {
match Uri::parse(&prefix_less(entry), UriFlags::NONE) {
Ok(uri) => Some(uri),
_ => None,
}
}
/// Try build home [Uri](https://docs.gtk.org/glib/struct.Uri.html) for `Self`
/// * return `None` if current request already match home or Uri not parsable
fn home(entry: &Entry) -> Option<Uri> {
let uri = uri(entry)?;
if uri.path().len() > 1 || uri.query().is_some() || uri.fragment().is_some() {
Some(Uri::build(
UriFlags::NONE,
&if uri.scheme() == "titan" {
GString::from("gemini")
} else {
uri.scheme()
},
uri.userinfo().as_deref(),
uri.host().as_deref(),
uri.port(),
"/",
None,
None,
))
} else {
None
}
}

View file

@ -15,7 +15,6 @@ use gtk::{
Entry, ListItem, ListView, Popover, SignalListItemFactory, SingleSelection, Entry, ListItem, ListView, Popover, SignalListItemFactory, SingleSelection,
}; };
pub use item::Item; pub use item::Item;
use sourceview::prelude::ListModelExt;
use std::{cell::RefCell, rc::Rc}; use std::{cell::RefCell, rc::Rc};
pub struct Suggestion { pub struct Suggestion {
@ -47,13 +46,39 @@ impl Suggestion {
.child(&{ .child(&{
let list_view = ListView::builder() let list_view = ListView::builder()
.show_separators(true) .show_separators(true)
.single_click_activate(true) .model(&{
.model( let s = SingleSelection::builder()
&SingleSelection::builder()
.model(&list_store) .model(&list_store)
.autoselect(false) .autoselect(false)
.build(), .build();
) s.connect_selected_notify({
let request = request.clone();
let signal_handler_id = signal_handler_id.clone();
move |this| {
use gtk::prelude::ObjectExt;
if let Some(signal_handler_id) =
signal_handler_id.borrow().as_ref()
{
request.block_signal(signal_handler_id);
}
request.set_text(
&this
.selected_item()
.unwrap()
.downcast_ref::<Item>()
.unwrap()
.request(),
);
request.select_region(0, -1);
if let Some(signal_handler_id) =
signal_handler_id.borrow().as_ref()
{
request.unblock_signal(signal_handler_id);
}
}
});
s
})
.factory(&{ .factory(&{
let f = SignalListItemFactory::new(); let f = SignalListItemFactory::new();
f.connect_setup(|_, this| { f.connect_setup(|_, this| {
@ -78,31 +103,7 @@ impl Suggestion {
.build(); .build();
list_view.connect_activate({ list_view.connect_activate({
let request = request.clone(); let request = request.clone();
let signal_handler_id = signal_handler_id.clone(); move |_, _| request.emit_activate()
move |this, i| {
use gtk::prelude::ObjectExt;
if let Some(signal_handler_id) =
signal_handler_id.borrow().as_ref()
{
request.block_signal(signal_handler_id);
}
request.set_text(
&this
.model()
.unwrap()
.item(i)
.unwrap()
.downcast_ref::<Item>()
.unwrap()
.request(),
);
request.select_region(0, -1);
if let Some(signal_handler_id) =
signal_handler_id.borrow().as_ref()
{
request.unblock_signal(signal_handler_id);
}
}
}); });
list_view list_view
}) })