def setCurrentValues(self): self.settings = Settings() self.settings.beginGroup("plot") plotStyle = self.settings.value("style", "dark") self.plotStyleList.setCurrentText(plotStyle.capitalize()) # does setCurrentText not emit currentTextChanged signal? self._enableDisableDeleteButton(plotStyle) self.customStyle.setName(plotStyle) self.customStyle.setStyle(self.mainWindow.plot.getStyle(plotStyle)) customRange = self.settings.value("customRange", False) rng = self.settings.value("range", "All") self.setCustomRange(customRange) if customRange: rng = int(rng) self.customRangeSpinBox.setValue(rng) else: items = [ self.plotRangeCombo.itemText(idx) for idx in range(self.plotRangeCombo.count()) ] idx = items.index(rng) self.plotRangeCombo.setCurrentIndex(idx) self.settings.endGroup()
def setCurrentValues(self): self.settings = Settings() self.settings.beginGroup("pb") bestMonthCriterion = self.settings.value("bestMonthCriterion", "distance").capitalize() self.bestMonthCriteria.setCurrentText(bestMonthCriterion) numSessions = self.settings.value("numSessions", 5, int) self.numSessionsBox.setValue(numSessions) for name, widget in self.summaryComboBoxes.items(): funcName = self.settings.value(f"summary/{name}", None) if funcName is None: funcName = self.mainWindow.summary.getFunc(name) widget.setCurrentText(funcName) self.settings.endGroup()
def __init__(self, style="dark"): plotStyleFile = os.path.dirname(Settings().fileName()) plotStyleFile = os.path.join(plotStyleFile, 'plotStyles.ini') self.settings = Settings(plotStyleFile, Settings.NativeFormat) self.keys = [ 'speed', 'distance', 'time', 'calories', 'odometer', 'highlightPoint', 'foreground', 'background' ] self.symbolKeys = ['speed', 'distance', 'time', 'calories'] # make defaults darkDefault = [ "#024aeb", "#cf0202", "#19b536", "#ff9100", "#4d4d4d", "#faed00", "#969696", "#000000" ] lightDefault = [ "#0981cb", "#d80d0d", "#2bb512", "#ff9100", "#9f9f9f", "#deb009", "#4d4d4d", "#ffffff" ] defaults = { 'dark': dict(zip(self.keys, darkDefault)), 'light': dict(zip(self.keys, lightDefault)) } defaultSymbols = {key: 'x' for key in self.symbolKeys} for styleName, styleDct in defaults.items(): if styleName not in self.settings.childGroups(): self.settings.beginGroup(styleName) for key, colour in styleDct.items(): self.settings.setValue(key, colour) for key, symbol in defaultSymbols.items(): self.settings.setValue(f"{key}Symbol", symbol) self.settings.endGroup() self.name = style
class PlotPreferences(QWidget): def __init__(self, mainWindow): super().__init__() self.mainWindow = mainWindow plotStyleGroup = GroupWidget("Plot style") styles = self.mainWindow.plot.getValidStyles() self.customStyle = StyleDesigner( styleKeys=self.mainWindow.plot.getStyleKeys(), symbolKeys=self.mainWindow.plot.getStyleSymbolKeys(), invalidNames=styles) self.customStyle.setEnabled(False) self.customStyle.saveStyle.connect(self._saveStyle) self.plotStyleList = QComboBox() styles = [s.capitalize() for s in styles] styles += ["Add custom theme..."] self.plotStyleList.addItems(styles) self.plotStyleList.currentTextChanged.connect( self._updateCustomStyleWidget) foregroundColour = self.palette().windowText().color() icon = makeForegroundIcon("edit", foregroundColour) self.editPlotStyleButton = QPushButton(icon, "") self.editPlotStyleButton.setCheckable(True) self.editPlotStyleButton.setToolTip("Edit theme") self.editPlotStyleButton.toggled.connect(self._editStyle) icon = makeForegroundIcon("trash", foregroundColour) self.deletePlotStyleButton = QPushButton(icon, "") self.deletePlotStyleButton.setToolTip("Delete theme") self.deletePlotStyleButton.clicked.connect(self._deleteTheme) self.plotStyleList.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) self.editPlotStyleButton.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) self.deletePlotStyleButton.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) plotStyleBox = QHBoxLayout() plotStyleBox.addWidget(self.plotStyleList) plotStyleBox.addWidget(self.editPlotStyleButton) plotStyleBox.addWidget(self.deletePlotStyleButton) plotStyleGroup.addLayout(plotStyleBox) plotStyleGroup.addWidget(self.customStyle) plotConfigGroup = GroupWidget("Default plot range", layout="vbox") self.plotRangeCombo = QComboBox() ranges = [ "1 month", "3 months", "6 months", "1 year", "Current year", "All" ] self.plotRangeCombo.addItems(ranges) self.customRangeCheckBox = QCheckBox("Custom range") self.customRangeSpinBox = QSpinBox() self.customRangeSpinBox.setSuffix(" months") maxMonths = len(mainWindow.data.splitMonths()) self.customRangeSpinBox.setRange(1, maxMonths) self.customRangeCheckBox.clicked.connect(self.setCustomRange) plotRangeLayout = QHBoxLayout() plotRangeLayout.addWidget(self.plotRangeCombo) customRangeLayout = QHBoxLayout() customRangeLayout.addWidget(self.customRangeCheckBox) customRangeLayout.addWidget(self.customRangeSpinBox) plotConfigGroup.addLayout(plotRangeLayout) plotConfigGroup.addLayout(customRangeLayout) mainLayout = QVBoxLayout() mainLayout.addWidget(plotStyleGroup) mainLayout.addWidget(plotConfigGroup) mainLayout.addStretch(1) self.setLayout(mainLayout) self.setCurrentValues() # apply initial state self.apply() def setCurrentValues(self): self.settings = Settings() self.settings.beginGroup("plot") plotStyle = self.settings.value("style", "dark") self.plotStyleList.setCurrentText(plotStyle.capitalize()) # does setCurrentText not emit currentTextChanged signal? self._enableDisableDeleteButton(plotStyle) self.customStyle.setName(plotStyle) self.customStyle.setStyle(self.mainWindow.plot.getStyle(plotStyle)) customRange = self.settings.value("customRange", False) rng = self.settings.value("range", "All") self.setCustomRange(customRange) if customRange: rng = int(rng) self.customRangeSpinBox.setValue(rng) else: items = [ self.plotRangeCombo.itemText(idx) for idx in range(self.plotRangeCombo.count()) ] idx = items.index(rng) self.plotRangeCombo.setCurrentIndex(idx) self.settings.endGroup() def _saveStyle(self, name, style, setStyle=False): self.mainWindow.plot.addCustomStyle(name, style, setStyle=setStyle) idx = self.plotStyleList.count() - 1 self.plotStyleList.insertItem(idx, name.capitalize()) self.plotStyleList.setCurrentIndex(idx) def _editStyle(self, edit): self.customStyle.setEditMode(edit) self._updateCustomStyleWidget() def apply(self): styleName = self.plotStyleList.currentText().lower() if styleName == "add custom theme...": styleName, styleDct = self.customStyle.getStyle() self._saveStyle(styleName, styleDct, setStyle=True) else: self.mainWindow.plot.setStyle(styleName) customRange = self.customRangeCheckBox.isChecked() if customRange: months = self.customRangeSpinBox.value() else: text = self.plotRangeCombo.currentText() if text == "1 year": text = "12 months" elif text == "Current year": text = f"{date.today().month} months" months = None if text == 'All' else int(text.strip(' months')) self.mainWindow.plot.setXAxisRange(months) self.settings.beginGroup("plot") self.settings.setValue("style", styleName) self.settings.setValue("customRange", customRange) if customRange: self.settings.setValue("range", self.customRangeSpinBox.value()) else: self.settings.setValue("range", self.plotRangeCombo.currentText()) self.settings.endGroup() @Slot(bool) def setCustomRange(self, custom): self.customRangeCheckBox.setChecked(custom) if custom: self.customRangeSpinBox.setEnabled(True) self.plotRangeCombo.setEnabled(False) else: self.customRangeSpinBox.setEnabled(False) self.plotRangeCombo.setEnabled(True) def _updateCustomStyleWidget(self, name=None): if name is None or name == "Add custom theme...": self.customStyle.setEnabled(True) if name is None: name = self.customStyle.name else: name = f"custom-{self.customStyle.name}" self.customStyle.setName(name) else: name = name.lower() style = self.mainWindow.plot.getStyle(name) self.customStyle.setStyle(style, name=name) self.customStyle.setEnabled(False) self._enableDisableDeleteButton(name) def _enableDisableDeleteButton(self, plotStyle): if plotStyle in self.mainWindow.plot.getDefaultStyles(): self.deletePlotStyleButton.setEnabled(False) self.deletePlotStyleButton.setToolTip( "Cannot delete default theme") else: self.deletePlotStyleButton.setEnabled(True) self.deletePlotStyleButton.setToolTip("Delete theme") def _deleteTheme(self): styleName = self.plotStyleList.currentText() items = [ self.plotStyleList.itemText(idx) for idx in range(self.plotStyleList.count()) ] idx = items.index(styleName) self.plotStyleList.removeItem(idx) self.mainWindow.plot.removeCustomStyle(styleName.lower())
class DataPreferences(QWidget): def __init__(self, mainWindow): super().__init__() self.mainWindow = mainWindow bestMonthGroup = GroupWidget("Best month", layout="grid") self.bestMonthCriteria = QComboBox() self.bestMonthCriteria.addItems( ["Time", "Distance", "Avg. speed", "Calories", "Gear"]) bestMonthLabel = QLabel("Criterion:") bestMonthGroup.addWidget(bestMonthLabel, 0, 0) bestMonthGroup.addWidget(self.bestMonthCriteria, 0, 1) topSessionsGroup = GroupWidget("Top sessions", layout="grid") self.numSessionsBox = QSpinBox() self.numSessionsBox.setMinimum(1) numSessionsLabel = QLabel("Number of top sessions:") topSessionsGroup.addWidget(numSessionsLabel, 0, 0) topSessionsGroup.addWidget(self.numSessionsBox, 0, 1) summaryCriteriaGroup = GroupWidget("Summary criteria", layout="grid") names = ["Time", "Distance", "Calories", "Speed", "Gear"] self.summaryComboBoxes = {} for row, name in enumerate(names): summaryCriteriaGroup.addWidget(QLabel(name), row, 0) box = FuncComboBox() self.summaryComboBoxes[name.lower()] = box summaryCriteriaGroup.addWidget(box, row, 1) mainLayout = QVBoxLayout() mainLayout.addWidget(summaryCriteriaGroup) mainLayout.addWidget(bestMonthGroup) mainLayout.addWidget(topSessionsGroup) mainLayout.addStretch(1) self.setLayout(mainLayout) self.setCurrentValues() # apply initial state self.apply() def setCurrentValues(self): self.settings = Settings() self.settings.beginGroup("pb") bestMonthCriterion = self.settings.value("bestMonthCriterion", "distance").capitalize() self.bestMonthCriteria.setCurrentText(bestMonthCriterion) numSessions = self.settings.value("numSessions", 5, int) self.numSessionsBox.setValue(numSessions) for name, widget in self.summaryComboBoxes.items(): funcName = self.settings.value(f"summary/{name}", None) if funcName is None: funcName = self.mainWindow.summary.getFunc(name) widget.setCurrentText(funcName) self.settings.endGroup() def apply(self): bestMonthCriterion = self.bestMonthCriteria.currentText().lower() self.mainWindow.pb.bestMonth.setColumn(bestMonthCriterion) numSessions = self.numSessionsBox.value() self.mainWindow.pb.bestSessions.setNumRows(numSessions) self.settings.beginGroup("pb") self.settings.setValue("bestMonthCriterion", bestMonthCriterion) self.settings.setValue("numSessions", numSessions) # make dict to pass to `setFunc` so it doesn't remake the viewer five times summaryFuncs = {} for name, widget in self.summaryComboBoxes.items(): funcName = widget.currentText() self.settings.setValue(f"summary/{name}", funcName) summaryFuncs[name] = funcName self.mainWindow.summary.setFunc(summaryFuncs) self.settings.endGroup()
def __init__(self): super().__init__() self.settings = Settings() self._saveLabel = QLabel() self._summaryLabel = QLabel() self.statusBar().addWidget(self._saveLabel) self.statusBar().addWidget(self._summaryLabel) self.file = self.getFile() self.sep = ',' if not os.path.exists(self.file): header = ['Date', 'Time', 'Distance (km)', 'Calories', 'Gear'] s = self.sep.join(header) with open(self.file, 'w') as fileobj: fileobj.write(s+'\n') df = pd.read_csv(self.file, sep=self.sep, parse_dates=['Date']) self.data = CycleData(df) self.save() self.dataAnalysis = CycleDataAnalysis(self.data) self.summary = Summary() numTopSessions = self.settings.value("pb/numSessions", 5, int) monthCriterion = self.settings.value("pb/bestMonthCriterion", "distance") self.pb = PersonalBests(self, numSessions=numTopSessions, monthCriterion=monthCriterion) self.viewer = CycleDataViewer(self) self.addData = AddCycleData() plotStyle = self.settings.value("plot/style", "dark") self.plot = CyclePlotWidget(self, style=plotStyle) self.pb.bestMonth.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum) self.pb.bestSessions.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum) self.addData.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum) self.summary.valueChanged.connect(self.viewer.newData) self.summary.valueChanged.connect(self.pb.newData) self.addData.newData.connect(self.data.append) self.data.dataChanged.connect(self.viewer.newData) self.data.dataChanged.connect(self.plot.newData) self.data.dataChanged.connect(self.pb.newData) self.data.dataChanged.connect(self.save) self.plot.pointSelected.connect(self.viewer.highlightItem) self.viewer.itemSelected.connect(self.plot.setCurrentPointFromDate) self.viewer.selectedSummary.connect(self._summaryLabel.setText) self.pb.itemSelected.connect(self.plot.setCurrentPointFromDate) self.pb.numSessionsChanged.connect(self.setPbSessionsDockLabel) self.pb.monthCriterionChanged.connect(self.setPbMonthDockLabel) self.fileChangedTimer = QTimer() self.fileChangedTimer.setInterval(100) self.fileChangedTimer.setSingleShot(True) self.fileChangedTimer.timeout.connect(self.csvFileChanged) self.fileWatcher = QFileSystemWatcher([self.file]) self.fileWatcher.fileChanged.connect(self.startTimer) dockWidgets = [(self.pb.bestMonth, Qt.LeftDockWidgetArea, f"Best month ({monthCriterion})", "PB month"), (self.pb.bestSessions, Qt.LeftDockWidgetArea, f"Top {intToStr(numTopSessions)} sessions", "PB sessions"), (self.viewer, Qt.LeftDockWidgetArea, "Monthly data"), (self.addData, Qt.LeftDockWidgetArea, "Add data")] for args in dockWidgets: self.createDockWidget(*args) self.setCentralWidget(self.plot) state = self.settings.value("window/state", None) if state is not None: self.restoreState(state) geometry = self.settings.value("window/geometry", None) if geometry is not None: self.restoreGeometry(geometry) self.prefDialog = PreferencesDialog(self) self.createActions() self.createMenus() fileDir = os.path.split(__file__)[0] path = os.path.join(fileDir, "..", "images/icon.png") icon = QIcon(path) self.setWindowIcon(icon)
class CycleTracks(QMainWindow): def __init__(self): super().__init__() self.settings = Settings() self._saveLabel = QLabel() self._summaryLabel = QLabel() self.statusBar().addWidget(self._saveLabel) self.statusBar().addWidget(self._summaryLabel) self.file = self.getFile() self.sep = ',' if not os.path.exists(self.file): header = ['Date', 'Time', 'Distance (km)', 'Calories', 'Gear'] s = self.sep.join(header) with open(self.file, 'w') as fileobj: fileobj.write(s+'\n') df = pd.read_csv(self.file, sep=self.sep, parse_dates=['Date']) self.data = CycleData(df) self.save() self.dataAnalysis = CycleDataAnalysis(self.data) self.summary = Summary() numTopSessions = self.settings.value("pb/numSessions", 5, int) monthCriterion = self.settings.value("pb/bestMonthCriterion", "distance") self.pb = PersonalBests(self, numSessions=numTopSessions, monthCriterion=monthCriterion) self.viewer = CycleDataViewer(self) self.addData = AddCycleData() plotStyle = self.settings.value("plot/style", "dark") self.plot = CyclePlotWidget(self, style=plotStyle) self.pb.bestMonth.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum) self.pb.bestSessions.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum) self.addData.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum) self.summary.valueChanged.connect(self.viewer.newData) self.summary.valueChanged.connect(self.pb.newData) self.addData.newData.connect(self.data.append) self.data.dataChanged.connect(self.viewer.newData) self.data.dataChanged.connect(self.plot.newData) self.data.dataChanged.connect(self.pb.newData) self.data.dataChanged.connect(self.save) self.plot.pointSelected.connect(self.viewer.highlightItem) self.viewer.itemSelected.connect(self.plot.setCurrentPointFromDate) self.viewer.selectedSummary.connect(self._summaryLabel.setText) self.pb.itemSelected.connect(self.plot.setCurrentPointFromDate) self.pb.numSessionsChanged.connect(self.setPbSessionsDockLabel) self.pb.monthCriterionChanged.connect(self.setPbMonthDockLabel) self.fileChangedTimer = QTimer() self.fileChangedTimer.setInterval(100) self.fileChangedTimer.setSingleShot(True) self.fileChangedTimer.timeout.connect(self.csvFileChanged) self.fileWatcher = QFileSystemWatcher([self.file]) self.fileWatcher.fileChanged.connect(self.startTimer) dockWidgets = [(self.pb.bestMonth, Qt.LeftDockWidgetArea, f"Best month ({monthCriterion})", "PB month"), (self.pb.bestSessions, Qt.LeftDockWidgetArea, f"Top {intToStr(numTopSessions)} sessions", "PB sessions"), (self.viewer, Qt.LeftDockWidgetArea, "Monthly data"), (self.addData, Qt.LeftDockWidgetArea, "Add data")] for args in dockWidgets: self.createDockWidget(*args) self.setCentralWidget(self.plot) state = self.settings.value("window/state", None) if state is not None: self.restoreState(state) geometry = self.settings.value("window/geometry", None) if geometry is not None: self.restoreGeometry(geometry) self.prefDialog = PreferencesDialog(self) self.createActions() self.createMenus() fileDir = os.path.split(__file__)[0] path = os.path.join(fileDir, "..", "images/icon.png") icon = QIcon(path) self.setWindowIcon(icon) def show(self): super().show() self.prefDialog.ok() @staticmethod def getFile(): home = os.path.expanduser('~') path = os.path.join(home, '.cycletracks') os.makedirs(path, exist_ok=True) file = os.path.join(path, 'cycletracks.csv') return file @Slot() def save(self): self.data.df.to_csv(self.file, sep=self.sep, index=False) self.backup() saveTime = datetime.now().strftime("%H:%M:%S") self._saveLabel.setText(f"Last saved at {saveTime}") @Slot() def backup(self): bak = self.file + '.bak' self.data.df.to_csv(bak, sep=self.sep, index=False) @Slot(str) def startTimer(self, file): self._fileChanged = file self.fileChangedTimer.start() @Slot() def csvFileChanged(self): df = pd.read_csv(self._fileChanged, sep=self.sep, parse_dates=['Date']) try: assert_frame_equal(self.data.df, df, check_exact=False) except AssertionError: msg = "CycleTracks csv file changed on disk. Do you want to reload?" btn = QMessageBox.question(self, "File changed on disk", msg) if btn == QMessageBox.Yes: self.loadCsvFile() def loadCsvFile(self): df = pd.read_csv(self.file, sep=self.sep, parse_dates=['Date']) self.backup() self.data.setDataFrame(df) def createDockWidget(self, widget, area, title, key=None): dock = QDockWidget() dock.setWidget(widget) dock.setWindowTitle(title) dock.setObjectName(title) self.addDockWidget(area, dock) if not hasattr(self, "dockWidgets"): self.dockWidgets = {} if key is None: key = title self.dockWidgets[key] = dock def setPbSessionsDockLabel(self, num): label = f"Top {intToStr(num)} sessions" self.dockWidgets["PB sessions"].setWindowTitle(label) def setPbMonthDockLabel(self, monthCriterion): label = f"Best month ({monthCriterion})" self.dockWidgets["PB month"].setWindowTitle(label) def closeEvent(self, *args, **kwargs): self.backup() state = self.saveState() geometry = self.saveGeometry() self.settings.setValue("window/state", state) self.settings.setValue("window/geometry", geometry) def createActions(self): self.exitAct = QAction("E&xit", self, shortcut="Ctrl+Q", statusTip="Exit the application", triggered=self.close) self.saveAct = QAction("&Save", self, shortcut="Ctrl+S", statusTip="Save data", triggered=self.save) self.preferencesAct = QAction("&Preferences", self, shortcut="F12", statusTip="Edit preferences", triggered=self.prefDialog.show) def createMenus(self): self.fileMenu = self.menuBar().addMenu("&File") self.fileMenu.addAction(self.saveAct) self.fileMenu.addSeparator() self.fileMenu.addAction(self.exitAct) self.editMenu = self.menuBar().addMenu("&Edit") self.editMenu.addAction(self.preferencesAct) self.viewMenu = self.menuBar().addMenu("&View") self.panelMenu = self.viewMenu.addMenu("&Panels") for key in sorted(self.dockWidgets): dock = self.dockWidgets[key] self.panelMenu.addAction(dock.toggleViewAction())
class PlotStyle: defaultStyles = ["dark", "light"] def __init__(self, style="dark"): plotStyleFile = os.path.dirname(Settings().fileName()) plotStyleFile = os.path.join(plotStyleFile, 'plotStyles.ini') self.settings = Settings(plotStyleFile, Settings.NativeFormat) self.keys = [ 'speed', 'distance', 'time', 'calories', 'odometer', 'highlightPoint', 'foreground', 'background' ] self.symbolKeys = ['speed', 'distance', 'time', 'calories'] # make defaults darkDefault = [ "#024aeb", "#cf0202", "#19b536", "#ff9100", "#4d4d4d", "#faed00", "#969696", "#000000" ] lightDefault = [ "#0981cb", "#d80d0d", "#2bb512", "#ff9100", "#9f9f9f", "#deb009", "#4d4d4d", "#ffffff" ] defaults = { 'dark': dict(zip(self.keys, darkDefault)), 'light': dict(zip(self.keys, lightDefault)) } defaultSymbols = {key: 'x' for key in self.symbolKeys} for styleName, styleDct in defaults.items(): if styleName not in self.settings.childGroups(): self.settings.beginGroup(styleName) for key, colour in styleDct.items(): self.settings.setValue(key, colour) for key, symbol in defaultSymbols.items(): self.settings.setValue(f"{key}Symbol", symbol) self.settings.endGroup() self.name = style @property def name(self): return self._styleName @name.setter def name(self, name): if name.lower() not in self.validStyles: msg = f"Plot style must be one of {', '.join(self.validStyles)}, not '{name}'." raise ValueError(msg) self._styleName = name.lower() @property def validStyles(self): return self.settings.childGroups() def __getattr__(self, name): if name in self.keys: return self._getStyle(name) else: return self.__getattribute__(name) def __getitem__(self, name): if name in self.keys: return self._getStyle(name) else: raise KeyError(f"PlotStyle has no field '{name}'") def _getStyle(self, field): if field in ['foreground', 'background']: return self.settings.value(f"{self.name}/{field}") dct = {'colour': self.settings.value(f"{self.name}/{field}")} symbol = self.settings.value(f"{self.name}/{field}Symbol") if symbol is not None: dct['symbol'] = symbol return dct def getStyleDict(self, name=None): if name is None: name = self.name style = {} for field in self.keys: dct = {'colour': self.settings.value(f"{name}/{field}")} symbol = self.settings.value(f"{name}/{field}Symbol") if symbol is not None: dct['symbol'] = symbol style[field] = dct return style def addStyle(self, name, style): self.settings.beginGroup(name) for key, value in style.items(): self.settings.setValue(key, value) self.settings.endGroup() def removeStyle(self, name): self.settings.remove(name)