Skip to content

Commit

Permalink
WIP: Add support for multi-source selection, and add a multi-source v…
Browse files Browse the repository at this point in the history
…iew pane in the conversationview.
  • Loading branch information
rocodes committed Oct 4, 2024
1 parent d85963e commit a901ad8
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 33 deletions.
119 changes: 86 additions & 33 deletions client/securedrop_client/gui/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,7 @@ def __init__(
app_state.set_selected_conversation_for_source
)
self.source_list.source_selection_cleared.connect(app_state.clear_selected_conversation)
self.source_list.multi_select.connect(app_state.show_multi_select_view)

# Create widgets
self.view_holder = QWidget()
Expand Down Expand Up @@ -836,18 +837,24 @@ def show_sources(self, sources: list[Source]) -> None:
"""
Update the sources list in the GUI with the supplied list of sources.
"""
# If no sources are supplied, display the EmptyConversationView with the no-sources message.
# If no sources are supplied, display the EmptyConversationView with the no-sources
# message.
#
# If there are sources but no source is selected in the GUI, display the
# EmptyConversationView with the no-source-selected messaging.
#
# If multiple sources are selected, show a variant of the view with the multi-select
# message.
#
# Otherwise, hide the EmptyConversationView.
if not sources:
self.empty_conversation_view.show_no_sources_message()
self.empty_conversation_view.show()
elif not self.source_list.get_selected_source():
self.empty_conversation_view.show_no_source_selected_message()
self.empty_conversation_view.show()
elif len(self.source_list.selectedItems()) > 1:
self.empty_conversation_view.show_multi_select_message()
else:
self.empty_conversation_view.hide()

Expand All @@ -863,37 +870,41 @@ def show_sources(self, sources: list[Source]) -> None:

def on_source_changed(self) -> None:
"""
Show conversation for the selected source.
Show conversation for the selected source, or, if multiple sources are selected,
show multi select view.
"""
try:
source = self.source_list.get_selected_source()
if not source:
return

self.controller.session.refresh(source)

# Immediately show the selected source as seen in the UI and then make a request to mark
# source as seen.
self.source_list.source_selected.emit(source.uuid)
self.controller.mark_seen(source)

# Get or create the SourceConversationWrapper
if source.uuid in self.source_conversations:
conversation_wrapper = self.source_conversations[source.uuid]
conversation_wrapper.conversation_view.update_conversation( # type: ignore[has-type]
source.collection
)
else:
conversation_wrapper = SourceConversationWrapper(
source, self.controller, self._state
)
self.source_conversations[source.uuid] = conversation_wrapper
if len(self.source_list.selectedItems()) > 1:
self.empty_conversation_view.show_multi_select_message()
else:
try:
source = self.source_list.get_selected_source()
if not source:
return

self.controller.session.refresh(source)

# Immediately show the selected source as seen in the UI and then make a request to mark
# source as seen.
self.source_list.source_selected.emit(source.uuid)
self.controller.mark_seen(source)

# Get or create the SourceConversationWrapper
if source.uuid in self.source_conversations:
conversation_wrapper = self.source_conversations[source.uuid]
conversation_wrapper.conversation_view.update_conversation( # type: ignore[has-type]
source.collection
)
else:
conversation_wrapper = SourceConversationWrapper(
source, self.controller, self._state
)
self.source_conversations[source.uuid] = conversation_wrapper

self.set_conversation(conversation_wrapper)
logger.debug(f"Set conversation to the selected source with uuid: {source.uuid}")
self.set_conversation(conversation_wrapper)
logger.debug(f"Set conversation to the selected source with uuid: {source.uuid}")

except sqlalchemy.exc.InvalidRequestError as e:
logger.debug(e)
except sqlalchemy.exc.InvalidRequestError as e:
logger.debug(e)

