diff --git a/data/ui/add_contact.ui b/data/ui/add_contact.ui index e93afe1b2b3cfc01c2e1b4f54824aaf98eb7f07c..5ef80c3d00d9833064c49433b3fc0860e5b7eabe 100644 --- a/data/ui/add_contact.ui +++ b/data/ui/add_contact.ui @@ -3,123 +3,264 @@ Copyright (c) 2019 Nico Alt SPDX-License-Identifier: AGPL-3.0-only License-Filename: LICENSE.md + + Based on parts of GNOME Fractal + https://gitlab.gnome.org/GNOME/fractal/blob/4.2.2/fractal-gtk/res/ui/login_flow.ui --> <interface> - <requires lib="gtk+" version="3.20"/> - <object class="GtkGrid" id="add_contact"> - <property name="visible">True</property> + <requires lib="gtk+" version="3.22"/> + <object class="GtkStack" id="add_contact_flow_stack"> <property name="can_focus">False</property> - <property name="halign">center</property> - <property name="valign">center</property> - <property name="margin_left">18</property> - <property name="margin_right">18</property> - <property name="margin_top">18</property> - <property name="margin_bottom">18</property> + <property name="hhomogeneous">True</property> + <property name="transition_type">GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT_RIGHT</property> <child> - <object class="GtkGrid" id="link_grid"> + <object class="GtkGrid" id="links_page"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="row_spacing">6</property> + <property name="halign">center</property> + <property name="valign">center</property> <property name="column_spacing">12</property> + <property name="row_spacing">24</property> <child> - <object class="GtkLabel" id="own_link_label"> + <object class="GtkLabel"> <property name="visible">True</property> - <property name="halign">center</property> - <property name="label" translatable="yes">Give this link to the contact you want to add</property> + <property name="use_underline">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes" context="add contact">Give this link to the contact you want to add</property> + <property name="wrap">True</property> + <property name="wrap_mode">PANGO_WRAP_WORD_CHAR</property> + <property name="mnemonic_widget">own_link_entry</property> + <style> + <class name="dim-label"/> + </style> </object> <packing> - <property name="left_attach">1</property> - <property name="top_attach">0</property> + <property name="top-attach">1</property> + <property name="left-attach">1</property> </packing> </child> <child> <object class="GtkEntry" id="own_link_entry"> <property name="visible">True</property> + <property name="max_width_chars">-1</property> + <property name="width_request">232</property> + <property name="can_focus">True</property> <property name="input_purpose">GTK_INPUT_PURPOSE_URL</property> </object> <packing> - <property name="left_attach">1</property> - <property name="top_attach">1</property> + <property name="top-attach">2</property> + <property name="left-attach">1</property> </packing> </child> <child> - <object class="GtkLabel" id="their_link_label"> + <object class="GtkLabel"> <property name="visible">True</property> - <property name="halign">center</property> - <property name="label" translatable="yes">Enter the link from your contact here</property> + <property name="use_underline">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes" context="add contact">Enter the link from your contact here</property> + <property name="wrap">True</property> + <property name="wrap_mode">PANGO_WRAP_WORD_CHAR</property> + <property name="mnemonic_widget">their_link_entry</property> + <style> + <class name="dim-label"/> + </style> </object> <packing> - <property name="left_attach">1</property> - <property name="top_attach">2</property> + <property name="top-attach">3</property> + <property name="left-attach">1</property> </packing> </child> <child> <object class="GtkEntry" id="their_link_entry"> <property name="visible">True</property> - <property name="placeholder_text" translatable="yes" context="link exchange: their link input field">Contact's link</property> + <property name="max_width_chars">-1</property> + <property name="width_request">232</property> + <property name="can_focus">True</property> + <property name="placeholder_text" translatable="yes" context="add contact">Contact's link</property> <property name="input_purpose">GTK_INPUT_PURPOSE_URL</property> </object> <packing> - <property name="left_attach">1</property> - <property name="top_attach">3</property> + <property name="top-attach">4</property> + <property name="left-attach">1</property> </packing> </child> <child> - <object class="GtkButton" id="link_continue"> - <property name="visible">True</property> - <property name="halign">center</property> - <property name="label" translatable="yes" context="link exchange: button – initiate pairing">Continue</property> - <signal name="clicked" handler="on_link_button_clicked"/> + <object class="GtkLabel" id="link_error_label"> + <property name="visible">False</property> + <property name="can_focus">False</property> + <property name="no_show_all">True</property> + <property name="label" translatable="yes" context="add contact">Please enter a link</property> + <property name="wrap">True</property> + <property name="wrap_mode">PANGO_WRAP_WORD_CHAR</property> + <style> + <class name="error-label"/> + </style> </object> <packing> - <property name="left_attach">1</property> - <property name="top_attach">4</property> + <property name="top-attach">5</property> + <property name="left-attach">1</property> </packing> </child> </object> + <packing> + <property name="name">links</property> + </packing> </child> <child> - <object class="GtkGrid" id="alias_grid"> - <property name="visible">False</property> + <object class="GtkGrid" id="alias_page"> + <property name="visible">True</property> <property name="can_focus">False</property> - <property name="row_spacing">6</property> + <property name="halign">center</property> + <property name="valign">center</property> <property name="column_spacing">12</property> + <property name="row_spacing">24</property> <child> - <object class="GtkLabel" id="alias_label"> + <object class="GtkLabel"> <property name="visible">True</property> - <property name="halign">center</property> - <property name="label" translatable="yes">Give your contact a nickname. Only you can see it.</property> + <property name="use_underline">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes" context="add contact">Give your contact a nickname. Only you can see it.</property> + <property name="halign">end</property> + <property name="wrap">True</property> + <property name="wrap_mode">PANGO_WRAP_WORD_CHAR</property> + <property name="mnemonic_widget">own_link_entry</property> + <style> + <class name="dim-label"/> + </style> </object> <packing> - <property name="left_attach">1</property> - <property name="top_attach">0</property> + <property name="top-attach">1</property> + <property name="left-attach">1</property> </packing> </child> <child> <object class="GtkEntry" id="alias_entry"> <property name="visible">True</property> - <property name="placeholder_text" translatable="yes" context="remote contact adding: alias input field">Enter a nickname</property> + <property name="max_width_chars">-1</property> + <property name="width_request">232</property> + <property name="can_focus">True</property> + <property name="placeholder_text" translatable="yes" context="add contact">Enter a nickname</property> + </object> + <packing> + <property name="top-attach">2</property> + <property name="left-attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="alias_error_label"> + <property name="visible">False</property> + <property name="can_focus">False</property> + <property name="no_show_all">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes" context="add contact">The alias may not be empty</property> + <property name="wrap">True</property> + <property name="wrap_mode">PANGO_WRAP_WORD_CHAR</property> + <style> + <class name="error-label"/> + </style> </object> <packing> - <property name="left_attach">1</property> - <property name="top_attach">1</property> + <property name="top-attach">3</property> + <property name="left-attach">1</property> </packing> </child> + </object> + <packing> + <property name="name">alias</property> + </packing> + </child> + </object> + <object class="GtkStack" id="add_contact_flow_headers"> + <property name="can_focus">False</property> + <property name="hhomogeneous">True</property> + <property name="visible_child_name" bind-source="add_contact_flow_stack" bind-property="visible-child-name" bind-flags="sync-create"/> + <property name="transition_duration" bind-source="add_contact_flow_stack" bind-property="transition-duration" bind-flags="sync-create"/> + <property name="transition_type" bind-source="add_contact_flow_stack" bind-property="transition-type" bind-flags="sync-create"/> + <child> + <object class="GtkHeaderBar"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="show_close_button">True</property> + <property name="width_request">360</property> + <property name="title">Add contact</property> <child> - <object class="GtkButton" id="add_contact_button"> + <object class="GtkButton"> <property name="visible">True</property> - <property name="halign">center</property> - <property name="label" translatable="yes" context="remote contact adding: button">Add Contact</property> - <signal name="clicked" handler="on_add_contact_button_clicked"/> + <property name="can_focus">True</property> + <signal name="clicked" handler="on_link_back_pressed"/> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="icon_name">go-previous-symbolic</property> + </object> + </child> </object> <packing> - <property name="left_attach">1</property> - <property name="top_attach">3</property> + <property name="pack_type">start</property> + </packing> + </child> + <child> + <object class="GtkButton"> + <property name="visible">True</property> + <property name="use_underline">True</property> + <property name="can_focus">True</property> + <property name="label" translatable="yes">Next</property> + <signal name="clicked" handler="on_links_next_pressed"/> + <style> + <class name="suggested-action"/> + </style> + </object> + <packing> + <property name="pack_type">end</property> </packing> </child> </object> + <packing> + <property name="name">links</property> + </packing> + </child> + <child> + <object class="GtkHeaderBar"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="show_close_button">True</property> + <property name="title" translatable="yes">Add contact</property> + <child> + <object class="GtkButton"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <signal name="clicked" handler="on_alias_back_pressed"/> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="icon_name">go-previous-symbolic</property> + </object> + </child> + </object> + <packing> + <property name="pack_type">start</property> + </packing> + </child> + <child> + <object class="GtkButton"> + <property name="visible">True</property> + <property name="use_underline">True</property> + <property name="can_focus">True</property> + <property name="label" translatable="yes">Add contact</property> + <signal name="clicked" handler="on_add_contact_pressed"/> + <style> + <class name="suggested-action"/> + </style> + </object> + <packing> + <property name="pack_type">end</property> + </packing> + </child> + </object> + <packing> + <property name="name">alias</property> + </packing> </child> </object> </interface> - diff --git a/src/briar/gtk/containers/add_contact.py b/src/briar/gtk/containers/add_contact.py index 7d51989ad0e2951595167b1fb08425f8b4f3935a..f0a946af846b007a69da634ee4272506186db6c5 100644 --- a/src/briar/gtk/containers/add_contact.py +++ b/src/briar/gtk/containers/add_contact.py @@ -2,6 +2,8 @@ # SPDX-License-Identifier: AGPL-3.0-only # License-Filename: LICENSE.md +from gettext import gettext as _ + from gi.repository import GLib from briar.api.models.contacts import Contacts @@ -11,33 +13,127 @@ from briar.gtk.define import APP class AddContactContainer(Container): - CONTAINER_UI = "/app/briar/gtk/ui/add_contact.ui" + ADD_CONTACT_UI = "/app/briar/gtk/ui/add_contact.ui" + STACK_NAME = "add_contact_flow_stack" + HEADERS_NAME = "add_contact_flow_headers" - def __init__(self): + def __init__(self, window): super().__init__() - self._api = APP().api + self._window = window self._setup_view() self._load_content() def _setup_view(self): - self.builder.add_from_resource(self.CONTAINER_UI) - self.add(self.builder.get_object("add_contact")) + self.builder.add_from_resource(self.ADD_CONTACT_UI) self.builder.connect_signals(self) + self._setup_add_contact_flow_stack() + self._setup_add_contact_flow_headers() + self._setup_link_keystroke_listener() + def _load_content(self): - contacts = Contacts(self._api) + contacts = Contacts(APP().api) own_link = contacts.get_link() self.builder.get_object("own_link_entry").set_text(own_link) + def _setup_add_contact_flow_stack(self): + self.add_contact_flow_stack = self.builder.get_object(self.STACK_NAME) + self.add_contact_flow_stack.show_all() + self.add(self.add_contact_flow_stack) + + def _setup_add_contact_flow_headers(self): + add_contact_flow_headers = self.builder.get_object(self.HEADERS_NAME) + add_contact_flow_headers.show_all() + self._window.set_titlebar(add_contact_flow_headers) + + def _setup_link_keystroke_listener(self): + their_link_entry = self.builder.get_object("their_link_entry") + their_link_entry.connect("key-press-event", + self._link_keystroke) + + # pylint: disable=unused-argument + def _link_keystroke(self, widget, event): + if event.hardware_keycode != 36 and event.hardware_keycode != 104: + return + self.on_links_next_pressed(None) + + # pylint: disable=unused-argument + def on_link_back_pressed(self, button): + self._back_to_main_window() + + # pylint: disable=unused-argument + def on_links_next_pressed(self, button): + link_error_label = self.builder.get_object("link_error_label") + if self._link_is_empty(): + link_error_label.set_label(_("Please enter a link")) + link_error_label.show() + return + if self._their_link_is_ours(): + link_error_label.show() + link_error_label.set_label( + _("Enter your contact's link, not your own")) + return + link_error_label.hide() + self._show_alias_page() + + def _their_link_is_ours(self): + their_link = self.builder.get_object("their_link_entry").get_text() + own_link = self.builder.get_object("own_link_entry").get_text() + return their_link == own_link + + def _link_is_empty(self): + their_link = self.builder.get_object("their_link_entry").get_text() + return len(their_link) == 0 + + def _show_alias_page(self): + alias_page = self.builder.get_object("alias_page") + self.add_contact_flow_stack.set_visible_child(alias_page) + + self._focus_alias_entry() + self._setup_alias_keystroke_listener() + + def _focus_alias_entry(self): + alias_entry = self.builder.get_object("alias_entry") + alias_entry.grab_focus() + + def _setup_alias_keystroke_listener(self): + alias_entry = self.builder.get_object("alias_entry") + alias_entry.connect("key-press-event", self._alias_keystroke) + # pylint: disable=unused-argument - def on_link_button_clicked(self, button): - self.builder.get_object("link_grid").set_visible(False) - self.builder.get_object("alias_grid").set_visible(True) + def _alias_keystroke(self, widget, event): + if event.hardware_keycode != 36 and event.hardware_keycode != 104: + return + self.on_add_contact_pressed(None) # pylint: disable=unused-argument - def on_add_contact_button_clicked(self, button): + def on_alias_back_pressed(self, button): + self._show_links_page() + + def _show_links_page(self): + links_page = self.builder.get_object("links_page") + self.add_contact_flow_stack.set_visible_child(links_page) + + # pylint: disable=unused-argument + def on_add_contact_pressed(self, button): + alias_error_label = self.builder.get_object( + "alias_error_label") + if self._alias_is_empty(): + alias_error_label.show() + return + alias_error_label.hide() + self._add_contact() + self._back_to_main_window() + + def _alias_is_empty(self): + alias = self.builder.get_object("alias_entry").get_text() + return len(alias) == 0 + + def _add_contact(self): + contacts = Contacts(APP().api) their_link = self.builder.get_object("their_link_entry").get_text() alias = self.builder.get_object("alias_entry").get_text() - contacts = Contacts(self._api) contacts.add_pending(their_link, alias) - GLib.idle_add(APP().window.back_to_main, None) + + def _back_to_main_window(self): + GLib.idle_add(self._window.back_to_main, None) diff --git a/src/briar/gtk/containers/main.py b/src/briar/gtk/containers/main.py index 32ad26e5a5f1c2e6b2d5f67b82e8de37e6ee64dd..5cdadcca928eb2df87b619c08d3e3c13494d6ab6 100644 --- a/src/briar/gtk/containers/main.py +++ b/src/briar/gtk/containers/main.py @@ -38,4 +38,5 @@ class MainContainer(Container): # pylint: disable=unused-argument @staticmethod def _contact_clicked(widget, contact_id): - GLib.idle_add(APP().get_property("active_window").open_private_chat, contact_id) + GLib.idle_add(APP().get_property("active_window"). + open_private_chat, contact_id) diff --git a/src/briar/gtk/window.py b/src/briar/gtk/window.py index dad4ff2b536e785780de915a38093a71b0c7e392..665d0063a15b9ebe9314c3aa57ed2a0cd935f9cd 100644 --- a/src/briar/gtk/window.py +++ b/src/briar/gtk/window.py @@ -63,9 +63,11 @@ class Window(ApplicationWindow): self._grid.show() self.add(self._grid) - def _reset_grid(self): + def _reset_window(self): + self._container.destroy() self._grid.destroy() self._setup_grid() + self._setup_toolbar() def _setup_toolbar(self): self._toolbar = Toolbar() @@ -81,18 +83,16 @@ class Window(ApplicationWindow): # pylint: disable=unused-argument def show_add_contact(self, widget): - self._reset_grid() self._setup_add_contact() def _setup_add_contact(self): - self._container = AddContactContainer() + self._grid.destroy() + self._container = AddContactContainer(self) self._container.show() - self._grid.add(self._container) - self._toolbar.show_back_button(True, self.back_to_main) - self._toolbar.show_add_contact_button(False) + self.add(self._container) def open_private_chat(self, contact_id): - self._reset_grid() + self._reset_window() self._setup_private_chat(contact_id) def _setup_private_chat(self, contact_id): @@ -104,6 +104,6 @@ class Window(ApplicationWindow): # pylint: disable=unused-argument def back_to_main(self, widget): - self._reset_grid() + self._reset_window() self._toolbar.show_back_button(False) self._setup_main_container()