class SchnuffiApp(QApplication): def __init__(self, version: str): super(SchnuffiApp, self).__init__(sys.argv) self.setApplicationName(APP_NAME) self.setApplicationVersion(version) self.setApplicationDisplayName(self.applicationName()) self.last_focus_tree = QWidget() self.pos_ui = SchnuffiWindow(self) self.last_focus_tree.deleteLater() self.last_focus_tree = self.pos_ui.ModifiedWidget self.focusChanged.connect(self.app_focus_changed) # Init Font Database FontRsc.init(FontRsc.regular_pixel_size) # Prepare exception handling self.error_message_box = GenericErrorBox(self.pos_ui) KnechtExceptionHook.app = self KnechtExceptionHook.setup_signal_destination(self.report_exception) def app_focus_changed(self, old_widget: QWidget, new_widget: QWidget): if isinstance(new_widget, QTreeWidget): self.last_focus_tree = new_widget def tree_with_focus(self) -> QTreeWidget: """ Return the current or last known QTreeView in focus """ widget_in_focus = self.focusWidget() if isinstance(widget_in_focus, QTreeWidget): self.last_focus_tree = widget_in_focus return self.last_focus_tree def report_exception(self, msg): """ Receives KnechtExceptHook exception signal """ msg = _( '<h3>Hoppla!</h3>Eine schwerwiegende Anwendungsausnahme ist aufgetreten. Speichern Sie ' 'Ihre Daten und starten Sie die Anwendung neu.<br><br>' ) + msg.replace('\n', '<br>') self.error_message_box.setWindowTitle(_('Anwendungsausnahme')) self.error_message_box.set_error_msg(msg) self.error_message_box.exec_()
def replace_widget_layout(layout: QLayout, old: QWidget, new: QWidget, recursive: bool = True): """ Replace an old widget by a new one in a layout. :param layout: The layout in which we replace the widget. :param old: The old / replaced widget. :param new: The new / replacing widget. :param recursive: If recursive is True, the function looks for old in layout's sub-layouts. """ flag = Qt.FindChildrenRecursively if recursive else \ ~Qt.FindChildrenRecursively layout.replaceWidget(old, new, flag) old.deleteLater()
class ExpanderWidget(QGroupBox): def __init__(self, parent, title, content=None): super(ExpanderWidget, self).__init__(parent) self._title = title self._toggle = QToolButton(self) self._content = QWidget() if content is None else content self.initUI() def setTitle(self, title: str): self._title = title def title(self) -> str: return self._title def setContent(self, widget: QWidget): widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.MinimumExpanding) widget.setMinimumHeight(0) self.layout().replaceWidget(self._content, widget, Qt.FindChildOption.FindDirectChildrenOnly) self._content.deleteLater() self._content = widget self._toggle_closed(self._toggle.isChecked()) def content(self) -> QWidget: return self._content def setEnabled(self, arg: bool): if not arg: self.toggle(True) super().setEnabled(arg) def initUI(self): self.setStyleSheet( 'ExpanderWidget{ ' ' padding:2px -1px -1px 0px; margin-left:-1px; ' f'{" margin-top: -19px; padding-top:19px; " if sys.platform.startswith("darwin") else ""}' '}' 'ExpanderWidget::title{ ' ' color: transparent; ' # ' height: 0px; ' '}' 'ExpanderWidget>#expanderToggle{ ' ' background: transparent;' ' border: 1px solid transparent;' ' border-bottom: 1px solid #B0B0B0;' ' border-radius:0;' f' font-size: {QApplication.font().pointSizeF()}pt; ' '}' 'ExpanderWidget>#expanderToggle:hover,' 'ExpanderWidget>#expanderToggle:checked:hover{ ' ' background: qlineargradient( x1:0 y1:-0.1, x2:0 y2:1, ' ' stop:0 white, stop:1 palette(button));' ' border: 1px solid #B0B0B0; border-radius:2px;' '}' 'ExpanderWidget>#expanderToggle:checked,' 'ExpanderWidget>#expanderToggle:checked:pressed,' 'ExpanderWidget>#expanderToggle:pressed{' ' background: palette(background);' ' border: 1px solid #B0B0B0; border-radius:2px;' '}' 'ExpanderWidget:disabled{' ' border: 1px solid transparent; border-radius:2px;' '}' 'ExpanderWidget>#expanderToggle:focus{' ' border: 1px solid palette(highlight); border-radius:2px;' '}') layout = QVBoxLayout() layout.setSpacing(0) layout.setContentsMargins(0, 0, 0, 0) self.setLayout(layout) self._toggle.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self._toggle.setObjectName('expanderToggle') self._toggle.setText(self._title) self._toggle.setAutoRaise(True) self._toggle.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self._toggle.setMinimumHeight(self._toggle.height()) self._toggle.setArrowType(Qt.ArrowType.DownArrow) self._toggle.setCheckable(True) self._toggle.setChecked(True) self._toggle.clicked.connect(self._toggle_closed) self._content.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self._content.setMinimumHeight(0) self._content.setMaximumHeight(0) self._content layout.addWidget(self._toggle) layout.addWidget(self._content) @Slot(bool) def _toggle_closed(self, closed: bool): self._toggle.setArrowType( Qt.ArrowType.DownArrow if closed else Qt.ArrowType.UpArrow) self._content.setMaximumHeight(0 if closed else 16777215) def toggle(self, closed: bool): self._toggle.setChecked(closed)
class Category(QWidget): def __init__(self, name, categoryTotal, transactions, state): ## initialCollapsed = True ## QWidget.__init__(self) self.name = name self.state = state self.transactions = [] self.categoryTotal = categoryTotal self.collapsed = initialCollapsed self.sectionLayout = QVBoxLayout() self.gridContainer = QWidget() self.transactionArea = QScrollArea() self.progressBar = ProgressBar() self.addHeader() self.addBody(transactions) self.setLayout(self.sectionLayout) self.setAcceptDrops(True) def addHeader(self): self.headerText = QLabel() self.headerText.setText(self.name) # self.headerText.setFont(self.boldFont) self.headerText.setCursor(Qt.PointingHandCursor) self.headerText.mousePressEvent = self.toggleCollapsed self.headerText.setMinimumWidth(100) self.collapseButton = QLabel() self.collapseButton.setText('⊞' if self.collapsed else '⊟') self.collapseButton.setMaximumWidth(12) self.collapseButton.setCursor(Qt.PointingHandCursor) self.collapseButton.setToolTip('collapse') self.collapseButton.mousePressEvent = self.toggleCollapsed self.progressBar = ProgressBar(self.name == 'Income') self.progressBar.setCursor(Qt.PointingHandCursor) self.progressBar.mousePressEvent = lambda event: self.promptEditVal( self.name, self.categoryTotal) self.progressBar.setToolTip('Edit Max') self.uncategorizedAmtDisplay = QLabel() self.uncategorizedAmtDisplay.setText('${:.2f}'.format( self.categoryTotal)) self.uncategorizedAmtDisplay.setAlignment(Qt.AlignRight | Qt.AlignVCenter) header = QHBoxLayout() header.addWidget(self.collapseButton) header.addWidget(self.headerText) if self.name != 'Uncategorized': header.addWidget(self.progressBar) if self.name != 'Income': self.headerText.setContextMenuPolicy(Qt.CustomContextMenu) self.connect( self.headerText, SIGNAL('customContextMenuRequested(const QPoint &)'), self.titleContextMenu) else: header.addWidget(self.uncategorizedAmtDisplay) self.sectionLayout.addLayout(header) def addBody(self, transactions): self.transactionGrid = QVBoxLayout() self.addTransactions(transactions) def addTransactions(self, transactions): self.transactions += transactions clearLayout(self.transactionGrid) self.transactionGrid = QVBoxLayout() self.transactions.sort(key=lambda t: t.date) for idx, transaction in enumerate(self.transactions): line = TransactionLine(transaction) line.setContextMenuPolicy(Qt.CustomContextMenu) self.connect(line, SIGNAL('customContextMenuRequested(const QPoint &)'), lambda event, t=line: self.lineContextMenu(t)) line.adjustSize() self.transactionGrid.addWidget(line) self.gridContainer.deleteLater() self.gridContainer = QWidget() self.gridContainer.setLayout(self.transactionGrid) self.transactionArea.deleteLater() self.transactionArea.setParent(None) self.transactionArea = QScrollArea() self.transactionArea.setWidget(self.gridContainer) self.transactionArea.setWidgetResizable(True) if self.collapsed: self.transactionArea.hide() self.sectionLayout.addWidget(self.transactionArea) self.sectionLayout.setMargin(0) self.updateAmtDisplay() # if self.name != 'Uncategorized': # self.toggleCollapsed(None) def removeTransaction(self, transaction): newTransactions = self.transactions newTransactions.remove(transaction) self.transactions = [] self.addTransactions(newTransactions) def updateAmtDisplay(self): totalAmount = 0 if len(self.transactions): for idx, transaction in enumerate(self.transactions): totalAmount += float(transaction.amt) if self.name == 'Uncategorized': self.uncategorizedAmtDisplay.setText('$' + str(totalAmount)) else: self.progressBar.updateValues(self.categoryTotal, totalAmount) def lineContextMenu(self, transactionLine): menu = QMenu(self) editAction = QAction('Edit Transaction Name') # editAction.triggered.connect(lambda evt: self.editTransaction(transactionLine)) menu.addAction(editAction) menu.exec_(QCursor.pos()) def titleContextMenu(self, event): menu = QMenu(self) editAction = QAction('Edit Name') editAction.triggered.connect(self.promptEditTitle) removeAction = QAction('Remove Category') removeAction.triggered.connect(self.removeCategory) menu.addAction(editAction) menu.addAction(removeAction) menu.exec_(QCursor.pos()) def toggleCollapsed(self, event): if not event or event.button() == Qt.MouseButton.LeftButton: self.collapsed = not self.collapsed if self.collapsed: self.collapseButton.setText('⊞') self.transactionArea.hide() self.collapseButton.setToolTip('expand') else: self.collapseButton.setText('⊟') self.transactionArea.show() self.collapseButton.setToolTip('collapse') def dragEnterEvent(self, event): if event.mimeData().hasText() and self.name != 'Income': event.setDropAction(Qt.CopyAction) event.accept() else: event.ignore() def dropEvent(self, event): if event.mimeData().hasText(): transactionTitle = event.mimeData().text() self.state.next(Events.transaction_drop_event, transactionTitle, self.name) def getTransactions(self): return self.transactions def removeCategory(self): self.state.next(Events.remove_category, self.name, self.transactions) def promptEditVal(self, title, currentAmount): modal = EditCategoryTotalModal() data = modal.getData(currentAmount) if data: self.state.next(Events.update_category_total, self.name, int(data)) self.categoryTotal = int(data) self.updateAmtDisplay() def promptEditTitle(self): modal = EditTextModal(self.name, 'Edit Title') data = modal.getData() if data: self.state.next(Events.update_category_title, self.name, data) self.name = data self.headerText.setText(self.name)