def refresh_source_conversations(self) -> None:
"""
Expand Down Expand Up @@ -1016,18 +1027,50 @@ def __init__(self) -> None:
no_source_selected_layout.addWidget(bullet3)
no_source_selected_layout.addSpacing(self.NEWLINE_HEIGHT_PX * 4)

# Multi-source selection widget, same css properties as Empty view
self.multi_source_selected = QWidget()
self.multi_source_selected.setObjectName("EmptyConversationView_no_sources")
multi_sources_layout = QVBoxLayout()
self.multi_source_selected.setLayout(no_sources_layout)
multi_sources_instructions = QLabel(_("Multiple Sources Selected"))
multi_sources_instructions.setObjectName("EmptyConversationView_instructions")
multi_sources_instructions.setWordWrap(True)
multi_sources_instruction_details1 = QLabel(
_(
"Select or de-select sources using Ctrl+click, Shift+click, or by dragging the mouse." # noqa: E501
)
)
multi_sources_instruction_details1.setWordWrap(True)
multi_sources_instruction_details2 = QLabel(
_("Use the top toolbar to delete multiple sources at once.")
)
multi_sources_instruction_details2.setWordWrap(True)
multi_sources_layout.addWidget(multi_sources_instructions)
multi_sources_layout.addSpacing(self.NEWLINE_HEIGHT_PX)
multi_sources_layout.addWidget(multi_sources_instruction_details1)
multi_sources_layout.addSpacing(self.NEWLINE_HEIGHT_PX)
multi_sources_layout.addWidget(multi_sources_instruction_details2)

# Add widgets
layout.addWidget(self.no_sources, alignment=Qt.AlignCenter)
layout.addWidget(self.no_source_selected, alignment=Qt.AlignCenter)
layout.addWidget(self.multi_source_selected, alignment=Qt.AlignCenter)

def show_no_sources_message(self) -> None:
self.no_sources.show()
self.no_source_selected.hide()
self.multi_source_selected.hide()

def show_no_source_selected_message(self) -> None:
self.no_sources.hide()
self.multi_source_selected.hide()
self.no_source_selected.show()

def show_multi_select_message(self) -> None:
self.no_sources.hide()
self.no_source_selected.hide()
self.multi_source_selected.show()


class SourceListWidgetItem(QListWidgetItem):
def __lt__(self, other: SourceListWidgetItem) -> bool:
Expand All @@ -1053,6 +1096,7 @@ class SourceList(QListWidget):

source_selection_changed = pyqtSignal(state.SourceId)
source_selection_cleared = pyqtSignal()
multi_select = pyqtSignal()
selected_sources = pyqtSignal(object) # set[Source]

NUM_SOURCES_TO_ADD_AT_A_TIME = 32
Expand Down Expand Up @@ -1267,11 +1311,20 @@ def set_snippet(self, source_uuid: str, collection_item_uuid: str, content: str)

@pyqtSlot()
def _on_item_selection_changed(self) -> None:
source = self.get_selected_source()
if source is not None:
self.source_selection_changed.emit(state.SourceId(source.uuid))
else:
"""
One or more items may be selected. If multiple items are selected, to avoid confusion,
don't preview any individual source conversation, but instead show a contextual message.
"""
count = self.selectedItems()
if count == 0:
self.source_selection_cleared.emit()
elif count == 1:
source = self.get_selected_source()
if source:
self.source_selection_changed.emit(state.SourceId(source.uuid))
else:
# Multiple items, change the conversation view
self.multi_select.emit()


class SourcePreview(SecureQLabel):
Expand Down
4 changes: 4 additions & 0 deletions client/securedrop_client/state/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,7 @@ def set_selected_conversation_for_source(self, source_id: SourceId) -> None:
@pyqtSlot()
def clear_selected_conversation(self) -> None:
self.selected_conversation = None

@pyqtSlot()
def show_multi_select_view(self) -> None:
self.selected_conversation = None

0 comments on commit a901ad8

Please sign in to comment.