Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create and place auth widget #278

Merged
merged 1 commit into from
Mar 26, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions securedrop_client/gui/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import logging
from PyQt5.QtWidgets import QMainWindow, QWidget, QHBoxLayout, QVBoxLayout, QDesktopWidget, \
QStatusBar
from PyQt5.QtWidgets import QMainWindow, QWidget, QHBoxLayout, QVBoxLayout, QDesktopWidget
from typing import List

from securedrop_client import __version__
from securedrop_client.db import Source
from securedrop_client.gui.widgets import ToolBar, MainView, LoginDialog, SourceConversationWrapper
from securedrop_client.gui.widgets import ToolBar, MainView, LoginDialog, StatusBar, \
SourceConversationWrapper
from securedrop_client.resources import load_icon

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -63,8 +63,7 @@ def __init__(self, sdc_home: str):
self.central_widget.setLayout(central_widget_layout)
self.setCentralWidget(self.central_widget)

self.status_bar = QStatusBar(self)
self.status_bar.setStyleSheet('background-color: #fff;')
self.status_bar = StatusBar()
central_widget_layout.addWidget(self.status_bar)

self.widget = QWidget()
Expand Down Expand Up @@ -98,7 +97,7 @@ def setup(self, controller):
"""
self.controller = controller # Reference the Client logic instance.
self.tool_bar.setup(self, controller)

self.status_bar.setup(controller)
self.set_status(_('Started SecureDrop Client. Please sign in.'), 20000)

self.login_dialog = LoginDialog(self)
Expand All @@ -120,6 +119,7 @@ def show_login(self):
self.login_dialog.setup(self.controller)
self.login_dialog.reset()
self.login_dialog.exec()
self.status_bar.show_refresh_icon()

def show_login_error(self, error):
"""
Expand All @@ -134,6 +134,7 @@ def hide_login(self):
"""
self.login_dialog.accept()
self.login_dialog = None
self.status_bar.hide_refresh_icon()

def update_error_status(self, error=None):
"""
Expand Down Expand Up @@ -168,6 +169,7 @@ def logout(self):
Update the UI to show the user is logged out.
"""
self.tool_bar.set_logged_out()
self.status_bar.hide_refresh_icon()

def on_source_changed(self):
"""
Expand Down Expand Up @@ -200,4 +202,4 @@ def set_status(self, message, duration=0):
Display a status message to the user. Optionally, supply a duration
(in milliseconds), the default will continuously show the message.
"""
self.status_bar.showMessage(message, duration)
self.status_bar.show_message(message, duration)
206 changes: 152 additions & 54 deletions securedrop_client/gui/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@
import arrow
import html
from PyQt5.QtCore import Qt, pyqtSlot
from PyQt5.QtGui import QIcon
from PyQt5.QtGui import QIcon, QPalette, QBrush, QColor, QFont
from PyQt5.QtWidgets import QListWidget, QLabel, QWidget, QListWidgetItem, QHBoxLayout, \
QPushButton, QVBoxLayout, QLineEdit, QScrollArea, QDialog, QAction, QMenu, QMessageBox, \
QToolButton, QSizePolicy, QTextEdit
QToolButton, QSizePolicy, QTextEdit, QStatusBar
from typing import List
from uuid import uuid4

Expand All @@ -35,48 +35,114 @@
logger = logging.getLogger(__name__)


class StatusBar(QStatusBar):
def __init__(self):
super().__init__()

self.setStyleSheet('''
QStatusBar { background-color: #fff; }
QStatusBar::item { border: none; }
QPushButton { border: none; }
''')

self.refresh = QPushButton()
self.refresh.clicked.connect(self.on_refresh_clicked)
self.refresh.setMaximumSize(30, 30)
refresh_pixmap = load_image('refresh.svg')
self.refresh.setIcon(QIcon(refresh_pixmap))
self.addPermanentWidget(self.refresh) # widget may not be obscured by temporary messages

def setup(self, controller):
"""
Assign a controller object (containing the application logic).
"""
self.controller = controller
self.controller.sync_events.connect(self._on_sync_event)

def on_refresh_clicked(self):
"""
Called when the refresh button is clicked.
"""
self.controller.sync_api()

def _on_sync_event(self, data):
"""
Called when the refresh call completes
"""
self.refresh.setEnabled(data != 'syncing')

def show_message(self, message, duration=0):
"""
Display a status message to the user. Optionally, supply a duration
(in milliseconds), the default will continuously show the message.
"""
self.showMessage(message, duration)

def hide_refresh_icon(self):
"""
Hide refresh icon.
"""
self.refresh.hide()

def show_refresh_icon(self):
"""
Show refresh icon.
"""
self.refresh.show()


class ToolBar(QWidget):
"""
Represents the tool bar across the top of the user interface.
"""

def __init__(self, parent: QWidget):
super().__init__(parent)
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)

self.user_state = QLabel(_('Signed out.'))

self.login = QPushButton(_('Sign in'))
self.login.setMaximumSize(80, 30)
layout = QVBoxLayout(self)
layout.setContentsMargins(20, 10, 20, 10)

self.setAutoFillBackground(True)
palette = QPalette()
palette.setBrush(QPalette.Background, QBrush(load_image('hexes.svg')))
self.setPalette(palette)
self.user_icon = QLabel()
self.user_icon.setFont(QFont("Helvetica [Cronyx]", 16, QFont.Bold))
self.user_icon.hide()
self.user_state = QLabel()
self.user_state.setFont(QFont("Helvetica [Cronyx]", 12, QFont.Bold))
self.user_state.hide()
self.user_menu = JournalistMenuButton(self)
self.user_menu.hide()

self.login = QPushButton(_('SIGN IN'))
self.login.setFont(QFont("Helvetica [Cronyx]", 10))
self.login.setMinimumSize(200, 40)
button_palette = self.login.palette()
button_palette.setColor(QPalette.Button, QColor('#eee'))
button_palette.setColor(QPalette.ButtonText, QColor('#000'))
self.login.setAutoFillBackground(True)
self.login.setPalette(button_palette)
self.login.update()
self.login.clicked.connect(self.on_login_clicked)

self.logout = QPushButton(_('Sign out'))
self.logout.clicked.connect(self.on_logout_clicked)
self.logout.setMaximumSize(80, 30)
self.logout.setVisible(False)

self.refresh = QPushButton()
self.refresh.clicked.connect(self.on_refresh_clicked)
self.refresh.setMaximumSize(30, 30)
refresh_pixmap = load_image('refresh.svg')
spacer = QWidget()
spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)

self.refresh.setIcon(QIcon(refresh_pixmap))
self.refresh.show()
logo = QLabel()
logo.setMinimumSize(200, 200)
logo.setPixmap(load_image('logo.png'))

self.logo = QLabel()
self.logo.setPixmap(load_image('icon.png'))
self.logo.setMinimumSize(200, 200)
user_layout = QHBoxLayout()
user_layout.addWidget(self.user_icon, 5, Qt.AlignLeft)
user_layout.addWidget(self.user_state, 5, Qt.AlignLeft)
user_layout.addWidget(self.login, 5, Qt.AlignLeft)
user_layout.addWidget(self.user_menu, 5, Qt.AlignLeft)
user_layout.addStretch()

journalist_layout = QHBoxLayout()
journalist_layout.addWidget(self.refresh, 1)
journalist_layout.addWidget(self.user_state, 5)
journalist_layout.addWidget(self.login, 5)
journalist_layout.addWidget(self.logout, 5)
journalist_layout.addStretch()

layout.addLayout(journalist_layout)
layout.addWidget(self.logo)
layout.addLayout(user_layout)
layout.addWidget(spacer, 5)
layout.addWidget(logo, 3, Qt.AlignCenter)
layout.addStretch()

def setup(self, window, controller):
Expand All @@ -90,25 +156,31 @@ def setup(self, window, controller):
self.window = window
self.controller = controller

self.controller.sync_events.connect(self._on_sync_event)

def set_logged_in_as(self, username):
"""
Update the UI to reflect that the user is logged in as "username".
"""
self.user_state.setText(html.escape(username))
self.login.setVisible(False)
self.logout.setVisible(True)
self.refresh.setVisible(True)
self.login.hide()

self.user_icon.setText(_('jo'))
self.user_icon.setStyleSheet('''
QLabel { background-color: #045fb4; color: cyan; padding: 10; border: 1px solid gray; }
''')
self.user_icon.show()

self.user_state.setText(_('{}').format(html.escape(username)))
self.user_state.show()

self.user_menu.show()

def set_logged_out(self):
"""
Update the UI to a logged out state.
"""
self.user_state.setText(_('Signed out.'))
self.login.setVisible(True)
self.logout.setVisible(False)
self.refresh.setVisible(False)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think when we log out we want the refresh button to be hidden no?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@creviera asked me about where the Offline icon was, yesterday—did you want to put that in on a separate PR or were you waiting for me to respond to your Slack, Allie? All icons are in table on this Issue: freedomofpress/securedrop-ux#17

Copy link
Contributor Author

@sssoleileraaa sssoleileraaa Mar 22, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah that's right! The new offline icon will be in the pr for this issue: #279. As I understand it, there will be three icons which are in this inventory list: freedomofpress/securedrop-ux#17. There you'll see an icon labeled "offline", the static refresh icon, and the active refresh icon.

@redshiftzero - Realizing now that when I moved the refresh icon to the StatusBar class, I didn't move this check, so yeah I didn't catch that I changed the behavior. Good catch. However, I'm working on a follow-up PR to change and test the new behavior of refresh, so if you're cool with leaving this as is for now, I'm cool with it. 😎

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We discussed this and decided that since the refresh icon appearing when not logged in is a regression it would be good to resolve as part of this PR

self.login.show()
self.user_icon.hide()
self.user_state.hide()
self.user_menu.hide()

def on_login_clicked(self):
"""
Expand All @@ -122,18 +194,6 @@ def on_logout_clicked(self):
"""
self.controller.logout()

def on_refresh_clicked(self):
"""
Called when the refresh button is clicked.
"""
self.controller.sync_api()

def _on_sync_event(self, data):
"""
Called when the refresh call completes
"""
self.refresh.setEnabled(data != 'syncing')


class MainView(QWidget):
"""
Expand All @@ -158,6 +218,7 @@ def __init__(self, parent):
self.error_status = QLabel('')
self.error_status.setObjectName('error_label')
left_layout.addWidget(self.error_status)
self.error_status.hide()

