Skip to content

Commit

Permalink
Add "Delete Sources" (batch delete) buttin in top bar. Change
Browse files Browse the repository at this point in the history
MainView layout to QVBoxLayout and add inner horizontal container to
accommodate inner top bar.

Update strings
  • Loading branch information
rocodes committed Oct 3, 2024
1 parent 6cfbca4 commit 1b0942d
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 11 deletions.
8 changes: 7 additions & 1 deletion client/securedrop_client/gui/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from securedrop_client.db import Source, User
from securedrop_client.gui.auth import LoginDialog
from securedrop_client.gui.shortcuts import Shortcuts
from securedrop_client.gui.widgets import BottomPane, LeftPane, MainView
from securedrop_client.gui.widgets import BottomPane, InnerTopPane, LeftPane, MainView
from securedrop_client.logic import Controller
from securedrop_client.resources import load_all_fonts, load_css, load_icon

Expand Down Expand Up @@ -63,6 +63,11 @@ def __init__(
self.setWindowTitle(_("SecureDrop Client {}").format(__version__))
self.setWindowIcon(load_icon(self.icon))

# Top Pane to hold batch actions, eventually will also hold
# search bar for keyword filtering. The Top Pane is not a top-level
# layout element, but instead is nested inside the central widget view.
self.top_pane = InnerTopPane()

# Bottom Pane to display activity and error messages
self.bottom_pane = BottomPane()

Expand Down Expand Up @@ -104,6 +109,7 @@ def setup(self, controller: Controller) -> None:
views used in the UI.
"""
self.controller = controller
self.top_pane.setup(self.controller)
self.bottom_pane.setup(self.controller)
self.left_pane.setup(self, self.controller)
self.main_view.setup(self.controller)
Expand Down
143 changes: 133 additions & 10 deletions client/securedrop_client/gui/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
QSizePolicy,
QSpacerItem,
QStatusBar,
QToolBar,
QToolButton,
QVBoxLayout,
QWidget,
Expand Down Expand Up @@ -199,7 +200,8 @@ def clear_error_status(self) -> None:

class LeftPane(QWidget):
"""
Represents the left side pane that contains user authentication actions and information.
Represents the left side pane that contains user authentication actions and, information,
and batch actions.
"""

def __init__(self) -> None:
Expand All @@ -215,14 +217,16 @@ def __init__(self) -> None:
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)

self.user_profile = UserProfile()
self.branding_barre = QLabel()
self.branding_barre.setPixmap(load_image("left_pane.svg"))
self.user_profile = UserProfile()

# Hide user profile widget until user logs in
self.user_profile.hide()

# Add widgets to layout
# Add widgets to layout. An improvement
# to this layout could be to set the branding barre as a
# background layout for the other elements
layout.addWidget(self.user_profile)
layout.addWidget(self.branding_barre)

Expand Down Expand Up @@ -424,6 +428,113 @@ def clear_message(self) -> None:
self._hide()


class InnerTopPane(QWidget):
"""
Top pane of the MainView window. This pane holds the Batch Action layout,
and eventually will hold the keyword search/filter by codename bar.
"""

def __init__(self) -> None:
super().__init__()
self.setObjectName("InnerTopPane")

# Use a vertical layout so that the keyword search bar can be added later
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
layout.setAlignment(Qt.AlignVCenter)
self.setLayout(layout)
self.setAttribute(Qt.WA_StyledBackground, True)

self.batch_actions = BatchActionWidget()
layout.addWidget(self.batch_actions)

def setup(self, controller: Controller) -> None:
self.batch_actions.setup(controller)


class BatchActionWidget(QWidget):
def __init__(self) -> None:
super().__init__()

# CSS style id
self.setObjectName("BatchActionWidget")

# Solid background colour
self.setAttribute(Qt.WA_StyledBackground, True)
layout = QHBoxLayout()
self.setLayout(layout)

self.toolbar = BatchActionToolbar()

layout.addWidget(self.toolbar)
layout.addStretch()

def setup(self, controller: Controller) -> None:
self.toolbar.setup(controller)


class BatchActionToolbar(QToolBar):
"""
A toolbar that contains batch actions (actions that target multiple
sources in the ConversationView, and therefore don't belong in the
individual conversation menu). Currently, this widget will hold the
"Delete Sources" (batch-delete) action.
For user-facing naming consistency, these items won't be called
"batch/bulk <delete>", but simply "<verb> <noun>s" (eg "Delete Sources"), where
the original nomenclature comes from the individual Source overflow QAction menu
items. Each item may have a tooltip, visible on hover, that provides a more
lengthy explanation (e.g., "Delete multiple source accounts").
"""

def __init__(self) -> None:
super().__init__()
self.setObjectName("BatchActionToolbar")
self.setContentsMargins(0, 0, 0, 0)

palette = QPalette()
palette.setBrush(
QPalette.Background, QBrush(Qt.NoBrush)
) # This makes the widget transparent
self.setPalette(palette)

# Style and attributes
self.setMovable(False)
self.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)

# TODO: Icon needs changing?
# TODO: set keyboard shortcut
batch_delete_action = QAction(
QIcon(load_image("delete_close.svg")), _("DELETE SOURCES"), self
)
batch_delete_action.setObjectName("BatchActionButton")
batch_delete_action.setToolTip(_("Delete selected source accounts"))
batch_delete_action.triggered.connect(self.delete_multiple_sources)

# TODO: Enhancement: start with action disabled via setEnabled(False),
# enable when sources are selected
self.addAction(batch_delete_action)

def delete_multiple_sources(self) -> None:
"""
Requires logged-in session. Delete currently-selected sources.
"""
logger.debug("delete_multiple_sources triggered")
if self.controller.api is None:
self.controller.on_action_requiring_login()
else:
# TODO rm: this is a placeholder
logger.debug("Delete sources clicked")
# TODO in followup PR
# An in-memory set of Sources selected by the user to be queued for deletion will
# live in the main app. If logged in, pass those to delete confirmation dialog,
# and if the user accepts the dialog, the sources will be deleted.

def setup(self, controller) -> None:
self.controller = controller


class UserProfile(QLabel):
"""
A widget that contains user profile information and options.
Expand Down Expand Up @@ -603,8 +714,8 @@ def _on_clicked(self) -> None:

class MainView(QWidget):
"""
Represents the main content of the application (containing the source list
and main context view).
Represents the main content of the application (containing the source list,
main context view, and top actions pane).
"""

def __init__(
Expand All @@ -620,12 +731,20 @@ def __init__(
self.setObjectName("MainView")

# Set layout
self._layout = QHBoxLayout(self)
self._layout = QVBoxLayout(self)
self._layout.setContentsMargins(0, 0, 0, 0)
self._layout.setSpacing(0)
self.setLayout(self._layout)

# Top pane layout (actions/eventual searchbar)
self.top_pane = InnerTopPane()

# Hold main conversation view and sourcelist
inner_container = QHBoxLayout(self)

# Set margins and spacing
self._layout.setContentsMargins(0, 0, 0, 0)
self._layout.setSpacing(0)
inner_container.setContentsMargins(0, 0, 0, 0)
inner_container.setSpacing(0)

# Create SourceList widget
self.source_list = SourceList()
Expand All @@ -647,8 +766,11 @@ def __init__(
self.view_layout.addWidget(self.empty_conversation_view)

# Add widgets to layout
self._layout.addWidget(self.source_list, stretch=1)
self._layout.addWidget(self.view_holder, stretch=2)
inner_container.addWidget(self.source_list, stretch=1)
inner_container.addWidget(self.view_holder, stretch=2)

self._layout.addWidget(self.top_pane)
self._layout.addLayout(inner_container, stretch=1)

# Note: We should not delete SourceConversationWrapper when its source is unselected. This
# is a temporary solution to keep copies of our objects since we do delete them.
Expand All @@ -660,6 +782,7 @@ def setup(self, controller: Controller) -> None:
"""
self.controller = controller
self.source_list.setup(controller)
self.top_pane.setup(controller)

def show_sources(self, sources: list[Source]) -> None:
"""
Expand Down
6 changes: 6 additions & 0 deletions client/securedrop_client/locale/messages.pot
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@ msgstr ""
msgid "Last Refresh: never"
msgstr ""

msgid "DELETE SOURCES"
msgstr ""

msgid "Delete selected source accounts"
msgstr ""

msgid "{}"
msgstr ""

Expand Down
33 changes: 33 additions & 0 deletions client/securedrop_client/resources/css/sdclient.css
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,39 @@ QWidget {
background-color: #85f6fe;
}

#InnerTopPane {
background-color: #f3f5f9;
}

#BatchActionWidget {
background-color: #fff;
min-height: 24px;
margin: 2px;
}

#BatchActionToolbar QToolButton {
font-family: 'Montserrat';
font-weight: 600;
font-size: 12px;
padding: 2px;
color: #2a319d;
background-color: #fff;
border: 2px solid #2a319d;
}

#BatchActionToolbar QToolButton:disabled {
color: #a5a8d8;
background-color: #9495b9;
border: 2px solid #a5a8d8;
}

#BatchActionToolbar QToolButton:hover,
#BatchActionToolbar QToolButton:checked {
background-color: #05a6fe;
border: 2px solid #05a6fe;
color: #fff;
}

#MainView {
min-height: 558;
}
Expand Down

0 comments on commit 1b0942d

Please sign in to comment.