Commit b88ec436 authored by Bilal Elmoussaoui's avatar Bilal Elmoussaoui

beta 3 release , fixes #12

parent 759e1c5b
......@@ -4,6 +4,8 @@ app_PYTHON = \
add_account.py \
confirmation.py \
account_row.py \
accounts_list.py \
accounts_window.py \
login_window.py \
headerbar.py \
applications_list.py \
......
......@@ -4,6 +4,7 @@ from gi.repository import Gtk, Gdk, GLib
from TwoFactorAuth.models.code import Code
from TwoFactorAuth.models.settings import SettingsReader
from TwoFactorAuth.models.database import Database
from TwoFactorAuth.widgets.confirmation import ConfirmationMessage
from TwoFactorAuth.utils import get_icon
from threading import Thread
from time import sleep
......@@ -19,24 +20,25 @@ class AccountRow(Thread, Gtk.ListBoxRow):
code_generated = True
alive = True
def __init__(self, parent, uid, name, secret_code, logo):
def __init__(self, parent, window, app):
Thread.__init__(self)
Gtk.ListBoxRow.__init__(self)
# Read default values
cfg = SettingsReader()
self.counter_max = cfg.read("refresh-time", "preferences")
self.counter = self.counter_max
self.window = window
self.parent = parent
self.id = uid
self.name = name
self.secret_code = Database.fetch_secret_code(secret_code)
self.id = app[0]
self.name = app[1]
self.secret_code = Database.fetch_secret_code(app[2])
if self.secret_code:
self.code = Code(self.secret_code)
else:
self.code_generated = False
logging.error(
"Could not read the secret code from, the keyring keys were reset manually")
self.logo = logo
self.logo = app[3]
# Create needed widgets
self.code_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
self.revealer = Gtk.Revealer()
......@@ -47,6 +49,7 @@ class AccountRow(Thread, Gtk.ListBoxRow):
# Create the list row
self.create_row()
self.start()
self.window.connect("key-press-event", self.__on_key_press)
GLib.timeout_add_seconds(1, self.refresh_listbox)
def get_id(self):
......@@ -166,12 +169,11 @@ class AccountRow(Thread, Gtk.ListBoxRow):
# Remove button
remove_event = Gtk.EventBox()
remove_button = Gtk.Image(xalign=0)
remove_button.set_from_icon_name(
"user-trash-symbolic", Gtk.IconSize.SMALL_TOOLBAR)
remove_button.set_from_icon_name("user-trash-symbolic",
Gtk.IconSize.SMALL_TOOLBAR)
remove_button.set_tooltip_text(_("Remove the account"))
remove_event.add(remove_button)
remove_event.connect("button-press-event",
self.parent.remove_account)
remove_event.connect("button-press-event", self.remove)
h_box.pack_end(remove_event, False, True, 6)
self.timer_label.set_label(_("Expires in %s seconds") % self.counter)
......@@ -195,7 +197,7 @@ class AccountRow(Thread, Gtk.ListBoxRow):
self.toggle_code_box()
def run(self):
while self.code_generated and self.parent.app.alive and self.alive:
while self.code_generated and self.window.app.alive and self.alive:
self.counter -= 1
if self.counter == 0:
self.counter = self.counter_max
......@@ -207,8 +209,7 @@ class AccountRow(Thread, Gtk.ListBoxRow):
sleep(1)
def refresh_listbox(self):
self.parent.list_box.hide()
self.parent.list_box.show_all()
self.window.accounts_list.refresh()
return self.code_generated
def regenerate_code(self):
......@@ -229,5 +230,38 @@ class AccountRow(Thread, Gtk.ListBoxRow):
label.set_text(_("Couldn't generate the secret code"))
self.code_generated = False
def __on_key_press(self, widget, event):
keyname = Gdk.keyval_name(event.keyval).lower()
if not self.window.is_locked():
if self.parent.get_selected_row_id() == self.get_id():
is_search_bar = self.window.search_bar.is_visible()
if keyname == "delete" and not is_search_bar:
self.remove()
return True
if keyname == "return":
self.toggle_code_box()
return True
if event.state & Gdk.ModifierType.CONTROL_MASK:
if keyname == 'c':
self.copy_code()
return True
return False
def remove(self, *args):
"""
Remove an account
"""
message = _("Do you really want to remove this account?")
confirmation = ConfirmationMessage(self.window, message)
confirmation.show()
if confirmation.get_confirmation():
self.kill()
self.window.accounts_list.remove(self)
self.window.app.db.remove_by_id(self.get_id())
confirmation.destroy()
self.window.refresh_window()
def update_timer_label(self):
self.timer_label.set_label(_("Expires in %s seconds") % self.counter)
from gi import require_version
require_version("Gtk", "3.0")
from gi.repository import Gtk, Gio, Gdk, GObject, GLib
from TwoFactorAuth.widgets.confirmation import ConfirmationMessage
from TwoFactorAuth.widgets.account_row import AccountRow
import logging
from gettext import gettext as _
from hashlib import sha256
class AccountsList(Gtk.ListBox):
scrolled_win = None
selected_count = 0
def __init__(self, application, window):
self.app = application
self.window = window
self.generate()
self.window.connect("key-press-event", self.on_key_press)
def generate(self):
Gtk.ListBox.__init__(self)
# Create a ScrolledWindow for accounts
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
self.get_style_context().add_class("applications-list")
self.set_adjustment()
self.set_selection_mode(Gtk.SelectionMode.SINGLE)
box.pack_start(self, True, True, 0)
self.scrolled_win = Gtk.ScrolledWindow()
self.scrolled_win.add_with_viewport(box)
apps = self.app.db.fetch_apps()
count = len(apps)
for app in apps:
self.add(AccountRow(self, self.window, app))
if count != 0:
self.select_row(self.get_row_at_index(0))
self.show_all()
def on_key_press(self, app, key_event):
"""
Keyboard Listener handling
"""
keyname = Gdk.keyval_name(key_event.keyval).lower()
if not self.window.is_locked():
if not self.window.no_account_box.is_visible():
if keyname == "up" or keyname == "down":
count = self.app.db.count()
dx = -1 if keyname == "up" else 1
selected_row = self.get_selected_row()
if selected_row is not None:
index = selected_row.get_index()
index = (index + dx)%count
self.select_row(self.get_row_at_index(index))
return True
return False
def toggle_select_mode(self):
pass_enabled = self.app.cfg.read("state", "login")
is_select_mode = self.window.hb.is_on_select_mode()
if is_select_mode:
self.set_selection_mode(Gtk.SelectionMode.MULTIPLE)
else:
self.set_selection_mode(Gtk.SelectionMode.SINGLE)
self.select_row(self.get_row_at_index(0))
for row in self.get_children():
checkbox = row.get_checkbox()
code_label = row.get_code_label()
visible = checkbox.get_visible()
selected = checkbox.get_active()
style_context = code_label.get_style_context()
if is_select_mode:
self.select_account(checkbox)
style_context.add_class("application-secret-code-select-mode")
else:
style_context.remove_class(
"application-secret-code-select-mode")
checkbox.set_visible(not visible)
checkbox.set_no_show_all(visible)
def append(self, app):
"""
Add an element to the ListBox
"""
app[2] = sha256(app[2].encode('utf-8')).hexdigest()
self.add(AccountRow(self, self.window, app))
self.show_all()
def remove_selected(self, *args):
"""
Remove selected accounts
"""
for row in self.get_selected_rows():
checkbox = row.get_checkbox()
if checkbox.get_active():
row.remove()
self.unselect_all()
self.toggle_select_mode()
self.window.refresh_window()
def select_account(self, checkbutton):
"""
Select an account
:param checkbutton:
"""
is_active = checkbutton.get_active()
is_visible = checkbutton.get_visible()
listbox_row = checkbutton.get_parent().get_parent().get_parent()
if is_active:
self.select_row(listbox_row)
if is_visible:
self.selected_count += 1
else:
self.unselect_row(listbox_row)
if is_visible:
self.selected_count -= 1
self.window.hb.remove_button.set_sensitive(self.selected_count > 0)
def get_selected_row_id(self):
selected_row = self.get_selected_row()
if selected_row:
return selected_row.get_id()
else:
return None
def get_scrolled_win(self):
return self.scrolled_win
def toggle(self, visible):
self.set_visible(visible)
self.set_no_show_all(not visible)
def is_visible(self):
return self.get_visible()
def hide(self):
self.toggle(False)
def show(self):
self.toggle(True)
def refresh(self):
self.scrolled_win.hide()
self.scrolled_win.show_all()
from gi import require_version
require_version("Gtk", "3.0")
from gi.repository import Gtk, Gio, Gdk, GObject, GLib
from TwoFactorAuth.widgets.accounts_list import AccountsList
from TwoFactorAuth.widgets.search_bar import SearchBar
import logging
from gettext import gettext as _
from hashlib import sha256
class AccountsWindow(Gtk.Box):
def __init__(self, application, window):
Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL)
self.app = application
self.window = window
self.generate()
def generate(self):
self.generate_accounts_list()
self.generate_search_bar()
self.pack_start(self.search_bar, False, True, 0)
self.pack_start(self.scrolled_win, True, True, 0)
def generate_accounts_list(self):
"""
Generate an account ListBox inside of a ScrolledWindow
"""
self.accounts_list = AccountsList(self.app, self.window)
self.scrolled_win = self.accounts_list.get_scrolled_win()
def generate_search_bar(self):
"""
Generate search bar box and entry
"""
self.search_bar = SearchBar(self.accounts_list, self.window,
self.window.hb.search_button)
def get_accounts_list(self):
return self.accounts_list
def get_search_bar(self):
return self.search_bar
def toggle(self, visible):
self.set_visible(visible)
self.set_no_show_all(not visible)
def is_visible(self):
return self.get_visible()
def hide(self):
self.toggle(False)
def show(self):
self.toggle(True)
......@@ -13,7 +13,7 @@ class AddAccount(Gtk.Window):
def __init__(self, window):
self.parent = window
self.selected_image = None
self.selected_logo = None
self.step = 1
self.logo_image = Gtk.Image(xalign=0)
self.secret_code = Gtk.Entry()
......@@ -109,7 +109,7 @@ class AddAccount(Gtk.Window):
"""
Update image logo
"""
self.selected_image = image
self.selected_logo = image
auth_icon = get_icon(image)
self.logo_image.clear()
self.logo_image.set_from_pixbuf(auth_icon)
......@@ -132,15 +132,13 @@ class AddAccount(Gtk.Window):
"""
Add a new application to the database
"""
name_entry = self.name_entry.get_text()
secret_entry = self.secret_code.get_text()
image_entry = self.selected_image if self.selected_image else "image-missing"
name = self.name_entry.get_text()
secret_code = self.secret_code.get_text()
logo = self.selected_logo if self.selected_logo else "image-missing"
try:
self.parent.app.db.add_account(name_entry, secret_entry,
image_entry)
self.parent.app.db.add_account(name, secret_code, logo)
uid = self.parent.app.db.get_latest_id()
self.parent.append_list_box(
uid, name_entry, secret_entry, image_entry)
self.parent.accounts_list.append([uid, name, secret_code, logo])
self.parent.refresh_window()
self.close_window()
except Exception as e:
......
......@@ -50,9 +50,9 @@ class ApplicationChooserWindow(Gtk.Window):
box_outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
if len(self.logos) > 0:
# Create a ScrolledWindow for installed applications
scrolled_win = Gtk.ScrolledWindow()
scrolled_win.add_with_viewport(box_outer)
self.main_box.pack_start(scrolled_win, True, True, 0)
self.scrolled_win = Gtk.ScrolledWindow()
self.scrolled_win.add_with_viewport(box_outer)
self.main_box.pack_start(self.scrolled_win, True, True, 0)
self.listbox.get_style_context().add_class("applications-list")
self.listbox.set_adjustment()
......@@ -66,6 +66,7 @@ class ApplicationChooserWindow(Gtk.Window):
app_logo = get_icon(img_path)
self.listbox.add(ApplicationRow(app_name, app_logo))
i += 1
self.listbox.select_row(self.listbox.get_row_at_index(0))
def generate_header_bar(self):
"""
......@@ -103,27 +104,32 @@ class ApplicationChooserWindow(Gtk.Window):
"""
Generate the search bar
"""
self.search_bar = SearchBar(self.listbox)
self.search_button.connect("toggled", self.search_bar.toggle)
self.search_bar = SearchBar(self.listbox, self, self.search_button)
self.main_box.pack_start(self.search_bar, False, True, 0)
def on_key_press(self, label, key_event):
"""
Keyboard listener handling
"""
key_pressed = Gdk.keyval_name(key_event.keyval).lower()
if key_pressed == "escape":
if self.search_bar.is_visible():
self.search_bar.toggle()
else:
keyname = Gdk.keyval_name(key_event.keyval).lower()
if keyname == "escape":
if not self.search_bar.is_visible():
self.close_window()
elif key_pressed == "f":
if key_event.state == Gdk.ModifierType.CONTROL_MASK:
self.search_button.set_active(
not self.search_button.get_active())
elif key_pressed == "return":
return True
if keyname == "up" or keyname == "down":
dx = -1 if keyname == "up" else 1
index = self.listbox.get_selected_row().get_index()
index = (index + dx)%len(self.logos)
selected_row = self.listbox.get_row_at_index(index)
self.listbox.select_row(selected_row)
return True
if keyname == "return":
self.select_logo()
return True
return False
def select_logo(self, *args):
"""
......
......@@ -141,7 +141,7 @@ class HeaderBar(Gtk.HeaderBar):
def toggle_settings_button(self, visible):
if not is_gnome():
self.settings_button.set_visible(visible)
self.lock_bsettings_buttonutton.set_no_show_all(not visible)
self.settings_button.set_no_show_all(not visible)
def toggle_lock_button(self, visible):
self.lock_button.set_visible(visible)
......
from gi import require_version
require_version("Gtk", "3.0")
from gi.repository import Gtk, Gio, Gdk, GObject, GLib
from gi.repository import Gtk, Gdk
import logging
from hashlib import sha256
from gettext import gettext as _
......@@ -9,14 +9,16 @@ class LoginWindow(Gtk.Box):
password_entry = None
unlock_button = None
def __init__(self, application):
self.app = application
def __init__(self, application, window):
Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL)
self.app = application
self.window = window
self.password_entry = Gtk.Entry()
self.unlock_button = Gtk.Button()
self.generate_box()
self.generate()
self.window.connect("key-press-event", self.__on_key_press)
def generate_box(self):
def generate(self):
password_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.password_entry.set_visibility(False)
......@@ -45,15 +47,32 @@ class LoginWindow(Gtk.Box):
self.password_entry.set_icon_from_icon_name(
Gtk.EntryIconPosition.SECONDARY, "dialog-error-symbolic")
def toggle_lock(self):
def __on_key_press(self, widget, event):
keyname = Gdk.keyval_name(event.keyval).lower()
if self.window.is_locked():
if keyname == "return":
self.on_unlock()
return True
else:
pass_enabled = self.app.cfg.read("state", "login")
if keyname == "l" and pass_enabled:
if event.state & Gdk.ModifierType.CONTROL_MASK:
self.toggle_lock()
return True
return False
def toggle_lock(self, *args):
"""
Lock/unlock the application
"""
self.app.locked = not self.app.locked
if self.app.locked:
self.focus()
self.app.refresh_menu()
self.app.win.refresh_window()
pass_enabled = self.app.cfg.read("state", "login")
if pass_enabled:
self.app.locked = not self.app.locked
if self.app.locked:
self.focus()
self.app.refresh_menu()
self.app.win.refresh_window()
def toggle(self, visible):
self.set_visible(visible)
......
......@@ -9,9 +9,9 @@ class NoAccountWindow(Gtk.Box):
def __init__(self):
Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL,
spacing=6)
self.generate_box()
self.generate()
def generate_box(self):
def generate(self):
logo_image = Gtk.Image()
logo_image.set_from_icon_name("dialog-information-symbolic",
Gtk.IconSize.DIALOG)
......@@ -25,6 +25,9 @@ class NoAccountWindow(Gtk.Box):
self.set_visible(visible)
self.set_no_show_all(not visible)
def is_visible(self):
return self.get_visible()
def hide(self):
self.toggle(False)
......
from gi import require_version
require_version("Gtk", "3.0")
from gi.repository import Gtk, Gio
from gi.repository import Gtk, Gio, Gdk
import logging
class SearchBar(Gtk.Box):
class SearchBar(Gtk.Revealer):
def __init__(self, list_accounts):
self.search_entry = Gtk.Entry()
self.list_accounts = list_accounts
def __init__(self, listbox, window, search_button):
self.search_entry = Gtk.SearchEntry()
self.listbox = listbox
self.search_button = search_button
self.window = window
self.generate()
self.search_button.connect("toggled", self.toggle)
self.window.connect("key-press-event", self.__on_key_press)
def generate(self):
Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL)
self.revealer = Gtk.Revealer()
Gtk.Revealer.__init__(self)
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
self.search_entry.set_width_chars(28)
self.search_entry.connect("changed", self.filter_applications)
self.search_entry.set_icon_from_icon_name(Gtk.EntryIconPosition.PRIMARY,
"system-search-symbolic")
self.search_entry.connect("search-changed", self.filter_applications)
box.pack_start(self.search_entry, True, False, 12)
box.props.margin = 6
self.revealer.add(box)
self.revealer.set_reveal_child(False)
self.pack_start(self.revealer, True, False, 0)
self.add(box)
self.set_reveal_child(False)
def toggle(self, *args):
if self.revealer.get_reveal_child():
self.revealer.set_reveal_child(False)
if self.is_visible():
self.set_reveal_child(False)
self.search_entry.set_text("")
self.list_accounts.set_filter_func(lambda x, y, z: True,
self.listbox.set_filter_func(lambda x, y, z: True,
None, False)
else:
self.revealer.set_reveal_child(True)
self.search_entry.grab_focus_without_selecting()
self.set_reveal_child(True)
self.focus()
def filter_func(self, row, data, notify_destroy):
"""
......@@ -48,23 +49,35 @@ class SearchBar(Gtk.Box):
else:
return True
def __on_key_press(self, widget, event):
keyname = Gdk.keyval_name(event.keyval).lower()
if keyname == 'escape' and self.search_button.get_active():
if self.search_entry.is_focus():
self.search_button.set_active(False)
else:
self.focus()
if not "is_locked" in dir(self.window) or not self.window.is_locked():
if keyname == "backspace":
if self.is_empty():
self.search_button.set_active(False)
return True
if event.state & Gdk.ModifierType.CONTROL_MASK: