subclassing: use composite templates everywhere

This commit is contained in:
Bilal Elmoussaoui 2020-12-03 15:10:24 +01:00
parent c581c12ab3
commit ddddb97c5f
25 changed files with 2226 additions and 1857 deletions

2452
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -20,6 +20,7 @@ surf = "2.1"
url = "2.1"
zbar-rust = "0.0"
secret-service = "1.1"
once_cell = "1.5"
[dependencies.gtk]
git = "https://github.com/gtk-rs/gtk4-rs"

View file

@ -15,7 +15,7 @@
<!-- UI Files -->
<file compressed="true" preprocess="xml-stripblanks" alias="shortcuts.ui">resources/ui/shortcuts.ui</file>
<file compressed="true" preprocess="xml-stripblanks">about_dialog.ui</file>
<file compressed="true" preprocess="xml-stripblanks">window.ui</file>
<file>window.ui</file>
<file compressed="true" preprocess="xml-stripblanks">preferences.ui</file>
<file compressed="true" preprocess="xml-stripblanks">preferences_password_page.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="preferences_provider_page.ui">resources/ui/preferences_provider_page.ui</file>

View file

@ -1,11 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<object class="HdyWindow" id="add_dialog">
<template parent="HdyWindow" class="AccountAddDialog">
<property name="modal">True</property>
<property name="default_width">360</property>
<property name="default_height">600</property>
<property name="title">Add a new account</property>
<property name="destroy_with_parent">True</property>
<child>
<object class="HdyLeaflet">
<child>
@ -143,8 +142,7 @@
</child>
<child>
<object class="GtkListBox" id="more_list">
<property name="visible">False</property>
<property name="visible">False</property>
<property name="selection_mode">none</property>
<child>
<object class="HdyActionRow" id="provider_website_row">
@ -211,7 +209,7 @@
</child>
</object>
</child>
</object>
</template>
<object class="GtkEntryCompletion" id="provider_completion">
<property name="minimum_key_length">2</property>
<property name="text_column">1</property>

View file

@ -10,7 +10,7 @@
<attribute name="action">account.delete</attribute>
</item>
</menu>
<object class="GtkListBoxRow" id="account_row">
<template parent="GtkListBoxRow" class="AccountRow">
<property name="can_focus">True</property>
<property name="valign">center</property>
<property name="selectable">False</property>
@ -118,5 +118,5 @@
<style>
<class name="account-row"/>
</style>
</object>
</template>
</interface>

View file

@ -6,7 +6,7 @@
<property name="step_increment">1</property>
<property name="page_increment">10</property>
</object>
<object class="HdyPreferencesWindow" id="preferences_window">
<template class="PreferencesWindow" parent="HdyPreferencesWindow">
<property name="default_width">360</property>
<property name="default_height">600</property>
<property name="search-enabled">False</property>
@ -126,6 +126,6 @@
</child>
</object>
</child>
</object>
</template>
</interface>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<object class="GtkBox" id="password_page">
<template parent="GtkBox" class="PasswordPage">
<property name="orientation">vertical</property>
<child>
<object class="GtkHeaderBar">
@ -123,7 +123,7 @@
</child>
</object>
</child>
</object>
</template>
<object class="GtkSizeGroup">
<property name="mode">GTK_SIZE_GROUP_HORIZONTAL</property>
<widgets>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<object class="GtkListBoxRow" id="provider_row">
<template parent="GtkListBoxRow" class="ProviderRow">
<property name="can_focus">True</property>
<property name="valign">center</property>
<property name="selectable">False</property>
@ -9,7 +9,7 @@
<property name="orientation">vertical</property>
<property name="vexpand">True</property>
<child>
<object class="GtkLabel" id="name">
<object class="GtkLabel" id="name_label">
<property name="halign">start</property>
<style>
<class name="title-2" />
@ -29,5 +29,5 @@
<style>
<class name="provider-row"/>
</style>
</object>
</template>
</interface>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<object class="GtkBox" id="providers_box">
<template parent="GtkBox" class="ProvidersList">
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="orientation">vertical</property>
@ -123,5 +123,5 @@
</child>
</object>
</child>
</object>
</template>
</interface>

View file

@ -18,157 +18,161 @@
<attribute name="action">app.about</attribute>
</item>
</menu>
<object class="HdyLeaflet" id="deck">
<property name="can-unfold">False</property>
<template class="Window" parent="HdyApplicationWindow">
<property name="icon_name">@APP_ID@</property>
<child>
<object class="HdyLeafletPage">
<property name="name">accounts</property>
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="GtkHeaderBar">
<property name="show-title-buttons">True</property>
<child type="start">
<object class="GtkButton" id="add_btn">
<property name="receives_default">True</property>
<property name="action_name">win.add-account</property>
<property name="icon-name">list-add-symbolic</property>
</object>
</child>
<child type="end">
<object class="GtkMenuButton">
<property name="receives_default">True</property>
<property name="menu-model">menu</property>
<property name="icon-name">open-menu-symbolic</property>
</object>
</child>
<child type="end">
<object class="GtkToggleButton" id="search_btn">
<property name="receives_default">True</property>
<property name="icon-name">system-search-symbolic</property>
</object>
</child>
<style>
<class name="titlebar"/>
</style>
</object>
</child>
<child>
<object class="GtkBox" id="container">
<property name="vexpand">True</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkSearchBar" id="search_bar">
<child>
<object class="GtkSearchEntry" id="search_entry">
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</property>
</object>
</child>
<child>
<object class="HdyLeafletPage">
<property name="name">login</property>
<property name="child">
<object class="GtkWindowHandle">
<child>
<object class="HdyLeaflet" id="deck">
<property name="can-unfold">False</property>
<child>
<object class="HdyLeafletPage">
<property name="name">accounts</property>
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="GtkBox">
<child>
<object class="GtkWindowControls">
<property name="side">start</property>
<property name="halign">start</property>
<property name="hexpand">true</property>
<property name="margin-top">6</property>
<property name="margin-bottom">6</property>
<property name="margin-start">6</property>
<object class="GtkHeaderBar">
<property name="show-title-buttons">True</property>
<child type="start">
<object class="GtkButton" id="add_btn">
<property name="receives_default">True</property>
<property name="action_name">win.add-account</property>
<property name="icon-name">list-add-symbolic</property>
</object>
</child>
<child type="end">
<object class="GtkMenuButton">
<property name="receives_default">True</property>
<property name="menu-model">menu</property>
<property name="icon-name">open-menu-symbolic</property>
</object>
</child>
<child type="end">
<object class="GtkToggleButton" id="search_btn">
<property name="receives_default">True</property>
<property name="icon-name">system-search-symbolic</property>
</object>
</child>
<style>
<class name="titlebar"/>
</style>
</object>
</child>
<child>
<object class="GtkBox" id="container">
<property name="vexpand">True</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkWindowControls">
<property name="side">end</property>
<property name="halign">end</property>
<property name="hexpand">true</property>
<property name="margin_top">6</property>
<property name="margin_bottom">6</property>
<property name="margin_end">6</property>
<object class="GtkSearchBar" id="search_bar">
<child>
<object class="GtkSearchEntry" id="search_entry">
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</property>
</object>
</child>
<child>
<object class="HdyLeafletPage">
<property name="name">login</property>
<property name="child">
<object class="GtkWindowHandle">
<child>
<object class="GtkBox">
<property name="halign">center</property>
<property name="valign">center</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="spacing">24</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkImage">
<property name="halign">center</property>
<property name="hexpand">True</property>
<property name="icon-name">@app-id@</property>
<property name="pixel-size">128</property>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="halign">center</property>
<property name="margin-top">12</property>
<property name="label" translatable="yes">Authenticator is locked</property>
<style>
<class name="title-2"/>
</style>
<object class="GtkBox">
<child>
<object class="GtkWindowControls">
<property name="side">start</property>
<property name="halign">start</property>
<property name="hexpand">true</property>
<property name="margin-top">6</property>
<property name="margin-bottom">6</property>
<property name="margin-start">6</property>
</object>
</child>
<child>
<object class="GtkWindowControls">
<property name="side">end</property>
<property name="halign">end</property>
<property name="hexpand">true</property>
<property name="margin_top">6</property>
<property name="margin_bottom">6</property>
<property name="margin_end">6</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<property name="halign">center</property>
<property name="valign">start</property>
<property name="margin-top">24</property>
<property name="valign">center</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="spacing">24</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkPasswordEntry" id="password_entry">
<object class="GtkImage">
<property name="halign">center</property>
<property name="activates-default">True</property>
<property name="show-peek-icon">True</property>
<property name="hexpand">True</property>
<property name="icon-name">@app-id@</property>
<property name="pixel-size">128</property>
</object>
</child>
<child>
<object class="GtkButton">
<property name="label" translatable="yes">Unlock</property>
<property name="action-name">win.unlock</property>
<property name="receives-default">True</property>
<object class="GtkLabel">
<property name="halign">center</property>
<property name="margin-top">16</property>
<property name="margin-top">12</property>
<property name="label" translatable="yes">Authenticator is locked</property>
<style>
<class name="suggested-action"/>
<class name="pill-button" />
<class name="large-button" />
<class name="title-2"/>
</style>
</object>
</child>
<child>
<object class="GtkBox">
<property name="halign">center</property>
<property name="valign">start</property>
<property name="margin-top">24</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkPasswordEntry" id="password_entry">
<property name="halign">center</property>
<property name="activates-default">True</property>
<property name="show-peek-icon">True</property>
</object>
</child>
<child>
<object class="GtkButton">
<property name="label" translatable="yes">Unlock</property>
<property name="action-name">win.unlock</property>
<property name="receives-default">True</property>
<property name="halign">center</property>
<property name="margin-top">16</property>
<style>
<class name="suggested-action"/>
<class name="pill-button" />
<class name="large-button" />
</style>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</property>
</object>
</property>
</child>
</object>
</child>
</object>
</template>
</interface>

