diff --git a/data/entry_row.ui b/data/entry_row.ui new file mode 100644 index 0000000000000000000000000000000000000000..0462dfa1f9a9e87b03dbfdb5f16e6aae8958acdb --- /dev/null +++ b/data/entry_row.ui @@ -0,0 +1,119 @@ + + + + + + True + False + baseline + + + 60 + True + False + + + True + False + center + center + True + True + 8 + + + + + 32 + 32 + True + False + False + False + False + center + center + 10 + 10 + none + + + True + False + 16 + application-x-executable-symbolic + + + + + + + + True + False + vertical + + + name_label + True + False + start + end + False + True + Title not specified + end + + + + + True + False + True + vertical + + + entry_subtitle_label + True + False + start + start + False + True + No username specified + end + + + + + + + + + ListBoxRowButton + True + True + True + end + center + 5 + 8 + + + True + False + edit-copy-symbolic + + + + + end + + + + + + diff --git a/data/passwordsafe.gresource.xml b/data/passwordsafe.gresource.xml index eaacb4cc95b6b5c92946d700d9a824498bde2278..52ae13ce9498197b1590f22083e5c8ed41db716a 100644 --- a/data/passwordsafe.gresource.xml +++ b/data/passwordsafe.gresource.xml @@ -11,6 +11,7 @@ created_database.ui database_settings_dialog.ui entry_page.ui + entry_row.ui group_page.ui unlock_database.ui unlocked_database.ui diff --git a/data/unlocked_database.ui b/data/unlocked_database.ui index 93341558fc6853f49b9e18e8c3dd64fd8b0bec24..8bf4c4606913d056989d715dbe45a99a831120f3 100644 --- a/data/unlocked_database.ui +++ b/data/unlocked_database.ui @@ -267,166 +267,6 @@ - - 1 - True - False - baseline - - - 60 - True - False - - - True - False - 10 - vertical - - - True - True - False - center - center - True - True - - - False - True - 0 - - - - - False - True - 0 - - - - - 32 - 32 - True - False - False - False - False - center - center - 10 - 10 - none - - - True - False - 16 - application-x-executable-symbolic - - - - - - False - False - 1 - - - - - True - False - vertical - - - name_label - True - False - start - end - False - True - Title not specified - end - - - False - True - 0 - - - - - True - False - True - vertical - - - entry_subtitle_label - True - False - start - start - False - True - No username specified - end - - - False - True - 0 - - - - - False - True - 1 - - - - - False - True - 2 - - - - - ListBoxRowButton - True - True - True - end - center - 5 - 8 - - - True - False - edit-copy-symbolic - - - - - False - False - end - 3 - - - - - True False @@ -440,7 +280,6 @@ - 1 True False baseline @@ -450,30 +289,18 @@ True False - - True - False - 10 - - - True - True - False - center - center - True - True - - - False - True - 0 - - + + True + False + center + center + True + True + 8 False - True + False 0 @@ -509,7 +336,6 @@ - 1 True False True @@ -598,6 +424,9 @@ + + 1 + @@ -1195,7 +1024,7 @@ 2 - + True False diff --git a/passwordsafe/color_widget.py b/passwordsafe/color_widget.py index 343846649cc784a46a75e225da9f0320ebeaab6c..51b1f5cab9442945afea1f7617b47e0f10c6ae40 100644 --- a/passwordsafe/color_widget.py +++ b/passwordsafe/color_widget.py @@ -82,8 +82,7 @@ class ColorEntryRow(Gtk.ListBoxRow): # pylint: disable=too-few-public-methods self._scrolled_page: ScrolledPage = scrolled_page self._entry_uuid: UUID = entry_uuid - self._selected_color: str = self._db_manager.get_entry_color_from_entry_uuid( - entry_uuid) + self._selected_color: str = self._db_manager.get_entry_color(entry_uuid) for color in Color: active: bool = (self._selected_color == color.value) diff --git a/passwordsafe/database_manager.py b/passwordsafe/database_manager.py index a8f5502546ed52ba287adfbf6e8b0196c554076a..87e92444f199b6c22e29e9fcb2564e71d48f7b6f 100644 --- a/passwordsafe/database_manager.py +++ b/passwordsafe/database_manager.py @@ -182,6 +182,8 @@ class DatabaseManager(GObject.GObject): def get_entry_name(self, data: Union[Entry, UUID]) -> str: """Get entry name from an uuid or an entry + Passing in an Entry is more performant than passing in a UUID + as we avoid having to look up the entry. :param data: UUID or Entry :returns: entry name or an empty string if it does not exist :rtype: str @@ -201,6 +203,8 @@ class DatabaseManager(GObject.GObject): def get_entry_username(self, data: Union[Entry, UUID]) -> str: """Get an entry username from an entry or an uuid + Passing in an Entry is more performant than passing in a UUID + as we avoid having to look up the entry. :param data: entry or uuid :returns: entry username or an empty string if it does not exist :rtype: str @@ -220,6 +224,8 @@ class DatabaseManager(GObject.GObject): def get_entry_password(self, data: Union[Entry, UUID]) -> str: """Get an entry password from an entry or an uuid + Passing in an Entry is more performant than passing in a UUID + as we avoid having to look up the entry. :param data: entry or uuid :returns: entry password or an empty string if it does not exist :rtype: str @@ -239,6 +245,8 @@ class DatabaseManager(GObject.GObject): def get_entry_url(self, data: Union[Entry, UUID]) -> str: """Get an entry url from an entry or an uuid + Passing in an Entry is more performant than passing in a UUID + as we avoid having to look up the entry. :param data: UUID or Entry :returns: entry url or an empty string if it does not exist :rtype: str @@ -255,9 +263,27 @@ class DatabaseManager(GObject.GObject): return entry.url or "" - # Return the belonging color for an entry uuid - def get_entry_color_from_entry_uuid(self, uuid): - entry = self.db.find_entries(uuid=uuid, first=True) + def get_entry_color(self, data: Union[Entry, UUID]) -> Union[str, Color]: + """Get an entry color from an entry or an uuid + + Passing in an Entry is more performant than passing in a UUID + as we avoid having to look up the entry. + :param data: UUID or Entry + :returns: entry color as str or Color.NONE.value + :rtype: str + """ + if isinstance(data, UUID): + entry: Entry = self.db.find_entries(uuid=data, first=True) + if not entry: + logging.warning( + "Trying to look up a non-existing UUID %s, this should " + "never happen", + data, + ) + return Color.NONE.value + else: + entry = data + if entry.get_custom_property("color_prop_LcljUMJZ9X") is None: return Color.NONE.value else: @@ -611,19 +637,6 @@ class DatabaseManager(GObject.GObject): # Read Database # - def get_groups_in_root(self): - return self.db.root_group.subgroups - - def get_groups_in_folder(self, uuid): - """Return list of all subgroups in a group""" - folder = self.get_group(uuid) - return folder.subgroups - - def get_entries_in_folder(self, uuid): - """Return list of all entries in a group""" - parent_group = self.get_group(uuid) - return parent_group.entries - # Return the root group of the database instance def get_root_group(self): return self.db.root_group diff --git a/passwordsafe/entry_page.py b/passwordsafe/entry_page.py index bdfaaaa979736007a26b00cd757af78c397e0020..c7c4205be66a4ec8ff121d9434e0b3920f76d9a5 100644 --- a/passwordsafe/entry_page.py +++ b/passwordsafe/entry_page.py @@ -67,33 +67,23 @@ class EntryPage: builder.add_from_resource("/org/gnome/PasswordSafe/entry_page.ui") - entry_uuid = self.unlocked_database.current_element.uuid + cur_entry = self.unlocked_database.current_element + entry_uuid = cur_entry.uuid scrolled_page = self.unlocked_database.get_current_page() - - if self.unlocked_database.database_manager.has_entry_name(entry_uuid) is True or add_all is True: + entry_name = self.unlocked_database.database_manager.get_entry_name(cur_entry) + if entry_name or add_all: if scrolled_page.name_property_row is NotImplemented: + # Create the name_property_row scrolled_page.name_property_row = builder.get_object("name_property_row") scrolled_page.name_property_value_entry = builder.get_object("name_property_value_entry") scrolled_page.name_property_value_entry.set_buffer(HistoryEntryBuffer([])) - value = self.unlocked_database.database_manager.get_entry_name(entry_uuid) - if self.unlocked_database.database_manager.has_entry_name(entry_uuid) is True: - scrolled_page.name_property_value_entry.set_text(value) - else: - scrolled_page.name_property_value_entry.set_text("") - - scrolled_page.name_property_value_entry.connect("changed", self.on_property_value_entry_changed, "name") - properties_list_box.add(scrolled_page.name_property_row) - scrolled_page.name_property_value_entry.grab_focus() - elif scrolled_page.name_property_row: - value = self.unlocked_database.database_manager.get_entry_name( - entry_uuid) - if self.unlocked_database.database_manager.has_entry_name(entry_uuid) is True: - scrolled_page.name_property_value_entry.set_text(value) - else: - scrolled_page.name_property_value_entry.set_text("") - scrolled_page.name_property_value_entry.connect("changed", self.on_property_value_entry_changed, "name") - properties_list_box.add(scrolled_page.name_property_row) + scrolled_page.name_property_value_entry.set_text(entry_name) + scrolled_page.name_property_value_entry.connect( + "changed", self.on_property_value_entry_changed, "name" + ) + properties_list_box.add(scrolled_page.name_property_row) + scrolled_page.name_property_value_entry.grab_focus() if self.unlocked_database.database_manager.has_entry_username(entry_uuid) is True or add_all is True: if scrolled_page.username_property_row is NotImplemented: diff --git a/passwordsafe/entry_row.py b/passwordsafe/entry_row.py index 713bfee994ed71635fa4289968f350f78e186666..0d6b5df8885e02fdccf692122cda04ac0f922251 100644 --- a/passwordsafe/entry_row.py +++ b/passwordsafe/entry_row.py @@ -1,77 +1,67 @@ # SPDX-License-Identifier: GPL-3.0-only from __future__ import annotations +import typing from gettext import gettext as _ from uuid import UUID from typing import Optional from gi.repository import Gtk + import passwordsafe.config_manager import passwordsafe.icon from passwordsafe.color_widget import Color +if typing.TYPE_CHECKING: + from pykeepass.entry import Entry + from passwordsafe.unlocked_database import UnlockedDatabase + class EntryRow(Gtk.ListBoxRow): - unlocked_database = NotImplemented - database_manager = NotImplemented - entry_uuid = NotImplemented - icon = NotImplemented - label = NotImplemented - password = NotImplemented - changed = False + builder = Gtk.Builder() selection_checkbox = NotImplemented - checkbox_box = NotImplemented - color = NotImplemented type = "EntryRow" - targets = NotImplemented - - def __init__(self, unlocked_database, dbm, entry): + def __init__(self, database: UnlockedDatabase, entry: Entry) -> None: Gtk.ListBoxRow.__init__(self) self.set_name("EntryRow") - self.unlocked_database = unlocked_database - self.database_manager = dbm + self.unlocked_database = database + self.db_manager = database.database_manager self.entry_uuid = entry.uuid - self.icon: Optional[int] = dbm.get_icon(entry) - self.label = dbm.get_entry_name(entry) - self.password = dbm.get_entry_password(entry) - self.color = dbm.get_entry_color_from_entry_uuid(self.entry_uuid) + self.icon: Optional[int] = self.db_manager.get_icon(entry) + self.label: str = entry.title or "" + self.color = self.db_manager.get_entry_color(entry) + self.username: str = entry.username or "" + if self.username.startswith("{REF:U"): + # Loopup reference and put in the "real" username + uuid = UUID(self.unlocked_database.reference_to_hex_uuid(self.username)) + self.username = self.db_manager.get_entry_username(uuid) self.assemble_entry_row() def assemble_entry_row(self): - builder = Gtk.Builder() - builder.add_from_resource( - "/org/gnome/PasswordSafe/unlocked_database.ui") - entry_event_box = builder.get_object("entry_event_box") + self.builder.add_from_resource("/org/gnome/PasswordSafe/entry_row.ui") + entry_event_box = self.builder.get_object("entry_event_box") entry_event_box.connect("button-press-event", self.unlocked_database.on_entry_row_button_pressed) - entry_icon = builder.get_object("entry_icon") - entry_name_label = builder.get_object("entry_name_label") - entry_subtitle_label = builder.get_object("entry_subtitle_label") - entry_copy_button = builder.get_object("entry_copy_button") - entry_color_button = builder.get_object("entry_color_button") + entry_icon = self.builder.get_object("entry_icon") + entry_name_label = self.builder.get_object("entry_name_label") + entry_subtitle_label = self.builder.get_object("entry_subtitle_label") + entry_copy_button = self.builder.get_object("entry_copy_button") + entry_color_button = self.builder.get_object("entry_color_button") # Icon icon_name: str = passwordsafe.icon.get_icon_name(self.icon) entry_icon.set_from_icon_name(icon_name, 20) # Title/Name - if self.database_manager.has_entry_name(self.entry_uuid) and self.label: + if self.label: entry_name_label.set_text(self.label) else: entry_name_label.set_markup("" + _("Title not specified") + "") # Subtitle - subtitle = self.database_manager.get_entry_username(self.entry_uuid) - if (self.database_manager.has_entry_username(self.entry_uuid) and subtitle): - username = self.database_manager.get_entry_username(self.entry_uuid) - if username.startswith("{REF:U"): - uuid = UUID( - self.unlocked_database.reference_to_hex_uuid(username)) - username = self.database_manager.get_entry_username(uuid) - entry_subtitle_label.set_text(username) - else: - entry_subtitle_label.set_text(username) + if self.username: + entry_subtitle_label.set_text(self.username) else: entry_subtitle_label.set_markup("" + _("No username specified") + "") @@ -82,21 +72,17 @@ class EntryRow(Gtk.ListBoxRow): image = entry_color_button.get_children()[0] image_style = image.get_style_context() if self.color != Color.NONE.value: + image_style.remove_class("DarkIcon") image_style.add_class("BrightIcon") - else: - image_style.add_class("DarkIcon") self.add(entry_event_box) - self.show_all() + self.show() # Selection Mode Checkboxes - self.checkbox_box = builder.get_object("entry_checkbox_box") - self.selection_checkbox = builder.get_object("selection_checkbox_entry") + self.selection_checkbox = self.builder.get_object("selection_checkbox_entry") self.selection_checkbox.connect("toggled", self.on_selection_checkbox_toggled) if self.unlocked_database.selection_ui.selection_mode_active is True: - self.checkbox_box.show_all() - else: - self.checkbox_box.hide() + self.selection_checkbox.show() def get_uuid(self): return self.entry_uuid @@ -107,19 +93,9 @@ class EntryRow(Gtk.ListBoxRow): def set_label(self, label): self.label = label - def update_password(self): - self.password = self.database_manager.get_entry_password( - self.entry_uuid) - def get_type(self): return self.type - def set_changed(self, boolean): - self.changed = boolean - - def get_changed(self): - return self.changed - def on_selection_checkbox_toggled(self, _widget): if self.selection_checkbox.get_active() is True: if self not in self.unlocked_database.selection_ui.entries_selected: @@ -143,7 +119,9 @@ class EntryRow(Gtk.ListBoxRow): # self.unlocked_database.selection_ui.cut_mode is True def on_entry_copy_button_clicked(self, _button): - self.unlocked_database.send_to_clipboard(self.database_manager.get_entry_password(self.entry_uuid)) + self.unlocked_database.send_to_clipboard( + self.db_manager.get_entry_password(self.entry_uuid) + ) def update_color(self, color): self.color = color diff --git a/passwordsafe/group_row.py b/passwordsafe/group_row.py index 9e8854e793defd8850a79524e65c7d0584d8bf58..54dcc1ee5c240571e189d1702e85acf4997418c4 100644 --- a/passwordsafe/group_row.py +++ b/passwordsafe/group_row.py @@ -8,7 +8,6 @@ class GroupRow(Gtk.ListBoxRow): group_uuid = NotImplemented label = NotImplemented selection_checkbox = NotImplemented - checkbox_box = NotImplemented edit_button = NotImplemented type = "GroupRow" @@ -38,16 +37,13 @@ class GroupRow(Gtk.ListBoxRow): group_name_label.set_markup("" + _("No group title specified") + "") self.add(group_event_box) - self.show_all() + self.show() # Selection Mode Checkboxes - self.checkbox_box = builder.get_object("group_checkbox_box") self.selection_checkbox = builder.get_object("selection_checkbox_group") self.selection_checkbox.connect("toggled", self.on_selection_checkbox_toggled) if self.unlocked_database.selection_ui.selection_mode_active is True: - self.checkbox_box.show_all() - else: - self.checkbox_box.hide() + self.selection_checkbox.show() # Edit Button self.edit_button = builder.get_object("group_edit_button") diff --git a/passwordsafe/pathbar.py b/passwordsafe/pathbar.py index 8914d24d63388e4f17e57a2436ce807007719610..63448d8858828222630f5fb66df400b3e4b7eaee 100644 --- a/passwordsafe/pathbar.py +++ b/passwordsafe/pathbar.py @@ -216,16 +216,16 @@ class Pathbar(Gtk.HBox): if self.database_manager.check_is_root_group(update_group) is True: update_group = self.database_manager.get_root_group() - page_name = update_group.uuid - self.unlocked_database.schedule_stack_page_for_destroy(page_name) + page_uuid = update_group.uuid + self.unlocked_database.schedule_stack_page_for_destroy(page_uuid) self.page_update_queried() else: if self.check_is_edit_page_from_group() is False: return - edit_page = self.unlocked_database.current_element - self.unlocked_database.schedule_stack_page_for_destroy(edit_page.uuid) + edited_uuid = self.unlocked_database.current_element.uuid + self.unlocked_database.schedule_stack_page_for_destroy(edited_uuid) def page_update_queried(self): """Marks the curent page as not dirty""" @@ -233,7 +233,12 @@ class Pathbar(Gtk.HBox): page.is_dirty = False def check_values_of_edit_page(self, parent_group: Group) -> bool: - """Check all values of the group/entry - if all are blank we delete the entry/group and return true""" + """Check all values of the current group/entry which we finished editing + + If all are blank we delete the entry/group and return True. + It also schedules the parent page for destruction if the + Entry/Group has been deleted. + """ current_elt = self.unlocked_database.current_element notes = self.database_manager.get_notes(current_elt) icon = self.database_manager.get_icon(current_elt) diff --git a/passwordsafe/search.py b/passwordsafe/search.py index 46df8c1f747b79f283b0ab2cf5c9b75f8c412a51..86cc87cc5bec8a800f6cec7181237b0bfa24d6cc 100644 --- a/passwordsafe/search.py +++ b/passwordsafe/search.py @@ -188,7 +188,12 @@ class Search: search_height += entry_row_height if skip is False: - row = EntryRow(self.unlocked_database, self.unlocked_database.database_manager, self.unlocked_database.database_manager.get_entry_object_from_uuid(uuid)) + row = EntryRow( + self.unlocked_database, + self.unlocked_database.database_manager.get_entry_object_from_uuid( + uuid + ), + ) self.search_list_box.add(row) self.cached_rows.append(row) else: diff --git a/passwordsafe/selection_ui.py b/passwordsafe/selection_ui.py index cce88e794c88015c3e3ee0bc5dabd87b2f28e1ff..d930493b16032442c2f8749bcbb409b39863329b 100644 --- a/passwordsafe/selection_ui.py +++ b/passwordsafe/selection_ui.py @@ -82,8 +82,8 @@ class SelectionUI: list_box = stack_page.get_children()[0].get_children()[0].get_children()[0].get_children()[0] for row in list_box: row.show() - if hasattr(row, "checkbox_box") is True: - row.checkbox_box.hide() + if hasattr(row, "selection_checkbox"): + row.selection_checkbox.hide() row.selection_checkbox.set_active(False) if hasattr(row, "edit_button") is True: row.edit_button.show_all() @@ -127,8 +127,8 @@ class SelectionUI: if stack_page.check_is_edit_page() is False: list_box = stack_page.get_children()[0].get_children()[0].get_children()[0].get_children()[0] for row in list_box: - if hasattr(row, "checkbox_box") is True: - row.checkbox_box.show() + if hasattr(row, "selection_checkbox"): + row.selection_checkbox.show() if hasattr(row, "edit_button") is True: row.edit_button.hide() diff --git a/passwordsafe/unlocked_database.py b/passwordsafe/unlocked_database.py index 1d9d1c84503fb830eb6cb1e0f1c63e13a5382bc1..dc814ec3f7aa89e27f71b04ed71ac0bd46f4e8eb 100644 --- a/passwordsafe/unlocked_database.py +++ b/passwordsafe/unlocked_database.py @@ -218,12 +218,10 @@ class UnlockedDatabase(GObject.GObject): def show_page_of_new_directory(self, edit_group, new_entry): # First, remove stack pages which should not exist because they are scheduled for remove - self.destroy_scheduled_stack_page() + self.destroy_current_page_if_scheduled() # Creation of group edit page if edit_group is True: - self.destroy_scheduled_stack_page() - builder = Gtk.Builder() builder.add_from_resource("/org/gnome/PasswordSafe/group_page.ui") scrolled_window = ScrolledPage(True) @@ -387,13 +385,17 @@ class UnlockedDatabase(GObject.GObject): """ return self._stack.get_children() - def schedule_stack_page_for_destroy(self, page_name): - self.scheduled_page_destroy.append(page_name) + def schedule_stack_page_for_destroy(self, page_uuid: UUID) -> None: + """Add page to the list of pages to be destroyed""" + logging.debug("Scheduling page %s for destruction") + self.scheduled_page_destroy.append(page_uuid) - def destroy_scheduled_stack_page(self): + def destroy_current_page_if_scheduled(self) -> None: + """If the current_element is in self.scheduled_page_destroy, destroy it""" page_uuid = self.current_element.uuid - + logging.debug("Test if we should destroy page %s", page_uuid) if page_uuid in self.scheduled_page_destroy: + logging.debug("Yes, destroying page %s", page_uuid) stack_page_name = self._stack.get_child_by_name(page_uuid.urn) if stack_page_name is not None: stack_page_name.destroy() @@ -410,17 +412,13 @@ class UnlockedDatabase(GObject.GObject): add_loading_indicator_thread = threading.Thread(target=self.add_loading_indicator_thread, args=(list_box, overlay)) add_loading_indicator_thread.start() - if self.current_element.is_root_group: - groups = self.database_manager.get_groups_in_root() - else: - groups = self.database_manager.get_groups_in_folder(self.current_element.uuid) - + groups = self.current_element.subgroups GLib.idle_add(self.group_instance_creation, list_box, sorted_list, groups) self.insert_entries_into_listbox(list_box, overlay) def insert_entries_into_listbox(self, list_box, overlay): - entries = self.database_manager.get_entries_in_folder(self.current_element.uuid) + entries = self.current_element.entries sorted_list = [] GLib.idle_add(self.entry_instance_creation, list_box, sorted_list, entries, overlay) @@ -440,7 +438,7 @@ class UnlockedDatabase(GObject.GObject): def entry_instance_creation(self, list_box, sorted_list, entries, overlay): for entry in entries: - entry_row = EntryRow(self, self.database_manager, entry) + entry_row = EntryRow(self, entry) sorted_list.append(entry_row) if self.list_box_sorting == "A-Z":