def on_save_file(self, default_filename): """ Opens a native file dialog and returns the chosen path of the file to be saved The dialog does not allow overwriting existing files - to get this behaviour while using native dialogs the QFileDialog has to be reopened on overwrite. A bit weird but enables visual consistency with the other file choose dialogs :param default_filename: The default filename to be used in the Qt file dialog :type default_filename: str/unicode :returns: The designated key file path :rtype: str/unicode """ def_path = os.path.join(os.getenv("HOME"), default_filename) while True: save_path = QFileDialog.getSaveFileName( self, _('Please create a new file'), def_path, options=QFileDialog.DontConfirmOverwrite) save_path = self.encode_qt_output(save_path[0]) if isinstance( save_path, tuple) else self.encode_qt_output(save_path) self.buttons.button(QDialogButtonBox.Ok).setText( _('Create')) # qt keeps changing this.. if os.path.exists(save_path): show_alert( self, _('File already exists:\n{filename}\n\n' '<b>Please create a new file!</b>').format( filename=save_path)) def_path = os.path.join(os.path.basename(save_path), default_filename) else: return save_path
def run(self): """ Listens on workers stdout and executes callbacks when answers arrive """ while True: try: buf = self.worker.stdout.readline() # blocks if buf: # check if worker output pipe closed response = json.loads(buf.strip(), encoding='utf-8') else: return assert('type' in response and 'msg' in response) assert(self.success_callback is not None and self.error_callback is not None) # there should be somebody waiting for an answer! # valid response received if response['type'] == 'error': QApplication.postEvent(self.parent, WorkerEvent(self.error_callback, response['msg'])) else: QApplication.postEvent(self.parent, WorkerEvent(self.success_callback, response['msg'])) # reset callbacks self.success_callback, self.error_callback = None, None except ValueError: # worker didn't return json -> probably crashed, show everything printed to stdout buf += self.worker.stdout.read() QApplication.postEvent(self.parent, WorkerEvent(callback=lambda msg: show_alert(self.parent, msg, critical=True), response=_('Error in communication:\n{error}').format(error=_(buf)))) return except (IOError, AssertionError) as communication_error: QApplication.postEvent(self.parent, WorkerEvent(callback=lambda msg: show_alert(self.parent, msg, critical=True), response=_('Error in communication:\n{error}').format(error=format_exception(communication_error)))) return
def __init__(self, parent): """ :param parent: The parent widget to be passed to modal dialogs :type parent: :class:`PyQt4.QtGui.QWidget` :raises: SudoException """ super(WorkerMonitor, self).__init__() self.parent = parent self.success_callback, self.error_callback = None, None self.modify_sudoers = False self.worker = None self._spawn_worker() if self.modify_sudoers: # adding user/program to /etc/sudoers.d/ requested self.execute({'type': 'request', 'msg': 'authorize'}, None, None) response = json.loads(self.worker.stdout.readline().strip(), encoding='utf-8') # blocks if response['type'] == 'error': show_alert(self.parent, response['msg']) else: message = _( 'Permanent `sudo` authorization for\n' '{program}\n' 'has been successfully added for user `{username}` to \n' '/etc/sudoers.d/lucky-luks\n').format( program=os.path.abspath(sys.argv[0]), username=os.getenv("USER")) show_info(self.parent, message, _('Success'))
def __init__(self, parent): """ :param parent: The parent widget to be passed to modal dialogs :type parent: :class:`PyQt4.QtGui.QWidget` :raises: SudoException """ super(WorkerMonitor, self).__init__() self.parent = parent self.success_callback, self.error_callback = None, None self.modify_sudoers = False self.worker = None self._spawn_worker() if self.modify_sudoers: # adding user/program to /etc/sudoers.d/ requested self.execute({'type': 'request', 'msg': 'authorize'}, None, None) response = json.loads(self.worker.stdout.readline().strip(), encoding='utf-8') # blocks if response['type'] == 'error': show_alert(self.parent, response['msg']) else: message = _('Permanent `sudo` authorization for\n' '{program}\n' 'has been successfully added for user `{username}` to \n' '/etc/sudoers.d/lucky-luks\n').format( program=os.path.abspath(sys.argv[0]), username=os.getenv("USER")) show_info(self.parent, message, _('Success'))
def display_create_failed(self, errormessage, stop_timer=False): """ Triggered when an error happend during the create process :param errormessage: errormessage to be shown :type errormessage: str :param stop_timer: stop a progress indicator? :type stop_timer: bool """ if stop_timer: self.set_progress_done(self.create_timer) show_alert(self, errormessage) self.display_create_done()
def on_initialized(self, message, error=False): """ Callback after worker send current state of container :param message: Contains an error description if error=True, otherwise the current state of the container (unlocked/closed) :type message: str :param critical: Error during initialization (default=False) :type critical: bool """ if error: show_alert(self, message, critical=True) else: self.is_unlocked = (True if message == 'unlocked' else False) self.enable_ui()
def toggle_container_status(self): """ Unlock or close container """ if self.is_unlocked: self.do_close_container() else: try: UnlockContainerDialog(self, self.worker, self.luks_device_name, self.encrypted_container, self.key_file, self.mount_point).communicate() self.is_unlocked = True except UserInputError as uie: show_alert(self, format_exception(uie)) self.is_unlocked = False self.refresh()
def on_switchpage_event(self, index): """ Event handler for tab switch: change text on OK button (Unlock/Create) """ new_ok_label = _('Unlock') if index == 1: # create if self.create_filesystem_type.currentText() == '': show_alert( self, _('No tools to format the filesystem found\n' 'Please install, eg for Debian/Ubuntu\n' '`apt-get install e2fslibs ntfs-3g`')) new_ok_label = _('Create') self.buttons.button(QDialogButtonBox.Ok).setText(new_ok_label)
def execute(self, command, success_callback, error_callback): """ Writes command to workers stdin and sets callbacks for listener thread :param command: The function to be done by the worker is in command[`msg`] the arguments are passed as named properties command[`device_name`] etc. :type command: dict :param success_callback: The function to be called if the worker finished successfully :type success_callback: function :param error_callback: The function to be called if the worker returns an error :type error_callback: function """ try: assert ('type' in command and 'msg' in command) # valid command obj? assert ( self.success_callback is None and self.error_callback is None ) # channel clear? (no qeue neccessary for the backend process) self.success_callback = success_callback self.error_callback = error_callback self.worker.stdin.write(json.dumps(command) + '\n') self.worker.stdin.flush() except (IOError, AssertionError) as communication_error: QApplication.postEvent( self.parent, WorkerEvent( callback=lambda msg: show_alert( self.parent, msg, critical=True), response=_('Error in communication:\n{error}').format( error=format_exception(communication_error))))
def on_container_closed(self, message, error, shutdown): """ Callback after worker closed container :param message: Contains an error description if error=True, otherwise the current state of the container (unlocked/closed) :type message: str :param error: Error during closing of container :type error: bool :param shutdown: Quit application after container successfully closed? :type shutdown: bool """ if error: show_alert(self, message) else: self.is_unlocked = False if not error and shutdown: # automatic shutdown only if container successfully closed QApplication.instance().quit() else: self.enable_ui()
def run(self): """ Listens on workers stdout and executes callbacks when answers arrive """ while True: try: buf = self.worker.stdout.readline() # blocks if buf: # check if worker output pipe closed response = json.loads(buf.strip(), encoding='utf-8') else: return assert ('type' in response and 'msg' in response) assert (self.success_callback is not None and self.error_callback is not None ) # there should be somebody waiting for an answer! # valid response received if response['type'] == 'error': QApplication.postEvent( self.parent, WorkerEvent(self.error_callback, response['msg'])) else: QApplication.postEvent( self.parent, WorkerEvent(self.success_callback, response['msg'])) # reset callbacks self.success_callback, self.error_callback = None, None except ValueError: # worker didn't return json -> probably crashed, show everything printed to stdout buf += self.worker.stdout.read() QApplication.postEvent( self.parent, WorkerEvent( callback=lambda msg: show_alert( self.parent, msg, critical=True), response=_('Error in communication:\n{error}').format( error=_(buf)))) return except (IOError, AssertionError) as communication_error: QApplication.postEvent( self.parent, WorkerEvent( callback=lambda msg: show_alert( self.parent, msg, critical=True), response=_('Error in communication:\n{error}').format( error=format_exception(communication_error)))) return
def on_accepted(self): """ Event handler for response: Start unlock or create action """ try: if self.tab_pane.currentIndex() == 1: self.on_create_container() else: UnlockContainerDialog( self, self.worker, self.get_luks_device_name(), self.get_encrypted_container(), self.get_keyfile(), self.get_mount_point()).communicate() # blocks # optionally create startmenu entry self.show_create_startmenu_entry() # all good, now switch to main window self.accept() except UserInputError as error: show_alert(self, format_exception(error))
def execute(self, command, success_callback, error_callback): """ Writes command to workers stdin and sets callbacks for listener thread :param command: The function to be done by the worker is in command[`msg`] the arguments are passed as named properties command[`device_name`] etc. :type command: dict :param success_callback: The function to be called if the worker finished successfully :type success_callback: function :param error_callback: The function to be called if the worker returns an error :type error_callback: function """ try: assert('type' in command and 'msg' in command) # valid command obj? assert(self.success_callback is None and self.error_callback is None) # channel clear? (no qeue neccessary for the backend process) self.success_callback = success_callback self.error_callback = error_callback self.worker.stdin.write(json.dumps(command) + '\n') self.worker.stdin.flush() except (IOError, AssertionError) as communication_error: QApplication.postEvent(self.parent, WorkerEvent(callback=lambda msg: show_alert(self.parent, msg, critical=True), response=_('Error in communication:\n{error}').format(error=format_exception(communication_error))))
def create_startmenu_entry(self): """ Creates a startmenu entry that lets the user skip the setup dialog and go directly to the main UI Includes a workaround for the safety net some desktop environments create around the startupmenu """ import random import string # command to be saved in shortcut: calling the script with the arguments entered in the dialog # put all arguments in single quotes and escape those in the strings (shell escape ' -> '\'') cmd = os.path.abspath(sys.argv[0]) cmd += " -c '" + self.get_encrypted_container().replace("'", "'\\'''") + "'" cmd += " -n '" + self.get_luks_device_name().replace("'", "'\\'''") + "'" if self.get_mount_point() is not None: cmd += " -m '" + self.get_mount_point().replace("'", "'\\'''") + "'" if self.get_keyfile() is not None: cmd += " -k '" + self.get_keyfile().replace("'", "'\\'''") + "'" # create .desktop-file filename = _('luckyLUKS') + '-' + ''.join( i for i in self.get_luks_device_name() if i not in ' \/:*?<>|' ) # xdg-desktop-menu has problems with some special chars if is_installed('xdg-desktop-menu' ): # create in tmp and add freedesktop menu entry # some desktop menus dont delete the .desktop files if a user removes items from the menu but keep track of those files instead # those items wont be readded later, the random part of the filename works around this behaviour desktop_file_path = os.path.join( '/tmp', filename + '-' + ''.join(random.choice(string.ascii_letters) for i in range(4)) + '.desktop') else: # or create in users home dir desktop_file_path = os.path.join(os.path.expanduser('~'), filename + '.desktop') desktop_file = codecs.open(desktop_file_path, 'w', 'utf-8') entry_name = _('Unlock {device_name}').format( device_name=self.get_luks_device_name()) desktop_file.write("[Desktop Entry]\n") desktop_file.write("Name=" + entry_name + "\n") desktop_file.write("Comment=" + self.get_luks_device_name() + " " + _('Encrypted Container Tool') + "\n") desktop_file.write("GenericName=" + _('Encrypted Container') + "\n") desktop_file.write("Categories=Utility;\n") desktop_file.write("Exec=" + cmd + "\n") desktop_file.write("Icon=dialog-password\n") desktop_file.write("NoDisplay=false\n") desktop_file.write("StartupNotify=false\n") desktop_file.write("Terminal=0\n") desktop_file.write("TerminalOptions=\n") desktop_file.write("Type=Application\n\n") desktop_file.close() os.chmod(desktop_file_path, 0o700) # some distros need the xbit to trust the desktop file if is_installed('xdg-desktop-menu'): # safest way to ensure updates: explicit uninstall followed by installing a new desktop file with different random part import glob for desktopfile in glob.glob( os.path.expanduser('~') + '/.local/share/applications/' + filename + '-*.desktop'): with open(os.devnull) as DEVNULL: subprocess.call( ['xdg-desktop-menu', 'uninstall', desktopfile], stdout=DEVNULL, stderr=subprocess.STDOUT) try: subprocess.check_output([ 'xdg-desktop-menu', 'install', '--novendor', desktop_file_path ], stderr=subprocess.STDOUT, universal_newlines=True) os.remove(desktop_file_path) # remove from tmp show_info( self, _('<b>` {name} `</b>\nadded to start menu').format( name=entry_name), _('Success')) except subprocess.CalledProcessError as cpe: home_dir_path = os.path.join( os.path.expanduser('~'), os.path.basename(desktop_file_path)) # move to homedir instead from shutil import move move(desktop_file_path, home_dir_path) show_alert(self, cpe.output) show_info( self, _('Adding to start menu not possible,\nplease place your shortcut manually.\n\nDesktop file saved to\n{location}' ).format(location=home_dir_path)) else: show_info( self, _('Adding to start menu not possible,\nplease place your shortcut manually.\n\nDesktop file saved to\n{location}' ).format(location=desktop_file_path))
def __init__(self, device_name=None, container_path=None, key_file=None, mount_point=None): """ Command line arguments checks are done here to be able to display a graphical dialog with error messages . If no arguments were supplied on the command line a setup dialog will be shown. All commands will be executed from a separate worker process with administrator privileges that gets initialized here. :param device_name: The device mapper name :type device_name: str/unicode or None :param container_path: The path of the container file :type container_path: str/unicode or None :param key_file: The path of an optional key file :type key_file: str/unicode or None :param mount_point: The path of an optional mount point :type mount_point: str/unicode or None """ super(MainWindow, self).__init__() self.luks_device_name = device_name self.encrypted_container = container_path self.key_file = key_file self.mount_point = mount_point self.worker = None self.is_waiting_for_worker = False self.is_unlocked = False self.is_initialized = False self.has_tray = QSystemTrayIcon.isSystemTrayAvailable() # L10n: program name - translatable for startmenu titlebar etc self.setWindowTitle(_('luckyLUKS')) self.setWindowIcon(QIcon.fromTheme('dialog-password', QApplication.style().standardIcon(QStyle.SP_DriveHDIcon))) # check if cryptsetup and sudo are installed not_installed_msg = _('{program_name} executable not found!\nPlease install, eg for Debian/Ubuntu\n`apt-get install {program_name}`') if not utils.is_installed('cryptsetup'): show_alert(self, not_installed_msg.format(program_name='cryptsetup'), critical=True) if not utils.is_installed('sudo'): show_alert(self, not_installed_msg.format(program_name='sudo'), critical=True) # quick sanity checks before asking for passwd if os.getuid() == 0: show_alert(self, _('Graphical programs should not be run as root!\nPlease call as normal user.'), critical=True) if self.encrypted_container and not os.path.exists(self.encrypted_container): show_alert(self, _('Container file not accessible\nor path does not exist:\n\n{file_path}').format(file_path=self.encrypted_container), critical=True) # only either encrypted_container or luks_device_name supplied if bool(self.encrypted_container) != bool(self.luks_device_name): show_alert(self, _('Invalid arguments:\n' 'Please call without any arguments\n' 'or supply both container and name.\n\n' '<b>{executable} -c CONTAINER -n NAME [-m MOUNTPOINT]</b>\n\n' 'CONTAINER = Path of the encrypted container file\n' 'NAME = A (unique) name to identify the unlocked container\n' 'Optional: MOUNTPOINT = where to mount the encrypted filesystem\n\n' 'If automatic mounting is configured on your system,\n' 'explicitly setting a mountpoint is not required\n\n' 'For more information, visit\n' '<a href="{project_url}">{project_url}</a>' ).format(executable=os.path.basename(sys.argv[0]), project_url=PROJECT_URL), critical=True) # spawn worker process with root privileges try: self.worker = utils.WorkerMonitor(self) # start communication thread self.worker.start() except utils.SudoException as se: show_alert(self, format_exception(se), critical=True) return # if no arguments supplied, display dialog to gather this information if self.encrypted_container is None and self.luks_device_name is None: from luckyLUKS.setupUI import SetupDialog sd = SetupDialog(self) if sd.exec_() == QDialog.Accepted: self.luks_device_name = sd.get_luks_device_name() self.encrypted_container = sd.get_encrypted_container() self.mount_point = sd.get_mount_point() self.key_file = sd.get_keyfile() self.is_unlocked = True # all checks in setup dialog -> skip initializing state else: # user closed dialog -> quit program # and check if a keyfile create thread has to be stopped # the worker process terminates itself when its parent dies if hasattr(sd, 'create_thread') and sd.create_thread.isRunning(): sd.create_thread.terminate() QApplication.instance().quit() return # center window on desktop qr = self.frameGeometry() cp = QDesktopWidget().availableGeometry().center() qr.moveCenter(cp) self.move(qr.topLeft()) # widget content main_grid = QGridLayout() main_grid.setSpacing(10) icon = QLabel() icon.setPixmap(QIcon.fromTheme('dialog-password', QApplication.style().standardIcon(QStyle.SP_DriveHDIcon)).pixmap(32)) main_grid.addWidget(icon, 0, 0) main_grid.addWidget(QLabel('<b>' + _('Handle encrypted container') + '</b>\n'), 0, 1, alignment=Qt.AlignCenter) main_grid.addWidget(QLabel(_('Name:')), 1, 0) main_grid.addWidget(QLabel('<b>{dev_name}</b>'.format(dev_name=self.luks_device_name)), 1, 1, alignment=Qt.AlignCenter) main_grid.addWidget(QLabel(_('File:')), 2, 0) main_grid.addWidget(QLabel(self.encrypted_container), 2, 1, alignment=Qt.AlignCenter) if self.key_file is not None: main_grid.addWidget(QLabel(_('Key:')), 3, 0) main_grid.addWidget(QLabel(self.key_file), 3, 1, alignment=Qt.AlignCenter) if self.mount_point is not None: main_grid.addWidget(QLabel(_('Mount:')), 4, 0) main_grid.addWidget(QLabel(self.mount_point), 4, 1, alignment=Qt.AlignCenter) main_grid.addWidget(QLabel(_('Status:')), 5, 0) self.label_status = QLabel('') main_grid.addWidget(self.label_status, 5, 1, alignment=Qt.AlignCenter) self.button_toggle_status = QPushButton('') self.button_toggle_status.setMinimumHeight(34) self.button_toggle_status.clicked.connect(self.toggle_container_status) main_grid.setRowMinimumHeight(6, 10) main_grid.addWidget(self.button_toggle_status, 7, 1) widget = QWidget() widget.setLayout(main_grid) widget.setContentsMargins(10, 10, 10, 10) self.setCentralWidget(widget) # tray popup menu if self.has_tray: tray_popup = QMenu(self) tray_popup.addAction(QIcon.fromTheme('dialog-password', QApplication.style().standardIcon(QStyle.SP_DriveHDIcon)), self.luks_device_name).setEnabled(False) tray_popup.addSeparator() self.tray_toggle_action = QAction(QApplication.style().standardIcon(QStyle. SP_DesktopIcon), _('Hide'), self) self.tray_toggle_action.triggered.connect(self.toggle_main_window) tray_popup.addAction(self.tray_toggle_action) quit_action = QAction(QApplication.style().standardIcon(QStyle.SP_MessageBoxCritical), _('Quit'), self) quit_action.triggered.connect(self.tray_quit) tray_popup.addAction(quit_action) # systray self.tray = QSystemTrayIcon(self) self.tray.setIcon(QIcon.fromTheme('dialog-password', QApplication.style().standardIcon(QStyle.SP_DriveHDIcon))) self.tray.setContextMenu(tray_popup) self.tray.activated.connect(self.toggle_main_window) self.tray.show() self.init_status()
def __init__(self, device_name=None, container_path=None, key_file=None, mount_point=None): """ Command line arguments checks are done here to be able to display a graphical dialog with error messages . If no arguments were supplied on the command line a setup dialog will be shown. All commands will be executed from a separate worker process with administrator privileges that gets initialized here. :param device_name: The device mapper name :type device_name: str/unicode or None :param container_path: The path of the container file :type container_path: str/unicode or None :param key_file: The path of an optional key file :type key_file: str/unicode or None :param mount_point: The path of an optional mount point :type mount_point: str/unicode or None """ super(MainWindow, self).__init__() self.luks_device_name = device_name self.encrypted_container = container_path self.key_file = key_file self.mount_point = mount_point self.worker = None self.is_waiting_for_worker = False self.is_unlocked = False self.is_initialized = False self.has_tray = QSystemTrayIcon.isSystemTrayAvailable() # L10n: program name - translatable for startmenu titlebar etc self.setWindowTitle(_('luckyLUKS')) self.setWindowIcon( QIcon.fromTheme( 'dialog-password', QApplication.style().standardIcon(QStyle.SP_DriveHDIcon))) # check if cryptsetup and sudo are installed not_installed_msg = _( '{program_name} executable not found!\nPlease install, eg for Debian/Ubuntu\n`apt-get install {program_name}`' ) if not utils.is_installed('cryptsetup'): show_alert(self, not_installed_msg.format(program_name='cryptsetup'), critical=True) if not utils.is_installed('sudo'): show_alert(self, not_installed_msg.format(program_name='sudo'), critical=True) # quick sanity checks before asking for passwd if os.getuid() == 0: show_alert( self, _('Graphical programs should not be run as root!\nPlease call as normal user.' ), critical=True) if self.encrypted_container and not os.path.exists( self.encrypted_container): show_alert( self, _('Container file not accessible\nor path does not exist:\n\n{file_path}' ).format(file_path=self.encrypted_container), critical=True) # only either encrypted_container or luks_device_name supplied if bool(self.encrypted_container) != bool(self.luks_device_name): show_alert( self, _('Invalid arguments:\n' 'Please call without any arguments\n' 'or supply both container and name.\n\n' '<b>{executable} -c CONTAINER -n NAME [-m MOUNTPOINT]</b>\n\n' 'CONTAINER = Path of the encrypted container file\n' 'NAME = A (unique) name to identify the unlocked container\n' 'Optional: MOUNTPOINT = where to mount the encrypted filesystem\n\n' 'If automatic mounting is configured on your system,\n' 'explicitly setting a mountpoint is not required\n\n' 'For more information, visit\n' '<a href="{project_url}">{project_url}</a>').format( executable=os.path.basename(sys.argv[0]), project_url=PROJECT_URL), critical=True) # spawn worker process with root privileges try: self.worker = utils.WorkerMonitor(self) # start communication thread self.worker.start() except utils.SudoException as se: show_alert(self, format_exception(se), critical=True) return # if no arguments supplied, display dialog to gather this information if self.encrypted_container is None and self.luks_device_name is None: from luckyLUKS.setupUI import SetupDialog sd = SetupDialog(self) if sd.exec_() == QDialog.Accepted: self.luks_device_name = sd.get_luks_device_name() self.encrypted_container = sd.get_encrypted_container() self.mount_point = sd.get_mount_point() self.key_file = sd.get_keyfile() self.is_unlocked = True # all checks in setup dialog -> skip initializing state else: # user closed dialog -> quit program # and check if a keyfile create thread has to be stopped # the worker process terminates itself when its parent dies if hasattr(sd, 'create_thread') and sd.create_thread.isRunning(): sd.create_thread.terminate() QApplication.instance().quit() return # center window on desktop qr = self.frameGeometry() cp = QDesktopWidget().availableGeometry().center() qr.moveCenter(cp) self.move(qr.topLeft()) # widget content main_grid = QGridLayout() main_grid.setSpacing(10) icon = QLabel() icon.setPixmap( QIcon.fromTheme( 'dialog-password', QApplication.style().standardIcon( QStyle.SP_DriveHDIcon)).pixmap(32)) main_grid.addWidget(icon, 0, 0) main_grid.addWidget(QLabel('<b>' + _('Handle encrypted container') + '</b>\n'), 0, 1, alignment=Qt.AlignCenter) main_grid.addWidget(QLabel(_('Name:')), 1, 0) main_grid.addWidget(QLabel( '<b>{dev_name}</b>'.format(dev_name=self.luks_device_name)), 1, 1, alignment=Qt.AlignCenter) main_grid.addWidget(QLabel(_('File:')), 2, 0) main_grid.addWidget(QLabel(self.encrypted_container), 2, 1, alignment=Qt.AlignCenter) if self.key_file is not None: main_grid.addWidget(QLabel(_('Key:')), 3, 0) main_grid.addWidget(QLabel(self.key_file), 3, 1, alignment=Qt.AlignCenter) if self.mount_point is not None: main_grid.addWidget(QLabel(_('Mount:')), 4, 0) main_grid.addWidget(QLabel(self.mount_point), 4, 1, alignment=Qt.AlignCenter) main_grid.addWidget(QLabel(_('Status:')), 5, 0) self.label_status = QLabel('') main_grid.addWidget(self.label_status, 5, 1, alignment=Qt.AlignCenter) self.button_toggle_status = QPushButton('') self.button_toggle_status.setMinimumHeight(34) self.button_toggle_status.clicked.connect(self.toggle_container_status) main_grid.setRowMinimumHeight(6, 10) main_grid.addWidget(self.button_toggle_status, 7, 1) widget = QWidget() widget.setLayout(main_grid) widget.setContentsMargins(10, 10, 10, 10) self.setCentralWidget(widget) # tray popup menu if self.has_tray: tray_popup = QMenu(self) tray_popup.addAction( QIcon.fromTheme( 'dialog-password', QApplication.style().standardIcon(QStyle.SP_DriveHDIcon)), self.luks_device_name).setEnabled(False) tray_popup.addSeparator() self.tray_toggle_action = QAction( QApplication.style().standardIcon(QStyle.SP_DesktopIcon), _('Hide'), self) self.tray_toggle_action.triggered.connect(self.toggle_main_window) tray_popup.addAction(self.tray_toggle_action) quit_action = QAction( QApplication.style().standardIcon( QStyle.SP_MessageBoxCritical), _('Quit'), self) quit_action.triggered.connect(self.tray_quit) tray_popup.addAction(quit_action) # systray self.tray = QSystemTrayIcon(self) self.tray.setIcon( QIcon.fromTheme( 'dialog-password', QApplication.style().standardIcon(QStyle.SP_DriveHDIcon))) self.tray.setContextMenu(tray_popup) self.tray.activated.connect(self.toggle_main_window) self.tray.show() self.init_status()