View file

@ -1,7 +1,7 @@
use crate::config;
use crate::helpers::Keyring;
use crate::models::{Account, Provider, ProvidersModel};
use crate::widgets::{AddAccountDialog, PreferencesWindow, View, Window, WindowPrivate};
use crate::widgets::{AccountAddDialog, PreferencesWindow, View, Window};
use gio::prelude::*;
use glib::subclass::prelude::*;
use glib::{subclass, WeakRef};
@ -95,12 +95,12 @@ impl ObjectImpl for ApplicationPrivate {
}
}
fn get_property(&self, _obj: &Self::Type, id: usize) -> Result<glib::Value, ()> {
fn get_property(&self, _obj: &Self::Type, id: usize) -> glib::Value {
let prop = &PROPERTIES[id];
match *prop {
subclass::Property("locked", ..) => Ok(self.locked.get().to_value()),
subclass::Property("can-be-locked", ..) => Ok(self.can_be_locked.get().to_value()),
subclass::Property("locked", ..) => self.locked.get().to_value(),
subclass::Property("can-be-locked", ..) => self.can_be_locked.get().to_value(),
_ => unimplemented!(),
}
}
@ -127,11 +127,11 @@ impl ApplicationImpl for ApplicationPrivate {
action!(
app,
"preferences",
clone!(@strong app, @weak self.model as model => move |_,_| {
clone!(@strong app => move |_,_| {
let window = app.get_active_window().unwrap();
let preferences = PreferencesWindow::new(model);
preferences.widget.set_transient_for(Some(&window));
preferences.widget.show();
let preferences = PreferencesWindow::new();
preferences.set_transient_for(Some(&window));
preferences.show();
})
);
@ -249,23 +249,21 @@ impl Application {
match action {
Action::OpenAddAccountDialog => {
let dialog = AddAccountDialog::new(self_.model.clone(), self_.sender.clone());
dialog.widget.set_transient_for(Some(&active_window));
dialog.widget.show();
let dialog = AccountAddDialog::new(self_.model.clone(), self_.sender.clone());
dialog.set_transient_for(Some(&active_window));
dialog.show();
}
Action::AccountCreated(account, provider) => {
let win_ = active_window.downcast_ref::<Window>().unwrap();
let priv_ = WindowPrivate::from_instance(win_);
let win = active_window.downcast_ref::<Window>().unwrap();
self_.model.add_account(&account, &provider);
priv_.providers.refilter();
win.providers().refilter();
}
Action::AccountRemoved(account) => {
let win_ = active_window.downcast_ref::<Window>().unwrap();
let priv_ = WindowPrivate::from_instance(win_);
let win = active_window.downcast_ref::<Window>().unwrap();
self_.model.remove_account(&account);
priv_.providers.refilter();
win.providers().refilter();
}
Action::SetView(view) => {
let win_ = active_window.downcast_ref::<Window>().unwrap();

View file

@ -46,7 +46,7 @@ pub(crate) fn scan(screenshot: &gio::File) -> Result<OtpAuth> {
let img = image::load_from_memory(&data)?;
let (width, height) = img.dimensions();
let img_data: Vec<u8> = img.to_luma().to_vec();
let img_data: Vec<u8> = img.to_luma8().to_vec();
let mut scanner = ZBarImageScanner::new();

View file

@ -122,14 +122,14 @@ impl ObjectImpl for AccountPriv {
}
}
fn get_property(&self, _obj: &Self::Type, id: usize) -> Result<glib::Value, ()> {
fn get_property(&self, _obj: &Self::Type, id: usize) -> glib::Value {
let prop = &PROPERTIES[id];
match *prop {
subclass::Property("id", ..) => Ok(self.id.get().to_value()),
subclass::Property("name", ..) => Ok(self.name.borrow().to_value()),
subclass::Property("token-id", ..) => Ok(self.token_id.borrow().to_value()),
subclass::Property("provider-id", ..) => Ok(self.provider_id.get().to_value()),
subclass::Property("id", ..) => self.id.get().to_value(),
subclass::Property("name", ..) => self.name.borrow().to_value(),
subclass::Property("token-id", ..) => self.token_id.borrow().to_value(),
subclass::Property("provider-id", ..) => self.provider_id.get().to_value(),
_ => unimplemented!(),
}
}

View file

@ -197,17 +197,17 @@ impl ObjectImpl for ProviderPriv {
}
}
fn get_property(&self, _obj: &Self::Type, id: usize) -> Result<glib::Value, ()> {
fn get_property(&self, _obj: &Self::Type, id: usize) -> glib::Value {
let prop = &PROPERTIES[id];
match *prop {
subclass::Property("id", ..) => Ok(self.id.get().to_value()),
subclass::Property("name", ..) => Ok(self.name.borrow().to_value()),
subclass::Property("period", ..) => Ok(self.period.get().to_value()),
subclass::Property("algorithm", ..) => Ok(self.algorithm.borrow().to_value()),
subclass::Property("website", ..) => Ok(self.website.borrow().to_value()),
subclass::Property("help-url", ..) => Ok(self.help_url.borrow().to_value()),
subclass::Property("image-uri", ..) => Ok(self.image_uri.borrow().to_value()),
subclass::Property("id", ..) => self.id.get().to_value(),
subclass::Property("name", ..) => self.name.borrow().to_value(),
subclass::Property("period", ..) => self.period.get().to_value(),
subclass::Property("algorithm", ..) => self.algorithm.borrow().to_value(),
subclass::Property("website", ..) => self.website.borrow().to_value(),
subclass::Property("help-url", ..) => self.help_url.borrow().to_value(),
subclass::Property("image-uri", ..) => self.image_uri.borrow().to_value(),
_ => unimplemented!(),
}
}

View file

@ -3,61 +3,155 @@ use crate::helpers::qrcode;
use crate::models::{Account, Provider, ProvidersModel};
use anyhow::Result;
use gio::prelude::*;
use gio::{subclass::ObjectSubclass, ActionMapExt};
use glib::subclass::prelude::*;
use glib::{glib_object_subclass, glib_wrapper};
use glib::{signal::Inhibit, Receiver, Sender};
use gtk::prelude::*;
use gtk::{prelude::*, CompositeTemplate};
use libhandy::ActionRowExt;
use once_cell::sync::OnceCell;
use std::cell::RefCell;
use std::rc::Rc;
pub enum AddAccountAction {
pub enum AccountAddAction {
SetIcon(gio::File),
SetProvider(Provider),
Save,
ScanQR,
}
pub struct AddAccountDialog {
pub widget: libhandy::Window,
builder: gtk::Builder,
global_sender: Sender<Action>,
sender: Sender<AddAccountAction>,
receiver: RefCell<Option<Receiver<AddAccountAction>>>,
model: Rc<ProvidersModel>,
selected_provider: Rc<RefCell<Option<Provider>>>,
actions: gio::SimpleActionGroup,
mod imp {
use super::*;
use glib::subclass;
use gtk::subclass::prelude::*;
#[derive(CompositeTemplate)]
pub struct AccountAddDialog {
pub global_sender: OnceCell<Sender<Action>>,
pub sender: Sender<AccountAddAction>,
pub receiver: RefCell<Option<Receiver<AccountAddAction>>>,
pub model: OnceCell<Rc<ProvidersModel>>,
pub selected_provider: OnceCell<Provider>,
pub actions: gio::SimpleActionGroup,
#[template_child(id = "username_entry")]
pub username_entry: TemplateChild<gtk::Entry>,
#[template_child(id = "token_entry")]
pub token_entry: TemplateChild<gtk::Entry>,
#[template_child(id = "more_list")]
pub more_list: TemplateChild<gtk::ListBox>,
#[template_child(id = "period_label")]
pub period_label: TemplateChild<gtk::Label>,
#[template_child(id = "provider_entry")]
pub provider_entry: TemplateChild<gtk::Entry>,
#[template_child(id = "algorithm_label")]
pub algorithm_label: TemplateChild<gtk::Label>,
#[template_child(id = "provider_website_row")]
pub provider_website_row: TemplateChild<libhandy::ActionRow>,
#[template_child(id = "provider_help_row")]
pub provider_help_row: TemplateChild<libhandy::ActionRow>,
#[template_child(id = "provider_completion")]
pub provider_completion: TemplateChild<gtk::EntryCompletion>,
#[template_child(id = "image")]
pub image: TemplateChild<gtk::Image>,
#[template_child(id = "spinner")]
pub spinner: TemplateChild<gtk::Spinner>,
#[template_child(id = "image_stack")]
pub image_stack: TemplateChild<gtk::Stack>,
}
impl ObjectSubclass for AccountAddDialog {
const NAME: &'static str = "AccountAddDialog";
type Type = super::AccountAddDialog;
type ParentType = libhandy::Window;
type Instance = subclass::simple::InstanceStruct<Self>;
type Class = subclass::simple::ClassStruct<Self>;
glib_object_subclass!();
fn new() -> Self {
let (sender, r) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
let receiver = RefCell::new(Some(r));
let actions = gio::SimpleActionGroup::new();
Self {
global_sender: OnceCell::new(),
sender,
receiver,
actions,
model: OnceCell::new(),
selected_provider: OnceCell::new(),
token_entry: TemplateChild::default(),
username_entry: TemplateChild::default(),
more_list: TemplateChild::default(),
period_label: TemplateChild::default(),
provider_entry: TemplateChild::default(),
algorithm_label: TemplateChild::default(),
provider_website_row: TemplateChild::default(),
provider_help_row: TemplateChild::default(),
provider_completion: TemplateChild::default(),
image: TemplateChild::default(),
spinner: TemplateChild::default(),
image_stack: TemplateChild::default(),
}
}
fn class_init(klass: &mut Self::Class) {
klass.set_template_from_resource("/com/belmoussaoui/Authenticator/account_add.ui");
Self::bind_template_children(klass);
}
}
impl ObjectImpl for AccountAddDialog {
fn constructed(&self, obj: &Self::Type) {
obj.init_template();
self.parent_constructed(obj);
}
}
impl WidgetImpl for AccountAddDialog {}
impl WindowImpl for AccountAddDialog {}
impl libhandy::subclass::window::WindowImpl for AccountAddDialog {}
}
glib_wrapper! {
pub struct AccountAddDialog(ObjectSubclass<imp::AccountAddDialog>) @extends gtk::Widget, gtk::Window, libhandy::Window;
}
impl AddAccountDialog {
pub fn new(model: Rc<ProvidersModel>, global_sender: Sender<Action>) -> Rc<Self> {
let builder = gtk::Builder::from_resource("/com/belmoussaoui/Authenticator/account_add.ui");
get_widget!(builder, libhandy::Window, add_dialog);
impl AccountAddDialog {
pub fn new(model: Rc<ProvidersModel>, global_sender: Sender<Action>) -> Self {
let dialog = glib::Object::new(Self::static_type(), &[])
.expect("Failed to create AccountAddDialog")
.downcast::<AccountAddDialog>()
.expect("Created object is of wrong type");
let (sender, r) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
let receiver = RefCell::new(Some(r));
let actions = gio::SimpleActionGroup::new();
let self_ = imp::AccountAddDialog::from_instance(&dialog);
self_.model.set(model);
self_.global_sender.set(global_sender);
let add_account_dialog = Rc::new(Self {
widget: add_dialog,
builder,
global_sender,
sender,
receiver,
actions,
model,
selected_provider: Rc::new(RefCell::new(None)),
});
add_account_dialog.setup_actions();
add_account_dialog.setup_signals();
add_account_dialog.setup_widgets(add_account_dialog.clone());
add_account_dialog
dialog.setup_actions();
dialog.setup_signals();
dialog.setup_widgets();
dialog
}
fn setup_signals(&self) {
get_widget!(self.builder, gtk::Entry, username_entry);
get_widget!(self.builder, gtk::Entry, token_entry);
let self_ = imp::AccountAddDialog::from_instance(self);
let validate_entries = clone!(@weak username_entry, @weak token_entry, @strong self.actions as actions => move |_: &gtk::Entry| {
let username_entry = self_.username_entry.get();
let token_entry = self_.token_entry.get();
let validate_entries = clone!(@weak username_entry, @weak token_entry, @strong self_.actions as actions => move |_: &gtk::Entry| {
let username = username_entry.get_text().unwrap();
let token = token_entry.get_text().unwrap();
@ -70,27 +164,33 @@ impl AddAccountDialog {
token_entry.connect_changed(validate_entries);
let event_controller = gtk::EventControllerKey::new();
event_controller.connect_key_pressed(clone!(@weak self.widget as widget => @default-return Inhibit(false), move |_, k, _, _| {
if k == 65307 {
widget.close();
}
Inhibit(false)
}));
self.widget.add_controller(&event_controller);
event_controller.connect_key_pressed(
clone!(@weak self as widget => @default-return Inhibit(false), move |_, k, _, _| {
if k == 65307 {
widget.close();
}
Inhibit(false)
}),
);
self.add_controller(&event_controller);
}
fn scan_qr(&self) -> Result<()> {
let self_ = imp::AccountAddDialog::from_instance(self);
let token_entry = self_.token_entry.get();
let username_entry = self_.username_entry.get();
qrcode::screenshot_area(
clone!(@strong self.builder as builder, @strong self.model as model,
@strong self.sender as sender => move |screenshot| {
clone!(@weak token_entry, @weak username_entry, @strong self_.model as model,
@strong self_.sender as sender => move |screenshot| {
if let Ok(otpauth) = qrcode::scan(&gio::File::new_for_uri(&screenshot)) {
get_widget!(builder, gtk::Entry, @token_entry).set_text(&otpauth.token);
token_entry.set_text(&otpauth.token);
if let Some(ref username) = otpauth.account {
get_widget!(builder, gtk::Entry, @username_entry).set_text(&username);
username_entry.set_text(&username);
}
if let Some(ref provider) = otpauth.issuer {
let provider = model.find_by_name(provider).unwrap();
send!(sender, AddAccountAction::SetProvider(provider));
let provider = model.get().unwrap().find_by_name(provider).unwrap();
send!(sender, AccountAddAction::SetProvider(provider));
}
}
}),
@ -99,98 +199,100 @@ impl AddAccountDialog {
}
fn save(&self) -> Result<()> {
if let Some(provider) = self.selected_provider.borrow().clone() {
let username = get_widget!(self.builder, gtk::Entry, @username_entry)
.get_text()
.unwrap();
let token = get_widget!(self.builder, gtk::Entry, @token_entry)
.get_text()
.unwrap();
let self_ = imp::AccountAddDialog::from_instance(self);
if let Some(provider) = self_.selected_provider.get().clone() {
let username = self_.username_entry.get().get_text().unwrap();
let token = self_.token_entry.get().get_text().unwrap();
let account = Account::create(&username, &token, provider.id())?;
send!(
self.global_sender,
Action::AccountCreated(account, provider)
self_.global_sender.get().unwrap(),
Action::AccountCreated(account, provider.clone())
);
}
Ok(())
}
fn set_provider(&self, provider: Provider) {
get_widget!(self.builder, gtk::ListBox, @more_list).show();
get_widget!(self.builder, gtk::Entry, @provider_entry).set_text(&provider.name());
get_widget!(self.builder, gtk::Label, @period_label)
.set_text(&format!("{} seconds", provider.period()));
get_widget!(self.builder, gtk::Label, @algorithm_label)
let self_ = imp::AccountAddDialog::from_instance(self);
self_.more_list.get().show();
self_.provider_entry.get().set_text(&provider.name());
self_
.period_label
.get()
.set_text(&provider.period().to_string());
self_
.algorithm_label
.get()
.set_text(&provider.algorithm().to_locale_string());
if let Some(ref website) = provider.website() {
get_widget!(self.builder, libhandy::ActionRow, provider_website_row);
provider_website_row.set_subtitle(Some(website));
self_.provider_website_row.get().set_subtitle(Some(website));
}
if let Some(ref help_url) = provider.help_url() {
get_widget!(self.builder, libhandy::ActionRow, provider_help_row);
provider_help_row.set_subtitle(Some(help_url));
self_.provider_help_row.get().set_subtitle(Some(help_url));
}
get_widget!(self.builder, gtk::Stack, @image_stack).set_visible_child_name("loading");
get_widget!(self.builder, gtk::Spinner, @spinner).start();
self_.image_stack.get().set_visible_child_name("loading");
self_.spinner.get().start();
let p = provider.clone();
let sender = self.sender.clone();
let sender = self_.sender.clone();
spawn!(async move {
if let Ok(file) = p.favicon().await {
send!(sender, AddAccountAction::SetIcon(file));
send!(sender, AccountAddAction::SetIcon(file));
}
});
self.selected_provider.replace(Some(provider));
self_.selected_provider.set(provider);
}
fn setup_actions(&self) {
let self_ = imp::AccountAddDialog::from_instance(self);
action!(
self.actions,
self_.actions,
"back",
clone!(@weak self.widget as dialog => move |_, _| {
clone!(@weak self as dialog => move |_, _| {
dialog.destroy();
})
);
action!(
self.actions,
self_.actions,
"save",
clone!(@strong self.sender as sender => move |_, _| {
send!(sender, AddAccountAction::Save);
clone!(@strong self_.sender as sender => move |_, _| {
send!(sender, AccountAddAction::Save);
})
);
action!(
self.actions,
self_.actions,
"scan-qr",
clone!(@strong self.sender as sender => move |_, _| {
send!(sender, AddAccountAction::ScanQR);
clone!(@strong self_.sender as sender => move |_, _| {
send!(sender, AccountAddAction::ScanQR);
})
);
self.widget.insert_action_group("add", Some(&self.actions));
get_action!(self.actions, @save).set_enabled(false);
self.insert_action_group("add", Some(&self_.actions));
get_action!(self_.actions, @save).set_enabled(false);
}
fn setup_widgets(&self, dialog: Rc<Self>) {
let receiver = self.receiver.borrow_mut().take().unwrap();
fn setup_widgets(&self) {
let self_ = imp::AccountAddDialog::from_instance(self);
let receiver = self_.receiver.borrow_mut().take().unwrap();
receiver.attach(
None,
clone!(@strong dialog => move |action| dialog.do_action(action)),
clone!(@weak self as dialog => move |action| dialog.do_action(action)),
);
get_widget!(self.builder, gtk::EntryCompletion, provider_completion);
provider_completion.set_model(Some(&self.model.completion_model()));
get_widget!(self.builder, gtk::Entry, @token_entry);
provider_completion.connect_match_selected(
clone!(@strong dialog, @strong self.model as model => move |_, store, iter| {
self_
.provider_completion
.get()
.set_model(Some(&self_.model.get().unwrap().completion_model()));
self_.provider_completion.get().connect_match_selected(
clone!(@strong self as dialog, @strong self_.model as model => move |_, store, iter| {
let provider_id = store.get_value(iter, 0). get_some::<i32>().unwrap();
let provider = model.find_by_id(provider_id).unwrap();
let provider = model.get().unwrap().find_by_id(provider_id).unwrap();
dialog.set_provider(provider);
Inhibit(false)
@ -198,21 +300,21 @@ impl AddAccountDialog {
);
}
fn do_action(&self, action: AddAccountAction) -> glib::Continue {
fn do_action(&self, action: AccountAddAction) -> glib::Continue {
match action {
AddAccountAction::SetIcon(file) => {
get_widget!(self.builder, gtk::Image, @image)
.set_from_file(file.get_path().unwrap());
get_widget!(self.builder, gtk::Spinner, @spinner).stop();
get_widget!(self.builder, gtk::Stack, @image_stack).set_visible_child_name("image");
AccountAddAction::SetIcon(file) => {
let self_ = imp::AccountAddDialog::from_instance(self);
self_.image.get().set_from_file(file.get_path().unwrap());
self_.spinner.get().stop();
self_.image_stack.get().set_visible_child_name("image");
}
AddAccountAction::SetProvider(p) => self.set_provider(p),
AddAccountAction::Save => {
AccountAddAction::SetProvider(p) => self.set_provider(p),
AccountAddAction::Save => {
if self.save().is_ok() {
self.widget.close();
self.close();
}
}
AddAccountAction::ScanQR => {
AccountAddAction::ScanQR => {
self.scan_qr();
}
};

View file

@ -1,5 +1,5 @@
mod add;
mod row;
pub use self::add::AddAccountDialog;
pub use self::add::AccountAddDialog;
pub use self::row::AccountRow;

View file

@ -1,84 +1,161 @@
use crate::application::Action;
use crate::models::Account;
use gio::ActionMapExt;
use glib::Sender;
use gtk::prelude::*;
use gio::prelude::*;
use gio::{subclass::ObjectSubclass, ActionMapExt};
use glib::subclass::prelude::*;
use glib::{glib_object_subclass, glib_wrapper};
use gtk::{prelude::*, CompositeTemplate};
use std::cell::RefCell;
mod imp {
use super::*;
use glib::subclass;
use gtk::subclass::prelude::*;
pub struct AccountRow<'a> {
pub widget: gtk::ListBoxRow,
builder: gtk::Builder,
sender: Sender<Action>,
account: &'a Account,
actions: gio::SimpleActionGroup,
}
static PROPERTIES: [subclass::Property; 1] = [subclass::Property("account", |name| {
glib::ParamSpec::object(
name,
"Account",
"The account",
Account::static_type(),
glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
)
})];
impl<'a> AccountRow<'a> {
pub fn new(account: &'a Account, sender: Sender<Action>) -> Self {
let builder = gtk::Builder::from_resource("/com/belmoussaoui/Authenticator/account_row.ui");
get_widget!(builder, gtk::ListBoxRow, account_row);
let actions = gio::SimpleActionGroup::new();
let row = Self {
widget: account_row,
builder,
sender,
account,
actions,
};
row.init();
row
#[derive(CompositeTemplate)]
pub struct AccountRow {
pub account: RefCell<Option<Account>>,
pub actions: gio::SimpleActionGroup,
#[template_child(id = "name_label")]
pub name_label: TemplateChild<gtk::Label>,
#[template_child(id = "name_entry")]
pub name_entry: TemplateChild<gtk::Entry>,
#[template_child(id = "edit_stack")]
pub edit_stack: TemplateChild<gtk::Stack>,
}
fn init(&self) {
self.widget
.insert_action_group("account", Some(&self.actions));
impl ObjectSubclass for AccountRow {
const NAME: &'static str = "AccountRow";
type Type = super::AccountRow;
type ParentType = gtk::ListBoxRow;
type Instance = subclass::simple::InstanceStruct<Self>;
type Class = subclass::simple::ClassStruct<Self>;
get_widget!(self.builder, gtk::Label, name_label);
get_widget!(self.builder, gtk::Entry, name_entry);
glib_object_subclass!();
self.account
.bind_property("name", &name_label, "label")
fn new() -> Self {
let actions = gio::SimpleActionGroup::new();
Self {
actions,
name_label: TemplateChild::default(),
name_entry: TemplateChild::default(),
edit_stack: TemplateChild::default(),
account: RefCell::new(None),
}
}
fn class_init(klass: &mut Self::Class) {
klass.set_template_from_resource("/com/belmoussaoui/Authenticator/account_row.ui");
Self::bind_template_children(klass);
klass.install_properties(&PROPERTIES);
}
}
impl ObjectImpl for AccountRow {
fn set_property(&self, _obj: &Self::Type, id: usize, value: &glib::Value) {
let prop = &PROPERTIES[id];
match *prop {
subclass::Property("account", ..) => {
let account = value
.get()
.expect("type conformity checked by `Object::set_property`");
self.account.replace(account);
}
_ => unimplemented!(),
}
}
fn get_property(&self, _obj: &Self::Type, id: usize) -> glib::Value {
let prop = &PROPERTIES[id];
match *prop {
subclass::Property("account", ..) => self.account.borrow().to_value(),
_ => unimplemented!(),
}
}
fn constructed(&self, obj: &Self::Type) {
obj.init_template();
obj.setup_actions();
obj.setup_widgets();
self.parent_constructed(obj);
}
}
impl WidgetImpl for AccountRow {}
impl ListBoxRowImpl for AccountRow {}
}
glib_wrapper! {
pub struct AccountRow(ObjectSubclass<imp::AccountRow>) @extends gtk::Widget, gtk::ListBoxRow;
}
impl AccountRow {
pub fn new(account: Account) -> Self {
glib::Object::new(Self::static_type(), &[("account", &account)])
.expect("Failed to create AccountRow")
.downcast::<AccountRow>()
.expect("Created object is of wrong type")
}
fn account(&self) -> Account {
let account = self.get_property("account").unwrap();
account.get::<Account>().unwrap().unwrap()
}
fn setup_widgets(&self) {
let self_ = imp::AccountRow::from_instance(self);
self.account()
.bind_property("name", &self_.name_label.get(), "label")
.flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE)
.build();
self.account
.bind_property("name", &name_entry, "text")
self.account()
.bind_property("name", &self_.name_entry.get(), "text")
.flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE)
.build();
action!(
self.actions,
"delete",
clone!(@strong self.sender as sender, @strong self.account as account => move |_, _| {
send!(sender, Action::AccountRemoved(account.clone()));
})
self_.name_entry.get().connect_changed(
clone!(@strong self_.actions as actions => move |entry| {
let name = entry.get_text().unwrap();
get_action!(actions, @save).set_enabled(!name.is_empty());
}),
);
}
fn setup_actions(&self) {
let self_ = imp::AccountRow::from_instance(self);
self.insert_action_group("account", Some(&self_.actions));
action!(self_.actions, "delete", move |_, _| {
//send!(sender, Action::AccountRemoved(account.clone()));
});
let edit_stack = self_.edit_stack.get();
action!(
self.actions,
self_.actions,
"edit",
clone!(@strong self.builder as builder => move |_, _| {
get_widget!(builder, gtk::Stack, edit_stack);
clone!(@weak edit_stack => move |_, _| {
edit_stack.set_visible_child_name("edit");
})
);
let name_entry = self_.name_entry.get();
action!(
self.actions,
self_.actions,
"save",
clone!(@weak name_entry,
@strong self.account as account,
@strong self.builder as builder => move |_, _| {
clone!(@weak edit_stack, @weak name_entry => move |_, _| {
let new_name = name_entry.get_text().unwrap();
account.set_name(&new_name);
get_widget!(builder, gtk::Stack, edit_stack);
edit_stack.set_visible_child_name("display");
})
);
name_entry.connect_changed(clone!(@strong self.actions as actions => move |entry| {
let name = entry.get_text().unwrap();
get_action!(actions, @save).set_enabled(!name.is_empty());
}));
}
}

View file

@ -3,7 +3,7 @@ mod preferences;
mod providers;
mod window;
pub use self::accounts::AddAccountDialog;
pub use self::accounts::AccountAddDialog;
pub use self::preferences::PreferencesWindow;
pub use self::providers::ProvidersList;
pub use self::window::{View, Window, WindowPrivate};
pub use self::window::{View, Window};

View file

@ -1,70 +1,131 @@
use crate::helpers::Keyring;
use gio::{ActionExt, ActionMapExt};
use gtk::prelude::*;
use std::cell::Cell;
use std::rc::Rc;
pub struct PasswordPage {
pub widget: gtk::Box,
builder: gtk::Builder,
actions: gio::SimpleActionGroup,
has_set_password: Cell<bool>,
use gio::prelude::*;
use gio::subclass::ObjectSubclass;
use glib::subclass::prelude::*;
use glib::{glib_object_subclass, glib_wrapper};
use gtk::{prelude::*, CompositeTemplate};
mod imp {
use super::*;
use glib::subclass;
use gtk::subclass::prelude::*;
#[derive(CompositeTemplate)]
pub struct PasswordPage {
// actions: gio::SimpleActionGroup,
pub has_set_password: Cell<bool>,
#[template_child(id = "current_password_entry")]
pub current_password_entry: TemplateChild<gtk::PasswordEntry>,
#[template_child(id = "password_entry")]
pub password_entry: TemplateChild<gtk::PasswordEntry>,
#[template_child(id = "confirm_password_entry")]
pub confirm_password_entry: TemplateChild<gtk::PasswordEntry>,
#[template_child(id = "current_password_row")]
pub current_password_row: TemplateChild<libhandy::ActionRow>,
}
impl ObjectSubclass for PasswordPage {
const NAME: &'static str = "PasswordPage";
type Type = super::PasswordPage;
type ParentType = gtk::Box;
type Instance = subclass::simple::InstanceStruct<Self>;
type Class = subclass::simple::ClassStruct<Self>;
glib_object_subclass!();
fn new() -> Self {
let has_set_password = Keyring::has_set_password().unwrap_or_else(|_| false);
Self {
has_set_password: Cell::new(has_set_password),
current_password_entry: TemplateChild::default(),
password_entry: TemplateChild::default(),
confirm_password_entry: TemplateChild::default(),
current_password_row: TemplateChild::default(),
}
}
fn class_init(klass: &mut Self::Class) {
klass.set_template_from_resource(
"/com/belmoussaoui/Authenticator/preferences_password_page.ui",
);
Self::bind_template_children(klass);
}
}
impl ObjectImpl for PasswordPage {
fn constructed(&self, obj: &Self::Type) {
obj.init_template();
obj.setup_widgets();
obj.setup_actions();
self.parent_constructed(obj);
}
}
impl WidgetImpl for PasswordPage {}
impl BoxImpl for PasswordPage {}
}
glib_wrapper! {
pub struct PasswordPage(ObjectSubclass<imp::PasswordPage>) @extends gtk::Widget, gtk::Box;
}
impl PasswordPage {
pub fn new(actions: gio::SimpleActionGroup) -> Rc<Self> {
let builder = gtk::Builder::from_resource(
"/com/belmoussaoui/Authenticator/preferences_password_page.ui",
);
get_widget!(builder, gtk::Box, password_page);
let has_set_password = Keyring::has_set_password().unwrap_or_else(|_| false);
let page = Rc::new(Self {
widget: password_page,
builder,
actions,
has_set_password: Cell::new(has_set_password),
});
page.init(page.clone());
page
pub fn new() -> Self {
glib::Object::new(Self::static_type(), &[])
.expect("Failed to create PasswordPage")
.downcast::<PasswordPage>()
.expect("Created object is of wrong type")
}
fn validate(&self) {
get_widget!(self.builder, gtk::PasswordEntry, current_password_entry);
get_widget!(self.builder, gtk::PasswordEntry, password_entry);
get_widget!(self.builder, gtk::PasswordEntry, confirm_password_entry);
let self_ = imp::PasswordPage::from_instance(self);
let current_password = current_password_entry.get_text().unwrap();
let password = password_entry.get_text().unwrap();
let password_repeat = confirm_password_entry.get_text().unwrap();
let current_password = self_.current_password_entry.get().get_text().unwrap();
let password = self_.password_entry.get().get_text().unwrap();
let password_repeat = self_.confirm_password_entry.get().get_text().unwrap();
let is_valid = if self.has_set_password.get() {
let is_valid = if self_.has_set_password.get() {
password_repeat == password && current_password != password && password != ""
} else {
password_repeat == password && password != ""
};
get_action!(self.actions, @save_password).set_enabled(is_valid);
// get_action!(self.actions, @save_password).set_enabled(is_valid);
}
fn init(&self, page: Rc<Self>) {
get_widget!(self.builder, gtk::PasswordEntry, current_password_entry);
get_widget!(self.builder, gtk::PasswordEntry, password_entry);
get_widget!(self.builder, gtk::PasswordEntry, confirm_password_entry);
get_widget!(self.builder, libhandy::ActionRow, current_password_row);
fn setup_widgets(&self) {
let self_ = imp::PasswordPage::from_instance(self);
password_entry.connect_changed(clone!(@strong page => move |_| page.validate()));
confirm_password_entry.connect_changed(clone!(@strong page => move |_| page.validate()));
self_
.password_entry
.get()
.connect_changed(clone!(@weak self as page=> move |_| page.validate()));
self_
.confirm_password_entry
.get()
.connect_changed(clone!(@weak self as page => move |_| page.validate()));
if !self.has_set_password.get() {
current_password_row.hide();
if !self_.has_set_password.get() {
self_.current_password_row.get().hide();
} else {
current_password_entry
.connect_changed(clone!(@strong page => move |_| page.validate()));
self_
.current_password_entry
.get()
.connect_changed(clone!(@weak self as page => move |_| page.validate()));
}
}
action!(
fn setup_actions(&self) {
let self_ = imp::PasswordPage::from_instance(self);
/*action!(
self.actions,
"save_password",
clone!(@strong page => move |_, _| {
@ -79,28 +140,27 @@ impl PasswordPage {
page.reset();
})
);
get_action!(self.actions, @save_password).set_enabled(false);
get_action!(self.actions, @reset_password).set_enabled(self.has_set_password.get());
get_action!(self.actions, @reset_password).set_enabled(self.has_set_password.get());*/
}
fn reset(&self) {
if Keyring::reset_password().is_ok() {
get_action!(self.actions, @close_page).activate(None);
get_action!(self.actions, @save_password).set_enabled(false);
get_action!(self.actions, @reset_password).set_enabled(false);
get_widget!(self.builder, libhandy::ActionRow, @current_password_row).hide();
self.has_set_password.set(false);
let self_ = imp::PasswordPage::from_instance(self);
// get_action!(self.actions, @close_page).activate(None);
// get_action!(self.actions, @save_password).set_enabled(false);
// get_action!(self.actions, @reset_password).set_enabled(false);
self_.current_password_row.get().hide();
self_.has_set_password.set(false);
}
}
fn save(&self) {
get_widget!(self.builder, gtk::PasswordEntry, current_password_entry);
get_widget!(self.builder, gtk::PasswordEntry, password_entry);
get_widget!(self.builder, gtk::PasswordEntry, confirm_password_entry);
let self_ = imp::PasswordPage::from_instance(self);
let current_password = current_password_entry.get_text().unwrap();
let password = password_entry.get_text().unwrap();
let current_password = self_.current_password_entry.get().get_text().unwrap();
let password = self_.password_entry.get().get_text().unwrap();
if Keyring::has_set_password().unwrap_or(false) {
if !Keyring::is_current_password(&current_password).unwrap_or(false) {
@ -109,14 +169,14 @@ impl PasswordPage {
}
if Keyring::set_password(&password).is_ok() {
get_widget!(self.builder, libhandy::ActionRow, @current_password_row).show();
current_password_entry.set_text("");
password_entry.set_text("");
confirm_password_entry.set_text("");
get_action!(self.actions, @save_password).set_enabled(false);
get_action!(self.actions, @reset_password).set_enabled(true);
get_action!(self.actions, @close_page).activate(None);
self.has_set_password.set(true);
self_.current_password_row.get().show();
self_.current_password_entry.get().set_text("");
self_.password_entry.get().set_text("");
self_.confirm_password_entry.get().set_text("");
// get_action!(self.actions, @save_password).set_enabled(false);
// get_action!(self.actions, @reset_password).set_enabled(true);
// get_action!(self.actions, @close_page).activate(None);
self_.has_set_password.set(true);
}
}
}

View file

@ -1,20 +1,17 @@
use super::window::PreferencesAction;
use crate::models::{Algorithm, Provider};
use glib::translate::ToGlib;
use glib::Sender;
use gtk::prelude::*;
use libhandy::ComboRowExt;
use libhandy::{ComboRowExt, EnumListModelExt};
use std::rc::Rc;
pub struct ProviderPage {
pub widget: gtk::Box,
builder: gtk::Builder,
sender: Sender<PreferencesAction>,
algorithms_model: libhandy::EnumListModel,
}
impl ProviderPage {
pub fn new(sender: Sender<PreferencesAction>) -> Rc<Self> {
pub fn new() -> Rc<Self> {
let builder = gtk::Builder::from_resource(
"/com/belmoussaoui/Authenticator/preferences_provider_page.ui",
);
@ -23,7 +20,6 @@ impl ProviderPage {
let page = Rc::new(Self {
widget: provider_page,
sender,
builder,
algorithms_model,
});
@ -49,7 +45,6 @@ impl ProviderPage {
.find_position(provider.algorithm().to_glib()),
);
let p = provider.clone();
/*let sender = self.sender.clone();
spawn!(async move {
if let Ok(file) = p.favicon().await {

View file

@ -1,7 +1,5 @@
use super::window::PreferencesAction;
use crate::models::{Provider, ProvidersModel};
use gio::ListModelExt;
use glib::Sender;
use gtk::prelude::*;
use std::rc::Rc;
@ -9,11 +7,10 @@ pub struct ProvidersPage {
pub widget: libhandy::PreferencesPage,
builder: gtk::Builder,
model: Rc<ProvidersModel>,
sender: Sender<PreferencesAction>,
}
impl ProvidersPage {
pub fn new(model: Rc<ProvidersModel>, sender: Sender<PreferencesAction>) -> Rc<Self> {
pub fn new(model: Rc<ProvidersModel>) -> Rc<Self> {
let builder = gtk::Builder::from_resource(
"/com/belmoussaoui/Authenticator/preferences_providers_page.ui",
);
@ -23,7 +20,6 @@ impl ProvidersPage {
widget: providers_page,
builder,
model,
sender,
});
page.init();
page
@ -44,13 +40,15 @@ impl ProvidersPage {
let selection_model = gtk::NoSelection::new(Some(&self.model.model));
providers_list.set_model(Some(&selection_model));
providers_list.connect_activate(
clone!(@strong self.sender as sender => move|listview, pos|{
let model = listview.get_model().unwrap();
let provider = model.get_object(pos).unwrap().downcast::<Provider>().unwrap();
send!(sender, PreferencesAction::EditProvider(provider));
}),
);
providers_list.connect_activate(move |listview, pos| {
let model = listview.get_model().unwrap();
let provider = model
.get_object(pos)
.unwrap()
.downcast::<Provider>()
.unwrap();
// send!(sender, PreferencesAction::EditProvider(provider));
});
}
}

View file

@ -1,118 +1,131 @@
use super::password_page::PasswordPage;
use super::provider_page::ProviderPage;
use super::providers_page::ProvidersPage;
use crate::config;
use crate::models::{Provider, ProvidersModel};
use gio::prelude::*;
use gio::ActionMapExt;
use gio::SettingsExt;
use glib::{Receiver, Sender};
use gtk::prelude::*;
use gio::{subclass::ObjectSubclass, SettingsExt};
use glib::subclass::prelude::*;
use glib::{glib_object_subclass, glib_wrapper};
use gtk::{prelude::*, CompositeTemplate};
use libhandy::PreferencesWindowExt;
use std::cell::RefCell;
use std::rc::Rc;
pub enum PreferencesAction {
EditProvider(Provider),
mod imp {
use super::*;
use glib::subclass;
use gtk::subclass::prelude::*;
use libhandy::subclass::{
preferences_window::PreferencesWindowImpl, window::WindowImpl as HdyWindowImpl,
};
#[derive(CompositeTemplate)]
pub struct PreferencesWindow {
pub settings: gio::Settings,
pub actions: gio::SimpleActionGroup,
pub password_page: PasswordPage,
#[template_child(id = "auto_lock_switch")]
pub auto_lock: TemplateChild<gtk::Switch>,
#[template_child(id = "dark_theme_switch")]
pub dark_theme: TemplateChild<gtk::Switch>,
#[template_child(id = "lock_timeout_spin_btn")]
pub lock_timeout: TemplateChild<gtk::SpinButton>,
}
impl ObjectSubclass for PreferencesWindow {
const NAME: &'static str = "PreferencesWindow";
type Type = super::PreferencesWindow;
type ParentType = libhandy::PreferencesWindow;
type Instance = subclass::simple::InstanceStruct<Self>;
type Class = subclass::simple::ClassStruct<Self>;
glib_object_subclass!();
fn new() -> Self {
let settings = gio::Settings::new(config::APP_ID);
let actions = gio::SimpleActionGroup::new();
Self {
settings,
actions,
password_page: PasswordPage::new(),
auto_lock: TemplateChild::default(),
dark_theme: TemplateChild::default(),
lock_timeout: TemplateChild::default(),
}
}
fn class_init(klass: &mut Self::Class) {
klass.set_template_from_resource("/com/belmoussaoui/Authenticator/preferences.ui");
Self::bind_template_children(klass);
}
}
impl ObjectImpl for PreferencesWindow {
fn constructed(&self, obj: &Self::Type) {
obj.init_template();
obj.setup_widgets();
obj.setup_actions();
self.parent_constructed(obj);
}
}
impl WidgetImpl for PreferencesWindow {}
impl WindowImpl for PreferencesWindow {}
impl HdyWindowImpl for PreferencesWindow {}
impl PreferencesWindowImpl for PreferencesWindow {}
}
pub struct PreferencesWindow {
pub widget: libhandy::PreferencesWindow,
builder: gtk::Builder,
settings: gio::Settings,
providers_model: Rc<ProvidersModel>,
password_page: Rc<PasswordPage>,
providers_page: Rc<ProvidersPage>,
provider_page: Rc<ProviderPage>,
actions: gio::SimpleActionGroup,
sender: Sender<PreferencesAction>,
receiver: RefCell<Option<Receiver<PreferencesAction>>>,
glib_wrapper! {
pub struct PreferencesWindow(ObjectSubclass<imp::PreferencesWindow>)
@extends gtk::Widget, gtk::Window, libhandy::Window, libhandy::PreferencesWindow;
}
impl PreferencesWindow {
pub fn new(providers_model: Rc<ProvidersModel>) -> Rc<Self> {
let builder = gtk::Builder::from_resource("/com/belmoussaoui/Authenticator/preferences.ui");
get_widget!(builder, libhandy::PreferencesWindow, preferences_window);
let settings = gio::Settings::new(config::APP_ID);
let actions = gio::SimpleActionGroup::new();
let (sender, r) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
let receiver = RefCell::new(Some(r));
let preferences = Rc::new(Self {
widget: preferences_window,
builder,
settings,
providers_page: ProvidersPage::new(providers_model.clone(), sender.clone()),
provider_page: ProviderPage::new(sender.clone()),
password_page: PasswordPage::new(actions.clone()),
providers_model,
actions,
sender,
receiver,
});
preferences.init(preferences.clone());
preferences.setup_actions();
preferences
pub fn new() -> Self {
glib::Object::new(Self::static_type(), &[])
.expect("Failed to create PreferencesWindow")
.downcast::<PreferencesWindow>()
.expect("Created object is of wrong type")
}
fn init(&self, preferences: Rc<Self>) {
get_widget!(self.builder, gtk::Switch, dark_theme_switch);
self.settings.bind(
fn setup_widgets(&self) {
let self_ = imp::PreferencesWindow::from_instance(self);
self_.settings.bind(
"dark-theme",
&dark_theme_switch,
&self_.dark_theme.get(),
"active",
gio::SettingsBindFlags::DEFAULT,
);
get_widget!(self.builder, gtk::Switch, auto_lock_switch);
self.settings.bind(
self_.settings.bind(
"auto-lock",
&auto_lock_switch,
&self_.auto_lock.get(),
"active",
gio::SettingsBindFlags::DEFAULT,
);
get_widget!(self.builder, gtk::SpinButton, lock_timeout_spin_btn);
auto_lock_switch
.bind_property("active", &lock_timeout_spin_btn, "sensitive")
self_
.auto_lock
.get()
.bind_property("active", &self_.lock_timeout.get(), "sensitive")
.flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE)
.build();
self.widget.add(&self.providers_page.widget);
let receiver = self.receiver.borrow_mut().take().unwrap();
receiver.attach(None, move |action| preferences.do_action(action));
}
fn do_action(&self, action: PreferencesAction) -> glib::Continue {
match action {
PreferencesAction::EditProvider(provider) => {
self.provider_page.set_provider(provider);
self.widget.present_subpage(&self.provider_page.widget)
}
}
glib::Continue(true)
}
fn setup_actions(&self) {
let self_ = imp::PreferencesWindow::from_instance(self);
action!(
self.actions,
self_.actions,
"show_password_page",
clone!(@strong self.builder as builder,
@strong self.password_page.widget as password_page,
@strong self.widget as widget => move |_, _| {
widget.present_subpage(&password_page);
clone!(@weak self as win, @weak self_.password_page as password_page => move |_, _| {
win.present_subpage(&password_page);
})
);
action!(
self.actions,
self_.actions,
"close_page",
clone!(@strong self.builder as builder,
@strong self.widget as widget => move |_, _| {
widget.close_subpage();
clone!(@weak self as win => move |_, _| {
win.close_subpage();
})
);
self.widget
.insert_action_group("preferences", Some(&self.actions));
self.insert_action_group("preferences", Some(&self_.actions));
}
}

View file

@ -1,69 +1,107 @@
use crate::application::Action;
use crate::models::{Provider, ProvidersModel};
use crate::widgets::providers::ProviderRow;
use gio::ListModelExt;
use glib::Sender;
use gtk::prelude::*;
use gio::{subclass::ObjectSubclass, ListModelExt};
use glib::subclass::prelude::*;
use glib::{glib_object_subclass, glib_wrapper};
use gtk::{prelude::*, CompositeTemplate};
use std::rc::Rc;
pub struct ProvidersList {
pub widget: gtk::Box,
builder: gtk::Builder,
pub filter_model: gtk::FilterListModel,
mod imp {
use super::*;
use glib::subclass;
use gtk::subclass::prelude::*;
#[derive(Debug, CompositeTemplate)]
pub struct ProvidersList {
#[template_child(id = "providers_list")]
pub providers_list: TemplateChild<gtk::ListBox>,
pub filter_model: gtk::FilterListModel,
}
impl ObjectSubclass for ProvidersList {
const NAME: &'static str = "ProvidersList";
type Type = super::ProvidersList;
type ParentType = gtk::Box;
type Instance = subclass::simple::InstanceStruct<Self>;
type Class = subclass::simple::ClassStruct<Self>;
glib_object_subclass!();
fn new() -> Self {
let filter_model =
gtk::FilterListModel::new(gtk::NONE_FILTER_LIST_MODEL, gtk::NONE_FILTER);
Self {
providers_list: TemplateChild::default(),
filter_model,
}
}
fn class_init(klass: &mut Self::Class) {
klass.set_template_from_resource("/com/belmoussaoui/Authenticator/providers_list.ui");
Self::bind_template_children(klass);
}
}
impl ObjectImpl for ProvidersList {
fn constructed(&self, obj: &Self::Type) {
obj.init_template();
obj.setup_widgets();
self.parent_constructed(obj);
}
}
impl WidgetImpl for ProvidersList {}
impl BoxImpl for ProvidersList {}
}
glib_wrapper! {
pub struct ProvidersList(ObjectSubclass<imp::ProvidersList>) @extends gtk::Widget, gtk::Box;
}
impl ProvidersList {
pub fn new() -> Self {
let builder =
gtk::Builder::from_resource("/com/belmoussaoui/Authenticator/providers_list.ui");
get_widget!(builder, gtk::Box, providers_box);
let filter_model = gtk::FilterListModel::new(gtk::NONE_FILTER_LIST_MODEL, gtk::NONE_FILTER);
let list = Self {
widget: providers_box,
builder,
filter_model,
};
list
glib::Object::new(Self::static_type(), &[])
.expect("Failed to create ProvidersList")
.downcast::<ProvidersList>()
.expect("Created object is of wrong type")
}
pub fn set_model(&self, model: Rc<ProvidersModel>) {
let self_ = imp::ProvidersList::from_instance(self);
let accounts_filter = gtk::CustomFilter::new(Some(Box::new(|object| {
let provider = object.downcast_ref::<Provider>().unwrap();
provider.has_accounts()
})));
self.filter_model.set_filter(Some(&accounts_filter));
self.filter_model.set_model(Some(&model.model));
self_.filter_model.set_filter(Some(&accounts_filter));
self_.filter_model.set_model(Some(&model.model));
}
pub fn refilter(&self) {
if let Some(filter) = self.filter_model.get_filter() {
let self_ = imp::ProvidersList::from_instance(self);
if let Some(filter) = self_.filter_model.get_filter() {
filter.changed(gtk::FilterChange::Different);
}
}
pub fn search(&self, text: String) {
get_widget!(self.builder, gtk::ListBox, providers_list);
let self_ = imp::ProvidersList::from_instance(self);
let accounts_filter = gtk::CustomFilter::new(Some(Box::new(move |object| {
let provider = object.downcast_ref::<Provider>().unwrap();
provider.search_accounts(text.clone());
provider.accounts().get_n_items() != 0
})));
self.filter_model.set_filter(Some(&accounts_filter));
self_.filter_model.set_filter(Some(&accounts_filter));
}
pub fn init(&self, sender: Sender<Action>) {
get_widget!(self.builder, gtk::ListBox, providers_list);
fn setup_widgets(&self) {
let self_ = imp::ProvidersList::from_instance(self);
providers_list.bind_model(
Some(&self.filter_model),
Some(Box::new(clone!(@strong sender => move |obj| {
let provider = obj.downcast_ref::<Provider>().unwrap();
let row = ProviderRow::new(provider, sender.clone());
row.widget.upcast::<gtk::Widget>()
}))),
self_.providers_list.get().bind_model(
Some(&self_.filter_model),
Some(Box::new(move |obj| {
let provider = obj.clone().downcast::<Provider>().unwrap();
ProviderRow::new(provider).upcast::<gtk::Widget>()
})),
);
}
}

View file

@ -1,52 +1,123 @@
use crate::application::Action;
use crate::models::{Account, Provider};
use crate::widgets::accounts::AccountRow;
use glib::Sender;
use gtk::prelude::*;
use gio::prelude::*;
use gio::subclass::ObjectSubclass;
use glib::subclass::prelude::*;
use glib::{glib_object_subclass, glib_wrapper};
use gtk::{prelude::*, CompositeTemplate};
use std::cell::RefCell;
mod imp {
use super::*;
use glib::subclass;
use gtk::subclass::prelude::*;
pub struct ProviderRow<'a> {
pub widget: gtk::ListBoxRow,
provider: &'a Provider,
builder: gtk::Builder,
sender: Sender<Action>,
}
static PROPERTIES: [subclass::Property; 1] = [subclass::Property("provider", |name| {
glib::ParamSpec::object(
name,
"Provider",
"The accounts provider",
Provider::static_type(),
glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
)
})];
impl<'a> ProviderRow<'a> {
pub fn new(provider: &'a Provider, sender: Sender<Action>) -> Self {
let builder =
gtk::Builder::from_resource("/com/belmoussaoui/Authenticator/provider_row.ui");
get_widget!(builder, gtk::ListBoxRow, provider_row);
let row = Self {
widget: provider_row,
builder,
sender,
provider,
};
row.init();
row
#[derive(CompositeTemplate)]
pub struct ProviderRow {
pub provider: RefCell<Option<Provider>>,
#[template_child(id = "name_label")]
pub name_label: TemplateChild<gtk::Label>,
#[template_child(id = "accounts_list")]
pub accounts_list: TemplateChild<gtk::ListBox>,
}
fn init(&self) {
get_widget!(self.builder, gtk::Label, name);
impl ObjectSubclass for ProviderRow {
const NAME: &'static str = "ProviderRow";
type Type = super::ProviderRow;
type ParentType = gtk::ListBoxRow;
type Instance = subclass::simple::InstanceStruct<Self>;
type Class = subclass::simple::ClassStruct<Self>;
self.provider
.bind_property("name", &name, "label")
glib_object_subclass!();
fn new() -> Self {
Self {
name_label: TemplateChild::default(),
accounts_list: TemplateChild::default(),
provider: RefCell::new(None),
}
}
fn class_init(klass: &mut Self::Class) {
klass.set_template_from_resource("/com/belmoussaoui/Authenticator/provider_row.ui");
Self::bind_template_children(klass);
klass.install_properties(&PROPERTIES);
}
}
impl ObjectImpl for ProviderRow {
fn set_property(&self, _obj: &Self::Type, id: usize, value: &glib::Value) {
let prop = &PROPERTIES[id];
match *prop {
subclass::Property("provider", ..) => {
let provider = value
.get()
.expect("type conformity checked by `Object::set_property`");
self.provider.replace(provider);
}
_ => unimplemented!(),
}
}
fn get_property(&self, _obj: &Self::Type, id: usize) -> glib::Value {
let prop = &PROPERTIES[id];
match *prop {
subclass::Property("provider", ..) => self.provider.borrow().to_value(),
_ => unimplemented!(),
}
}
fn constructed(&self, obj: &Self::Type) {
obj.init_template();
obj.setup_widgets();
self.parent_constructed(obj);
}
}
impl WidgetImpl for ProviderRow {}
impl ListBoxRowImpl for ProviderRow {}
}
glib_wrapper! {
pub struct ProviderRow(ObjectSubclass<imp::ProviderRow>) @extends gtk::Widget, gtk::ListBoxRow;
}
impl ProviderRow {
pub fn new(provider: Provider) -> Self {
glib::Object::new(Self::static_type(), &[("provider", &provider)])
.expect("Failed to create ProviderRow")
.downcast::<ProviderRow>()
.expect("Created object is of wrong type")
}
fn provider(&self) -> Provider {
let provider = self.get_property("provider").unwrap();
provider.get::<Provider>().unwrap().unwrap()
}
fn setup_widgets(&self) {
let self_ = imp::ProviderRow::from_instance(self);
self.provider()
.bind_property("name", &self_.name_label.get(), "label")
.flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE)
.build();
get_widget!(self.builder, gtk::ListBox, accounts_list);
accounts_list.bind_model(
Some(self.provider.accounts()),
Some(Box::new(
clone!(@strong self.sender as sender => move |account: &glib::Object| {
let account: &Account = account
.downcast_ref::<Account>()
.unwrap();
let row = AccountRow::new(account, sender.clone());
row.widget.upcast::<gtk::Widget>()
}),
)),
self_.accounts_list.get().bind_model(
Some(self.provider().accounts()),
Some(Box::new(move |account: &glib::Object| {
let account = account.clone().downcast::<Account>().unwrap();
AccountRow::new(account).upcast::<gtk::Widget>()
})),
);
}
}

View file

@ -5,11 +5,11 @@ use crate::models::ProvidersModel;
use crate::widgets::providers::ProvidersList;
use crate::window_state;
use gio::prelude::*;
use glib::subclass;
use gio::subclass::ObjectSubclass;
use glib::subclass::prelude::*;
use glib::{glib_object_subclass, glib_wrapper};
use glib::{signal::Inhibit, Sender};
use gtk::prelude::*;
use gtk::subclass::prelude::{WidgetImpl, WindowImpl};
use gtk::{prelude::*, CompositeTemplate};
use libhandy::prelude::*;
use std::rc::Rc;
@ -19,45 +19,75 @@ pub enum View {
Accounts,
}
pub struct WindowPrivate {
builder: gtk::Builder,
settings: gio::Settings,
pub providers: Rc<ProvidersList>,
}
mod imp {
use super::*;
use glib::subclass;
use gtk::subclass::prelude::*;
use libhandy::subclass::application_window::ApplicationWindowImpl as HdyApplicationWindowImpl;
impl ObjectSubclass for WindowPrivate {
const NAME: &'static str = "Window";
type Type = super::Window;
type ParentType = libhandy::ApplicationWindow;
type Instance = subclass::simple::InstanceStruct<Self>;
type Class = subclass::simple::ClassStruct<Self>;
#[derive(Debug, CompositeTemplate)]
pub struct Window {
pub settings: gio::Settings,
pub providers: ProvidersList,
#[template_child(id = "search_entry")]
pub search_entry: TemplateChild<gtk::SearchEntry>,
#[template_child(id = "deck")]
pub deck: TemplateChild<libhandy::Leaflet>,
#[template_child(id = "container")]
pub container: TemplateChild<gtk::Box>,
#[template_child(id = "search_bar")]
pub search_bar: TemplateChild<gtk::SearchBar>,
#[template_child(id = "search_btn")]
pub search_btn: TemplateChild<gtk::ToggleButton>,
#[template_child(id = "password_entry")]
pub password_entry: TemplateChild<gtk::PasswordEntry>,
}
glib_object_subclass!();
impl ObjectSubclass for Window {
const NAME: &'static str = "Window";
type Type = super::Window;
type ParentType = libhandy::ApplicationWindow;
type Instance = subclass::simple::InstanceStruct<Self>;
type Class = subclass::simple::ClassStruct<Self>;
fn new() -> Self {
let builder = gtk::Builder::from_resource("/com/belmoussaoui/Authenticator/window.ui");
let settings = gio::Settings::new(APP_ID);
let providers = Rc::new(ProvidersList::new());
Self {
builder,
settings,
providers,
glib_object_subclass!();
fn new() -> Self {
let settings = gio::Settings::new(APP_ID);
let providers = ProvidersList::new();
Self {
settings,
providers,
search_entry: TemplateChild::default(),
deck: TemplateChild::default(),
container: TemplateChild::default(),
search_bar: TemplateChild::default(),
search_btn: TemplateChild::default(),
password_entry: TemplateChild::default(),
}
}
fn class_init(klass: &mut Self::Class) {
klass.set_template_from_resource("/com/belmoussaoui/Authenticator/window.ui");
Self::bind_template_children(klass);
}
}
impl ObjectImpl for Window {
fn constructed(&self, obj: &Self::Type) {
obj.init_template();
self.parent_constructed(obj);
}
}
impl WidgetImpl for Window {}
impl WindowImpl for Window {}
impl ApplicationWindowImpl for Window {}
impl HdyApplicationWindowImpl for Window {}
}
impl ObjectImpl for WindowPrivate {}
impl WidgetImpl for WindowPrivate {}
impl WindowImpl for WindowPrivate {}
impl gtk::subclass::prelude::ApplicationWindowImpl for WindowPrivate {}
impl libhandy::subclass::prelude::ApplicationWindowImpl for WindowPrivate {}
glib_wrapper! {
pub struct Window(ObjectSubclass<WindowPrivate>)
pub struct Window(ObjectSubclass<imp::Window>)
@extends gtk::Widget, gtk::Window, gtk::ApplicationWindow, libhandy::ApplicationWindow, gio::ActionMap;
}
@ -72,7 +102,7 @@ impl Window {
if PROFILE == "Devel" {
window.get_style_context().add_class("devel");
}
window.init(model, sender.clone());
window.init(model);
window.setup_actions(app, sender.clone());
window.set_view(View::Login); // Start by default in an empty state
window.setup_signals(app, sender);
@ -80,22 +110,25 @@ impl Window {
}
pub fn set_view(&self, view: View) {
let self_ = WindowPrivate::from_instance(self);
get_widget!(self_.builder, libhandy::Leaflet, deck);
let self_ = imp::Window::from_instance(self);
match view {
View::Login => {
deck.set_visible_child_name("login");
self_.deck.get().set_visible_child_name("login");
}
View::Accounts => {
deck.set_visible_child_name("accounts");
self_.deck.get().set_visible_child_name("accounts");
}
}
}
fn init(&self, model: Rc<ProvidersModel>, sender: Sender<Action>) {
let self_ = WindowPrivate::from_instance(self);
pub fn providers(&self) -> ProvidersList {
let self_ = imp::Window::from_instance(self);
self_.providers.clone()
}
fn init(&self, model: Rc<ProvidersModel>) {
let self_ = imp::Window::from_instance(self);
self_.providers.set_model(model.clone());
self_.providers.init(sender.clone());
// load latest window state
window_state::load(&self, &self_.settings);
// save window state on delete event
@ -110,31 +143,30 @@ impl Window {
get_widget!(builder, gtk::ShortcutsWindow, shortcuts);
self.set_help_overlay(Some(&shortcuts));
get_widget!(self_.builder, gtk::Box, container);
container.append(&self_.providers.widget);
self_.container.get().append(&self_.providers);
get_widget!(self_.builder, gtk::SearchBar, search_bar);
get_widget!(self_.builder, gtk::ToggleButton, search_btn);
search_btn
.bind_property("active", &search_bar, "search-mode-enabled")
self_
.search_btn
.get()
.bind_property("active", &self_.search_bar.get(), "search-mode-enabled")
.flags(glib::BindingFlags::BIDIRECTIONAL | glib::BindingFlags::SYNC_CREATE)
.build();
get_widget!(self_.builder, gtk::SearchEntry, search_entry);
search_entry.connect_search_changed(
self_.search_entry.get().connect_search_changed(
clone!(@weak self_.providers as providers => move |entry| {
let text = entry.get_text().unwrap().to_string();
providers.search(text);
}),
);
search_entry.connect_stop_search(clone!(@strong search_btn => move |entry| {
entry.set_text("");
search_btn.set_active(false);
}));
get_widget!(self_.builder, libhandy::Leaflet, deck);
libhandy::ApplicationWindowExt::set_child(self, Some(&deck));
let search_btn = self_.search_btn.get();
self_
.search_entry
.get()
.connect_stop_search(clone!(@weak search_btn => move |entry| {
entry.set_text("");
search_btn.set_active(false);
}));
let gtk_settings = gtk::Settings::get_default().unwrap();
self_.settings.bind(
@ -147,12 +179,12 @@ impl Window {
}
fn setup_actions(&self, app: &Application, sender: Sender<Action>) {
let self_ = WindowPrivate::from_instance(self);
let self_ = imp::Window::from_instance(self);
let search_btn = self_.search_btn.get();
action!(
self,
"search",
clone!(@strong self_.builder as builder => move |_,_| {
get_widget!(builder, gtk::ToggleButton, search_btn);
clone!(@weak search_btn => move |_,_| {
search_btn.set_active(!search_btn.get_active());
})
);
@ -165,11 +197,11 @@ impl Window {
})
);
let password_entry = self_.password_entry.get();
action!(
self,
"unlock",
clone!(@strong sender, @strong self_.builder as builder, @strong app => move |_, _| {
get_widget!(builder, gtk::PasswordEntry, password_entry);
clone!(@strong sender, @weak password_entry, @strong app => move |_, _| {
let password = password_entry.get_text().unwrap();
if Keyring::is_current_password(&password).unwrap() {
password_entry.set_text("");