/
VolcanoByte
/
vbNotes
Обзор
Документация
Войти
/
VolcanoByte
/
vbNotes
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
sources/uFormMain.py
683 строки
26 KB
Eugene Gavrilyuk
fix sync and options
21 апр 2026, 15:03
21 апр 2026, 15:03
6f150f4
Код
Авторство
О чём код?
import os.path from functools import partial from datetime import datetime as dt from PyQt6.QtCore import Qt, QItemSelectionModel, QByteArray, QEvent, QThread from PyQt6.QtGui import QAction, QKeyEvent, QFontMetricsF, QShortcut, QKeySequence from PyQt6.QtWidgets import (QMainWindow, QWidget, QLabel, QHBoxLayout, QMessageBox, QFileDialog) from formMain import Ui_MainWindow from uConsts import (AppConsts, PageConsts, StoreConsts, ThemeConsts, SyncConsts, FILE_FILTERS) from uDB import db from uFormOptions import FormOptions from uFormProperties import FormProperties from uFormSearch import FormSearch from uGlobal import app, store, recentFiles, MessageLanguageConsts as mlc from uNote import Note from uSQL import SQL_TAG_SELECT_ALL from uThemes import lightPalette, darkPalette, darkStyleSheet, lightStyleSheet from uUtils import (confirmOkCancelDlg, confirmYesNoCancelDlg, PopUp, makeMarkdown) from uWebDavSync import WebDavSync class FormMain(QMainWindow, Ui_MainWindow): """Главная форма приложения""" def __init__(self): super().__init__() self.panelNotesVisible = None self.panelTagsVisible = None self.thread = None self.worker = None self.popup = None self.isChanged = False self.selectedNote = None self.findWhat = None self.setupUi(self) self.setWindowTitle(AppConsts.Title + ' - ' + db.filename) self.edNoteEditTags.installEventFilter(self) self.connectActions() self.connectChanges() # Настройка статусбара self.statusCount = QLabel() self.statusMessage = QLabel() widget = QWidget() widget.setLayout(QHBoxLayout()) self.statusBar().addWidget(self.statusMessage) self.statusBar().addPermanentWidget(self.statusCount) # Настройка редактора тегов self.edNoteEditTags.setPlaceholderText("Type your tags") self.edNoteEditTags.textEdited.connect(self.on_TagsTextEdited) self.readSettings() self.updateFont() self.updateTheme() if self.lvTags.tagsSelected: self.refresh(db.queryTagsNotes(self.lvTags.tagsSelected)) else: self.refresh(db.queryAllNotes()) self.isCheckedOnStart = False self.timer_id = self.startTimer(0) def checkFresh(self, where: str): """Проверка наличия свежей базы. where - 'local' или 'remote'""" sync = WebDavSync(db.property('fldPropertySyncURL'), db.property('fldPropertySyncUsername'), db.property('fldPropertySyncPassword')) try: if (where == 'remote' and store.value(StoreConsts.OptionsUpdateStart, True) and sync.check() == SyncConsts.YoungerBackup and confirmOkCancelDlg(self, mlc.syncFormBackup) == QMessageBox.StandardButton.Ok or where == 'local' and store.value(StoreConsts.OptionsUpdateFinish, True) and sync.check() == SyncConsts.YoungerDB and confirmOkCancelDlg(self, mlc.syncFormLocal) == QMessageBox.StandardButton.Ok): self.on_fileSynchronize() finally: WebDavSync.alreadyStarted = False def connectActions(self): self.lvNotes.selectionModel().selectionChanged.connect( self.on_selectNote) self.lvNotes.editFunc = self.on_editModify self.lvTags.selectionModel().selectionChanged.connect( self.on_selectTag) self.lvTags.refreshFunc = self.refresh self.aiFileNewCollection.triggered.connect(self.on_fileNewCollection) self.aiFileOpenCollection.triggered.connect(self.on_fileOpenCollection) self.menuOpenRecent.aboutToShow.connect(self.populateOpenRecent) self.aiFileProperties.triggered.connect(self.on_fileProperties) self.aiFileSynchronize.triggered.connect(self.on_fileSynchronize) self.aiEditOptions.triggered.connect(self.on_editOptions) self.aiFileExit.triggered.connect(self.close) self.aiEditNoteAdd.triggered.connect(self.on_editAdd) self.aiEditNoteModify.triggered.connect(self.on_editModify) self.aiEditNoteDelete.triggered.connect(self.on_editDelete) self.aiEditTagAdd.triggered.connect(self.on_editTagAdd) self.aiEditTagDelete.triggered.connect(self.on_editTagDelete) self.aiEditChangesConfirm.triggered.connect( self.on_editChangesConfirm) self.aiEditChangesCancel.triggered.connect(self.on_editChangesCancel) self.aiSearchFind.triggered.connect(self.on_searchFind) QShortcut(QKeySequence('Ctrl+F'), self.lvNotes, self.on_searchFind) self.menuViewPanels.addAction(self.dockNotes.toggleViewAction()) self.menuViewPanels.addAction(self.dockTags.toggleViewAction()) self.menuViewPanels.addAction(self.tbStandard.toggleViewAction()) self.aiViewClearSelection.triggered.connect( self.on_viewClearSelection) def connectChanges(self): # Отслеживание изменений при редактировании заметки self.edNoteEditTitle.textChanged.connect(self.setIsChanged) self.edNoteEditDescription.textChanged.connect(self.setIsChanged) self.edNoteEditTags.textChanged.connect(self.setIsChanged) self.teNoteEditText.textChanged.connect(self.setIsChanged) def eventFilter(self, source, event: QKeyEvent): """Перехват клавиш в форме редактирования""" if event.type() == QEvent.Type.KeyPress: km = event.modifiers() # Перехват клавиш для управления и вставки тегов if source == self.edNoteEditTags: # Shift-Return if (event.key() == Qt.Key.Key_Return and km == Qt.KeyboardModifier.ShiftModifier): self.on_TagsReturnPressed() return True # Shift-Up elif (event.key() == Qt.Key.Key_Up and km == Qt.KeyboardModifier.ShiftModifier): self.on_TagsComboUp() return True # Shift-Down elif (event.key() == Qt.Key.Key_Down and km == Qt.KeyboardModifier.ShiftModifier): self.on_TagsComboDown() return True return super().eventFilter(source, event) def timerEvent(self, event): """Обновляем состояние пунктов меню и кнопок""" isNoteSelected = self.lvNotes.selectionModel().hasSelection() isEditing = self.stackedWidget.currentIndex() == PageConsts.PageEdit isEditingOk = bool(self.edNoteEditTitle.text()) isTagComboBoxEmpty = len(self.cbxTags.currentText().strip()) == 0 isSyncAvailable = (not WebDavSync.alreadyStarted and bool(db.property('fldPropertySyncURL')) and bool(db.property('fldPropertySyncUsername')) and bool(db.property('fldPropertySyncPassword'))) self.widgetComboBoxTag.setEnabled(isNoteSelected and not isEditing) self.lvNotes.setEnabled(not isEditing) self.lvTags.setEnabled(not isEditing) self.btnNoteEditConfirm.setEnabled(isEditingOk) self.aiFileSynchronize.setEnabled(isSyncAvailable) self.aiEditOptions.setEnabled(not isEditing) self.aiEditNoteAdd.setEnabled(not isEditing) self.aiEditNoteModify.setEnabled(isNoteSelected and not isEditing) self.aiEditNoteDelete.setEnabled(isNoteSelected and not isEditing) self.aiEditTagAdd.setEnabled(isNoteSelected and not isEditing and not isTagComboBoxEmpty) self.btnTagAdd.setEnabled(self.aiEditTagAdd.isEnabled()) self.aiEditTagDelete.setEnabled(isNoteSelected and not isEditing and not isTagComboBoxEmpty) self.btnTagDelete.setEnabled(self.aiEditTagDelete.isEnabled()) self.aiEditChangesConfirm.setEnabled(isEditing and isEditingOk) self.aiEditChangesCancel.setEnabled(isEditing) self.aiViewClearSelection.setEnabled(not isEditing) if not self.isCheckedOnStart: self.isCheckedOnStart = True self.checkFresh('remote') def writeSettings(self): """Сохранение настроек""" store.setValue(StoreConsts.MainFormGeometry, self.saveGeometry()) store.setValue(StoreConsts.MainFormState, self.saveState()) def readSettings(self): """Загрузка настроек""" self.restoreGeometry(store.value( StoreConsts.MainFormGeometry, QByteArray())) self.restoreState(store.value( StoreConsts.MainFormState, QByteArray())) def updateTheme(self): theme = store.value(StoreConsts.OptionsTheme, AppConsts.DefaultTheme) if theme == ThemeConsts.Dark: app.setPalette(darkPalette) self.lvTags.setStyleSheet(darkStyleSheet) else: app.setPalette(lightPalette) self.lvTags.setStyleSheet(lightStyleSheet) def updateFont(self): fontName = store.value(StoreConsts.OptionsFontName, AppConsts.DefaultCodeFontName) fontSize = int(store.value(StoreConsts.OptionsFontSize, AppConsts.DefaultCodeFontSize)) tabSize = int(store.value(StoreConsts.OptionsTabSize, AppConsts.DefaultCodeTabSize)) font = self.tbNoteViewText.currentFont() font.setFamily(fontName) font.setPointSize(fontSize) self.tbNoteViewText.setFont(font) self.teNoteEditText.setFont(font) self.teNoteEditText.setTabStopDistance( QFontMetricsF(font).horizontalAdvance(' ') * tabSize) def hideEvent(self, event): self.panelNotesVisible = self.dockNotes.isVisible() self.panelTagsVisible = self.dockTags.isVisible() def showEvent(self, event): if self.panelNotesVisible: self.dockNotes.setVisible(self.panelNotesVisible) if self.panelTagsVisible: self.dockTags.setVisible(self.panelTagsVisible) def closeEvent(self, event): self.checkFresh('local') self.writeSettings() super().closeEvent(event) event.accept() def getSelectedNote(self, selected): """Возвращает id выбранной заметки в ListView""" row = selected.indexes()[0].row() return self.lvNotes.noteProxy.index(row, 0).data() def getSelectedNotes(self) -> set: """Возвращает множество выбранных заметок""" res = set() for _row in range(self.lvNotes.noteProxy.rowCount()): if self.lvNotes.selectionModel().isRowSelected(_row): _index = (self.lvNotes.noteProxy.index(_row, 0).data()) res.add(_index) return res def getVisibleNotes(self) -> set: res = set() for _row in range(self.lvNotes.noteProxy.rowCount()): _index = (self.lvNotes.noteProxy.index(_row, 0).data()) res.add(_index) return res def getSelectedTag(self, selected) -> str: """Возвращает выбранный тег в ListView""" row = selected.indexes()[0].row() res = self.lvTags.model().index(row, 1).data() return res def refresh(self, query: str = None, pageIfNoRow: int = PageConsts.PageWelcome): """Обновление моделей списка заметок (и тегов)""" if query: self.lvNotes.noteQuery = query self.lvNotes.noteModel.setQuery(self.lvNotes.noteQuery) self.refreshTags() self.stackedWidget.setCurrentIndex(pageIfNoRow) self.refreshInfo() def refreshInfo(self): _dt = dt.fromtimestamp(db.timestamp()) self.setWindowTitle(f"{AppConsts.Title} - {db.syncId} " f"[{dt.strftime(_dt, '%Y-%m-%d %H-%M')}]") self.statusCount.setText(mlc.statusBarCounter % ( self.lvNotes.model().rowCount(), self.lvTags.model().rowCount())) self.statusMessage.setText(mlc.statusBarReady) def refreshTags(self): """Обновление модели списка тегов""" _ids = self.getSelectedNotes() if not _ids: _ids = self.getVisibleNotes() self.lvTags.tagModel.setQuery(db.queryNotesTags(_ids)) self.cbxTags.tagComboBoxModel.setQuery(SQL_TAG_SELECT_ALL) self.cbxTags.clearEditText() self.cbxNoteEditTags.tagComboBoxModel.setQuery(SQL_TAG_SELECT_ALL) self.cbxNoteEditTags.clearEditText() def updateNotesList(self, noteId: int = None): """Обновление списка заметок (если указан noteId, то выделяем ее)""" self.refresh() # Выделяем сниппет if noteId: _start = self.lvNotes.noteProxy.index(0, 0) _matches = self.lvNotes.noteProxy.match( _start, Qt.ItemDataRole.DisplayRole, noteId, 1, Qt.MatchFlag.MatchExactly) if _matches: _index = _matches[0] self.lvNotes.selectionModel().clearSelection() self.lvNotes.selectionModel().setCurrentIndex( _index, QItemSelectionModel.SelectionFlag.Select | QItemSelectionModel.SelectionFlag.Rows) def fillPageView(self, note: Note): """Заполнение страницы просмотра""" # self.selectedNote = note.data['fldNoteId'] self.lblNoteViewTitle.setText(note.data['fldNoteTitle']) self.lblNoteViewDescription.setText( note.data['fldNoteDescription']) self.lblNoteViewTags.setText(note.tagsToString()) try: _ts = float(note.data['fldNoteUpdatedAt']) _activity = dt.strftime(dt.fromtimestamp(_ts), f"%d-%m-%Y %H-%M") except ValueError: _activity = mlc.unknown self.lblNoteViewLastActive.setText( mlc.lastActive % _activity) _text = note.data['fldNoteText'] _style = store.value(StoreConsts.OptionsStyle, AppConsts.DefaultCodeStyle) self.tbNoteViewText.setText(makeMarkdown(_style, _text)) def clearPageEdit(self): self.edNoteEditTitle.clear() self.edNoteEditDescription.clear() self.edNoteEditTags.clear() self.teNoteEditText.clear() def fillPageEdit(self, note: Note): self.edNoteEditTitle.setText(note.data['fldNoteTitle']) self.edNoteEditDescription.setText( note.data['fldNoteDescription']) self.edNoteEditTags.setText(note.tagsToString()) self.teNoteEditText.setText(note.data['fldNoteText']) @staticmethod def getTagInPosition(tags: list, pos: int) -> int | None: """Определение тега в списке по указанному положению курсора.""" _len = 1 _index = 0 for tag in tags: _len += len(tag) if pos < _len: return _index _len += 1 _index += 1 def on_TagsComboUp(self): _index = self.cbxNoteEditTags.currentIndex() if _index != 0: self.cbxNoteEditTags.setCurrentIndex(_index - 1) def on_TagsComboDown(self): _index = self.cbxNoteEditTags.currentIndex() if _index != self.cbxNoteEditTags.count() - 1: self.cbxNoteEditTags.setCurrentIndex(_index + 1) def on_TagsReturnPressed(self): """Замена тега в позиции курсора""" # Проверяем, нет ли уже такого тега tags = [x.strip() for x in self.edNoteEditTags.text().split(',')] if self.cbxNoteEditTags.currentText() in tags: return pos = self.edNoteEditTags.cursorPosition() tags = self.edNoteEditTags.text().split(',') tagIndex = self.getTagInPosition(tags, pos) if tagIndex is not None: tags[tagIndex] = self.cbxNoteEditTags.currentText() self.edNoteEditTags.setText(', '.join([x.strip() for x in tags])) def on_TagsTextEdited(self, text): """Определение положения курсора в списке тегов и выбор подходящего в выпадающем списке.""" pos = self.edNoteEditTags.cursorPosition() tags = text.split(',') tagIndex = self.getTagInPosition(tags, pos) _tag = tags[tagIndex].strip() if _tag: _comboIndex = self.cbxNoteEditTags.findText( _tag, Qt.MatchFlag.MatchStartsWith) if _comboIndex >= 0: self.cbxNoteEditTags.setCurrentIndex(_comboIndex) def on_selectNote(self, selected): """Реализация события выбора заметки""" # Пересчитываем теги if self.lvNotes.selectionModel().hasSelection(): _ids = self.getSelectedNotes() else: # Теги только тех заметок, которые присутствуют в списке _ids = self.getVisibleNotes() self.lvTags.tagModel.setQuery(db.queryNotesTags(_ids)) if selected: noteId = self.getSelectedNote(selected) self.selectedNote = db.getNote(noteId) self.fillPageView(self.selectedNote) self.stackedWidget.setCurrentIndex(PageConsts.PageView) else: self.selectedNote = None self.stackedWidget.setCurrentIndex(PageConsts.PageWelcome) def on_selectTag(self, selected): """Реализация события выбора тега""" if selected: tag = self.getSelectedTag(selected) self.cbxTags.setCurrentText(tag) @staticmethod def getFileNameDialog(caption: str, accept: QFileDialog.AcceptMode, mode: QFileDialog.FileMode) -> str | None: """Подготовка диалога и выбор файла""" dialog = QFileDialog() dialog.setWindowTitle(caption) dialog.setNameFilters(FILE_FILTERS) dialog.selectNameFilter(FILE_FILTERS[0]) dialog.setAcceptMode(accept) dialog.setFileMode(mode) ok = dialog.exec() if ok: return dialog.selectedFiles()[0] def openDB(self, filename: str): db.open(filename) recentFiles.add(filename) store.setValue(StoreConsts.DBFileName, filename) self.refresh() def on_fileNewCollection(self): """Создание новой коллекции""" filename = self.getFileNameDialog('New collection', QFileDialog.AcceptMode.AcceptSave, QFileDialog.FileMode.AnyFile) if filename is not None: db.close() if os.path.exists(filename): os.remove(filename) self.openDB(filename) def on_fileOpenCollection(self): """Открытие коллекции""" filename = self.getFileNameDialog('Open collection', QFileDialog.AcceptMode.AcceptOpen, QFileDialog.FileMode.ExistingFile) if filename is not None: self.on_fileOpenRecent(filename) def on_fileOpenRecent(self, filename): if not os.path.exists(filename): if (confirmOkCancelDlg(self, mlc.deleteRecentConfirm) == QMessageBox.StandardButton.Ok): recentFiles.remove(filename) else: return else: db.close() self.openDB(filename) def populateOpenRecent(self): # Шаг 1. Удаляем старые пункты меню self.menuOpenRecent.clear() # Шаг 2. Динамически создаем действия actions = [] for filename in recentFiles.recentFiles: action = QAction(filename, self) action.triggered.connect(partial(self.on_fileOpenRecent, filename)) actions.append(action) # Шаг 3. Добавляем действия к меню self.menuOpenRecent.addActions(actions) def on_fileProperties(self): winProperties = FormProperties(self) if winProperties.exec(): pass def on_fileSynchronize(self): self.thread = QThread() self.worker = WebDavSync(db.property('fldPropertySyncURL'), db.property('fldPropertySyncUsername'), db.property('fldPropertySyncPassword')) self.popup = PopUp(self) # Перед запуском синхронизации нужно закрыть базу db.close() self.worker.moveToThread(self.thread) self.thread.started.connect(self.worker.run) self.worker.finished.connect(self.finished) self.worker.finished.connect(self.thread.quit) self.worker.finished.connect(self.worker.deleteLater) self.thread.finished.connect(self.thread.deleteLater) self.worker.finished.connect(self.close_popup) self.thread.start() self.show_popup() # Как закончили синхронизацию, нужно переоткрыть базу db.reopen() if WebDavSync.result[0]: self.refresh() self.statusMessage.setText(WebDavSync.result[1]) @staticmethod def finished(): WebDavSync.alreadyStarted = False def close_popup(self): self.popup.close() def show_popup(self): self.popup.exec() def on_editAdd(self): """Добавление новой заметки""" self.selectedNote = None self.clearPageEdit() self.stackedWidget.setCurrentIndex(PageConsts.PageEdit) def setIsChanged(self): self.isChanged = True def on_editModify(self): """Изменение выделенной заметки""" self.fillPageEdit(self.selectedNote) self.stackedWidget.setCurrentIndex(PageConsts.PageEdit) self.edNoteEditTitle.setFocus() self.isChanged = False def on_editDelete(self): """Удаление заметки.""" if (confirmOkCancelDlg(self, mlc.deleteConfirm) == QMessageBox.StandardButton.Ok): ids = self.getSelectedNotes() db.deleteNotes(ids) self.lvTags.checkIsTagsExists() def on_editTagAdd(self): _tag = self.cbxTags.currentText() _ids = self.getSelectedNotes() for _id in _ids: db.addNoteTag(_id, _tag) db.prepareTags() self.refreshTags() self.fillPageView(self.selectedNote) def on_editTagDelete(self): _tag = self.cbxTags.currentText() _ids = self.getSelectedNotes() for _id in _ids: db.deleteNoteTag(_id, _tag) db.prepareTags() self.refreshTags() self.fillPageView(self.selectedNote) def on_editOptions(self): winOptions = FormOptions(self) if winOptions.exec(): self.updateFont() self.updateTheme() self.refresh() def on_editChangesConfirm(self): """Подтверждение изменения заметки""" note = Note() note.data['fldNoteTitle'] = self.edNoteEditTitle.text() note.data['fldNoteDescription'] = ( self.edNoteEditDescription.text()) note.data['fldNoteText'] = self.teNoteEditText.toPlainText() note.tags = Note.stringToTags(self.edNoteEditTags.text()) if self.selectedNote: _id = int(self.selectedNote.data['fldNoteId']) db.updateNote(_id, note) else: _id = db.addNote(note) db.prepareTags() self.updateNotesList(_id) def on_editChangesCancel(self): """Отмена изменения заметки""" if self.isChanged: button = confirmYesNoCancelDlg(self, mlc.changeConfirm) if button == QMessageBox.StandardButton.Yes: self.on_editChangesConfirm() elif button == QMessageBox.StandardButton.Cancel: return if self.lvNotes.selectionModel().hasSelection(): self.stackedWidget.setCurrentIndex(PageConsts.PageView) else: self.stackedWidget.setCurrentIndex(PageConsts.PageWelcome) def on_viewClearSelection(self): """Сброс выделения заметок и тегов""" if self.lvNotes.selectionModel().hasSelection(): self.refresh() else: self.lvTags.clearTags() def on_searchFind(self): winSearch = FormSearch(self) if self.findWhat: winSearch.cbxWhat.setCurrentText(self.findWhat) if winSearch.exec(): self.findWhat = winSearch.cbxWhat.currentText() where = set() if store.value(StoreConsts.SearchTitle, False): where.add('fldNoteTitle') if store.value(StoreConsts.SearchDescription, False): where.add('fldNoteDescription') if store.value(StoreConsts.SearchText, False): where.add('fldNoteText') query = ('SELECT * FROM tblNotes WHERE fldNoteId IN ' + db.find(self.findWhat, where)) self.refresh(query, PageConsts.PageSearchNotFound)