diff options
author | Miguel Jacq <mig@mig5.net> | 2018-02-25 08:27:46 +1100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-02-25 08:27:46 +1100 |
commit | dface51dd0e00e4316d5384d14fe77568e51bf41 (patch) | |
tree | 38400a42f9d8db3cc19eecacaf046a2f76dcd9c2 /onionshare_gui | |
parent | 6b7a6a9ec49ea9b33f9046526e8284f665eb6369 (diff) | |
parent | 560f27fc76f8b33c1d6e62c02db181b0c6ce8079 (diff) | |
download | onionshare-dface51dd0e00e4316d5384d14fe77568e51bf41.tar.gz onionshare-dface51dd0e00e4316d5384d14fe77568e51bf41.zip |
Merge pull request #588 from micahflee/ux-update
Major user experience update
Diffstat (limited to 'onionshare_gui')
-rw-r--r-- | onionshare_gui/downloads.py | 8 | ||||
-rw-r--r-- | onionshare_gui/file_selection.py | 238 | ||||
-rw-r--r-- | onionshare_gui/onionshare_gui.py | 291 | ||||
-rw-r--r-- | onionshare_gui/server_status.py | 208 | ||||
-rw-r--r-- | onionshare_gui/settings_dialog.py | 18 |
5 files changed, 566 insertions, 197 deletions
diff --git a/onionshare_gui/downloads.py b/onionshare_gui/downloads.py index 60bd59ac..166f14a4 100644 --- a/onionshare_gui/downloads.py +++ b/onionshare_gui/downloads.py @@ -34,13 +34,15 @@ class Download(object): # make a new progress bar cssStyleData =""" QProgressBar { - border: 2px solid grey; - border-radius: 5px; + border: 1px solid #4e064f; + background-color: #ffffff !important; text-align: center; + color: #9b9b9b; + font-size: 12px; } QProgressBar::chunk { - background: qlineargradient(x1: 0.5, y1: 0, x2: 0.5, y2: 1, stop: 0 #b366ff, stop: 1 #d9b3ff); + background-color: #4e064f; width: 10px; }""" self.progress_bar = QtWidgets.QProgressBar() diff --git a/onionshare_gui/file_selection.py b/onionshare_gui/file_selection.py index da03d24d..29bcc592 100644 --- a/onionshare_gui/file_selection.py +++ b/onionshare_gui/file_selection.py @@ -23,6 +23,50 @@ from .alert import Alert from onionshare import strings, common +class DropHereLabel(QtWidgets.QLabel): + """ + When there are no files or folders in the FileList yet, display the + 'drop files here' message and graphic. + """ + def __init__(self, parent, image=False): + self.parent = parent + super(DropHereLabel, self).__init__(parent=parent) + self.setAcceptDrops(True) + self.setAlignment(QtCore.Qt.AlignCenter) + + if image: + self.setPixmap(QtGui.QPixmap.fromImage(QtGui.QImage(common.get_resource_path('images/logo_transparent.png')))) + else: + self.setText(strings._('gui_drag_and_drop', True)) + self.setStyleSheet('color: #999999;') + + self.hide() + + def dragEnterEvent(self, event): + self.parent.drop_here_image.hide() + self.parent.drop_here_text.hide() + event.accept() + + +class DropCountLabel(QtWidgets.QLabel): + """ + While dragging files over the FileList, this counter displays the + number of files you're dragging. + """ + def __init__(self, parent): + self.parent = parent + super(DropCountLabel, self).__init__(parent=parent) + self.setAcceptDrops(True) + self.setAlignment(QtCore.Qt.AlignCenter) + self.setText(strings._('gui_drag_and_drop', True)) + self.setStyleSheet('color: #ffffff; background-color: #f44449; font-weight: bold; padding: 5px 10px; border-radius: 10px;') + self.hide() + + def dragEnterEvent(self, event): + self.hide() + event.accept() + + class FileList(QtWidgets.QListWidget): """ The list of files and folders in the GUI. @@ -35,63 +79,82 @@ class FileList(QtWidgets.QListWidget): self.setAcceptDrops(True) self.setIconSize(QtCore.QSize(32, 32)) self.setSortingEnabled(True) - self.setMinimumHeight(200) + self.setMinimumHeight(205) self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) - - class DropHereLabel(QtWidgets.QLabel): - """ - When there are no files or folders in the FileList yet, display the - 'drop files here' message and graphic. - """ - def __init__(self, parent, image=False): - self.parent = parent - super(DropHereLabel, self).__init__(parent=parent) - self.setAcceptDrops(True) - self.setAlignment(QtCore.Qt.AlignCenter) - - if image: - self.setPixmap(QtGui.QPixmap.fromImage(QtGui.QImage(common.get_resource_path('images/drop_files.png')))) - else: - self.setText(strings._('gui_drag_and_drop', True)) - self.setStyleSheet('color: #999999;') - - self.hide() - - def dragEnterEvent(self, event): - self.parent.drop_here_image.hide() - self.parent.drop_here_text.hide() - event.ignore() - self.drop_here_image = DropHereLabel(self, True) self.drop_here_text = DropHereLabel(self, False) - - self.filenames = [] - self.update() + self.drop_count = DropCountLabel(self) + self.resizeEvent(None) + self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) def update(self): """ Update the GUI elements based on the current state. """ # file list should have a background image if empty - if len(self.filenames) == 0: + if self.count() == 0: self.drop_here_image.show() self.drop_here_text.show() else: self.drop_here_image.hide() self.drop_here_text.hide() + def server_started(self): + """ + Update the GUI when the server starts, by hiding delete buttons. + """ + self.setAcceptDrops(False) + self.setCurrentItem(None) + self.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) + for index in range(self.count()): + self.item(index).item_button.hide() + + def server_stopped(self): + """ + Update the GUI when the server stops, by showing delete buttons. + """ + self.setAcceptDrops(True) + self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) + for index in range(self.count()): + self.item(index).item_button.show() + def resizeEvent(self, event): """ When the widget is resized, resize the drop files image and text. """ - self.drop_here_image.setGeometry(0, 0, self.width(), self.height()) - self.drop_here_text.setGeometry(0, 0, self.width(), self.height()) + offset = 70 + self.drop_here_image.setGeometry(0, 0, self.width(), self.height() - offset) + self.drop_here_text.setGeometry(0, offset, self.width(), self.height() - offset) + + if self.count() > 0: + # Add and delete an empty item, to force all items to get redrawn + # This is ugly, but the only way I could figure out how to proceed + item = QtWidgets.QListWidgetItem('fake item') + self.addItem(item) + self.takeItem(self.row(item)) + self.update() + + # Extend any filenames that were truncated to fit the window + # We use 200 as a rough guess at how wide the 'file size + delete button' widget is + # and extend based on the overall width minus that amount. + for index in range(self.count()): + metrics = QtGui.QFontMetrics(self.item(index).font()) + elided = metrics.elidedText(self.item(index).basename, QtCore.Qt.ElideRight, self.width() - 200) + self.item(index).setText(elided) + def dragEnterEvent(self, event): """ dragEnterEvent for dragging files and directories into the widget. """ if event.mimeData().hasUrls: + self.setStyleSheet('FileList { border: 3px solid #538ad0; }') + count = len(event.mimeData().urls()) + self.drop_count.setText('+{}'.format(count)) + + size_hint = self.drop_count.sizeHint() + self.drop_count.setGeometry(self.width() - size_hint.width() - 10, self.height() - size_hint.height() - 10, size_hint.width(), size_hint.height()) + self.drop_count.show() event.accept() else: event.ignore() @@ -100,6 +163,8 @@ class FileList(QtWidgets.QListWidget): """ dragLeaveEvent for dragging files and directories into the widget. """ + self.setStyleSheet('FileList { border: none; }') + self.drop_count.hide() event.accept() self.update() @@ -125,36 +190,84 @@ class FileList(QtWidgets.QListWidget): self.add_file(filename) else: event.ignore() + + self.setStyleSheet('border: none;') + self.drop_count.hide() + self.files_dropped.emit() def add_file(self, filename): """ Add a file or directory to this widget. """ - if filename not in self.filenames: + filenames = [] + for index in range(self.count()): + filenames.append(self.item(index).filename) + + if filename not in filenames: if not os.access(filename, os.R_OK): Alert(strings._("not_a_readable_file", True).format(filename)) return - self.filenames.append(filename) - # Re-sort the list internally - self.filenames.sort() - fileinfo = QtCore.QFileInfo(filename) - basename = os.path.basename(filename.rstrip('/')) ip = QtWidgets.QFileIconProvider() icon = ip.icon(fileinfo) if os.path.isfile(filename): - size = common.human_readable_filesize(fileinfo.size()) + size_bytes = fileinfo.size() + size_readable = common.human_readable_filesize(size_bytes) else: - size = common.human_readable_filesize(common.dir_size(filename)) - item_name = '{0:s} ({1:s})'.format(basename, size) - item = QtWidgets.QListWidgetItem(item_name) - item.setToolTip(size) + size_bytes = common.dir_size(filename) + size_readable = common.human_readable_filesize(size_bytes) + # Create a new item + item = QtWidgets.QListWidgetItem() item.setIcon(icon) + item.size_bytes = size_bytes + + # Item's filename attribute and size labels + item.filename = filename + item_size = QtWidgets.QLabel(size_readable) + item_size.setStyleSheet('QLabel { color: #666666; font-size: 11px; }') + + item.basename = os.path.basename(filename.rstrip('/')) + # Use the basename as the method with which to sort the list + metrics = QtGui.QFontMetrics(item.font()) + elided = metrics.elidedText(item.basename, QtCore.Qt.ElideRight, self.sizeHint().width()) + item.setData(QtCore.Qt.DisplayRole, elided) + + # Item's delete button + def delete_item(): + itemrow = self.row(item) + self.takeItem(itemrow) + self.files_updated.emit() + + item.item_button = QtWidgets.QPushButton() + item.item_button.setDefault(False) + item.item_button.setFlat(True) + item.item_button.setIcon( QtGui.QIcon(common.get_resource_path('images/file_delete.png')) ) + item.item_button.clicked.connect(delete_item) + item.item_button.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + + # Item info widget, with a white background + item_info_layout = QtWidgets.QHBoxLayout() + item_info_layout.addWidget(item_size) + item_info_layout.addWidget(item.item_button) + item_info = QtWidgets.QWidget() + item_info.setObjectName('item-info') + item_info.setLayout(item_info_layout) + + # Create the item's widget and layouts + item_hlayout = QtWidgets.QHBoxLayout() + item_hlayout.addStretch() + item_hlayout.addWidget(item_info) + widget = QtWidgets.QWidget() + widget.setLayout(item_hlayout) + + item.setSizeHint(widget.sizeHint()) + self.addItem(item) + self.setItemWidget(item, widget) self.files_updated.emit() @@ -168,21 +281,23 @@ class FileSelection(QtWidgets.QVBoxLayout): super(FileSelection, self).__init__() self.server_on = False - # file list + # File list self.file_list = FileList() - self.file_list.currentItemChanged.connect(self.update) + self.file_list.itemSelectionChanged.connect(self.update) self.file_list.files_dropped.connect(self.update) + self.file_list.files_updated.connect(self.update) - # buttons + # Buttons self.add_button = QtWidgets.QPushButton(strings._('gui_add', True)) self.add_button.clicked.connect(self.add) self.delete_button = QtWidgets.QPushButton(strings._('gui_delete', True)) self.delete_button.clicked.connect(self.delete) button_layout = QtWidgets.QHBoxLayout() + button_layout.addStretch() button_layout.addWidget(self.add_button) button_layout.addWidget(self.delete_button) - # add the widgets + # Add the widgets self.addWidget(self.file_list) self.addLayout(button_layout) @@ -192,21 +307,20 @@ class FileSelection(QtWidgets.QVBoxLayout): """ Update the GUI elements based on the current state. """ - # all buttons should be disabled if the server is on + # All buttons should be hidden if the server is on if self.server_on: - self.add_button.setEnabled(False) - self.delete_button.setEnabled(False) + self.add_button.hide() + self.delete_button.hide() else: - self.add_button.setEnabled(True) + self.add_button.show() - # delete button should be disabled if item isn't selected - current_item = self.file_list.currentItem() - if not current_item: - self.delete_button.setEnabled(False) + # Delete button should be hidden if item isn't selected + if len(self.file_list.selectedItems()) == 0: + self.delete_button.hide() else: - self.delete_button.setEnabled(True) + self.delete_button.show() - # update the file list + # Update the file list self.file_list.update() def add(self): @@ -218,6 +332,7 @@ class FileSelection(QtWidgets.QVBoxLayout): for filename in file_dialog.selectedFiles(): self.file_list.add_file(filename) + self.file_list.setCurrentItem(None) self.update() def delete(self): @@ -227,9 +342,10 @@ class FileSelection(QtWidgets.QVBoxLayout): selected = self.file_list.selectedItems() for item in selected: itemrow = self.file_list.row(item) - self.file_list.filenames.pop(itemrow) self.file_list.takeItem(itemrow) self.file_list.files_updated.emit() + + self.file_list.setCurrentItem(None) self.update() def server_started(self): @@ -237,7 +353,7 @@ class FileSelection(QtWidgets.QVBoxLayout): Gets called when the server starts. """ self.server_on = True - self.file_list.setAcceptDrops(False) + self.file_list.server_started() self.update() def server_stopped(self): @@ -245,14 +361,14 @@ class FileSelection(QtWidgets.QVBoxLayout): Gets called when the server stops. """ self.server_on = False - self.file_list.setAcceptDrops(True) + self.file_list.server_stopped() self.update() def get_num_files(self): """ Returns the total number of files and folders in the list. """ - return len(self.file_list.filenames) + return len(range(self.file_list.count())) def setFocus(self): """ diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index 582ebdb3..e6987cfb 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -56,6 +56,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.setWindowTitle('OnionShare') self.setWindowIcon(QtGui.QIcon(common.get_resource_path('images/logo.png'))) + self.setMinimumWidth(430) # Load settings self.config = config @@ -72,20 +73,31 @@ class OnionShareGui(QtWidgets.QMainWindow): self.server_status = ServerStatus(self.qtapp, self.app, web, self.file_selection, self.settings) self.server_status.server_started.connect(self.file_selection.server_started) self.server_status.server_started.connect(self.start_server) + self.server_status.server_started.connect(self.update_server_status_indicator) self.server_status.server_stopped.connect(self.file_selection.server_stopped) self.server_status.server_stopped.connect(self.stop_server) + self.server_status.server_stopped.connect(self.update_server_status_indicator) + self.server_status.server_stopped.connect(self.update_primary_action) + self.server_status.server_canceled.connect(self.cancel_server) + self.server_status.server_canceled.connect(self.file_selection.server_stopped) + self.server_status.server_canceled.connect(self.update_primary_action) self.start_server_finished.connect(self.clear_message) self.start_server_finished.connect(self.server_status.start_server_finished) + self.start_server_finished.connect(self.update_server_status_indicator) self.stop_server_finished.connect(self.server_status.stop_server_finished) + self.stop_server_finished.connect(self.update_server_status_indicator) self.file_selection.file_list.files_updated.connect(self.server_status.update) + self.file_selection.file_list.files_updated.connect(self.update_primary_action) self.server_status.url_copied.connect(self.copy_url) self.server_status.hidservauth_copied.connect(self.copy_hidservauth) self.starting_server_step2.connect(self.start_server_step2) self.starting_server_step3.connect(self.start_server_step3) self.starting_server_error.connect(self.start_server_error) + self.server_status.button_clicked.connect(self.clear_message) # Filesize warning self.filesize_warning = QtWidgets.QLabel() + self.filesize_warning.setWordWrap(True) self.filesize_warning.setStyleSheet('padding: 10px 0; font-weight: bold; color: #333333;') self.filesize_warning.hide() @@ -99,38 +111,95 @@ class OnionShareGui(QtWidgets.QMainWindow): self.vbar = self.downloads_container.verticalScrollBar() self.downloads_container.hide() # downloads start out hidden self.new_download = False + self.downloads_in_progress = 0 + self.downloads_completed = 0 - # Status bar - self.status_bar = QtWidgets.QStatusBar() - self.status_bar.setSizeGripEnabled(False) - self.status_bar.setStyleSheet( - "QStatusBar::item { border: 0px; }") - version_label = QtWidgets.QLabel('v{0:s}'.format(common.get_version())) - version_label.setStyleSheet('color: #666666') + # Info label along top of screen + self.info_layout = QtWidgets.QHBoxLayout() + self.info_label = QtWidgets.QLabel() + self.info_label.setStyleSheet('QLabel { font-size: 12px; color: #666666; }') + + self.info_in_progress_download_count = QtWidgets.QLabel() + self.info_in_progress_download_count.setStyleSheet('QLabel { font-size: 12px; color: #666666; }') + + self.info_completed_downloads_count = QtWidgets.QLabel() + self.info_completed_downloads_count.setStyleSheet('QLabel { font-size: 12px; color: #666666; }') + + self.update_downloads_completed(self.downloads_in_progress) + self.update_downloads_in_progress(self.downloads_in_progress) + + self.info_layout.addWidget(self.info_label) + self.info_layout.addStretch() + self.info_layout.addWidget(self.info_in_progress_download_count) + self.info_layout.addWidget(self.info_completed_downloads_count) + + self.info_widget = QtWidgets.QWidget() + self.info_widget.setLayout(self.info_layout) + self.info_widget.hide() + + # Settings button on the status bar self.settings_button = QtWidgets.QPushButton() self.settings_button.setDefault(False) self.settings_button.setFlat(True) + self.settings_button.setFixedWidth(40) self.settings_button.setIcon( QtGui.QIcon(common.get_resource_path('images/settings.png')) ) self.settings_button.clicked.connect(self.open_settings) - self.status_bar.addPermanentWidget(version_label) + + # Server status indicator on the status bar + self.server_status_image_stopped = QtGui.QImage(common.get_resource_path('images/server_stopped.png')) + self.server_status_image_working = QtGui.QImage(common.get_resource_path('images/server_working.png')) + self.server_status_image_started = QtGui.QImage(common.get_resource_path('images/server_started.png')) + self.server_status_image_label = QtWidgets.QLabel() + self.server_status_image_label.setFixedWidth(20) + self.server_status_label = QtWidgets.QLabel() + self.server_status_label.setStyleSheet('QLabel { font-style: italic; color: #666666; }') + server_status_indicator_layout = QtWidgets.QHBoxLayout() + server_status_indicator_layout.addWidget(self.server_status_image_label) + server_status_indicator_layout.addWidget(self.server_status_label) + self.server_status_indicator = QtWidgets.QWidget() + self.server_status_indicator.setLayout(server_status_indicator_layout) + self.update_server_status_indicator() + + # Status bar + self.status_bar = QtWidgets.QStatusBar() + self.status_bar.setSizeGripEnabled(False) + statusBar_cssStyleData =""" + QStatusBar { + font-style: italic; + color: #666666; + } + + QStatusBar::item { + border: 0px; + }""" + + self.status_bar.setStyleSheet(statusBar_cssStyleData) + self.status_bar.addPermanentWidget(self.server_status_indicator) self.status_bar.addPermanentWidget(self.settings_button) self.setStatusBar(self.status_bar) # Status bar, zip progress bar self._zip_progress_bar = None - - # Persistent URL notification - self.persistent_url_label = QtWidgets.QLabel(strings._('persistent_url_in_use', True)) - self.persistent_url_label.setStyleSheet('padding: 10px 0; font-weight: bold; color: #333333;') - self.persistent_url_label.hide() + # Status bar, sharing messages + self.server_share_status_label = QtWidgets.QLabel('') + self.server_share_status_label.setStyleSheet('QLabel { font-style: italic; color: #666666; padding: 2px; }') + self.status_bar.insertWidget(0, self.server_share_status_label) + + # Primary action layout + primary_action_layout = QtWidgets.QVBoxLayout() + primary_action_layout.addWidget(self.server_status) + primary_action_layout.addWidget(self.filesize_warning) + primary_action_layout.addWidget(self.downloads_container) + self.primary_action = QtWidgets.QWidget() + self.primary_action.setLayout(primary_action_layout) + self.primary_action.hide() + self.update_primary_action() # Main layout self.layout = QtWidgets.QVBoxLayout() + self.layout.addWidget(self.info_widget) self.layout.addLayout(self.file_selection) - self.layout.addLayout(self.server_status) - self.layout.addWidget(self.filesize_warning) - self.layout.addWidget(self.persistent_url_label) - self.layout.addWidget(self.downloads_container) + self.layout.addWidget(self.primary_action) central_widget = QtWidgets.QWidget() central_widget.setLayout(self.layout) self.setCentralWidget(central_widget) @@ -158,6 +227,46 @@ class OnionShareGui(QtWidgets.QMainWindow): # After connecting to Tor, check for updates self.check_for_updates() + def update_primary_action(self): + # Show or hide primary action layout + file_count = self.file_selection.file_list.count() + if file_count > 0: + self.primary_action.show() + self.info_widget.show() + + # Update the file count in the info label + total_size_bytes = 0 + for index in range(self.file_selection.file_list.count()): + item = self.file_selection.file_list.item(index) + total_size_bytes += item.size_bytes + total_size_readable = common.human_readable_filesize(total_size_bytes) + + if file_count > 1: + self.info_label.setText(strings._('gui_file_info', True).format(file_count, total_size_readable)) + else: + self.info_label.setText(strings._('gui_file_info_single', True).format(file_count, total_size_readable)) + + else: + self.primary_action.hide() + self.info_widget.hide() + + # Resize window + self.adjustSize() + + def update_server_status_indicator(self): + common.log('OnionShareGui', 'update_server_status_indicator') + + # Set the status image + if self.server_status.status == self.server_status.STATUS_STOPPED: + self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_stopped)) + self.server_status_label.setText(strings._('gui_status_indicator_stopped', True)) + elif self.server_status.status == self.server_status.STATUS_WORKING: + self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_working)) + self.server_status_label.setText(strings._('gui_status_indicator_working', True)) + elif self.server_status.status == self.server_status.STATUS_STARTED: + self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_started)) + self.server_status_label.setText(strings._('gui_status_indicator_started', True)) + def _initSystemTray(self): system = common.get_platform() @@ -238,11 +347,17 @@ class OnionShareGui(QtWidgets.QMainWindow): if self.server_status.file_selection.get_num_files() > 0: self.server_status.server_button.setEnabled(True) self.status_bar.clearMessage() + # If we switched off the shutdown timeout setting, ensure the widget is hidden. + if not self.settings.get('shutdown_timeout'): + self.server_status.shutdown_timeout_container.hide() d = SettingsDialog(self.onion, self.qtapp, self.config) d.settings_saved.connect(reload_settings) d.exec_() + # When settings close, refresh the server status UI + self.server_status.update() + def start_server(self): """ Start the onionshare server. This uses multiple threads to start the Tor onion @@ -257,7 +372,9 @@ class OnionShareGui(QtWidgets.QMainWindow): # Hide and reset the downloads if we have previously shared self.downloads_container.hide() self.downloads.reset_downloads() + self.reset_info_counters() self.status_bar.clearMessage() + self.server_share_status_label.setText('') # Reset web counters web.download_count = 0 @@ -284,9 +401,10 @@ class OnionShareGui(QtWidgets.QMainWindow): # wait for modules in thread to load, preventing a thread-related cx_Freeze crash time.sleep(0.2) - t = threading.Thread(target=start_onion_service, kwargs={'self': self}) - t.daemon = True - t.start() + common.log('OnionshareGui', 'start_server', 'Starting an onion thread') + self.t = OnionThread(function=start_onion_service, kwargs={'self': self}) + self.t.daemon = True + self.t.start() def start_server_step2(self): """ @@ -296,8 +414,11 @@ class OnionShareGui(QtWidgets.QMainWindow): # add progress bar to the status bar, indicating the crunching of files. self._zip_progress_bar = ZipProgressBar(0) - self._zip_progress_bar.total_files_size = OnionShareGui._compute_total_size( - self.file_selection.file_list.filenames) + self.filenames = [] + for index in range(self.file_selection.file_list.count()): + self.filenames.append(self.file_selection.file_list.item(index).filename) + + self._zip_progress_bar.total_files_size = OnionShareGui._compute_total_size(self.filenames) self.status_bar.insertWidget(0, self._zip_progress_bar) # prepare the files for sending in a new thread @@ -307,7 +428,7 @@ class OnionShareGui(QtWidgets.QMainWindow): if self._zip_progress_bar != None: self._zip_progress_bar.update_processed_size_signal.emit(x) try: - web.set_file_info(self.file_selection.file_list.filenames, processed_size_callback=_set_processed_size) + web.set_file_info(self.filenames, processed_size_callback=_set_processed_size) self.app.cleanup_filenames.append(web.zip_filename) self.starting_server_step3.emit() @@ -317,7 +438,6 @@ class OnionShareGui(QtWidgets.QMainWindow): self.starting_server_error.emit(e.strerror) return - #self.status_bar.showMessage(strings._('gui_starting_server2', True)) t = threading.Thread(target=finish_starting_server, kwargs={'self': self}) t.daemon = True t.start() @@ -339,7 +459,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.filesize_warning.setText(strings._("large_filesize", True)) self.filesize_warning.show() - if self.server_status.timer_enabled: + if self.settings.get('shutdown_timeout'): # Convert the date value to seconds between now and then now = QtCore.QDateTime.currentDateTime() self.timeout = now.secsTo(self.server_status.timeout) @@ -352,9 +472,6 @@ class OnionShareGui(QtWidgets.QMainWindow): self.stop_server() self.start_server_error(strings._('gui_server_started_after_timeout')) - if self.settings.get('save_private_key'): - self.persistent_url_label.show() - def start_server_error(self, error): """ If there's an error when trying to start the onion service @@ -370,6 +487,14 @@ class OnionShareGui(QtWidgets.QMainWindow): self._zip_progress_bar = None self.status_bar.clearMessage() + def cancel_server(self): + """ + Cancel the server while it is preparing to start + """ + if self.t: + self.t.terminate() + self.stop_server() + def stop_server(self): """ Stop the onionshare server. @@ -386,10 +511,13 @@ class OnionShareGui(QtWidgets.QMainWindow): # Remove ephemeral service, but don't disconnect from Tor self.onion.cleanup(stop_tor=False) self.filesize_warning.hide() - self.persistent_url_label.hide() - self.stop_server_finished.emit() + self.downloads_in_progress = 0 + self.downloads_completed = 0 + self.update_downloads_in_progress(0) + self.file_selection.file_list.adjustSize() self.set_server_active(False) + self.stop_server_finished.emit() def check_for_updates(self): """ @@ -455,6 +583,8 @@ class OnionShareGui(QtWidgets.QMainWindow): self.downloads_container.show() # show the downloads layout self.downloads.add_download(event["data"]["id"], web.zip_filesize) self.new_download = True + self.downloads_in_progress += 1 + self.update_downloads_in_progress(self.downloads_in_progress) if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'): self.systemTray.showMessage(strings._('systray_download_started_title', True), strings._('systray_download_started_message', True)) @@ -469,16 +599,30 @@ class OnionShareGui(QtWidgets.QMainWindow): if event["data"]["bytes"] == web.zip_filesize: if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'): self.systemTray.showMessage(strings._('systray_download_completed_title', True), strings._('systray_download_completed_message', True)) + # Update the total 'completed downloads' info + self.downloads_completed += 1 + self.update_downloads_completed(self.downloads_completed) + # Update the 'in progress downloads' info + self.downloads_in_progress -= 1 + self.update_downloads_in_progress(self.downloads_in_progress) + # close on finish? if not web.get_stay_open(): self.server_status.stop_server() - self.status_bar.showMessage(strings._('closing_automatically', True)) + self.status_bar.clearMessage() + self.server_share_status_label.setText(strings._('closing_automatically', True)) else: if self.server_status.status == self.server_status.STATUS_STOPPED: self.downloads.cancel_download(event["data"]["id"]) + self.downloads_in_progress = 0 + self.update_downloads_in_progress(self.downloads_in_progress) + elif event["type"] == web.REQUEST_CANCELED: self.downloads.cancel_download(event["data"]["id"]) + # Update the 'in progress downloads' info + self.downloads_in_progress -= 1 + self.update_downloads_in_progress(self.downloads_in_progress) if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'): self.systemTray.showMessage(strings._('systray_download_canceled_title', True), strings._('systray_download_canceled_message', True)) @@ -487,30 +631,37 @@ class OnionShareGui(QtWidgets.QMainWindow): # If the auto-shutdown timer has stopped, stop the server if self.server_status.status == self.server_status.STATUS_STARTED: - if self.app.shutdown_timer and self.server_status.timer_enabled: + if self.app.shutdown_timer and self.settings.get('shutdown_timeout'): if self.timeout > 0: + now = QtCore.QDateTime.currentDateTime() + seconds_remaining = now.secsTo(self.server_status.timeout) + self.server_status.server_button.setText(strings._('gui_stop_server_shutdown_timeout', True).format(seconds_remaining)) if not self.app.shutdown_timer.is_alive(): # If there were no attempts to download the share, or all downloads are done, we can stop if web.download_count == 0 or web.done: self.server_status.stop_server() - self.status_bar.showMessage(strings._('close_on_timeout', True)) + self.status_bar.clearMessage() + self.server_share_status_label.setText(strings._('close_on_timeout', True)) # A download is probably still running - hold off on stopping the share else: - self.status_bar.showMessage(strings._('timeout_download_still_running', True)) + self.status_bar.clearMessage() + self.server_share_status_label.setText(strings._('timeout_download_still_running', True)) def copy_url(self): """ When the URL gets copied to the clipboard, display this in the status bar. """ common.log('OnionShareGui', 'copy_url') - self.status_bar.showMessage(strings._('gui_copied_url', True), 2000) + if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'): + self.systemTray.showMessage(strings._('gui_copied_url_title', True), strings._('gui_copied_url', True)) def copy_hidservauth(self): """ When the stealth onion service HidServAuth gets copied to the clipboard, display this in the status bar. """ common.log('OnionShareGui', 'copy_hidservauth') - self.status_bar.showMessage(strings._('gui_copied_hidservauth', True), 2000) + if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'): + self.systemTray.showMessage(strings._('gui_copied_hidservauth_title', True), strings._('gui_copied_hidservauth', True)) def clear_message(self): """ @@ -522,22 +673,52 @@ class OnionShareGui(QtWidgets.QMainWindow): """ Disable the Settings button while an OnionShare server is active. """ - self.settings_button.setEnabled(not active) if active: - self.settings_button.setIcon( QtGui.QIcon(common.get_resource_path('images/settings_inactive.png')) ) + self.settings_button.hide() else: - self.settings_button.setIcon( QtGui.QIcon(common.get_resource_path('images/settings.png')) ) + self.settings_button.show() # Disable settings menu action when server is active self.settingsAction.setEnabled(not active) + def reset_info_counters(self): + """ + Set the info counters back to zero. + """ + self.update_downloads_completed(0) + self.update_downloads_in_progress(0) + + def update_downloads_completed(self, count): + """ + Update the 'Downloads completed' info widget. + """ + if count == 0: + self.info_completed_downloads_image = common.get_resource_path('images/download_completed_none.png') + else: + self.info_completed_downloads_image = common.get_resource_path('images/download_completed.png') + self.info_completed_downloads_count.setText('<img src={0:s} /> {1:d}'.format(self.info_completed_downloads_image, count)) + self.info_completed_downloads_count.setToolTip(strings._('info_completed_downloads_tooltip', True).format(count)) + + def update_downloads_in_progress(self, count): + """ + Update the 'Downloads in progress' info widget. + """ + if count == 0: + self.info_in_progress_download_image = common.get_resource_path('images/download_in_progress_none.png') + else: + self.info_in_progress_download_image = common.get_resource_path('images/download_in_progress.png') + self.info_in_progress_download_count.setText('<img src={0:s} /> {1:d}'.format(self.info_in_progress_download_image, count)) + self.info_in_progress_download_count.setToolTip(strings._('info_in_progress_downloads_tooltip', True).format(count)) + def closeEvent(self, e): common.log('OnionShareGui', 'closeEvent') try: if self.server_status.status != self.server_status.STATUS_STOPPED: + common.log('OnionShareGui', 'closeEvent, opening warning dialog') dialog = QtWidgets.QMessageBox() - dialog.setWindowTitle("OnionShare") + dialog.setWindowTitle(strings._('gui_quit_title', True)) dialog.setText(strings._('gui_quit_warning', True)) + dialog.setIcon(QtWidgets.QMessageBox.Critical) quit_button = dialog.addButton(strings._('gui_quit_warning_quit', True), QtWidgets.QMessageBox.YesRole) dont_quit_button = dialog.addButton(strings._('gui_quit_warning_dont_quit', True), QtWidgets.QMessageBox.NoRole) dialog.setDefaultButton(dont_quit_button) @@ -566,14 +747,15 @@ class ZipProgressBar(QtWidgets.QProgressBar): self.setFormat(strings._('zip_progress_bar_format')) cssStyleData =""" QProgressBar { - background-color: rgba(255, 255, 255, 0.0) !important; - border: 0px; + border: 1px solid #4e064f; + background-color: #ffffff !important; text-align: center; + color: #9b9b9b; } QProgressBar::chunk { border: 0px; - background: qlineargradient(x1: 0.5, y1: 0, x2: 0.5, y2: 1, stop: 0 #b366ff, stop: 1 #d9b3ff); + background-color: #4e064f; width: 10px; }""" self.setStyleSheet(cssStyleData) @@ -607,3 +789,26 @@ class ZipProgressBar(QtWidgets.QProgressBar): self.setValue(100) else: self.setValue(0) + + +class OnionThread(QtCore.QThread): + """ + A QThread for starting our Onion Service. + By using QThread rather than threading.Thread, we are able + to call quit() or terminate() on the startup if the user + decided to cancel (in which case do not proceed with obtaining + the Onion address and starting the web server). + """ + def __init__(self, function, kwargs=None): + super(OnionThread, self).__init__() + common.log('OnionThread', '__init__') + self.function = function + if not kwargs: + self.kwargs = {} + else: + self.kwargs = kwargs + + def run(self): + common.log('OnionThread', 'run') + + self.function(**self.kwargs) diff --git a/onionshare_gui/server_status.py b/onionshare_gui/server_status.py index 442ae440..03540415 100644 --- a/onionshare_gui/server_status.py +++ b/onionshare_gui/server_status.py @@ -23,12 +23,14 @@ from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings, common, settings -class ServerStatus(QtWidgets.QVBoxLayout): +class ServerStatus(QtWidgets.QWidget): """ The server status chunk of the GUI. """ server_started = QtCore.pyqtSignal() server_stopped = QtCore.pyqtSignal() + server_canceled = QtCore.pyqtSignal() + button_clicked = QtCore.pyqtSignal() url_copied = QtCore.pyqtSignal() hidservauth_copied = QtCore.pyqtSignal() @@ -47,100 +49,103 @@ class ServerStatus(QtWidgets.QVBoxLayout): self.settings = settings - # Helper boolean as this is used in a few places - self.timer_enabled = False # Shutdown timeout layout - self.server_shutdown_timeout_checkbox = QtWidgets.QCheckBox() - self.server_shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Unchecked) - self.server_shutdown_timeout_checkbox.toggled.connect(self.shutdown_timeout_toggled) - self.server_shutdown_timeout_checkbox.setText(strings._("gui_settings_shutdown_timeout_choice", True)) - self.server_shutdown_timeout_label = QtWidgets.QLabel(strings._('gui_settings_shutdown_timeout', True)) - self.server_shutdown_timeout = QtWidgets.QDateTimeEdit() + self.shutdown_timeout_label = QtWidgets.QLabel(strings._('gui_settings_shutdown_timeout', True)) + self.shutdown_timeout = QtWidgets.QDateTimeEdit() # Set proposed timeout to be 5 minutes into the future - self.server_shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300)) + self.shutdown_timeout.setDisplayFormat("hh:mm A MMM d, yy") + self.shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300)) # Onion services can take a little while to start, so reduce the risk of it expiring too soon by setting the minimum to 2 min from now - self.server_shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(120)) - self.server_shutdown_timeout.setCurrentSectionIndex(4) - self.server_shutdown_timeout_label.hide() - self.server_shutdown_timeout.hide() - shutdown_timeout_layout_group = QtWidgets.QHBoxLayout() - shutdown_timeout_layout_group.addWidget(self.server_shutdown_timeout_checkbox) - shutdown_timeout_layout_group.addWidget(self.server_shutdown_timeout_label) - shutdown_timeout_layout_group.addWidget(self.server_shutdown_timeout) - # server layout - self.status_image_stopped = QtGui.QImage(common.get_resource_path('images/server_stopped.png')) - self.status_image_working = QtGui.QImage(common.get_resource_path('images/server_working.png')) - self.status_image_started = QtGui.QImage(common.get_resource_path('images/server_started.png')) - self.status_image_label = QtWidgets.QLabel() - self.status_image_label.setFixedWidth(30) + self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(120)) + self.shutdown_timeout.setCurrentSection(QtWidgets.QDateTimeEdit.MinuteSection) + shutdown_timeout_layout = QtWidgets.QHBoxLayout() + shutdown_timeout_layout.addWidget(self.shutdown_timeout_label) + shutdown_timeout_layout.addWidget(self.shutdown_timeout) + + # Shutdown timeout container, so it can all be hidden and shown as a group + shutdown_timeout_container_layout = QtWidgets.QVBoxLayout() + shutdown_timeout_container_layout.addLayout(shutdown_timeout_layout) + self.shutdown_timeout_container = QtWidgets.QWidget() + self.shutdown_timeout_container.setLayout(shutdown_timeout_container_layout) + self.shutdown_timeout_container.hide() + + + # Server layout self.server_button = QtWidgets.QPushButton() self.server_button.clicked.connect(self.server_button_clicked) - server_layout = QtWidgets.QHBoxLayout() - server_layout.addWidget(self.status_image_label) - server_layout.addWidget(self.server_button) - # url layout + # URL layout url_font = QtGui.QFont() - self.url_label = QtWidgets.QLabel() - self.url_label.setFont(url_font) - self.url_label.setWordWrap(False) - self.url_label.setAlignment(QtCore.Qt.AlignCenter) + self.url_description = QtWidgets.QLabel(strings._('gui_url_description', True)) + self.url_description.setWordWrap(True) + self.url_description.setMinimumHeight(50) + self.url = QtWidgets.QLabel() + self.url.setFont(url_font) + self.url.setWordWrap(True) + self.url.setMinimumHeight(60) + self.url.setMinimumSize(self.url.sizeHint()) + self.url.setStyleSheet('QLabel { background-color: #ffffff; color: #000000; padding: 10px; border: 1px solid #666666; }') + + url_buttons_style = 'QPushButton { color: #3f7fcf; }' self.copy_url_button = QtWidgets.QPushButton(strings._('gui_copy_url', True)) + self.copy_url_button.setFlat(True) + self.copy_url_button.setStyleSheet(url_buttons_style) self.copy_url_button.clicked.connect(self.copy_url) self.copy_hidservauth_button = QtWidgets.QPushButton(strings._('gui_copy_hidservauth', True)) + self.copy_hidservauth_button.setFlat(True) + self.copy_hidservauth_button.setStyleSheet(url_buttons_style) self.copy_hidservauth_button.clicked.connect(self.copy_hidservauth) - url_layout = QtWidgets.QHBoxLayout() - url_layout.addWidget(self.url_label) - url_layout.addWidget(self.copy_url_button) - url_layout.addWidget(self.copy_hidservauth_button) - - # add the widgets - self.addLayout(shutdown_timeout_layout_group) - self.addLayout(server_layout) - self.addLayout(url_layout) + url_buttons_layout = QtWidgets.QHBoxLayout() + url_buttons_layout.addWidget(self.copy_url_button) + url_buttons_layout.addWidget(self.copy_hidservauth_button) + url_buttons_layout.addStretch() + + url_layout = QtWidgets.QVBoxLayout() + url_layout.addWidget(self.url_description) + url_layout.addWidget(self.url) + url_layout.addLayout(url_buttons_layout) + + # Add the widgets + layout = QtWidgets.QVBoxLayout() + layout.addWidget(self.server_button) + layout.addLayout(url_layout) + layout.addWidget(self.shutdown_timeout_container) + self.setLayout(layout) self.update() - def shutdown_timeout_toggled(self, checked): - """ - Shutdown timer option was toggled. If checked, show the timer settings. - """ - if checked: - self.timer_enabled = True - # Hide the checkbox, show the options - self.server_shutdown_timeout_label.show() - # Reset the default timer to 5 minutes into the future after toggling the option on - self.server_shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300)) - self.server_shutdown_timeout.show() - else: - self.timer_enabled = False - self.server_shutdown_timeout_label.hide() - self.server_shutdown_timeout.hide() - def shutdown_timeout_reset(self): """ Reset the timeout in the UI after stopping a share """ - self.server_shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Unchecked) - self.server_shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300)) - self.server_shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(120)) + self.shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300)) + self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(120)) def update(self): """ Update the GUI elements based on the current state. """ - # set the status image - if self.status == self.STATUS_STOPPED: - self.status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.status_image_stopped)) - elif self.status == self.STATUS_WORKING: - self.status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.status_image_working)) - elif self.status == self.STATUS_STARTED: - self.status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.status_image_started)) - - # set the URL fields + # Set the URL fields if self.status == self.STATUS_STARTED: - self.url_label.setText('http://{0:s}/{1:s}'.format(self.app.onion_host, self.web.slug)) - self.url_label.show() + self.url_description.show() + + info_image = common.get_resource_path('images/info.png') + self.url_description.setText(strings._('gui_url_description', True).format(info_image)) + # Show a Tool Tip explaining the lifecycle of this URL + if self.settings.get('save_private_key'): + if self.settings.get('close_after_first_download'): + self.url_description.setToolTip(strings._('gui_url_label_onetime_and_persistent', True)) + else: + self.url_description.setToolTip(strings._('gui_url_label_persistent', True)) + else: + if self.settings.get('close_after_first_download'): + self.url_description.setToolTip(strings._('gui_url_label_onetime', True)) + else: + self.url_description.setToolTip(strings._('gui_url_label_stay_open', True)) + + self.url.setText('http://{0:s}/{1:s}'.format(self.app.onion_host, self.web.slug)) + self.url.show() + self.copy_url_button.show() if self.settings.get('save_private_key'): @@ -148,53 +153,63 @@ class ServerStatus(QtWidgets.QVBoxLayout): self.settings.set('slug', self.web.slug) self.settings.save() + if self.settings.get('shutdown_timeout'): + self.shutdown_timeout_container.hide() + if self.app.stealth: self.copy_hidservauth_button.show() else: self.copy_hidservauth_button.hide() - - # resize parent widget - p = self.parentWidget() - p.resize(p.sizeHint()) else: - self.url_label.hide() + self.url_description.hide() + self.url.hide() self.copy_url_button.hide() self.copy_hidservauth_button.hide() - # button + # Button + button_stopped_style = 'QPushButton { background-color: #5fa416; color: #ffffff; padding: 10px; border: 0; border-radius: 5px; }' + button_working_style = 'QPushButton { background-color: #4c8211; color: #ffffff; padding: 10px; border: 0; border-radius: 5px; font-style: italic; }' + button_started_style = 'QPushButton { background-color: #d0011b; color: #ffffff; padding: 10px; border: 0; border-radius: 5px; }' if self.file_selection.get_num_files() == 0: - self.server_button.setEnabled(False) - self.server_button.setText(strings._('gui_start_server', True)) + self.server_button.hide() else: + self.server_button.show() + if self.status == self.STATUS_STOPPED: + self.server_button.setStyleSheet(button_stopped_style) self.server_button.setEnabled(True) self.server_button.setText(strings._('gui_start_server', True)) - self.server_shutdown_timeout.setEnabled(True) - self.server_shutdown_timeout_checkbox.setEnabled(True) + self.server_button.setToolTip('') + if self.settings.get('shutdown_timeout'): + self.shutdown_timeout_container.show() elif self.status == self.STATUS_STARTED: + self.server_button.setStyleSheet(button_started_style) self.server_button.setEnabled(True) self.server_button.setText(strings._('gui_stop_server', True)) - self.server_shutdown_timeout.setEnabled(False) - self.server_shutdown_timeout_checkbox.setEnabled(False) + if self.settings.get('shutdown_timeout'): + self.shutdown_timeout_container.hide() + self.server_button.setToolTip(strings._('gui_stop_server_shutdown_timeout_tooltip', True).format(self.timeout)) elif self.status == self.STATUS_WORKING: - self.server_button.setEnabled(False) + self.server_button.setStyleSheet(button_working_style) + self.server_button.setEnabled(True) self.server_button.setText(strings._('gui_please_wait')) - self.server_shutdown_timeout.setEnabled(False) - self.server_shutdown_timeout_checkbox.setEnabled(False) + if self.settings.get('shutdown_timeout'): + self.shutdown_timeout_container.hide() else: + self.server_button.setStyleSheet(button_working_style) self.server_button.setEnabled(False) self.server_button.setText(strings._('gui_please_wait')) - self.server_shutdown_timeout.setEnabled(False) - self.server_shutdown_timeout_checkbox.setEnabled(False) + if self.settings.get('shutdown_timeout'): + self.shutdown_timeout_container.hide() def server_button_clicked(self): """ Toggle starting or stopping the server. """ if self.status == self.STATUS_STOPPED: - if self.timer_enabled: + if self.settings.get('shutdown_timeout'): # Get the timeout chosen, stripped of its seconds. This prevents confusion if the share stops at (say) 37 seconds past the minute chosen - self.timeout = self.server_shutdown_timeout.dateTime().toPyDateTime().replace(second=0, microsecond=0) + self.timeout = self.shutdown_timeout.dateTime().toPyDateTime().replace(second=0, microsecond=0) # If the timeout has actually passed already before the user hit Start, refuse to start the server. if QtCore.QDateTime.currentDateTime().toPyDateTime() > self.timeout: Alert(strings._('gui_server_timeout_expired', QtWidgets.QMessageBox.Warning)) @@ -204,6 +219,9 @@ class ServerStatus(QtWidgets.QVBoxLayout): self.start_server() elif self.status == self.STATUS_STARTED: self.stop_server() + elif self.status == self.STATUS_WORKING: + self.cancel_server() + self.button_clicked.emit() def start_server(self): """ @@ -230,6 +248,16 @@ class ServerStatus(QtWidgets.QVBoxLayout): self.update() self.server_stopped.emit() + def cancel_server(self): + """ + Cancel the server. + """ + common.log('ServerStatus', 'cancel_server', 'Canceling the server mid-startup') + self.status = self.STATUS_WORKING + self.shutdown_timeout_reset() + self.update() + self.server_canceled.emit() + def stop_server_finished(self): """ The server has finished stopping. diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py index 424f2589..a279723e 100644 --- a/onionshare_gui/settings_dialog.py +++ b/onionshare_gui/settings_dialog.py @@ -60,6 +60,11 @@ class SettingsDialog(QtWidgets.QDialog): self.systray_notifications_checkbox.setCheckState(QtCore.Qt.Checked) self.systray_notifications_checkbox.setText(strings._("gui_settings_systray_notifications", True)) + # Whether or not to use a shutdown timer + self.shutdown_timeout_checkbox = QtWidgets.QCheckBox() + self.shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Checked) + self.shutdown_timeout_checkbox.setText(strings._("gui_settings_shutdown_timeout_checkbox", True)) + # Whether or not to save the Onion private key for reuse self.save_private_key_checkbox = QtWidgets.QCheckBox() self.save_private_key_checkbox.setCheckState(QtCore.Qt.Unchecked) @@ -69,6 +74,7 @@ class SettingsDialog(QtWidgets.QDialog): sharing_group_layout = QtWidgets.QVBoxLayout() sharing_group_layout.addWidget(self.close_after_first_download_checkbox) sharing_group_layout.addWidget(self.systray_notifications_checkbox) + sharing_group_layout.addWidget(self.shutdown_timeout_checkbox) sharing_group_layout.addWidget(self.save_private_key_checkbox) sharing_group = QtWidgets.QGroupBox(strings._("gui_settings_sharing_label", True)) sharing_group.setLayout(sharing_group_layout) @@ -80,12 +86,14 @@ class SettingsDialog(QtWidgets.QDialog): stealth_details.setWordWrap(True) stealth_details.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) stealth_details.setOpenExternalLinks(True) + stealth_details.setMinimumSize(stealth_details.sizeHint()) self.stealth_checkbox = QtWidgets.QCheckBox() self.stealth_checkbox.setCheckState(QtCore.Qt.Unchecked) self.stealth_checkbox.setText(strings._("gui_settings_stealth_option", True)) hidservauth_details = QtWidgets.QLabel(strings._('gui_settings_stealth_hidservauth_string', True)) hidservauth_details.setWordWrap(True) + hidservauth_details.setMinimumSize(hidservauth_details.sizeHint()) hidservauth_details.hide() self.hidservauth_copy_button = QtWidgets.QPushButton(strings._('gui_copy_hidservauth', True)) @@ -317,9 +325,12 @@ class SettingsDialog(QtWidgets.QDialog): self.save_button.clicked.connect(self.save_clicked) self.cancel_button = QtWidgets.QPushButton(strings._('gui_settings_button_cancel', True)) self.cancel_button.clicked.connect(self.cancel_clicked) + version_label = QtWidgets.QLabel('OnionShare {0:s}'.format(common.get_version())) + version_label.setStyleSheet('color: #666666') self.help_button = QtWidgets.QPushButton(strings._('gui_settings_button_help', True)) self.help_button.clicked.connect(self.help_clicked) buttons_layout = QtWidgets.QHBoxLayout() + buttons_layout.addWidget(version_label) buttons_layout.addWidget(self.help_button) buttons_layout.addStretch() buttons_layout.addWidget(self.save_button) @@ -371,6 +382,12 @@ class SettingsDialog(QtWidgets.QDialog): else: self.systray_notifications_checkbox.setCheckState(QtCore.Qt.Unchecked) + shutdown_timeout = self.old_settings.get('shutdown_timeout') + if shutdown_timeout: + self.shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Checked) + else: + self.shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Unchecked) + save_private_key = self.old_settings.get('save_private_key') if save_private_key: self.save_private_key_checkbox.setCheckState(QtCore.Qt.Checked) @@ -723,6 +740,7 @@ class SettingsDialog(QtWidgets.QDialog): settings.set('close_after_first_download', self.close_after_first_download_checkbox.isChecked()) settings.set('systray_notifications', self.systray_notifications_checkbox.isChecked()) + settings.set('shutdown_timeout', self.shutdown_timeout_checkbox.isChecked()) if self.save_private_key_checkbox.isChecked(): settings.set('save_private_key', True) settings.set('private_key', self.old_settings.get('private_key')) |