[fractal/ui-refactor: 7/16] Move app::connect to appop module
- From: Alejandro Domínguez <aledomu src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [fractal/ui-refactor: 7/16] Move app::connect to appop module
- Date: Sat, 5 Dec 2020 15:21:33 +0000 (UTC)
commit 1e7e438e5cf4150928f083eeb458d6be3776f158
Author: Alejandro Domínguez <adomu net-c com>
Date: Wed Oct 14 02:28:05 2020 +0200
Move app::connect to appop module
fractal-gtk/src/actions/account_settings.rs | 46 ++--
fractal-gtk/src/app/connect/account.rs | 231 ---------------------
fractal-gtk/src/app/connect/autocomplete.rs | 29 ---
fractal-gtk/src/app/connect/direct.rs | 126 -----------
fractal-gtk/src/app/connect/directory.rs | 192 -----------------
fractal-gtk/src/app/connect/headerbar.rs | 55 -----
fractal-gtk/src/app/connect/invite.rs | 151 --------------
fractal-gtk/src/app/connect/join_room.rs | 54 -----
fractal-gtk/src/app/connect/language.rs | 42 ----
fractal-gtk/src/app/connect/leave_room.rs | 37 ----
fractal-gtk/src/app/connect/markdown.rs | 90 --------
fractal-gtk/src/app/connect/mod.rs | 40 ----
fractal-gtk/src/app/connect/new_room.rs | 70 -------
fractal-gtk/src/app/connect/roomlist_search.rs | 61 ------
fractal-gtk/src/app/connect/send.rs | 69 ------
fractal-gtk/src/app/connect/swipeable_widgets.rs | 67 ------
fractal-gtk/src/app/mod.rs | 5 +-
fractal-gtk/src/appop/attach.rs | 44 ++--
fractal-gtk/src/appop/connect/account.rs | 230 ++++++++++++++++++++
fractal-gtk/src/appop/connect/autocomplete.rs | 25 +++
fractal-gtk/src/appop/connect/direct.rs | 130 ++++++++++++
fractal-gtk/src/appop/connect/directory.rs | 192 +++++++++++++++++
fractal-gtk/src/appop/connect/headerbar.rs | 52 +++++
fractal-gtk/src/appop/connect/invite.rs | 158 ++++++++++++++
fractal-gtk/src/appop/connect/join_room.rs | 55 +++++
fractal-gtk/src/appop/connect/language.rs | 39 ++++
fractal-gtk/src/appop/connect/leave_room.rs | 36 ++++
fractal-gtk/src/appop/connect/markdown.rs | 87 ++++++++
fractal-gtk/src/appop/connect/mod.rs | 36 ++++
fractal-gtk/src/appop/connect/new_room.rs | 69 ++++++
fractal-gtk/src/appop/connect/roomlist_search.rs | 60 ++++++
fractal-gtk/src/appop/connect/send.rs | 69 ++++++
fractal-gtk/src/appop/connect/swipeable_widgets.rs | 63 ++++++
fractal-gtk/src/appop/mod.rs | 1 +
fractal-gtk/src/meson.build | 30 +--
fractal-gtk/src/widgets/autocomplete.rs | 4 +-
36 files changed, 1362 insertions(+), 1383 deletions(-)
---
diff --git a/fractal-gtk/src/actions/account_settings.rs b/fractal-gtk/src/actions/account_settings.rs
index 3526347f..04cd63da 100644
--- a/fractal-gtk/src/actions/account_settings.rs
+++ b/fractal-gtk/src/actions/account_settings.rs
@@ -4,17 +4,15 @@ use gio::prelude::*;
use gio::SimpleAction;
use gio::SimpleActionGroup;
use glib::clone;
-use std::sync::{Arc, Mutex};
-use crate::app::RUNTIME;
-use crate::appop::AppOp;
+use crate::app::{UpdateApp, RUNTIME};
use crate::widgets::FileDialog::open;
use crate::actions::ButtonState;
// This creates all actions a user can perform in the account settings
-pub fn new(window: >k::Window, op: Arc<Mutex<AppOp>>) -> gio::SimpleActionGroup {
+pub fn new(window: >k::Window, app_tx: glib::Sender<UpdateApp>) -> gio::SimpleActionGroup {
let actions = SimpleActionGroup::new();
// TODO create two stats loading interaction and connect it to the avatar box
let change_avatar =
@@ -23,26 +21,28 @@ pub fn new(window: >k::Window, op: Arc<Mutex<AppOp>>) -> gio::SimpleActionGrou
actions.add_action(&change_avatar);
change_avatar.connect_activate(clone!(@weak window => move |a, _| {
- let (session_client, uid) = unwrap_or_unit_return!(
- op.lock().unwrap().login_data.as_ref().map(|ld| (ld.session_client.clone(), ld.uid.clone()))
- );
-
- let filter = gtk::FileFilter::new();
- filter.add_mime_type("image/*");
- filter.set_name(Some(i18n("Images").as_str()));
- if let Some(path) = open(&window, i18n("Select a new avatar").as_str(), &[filter]) {
- a.change_state(&ButtonState::Insensitive.into());
- RUNTIME.spawn(async move {
- match user::set_user_avatar(session_client, &uid, path).await {
- Ok(path) => {
- APPOP!(show_new_avatar, (path));
+ let _ = app_tx.send(Box::new(clone!(@weak a => move |op| {
+ let (session_client, uid) = unwrap_or_unit_return!(
+ op.login_data.as_ref().map(|ld| (ld.session_client.clone(), ld.uid.clone()))
+ );
+
+ let filter = gtk::FileFilter::new();
+ filter.add_mime_type("image/*");
+ filter.set_name(Some(i18n("Images").as_str()));
+ if let Some(path) = open(&window, i18n("Select a new avatar").as_str(), &[filter]) {
+ a.change_state(&ButtonState::Insensitive.into());
+ RUNTIME.spawn(async move {
+ match user::set_user_avatar(session_client, &uid, path).await {
+ Ok(path) => {
+ APPOP!(show_new_avatar, (path));
+ }
+ Err(err) => {
+ err.handle_error();
+ }
}
- Err(err) => {
- err.handle_error();
- }
- }
- });
- }
+ });
+ }
+ })));
}));
actions
diff --git a/fractal-gtk/src/app/mod.rs b/fractal-gtk/src/app/mod.rs
index 53d3cc34..5af5efa8 100644
--- a/fractal-gtk/src/app/mod.rs
+++ b/fractal-gtk/src/app/mod.rs
@@ -18,7 +18,6 @@ use crate::config;
use crate::uibuilder;
use crate::widgets;
-mod connect;
mod windowstate;
use windowstate::WindowState;
@@ -167,7 +166,7 @@ impl App {
let app = AppRef::new(Self { ui });
- app.connect_gtk();
+ let _ = get_app_tx().send(Box::new(|op| op.connect_gtk()));
app
}
@@ -223,7 +222,7 @@ impl App {
}
// TODO: Deprecated. It should be removed
-pub(self) fn get_op() -> &'static GlobalAppOp {
+pub fn get_op() -> &'static GlobalAppOp {
unsafe { OP.as_ref().expect("Fatal: AppOp has not been initialized") }
}
diff --git a/fractal-gtk/src/appop/attach.rs b/fractal-gtk/src/appop/attach.rs
index dd6688cc..21c77bbf 100644
--- a/fractal-gtk/src/appop/attach.rs
+++ b/fractal-gtk/src/appop/attach.rs
@@ -4,7 +4,6 @@ use glib::clone;
use std::fs::File;
use std::io::prelude::*;
use std::path::PathBuf;
-use std::sync::{Arc, Mutex};
use anyhow::Error;
@@ -72,6 +71,24 @@ impl AppOp {
}
}
}
+
+ pub fn paste(&self) {
+ if let Some(display) = gdk::Display::get_default() {
+ if let Some(clipboard) = gtk::Clipboard::get_default(&display) {
+ if clipboard.wait_is_image_available() {
+ if let Some(pixb) = clipboard.wait_for_image() {
+ self.draw_image_paste_dialog(&pixb);
+
+ // removing text from clipboard
+ clipboard.set_text("");
+ clipboard.set_image(&pixb);
+ }
+ } else {
+ // TODO: manage code pasting
+ }
+ }
+ }
+ }
}
fn store_pixbuf(pixb: &Pixbuf) -> Result<PathBuf, Error> {
@@ -85,28 +102,3 @@ fn store_pixbuf(pixb: &Pixbuf) -> Result<PathBuf, Error> {
Ok(path)
}
-
-/// This function receives the appop mutex to avoid lock the interface
-/// This was previously an appop method that receives &self, but that
-/// force us to lock the interface for the entire function that causes
-/// problems because we call to wait_is_image_available that makes that
-/// tries to continue the loop and that give us to a deadlock so
-/// this function minimize the lock and avoid that kind of problems
-/// See: https://gitlab.gnome.org/GNOME/fractal/issues/284
-pub fn paste(op: Arc<Mutex<AppOp>>) {
- if let Some(display) = gdk::Display::get_default() {
- if let Some(clipboard) = gtk::Clipboard::get_default(&display) {
- if clipboard.wait_is_image_available() {
- if let Some(pixb) = clipboard.wait_for_image() {
- op.lock().unwrap().draw_image_paste_dialog(&pixb);
-
- // removing text from clipboard
- clipboard.set_text("");
- clipboard.set_image(&pixb);
- }
- } else {
- // TODO: manage code pasting
- }
- }
- }
-}
diff --git a/fractal-gtk/src/appop/connect/account.rs b/fractal-gtk/src/appop/connect/account.rs
new file mode 100644
index 00000000..f1e431e0
--- /dev/null
+++ b/fractal-gtk/src/appop/connect/account.rs
@@ -0,0 +1,230 @@
+use gio::ActionMapExt;
+use glib::clone;
+use gtk::prelude::*;
+
+use crate::appop::AppOp;
+
+use crate::actions::{AccountSettings, StateExt};
+
+pub fn connect(appop: &AppOp) {
+ let app_tx = appop.app_tx.clone();
+ let builder = &appop.ui.builder;
+ let cancel_password = appop
+ .ui
+ .builder
+ .get_object::<gtk::Button>("password-dialog-cancel")
+ .expect("Can't find password-dialog-cancel in ui file.");
+ let confirm_password = appop
+ .ui
+ .builder
+ .get_object::<gtk::Button>("password-dialog-apply")
+ .expect("Can't find password-dialog-apply in ui file.");
+ let password_dialog = appop
+ .ui
+ .builder
+ .get_object::<gtk::Dialog>("password_dialog")
+ .expect("Can't find password_dialog in ui file.");
+ let avatar_btn = appop
+ .ui
+ .builder
+ .get_object::<gtk::Button>("account_settings_avatar_button")
+ .expect("Can't find account_settings_avatar_button in ui file.");
+ let name_entry = appop
+ .ui
+ .builder
+ .get_object::<gtk::Entry>("account_settings_name")
+ .expect("Can't find account_settings_name in ui file.");
+ let name_btn = appop
+ .ui
+ .builder
+ .get_object::<gtk::Button>("account_settings_name_button")
+ .expect("Can't find account_settings_name_button in ui file.");
+ let password_btn = appop
+ .ui
+ .builder
+ .get_object::<gtk::Button>("account_settings_password")
+ .expect("Can't find account_settings_password in ui file.");
+ let old_password = appop
+ .ui
+ .builder
+ .get_object::<gtk::Entry>("password-dialog-old-entry")
+ .expect("Can't find password-dialog-old-entry in ui file.");
+ let new_password = appop
+ .ui
+ .builder
+ .get_object::<gtk::Entry>("password-dialog-entry")
+ .expect("Can't find password-dialog-entry in ui file.");
+ let verify_password = appop
+ .ui
+ .builder
+ .get_object::<gtk::Entry>("password-dialog-verify-entry")
+ .expect("Can't find password-dialog-verify-entry in ui file.");
+ let destruction_entry = appop
+ .ui
+ .builder
+ .get_object::<gtk::Entry>("account_settings_delete_password_confirm")
+ .expect("Can't find account_settings_delete_password_confirm in ui file.");
+ let destruction_btn = appop
+ .ui
+ .builder
+ .get_object::<gtk::Button>("account_settings_delete_btn")
+ .expect("Can't find account_settings_delete_btn in ui file.");
+
+ let window = appop.ui.main_window.upcast_ref::<gtk::Window>();
+ let actions = AccountSettings::new(&window, app_tx.clone());
+ let container = appop
+ .ui
+ .builder
+ .get_object::<gtk::Box>("account_settings_box")
+ .expect("Can't find account_settings_box in ui file.");
+ container.insert_action_group("user-settings", Some(&actions));
+
+ /* Body */
+ if let Some(action) = actions.lookup_action("change-avatar") {
+ action.bind_button_state(&avatar_btn);
+ avatar_btn.set_action_name(Some("user-settings.change-avatar"));
+ let avatar_spinner = appop
+ .ui
+ .builder
+ .get_object::<gtk::Spinner>("account_settings_avatar_spinner")
+ .expect("Can't find account_settings_avatar_spinner in ui file.");
+ avatar_btn.connect_property_sensitive_notify(
+ clone!(@weak avatar_spinner as spinner => move |w| {
+ if w.get_sensitive() {
+ spinner.hide();
+ spinner.stop();
+ } else {
+ spinner.start();
+ spinner.show();
+ }
+ }),
+ );
+ }
+
+ name_entry.connect_property_text_notify(
+ clone!(@strong app_tx, @strong name_btn as button => move |w| {
+ let _ = app_tx.send(Box::new(clone!(@strong w, @strong button => move |op| {
+ let username = w.get_text();
+ if !username.is_empty()
+ && op
+ .login_data
+ .as_ref()
+ .and_then(|login_data| login_data.username.as_ref())
+ .filter(|u| **u != username)
+ .is_some()
+ {
+ button.show();
+ return;
+ }
+ button.hide();
+ })));
+ }),
+ );
+
+ let button = name_btn.clone();
+ name_entry.connect_activate(move |_w| {
+ let _ = button.emit("clicked", &[]);
+ });
+
+ name_btn.connect_clicked(clone!(@strong app_tx => move |_w| {
+ let _ = app_tx.send(Box::new(|op| op.update_username_account_settings()));
+ }));
+
+ /*
+ fn update_password_strength(builder: >k::Builder) {
+ let bar = builder
+ .get_object::<gtk::LevelBar>("password-dialog-strength-indicator")
+ .expect("Can't find password-dialog-strength-indicator in ui file.");
+ let label = builder
+ .get_object::<gtk::Label>("password-dialog-hint")
+ .expect("Can't find password-dialog-hint in ui file.");
+ let strength_level = 10f64;
+ bar.set_value(strength_level);
+ label.set_label("text");
+ }
+ */
+
+ fn validate_password_input(builder: >k::Builder) {
+ let hint = builder
+ .get_object::<gtk::Label>("password-dialog-verify-hint")
+ .expect("Can't find password-dialog-verify-hint in ui file.");
+ let confirm_password = builder
+ .get_object::<gtk::Button>("password-dialog-apply")
+ .expect("Can't find password-dialog-apply in ui file.");
+ let old = builder
+ .get_object::<gtk::Entry>("password-dialog-old-entry")
+ .expect("Can't find password-dialog-old-entry in ui file.");
+ let new = builder
+ .get_object::<gtk::Entry>("password-dialog-entry")
+ .expect("Can't find password-dialog-entry in ui file.");
+ let verify = builder
+ .get_object::<gtk::Entry>("password-dialog-verify-entry")
+ .expect("Can't find password-dialog-verify-entry in ui file.");
+
+ let mut empty = true;
+ let mut matching = true;
+ let old_p = old.get_text();
+ let new_p = new.get_text();
+ let verify_p = verify.get_text();
+
+ if new_p != verify_p {
+ matching = false;
+ }
+ if !new_p.is_empty() && !verify_p.is_empty() && !old_p.is_empty() {
+ empty = false;
+ }
+
+ if matching {
+ hint.hide();
+ } else {
+ hint.show();
+ }
+
+ confirm_password.set_sensitive(matching && !empty);
+ }
+
+ /* Passsword dialog */
+ password_btn.connect_clicked(clone!(@strong app_tx => move |_| {
+ let _ = app_tx.send(Box::new(|op| op.show_password_dialog()));
+ }));
+
+ password_dialog.connect_delete_event(clone!(@strong app_tx => move |_, _| {
+ let _ = app_tx.send(Box::new(|op| op.close_password_dialog()));
+ glib::signal::Inhibit(true)
+ }));
+
+ /* Headerbar */
+ cancel_password.connect_clicked(clone!(@strong app_tx => move |_| {
+ let _ = app_tx.send(Box::new(|op| op.close_password_dialog()));
+ }));
+
+ confirm_password.connect_clicked(clone!(@strong app_tx => move |_| {
+ let _ = app_tx.send(Box::new(|op| {
+ op.set_new_password();
+ op.close_password_dialog();
+ }));
+ }));
+
+ /* Body */
+ verify_password.connect_property_text_notify(clone!(@strong builder => move |_| {
+ validate_password_input(&builder.clone());
+ }));
+ new_password.connect_property_text_notify(clone!(@strong builder => move |_| {
+ validate_password_input(&builder.clone());
+ }));
+ old_password.connect_property_text_notify(clone!(@strong builder => move |_| {
+ validate_password_input(&builder)
+ }));
+
+ destruction_entry.connect_property_text_notify(clone!(@strong destruction_btn => move |w| {
+ if !w.get_text().is_empty() {
+ destruction_btn.set_sensitive(true);
+ return;
+ }
+ destruction_btn.set_sensitive(false);
+ }));
+
+ destruction_btn.connect_clicked(move |_| {
+ let _ = app_tx.send(Box::new(|op| op.account_destruction()));
+ });
+}
diff --git a/fractal-gtk/src/appop/connect/autocomplete.rs b/fractal-gtk/src/appop/connect/autocomplete.rs
new file mode 100644
index 00000000..a3c64474
--- /dev/null
+++ b/fractal-gtk/src/appop/connect/autocomplete.rs
@@ -0,0 +1,25 @@
+use gtk::prelude::*;
+
+use crate::widgets;
+
+use crate::appop::AppOp;
+
+pub fn connect(appop: &AppOp) {
+ let popover = appop
+ .ui
+ .builder
+ .get_object::<gtk::Popover>("autocomplete_popover")
+ .expect("Can't find autocomplete_popover in ui file.");
+ let listbox = appop
+ .ui
+ .builder
+ .get_object::<gtk::ListBox>("autocomplete_listbox")
+ .expect("Can't find autocomplete_listbox in ui file.");
+ let window: gtk::Window = appop
+ .ui
+ .builder
+ .get_object("main_window")
+ .expect("Can't find main_window in ui file.");
+
+ widgets::Autocomplete::new(window, appop.ui.sventry.view.clone(), popover, listbox).connect();
+}
diff --git a/fractal-gtk/src/appop/connect/direct.rs b/fractal-gtk/src/appop/connect/direct.rs
new file mode 100644
index 00000000..eefce9e6
--- /dev/null
+++ b/fractal-gtk/src/appop/connect/direct.rs
@@ -0,0 +1,130 @@
+use glib::clone;
+use gtk::prelude::*;
+
+use glib::source::Continue;
+use std::sync::{Arc, Mutex};
+
+use crate::appop::AppOp;
+
+pub fn connect(appop: &AppOp) {
+ let app_tx = appop.app_tx.clone();
+ let cancel = appop
+ .ui
+ .builder
+ .get_object::<gtk::Button>("cancel_direct_chat")
+ .expect("Can't find cancel_direct_chat in ui file.");
+ let invite = appop
+ .ui
+ .builder
+ .get_object::<gtk::Button>("direct_chat_button")
+ .expect("Can't find direct_chat_button in ui file.");
+ let to_chat_entry_box = appop
+ .ui
+ .builder
+ .get_object::<gtk::Box>("to_chat_entry_box")
+ .expect("Can't find to_chat_entry_box in ui file.");
+ let to_chat_entry = appop
+ .ui
+ .builder
+ .get_object::<gtk::TextView>("to_chat_entry")
+ .expect("Can't find to_chat_entry in ui file.");
+ let dialog = appop
+ .ui
+ .builder
+ .get_object::<gtk::Dialog>("direct_chat_dialog")
+ .expect("Can't find direct_chat_dialog in ui file.");
+
+ if let Some(buffer) = to_chat_entry.get_buffer() {
+ let placeholder_tag = gtk::TextTag::new(Some("placeholder"));
+
+ placeholder_tag.set_property_foreground_rgba(Some(&gdk::RGBA {
+ red: 1.0,
+ green: 1.0,
+ blue: 1.0,
+ alpha: 0.5,
+ }));
+
+ if let Some(tag_table) = buffer.get_tag_table() {
+ tag_table.add(&placeholder_tag);
+ }
+ }
+
+ // this is used to cancel the timeout and not search for every key input. We'll wait 500ms
+ // without key release event to launch the search
+ let source_id: Arc<Mutex<Option<glib::source::SourceId>>> = Arc::new(Mutex::new(None));
+ to_chat_entry.connect_key_release_event(clone!(@strong app_tx => move |entry, _| {
+ {
+ let mut id = source_id.lock().unwrap();
+ if let Some(sid) = id.take() {
+ glib::source::source_remove(sid);
+ }
+ }
+
+ let sid = glib::timeout_add_local(
+ 500,
+ clone!(
+ @strong entry,
+ @strong source_id,
+ @strong app_tx
+ => move || {
+ if let Some(buffer) = entry.get_buffer() {
+ let start = buffer.get_start_iter();
+ let end = buffer.get_end_iter();
+
+ if let Some(text) =
+ buffer.get_text(&start, &end, false).map(|gstr| gstr.to_string())
+ {
+ let _ = app_tx.send(Box::new(|op| op.search_invite_user(text)));
+ }
+ }
+
+ *(source_id.lock().unwrap()) = None;
+ Continue(false)
+ }),
+ );
+
+ *(source_id.lock().unwrap()) = Some(sid);
+ glib::signal::Inhibit(false)
+ }));
+
+ to_chat_entry.connect_focus_in_event(
+ clone!(@strong to_chat_entry_box, @strong app_tx => move |_, _| {
+ to_chat_entry_box.get_style_context().add_class("message-input-focused");
+
+ let _ = app_tx.send(Box::new(|op| op.remove_invite_user_dialog_placeholder()));
+
+ Inhibit(false)
+ }),
+ );
+
+ to_chat_entry.connect_focus_out_event(
+ clone!(@strong to_chat_entry_box, @strong app_tx => move |_, _| {
+ to_chat_entry_box.get_style_context().remove_class("message-input-focused");
+
+ let _ = app_tx.send(Box::new(|op| op.set_invite_user_dialog_placeholder()));
+
+ Inhibit(false)
+ }),
+ );
+
+ if let Some(buffer) = to_chat_entry.get_buffer() {
+ buffer.connect_delete_range(clone!(@strong app_tx => move |_, _, _| {
+ glib::idle_add_local(clone!(@strong app_tx => move || {
+ let _ = app_tx.send(Box::new(|op| op.detect_removed_invite()));
+ Continue(false)
+ }));
+ }));
+ }
+
+ dialog.connect_delete_event(clone!(@strong app_tx => move |_, _| {
+ let _ = app_tx.send(Box::new(|op| op.close_direct_chat_dialog()));
+ glib::signal::Inhibit(true)
+ }));
+ cancel.connect_clicked(clone!(@strong app_tx => move |_| {
+ let _ = app_tx.send(Box::new(|op| op.close_direct_chat_dialog()));
+ }));
+ invite.set_sensitive(false);
+ invite.connect_clicked(move |_| {
+ let _ = app_tx.send(Box::new(|op| op.start_chat()));
+ });
+}
diff --git a/fractal-gtk/src/appop/connect/directory.rs b/fractal-gtk/src/appop/connect/directory.rs
new file mode 100644
index 00000000..b987ccc5
--- /dev/null
+++ b/fractal-gtk/src/appop/connect/directory.rs
@@ -0,0 +1,192 @@
+use glib::clone;
+
+use crate::util::i18n::i18n;
+
+use gtk::prelude::*;
+use libhandy::prelude::*;
+
+use crate::appop::{AppOp, RoomSearchPagination};
+
+pub fn connect(appop: &AppOp) {
+ let app_tx = appop.app_tx.clone();
+ let q = appop
+ .ui
+ .builder
+ .get_object::<gtk::Entry>("directory_search_entry")
+ .expect("Can't find directory_search_entry in ui file.");
+
+ let directory_stack = appop
+ .ui
+ .builder
+ .get_object::<gtk::Stack>("directory_stack")
+ .expect("Can't find directory_stack in ui file.");
+
+ let clamp = libhandy::Clamp::new();
+ let listbox = gtk::ListBox::new();
+
+ clamp.set_maximum_size(800);
+ clamp.set_hexpand(true);
+ clamp.set_vexpand(true);
+ clamp.set_margin_top(24);
+ clamp.set_margin_start(12);
+ clamp.set_margin_end(12);
+
+ let frame = gtk::Frame::new(None);
+ frame.set_shadow_type(gtk::ShadowType::In);
+ frame.add(&listbox);
+ frame.get_style_context().add_class("room-directory");
+ clamp.add(&frame);
+ listbox.show();
+ frame.show();
+ clamp.show();
+ directory_stack.add_named(&clamp, "directory_clamp");
+
+ appop
+ .ui
+ .builder
+ .expose_object::<gtk::ListBox>("directory_room_list", &listbox);
+ appop
+ .ui
+ .builder
+ .expose_object::<libhandy::Clamp>("directory_clamp", &clamp);
+
+ let directory_choice_label = appop
+ .ui
+ .builder
+ .get_object::<gtk::Label>("directory_choice_label")
+ .expect("Can't find directory_choice_label in ui file.");
+
+ let default_matrix_server_radio = appop
+ .ui
+ .builder
+ .get_object::<gtk::RadioButton>("default_matrix_server_radio")
+ .expect("Can't find default_matrix_server_radio in ui file.");
+
+ let other_protocol_radio = appop
+ .ui
+ .builder
+ .get_object::<gtk::RadioButton>("other_protocol_radio")
+ .expect("Can't find other_protocol_radio in ui file.");
+
+ let protocol_combo = appop
+ .ui
+ .builder
+ .get_object::<gtk::ComboBox>("protocol_combo")
+ .expect("Can't find protocol_combo in ui file.");
+
+ let protocol_model = appop
+ .ui
+ .builder
+ .get_object::<gtk::ListStore>("protocol_model")
+ .expect("Can't find protocol_model in ui file.");
+
+ let other_homeserver_radio = appop
+ .ui
+ .builder
+ .get_object::<gtk::RadioButton>("other_homeserver_radio")
+ .expect("Can't find other_homeserver_radio in ui file.");
+
+ let other_homeserver_url_entry = appop
+ .ui
+ .builder
+ .get_object::<gtk::Entry>("other_homeserver_url_entry")
+ .expect("Can't find other_homeserver_url_entry in ui file.");
+
+ let other_homeserver_url = appop
+ .ui
+ .builder
+ .get_object::<gtk::EntryBuffer>("other_homeserver_url")
+ .expect("Can't find other_homeserver_url in ui file.");
+
+ let scroll = appop
+ .ui
+ .builder
+ .get_object::<gtk::ScrolledWindow>("directory_scroll")
+ .expect("Can't find directory_scroll in ui file.");
+
+ scroll.connect_edge_reached(clone!(@strong app_tx => move |_, dir| {
+ if dir == gtk::PositionType::Bottom {
+ let _ = app_tx.send(Box::new(|op| op.load_more_rooms()));
+ }
+ }));
+
+ q.connect_activate(move |_| {
+ let _ = app_tx.send(Box::new(|op| {
+ op.directory_pagination = RoomSearchPagination::Initial;
+ op.search_rooms();
+ }));
+ });
+
+ default_matrix_server_radio.connect_toggled(clone!(
+ @strong directory_choice_label,
+ @strong default_matrix_server_radio,
+ @strong protocol_combo,
+ @strong other_homeserver_url_entry
+ => move |_| {
+ if default_matrix_server_radio.get_active() {
+ protocol_combo.set_sensitive(false);
+ other_homeserver_url_entry.set_sensitive(false);
+ }
+
+ directory_choice_label.set_text(&i18n("Default Matrix Server"));
+ }));
+
+ other_protocol_radio.connect_toggled(clone!(
+ @strong directory_choice_label,
+ @strong other_protocol_radio,
+ @strong protocol_combo,
+ @strong protocol_model,
+ @strong other_homeserver_url_entry
+ => move |_| {
+ if other_protocol_radio.get_active() {
+ protocol_combo.set_sensitive(true);
+ other_homeserver_url_entry.set_sensitive(false);
+ }
+
+ let active = protocol_combo.get_active().map_or(-1, |uint| uint as i32);
+ let protocol: String = match protocol_model.iter_nth_child(None, active) {
+ Some(it) => {
+ let v = protocol_model.get_value(&it, 0);
+ v.get().unwrap().unwrap()
+ }
+ None => String::new(),
+ };
+
+ directory_choice_label.set_text(&protocol);
+ }));
+
+ protocol_combo.connect_changed(clone!(
+ @strong directory_choice_label,
+ @strong protocol_combo,
+ @strong protocol_model
+ => move |_| {
+ let active = protocol_combo.get_active().map_or(-1, |uint| uint as i32);
+ let protocol: String = match protocol_model.iter_nth_child(None, active) {
+ Some(it) => {
+ let v = protocol_model.get_value(&it, 0);
+ v.get().unwrap().unwrap()
+ }
+ None => String::new(),
+ };
+
+ directory_choice_label.set_text(&protocol);
+ }));
+
+ other_homeserver_radio.connect_toggled(clone!(
+ @strong other_homeserver_radio,
+ @strong protocol_combo,
+ @strong other_homeserver_url_entry
+ => move |_| {
+ if other_homeserver_radio.get_active() {
+ protocol_combo.set_sensitive(false);
+ other_homeserver_url_entry.set_sensitive(true);
+ }
+ }));
+
+ other_homeserver_url_entry.connect_changed(clone!(
+ @strong directory_choice_label,
+ @strong other_homeserver_url
+ => move |_| {
+ directory_choice_label.set_text(&other_homeserver_url.get_text());
+ }));
+}
diff --git a/fractal-gtk/src/appop/connect/headerbar.rs b/fractal-gtk/src/appop/connect/headerbar.rs
new file mode 100644
index 00000000..f09c47da
--- /dev/null
+++ b/fractal-gtk/src/appop/connect/headerbar.rs
@@ -0,0 +1,52 @@
+use glib::clone;
+use gtk::prelude::*;
+use libhandy::HeaderBarExt;
+
+use crate::appop::AppOp;
+
+pub fn connect(appop: &AppOp) {
+ if let Some(set) = gtk::Settings::get_default() {
+ let left_header: libhandy::HeaderBar = appop
+ .ui
+ .builder
+ .get_object("left-header")
+ .expect("Can't find left-header in ui file.");
+
+ let right_header: libhandy::HeaderBar = appop
+ .ui
+ .builder
+ .get_object("room_header_bar")
+ .expect("Can't find room_header_bar in ui file.");
+
+ if let Some(decor) = set.get_property_gtk_decoration_layout() {
+ let decor = decor.to_string();
+ let decor_split: Vec<String> = decor.splitn(2, ':').map(|s| s.to_string()).collect();
+ // Check if the close button is to the right; If not,
+ // change the headerbar controls
+ if decor_split.len() > 1 && !decor_split[1].contains("close") {
+ right_header.set_show_close_button(false);
+ left_header.set_show_close_button(true);
+ }
+ };
+
+ set.connect_property_gtk_decoration_layout_notify(clone!(
+ @strong right_header,
+ @strong left_header,
+ @strong set
+ => move |_| {
+ if let Some(decor) = set.get_property_gtk_decoration_layout() {
+ let decor = decor.to_string();
+ let decor_split: Vec<String> = decor.splitn(2,':').map(|s| s.to_string()).collect();
+ // Change the headerbar controls depending on position
+ // of close
+ if decor_split.len() > 1 && decor_split[1].contains("close") {
+ left_header.set_show_close_button(false);
+ right_header.set_show_close_button(true);
+ } else {
+ right_header.set_show_close_button(false);
+ left_header.set_show_close_button(true);
+ }
+ };
+ }));
+ };
+}
diff --git a/fractal-gtk/src/appop/connect/invite.rs b/fractal-gtk/src/appop/connect/invite.rs
new file mode 100644
index 00000000..b3d23e06
--- /dev/null
+++ b/fractal-gtk/src/appop/connect/invite.rs
@@ -0,0 +1,158 @@
+use glib::clone;
+use gtk::prelude::*;
+
+use glib::source::Continue;
+use std::sync::{Arc, Mutex};
+
+use crate::appop::AppOp;
+
+pub fn connect_dialog(appop: &AppOp) {
+ let app_tx = appop.app_tx.clone();
+ let dialog = appop
+ .ui
+ .builder
+ .get_object::<gtk::MessageDialog>("invite_dialog")
+ .expect("Can't find invite_dialog in ui file.");
+ let accept = appop
+ .ui
+ .builder
+ .get_object::<gtk::Button>("invite_accept")
+ .expect("Can't find invite_accept in ui file.");
+ let reject = appop
+ .ui
+ .builder
+ .get_object::<gtk::Button>("invite_reject")
+ .expect("Can't find invite_reject in ui file.");
+
+ reject.connect_clicked(clone!(@strong dialog, @strong app_tx => move |_| {
+ let _ = app_tx.send(Box::new(|op| op.accept_inv(false)));
+ dialog.hide();
+ }));
+ dialog.connect_delete_event(clone!(@strong dialog, @strong app_tx => move |_, _| {
+ let _ = app_tx.send(Box::new(|op| op.accept_inv(false)));
+ dialog.hide();
+ glib::signal::Inhibit(true)
+ }));
+
+ accept.connect_clicked(clone!(@strong dialog => move |_| {
+ let _ = app_tx.send(Box::new(|op| op.accept_inv(true)));
+ dialog.hide();
+ }));
+}
+
+pub fn connect_user(appop: &AppOp) {
+ let app_tx = appop.app_tx.clone();
+ let cancel = appop
+ .ui
+ .builder
+ .get_object::<gtk::Button>("cancel_invite")
+ .expect("Can't find cancel_invite in ui file.");
+ let invite = appop
+ .ui
+ .builder
+ .get_object::<gtk::Button>("invite_button")
+ .expect("Can't find invite_button in ui file.");
+ let invite_entry_box = appop
+ .ui
+ .builder
+ .get_object::<gtk::Box>("invite_entry_box")
+ .expect("Can't find invite_entry_box in ui file.");
+ let invite_entry = appop
+ .ui
+ .builder
+ .get_object::<gtk::TextView>("invite_entry")
+ .expect("Can't find invite_entry in ui file.");
+ let dialog = appop
+ .ui
+ .builder
+ .get_object::<gtk::Dialog>("invite_user_dialog")
+ .expect("Can't find invite_user_dialog in ui file.");
+
+ if let Some(buffer) = invite_entry.get_buffer() {
+ let placeholder_tag = gtk::TextTag::new(Some("placeholder"));
+
+ placeholder_tag.set_property_foreground_rgba(Some(&gdk::RGBA {
+ red: 1.0,
+ green: 1.0,
+ blue: 1.0,
+ alpha: 0.5,
+ }));
+
+ if let Some(tag_table) = buffer.get_tag_table() {
+ tag_table.add(&placeholder_tag);
+ }
+ }
+
+ // this is used to cancel the timeout and not search for every key input. We'll wait 500ms
+ // without key release event to launch the search
+ let source_id: Arc<Mutex<Option<glib::source::SourceId>>> = Arc::new(Mutex::new(None));
+ invite_entry.connect_key_release_event(clone!(@strong app_tx => move |entry, _| {
+ {
+ let mut id = source_id.lock().unwrap();
+ if let Some(sid) = id.take() {
+ glib::source::source_remove(sid);
+ }
+ }
+
+ let sid = glib::timeout_add_local(
+ 500,
+ clone!(@strong entry, @strong source_id, @strong app_tx => move || {
+ if let Some(buffer) = entry.get_buffer() {
+ let start = buffer.get_start_iter();
+ let end = buffer.get_end_iter();
+
+ if let Some(text) = buffer.get_text(&start, &end, false).map(|gstr|
gstr.to_string()) {
+ let _ = app_tx.send(Box::new(|op| op.search_invite_user(text)));
+ }
+ }
+
+ *(source_id.lock().unwrap()) = None;
+ Continue(false)
+ }),
+ );
+
+ *(source_id.lock().unwrap()) = Some(sid);
+ glib::signal::Inhibit(false)
+ }));
+
+ invite_entry.connect_focus_in_event(
+ clone!(@strong invite_entry_box, @strong app_tx => move |_, _| {
+ invite_entry_box.get_style_context().add_class("message-input-focused");
+
+ let _ = app_tx.send(Box::new(|op| op.remove_invite_user_dialog_placeholder()));
+
+ Inhibit(false)
+ }),
+ );
+
+ invite_entry.connect_focus_out_event(
+ clone!(@strong invite_entry_box, @strong app_tx => move |_, _| {
+ invite_entry_box.get_style_context().remove_class("message-input-focused");
+
+ let _ = app_tx.send(Box::new(|op| op.set_invite_user_dialog_placeholder()));
+
+ Inhibit(false)
+ }),
+ );
+
+ if let Some(buffer) = invite_entry.get_buffer() {
+ buffer.connect_delete_range(clone!(@strong app_tx => move |_, _, _| {
+ glib::idle_add_local(clone!(@strong app_tx => move || {
+ let _ = app_tx.send(Box::new(|op| op.detect_removed_invite()));
+ Continue(false)
+ }));
+ }));
+ }
+
+ dialog.connect_delete_event(clone!(@strong app_tx => move |_, _| {
+ let _ = app_tx.send(Box::new(|op| op.close_invite_dialog()));
+ glib::signal::Inhibit(true)
+ }));
+ cancel.connect_clicked(clone!(@strong app_tx => move |_| {
+ let _ = app_tx.send(Box::new(|op| op.close_invite_dialog()));
+ }));
+ invite.set_sensitive(false);
+ invite.connect_clicked(move |_| {
+ let _ = app_tx.send(Box::new(|op| op.invite()));
+ });
+}
diff --git a/fractal-gtk/src/appop/connect/join_room.rs b/fractal-gtk/src/appop/connect/join_room.rs
new file mode 100644
index 00000000..87306933
--- /dev/null
+++ b/fractal-gtk/src/appop/connect/join_room.rs
@@ -0,0 +1,55 @@
+use glib::clone;
+use gtk::prelude::*;
+
+use crate::appop::AppOp;
+
+pub fn connect(appop: &AppOp) {
+ let app_tx = appop.app_tx.clone();
+ let dialog = appop
+ .ui
+ .builder
+ .get_object::<gtk::Dialog>("join_room_dialog")
+ .expect("Can't find join_room_dialog in ui file.");
+ let cancel = appop
+ .ui
+ .builder
+ .get_object::<gtk::Button>("cancel_join_room")
+ .expect("Can't find cancel_join_room in ui file.");
+ let confirm = appop
+ .ui
+ .builder
+ .get_object::<gtk::Button>("join_room_button")
+ .expect("Can't find join_room_button in ui file.");
+ let entry = appop
+ .ui
+ .builder
+ .get_object::<gtk::Entry>("join_room_name")
+ .expect("Can't find join_room_name in ui file.");
+
+ cancel.connect_clicked(clone!(@strong entry, @strong dialog => move |_| {
+ dialog.hide();
+ entry.set_text("");
+ }));
+ dialog.connect_delete_event(clone!(@strong entry, @strong dialog => move |_, _| {
+ dialog.hide();
+ entry.set_text("");
+ glib::signal::Inhibit(true)
+ }));
+
+ confirm.connect_clicked(
+ clone!(@strong entry, @strong dialog, @strong app_tx => move |_| {
+ dialog.hide();
+ let _ = app_tx.send(Box::new(|op| op.join_to_room()));
+ entry.set_text("");
+ }),
+ );
+
+ entry.connect_activate(clone!(@strong dialog => move |entry| {
+ dialog.hide();
+ let _ = app_tx.send(Box::new(|op| op.join_to_room()));
+ entry.set_text("");
+ }));
+ entry.connect_changed(clone!(@strong confirm => move |entry| {
+ confirm.set_sensitive(entry.get_buffer().get_length() > 0);
+ }));
+}
diff --git a/fractal-gtk/src/appop/connect/language.rs b/fractal-gtk/src/appop/connect/language.rs
new file mode 100644
index 00000000..e7f90ad1
--- /dev/null
+++ b/fractal-gtk/src/appop/connect/language.rs
@@ -0,0 +1,39 @@
+use crate::app::RUNTIME;
+use crate::appop::AppOp;
+use crate::backend::{room, HandleError};
+use glib::object::Cast;
+use gtk::prelude::*;
+
+// The TextBufferExt alias is necessary to avoid conflict with gtk's TextBufferExt
+use gspell::{CheckerExt, TextBuffer, TextBufferExt as GspellTextBufferExt};
+
+pub fn connect(appop: &AppOp) {
+ let app_tx = appop.app_tx.clone();
+ let textview = appop.ui.sventry.view.upcast_ref::<gtk::TextView>();
+ if let Some(checker) = textview
+ .get_buffer()
+ .and_then(|gtk_buffer| TextBuffer::get_from_gtk_text_buffer(>k_buffer))
+ .and_then(|gs_buffer| gs_buffer.get_spell_checker())
+ {
+ let _signal_handler = checker.connect_property_language_notify(move |checker| {
+ let _ = app_tx.send(Box::new(clone!(@weak checker => move |op| {
+ if let Some(lang_code) = checker
+ .get_language()
+ .and_then(|lang| lang.get_code())
+ .map(String::from)
+ {
+ if let (Some(active_room), Some(login_data)) = (op.active_room.clone(),
op.login_data.as_ref()) {
+ let session_client = login_data.session_client.clone();
+ let uid = login_data.uid.clone();
+ RUNTIME.spawn(async move {
+ let query = room::set_language(session_client, &uid, &active_room,
lang_code).await;
+ if let Err(err) = query {
+ err.handle_error();
+ }
+ });
+ }
+ }
+ })));
+ });
+ }
+}
diff --git a/fractal-gtk/src/appop/connect/leave_room.rs b/fractal-gtk/src/appop/connect/leave_room.rs
new file mode 100644
index 00000000..ae528b74
--- /dev/null
+++ b/fractal-gtk/src/appop/connect/leave_room.rs
@@ -0,0 +1,36 @@
+use glib::clone;
+use gtk::prelude::*;
+
+use crate::appop::AppOp;
+
+pub fn connect(appop: &AppOp) {
+ let app_tx = appop.app_tx.clone();
+ let dialog = appop
+ .ui
+ .builder
+ .get_object::<gtk::Dialog>("leave_room_dialog")
+ .expect("Can't find leave_room_dialog in ui file.");
+ let cancel = appop
+ .ui
+ .builder
+ .get_object::<gtk::Button>("leave_room_cancel")
+ .expect("Can't find leave_room_cancel in ui file.");
+ let confirm = appop
+ .ui
+ .builder
+ .get_object::<gtk::Button>("leave_room_confirm")
+ .expect("Can't find leave_room_confirm in ui file.");
+
+ cancel.connect_clicked(clone!(@strong dialog => move |_| {
+ dialog.hide();
+ }));
+ dialog.connect_delete_event(clone!(@strong dialog => move |_, _| {
+ dialog.hide();
+ glib::signal::Inhibit(true)
+ }));
+
+ confirm.connect_clicked(clone!(@strong dialog => move |_| {
+ dialog.hide();
+ let _ = app_tx.send(Box::new(|op| op.really_leave_active_room()));
+ }));
+}
diff --git a/fractal-gtk/src/appop/connect/markdown.rs b/fractal-gtk/src/appop/connect/markdown.rs
new file mode 100644
index 00000000..dbcbd9fd
--- /dev/null
+++ b/fractal-gtk/src/appop/connect/markdown.rs
@@ -0,0 +1,87 @@
+use glib::clone;
+use gtk::prelude::*;
+use sourceview4::prelude::*;
+
+use crate::util;
+
+use crate::appop::AppOp;
+
+pub fn connect(appop: &AppOp) {
+ let app_tx = appop.app_tx.clone();
+ let md_popover_btn = &appop.ui.sventry.markdown;
+ let md_img = appop.ui.sventry.markdown_img.clone();
+ let buffer = appop.ui.sventry.buffer.clone();
+
+ let popover: gtk::Popover = appop
+ .ui
+ .builder
+ .get_object("markdown_popover")
+ .expect("Couldn't find markdown_popover in ui file.");
+
+ let markdown_switch: gtk::Switch = appop
+ .ui
+ .builder
+ .get_object("markdown_switch")
+ .expect("Couldn't find markdown_switch in ui file.");
+
+ let txt: gtk::Grid = appop
+ .ui
+ .builder
+ .get_object("tutorial_text_box")
+ .expect("Couldn't find tutorial_text_box in ui file.");
+
+ let md_lang =
+ sourceview4::LanguageManager::get_default().and_then(|lm| lm.get_language("markdown"));
+
+ md_popover_btn.set_popover(Some(&popover));
+
+ let md_active = util::get_markdown_schema();
+ if md_active {
+ let _ = app_tx.send(Box::new(|op| {
+ op.md_enabled = true;
+ }));
+ markdown_switch.set_active(true);
+ md_img.set_from_icon_name(Some("format-indent-more-symbolic"), gtk::IconSize::Menu);
+ txt.get_style_context().remove_class("dim-label");
+
+ if let Some(md_lang) = md_lang.clone() {
+ buffer.set_highlight_matching_brackets(true);
+ buffer.set_language(Some(&md_lang));
+ buffer.set_highlight_syntax(true);
+ }
+ }
+
+ markdown_switch.connect_property_active_notify(clone!(@strong markdown_switch => move |_| {
+ let md_active = markdown_switch.get_active();
+ let _ = app_tx.send(Box::new(move |op| {
+ op.md_enabled = md_active;
+ }));
+
+ if markdown_switch.get_active() {
+ md_img.set_from_icon_name(
+ Some("format-indent-more-symbolic"),
+ gtk::IconSize::Menu,
+ );
+ txt.get_style_context().remove_class("dim-label");
+ util::set_markdown_schema(true);
+
+ if let Some(md_lang) = md_lang.clone() {
+ buffer.set_highlight_matching_brackets(true);
+ buffer.set_language(Some(&md_lang));
+ buffer.set_highlight_syntax(true);
+ }
+ } else {
+ md_img.set_from_icon_name(
+ Some("format-justify-left-symbolic"),
+ gtk::IconSize::Menu,
+ );
+ txt.get_style_context().add_class("dim-label");
+ util::set_markdown_schema(false);
+
+ let lang: Option<&sourceview4::Language> = None;
+ buffer.set_highlight_matching_brackets(false);
+ buffer.set_language(lang);
+ buffer.set_highlight_syntax(false);
+ }
+ }));
+}
diff --git a/fractal-gtk/src/appop/connect/mod.rs b/fractal-gtk/src/appop/connect/mod.rs
new file mode 100644
index 00000000..144d1c3a
--- /dev/null
+++ b/fractal-gtk/src/appop/connect/mod.rs
@@ -0,0 +1,36 @@
+mod account;
+mod autocomplete;
+mod direct;
+mod directory;
+mod headerbar;
+mod invite;
+mod join_room;
+mod language;
+mod leave_room;
+mod markdown;
+mod new_room;
+mod roomlist_search;
+mod send;
+mod swipeable_widgets;
+
+use crate::appop::AppOp;
+
+impl AppOp {
+ pub fn connect_gtk(&self) {
+ headerbar::connect(self);
+ send::connect(self);
+ markdown::connect(self);
+ autocomplete::connect(self);
+ language::connect(self);
+ directory::connect(self);
+ leave_room::connect(self);
+ new_room::connect(self);
+ join_room::connect(self);
+ account::connect(self);
+ invite::connect_dialog(self);
+ invite::connect_user(self);
+ direct::connect(self);
+ roomlist_search::connect(self);
+ swipeable_widgets::connect(self);
+ }
+}
diff --git a/fractal-gtk/src/appop/connect/new_room.rs b/fractal-gtk/src/appop/connect/new_room.rs
new file mode 100644
index 00000000..aff74984
--- /dev/null
+++ b/fractal-gtk/src/appop/connect/new_room.rs
@@ -0,0 +1,69 @@
+use glib::clone;
+use gtk::prelude::*;
+
+use crate::appop::AppOp;
+
+pub fn connect(appop: &AppOp) {
+ let app_tx = appop.app_tx.clone();
+ let dialog = appop
+ .ui
+ .builder
+ .get_object::<gtk::Dialog>("new_room_dialog")
+ .expect("Can't find new_room_dialog in ui file.");
+ let cancel = appop
+ .ui
+ .builder
+ .get_object::<gtk::Button>("cancel_new_room")
+ .expect("Can't find cancel_new_room in ui file.");
+ let confirm = appop
+ .ui
+ .builder
+ .get_object::<gtk::Button>("new_room_button")
+ .expect("Can't find new_room_button in ui file.");
+ let entry = appop
+ .ui
+ .builder
+ .get_object::<gtk::Entry>("new_room_name")
+ .expect("Can't find new_room_name in ui file.");
+ let private = appop
+ .ui
+ .builder
+ .get_object::<gtk::ToggleButton>("private_visibility_button")
+ .expect("Can't find private_visibility_button in ui file.");
+
+ private.set_active(true);
+ cancel.connect_clicked(
+ clone!(@strong entry, @strong dialog, @strong private => move |_| {
+ dialog.hide();
+ entry.set_text("");
+ private.set_active(true);
+ }),
+ );
+ dialog.connect_delete_event(
+ clone!(@strong entry, @strong dialog, @strong private => move |_, _| {
+ dialog.hide();
+ entry.set_text("");
+ private.set_active(true);
+ glib::signal::Inhibit(true)
+ }),
+ );
+
+ confirm.connect_clicked(
+ clone!(@strong entry, @strong dialog, @strong private, @strong app_tx => move |_| {
+ dialog.hide();
+ let _ = app_tx.send(Box::new(|op| op.create_new_room()));
+ entry.set_text("");
+ private.set_active(true);
+ }),
+ );
+
+ entry.connect_activate(clone!(@strong dialog => move |entry| {
+ dialog.hide();
+ let _ = app_tx.send(Box::new(|op| op.create_new_room()));
+ entry.set_text("");
+ private.set_active(true);
+ }));
+ entry.connect_changed(clone!(@strong confirm => move |entry| {
+ confirm.set_sensitive(entry.get_buffer().get_length() > 0);
+ }));
+}
diff --git a/fractal-gtk/src/appop/connect/roomlist_search.rs
b/fractal-gtk/src/appop/connect/roomlist_search.rs
new file mode 100644
index 00000000..774eecc4
--- /dev/null
+++ b/fractal-gtk/src/appop/connect/roomlist_search.rs
@@ -0,0 +1,60 @@
+use glib::clone;
+use gtk::prelude::*;
+
+use crate::appop::AppOp;
+
+pub fn connect(appop: &AppOp) {
+ let app_tx = appop.app_tx.clone();
+ let search_btn = appop
+ .ui
+ .builder
+ .get_object::<gtk::ToggleButton>("room_search_button")
+ .expect("Can't find room_search_button in ui file.");
+ let search_bar = appop
+ .ui
+ .builder
+ .get_object::<gtk::SearchBar>("room_list_searchbar")
+ .expect("Can't find room_list_searchbar in ui file.");
+ let search_entry = appop
+ .ui
+ .builder
+ .get_object::<gtk::SearchEntry>("room_list_search")
+ .expect("Can't find room_list_search in ui file.");
+
+ search_btn.connect_toggled(clone!(@strong search_bar => move |btn| {
+ search_bar.set_search_mode(btn.get_active());
+ }));
+
+ search_bar.connect_property_search_mode_enabled_notify(
+ clone!(@strong search_btn => move |headerbar| {
+ search_btn.set_active(headerbar.get_search_mode());
+ }),
+ );
+
+ search_entry.connect_search_changed(move |entry| {
+ let search_text = Some(entry.get_text().to_string());
+ let _ = app_tx.send(Box::new(|op| op.filter_rooms(search_text)));
+ });
+
+ // hidding left and right boxes to align with top buttons
+ let boxes = search_bar.get_children()[0]
+ .clone()
+ .downcast::<gtk::Revealer>()
+ .unwrap() // revealer
+ .get_children()[0]
+ .clone()
+ .downcast::<gtk::Box>()
+ .unwrap(); // box
+ boxes.get_children()[0]
+ .clone()
+ .downcast::<gtk::Box>()
+ .unwrap()
+ .hide();
+ boxes.get_children()[1].clone().set_hexpand(true);
+ boxes.get_children()[1].clone().set_halign(gtk::Align::Fill);
+ boxes.get_children()[2]
+ .clone()
+ .downcast::<gtk::Box>()
+ .unwrap()
+ .hide();
+}
diff --git a/fractal-gtk/src/appop/connect/send.rs b/fractal-gtk/src/appop/connect/send.rs
new file mode 100644
index 00000000..2682a4f0
--- /dev/null
+++ b/fractal-gtk/src/appop/connect/send.rs
@@ -0,0 +1,69 @@
+use glib::clone;
+use gtk::prelude::*;
+use sourceview4::BufferExt;
+
+use crate::actions::activate_action;
+use crate::appop::AppOp;
+
+const MAX_INPUT_HEIGHT: i32 = 100;
+
+pub fn connect(appop: &AppOp) {
+ let app_tx = appop.app_tx.clone();
+ appop.ui.sventry.container.set_redraw_on_allocate(true);
+ let msg_entry = appop.ui.sventry.view.clone();
+ let buffer = &appop.ui.sventry.buffer;
+ buffer.set_highlight_matching_brackets(false);
+
+ let msg_entry_box = appop.ui.sventry.entry_box.clone();
+ msg_entry_box.set_redraw_on_allocate(true);
+
+ if let Some(adjustment) = appop.ui.sventry.scroll.get_vadjustment() {
+ adjustment.connect_value_changed(clone!(@strong msg_entry => move |adj| {
+ if msg_entry.get_allocated_height() < MAX_INPUT_HEIGHT {
+ adj.set_value(0.0);
+ }
+ }));
+ }
+
+ let autocomplete_popover = appop
+ .ui
+ .builder
+ .get_object::<gtk::Popover>("autocomplete_popover")
+ .expect("Can't find autocomplete_popover in ui file.");
+
+ msg_entry.connect_key_press_event(
+ clone!(@strong app_tx => move |_, key| match key.get_keyval() {
+ gdk::keys::constants::Return | gdk::keys::constants::KP_Enter
+ if !key.get_state().contains(gdk::ModifierType::SHIFT_MASK)
+ && !autocomplete_popover.is_visible() =>
+ {
+ activate_action(&app_tx, "app", "send-message");
+ Inhibit(true)
+ }
+ _ => Inhibit(false),
+ }),
+ );
+
+ msg_entry.connect_key_release_event(clone!(@strong app_tx => move |_, ev| {
+ if ev.get_keyval().to_unicode().is_some() {
+ let _ = app_tx.send(Box::new(|op| op.send_typing()));
+ }
+ Inhibit(false)
+ }));
+
+ msg_entry.connect_paste_clipboard(move |_| {
+ let _ = app_tx.send(Box::new(|op| op.paste()));
+ });
+
+ msg_entry.connect_focus_in_event(clone!(@strong msg_entry_box => move |_, _| {
+ msg_entry_box.get_style_context().add_class("message-input-focused");
+
+ Inhibit(false)
+ }));
+
+ msg_entry.connect_focus_out_event(clone!(@strong msg_entry_box => move |_, _| {
+ msg_entry_box.get_style_context().remove_class("message-input-focused");
+
+ Inhibit(false)
+ }));
+}
diff --git a/fractal-gtk/src/appop/connect/swipeable_widgets.rs
b/fractal-gtk/src/appop/connect/swipeable_widgets.rs
new file mode 100644
index 00000000..16315db8
--- /dev/null
+++ b/fractal-gtk/src/appop/connect/swipeable_widgets.rs
@@ -0,0 +1,63 @@
+use gio::prelude::*;
+use gtk::prelude::*;
+use libhandy::prelude::*;
+
+use crate::appop::AppOp;
+
+// Set up HdyDeck and HdyLeaflet so that swipes trigger the
+// same behaviour as the back button.
+pub fn connect(appop: &AppOp) {
+ let deck: libhandy::Deck = appop
+ .ui
+ .builder
+ .get_object("main_deck")
+ .expect("Can't find main_deck in UI file");
+ let leaflet: libhandy::Leaflet = appop
+ .ui
+ .builder
+ .get_object("chat_page")
+ .expect("Can't find chat_page in UI file");
+
+ let app = gio::Application::get_default()
+ .expect("Could not get default application")
+ .downcast::<gtk::Application>()
+ .unwrap();
+ let global_back = app
+ .lookup_action("back")
+ .expect("Could not get back action");
+
+ deck.connect_property_transition_running_notify(
+ clone!(@weak app, @weak global_back => move |deck| {
+ let child: Option<String> = deck.get_visible_child_name().map(|g| g.to_string());
+ if !deck.get_transition_running() && child == Some("chat".to_string()) {
+ // Re-enable global back when returning to main view
+ let _ = global_back.set_property("enabled", &true);
+ app.activate_action("back", None);
+ }
+ }),
+ );
+
+ deck.connect_property_visible_child_notify(
+ clone!(@weak app, @weak global_back => move |deck| {
+ let child: Option<String> = deck.get_visible_child_name().map(|g| g.to_string());
+ if !deck.get_transition_running() && child == Some("chat".to_string()) {
+ let _ = global_back.set_property("enabled", &true);
+ app.activate_action("back", None);
+ }
+ }),
+ );
+
+ leaflet.connect_property_child_transition_running_notify(clone!(@weak app => move |leaflet| {
+ let child: Option<String> = leaflet.get_visible_child_name().map(|g| g.to_string());
+ if !leaflet.get_child_transition_running() && child == Some("sidebar".to_string()) {
+ app.activate_action("back", None);
+ }
+ }));
+
+ leaflet.connect_property_visible_child_notify(clone!(@weak app => move |leaflet| {
+ let child: Option<String> = deck.get_visible_child_name().map(|g| g.to_string());
+ if !leaflet.get_child_transition_running() && child == Some("sidebar".to_string()) {
+ app.activate_action("back", None);
+ }
+ }));
+}
diff --git a/fractal-gtk/src/appop/mod.rs b/fractal-gtk/src/appop/mod.rs
index 8d00827a..46e66c78 100644
--- a/fractal-gtk/src/appop/mod.rs
+++ b/fractal-gtk/src/appop/mod.rs
@@ -30,6 +30,7 @@ use crate::widgets;
mod about;
mod account;
pub mod attach;
+mod connect;
mod directory;
mod invite;
mod login;
diff --git a/fractal-gtk/src/meson.build b/fractal-gtk/src/meson.build
index 994cf799..1ad69ed6 100644
--- a/fractal-gtk/src/meson.build
+++ b/fractal-gtk/src/meson.build
@@ -52,23 +52,23 @@ app_sources = files(
'api/identity.rs',
'api/mod.rs',
'api/r0.rs',
- 'app/connect/account.rs',
- 'app/connect/autocomplete.rs',
- 'app/connect/directory.rs',
- 'app/connect/direct.rs',
- 'app/connect/headerbar.rs',
- 'app/connect/invite.rs',
- 'app/connect/join_room.rs',
- 'app/connect/language.rs',
- 'app/connect/leave_room.rs',
- 'app/connect/markdown.rs',
- 'app/connect/mod.rs',
- 'app/connect/new_room.rs',
- 'app/connect/roomlist_search.rs',
- 'app/connect/send.rs',
- 'app/connect/swipeable_widgets.rs',
'app/mod.rs',
'app/windowstate.rs',
+ 'appop/connect/account.rs',
+ 'appop/connect/autocomplete.rs',
+ 'appop/connect/directory.rs',
+ 'appop/connect/direct.rs',
+ 'appop/connect/headerbar.rs',
+ 'appop/connect/invite.rs',
+ 'appop/connect/join_room.rs',
+ 'appop/connect/language.rs',
+ 'appop/connect/leave_room.rs',
+ 'appop/connect/markdown.rs',
+ 'appop/connect/mod.rs',
+ 'appop/connect/new_room.rs',
+ 'appop/connect/roomlist_search.rs',
+ 'appop/connect/send.rs',
+ 'appop/connect/swipeable_widgets.rs',
'appop/about.rs',
'appop/account.rs',
'appop/attach.rs',
diff --git a/fractal-gtk/src/widgets/autocomplete.rs b/fractal-gtk/src/widgets/autocomplete.rs
index ca713824..7244e314 100644
--- a/fractal-gtk/src/widgets/autocomplete.rs
+++ b/fractal-gtk/src/widgets/autocomplete.rs
@@ -10,6 +10,7 @@ use gtk::TextTag;
use crate::model::member::Member;
+use crate::app;
use crate::appop::AppOp;
use crate::widgets;
@@ -27,7 +28,6 @@ pub struct Autocomplete {
impl Autocomplete {
pub fn new(
- op: Arc<Mutex<AppOp>>,
window: gtk::Window,
msg_entry: sourceview4::View,
popover: gtk::Popover,
@@ -42,7 +42,7 @@ impl Autocomplete {
popover_position: None,
popover_search: None,
popover_closing: false,
- op,
+ op: app::get_op().clone(),
}
}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]