self.layout.addWidget(left_column, 4)

Expand Down Expand Up @@ -893,6 +954,43 @@ def trigger(self):
self.messagebox.launch()


class JournalistMenu(QMenu):
"""A menu next to the journalist username.

A menu that provides login options.
"""

def __init__(self, parent):
super().__init__()
self.logout = QAction(_('SIGN OUT'))
self.logout.setFont(QFont("Helvetica [Cronyx]", 10))
self.addAction(self.logout)
self.logout.triggered.connect(parent.on_logout_clicked)


class JournalistMenuButton(QToolButton):
"""An menu button for the journalist menu

This button is responsible for launching the journalist menu on click.
"""

def __init__(self, parent):
super().__init__()

self.setStyleSheet('''
QToolButton::menu-indicator { image: none; }
QToolButton { border: none; }
''')
arrow = load_image("dropdown_arrow.svg")
self.setIcon(QIcon(arrow))
self.setMinimumSize(20, 20)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it me or do these pixmaps look kind of blocky? (just for the dropdown SVG and the refresh SVG)

Screen Shot 2019-03-21 at 4 42 30 PM

note that it's possible this is a macOS only thing - let me know if so and we can disregard since we're targeting Linux.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blarg—is that crappy art on my part, @creviera? If so, happy to re-rip another one... 🤢

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ninavizz - Some higher fidelity images would be great! I'm currently using the svgs provided in the inventory list.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ninavizz and @redshiftzero - In a follow-up PR I was able to improve the look of the refresh icon by changing this (which looked pixely/blocky):

        refresh_pixmap = load_image('refresh.svg')
        self.refresh.setIcon(QIcon(refresh_pixmap))

To the following code:

        pixmap = load_image('refresh.svg')
        scaled = pixmap.scaled(18, 18, Qt.KeepAspectRatio, transformMode=Qt.SmoothTransformation)
        icon.addPixmap(scaled, QIcon.Normal)

But I think I need to do more research into how to work with SVGs and QIcons. I may need to create a QSvgWidget which are used to display the contents of SVG files: https://doc.qt.io/archives/qt-5.10/qtsvg-module.html


self.menu = JournalistMenu(parent)
self.setMenu(self.menu)

self.setPopupMode(QToolButton.InstantPopup)


class SourceMenu(QMenu):
"""Renders menu having various operations.

Expand Down Expand Up @@ -921,7 +1019,7 @@ def __init__(self, source, controller):
class SourceMenuButton(QToolButton):
"""An ellipse based source menu button.

This button is responsible for launching menu on click.
This button is responsible for launching the source menu on click.
"""

def __init__(self, source, controller):
Expand Down
11 changes: 11 additions & 0 deletions securedrop_client/resources/images/dropdown_arrow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading