Beispiel #1
0
class VentanaPrincipal(QTabWidget):

	def __init__(self):
		super(VentanaPrincipal, self).__init__()
		self.ui = Ui_TabWidget()
		self.ui.setupUi(self)

		# DB
		self.db = QSqlDatabase.addDatabase("QSQLITE")
		self.db.setDatabaseName("cuentas.db")
		self.conector = QSqlDatabase.database()
		self.general_query = QSqlQuery()
		self.organizar_db()
		# Querys para psw_tool
		self.master_query = QSqlQuery()
		self.save_query = QSqlQuery()
		self.passwords_query_retrieve = QSqlQuery()
		self.passwords_query_write = QSqlQuery()
		self.popular = QSqlQuery()
		self.comboBoxes_query_categoria = QSqlQuery()
		self.comboBoxes_query_mail = QSqlQuery()
		self.comboBoxes_query_usuario = QSqlQuery()
		self.verificar_columna_contrasenas = QSqlQuery()
		self.filtro = QSqlQuery()
		# Querys para pswItemDelegate
		self.all_data = QSqlQuery()
		#  BASE DE DATOS en UI
		self.model = QSqlTableModel()  # pswItemDelegate.CustomSqlModel()
		self.organizar_tabla_ui()
		self.model.setEditStrategy(QSqlTableModel.OnManualSubmit)  # Va aca abajo por self.model.setTable('passwords')
		# Para cuando se cambian datos
		self.model.dataChanged.connect(self.celdas_cambiadas)
		self.ui.tabla_db.setContextMenuPolicy(Qt.CustomContextMenu)
		self.ui.tabla_db.customContextMenuRequested.connect(self.context_menu)
		# Filtro DB
		# .connect(lambda: self.
		self.tabBarClicked.connect(self.actualizar_tabs)
		# Iconos DB en UI
		self.icon_seguridad = QIcon()
		self.icon_seguridad.addPixmap(QPixmap(":/media/iconografia/locked.png"), QIcon.Normal, QIcon.Off)
		self.icon_desbloqueado = QIcon()
		self.icon_desbloqueado.addPixmap(QPixmap(":/media/iconografia/unlocked.png"), QIcon.Normal, QIcon.Off)
		self.icon_editar = QIcon()
		self.icon_editar.addPixmap(QPixmap(":/media/iconografia/pencil.png"), QIcon.Normal, QIcon.Off)
		self.icon_guardar = QIcon()
		self.icon_guardar.addPixmap(QPixmap(":/media/iconografia/broken-pencil.png"), QIcon.Normal, QIcon.Off)

		# Procesos iniciales
		self.cargar_config()
		self.master_key = None
		self.seguridad_alterada = False
		self.candado = "cerrado"
		self.modo_boton_editar_guardar = "editar"
		self.contrasenas_nuevas = {}
		self.edits = {}
		self.revisar_columna_contrasenas()
		self.cargar_opciones_combo_boxes()
		# Alertas
		# Iconos para las alertas sin UI
		self.icon_ventana = QIcon()
		self.icon_ventana.addPixmap(QPixmap(":/media/imagenes/main_frame.png"), QIcon.Normal, QIcon.Off)
		self.alerta_config = QMessageBox(QMessageBox.Warning,
										"Problema con la configuración actual",
										"Existen dos posibilidades para este error:\n\n1. El archivo de configuración está dañando\n2. Usted tiene todas las opciones desmarcadas (el amor no cuenta)\n\nPara solucionarlo, borre el archivo de configuración ('opciones.ini'),\no marque alguna opción en la pestaña de configuración y guarde su selección\n"
										)
		self.alerta_master_psw_mala = QMessageBox(QMessageBox.Warning,
												"Problema con la contraseña ingresada",
												"Por favor tome precauciones con la elección de la contraseña maestra.\nPara poder proseguir debe ingresar una contraseña con más de 5 y menos de 17 caracteres, o sino presione cancelar."
												)
		self.alerta_master_psw_incorrecta = QMessageBox(QMessageBox.Warning,
														"Problema con la contraseña ingresada",
														"La contraseña que ingresaste es incorrecta"
														)
		self.alerta_guardado_exitoso = QMessageBox(QMessageBox.Information, "Información guardada",
													"Toda la información que ingresaste se guardó con éxito."
												)
		self.alerta_config.setWindowIcon(self.icon_ventana)
		self.alerta_master_psw_mala.setWindowIcon(self.icon_ventana)
		self.alerta_master_psw_incorrecta.setWindowIcon(self.icon_ventana)
		self.alerta_guardado_exitoso.setWindowIcon(self.icon_ventana)
		# Alertas con su propia UI
		# Dialog info
		self.dialogo_info = QDialog()
		self.info_app = Ui_dialogo_info()
		self.info_app.setupUi(self.dialogo_info)
		# Alerta master psw
		self.dialogo_master_psw = QDialog()
		self.alerta_master_psw = Ui_dialogo_master_psw()
		self.alerta_master_psw.setupUi(self.dialogo_master_psw)

		# Botones
		self.ui.boton_guardar_config.clicked.connect(self.guardar_config)
		self.ui.boton_info.clicked.connect(self.cargar_info)
		self.ui.boton_generar.clicked.connect(self.llamar_generar_contrasena)  # Boton generar contrasñea
		self.ui.boton_guardar.clicked.connect(self.guardar_contrasena)  # boton guardar data
		# Si presionan el boton revelar contraseña
		self.ui.reveal_psw.clicked.connect(self.mostrar_contrasena)
		self.alerta_master_psw.reveal_master_psw.clicked.connect(self.mostrar_contrasena_maestra)
		# Icono del boton revelar contraseña
		self.icon_not_view = QIcon()
		self.icon_not_view.addPixmap(QPixmap(":/media/iconografia/not_view.png"), QIcon.Normal, QIcon.Off)
		self.icon_view = QIcon()
		self.icon_view.addPixmap(QPixmap(":/media/iconografia/view.png"), QIcon.Normal, QIcon.Off)
		# Click en botones copiar # Otra manera de hacerlo: partial(self.llamar_copiar(n)) using functools.partial
		self.ui.cp1.clicked.connect(lambda: self.llamar_copiar(1))
		self.ui.cp2.clicked.connect(lambda: self.llamar_copiar(2))
		self.ui.cp3.clicked.connect(lambda: self.llamar_copiar(3))
		# Botones DB
		self.ui.boton_filtro.clicked.connect(lambda: self.filtrar())
		self.ui.boton_editar.clicked.connect(lambda: self.gestor_boton_editar_guardar())
		self.ui.boton_seguridad.clicked.connect(lambda: self.mostrar_contrasenas())
		# Botones Info
		self.info_app.boton_steam.clicked.connect(lambda: backend.abrir_link("https://steamcommunity.com/id/JosephKm"))
		self.info_app.boton_discord.clicked.connect(lambda: backend.abrir_link("https://discord.gg/wYuXPQS"))
		self.info_app.boton_github.clicked.connect(
			lambda: backend.abrir_link("https://github.com/kuttz-dev/Password-manager"))
		# Si se presiona la pestaña de configuracion
		# self.ui.tab_3.connect(self.cargar_config)
		# SETEAR COMBOBOXES
		self.ui.comboBox_usuario.setInsertPolicy(QComboBox.InsertAlphabetically)
		self.ui.comboBox_mail.setInsertPolicy(QComboBox.InsertAlphabetically)
		self.ui.comboBox_categoria.setInsertPolicy(QComboBox.InsertAlphabetically)
		self.ui.comboBox_usuario.setDuplicatesEnabled(False)
		self.ui.comboBox_mail.setDuplicatesEnabled(False)
		self.ui.comboBox_categoria.setDuplicatesEnabled(False)
		self.ui.comboBox_usuario.clearEditText()
		self.ui.comboBox_mail.clearEditText()
		self.ui.comboBox_categoria.clearEditText()


	def organizar_db(self):
		self.general_query.exec_(
			'CREATE TABLE IF NOT EXISTS passwords (id INTEGER PRIMARY KEY ASC, categoria TEXT, icono BLOB, servicio TEXT, mail TEXT, usuario TEXT, contraseña_encriptada BLOB);'
		)
		self.general_query.exec_(
			'CREATE TABLE IF NOT EXISTS maestra (id INTEGER PRIMARY KEY, muestra BLOB);'
		)
		self.general_query.exec_(
			'DELETE FROM passwords WHERE usuario = "" AND mail = "" AND contraseña_encriptada = ""'
		)
		self.db.commit()

	def borrar_columna_contrasenas(self):
		self.general_query.exec_(
			'DROP TABLE IF EXISTS temporal'
		)
		self.general_query.exec_(
			'CREATE TABLE temporal (id INTEGER PRIMARY KEY ASC, categoria TEXT, icono BLOB, servicio TEXT, mail TEXT, usuario TEXT, contraseña_encriptada BLOB);'
		)
		self.popular.exec_(
			'INSERT INTO temporal(id, categoria, icono, servicio, mail, usuario, contraseña_encriptada) SELECT id, categoria, icono, servicio, mail, usuario, contraseña_encriptada FROM passwords'
		)
		self.db.commit()
		self.general_query.exec_(
			'DROP TABLE IF EXISTS passwords'
		)
		self.popular.exec_(
			'ALTER TABLE temporal RENAME TO passwords'
		)
		self.general_query.exec_(
			'DROP TABLE IF EXISTS temporal'
		)
		self.db.commit()

	def cargar_config(self):
		largo, mayus, minus, numeros, special, icono = backend.obtener_cfg()
		self.ui.spinBox_largo.setProperty("value", int(largo))
		self.ui.check_mayus.setChecked(backend.string2bool(mayus))
		self.ui.check_min.setChecked(backend.string2bool(minus))
		self.ui.check_numeros.setChecked(backend.string2bool(numeros))
		self.ui.check_caracteres.setChecked(backend.string2bool(special))
		self.ui.check_amor.setChecked(backend.string2bool(icono))

	def guardar_config(self):
		hay_opcion_verdadera = False
		largo = self.ui.spinBox_largo.value()
		mayus = self.ui.check_mayus.checkState()
		minus = self.ui.check_min.checkState()
		numeros = self.ui.check_numeros.checkState()
		special = self.ui.check_caracteres.checkState()
		icono = self.ui.check_amor.checkState()

		estado_checkeado = [mayus, minus, numeros, special, icono]
		for i in range(len(estado_checkeado)):
			if str(estado_checkeado[i]) == "PySide2.QtCore.Qt.CheckState.Checked":
				if i != 4:  # len de estado_checkeado - 1
					hay_opcion_verdadera = True
				estado_checkeado[i] = True
			else:
				estado_checkeado[i] = False

		if hay_opcion_verdadera is False:
			return self.alerta_config.exec()

		backend.generar_cfg(largo,
							estado_checkeado[0],
							estado_checkeado[1],
							estado_checkeado[2],
							estado_checkeado[3],
							estado_checkeado[4]
							)

		self.cargar_config()

	def cargar_info(self):
		return self.dialogo_info.exec()

	def llamar_generar_contrasena(self):
		#  Primero obtenemos que tipo de contraseña quiere el usuario
		largo, mayus, minus, numeros, special, icono = backend.obtener_cfg()
		mayus = backend.string2bool(mayus)
		minus = backend.string2bool(minus)
		numeros = backend.string2bool(numeros)
		special = backend.string2bool(special)

		if mayus is False and minus is False and numeros is False and special is False:
			self.alerta_config.exec()

		texto_contrasena = backend.generar_contraseña(int(largo), mayus, minus, numeros, special)
		self.ui.input_psw.setText(str(texto_contrasena))  # Ponemos la contraseña en la aplicacion

	def mostrar_contrasena(self):
		# Si esta en password a normal y viceversa
		if self.ui.input_psw.echoMode() == QLineEdit.EchoMode.Password:
			self.ui.reveal_psw.setIcon(self.icon_not_view)
			self.ui.input_psw.setEchoMode(QLineEdit.Normal)
		else:
			self.ui.reveal_psw.setIcon(self.icon_view)
			self.ui.input_psw.setEchoMode(QLineEdit.Password)

	def mostrar_contrasena_maestra(self, modo_echo=False):
		# Si no se cambio el estado va a estar en False y se va a usar el echoMode del input
		if modo_echo is False:
			modo_echo = self.alerta_master_psw.input_master_psw.echoMode()
		# Si esta en password a normal y viceversa
		if modo_echo == QLineEdit.EchoMode.Password:
			self.alerta_master_psw.reveal_master_psw.setIcon(self.icon_not_view)
			self.alerta_master_psw.input_master_psw.setEchoMode(QLineEdit.Normal)
		else:
			self.alerta_master_psw.reveal_master_psw.setIcon(self.icon_view)
			self.alerta_master_psw.input_master_psw.setEchoMode(QLineEdit.Password)

	def llamar_copiar(self, numero_boton):
		if numero_boton == 1:
			backend.copiar(str(self.ui.input_psw.text()))
		if numero_boton == 2:
			backend.copiar(str(self.ui.comboBox_usuario.currentText()))
		if numero_boton == 3:
			backend.copiar(str(self.ui.comboBox_mail.currentText()))

	def preparar_favicon(self, url):
		try:
			archivo_ico = backend.descargar_favico(url)
		except Exception:  # Si lo que se ingreso era un link pero no se consiguio favicon
			with open("media/favicons/domain.ico") as ico:
				return QByteArray(ico.read())
		# Si no se consiguio la imagen
		if archivo_ico is None:
			return None
		with open(archivo_ico, "rb") as ico:
			return QByteArray(ico.read())

	def guardar_contrasena(self):
		# self.ui.comboBox_usuario.currentText() / self.ui.comboBox_mail.currentText()
		# self.ui.comboBox_categoria.currentText() / self.ui.input_url.text()
		# self.ui.input_psw.text()

		if self.master_key is None:
			try:
				self.master_key = self.pedir_contrasena_maestra()
			except Exception:
				return
		if self.ui.input_psw.text() != "":
			contrasena_ingresada_encriptada = QByteArray(pswCrypto.encriptar(self.ui.input_psw.text(), self.master_key))
		else:
			contrasena_ingresada_encriptada = ""

		try:
			fav_icon = self.preparar_favicon(self.ui.input_url.text())

			self.save_query.prepare(
				'INSERT INTO passwords (categoria, icono, servicio, mail, usuario, contraseña_encriptada) VALUES(?,?,?,?,?,?)'
			)
			self.save_query.addBindValue(self.ui.comboBox_categoria.currentText())
			self.save_query.addBindValue(fav_icon)
			self.save_query.addBindValue(self.ui.input_url.text())
			self.save_query.addBindValue(self.ui.comboBox_mail.currentText())
			self.save_query.addBindValue(self.ui.comboBox_usuario.currentText())
			self.save_query.addBindValue(contrasena_ingresada_encriptada)
			self.save_query.exec_()
			self.db.commit()
			self.model.select()
			self.ui.tabla_db.resizeColumnsToContents()
			self.cargar_opciones_combo_boxes()
			return self.alerta_guardado_exitoso.exec()

		except Exception as ex:
			template = "An exception of type {0} occurred. Arguments:\n{1!r}"
			message = template.format(type(ex).__name__, ex.args)
			print(message)

	def pedir_contrasena_maestra(self):
		self.dialogo_master_psw.exec()

		contrasena_maestra = self.alerta_master_psw.input_master_psw.text()
		# Borrarmos el texto porque no se borra solo, y lo volvemos secreto de nuevo
		self.alerta_master_psw.input_master_psw.setText("")
		self.mostrar_contrasena_maestra(QLineEdit.EchoMode.Normal)

		# Si le dio a cancelar
		if bool(self.dialogo_master_psw.result()) is False:
			raise Exception("Accion canelada")

		# Comprobamos que cumpla requisitos
		if contrasena_maestra == "" or len(contrasena_maestra) < 6 or len(contrasena_maestra) > 16:
			self.alerta_master_psw_mala.exec()
			return self.pedir_contraseña_maestra()

		# Encriptacion
		key_contrasena_maestra = pswCrypto.generar_key(contrasena_maestra)  # La convertimos en una key
		# Verificacion
		# Obtenemos la muestra guardada en la db
		muestra_db = backend.obtener_muestra_db()
		# Si no habia guardamos una nueva con esta contraseña maestra
		if muestra_db is None:
			array = QByteArray(backend.generar_muestra(key_contrasena_maestra))
			self.master_query.prepare("INSERT INTO maestra (id, muestra) VALUES(1, ?)")
			self.master_query.addBindValue(array)
			self.master_query.exec_()

			return key_contrasena_maestra

		else:
			# Ahora si verificamos
			psw_correcta = backend.verificar_key(muestra_db, key_contrasena_maestra)
			if psw_correcta is True:
				return key_contrasena_maestra
			else:
				self.alerta_master_psw_incorrecta.exec_()
				raise Exception("Contraseña maestra incorrecta")

	def filtrar(self):
		if self.ui.input_filtro.text() == "":
			self.model.setFilter("")
			return self.model.select()
		else:
			self.model.setFilter(
				"{} LIKE '{}%'".format(self.ui.combobox_filtro.currentText().lower(), self.ui.input_filtro.text())
			)
			return self.model.select()

	def mostrar_contrasenas(self):
		if self.master_key is None:
			try:
				self.master_key = self.pedir_contrasena_maestra()
			except Exception:
				return

		if self.candado == "abierto":  # Si esta abierto
			self.borrar_columna_contrasenas()
			self.ui.combobox_filtro.removeItem(4)
			self.organizar_tabla_ui()
			self.ui.boton_seguridad.setIcon(self.icon_seguridad)
			self.candado = "cerrado"  # lo cerramos
			return

		else:  # Si estaba cerrado
			self.candado = "abierto"  # lo abrimos
		# y se abre asi:
		self.ui.boton_seguridad.setIcon(self.icon_desbloqueado)
		# Conseguimos las contraseñas encriptadas
		self.passwords_query_retrieve.exec_('SELECT id, contraseña_encriptada FROM passwords')
		# Creamos una columna para las contraseñas descifradas
		self.general_query.exec_('ALTER TABLE passwords ADD contraseña TEXT')
		self.db.commit()
		# Para cada contraseña
		while self.passwords_query_retrieve.next():
			# Si se guardo un texto vacio
			contrasena_descifrada = self.passwords_query_retrieve.value(1)
			if type(contrasena_descifrada) != str:
				contrasena_descifrada = pswCrypto.descifrar(contrasena_descifrada.data(), self.master_key)

			self.passwords_query_write.prepare('UPDATE passwords SET contraseña = ? WHERE id= ?')
			self.passwords_query_write.addBindValue(contrasena_descifrada)
			self.passwords_query_write.addBindValue(self.passwords_query_retrieve.value(0))
			self.passwords_query_write.exec_()
		self.ui.combobox_filtro.addItem("Contraseña")
		self.organizar_tabla_ui()  # Las hacemos visibles el la tabla

	def actualizar_tabs(self, index):
		if index == 0:
			self.resize(437, 200)
			self.cargar_opciones_combo_boxes()
		if index == 1 and self.ui.boton_seguridad.isEnabled() is True:
			self.organizar_tabla_ui()
		elif index == 2:
			self.resize(437, 200)
			self.cargar_config()

	def organizar_tabla_ui(self):
		self.model.setTable('passwords')
		self.model.setSort(1, Qt.AscendingOrder)
		self.model.select()
		self.ui.tabla_db.setModel(self.model)
		self.ui.tabla_db.setItemDelegateForColumn(2, pswItemDelegate.ImageDelegate(self.ui.tabla_db))
		self.ui.tabla_db.hideColumn(0)  # Escondemos id
		self.ui.tabla_db.hideColumn(6)  # Escondemos las contraseñas encriptadas
		self.ui.tabla_db.setWindowTitle("Lista de cuentas")
		# Tamaño columnas
		#self.ui.tabla_db.resizeColumnsToContents()
		self.ui.tabla_db.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
		self.ui.tabla_db.horizontalHeader().setSectionResizeMode(2, QHeaderView.Fixed)  # iconos
		self.ui.tabla_db.setColumnWidth(2, 32)  # tamaño iconos
		self.ui.tabla_db.verticalHeader().setVisible(False)
		self.ui.tabla_db.setSortingEnabled(True)

	def cargar_opciones_combo_boxes(self):
		a = self.ui.comboBox_categoria.currentText()
		b = self.ui.comboBox_mail.currentText()
		c = self.ui.comboBox_usuario.currentText()

		self.comboBoxes_query_categoria.exec_(
			'SELECT DISTINCT categoria FROM passwords'
		)
		self.comboBoxes_query_mail.exec_(
			'SELECT DISTINCT mail FROM passwords'
		)
		self.comboBoxes_query_usuario.exec_(
			'SELECT DISTINCT usuario FROM passwords'
		)
		# Borramos las anteriores
		self.ui.comboBox_usuario.clear()
		self.ui.comboBox_mail.clear()
		self.ui.comboBox_categoria.clear()
		# Cargamos las nuevas
		while self.comboBoxes_query_categoria.next():
			if self.comboBoxes_query_categoria.value(0) == "" or self.comboBoxes_query_categoria.value(0) is None:
				continue
			self.ui.comboBox_categoria.addItem(self.comboBoxes_query_categoria.value(0))

		while self.comboBoxes_query_mail.next():
			if self.comboBoxes_query_mail.value(0) == "" or self.comboBoxes_query_mail.value(0) is None:
				continue
			self.ui.comboBox_mail.addItem(self.comboBoxes_query_mail.value(0))

		while self.comboBoxes_query_usuario.next():
			if self.comboBoxes_query_usuario.value(0) == "" or self.comboBoxes_query_usuario.value(0) is None:
				continue
			self.ui.comboBox_usuario.addItem(self.comboBoxes_query_usuario.value(0))
		self.ui.comboBox_categoria.setCurrentText(a)
		self.ui.comboBox_mail.setCurrentText(b)
		self.ui.comboBox_usuario.setCurrentText(c)

	def revisar_columna_contrasenas(self, borrar=True):
		existe = self.verificar_columna_contrasenas.exec_('SELECT contraseña FROM passwords LIMIT 1')
		if existe is True and borrar is True:
			self.verificar_columna_contrasenas.finish()
			self.borrar_columna_contrasenas()
			return self.organizar_tabla_ui()

		elif existe is True:
			return "Existe"

	def gestor_boton_editar_guardar(self):
		if self.modo_boton_editar_guardar == "editar":
			try:
				self.editar_tabla_ui()
			except Exception:
				return
			self.modo_boton_editar_guardar = "guardar"  # Cambiamos al proximo modo
			return self.ui.boton_editar.setIcon(self.icon_guardar)

		elif self.modo_boton_editar_guardar == "guardar":
			self.borrar_columna_contrasenas()
			self.organizar_tabla_ui()
			self.candado = "cerrado"
			self.ui.boton_seguridad.setDisabled(False)
			self.ui.boton_seguridad.setIcon(self.icon_seguridad)
			self.ui.boton_editar.setIcon(self.icon_editar)
			self.modo_boton_editar_guardar = "editar"  # Cambiamos al proximo modo
			return

	def editar_tabla_ui(self):
		if self.master_key is None:
			try:
				self.master_key = self.pedir_contrasena_maestra()
			except Exception:
				raise Exception("No se pudo conseguir master key")

		self.ui.boton_seguridad.setDisabled(True)
		if self.candado == "cerrado":
			self.mostrar_contrasenas()

	def celdas_cambiadas(self, top_left, bottom_right):
		if self.modo_boton_editar_guardar == "editar":
			return
		else:
			if top_left.column() == 7:

				contrasena_editada_encriptada = ""
				if self.model.record(top_left.row()).value('contraseña') != "":
					contrasena_editada_encriptada = QByteArray(
						pswCrypto.encriptar(self.model.record(top_left.row()).value('contraseña'), self.master_key))

				casilla_editada = QSqlField("contraseña_encriptada")
				casilla_editada.setValue(contrasena_editada_encriptada)
				valores_fila = self.model.record(top_left.row())
				valores_fila.replace(6, casilla_editada)
				return self.model.updateRowInTable(top_left.row(), valores_fila)
			else:
				self.model.updateRowInTable(top_left.row(), self.model.record(top_left.row()))
				return self.db.commit()

	def borrar_linea(self, row):
		self.model.deleteRowFromTable(row)
		return self.model.select()

	def context_menu(self):
		# Menu con click derecho
		if self.ui.tabla_db.selectedIndexes() and self.modo_boton_editar_guardar == "guardar":
			menu = QMenu()
			borrar_data = menu.addAction("Borrar linea de la base de datos")
			borrar_data.triggered.connect(lambda: self.borrar_linea(self.ui.tabla_db.currentIndex().row()))
			cursor = QCursor()
			menu.exec_(cursor.pos())
