From e1e61a62b933e710c68c58554b8f93d575c38bb2 Mon Sep 17 00:00:00 2001 From: yakimka Date: Sat, 14 Dec 2019 23:19:48 +0200 Subject: [PATCH] Settings (#3) * Add settings window * Add tests --- CherryTomato/__init__.py | 4 + CherryTomato/about_ui.py | 50 +++--- CherryTomato/about_ui.ui | 6 +- CherryTomato/{about.py => about_window.py} | 5 +- CherryTomato/main.py | 9 +- CherryTomato/main_ui.py | 4 + CherryTomato/main_ui.ui | 6 + CherryTomato/main_window.py | 78 ++++++---- CherryTomato/settings.py | 142 +++++++++++++++++ CherryTomato/settings_ui.py | 105 +++++++++++++ CherryTomato/settings_ui.ui | 172 +++++++++++++++++++++ CherryTomato/settings_window.py | 50 ++++++ CherryTomato/tomato_timer.py | 38 ++--- tests/conftest.py | 42 +++-- tests/test_tomato_timer.py | 40 ++--- tests/test_tomato_timer_slots.py | 4 +- tests/ui/test_main_window.py | 30 ++++ 17 files changed, 672 insertions(+), 113 deletions(-) rename CherryTomato/{about.py => about_window.py} (65%) create mode 100644 CherryTomato/settings.py create mode 100644 CherryTomato/settings_ui.py create mode 100644 CherryTomato/settings_ui.ui create mode 100644 CherryTomato/settings_window.py create mode 100644 tests/ui/test_main_window.py diff --git a/CherryTomato/__init__.py b/CherryTomato/__init__.py index acdc0a1..a8a8035 100644 --- a/CherryTomato/__init__.py +++ b/CherryTomato/__init__.py @@ -3,3 +3,7 @@ BASE_DIR = os.path.dirname(os.path.abspath(__file__)) MEDIA_DIR = os.path.join(BASE_DIR, 'media') APP_ICON = os.path.join(MEDIA_DIR, 'icon.png') + +VERSION = '0.2.0' +ORGANIZATION_NAME = 'CherryTomato' +APPLICATION_NAME = 'CherryTomato' diff --git a/CherryTomato/about_ui.py b/CherryTomato/about_ui.py index 341b77d..412bd98 100644 --- a/CherryTomato/about_ui.py +++ b/CherryTomato/about_ui.py @@ -16,43 +16,43 @@ def setupUi(self, About): About.resize(402, 222) self.verticalLayout = QtWidgets.QVBoxLayout(About) self.verticalLayout.setObjectName("verticalLayout") - self.label_title = QtWidgets.QLabel(About) + self.labelTitle = QtWidgets.QLabel(About) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.label_title.sizePolicy().hasHeightForWidth()) - self.label_title.setSizePolicy(sizePolicy) + sizePolicy.setHeightForWidth(self.labelTitle.sizePolicy().hasHeightForWidth()) + self.labelTitle.setSizePolicy(sizePolicy) font = QtGui.QFont() font.setPointSize(13) font.setBold(True) font.setWeight(75) - self.label_title.setFont(font) - self.label_title.setText("CherryTomato") - self.label_title.setAlignment(QtCore.Qt.AlignHCenter|QtCore.Qt.AlignTop) - self.label_title.setObjectName("label_title") - self.verticalLayout.addWidget(self.label_title) - self.label_text = QtWidgets.QLabel(About) + self.labelTitle.setFont(font) + self.labelTitle.setText("CherryTomato") + self.labelTitle.setAlignment(QtCore.Qt.AlignHCenter|QtCore.Qt.AlignTop) + self.labelTitle.setObjectName("labelTitle") + self.verticalLayout.addWidget(self.labelTitle) + self.labelText = QtWidgets.QLabel(About) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.MinimumExpanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(1) - sizePolicy.setHeightForWidth(self.label_text.sizePolicy().hasHeightForWidth()) - self.label_text.setSizePolicy(sizePolicy) - self.label_text.setTextFormat(QtCore.Qt.RichText) - self.label_text.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) - self.label_text.setWordWrap(True) - self.label_text.setOpenExternalLinks(True) - self.label_text.setObjectName("label_text") - self.verticalLayout.addWidget(self.label_text) - self.label_copyright = QtWidgets.QLabel(About) + sizePolicy.setHeightForWidth(self.labelText.sizePolicy().hasHeightForWidth()) + self.labelText.setSizePolicy(sizePolicy) + self.labelText.setTextFormat(QtCore.Qt.RichText) + self.labelText.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) + self.labelText.setWordWrap(True) + self.labelText.setOpenExternalLinks(True) + self.labelText.setObjectName("labelText") + self.verticalLayout.addWidget(self.labelText) + self.labelCopyright = QtWidgets.QLabel(About) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.label_copyright.sizePolicy().hasHeightForWidth()) - self.label_copyright.setSizePolicy(sizePolicy) - self.label_copyright.setText("Copyright © 2019 yakimka") - self.label_copyright.setAlignment(QtCore.Qt.AlignBottom|QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft) - self.label_copyright.setObjectName("label_copyright") - self.verticalLayout.addWidget(self.label_copyright) + sizePolicy.setHeightForWidth(self.labelCopyright.sizePolicy().hasHeightForWidth()) + self.labelCopyright.setSizePolicy(sizePolicy) + self.labelCopyright.setText("Copyright © 2019 yakimka") + self.labelCopyright.setAlignment(QtCore.Qt.AlignBottom|QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft) + self.labelCopyright.setObjectName("labelCopyright") + self.verticalLayout.addWidget(self.labelCopyright) self.retranslateUi(About) QtCore.QMetaObject.connectSlotsByName(About) @@ -60,4 +60,4 @@ def setupUi(self, About): def retranslateUi(self, About): _translate = QtCore.QCoreApplication.translate About.setWindowTitle(_translate("About", "About CherryTomato")) - self.label_text.setText(_translate("About", "

CherryTomato is a simple tomato timer app.

It\'s written in Python 3 using PyQt5.

Project page: https://github.com/yakimka/CherryTomato

")) + self.labelText.setText(_translate("About", "

CherryTomato is a simple tomato timer app.

It\'s written in Python 3 using PyQt5.

Project page: https://github.com/yakimka/CherryTomato

")) diff --git a/CherryTomato/about_ui.ui b/CherryTomato/about_ui.ui index 6c569cd..699a2af 100644 --- a/CherryTomato/about_ui.ui +++ b/CherryTomato/about_ui.ui @@ -15,7 +15,7 @@ - + 0 @@ -38,7 +38,7 @@ - + 0 @@ -63,7 +63,7 @@ - + 0 diff --git a/CherryTomato/about.py b/CherryTomato/about_window.py similarity index 65% rename from CherryTomato/about.py rename to CherryTomato/about_window.py index 46886ff..15b08b6 100644 --- a/CherryTomato/about.py +++ b/CherryTomato/about_window.py @@ -1,7 +1,7 @@ from PyQt5 import QtWidgets from PyQt5.QtGui import QIcon -from CherryTomato import APP_ICON +from CherryTomato import APP_ICON, VERSION from .about_ui import Ui_About @@ -11,4 +11,7 @@ def __init__(self): self.setupUi(self) + title = f'{self.labelTitle.text()} {VERSION}' + self.labelTitle.setText(title) + self.setWindowIcon(QIcon(APP_ICON)) diff --git a/CherryTomato/main.py b/CherryTomato/main.py index a78783e..4b79ead 100755 --- a/CherryTomato/main.py +++ b/CherryTomato/main.py @@ -3,12 +3,17 @@ import sys from PyQt5 import Qt +from PyQt5.QtCore import QCoreApplication -from CherryTomato.main_window import TomatoTimerWindow +from CherryTomato import ORGANIZATION_NAME, APPLICATION_NAME +from CherryTomato.main_window import CherryTomatoMainWindow + +QCoreApplication.setApplicationName(ORGANIZATION_NAME) +QCoreApplication.setApplicationName(APPLICATION_NAME) app = Qt.QApplication(sys.argv) -watch = TomatoTimerWindow() +watch = CherryTomatoMainWindow() watch.show() app.exec_() diff --git a/CherryTomato/main_ui.py b/CherryTomato/main_ui.py index 099181a..60a75b3 100644 --- a/CherryTomato/main_ui.py +++ b/CherryTomato/main_ui.py @@ -53,6 +53,9 @@ def setupUi(self, MainWindow): MainWindow.setMenuBar(self.menuBar) self.actionAbout = QtWidgets.QAction(MainWindow) self.actionAbout.setObjectName("actionAbout") + self.actionSettings = QtWidgets.QAction(MainWindow) + self.actionSettings.setObjectName("actionSettings") + self.menuFile.addAction(self.actionSettings) self.menuFile.addAction(self.actionAbout) self.menuBar.addAction(self.menuFile.menuAction()) @@ -64,4 +67,5 @@ def retranslateUi(self, MainWindow): MainWindow.setWindowTitle(_translate("MainWindow", "CherryTomato")) self.menuFile.setTitle(_translate("MainWindow", "File")) self.actionAbout.setText(_translate("MainWindow", "About")) + self.actionSettings.setText(_translate("MainWindow", "Settings")) from CherryTomato.widget import QRoundProgressBar, QRoundPushbutton diff --git a/CherryTomato/main_ui.ui b/CherryTomato/main_ui.ui index f1ca5bb..b677000 100644 --- a/CherryTomato/main_ui.ui +++ b/CherryTomato/main_ui.ui @@ -99,6 +99,7 @@ border-radius: 20px; File + @@ -108,6 +109,11 @@ border-radius: 20px; About + + + Settings + + diff --git a/CherryTomato/main_window.py b/CherryTomato/main_window.py index 78a4667..1134aec 100644 --- a/CherryTomato/main_window.py +++ b/CherryTomato/main_window.py @@ -1,19 +1,16 @@ import os from PyQt5 import Qt, QtCore -from PyQt5.QtCore import QSettings, QSize, QPoint from PyQt5.QtGui import QBrush, QColor, QPalette, QIcon from PyQt5.QtMultimedia import QSound -from CherryTomato import about, APP_ICON, MEDIA_DIR +from CherryTomato import about_window, APP_ICON, MEDIA_DIR, settings_window from CherryTomato.main_ui import Ui_MainWindow +from CherryTomato.settings import CherryTomatoSettings from CherryTomato.tomato_timer import TomatoTimer -class TomatoTimerWindow(Qt.QMainWindow, Ui_MainWindow): - APP_TITLE = 'CherryTomato' - NOTIFICATION_ON = True - +class CherryTomatoMainWindow(Qt.QMainWindow, Ui_MainWindow): def __init__(self, parent=None): super().__init__(parent) @@ -21,10 +18,8 @@ def __init__(self, parent=None): self.setWindowIcon(QIcon(APP_ICON)) - self.settings = QSettings('yakimka', self.APP_TITLE) - # Initial window size/pos last saved. Use default values for first time - self.resize(self.settings.value('size', QSize(400, 520))) - self.move(self.settings.value('pos', QPoint(50, 50))) + self.settings = CherryTomatoSettings() + self.setWindowSizeAndPosition() self.tomatoTimer = TomatoTimer() @@ -34,8 +29,27 @@ def __init__(self, parent=None): self.display() - self.aboutWindow = about.About() + self.aboutWindow = about_window.About() self.actionAbout.triggered.connect(self.showAboutWindow) + self.settingsWindow = settings_window.Settings() + self.actionSettings.triggered.connect(self.showSettingsWindow) + self.settingsWindow.closing.connect(self.update) + + def update(self): + self.tomatoTimer.updateState() + + def setWindowSizeAndPosition(self): + # Initial window size/pos last saved. Use default values for first time + while True: + try: + self.resize(self.settings.size) + self.move(self.settings.position) + except TypeError: + del self.settings.size + del self.settings.position + continue + else: + break def showAboutWindow(self): centerX, centerY = self.getCenterPoint() @@ -44,6 +58,13 @@ def showAboutWindow(self): self.aboutWindow.move(x, y) self.aboutWindow.show() + def showSettingsWindow(self): + centerX, centerY = self.getCenterPoint() + x = int(centerX - self.settingsWindow.width() / 2) + y = int(centerY - self.settingsWindow.height() / 2) + self.settingsWindow.move(x, y) + self.settingsWindow.show() + def getCenterPoint(self): x = int(self.x() + self.width() / 2) y = int(self.y() + self.height() / 2) @@ -52,8 +73,8 @@ def getCenterPoint(self): def closeEvent(self, e): # Write window size and position to config file - self.settings.setValue('size', self.size()) - self.settings.setValue('pos', self.pos()) + self.settings.size = self.size() + self.settings.position = self.pos() e.accept() @@ -74,22 +95,16 @@ def display(self): @Qt.pyqtSlot() def do_start(self): # trigger through proxy - self.tomatoTimer.start() - - @Qt.pyqtSlot() - def do_stop(self): - # trigger through proxy - self.tomatoTimer.abort() + if not self.tomatoTimer.running: + self.tomatoTimer.start() + else: + self.tomatoTimer.abort() def changeButtonState(self): if not self.tomatoTimer.running: self.button.setImage('play.png') - self.button.clicked.disconnect() - self.button.clicked.connect(self.do_start) else: self.button.setImage('stop.png') - self.button.clicked.disconnect() - self.button.clicked.connect(self.do_stop) def setRed(self): # https://coolors.co/eff0f1-d11f2a-8da1b9-95adb6-fad0cf @@ -130,12 +145,13 @@ def keyPressEvent(self, event): @Qt.pyqtSlot() def setFocusOnWindowAndPlayNotification(self): - self.raise_() - self.show() - self.activateWindow() - if self.windowState() == QtCore.Qt.WindowMinimized: - # Window is minimised. Restore it. - self.setWindowState(QtCore.Qt.WindowNoState) - - if self.NOTIFICATION_ON: + if self.settings.interrupt: + self.raise_() + self.show() + self.activateWindow() + if self.windowState() == QtCore.Qt.WindowMinimized: + # Window is minimised. Restore it. + self.setWindowState(QtCore.Qt.WindowNoState) + + if self.settings.notification: QSound.play(os.path.join(MEDIA_DIR, 'sound.wav')) diff --git a/CherryTomato/settings.py b/CherryTomato/settings.py new file mode 100644 index 0000000..d05ea44 --- /dev/null +++ b/CherryTomato/settings.py @@ -0,0 +1,142 @@ +from collections import UserString + +from PyQt5.QtCore import QSettings, QSize, QPoint + + +class State(UserString): + def __init__(self, seq: object, time: int): + self.time = time + super().__init__(seq) + + +class CherryTomatoSettings: + SIZE = 'size' + SIZE_DEFAULT = QSize(400, 520) + POSITION = 'position' + POSITION_DEFAULT = QPoint(50, 50) + + STATE_TOMATO = 'state_tomato' + TOMATO_DEFAULT = 25 * 60 + STATE_BREAK = 'state_break' + BREAK_DEFAULT = 5 * 60 + STATE_LONG_BREAK = 'state_long_break' + LONG_BREAK_DEFAULT = 15 * 60 + + NOTIFICATION = 'notification' + NOTIFICATION_DEFAULT = True + INTERRUPT = 'interrupt' + INTERRUPT_DEFAULT = True + + REPEAT = 'repeat' + REPEAT_DEFAULT = 4 + AUTO_STOP_TOMATO = 'auto_stop_tomato' + AUTO_STOP_TOMATO_DEFAULT = True + AUTO_STOP_BREAK = 'auto_stop_break' + AUTO_STOP_BREAK_DEFAULT = False + SWITCH_TO_TOMATO_ON_ABORT = 'switch_to_tomato_on_abort' + SWITCH_TO_TOMATO_ON_ABORT_DEFAULT = True + + def __init__(self): + self.settings = QSettings() + + @property + def size(self): + return self.settings.value(self.SIZE, self.SIZE_DEFAULT) + + @size.setter + def size(self, val): + self.settings.setValue(self.SIZE, val) + + @size.deleter + def size(self): + self.settings.remove(self.SIZE) + + @property + def position(self): + return self.settings.value(self.POSITION, self.POSITION_DEFAULT) + + @position.setter + def position(self, val): + self.settings.setValue(self.POSITION, val) + + @position.deleter + def position(self): + self.settings.remove(self.POSITION) + + @property + def notification(self): + return self.settings.value(self.NOTIFICATION, self.NOTIFICATION_DEFAULT, type=bool) + + @notification.setter + def notification(self, val): + self.settings.setValue(self.NOTIFICATION, val) + + @property + def interrupt(self): + return self.settings.value(self.INTERRUPT, self.INTERRUPT_DEFAULT, type=bool) + + @interrupt.setter + def interrupt(self, val): + self.settings.setValue(self.INTERRUPT, val) + + @property + def repeat(self): + return self.settings.value(self.REPEAT, self.REPEAT_DEFAULT, type=int) + + @repeat.setter + def repeat(self, val): + self.settings.setValue(self.REPEAT, val) + + @property + def autoStopTomato(self): + return self.settings.value(self.AUTO_STOP_TOMATO, self.AUTO_STOP_TOMATO_DEFAULT, type=bool) + + @autoStopTomato.setter + def autoStopTomato(self, val): + self.settings.setValue(self.AUTO_STOP_TOMATO, val) + + @property + def autoStopBreak(self): + return self.settings.value(self.AUTO_STOP_BREAK, self.AUTO_STOP_BREAK_DEFAULT, type=bool) + + @autoStopBreak.setter + def autoStopBreak(self, val): + self.settings.setValue(self.AUTO_STOP_BREAK, val) + + @property + def switchToTomatoOnAbort(self): + return self.settings.value(self.SWITCH_TO_TOMATO_ON_ABORT, self.SWITCH_TO_TOMATO_ON_ABORT_DEFAULT, type=bool) + + @switchToTomatoOnAbort.setter + def switchToTomatoOnAbort(self, val): + self.settings.setValue(self.SWITCH_TO_TOMATO_ON_ABORT, val) + + @property + def stateTomato(self): + return self.settings.value(self.STATE_TOMATO, State('tomato', self.TOMATO_DEFAULT)) + + @stateTomato.setter + def stateTomato(self, min): + state = self.stateTomato + state.time = min + self.settings.setValue(self.STATE_TOMATO, state) + + @property + def stateBreak(self): + return self.settings.value(self.STATE_BREAK, State('break', self.BREAK_DEFAULT)) + + @stateBreak.setter + def stateBreak(self, min): + state = self.stateBreak + state.time = min + self.settings.setValue(self.STATE_BREAK, state) + + @property + def stateLongBreak(self): + return self.settings.value(self.STATE_LONG_BREAK, State('long_break', self.LONG_BREAK_DEFAULT)) + + @stateLongBreak.setter + def stateLongBreak(self, min): + state = self.stateLongBreak + state.time = min + self.settings.setValue(self.STATE_LONG_BREAK, state) diff --git a/CherryTomato/settings_ui.py b/CherryTomato/settings_ui.py new file mode 100644 index 0000000..5c45155 --- /dev/null +++ b/CherryTomato/settings_ui.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'settings_ui.ui' +# +# Created by: PyQt5 UI code generator 5.13.1 +# +# WARNING! All changes made in this file will be lost! + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_Settings(object): + def setupUi(self, Settings): + Settings.setObjectName("Settings") + Settings.resize(419, 575) + Settings.setLayoutDirection(QtCore.Qt.LeftToRight) + self.verticalLayout = QtWidgets.QVBoxLayout(Settings) + self.verticalLayout.setObjectName("verticalLayout") + self.minutes = QtWidgets.QGroupBox(Settings) + self.minutes.setTitle("") + self.minutes.setObjectName("minutes") + self.formLayout = QtWidgets.QFormLayout(self.minutes) + self.formLayout.setObjectName("formLayout") + self.stateTomatoLabel = QtWidgets.QLabel(self.minutes) + self.stateTomatoLabel.setObjectName("stateTomatoLabel") + self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.stateTomatoLabel) + self.stateTomato = QtWidgets.QSpinBox(self.minutes) + self.stateTomato.setMinimum(1) + self.stateTomato.setObjectName("stateTomato") + self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.stateTomato) + self.stateBreakLabel = QtWidgets.QLabel(self.minutes) + self.stateBreakLabel.setObjectName("stateBreakLabel") + self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.stateBreakLabel) + self.stateBreak = QtWidgets.QSpinBox(self.minutes) + self.stateBreak.setMinimum(1) + self.stateBreak.setObjectName("stateBreak") + self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.stateBreak) + self.stateLongBreakLabel = QtWidgets.QLabel(self.minutes) + self.stateLongBreakLabel.setObjectName("stateLongBreakLabel") + self.formLayout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.stateLongBreakLabel) + self.stateLongBreak = QtWidgets.QSpinBox(self.minutes) + self.stateLongBreak.setMinimum(1) + self.stateLongBreak.setObjectName("stateLongBreak") + self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.stateLongBreak) + self.repeatLabel = QtWidgets.QLabel(self.minutes) + self.repeatLabel.setObjectName("repeatLabel") + self.formLayout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.repeatLabel) + self.repeat = QtWidgets.QSpinBox(self.minutes) + self.repeat.setMinimum(1) + self.repeat.setObjectName("repeat") + self.formLayout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.repeat) + self.verticalLayout.addWidget(self.minutes) + self.checkboxes = QtWidgets.QGroupBox(Settings) + self.checkboxes.setTitle("") + self.checkboxes.setObjectName("checkboxes") + self.formLayout_2 = QtWidgets.QFormLayout(self.checkboxes) + self.formLayout_2.setObjectName("formLayout_2") + self.notification = QtWidgets.QCheckBox(self.checkboxes) + self.notification.setLayoutDirection(QtCore.Qt.LeftToRight) + self.notification.setAutoRepeat(False) + self.notification.setAutoExclusive(False) + self.notification.setTristate(False) + self.notification.setObjectName("notification") + self.formLayout_2.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.notification) + self.interrupt = QtWidgets.QCheckBox(self.checkboxes) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.interrupt.sizePolicy().hasHeightForWidth()) + self.interrupt.setSizePolicy(sizePolicy) + self.interrupt.setLayoutDirection(QtCore.Qt.LeftToRight) + self.interrupt.setAutoFillBackground(False) + self.interrupt.setObjectName("interrupt") + self.formLayout_2.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.interrupt) + self.autoStopTomato = QtWidgets.QCheckBox(self.checkboxes) + self.autoStopTomato.setLayoutDirection(QtCore.Qt.LeftToRight) + self.autoStopTomato.setObjectName("autoStopTomato") + self.formLayout_2.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.autoStopTomato) + self.autoStopBreak = QtWidgets.QCheckBox(self.checkboxes) + self.autoStopBreak.setLayoutDirection(QtCore.Qt.LeftToRight) + self.autoStopBreak.setObjectName("autoStopBreak") + self.formLayout_2.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.autoStopBreak) + self.switchToTomatoOnAbort = QtWidgets.QCheckBox(self.checkboxes) + self.switchToTomatoOnAbort.setMinimumSize(QtCore.QSize(0, 0)) + self.switchToTomatoOnAbort.setLayoutDirection(QtCore.Qt.LeftToRight) + self.switchToTomatoOnAbort.setObjectName("switchToTomatoOnAbort") + self.formLayout_2.setWidget(4, QtWidgets.QFormLayout.FieldRole, self.switchToTomatoOnAbort) + self.verticalLayout.addWidget(self.checkboxes) + + self.retranslateUi(Settings) + QtCore.QMetaObject.connectSlotsByName(Settings) + + def retranslateUi(self, Settings): + _translate = QtCore.QCoreApplication.translate + Settings.setWindowTitle(_translate("Settings", "Settings")) + self.stateTomatoLabel.setText(_translate("Settings", "Tomato time (min)")) + self.stateBreakLabel.setText(_translate("Settings", "Break time (min)")) + self.stateLongBreakLabel.setText(_translate("Settings", "Long break time (min)")) + self.repeatLabel.setText(_translate("Settings", "Long break after (tomatoes)")) + self.notification.setText(_translate("Settings", "Sound notification")) + self.interrupt.setText(_translate("Settings", "Interrupt me")) + self.autoStopTomato.setText(_translate("Settings", "Auto stop tomato after break")) + self.autoStopBreak.setText(_translate("Settings", "Auto stop break after tomato")) + self.switchToTomatoOnAbort.setText(_translate("Settings", "Skip break if you press stop button")) diff --git a/CherryTomato/settings_ui.ui b/CherryTomato/settings_ui.ui new file mode 100644 index 0000000..8bdba5f --- /dev/null +++ b/CherryTomato/settings_ui.ui @@ -0,0 +1,172 @@ + + + Settings + + + + 0 + 0 + 419 + 575 + + + + Settings + + + Qt::LeftToRight + + + + + + + + + + + + Tomato time (min) + + + + + + + 1 + + + + + + + Break time (min) + + + + + + + 1 + + + + + + + Long break time (min) + + + + + + + 1 + + + + + + + Long break after (tomatoes) + + + + + + + 1 + + + + + + + + + + + + + + + + Qt::LeftToRight + + + Sound notification + + + false + + + false + + + false + + + + + + + + 0 + 0 + + + + Qt::LeftToRight + + + false + + + Interrupt me + + + + + + + Qt::LeftToRight + + + Auto stop tomato after break + + + + + + + Qt::LeftToRight + + + Auto stop break after tomato + + + + + + + + 0 + 0 + + + + Qt::LeftToRight + + + Skip break if you press stop button + + + + + + + + + + + diff --git a/CherryTomato/settings_window.py b/CherryTomato/settings_window.py new file mode 100644 index 0000000..d98859f --- /dev/null +++ b/CherryTomato/settings_window.py @@ -0,0 +1,50 @@ +from PyQt5 import QtWidgets +from PyQt5.QtCore import pyqtSignal +from PyQt5.QtGui import QIcon + +from CherryTomato import APP_ICON +from CherryTomato.settings import CherryTomatoSettings +from CherryTomato.settings_ui import Ui_Settings + + +class Settings(QtWidgets.QWidget, Ui_Settings): + closing = pyqtSignal() + + def __init__(self): + super().__init__() + + self.setupUi(self) + + self.setWindowIcon(QIcon(APP_ICON)) + self.settings = CherryTomatoSettings() + + state_tomato = self.settings.stateTomato + self.stateTomato.setValue(int(state_tomato.time / 60)) + state_break = self.settings.stateBreak + self.stateBreak.setValue(int(state_break.time / 60)) + state_long_break = self.settings.stateLongBreak + self.stateLongBreak.setValue(int(state_long_break.time / 60)) + self.repeat.setValue(self.settings.repeat) + + self.notification.setChecked(self.settings.notification) + self.interrupt.setChecked(self.settings.interrupt) + self.autoStopTomato.setChecked(self.settings.autoStopTomato) + self.autoStopBreak.setChecked(self.settings.autoStopBreak) + self.switchToTomatoOnAbort.setChecked(self.settings.switchToTomatoOnAbort) + + def closeEvent(self, e): + e.accept() + self.updateSettings() + self.closing.emit() + + def updateSettings(self): + self.settings.stateTomato = self.stateTomato.value() * 60 + self.settings.stateBreak = self.stateBreak.value() * 60 + self.settings.stateLongBreak = self.stateLongBreak.value() * 60 + self.settings.repeat = self.repeat.value() + + self.settings.notification = self.notification.isChecked() + self.settings.interrupt = self.interrupt.isChecked() + self.settings.autoStopTomato = self.autoStopTomato.isChecked() + self.settings.autoStopBreak = self.autoStopBreak.isChecked() + self.settings.switchToTomatoOnAbort = self.switchToTomatoOnAbort.isChecked() diff --git a/CherryTomato/tomato_timer.py b/CherryTomato/tomato_timer.py index 5f55bf1..f4f168b 100644 --- a/CherryTomato/tomato_timer.py +++ b/CherryTomato/tomato_timer.py @@ -1,25 +1,13 @@ -from collections import UserString - from PyQt5 import Qt from PyQt5.QtCore import QObject, pyqtSignal +from CherryTomato.settings import CherryTomatoSettings -class State(UserString): - def __init__(self, seq: object, time: int): - self.time = time - super().__init__(seq) +settings = CherryTomatoSettings() class TomatoTimer(QObject): - TOMATOS_BEFORE_LONG_BREAK = 4 - AUTO_STOP_TOMATO = True - AUTO_STOP_BREAK = False - SWITCH_TO_TOMATO_ON_ABORT = True - TICK_TIME = 64 # ms - STATE_TOMATO = State('tomato', 25 * 60) - STATE_BREAK = State('break', 5 * 60) - STATE_LONG_BREAK = State('long_break', 20 * 60) stateChanged = pyqtSignal() finished = pyqtSignal() @@ -27,6 +15,8 @@ class TomatoTimer(QObject): def __init__(self): super().__init__() + self.settings = settings + self.tomatoes = 0 self.state = None @@ -54,7 +44,7 @@ def start(self): def abort(self): self.stop() - if not self.isTomato() and self.SWITCH_TO_TOMATO_ON_ABORT: + if not self.isTomato() and self.settings.switchToTomatoOnAbort: self.changeState() else: self.reset() @@ -76,16 +66,15 @@ def tick(self): if self._isNeedAutoStop(): self.stop() self.notifyTimerIsOver() - self.notifyAboutAnyChange() def isTomato(self): - return self.state == self.STATE_TOMATO + return self.state == self.settings.stateTomato def _isNeedAutoStop(self): - if self.isTomato() and self.AUTO_STOP_TOMATO: + if self.isTomato() and self.settings.autoStopTomato: return True - elif not self.isTomato() and self.AUTO_STOP_BREAK: + elif not self.isTomato() and self.settings.autoStopBreak: return True return False @@ -101,13 +90,18 @@ def changeState(self): def _statesGen(self): while True: - yield self.STATE_TOMATO - yield self.STATE_LONG_BREAK if self._isTimeForLongBreak() else self.STATE_BREAK + yield self.settings.stateTomato + yield self.settings.stateLongBreak if self._isTimeForLongBreak() else self.settings.stateBreak def _isTimeForLongBreak(self): if self.tomatoes == 0: return False - return self.isTomato() and self.tomatoes % self.TOMATOS_BEFORE_LONG_BREAK == 0 + return self.isTomato() and self.tomatoes % self.settings.repeat == 0 + + def updateState(self): + if not self.running: + self.reset() + self.notifyAboutAnyChange() def reset(self): self.seconds = self.state.time diff --git a/tests/conftest.py b/tests/conftest.py index b515a66..87fc634 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,20 +1,44 @@ +from unittest.mock import MagicMock + import pytest +from CherryTomato.settings import CherryTomatoSettings from CherryTomato.tomato_timer import TomatoTimer +class MockQSettings: + def value(self, key, default, type=None): + return default + + setValue = MagicMock() + remove = MagicMock() + + @pytest.fixture -def tomato(monkeypatch): - monkeypatch.setattr(TomatoTimer, 'TICK_TIME', 64) - monkeypatch.setattr(TomatoTimer.STATE_TOMATO, 'time', 100) - monkeypatch.setattr(TomatoTimer.STATE_BREAK, 'time', 25) - monkeypatch.setattr(TomatoTimer.STATE_LONG_BREAK, 'time', 50) +def mock_qsettings(monkeypatch, mocker): + qsettings = mocker.patch('CherryTomato.settings.QSettings', MockQSettings) + + monkeypatch.setattr(CherryTomatoSettings, 'TOMATO_DEFAULT', 100) + monkeypatch.setattr(CherryTomatoSettings, 'BREAK_DEFAULT', 25) + monkeypatch.setattr(CherryTomatoSettings, 'LONG_BREAK_DEFAULT', 50) + monkeypatch.setattr(CherryTomatoSettings, 'REPEAT_DEFAULT', 4) + monkeypatch.setattr(CherryTomatoSettings, 'AUTO_STOP_TOMATO_DEFAULT', False) + monkeypatch.setattr(CherryTomatoSettings, 'AUTO_STOP_BREAK_DEFAULT', False) + monkeypatch.setattr(CherryTomatoSettings, 'SWITCH_TO_TOMATO_ON_ABORT_DEFAULT', True) - monkeypatch.setattr(TomatoTimer, 'TOMATOS_BEFORE_LONG_BREAK', 4) - monkeypatch.setattr(TomatoTimer, 'AUTO_STOP_TOMATO', False) - monkeypatch.setattr(TomatoTimer, 'AUTO_STOP_BREAK', False) - monkeypatch.setattr(TomatoTimer, 'SWITCH_TO_TOMATO_ON_ABORT', True) + return qsettings +@pytest.fixture +def settings(mock_qsettings): + settings = CherryTomatoSettings() + + return settings + + +@pytest.fixture +def tomato(settings, monkeypatch, mocker): + monkeypatch.setattr(TomatoTimer, 'TICK_TIME', 64) + mocker.patch('CherryTomato.tomato_timer.settings', settings) tomato = TomatoTimer() return tomato diff --git a/tests/test_tomato_timer.py b/tests/test_tomato_timer.py index 94a0de0..04f94de 100644 --- a/tests/test_tomato_timer.py +++ b/tests/test_tomato_timer.py @@ -2,7 +2,11 @@ import pytest -from CherryTomato.tomato_timer import TomatoTimer +from CherryTomato.settings import State + +STATE_TOMATO = State('tomato', 100) +STATE_BREAK = State('break', 25) +STATE_LONG_BREAK = State('long_break', 50) def test_instance(tomato): @@ -81,11 +85,11 @@ def test_abort(mock_for_abort, tomato): (False, False) ]) def test_abort_with_switch_to_tomato_flag(mock_for_abort, tomato_on_break, flag_value, changed): - tomato_on_break.SWITCH_TO_TOMATO_ON_ABORT = flag_value + tomato_on_break.settings.SWITCH_TO_TOMATO_ON_ABORT_DEFAULT = flag_value tomato_on_break.abort() - assert (tomato_on_break.STATE_BREAK != tomato_on_break.state) is changed + assert ('break' != tomato_on_break.state) is changed def test_create_timer(mock_qt_timer, tomato): @@ -93,17 +97,17 @@ def test_create_timer(mock_qt_timer, tomato): @pytest.mark.parametrize('state,expected', [ - (TomatoTimer.STATE_TOMATO, True), - (TomatoTimer.STATE_BREAK, False), - (TomatoTimer.STATE_LONG_BREAK, False), + (STATE_TOMATO, True), + (STATE_BREAK, False), + (STATE_LONG_BREAK, False), ]) def test_is_tomato(tomato, state, expected): tomato.state = state assert tomato.isTomato() is expected -def test_change_state(tomato): - for i, state in enumerate(cycle([tomato.STATE_TOMATO, tomato.STATE_BREAK])): +def test_change_state(tomato, settings): + for i, state in enumerate(cycle([settings.stateTomato, settings.stateBreak])): assert tomato.state == state assert tomato.seconds == state.time tomato.changeState() @@ -112,18 +116,18 @@ def test_change_state(tomato): @pytest.mark.parametrize('tomatoes,const_value,expected', [ - (0, 4, TomatoTimer.STATE_BREAK), - (3, 4, TomatoTimer.STATE_BREAK), - (4, 4, TomatoTimer.STATE_LONG_BREAK), - (5, 4, TomatoTimer.STATE_BREAK), - (12, 1, TomatoTimer.STATE_LONG_BREAK), - (1, 10, TomatoTimer.STATE_BREAK), - (20, 10, TomatoTimer.STATE_LONG_BREAK), - (100, 10, TomatoTimer.STATE_LONG_BREAK), + (0, 4, STATE_BREAK), + (3, 4, STATE_BREAK), + (4, 4, STATE_LONG_BREAK), + (5, 4, STATE_BREAK), + (12, 1, STATE_LONG_BREAK), + (1, 10, STATE_BREAK), + (20, 10, STATE_LONG_BREAK), + (100, 10, STATE_LONG_BREAK), ]) def test_change_state_to_long_break(tomato, tomatoes, const_value, expected): - tomato.TOMATOS_BEFORE_LONG_BREAK = const_value + tomato.settings.REPEAT_DEFAULT = const_value tomato.tomatoes = tomatoes tomato.changeState() @@ -137,4 +141,4 @@ def test_reset(tomato): tomato.reset() - assert tomato.seconds == tomato.STATE_TOMATO.time + assert tomato.seconds == 100 diff --git a/tests/test_tomato_timer_slots.py b/tests/test_tomato_timer_slots.py index da5f617..98c5400 100644 --- a/tests/test_tomato_timer_slots.py +++ b/tests/test_tomato_timer_slots.py @@ -39,8 +39,8 @@ def test_tick_tomatos_count_from_break(tomato_on_break, qtbot): (False, False, False), ]) def test_tick_auto_stop(tomato, qtbot, mock_stop, is_tomato, flag, auto_stop): - tomato.AUTO_STOP_TOMATO = flag - tomato.AUTO_STOP_BREAK = flag + tomato.settings.AUTO_STOP_TOMATO_DEFAULT = flag + tomato.settings.AUTO_STOP_BREAK_DEFAULT = flag if not is_tomato: tomato.changeState() diff --git a/tests/ui/test_main_window.py b/tests/ui/test_main_window.py new file mode 100644 index 0000000..f9c7a69 --- /dev/null +++ b/tests/ui/test_main_window.py @@ -0,0 +1,30 @@ +import pytest +from PyQt5 import QtCore + +from CherryTomato.main_window import CherryTomatoMainWindow + + +@pytest.fixture(params=['tomato', 'break']) +def mock_main_window(request, mock_qsettings, qtbot, tomato, monkeypatch): + window = CherryTomatoMainWindow() + if request.param == 'break': + tomato.changeState() + monkeypatch.setattr(window, 'tomatoTimer', tomato) + window.show() + qtbot.addWidget(window) + return window + + +def test_start_button_with_tomato(mock_main_window, qtbot): + qtbot.mouseClick(mock_main_window.button, QtCore.Qt.LeftButton) + + assert mock_main_window.tomatoTimer.running + + +def test_stop_button_with_tomato(mock_main_window, qtbot): + qtbot.mouseClick(mock_main_window.button, QtCore.Qt.LeftButton) + + qtbot.mouseClick(mock_main_window.button, QtCore.Qt.LeftButton) + + assert mock_main_window.tomatoTimer.running is False +