예제 #1
0
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_()
예제 #2
0
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()
예제 #3
0
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)
예제 #4
0
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)