Beispiel #2
0
class Settings(object):
    class _MigrationFailed(ExpectedError):
        pass

    def __init__(self,
                 cfg,
                 main_cfg,
                 start_service,
                 exit_service,
                 parent=None,
                 size=None,
                 migrate=False,
                 dp=1,
                 get_offline_dirs=lambda: None,
                 set_offline_dirs=lambda o, no: None):
        super(Settings, self).__init__()
        self._cfg = cfg
        self._main_cfg = main_cfg
        self._start_service = start_service
        self._exit_service = exit_service
        self._parent = parent
        self._size = size
        self._dp = dp
        self._get_offline_dirs = get_offline_dirs
        self._set_offline_dirs = set_offline_dirs

        self._dialog = QDialog(parent)
        self._dialog.setWindowIcon(QIcon(':/images/icon.png'))
        self._dialog.setAttribute(Qt.WA_MacFrameworkScaled)
        self._ui = settings.Ui_Dialog()
        self._ui.setupUi(self._dialog)
        self._max_root_len = get_max_root_len(self._cfg)
        self._migrate = migrate
        self._migration = None
        self._migration_cancelled = False

        try:
            self._ui.account_type.setText(
                license_display_name_from_constant(self._cfg.license_type))
            self._ui.account_type.setVisible(True)
            self._ui.account_type_header.setVisible(True)
            self._ui.account_upgrade.setVisible(True)
        except KeyError:
            pass
        upgrade_license_types = (FREE_LICENSE, FREE_TRIAL_LICENSE)
        if self._cfg.license_type in upgrade_license_types:
            self._ui.account_upgrade.setText('<a href="{}">{}</a>'.format(
                GET_PRO_URI.format(self._cfg.host), tr('Upgrade')))
            self._ui.account_upgrade.setTextFormat(Qt.RichText)
            self._ui.account_upgrade.setTextInteractionFlags(
                Qt.TextBrowserInteraction)
            self._ui.account_upgrade.setOpenExternalLinks(True)
            self._ui.account_upgrade.setAlignment(Qt.AlignLeft)
        else:
            self._ui.account_upgrade.setText("")

        self._ui.centralWidget.setFrameShape(QFrame.NoFrame)
        self._ui.centralWidget.setLineWidth(1)

        self._ui.language_comboBox.addItem(tr('English'))
        self._ui.language_comboBox.setEnabled(False)

        self._connect_slots()
        self._set_fonts()
        self._ui.tabWidget.setCurrentIndex(0)

        self._smart_sync_dialog = None

        self.logged_out = Signal(bool)
        self.logging_disabled_changed = Signal(bool)

        # FIXMe: without line below app crashes on exit after settings opened
        self._dialog.mousePressEvent = self.on_mouse_press_event

    def on_mouse_press_event(self, ev):
        pass

    def _connect_slots(self):
        ui = self._ui

        ui.logout_button.clicked.connect(self._logout)

        ui.download_auto_radioButton.clicked.connect(
            lambda: ui.download_limit_edit.setEnabled(
                False) or ui.download_limit_edit.clear())
        ui.download_limit_radioButton.clicked.connect(
            lambda: ui.download_limit_edit.setEnabled(True))

        ui.upload_auto_radioButton.clicked.connect(
            lambda: ui.upload_limit_edit.setEnabled(
                False) or ui.upload_limit_edit.clear())
        ui.upload_limit_radioButton.clicked.connect(
            lambda: ui.upload_limit_edit.setEnabled(True))

        ui.buttonBox.accepted.connect(self._dialog.accept)
        ui.buttonBox.rejected.connect(self._dialog.reject)

        ui.smart_sync_button.clicked.connect(
            self._on_smart_sync_button_clicked)

        ui.location_button.clicked.connect(
            self._on_sync_folder_location_button_clicked)

        ui.location_button.enterEvent = lambda _: \
            ui.location_button.setIcon(QIcon(
                ':/images/settings/pencil_hovered.svg'))
        ui.location_button.leaveEvent = lambda _: \
            ui.location_button.setIcon(QIcon(
                ':/images/settings/pencil.svg'))
        ui.smart_sync_button.enterEvent = lambda _: \
            ui.smart_sync_button.setIcon(QIcon(
                ':/images/settings/folder_sync_hovered.svg'))
        ui.smart_sync_button.leaveEvent = lambda _: \
            ui.smart_sync_button.setIcon(QIcon(
                ':/images/settings/folder_sync.svg'))
        ui.logout_button.enterEvent = lambda _: \
            ui.logout_button.setIcon(QIcon(
                ':/images/settings/logout_hovered.svg'))
        ui.logout_button.leaveEvent = lambda _: \
            ui.logout_button.setIcon(QIcon(
                ':/images/settings/logout.svg'))

    def _set_fonts(self):
        ui = self._ui
        controls = [ui.tabWidget, ui.language_comboBox]
        controls.extend([c for c in ui.tabWidget.findChildren(QLabel)])
        controls.extend([c for c in ui.tabWidget.findChildren(QLineEdit)])
        controls.extend([c for c in ui.tabWidget.findChildren(QPushButton)])
        controls.extend([c for c in ui.tabWidget.findChildren(QCheckBox)])
        controls.extend([c for c in ui.tabWidget.findChildren(QRadioButton)])

        for control in controls:
            font = control.font()
            font_size = control.font().pointSize() * self._dp
            if font_size > 0:
                control_font = QFont(font.family(), font_size)
                control_font.setBold(font.bold())
                control.setFont(control_font)

    def _logout(self):
        userAnswer = msgbox(tr('Keep local files on device?'),
                            buttons=[
                                (tr('Clear all'), 'Wipe'),
                                (tr('Keep'), 'Keep'),
                            ],
                            parent=self._dialog,
                            default_index=1,
                            enable_close_button=True)

        if userAnswer == '':
            return

        wipe_all = userAnswer == 'Wipe'
        if not wipe_all:
            self._cfg.set_settings({'user_password_hash': ""})

        self.logged_out.emit(wipe_all)

        self._dialog.reject()

    def show(self, on_finished):
        def finished():
            if self._dialog.result() == QDialog.Accepted:
                self._apply_settings()
            self._dialog.finished.disconnect(finished)
            on_finished()

        self._setup_to_ui()
        if self._migrate:
            self._ui.tabWidget.setCurrentIndex(1)  # Account page
            QTimer.singleShot(100,
                              self._on_sync_folder_location_button_clicked)
        self._dialog.finished.connect(finished)
        self._dialog.raise_()
        self._dialog.setModal(True)
        self._dialog.show()

    def _setup_to_ui(self):
        ui = self._ui
        cfg = self._cfg

        portable = is_portable()

        if cfg.get_setting('lang', None) is None:
            self._ui.language_comboBox.setCurrentIndex(0)
        else:
            lang = cfg.lang if cfg.lang in get_available_languages() else 'en'
            assert lang in get_available_languages()
            for i in range(1, ui.language_comboBox.count()):
                if ui.language_comboBox.itemText(i) == lang:
                    ui.language_comboBox.setCurrentIndex(i)
                    break

        ui.location_edit.setText(
            FilePath(cfg.sync_directory) if cfg.sync_directory else '')
        ui.location_button.setEnabled(not portable)
        if portable:
            ui.location_button.setToolTip(tr("Disabled in portable version"))
        ui.email_label.setText(cfg.user_email if cfg.user_email else '')

        def set_limit(limit, auto_btn, manual_btn, edit):
            edit.setValidator(QRegExpValidator(QRegExp("\\d{1,9}")))
            if limit:
                manual_btn.setChecked(True)
                edit.setText(str(limit))
            else:
                auto_btn.setChecked(True)
                auto_btn.click()

        set_limit(limit=cfg.download_limit,
                  auto_btn=ui.download_auto_radioButton,
                  manual_btn=ui.download_limit_radioButton,
                  edit=ui.download_limit_edit)
        set_limit(limit=cfg.upload_limit,
                  auto_btn=ui.upload_auto_radioButton,
                  manual_btn=ui.upload_limit_radioButton,
                  edit=ui.upload_limit_edit)

        ui.autologin_checkbox.setChecked(self._main_cfg.autologin)
        ui.autologin_checkbox.setEnabled(not portable)
        if portable:
            ui.autologin_checkbox.setToolTip(
                tr("Disabled in portable version"))
        ui.tracking_checkbox.setChecked(cfg.send_statistics)
        ui.autoupdate_checkbox.setChecked(self._main_cfg.autoupdate)
        ui.download_backups_checkBox.setChecked(cfg.download_backups)
        ui.is_smart_sync_checkBox.setChecked(cfg.smart_sync)
        ui.disable_logging_checkBox.setChecked(self._main_cfg.logging_disabled)

        # Disable smart sync for free license
        if not cfg.license_type or cfg.license_type == FREE_LICENSE:
            ui.is_smart_sync_checkBox.setText(
                tr("SmartSync+ is not available for your license"))
            ui.is_smart_sync_checkBox.setChecked(False)
            ui.is_smart_sync_checkBox.setCheckable(False)
            ui.smart_sync_button.setEnabled(False)

        ui.startup_checkbox.setChecked(is_in_system_startup())
        ui.startup_checkbox.setEnabled(not portable)
        if portable:
            ui.startup_checkbox.setToolTip(tr("Disabled in portable version"))

    def _apply_settings(self):
        service_settings, main_settings = self._get_configs_from_ui()
        if main_settings['logging_disabled'] != \
                self._main_cfg.logging_disabled:
            self.logging_disabled_changed.emit(
                main_settings['logging_disabled'])
        self._cfg.set_settings(service_settings)
        self._main_cfg.set_settings(main_settings)
        if self._ui.startup_checkbox.isChecked():
            if not is_in_system_startup():
                add_to_system_startup()
        else:
            if is_in_system_startup():
                remove_from_system_startup()

    def _config_is_changed(self):
        service_settings, main_settings = self._get_configs_from_ui()
        for param, value in service_settings.items():
            if self._cfg.get_setting(param) != value:
                return True
        for param, value in main_settings.items():
            if self._main_cfg.get_setting(param) != value:
                return True

        return False

    def _get_configs_from_ui(self):
        ui = self._ui
        return {
            'lang': (str(ui.language_comboBox.currentText())
                     if ui.language_comboBox.currentIndex() > 0 else None),
            'upload_limit': (0 if ui.upload_auto_radioButton.isChecked()
                             or not ui.upload_limit_edit.text() else int(
                                 ui.upload_limit_edit.text())),
            'download_limit': (0 if ui.download_auto_radioButton.isChecked()
                               or not ui.download_limit_edit.text() else int(
                                   ui.download_limit_edit.text())),
            'send_statistics':
            bool(ui.tracking_checkbox.isChecked()),
            'download_backups':
            bool(ui.download_backups_checkBox.isChecked()),
            'smart_sync':
            bool(ui.is_smart_sync_checkBox.isChecked()),
            'autologin':
            bool(ui.autologin_checkbox.isChecked()),
        }, {
            'autologin': bool(ui.autologin_checkbox.isChecked()),
            'autoupdate': bool(ui.autoupdate_checkbox.isChecked()),
            'logging_disabled': bool(ui.disable_logging_checkBox.isChecked()),
            'download_backups': bool(ui.download_backups_checkBox.isChecked()),
        }

    def _on_smart_sync_button_clicked(self):
        self._get_offline_dirs()
        root = str(self._ui.location_edit.text())
        self._smart_sync_dialog = SmartSyncDialog(self._dialog)
        offline, online = self._smart_sync_dialog.show(root_path=root,
                                                       hide_dotted=True)
        if offline or online:
            logger.info("Directories set to be offline: (%s)",
                        ", ".join(map(lambda s: u"'%s'" % s, offline)))
            self._set_offline_dirs(offline, online)

    def offline_dirs(self, offline_dirs):
        root = str(self._ui.location_edit.text())
        pc = PathConverter(root)
        offline_dirs_abs_paths = set(
            map(lambda p: pc.create_abspath(p), offline_dirs))
        if self._smart_sync_dialog:
            self._smart_sync_dialog.set_offline_paths(offline_dirs_abs_paths)

    def _on_sync_folder_location_button_clicked(self):
        selected_folder = QFileDialog.getExistingDirectory(
            self._dialog, tr('Choose Pvtbox folder location'),
            get_parent_dir(FilePath(self._cfg.sync_directory)))
        selected_folder = ensure_unicode(selected_folder)

        try:
            if not selected_folder:
                raise self._MigrationFailed("Folder is not selected")

            if len(selected_folder + "/Pvtbox") > self._max_root_len:
                if not self._migrate:
                    msgbox(tr("Destination path too long. "
                              "Please select shorter path."),
                           tr("Path too long"),
                           parent=self._dialog)
                raise self._MigrationFailed("Destination path too long")

            free_space = get_free_space(selected_folder)
            selected_folder = get_data_dir(dir_parent=selected_folder,
                                           create=False)
            if FilePath(selected_folder) == FilePath(self._cfg.sync_directory):
                raise self._MigrationFailed("Same path selected")

            if FilePath(selected_folder) in FilePath(self._cfg.sync_directory):
                msgbox(tr("Can't migrate into existing Pvtbox folder.\n"
                          "Please choose other location"),
                       tr("Invalid Pvtbox folder location"),
                       parent=self._dialog)
                raise self._MigrationFailed(
                    "Can't migrate into existing Pvtbox folder")

            if self._size and free_space < self._size:
                logger.debug(
                    "No disk space in %s. Free space: %s. Needed: %s.",
                    selected_folder, free_space, self._size)
                msgbox(tr(
                    "Insufficient disk space for migration to\n{}.\n"
                    "Please clean disk", selected_folder),
                       tr("No disk space"),
                       parent=self._dialog)
                raise self._MigrationFailed(
                    "Insufficient disk space for migration")

            self._migration_cancelled = False
            dialog = QProgressDialog(self._dialog)
            dialog.setWindowTitle(tr('Migrating to new Pvtbox folder'))
            dialog.setWindowIcon(QIcon(':/images/icon.svg'))
            dialog.setModal(True)
            dialog.setMinimum(0)
            dialog.setMaximum(100)
            dialog.setMinimumSize(400, 80)
            dialog.setAutoClose(False)

            def progress(value):
                logger.debug("Migration dialog progress received: %s", value)
                dialog.setValue(value)

            def migration_failed(error):
                logger.warning("Migration failed with error: %s", error)
                msgbox(error,
                       tr('Migration to new Pvtbox folder error'),
                       parent=dialog)
                dialog.cancel()
                self._migration_cancelled = True
                done()

            def cancel():
                logger.debug("Migration dialog cancelled")
                self._migration_cancelled = True
                self._migration.cancel()

            def done():
                logger.debug("Migration done")
                try:
                    self._migration.progress.disconnect(progress)
                    self._migration.failed.disconnect(migration_failed)
                    self._migration.done.disconnect(done)
                    dialog.canceled.disconnect(cancel)
                except Exception as e:
                    logger.warning("Can't disconnect signal %s", e)
                dialog.hide()
                dialog.done(QDialog.Accepted)
                dialog.close()

            self._migration = SyncDirMigration(self._cfg, parent=self._dialog)
            self._migration.progress.connect(progress, Qt.QueuedConnection)
            self._migration.failed.connect(migration_failed,
                                           Qt.QueuedConnection)
            self._migration.done.connect(done, Qt.QueuedConnection)
            dialog.canceled.connect(cancel)
            self._exit_service()
            old_dir = self._cfg.sync_directory
            self._migration.migrate(old_dir, selected_folder)

            def on_finished():
                logger.info("Migration dialog closed")
                if not self._migration_cancelled:
                    logger.debug("Setting new location")
                    self._ui.location_edit.setText(FilePath(selected_folder))

                    disable_file_logging(logger)
                    shutil.rmtree(op.join(old_dir, '.pvtbox'),
                                  ignore_errors=True)
                    set_root_directory(FilePath(selected_folder))
                    enable_file_logging(logger)

                    make_dir_hidden(get_patches_dir(selected_folder))

                self._start_service()

            dialog.finished.connect(on_finished)
            dialog.show()

        except self._MigrationFailed as e:
            logger.warning("Sync dir migration failed. Reason: %s", e)
        finally:
            if self._migrate:
                self._dialog.accept()