Beispiel #1
0
    def __init__(self, app):
        QMainWindow.__init__(self, None)
        self.documentPath = None
        self.doc = DocumentModel(app=app.model)
        self.app = app

        self._setupUi()

        # Create base elements
        self.model = MainWindowModel(document=self.doc)
        self.model2view = {}
        self.alookup = Lookup(self, model=self.model.account_lookup)
        self.clookup = Lookup(self, model=self.model.completion_lookup)
        self.drsel = DateRangeSelector(mainwindow=self,
                                       view=self.dateRangeSelectorView)
        self.sfield = SearchField(model=self.model.search_field,
                                  view=self.searchLineEdit)
        self.recentDocuments = Recent(self.app, 'recentDocuments')
        self.recentDocuments.addMenu(self.menuOpenRecent)

        self.doc.view = self
        self.model.view = self

        self._updateUndoActions()
        self._bindSignals()
Beispiel #2
0
    def __init__(self, app):
        QMainWindow.__init__(self, None)
        self.documentPath = None
        self.doc = DocumentModel(app=app.model)
        self.app = app

        self._setupUi()

        # Create base elements
        self.model = MainWindowModel(document=self.doc)
        self.model2view = {}
        self.alookup = Lookup(self, model=self.model.account_lookup)
        self.clookup = Lookup(self, model=self.model.completion_lookup)
        self.drsel = DateRangeSelector(mainwindow=self, view=self.dateRangeSelectorView)
        self.sfield = SearchField(model=self.model.search_field, view=self.searchLineEdit)
        self.recentDocuments = Recent(self.app, 'recentDocuments')
        self.recentDocuments.addMenu(self.menuOpenRecent)

        self.doc.view = self
        self.model.view = self

        self._updateUndoActions()
        self._bindSignals()
Beispiel #3
0
 def __init__(self, app: pyref):
     model = Document(app.model)
     PyGUIObject.__init__(self, model)
     self.model.connect()
Beispiel #4
0
 def __init__(self, app):
     QObject.__init__(self)
     self.app = app
     self.documentPath = None
     self.model = DocumentModel(app=app.model)
     self.model.view = self
Beispiel #5
0
class Document(QObject):
    def __init__(self, app):
        QObject.__init__(self)
        self.app = app
        self.documentPath = None
        self.model = DocumentModel(app=app.model)
        self.model.view = self

    # --- Public
    def close(self):
        if self.documentPath:
            self.model.close()

    def confirmDestructiveAction(self):
        # Asks whether the user wants to continue before continuing with an action that will replace
        # the current document. Will save the document as needed. Returns True if the action can
        # continue.
        if not self.model.is_dirty():
            return True
        title = tr("Unsaved Document")
        msg = tr("Do you want to save your changes before continuing?")
        buttons = QMessageBox.Save | QMessageBox.Cancel | QMessageBox.Discard
        result = QMessageBox.question(self.app.mainWindow, title, msg, buttons)
        if result == QMessageBox.Save:
            self.save()
            if self.model.is_dirty(): # "save as" was cancelled
                return False
            else:
                return True
        elif result == QMessageBox.Cancel:
            return False
        elif result == QMessageBox.Discard:
            return True

    def new(self):
        if not self.confirmDestructiveAction():
            return
        self.close()
        self.documentPath = None
        self.model.clear()
        self.documentPathChanged.emit()

    def open(self, docpath, initial=False):
        # initial flag is true when open() is called at the document's initialization. When that's
        # the case, we need to create a new document when we fail opening this one.
        if not self.confirmDestructiveAction():
            return
        self.close()
        try:
            self.model.load_from_xml(docpath)
            self.documentPath = docpath
        except FileFormatError as e:
            QMessageBox.warning(self.app.mainWindow, tr("Cannot load file"), str(e))
            if initial:
                self.new()
        self.documentPathChanged.emit()
        self.documentOpened.emit(docpath)

    def openDocument(self):
        title = tr("Select a document to load")
        filters = tr("moneyGuru Documents (*.moneyguru)")
        docpath, filetype = QFileDialog.getOpenFileName(self.app.mainWindow, title, '', filters)
        if docpath:
            self.open(docpath)

    def openExampleDocument(self):
        if not self.confirmDestructiveAction():
            return
        self.close()
        dirpath = tempfile.mkdtemp()
        destpath = op.join(dirpath, 'example.moneyguru')
        QFile.copy(':/example.moneyguru', destpath)
        self.model.load_from_xml(destpath)
        self.model.adjust_example_file()
        self.documentPath = None # As if it was a new doc. Save As is required.
        self.documentPathChanged.emit()

    def save(self):
        if self.documentPath is not None:
            self.model.save_to_xml(self.documentPath)
        else:
            self.saveAs()

    def saveAs(self):
        title = tr("Save As")
        filters = tr("moneyGuru Documents (*.moneyguru)")
        docpath = str(QFileDialog.getSaveFileName(self.app.mainWindow, title, '', filters))
        if docpath:
            if not docpath.endswith('.moneyguru'):
                docpath += '.moneyguru'
            self.model.save_to_xml(docpath)
            self.documentPath = docpath
            self.documentPathChanged.emit()
            self.documentSavedAs.emit(docpath)

    # model --> view
    def query_for_schedule_scope(self):
        if QApplication.keyboardModifiers() & Qt.ShiftModifier:
            return ScheduleScope.Global
        if not self.app.model.show_schedule_scope_dialog:
            return ScheduleScope.Local
        dialog = ScheduleScopeDialog(self.app.mainWindow)
        return dialog.queryForScope()

    # --- Signals
    documentOpened = pyqtSignal(str)
    documentSavedAs = pyqtSignal(str)
    documentPathChanged = pyqtSignal()
Beispiel #6
0
class MainWindow(QMainWindow):
    def __init__(self, app):
        QMainWindow.__init__(self, None)
        self.documentPath = None
        self.doc = DocumentModel(app=app.model)
        self.app = app

        self._setupUi()

        # Create base elements
        self.model = MainWindowModel(document=self.doc)
        self.model2view = {}
        self.alookup = Lookup(self, model=self.model.account_lookup)
        self.clookup = Lookup(self, model=self.model.completion_lookup)
        self.drsel = DateRangeSelector(mainwindow=self, view=self.dateRangeSelectorView)
        self.sfield = SearchField(model=self.model.search_field, view=self.searchLineEdit)
        self.recentDocuments = Recent(self.app, 'recentDocuments')
        self.recentDocuments.addMenu(self.menuOpenRecent)

        self.doc.view = self
        self.model.view = self

        self._updateUndoActions()
        self._bindSignals()

    def _setupUi(self): # has to take place *before* base elements creation
        self.setWindowTitle("moneyGuru")
        self.resize(700, 580)
        self.centralwidget = QWidget(self)
        self.verticalLayout = QVBoxLayout(self.centralwidget)
        self.verticalLayout.setSpacing(0)
        self.verticalLayout.setContentsMargins(0, 0, 0, 0)
        self.topBar = QWidget(self.centralwidget)
        self.horizontalLayout_2 = QHBoxLayout(self.topBar)
        self.horizontalLayout_2.setContentsMargins(2, 0, 2, 0)
        spacerItem = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
        self.horizontalLayout_2.addItem(spacerItem)
        self.dateRangeSelectorView = DateRangeSelectorView(self.topBar)
        self.dateRangeSelectorView.setMinimumSize(QSize(220, 0))
        self.horizontalLayout_2.addWidget(self.dateRangeSelectorView)
        spacerItem1 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
        self.horizontalLayout_2.addItem(spacerItem1)
        self.searchLineEdit = SearchEdit(self.topBar)
        self.searchLineEdit.setMaximumSize(QSize(240, 16777215))
        self.horizontalLayout_2.addWidget(self.searchLineEdit)
        self.verticalLayout.addWidget(self.topBar)
        self.tabBar = TabBarPlus(self.centralwidget)
        self.tabBar.setMinimumSize(QSize(0, 20))
        self.verticalLayout.addWidget(self.tabBar)
        self.mainView = QStackedWidget(self.centralwidget)
        self.verticalLayout.addWidget(self.mainView)

        # Bottom buttons & status label
        self.bottomBar = QWidget(self.centralwidget)
        self.horizontalLayout = QHBoxLayout(self.bottomBar)
        self.horizontalLayout.setContentsMargins(2, 2, 2, 2)
        self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
        self.newItemButton = QPushButton(self.bottomBar)
        buttonSizePolicy = QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
        buttonSizePolicy.setHorizontalStretch(0)
        buttonSizePolicy.setVerticalStretch(0)
        buttonSizePolicy.setHeightForWidth(self.newItemButton.sizePolicy().hasHeightForWidth())
        self.newItemButton.setSizePolicy(buttonSizePolicy)
        self.newItemButton.setIcon(QIcon(QPixmap(':/plus_8')))
        self.horizontalLayout.addWidget(self.newItemButton)
        self.deleteItemButton = QPushButton(self.bottomBar)
        self.deleteItemButton.setSizePolicy(buttonSizePolicy)
        self.deleteItemButton.setIcon(QIcon(QPixmap(':/minus_8')))
        self.horizontalLayout.addWidget(self.deleteItemButton)
        self.editItemButton = QPushButton(self.bottomBar)
        self.editItemButton.setSizePolicy(buttonSizePolicy)
        self.editItemButton.setIcon(QIcon(QPixmap(':/info_gray_12')))
        self.horizontalLayout.addWidget(self.editItemButton)
        self.horizontalLayout.addItem(horizontalSpacer(size=20))
        self.graphVisibilityButton = QPushButton()
        self.graphVisibilityButton.setSizePolicy(buttonSizePolicy)
        self.graphVisibilityButton.setIcon(QIcon(QPixmap(':/graph_visibility_on_16')))
        self.horizontalLayout.addWidget(self.graphVisibilityButton)
        self.piechartVisibilityButton = QPushButton()
        self.piechartVisibilityButton.setSizePolicy(buttonSizePolicy)
        self.piechartVisibilityButton.setIcon(QIcon(QPixmap(':/piechart_visibility_on_16')))
        self.horizontalLayout.addWidget(self.piechartVisibilityButton)
        self.columnsVisibilityButton = QPushButton()
        self.columnsVisibilityButton.setSizePolicy(buttonSizePolicy)
        self.columnsVisibilityButton.setIcon(QIcon(QPixmap(':/columns_16')))
        self.horizontalLayout.addWidget(self.columnsVisibilityButton)

        self.statusLabel = QLabel(tr("Status"))
        self.statusLabel.setAlignment(Qt.AlignCenter)
        self.horizontalLayout.addWidget(self.statusLabel)
        self.verticalLayout.addWidget(self.bottomBar)


        self.setCentralWidget(self.centralwidget)
        self.menubar = QMenuBar(self)
        self.menubar.setGeometry(QRect(0, 0, 700, 20))
        self.menuFile = QMenu(tr("File"))
        self.menuOpenRecent = QMenu(tr("Open Recent"))
        self.menuView = QMenu(tr("View"))
        self.menuDateRange = QMenu(tr("Date Range"))
        self.menuEdit = QMenu(tr("Edit"))
        self.menuHelp = QMenu(tr("Help"))
        self.setMenuBar(self.menubar)
        self.actionOpenDocument = QAction(tr("Open..."), self)
        self.actionOpenDocument.setShortcut("Ctrl+O")
        self.actionShowNetWorth = QAction(tr("Net Worth"), self)
        self.actionShowNetWorth.setShortcut("Ctrl+1")
        self.actionShowNetWorth.setIcon(QIcon(QPixmap(':/balance_sheet_48')))
        self.actionShowProfitLoss = QAction(escapeamp(tr("Profit & Loss")), self)
        self.actionShowProfitLoss.setShortcut("Ctrl+2")
        self.actionShowProfitLoss.setIcon(QIcon(QPixmap(':/income_statement_48')))
        self.actionShowTransactions = QAction(tr("Transactions"), self)
        self.actionShowTransactions.setShortcut("Ctrl+3")
        self.actionShowTransactions.setIcon(QIcon(QPixmap(':/transaction_table_48')))
        self.actionShowSelectedAccount = QAction(tr("Show Account"), self)
        self.actionShowSelectedAccount.setShortcut("Ctrl+]")
        self.actionNewItem = QAction(tr("New Item"), self)
        self.actionNewItem.setShortcut("Ctrl+N")
        self.actionDeleteItem = QAction(tr("Remove Selected"), self)
        self.actionEditItem = QAction(tr("Show Info"), self)
        self.actionEditItem.setShortcut("Ctrl+I")
        self.actionToggleGraph = QAction(tr("Toggle Graph"), self)
        self.actionToggleGraph.setShortcut("Ctrl+Alt+G")
        self.actionTogglePieChart = QAction(tr("Toggle Pie Chart"), self)
        self.actionTogglePieChart.setShortcut("Ctrl+Alt+P")
        self.actionMoveUp = QAction(tr("Move Up"), self)
        self.actionMoveUp.setShortcut("Ctrl++")
        self.actionMoveDown = QAction(tr("Move Down"), self)
        self.actionMoveDown.setShortcut("Ctrl+-")
        self.actionNavigateBack = QAction(tr("Go Back"), self)
        self.actionNavigateBack.setShortcut("Ctrl+[")
        self.actionNewAccountGroup = QAction(tr("New Account Group"), self)
        self.actionNewAccountGroup.setShortcut("Ctrl+Shift+N")
        self.actionShowNextView = QAction(tr("Next View"), self)
        self.actionShowNextView.setShortcut("Ctrl+Shift+]")
        self.actionShowPreviousView = QAction(tr("Previous View"), self)
        self.actionShowPreviousView.setShortcut("Ctrl+Shift+[")
        self.actionNewDocument = QAction(tr("New Document"), self)
        self.actionImport = QAction(tr("Import..."), self)
        self.actionImport.setShortcut("Ctrl+Alt+I")
        self.actionExport = QAction(tr("Export..."), self)
        self.actionExport.setShortcut("Ctrl+Alt+E")
        self.actionSave = QAction(tr("Save"), self)
        self.actionSave.setShortcut("Ctrl+S")
        self.actionSaveAs = QAction(tr("Save As..."), self)
        self.actionSaveAs.setShortcut("Ctrl+Shift+S")
        self.actionAbout = QAction(tr("About moneyGuru"), self)
        self.actionToggleReconciliationMode = QAction(tr("Toggle Reconciliation Mode"), self)
        self.actionToggleReconciliationMode.setShortcut("Ctrl+Shift+R")
        self.actionToggleAccountExclusion = QAction(tr("Toggle Exclusion Status of Account"), self)
        self.actionToggleAccountExclusion.setShortcut("Ctrl+Shift+X")
        self.actionShowSchedules = QAction(tr("Schedules"), self)
        self.actionShowSchedules.setShortcut("Ctrl+4")
        self.actionShowSchedules.setIcon(QIcon(QPixmap(':/schedules_48')))
        self.actionReconcileSelected = QAction(tr("Reconcile Selection"), self)
        self.actionReconcileSelected.setShortcut("Ctrl+R")
        self.actionMakeScheduleFromSelected = QAction(tr("Make Schedule from Selected"), self)
        self.actionMakeScheduleFromSelected.setShortcut("Ctrl+M")
        self.actionShowPreferences = QAction(tr("Preferences..."), self)
        self.actionPrint = QAction(tr("Print..."), self)
        self.actionPrint.setShortcut("Ctrl+P")
        self.actionQuit = QAction(tr("Quit moneyGuru"), self)
        self.actionQuit.setShortcut("Ctrl+Q")
        self.actionUndo = QAction(tr("Undo"), self)
        self.actionUndo.setShortcut("Ctrl+Z")
        self.actionRedo = QAction(tr("Redo"), self)
        self.actionRedo.setShortcut("Ctrl+Y")
        self.actionShowHelp = QAction(tr("moneyGuru Help"), self)
        self.actionShowHelp.setShortcut("F1")
        self.actionDuplicateTransaction = QAction(tr("Duplicate Transaction"), self)
        self.actionDuplicateTransaction.setShortcut("Ctrl+D")
        self.actionJumpToAccount = QAction(tr("Jump to Account..."), self)
        self.actionJumpToAccount.setShortcut("Ctrl+Shift+A")
        self.actionNewTab = QAction(tr("New Tab"), self)
        self.actionNewTab.setShortcut("Ctrl+T")
        self.actionCloseTab = QAction(tr("Close Tab"), self)
        self.actionCloseTab.setShortcut("Ctrl+W")

        self.menuFile.addAction(self.actionNewDocument)
        self.menuFile.addAction(self.actionNewTab)
        self.menuFile.addAction(self.actionOpenDocument)
        self.menuFile.addAction(self.menuOpenRecent.menuAction())
        self.menuFile.addAction(self.actionImport)
        self.menuFile.addSeparator()
        self.menuFile.addAction(self.actionCloseTab)
        self.menuFile.addAction(self.actionSave)
        self.menuFile.addAction(self.actionSaveAs)
        self.menuFile.addAction(self.actionExport)
        self.menuFile.addAction(self.actionPrint)
        self.menuFile.addAction(self.actionQuit)
        self.menuView.addAction(self.actionShowNetWorth)
        self.menuView.addAction(self.actionShowProfitLoss)
        self.menuView.addAction(self.actionShowTransactions)
        self.menuView.addAction(self.actionShowSchedules)
        self.menuView.addAction(self.actionShowPreviousView)
        self.menuView.addAction(self.actionShowNextView)
        self.menuView.addAction(self.menuDateRange.menuAction())
        self.menuView.addAction(self.actionShowPreferences)
        self.menuView.addAction(self.actionToggleGraph)
        self.menuView.addAction(self.actionTogglePieChart)
        self.menuEdit.addAction(self.actionNewItem)
        self.menuEdit.addAction(self.actionNewAccountGroup)
        self.menuEdit.addAction(self.actionDeleteItem)
        self.menuEdit.addAction(self.actionEditItem)
        self.menuEdit.addSeparator()
        self.menuEdit.addAction(self.actionMoveUp)
        self.menuEdit.addAction(self.actionMoveDown)
        self.menuEdit.addAction(self.actionDuplicateTransaction)
        self.menuEdit.addAction(self.actionMakeScheduleFromSelected)
        self.menuEdit.addAction(self.actionReconcileSelected)
        self.menuEdit.addAction(self.actionToggleReconciliationMode)
        self.menuEdit.addAction(self.actionToggleAccountExclusion)
        self.menuEdit.addSeparator()
        self.menuEdit.addAction(self.actionShowSelectedAccount)
        self.menuEdit.addAction(self.actionNavigateBack)
        self.menuEdit.addAction(self.actionJumpToAccount)
        self.menuEdit.addSeparator()
        self.menuEdit.addAction(self.actionUndo)
        self.menuEdit.addAction(self.actionRedo)
        self.menuHelp.addAction(self.actionShowHelp)
        self.menuHelp.addAction(self.actionAbout)
        mainmenus = [self.menuFile, self.menuEdit, self.menuView, self.menuHelp]
        for menu in mainmenus:
            self.menubar.addAction(menu.menuAction())
            setAccelKeys(menu)
        setAccelKeys(self.menubar)
        self.tabBar.setMovable(True)
        self.tabBar.setTabsClosable(True)
        self.tabBar.setExpanding(False)

        seq = QKeySequence(Qt.CTRL + Qt.SHIFT + Qt.Key_Right)
        self._shortcutNextTab = QShortcut(seq, self)
        seq = QKeySequence(Qt.CTRL + Qt.SHIFT + Qt.Key_Left)
        self._shortcutPrevTab = QShortcut(seq, self)

    def _bindSignals(self):
        self.newItemButton.clicked.connect(self.actionNewItem.trigger)
        self.deleteItemButton.clicked.connect(self.actionDeleteItem.trigger)
        self.editItemButton.clicked.connect(self.actionEditItem.trigger)
        self.graphVisibilityButton.clicked.connect(self.actionToggleGraph.trigger)
        self.piechartVisibilityButton.clicked.connect(self.actionTogglePieChart.trigger)
        self.columnsVisibilityButton.clicked.connect(self.columnsVisibilityButtonClicked)
        self.recentDocuments.mustOpenItem.connect(self.open)
        self.tabBar.currentChanged.connect(self.currentTabChanged)
        self.tabBar.tabCloseRequested.connect(self.tabCloseRequested)
        self.tabBar.tabMoved.connect(self.tabMoved)
        self.tabBar.plusClicked.connect(self.model.new_tab)

        # Views
        self.actionShowNetWorth.triggered.connect(self.showNetWorthTriggered)
        self.actionShowProfitLoss.triggered.connect(self.showProfitLossTriggered)
        self.actionShowTransactions.triggered.connect(self.showTransactionsTriggered)
        self.actionShowSchedules.triggered.connect(self.showSchedulesTriggered)
        self.actionShowPreviousView.triggered.connect(self.showPreviousViewTriggered)
        self.actionShowNextView.triggered.connect(self.showNextViewTriggered)
        self.actionShowPreferences.triggered.connect(self.app.showPreferences)
        self.actionToggleGraph.triggered.connect(self.toggleGraphTriggered)
        self.actionTogglePieChart.triggered.connect(self.togglePieChartTriggered)

        # Document Edition
        self.actionNewItem.triggered.connect(self.newItemTriggered)
        self.actionNewAccountGroup.triggered.connect(self.newAccountGroupTriggered)
        self.actionDeleteItem.triggered.connect(self.deleteItemTriggered)
        self.actionEditItem.triggered.connect(self.editItemTriggered)
        self.actionMoveUp.triggered.connect(self.moveUpTriggered)
        self.actionMoveDown.triggered.connect(self.moveDownTriggered)
        self.actionDuplicateTransaction.triggered.connect(self.model.duplicate_item)
        self.actionUndo.triggered.connect(self.model.undo)
        self.actionRedo.triggered.connect(self.model.redo)

        # Open / Save / Import / Export / New
        self.actionNewDocument.triggered.connect(self.new)
        self.actionOpenDocument.triggered.connect(self.openDocument)
        self.actionImport.triggered.connect(self.importDocument)
        self.actionSave.triggered.connect(self.save)
        self.actionSaveAs.triggered.connect(self.saveAs)
        self.actionExport.triggered.connect(self.model.export)

        # Misc
        self.actionNewTab.triggered.connect(self.model.new_tab)
        self.actionCloseTab.triggered.connect(self.closeTabTriggered)
        self.actionShowSelectedAccount.triggered.connect(self.model.show_account)
        self.actionNavigateBack.triggered.connect(self.navigateBackTriggered)
        self.actionJumpToAccount.triggered.connect(self.jumpToAccountTriggered)
        self.actionMakeScheduleFromSelected.triggered.connect(self.makeScheduleFromSelectedTriggered)
        self.actionReconcileSelected.triggered.connect(self.reconcileSelectedTriggered)
        self.actionToggleReconciliationMode.triggered.connect(self.toggleReconciliationModeTriggered)
        self.actionToggleAccountExclusion.triggered.connect(self.toggleAccountExclusionTriggered)
        self.actionPrint.triggered.connect(self._print)
        self.actionShowHelp.triggered.connect(self.app.showHelp)
        self.actionAbout.triggered.connect(self.aboutTriggered)
        self.actionQuit.triggered.connect(self.close)

        # Extra Shortcuts
        self._shortcutNextTab.activated.connect(self.showNextViewTriggered)
        self._shortcutPrevTab.activated.connect(self.showPreviousViewTriggered)

    # --- QWidget overrides
    def closeEvent(self, event):
        if self.confirmDestructiveAction():
            event.accept()
        else:
            event.ignore()

    # --- Private
    def _print(self):
        dialog = QPrintDialog(self)
        if dialog.exec_() != QPrintDialog.Accepted:
            return
        printer = dialog.printer()
        currentView = self.mainView.currentWidget()
        viewPrinter = ViewPrinter(printer, currentView)
        currentView.fitViewsForPrint(viewPrinter)
        viewPrinter.render()

    def _getViewforPane(self, pane_type, pane_view):
        if pane_view in self.model2view:
            view = self.model2view[pane_view]
        else:
            view = PANETYPE2VIEWCLASS[pane_type](model=pane_view, mainwindow=self)
            self.model2view[pane_view] = view
            self.mainView.addWidget(view)
            view.restoreSubviewsSize()
        return view

    def _setTabIndex(self, index):
        if not self.tabBar.count():
            return
        self.tabBar.setCurrentIndex(index)
        self._updateActionsState()
        pane_type = self.model.pane_type(index)
        pane_view = self.model.pane_view(index)
        view = self._getViewforPane(pane_type, pane_view)
        self.mainView.setCurrentWidget(view)
        view.setFocus()

    def _activeView(self):
        paneIndex = self.model.current_pane_index
        return self.model.pane_view(paneIndex)

    def _updateActionsState(self):
        # Updates enable/disable checked/unchecked state of all actions. These state can change
        # under various conditions: main view change, date range type change and when reconciliation
        # mode is toggled

        # Determine what actions are enabled
        view = self._activeView()
        viewType = view.VIEW_TYPE
        isSheet = viewType in {PaneType.NetWorth, PaneType.Profit}
        isTransactionOrEntryTable = viewType in {PaneType.Transaction, PaneType.Account}
        canToggleReconciliation = viewType == PaneType.Account and view.can_toggle_reconciliation_mode

        newItemLabel = {
            PaneType.NetWorth: tr("New Account"),
            PaneType.Profit: tr("New Account"),
            PaneType.Transaction: tr("New Transaction"),
            PaneType.Account: tr("New Transaction"),
            PaneType.Schedule: tr("New Schedule"),
            PaneType.GeneralLedger: tr("New Transaction"),
        }.get(viewType, tr("New Item")) # XXX make "New Item" disabled
        self.actionNewItem.setText(newItemLabel)
        self.actionNewAccountGroup.setEnabled(isSheet)
        self.actionMoveDown.setEnabled(isTransactionOrEntryTable)
        self.actionMoveUp.setEnabled(isTransactionOrEntryTable)
        self.actionDuplicateTransaction.setEnabled(isTransactionOrEntryTable)
        self.actionMakeScheduleFromSelected.setEnabled(isTransactionOrEntryTable)
        self.actionReconcileSelected.setEnabled(viewType == PaneType.Account and view.reconciliation_mode)
        self.actionShowNextView.setEnabled(self.model.current_pane_index < self.model.pane_count-1)
        self.actionShowPreviousView.setEnabled(self.model.current_pane_index > 0)
        self.actionShowSelectedAccount.setEnabled(isSheet or isTransactionOrEntryTable)
        self.actionNavigateBack.setEnabled(viewType == PaneType.Account)
        self.actionToggleReconciliationMode.setEnabled(canToggleReconciliation)
        self.actionToggleAccountExclusion.setEnabled(isSheet)

    def _updateUndoActions(self):
        if self.doc.can_undo():
            self.actionUndo.setEnabled(True)
            self.actionUndo.setText(tr("Undo {0}").format(self.doc.undo_description()))
        else:
            self.actionUndo.setEnabled(False)
            self.actionUndo.setText(tr("Undo"))
        if self.doc.can_redo():
            self.actionRedo.setEnabled(True)
            self.actionRedo.setText(tr("Redo {0}").format(self.doc.redo_description()))
        else:
            self.actionRedo.setEnabled(False)
            self.actionRedo.setText(tr("Redo"))

    # --- Actions
    # Document open/save/close
    def confirmDestructiveAction(self):
        # Asks whether the user wants to continue before continuing with an action that will replace
        # the current document. Will save the document as needed. Returns True if the action can
        # continue.
        if not self.model.document.is_dirty():
            return True
        title = tr("Unsaved Document")
        msg = tr("Do you want to save your changes before continuing?")
        buttons = QMessageBox.Save | QMessageBox.Cancel | QMessageBox.Discard
        result = QMessageBox.question(self.app.mainWindow, title, msg, buttons)
        if result == QMessageBox.Save:
            self.save()
            if self.model.document.is_dirty(): # "save as" was cancelled
                return False
            else:
                return True
        elif result == QMessageBox.Cancel:
            return False
        elif result == QMessageBox.Discard:
            return True

    def new(self):
        if not self.confirmDestructiveAction():
            return
        self.model.close()
        self.documentPath = None
        self.model.clear()
        self.documentPathChanged()

    def open(self, docpath, initial=False):
        # initial flag is true when open() is called at the document's initialization. When that's
        # the case, we need to create a new document when we fail opening this one.
        if not self.confirmDestructiveAction():
            return
        self.model.close()
        try:
            self.model.load_from_xml(docpath)
            self.documentPath = docpath
        except FileFormatError as e:
            QMessageBox.warning(self.app.mainWindow, tr("Cannot load file"), str(e))
            if initial:
                self.new()
        self.documentPathChanged()
        self.recentDocuments.insertItem(docpath)

    def openDocument(self):
        title = tr("Select a document to load")
        filters = tr("moneyGuru Documents (*.moneyguru)")
        docpath, filetype = QFileDialog.getOpenFileName(self.app.mainWindow, title, '', filters)
        if docpath:
            self.open(docpath)

    def save(self):
        if self.documentPath is not None:
            self.model.save_to_xml(self.documentPath)
        else:
            self.saveAs()

    def saveAs(self):
        title = tr("Save As")
        filters = tr("moneyGuru Documents (*.moneyguru)")
        docpath = QFileDialog.getSaveFileName(self.app.mainWindow, title, '', filters)[0]
        if docpath:
            if not docpath.endswith('.moneyguru'):
                docpath += '.moneyguru'
            self.model.save_to_xml(docpath)
            self.documentPath = docpath
            self.documentPathChanged()
            self.recentDocuments.insertItem(docpath)

    # Views
    def showNetWorthTriggered(self):
        self.model.select_pane_of_type(PaneType.NetWorth)

    def showProfitLossTriggered(self):
        self.model.select_pane_of_type(PaneType.Profit)

    def showTransactionsTriggered(self):
        self.model.select_pane_of_type(PaneType.Transaction)

    def showSchedulesTriggered(self):
        self.model.select_pane_of_type(PaneType.Schedule)

    def showPreviousViewTriggered(self):
        self.model.select_previous_view()

    def showNextViewTriggered(self):
        self.model.select_next_view()

    # Document Edition
    def newItemTriggered(self):
        self.model.new_item()

    def newAccountGroupTriggered(self):
        self.model.new_group()

    def deleteItemTriggered(self):
        self.model.delete_item()

    def editItemTriggered(self):
        self.model.edit_item()

    def moveUpTriggered(self):
        self.model.move_up()

    def moveDownTriggered(self):
        self.model.move_down()

    # Misc
    def closeTabTriggered(self):
        self.model.close_pane(self.model.current_pane_index)

    def navigateBackTriggered(self):
        self.model.navigate_back()

    def jumpToAccountTriggered(self):
        self.model.jump_to_account()

    def makeScheduleFromSelectedTriggered(self):
        self.model.make_schedule_from_selected()

    def reconcileSelectedTriggered(self):
        self._activeView().etable.toggle_reconciled()

    def toggleReconciliationModeTriggered(self):
        self._activeView().toggle_reconciliation_mode()
        self._updateActionsState()

    def toggleAccountExclusionTriggered(self):
        viewType = self.model.pane_type(self.model.current_pane_index)
        if viewType in {PaneType.NetWorth, PaneType.Profit}:
            self._activeView().sheet.toggle_excluded()

    def toggleGraphTriggered(self):
        self.model.toggle_area_visibility(PaneArea.BottomGraph)

    def togglePieChartTriggered(self):
        self.model.toggle_area_visibility(PaneArea.RightChart)

    def columnsVisibilityButtonClicked(self):
        items = self.model.column_menu_items()
        if not items:
            return
        menu = QMenu()
        for i, (display, marked) in enumerate(items):
            action = menu.addAction(display)
            action.setCheckable(True)
            action.setChecked(marked)
            action.setData(i)
            action.triggered.connect(self.columnsMenuItemWasClicked)
        self._columnMenuHolder = menu # we need to hold a reference to it while it popups
        button = self.columnsVisibilityButton
        menu.popup(button.parentWidget().mapToGlobal(button.geometry().topLeft()))

    def columnsMenuItemWasClicked(self):
        action = self.sender()
        if action is not None:
            index = action.data()
            self.model.toggle_column_menu_item(index)

    def aboutTriggered(self):
        self.app.showAboutBox()

    def importDocument(self):
        title = tr("Select a document to import")
        filters = tr("Supported files (*.moneyguru *.ofx *.qfx *.qif *.csv *.txt);;All files (*)")
        docpath, filetype = QFileDialog.getOpenFileName(self.app.mainWindow, title, '', filters)
        # There's a strange glitch under GNOME where, right after the dialog is gone, the main
        # window isn't the active window, but it will become active if we give it enough time. If we
        # start showing the import window before that happens, we'll end up with an import window
        # under the main window, which is bad. Therefore, we process events until this happens. We
        # do this in a big forloop instead of a while to avoid a possible infinite loop.
        for i in range(10000):
            if self.app.mainWindow.isActiveWindow():
                break
            QApplication.processEvents()
        if docpath:
            try:
                self.model.parse_file_for_import(docpath)
            except FileFormatError as e:
                QMessageBox.warning(self.app.mainWindow, tr("Cannot import file"), str(e))

    # --- Other Signals
    def currentTabChanged(self, index):
        self.model.current_pane_index = index
        self._setTabIndex(index)

    def documentPathChanged(self):
        if self.documentPath:
            title = "moneyGuru ({})".format(self.documentPath)
        else:
            title = "moneyGuru"
        self.setWindowTitle(title)

    def tabCloseRequested(self, index):
        self.model.close_pane(index)

    def tabMoved(self, fromIndex, toIndex):
        # We don't refresh panes because tabMoved is apparently now called *during* drag operations.
        # If we start a full pane refresh during a drag operation, we segfault.
        self.model.move_pane(fromIndex, toIndex, refresh_panes=False)

    # --- document model --> view
    def query_for_schedule_scope(self):
        if QApplication.keyboardModifiers() & Qt.ShiftModifier:
            return ScheduleScope.Global
        if not self.app.model.show_schedule_scope_dialog:
            return ScheduleScope.Local
        dialog = ScheduleScopeDialog(self)
        return dialog.queryForScope()

    # --- model --> view
    def change_current_pane(self):
        self._setTabIndex(self.model.current_pane_index)

    def get_panel_view(self, model):
        if isinstance(model, CustomDateRangePanelModel):
            return CustomDateRangePanel(model, self)
        elif isinstance(model, CSVOptionsModel):
            return CSVOptionsWindow(model, self)
        elif isinstance(model, ImportWindowModel):
            return ImportWindow(model, self, self.app.prefs)
        else:
            return ExportPanel(model, self)

    def refresh_panes(self):
        while self.tabBar.count() < self.model.pane_count:
            self.tabBar.addTab('')
        for i in range(self.model.pane_count):
            pane_label = self.model.pane_label(i)
            pane_label = escapeamp(pane_label)
            self.tabBar.setTabText(i, pane_label)
            pane_type = self.model.pane_type(i)
            pane_view = self.model.pane_view(i)
            # Ensure that the view's "view" has been created and bound
            self._getViewforPane(pane_type, pane_view)
            iconname = PANETYPE2ICON.get(pane_type)
            icon = QIcon(QPixmap(':/{0}'.format(iconname))) if iconname else QIcon()
            self.tabBar.setTabIcon(i, icon)
        # It's important that we proceed with tab removal *after* we've completed tab initialization.
        # We're walking on eggshells here. refresh_panes() can be called in multiple situations, one
        # of them is during the opening of a document. When that happens when another document was
        # previously opened, all views' model are uninitalized and don't have their "view" attribute
        # set yet. If we proceed with the setCurrentIndex() call below before _getViewforPane()
        # could be called above, we get a crash.
        if self.tabBar.currentIndex() >= self.model.pane_count:
            # Normally, we don't touch the tabBar index here and wait for change_current_pane,
            # but when we remove tabs, it's possible that currentTabChanged end up being called and
            # then the tab selection is bugged. I tried disconnecting/reconnecting the signal, but
            # this is buggy. So when a selected tab is about to be removed and is out of bounds,
            # we change the selection to the last index in the model. We don't use
            # self.model.current_pane_index because in some cases, it's -1 and prevents this crash
            # preventer from preventing its crash.
            self.tabBar.setCurrentIndex(self.model.pane_count - 1)
        while self.tabBar.count() > self.model.pane_count:
            self.tabBar.removeTab(self.tabBar.count()-1)
        self.tabBar.setTabsClosable(self.model.pane_count > 1)

    def refresh_status_line(self):
        self.statusLabel.setText(self.model.status_line)

    def refresh_undo_actions(self):
        self._updateUndoActions()

    def restore_window_frame(self, frame):
        self.setGeometry(*frame)

    def save_window_frame(self):
        r = self.geometry()
        return (r.x(), r.y(), r.width(), r.height())

    def show_message(self, msg):
        title = tr("Warning")
        QMessageBox.warning(self, title, msg)

    def update_area_visibility(self):
        hidden = self.model.hidden_areas
        graphimg = ':/graph_visibility_{}_16'.format('off' if PaneArea.BottomGraph in hidden else 'on')
        pieimg = ':/piechart_visibility_{}_16'.format('off' if PaneArea.RightChart in hidden else 'on')
        self.graphVisibilityButton.setIcon(QIcon(QPixmap(graphimg)))
        self.piechartVisibilityButton.setIcon(QIcon(QPixmap(pieimg)))

    def view_closed(self, index):
        self.tabBar.removeTab(index)
        self.tabBar.setTabsClosable(self.model.pane_count > 1)
Beispiel #7
0
class Document(QObject):
    def __init__(self, app):
        QObject.__init__(self)
        self.app = app
        self.documentPath = None
        self.model = DocumentModel(app=app.model)
        self.model.view = self
    
    #--- Public
    def close(self):
        if self.documentPath:
            self.model.close()
    
    def confirmDestructiveAction(self):
        # Asks whether the user wants to continue before continuing with an action that will replace
        # the current document. Will save the document as needed. Returns True if the action can
        # continue.
        if not self.model.is_dirty():
            return True
        title = tr("Unsaved Document")
        msg = tr("Do you want to save your changes before continuing?")
        buttons = QMessageBox.Save | QMessageBox.Cancel | QMessageBox.Discard
        result = QMessageBox.question(self.app.mainWindow, title, msg, buttons)
        if result == QMessageBox.Save:
            self.save()
            if self.model.is_dirty(): # "save as" was cancelled
                return False
            else:
                return True
        elif result == QMessageBox.Cancel:
            return False
        elif result == QMessageBox.Discard:
            return True
    
    def importDocument(self):
        title = tr("Select a document to import")
        filters = tr("Supported files (*.moneyguru *.ofx *.qfx *.qif *.csv *.txt)")
        docpath = str(QFileDialog.getOpenFileName(self.app.mainWindow, title, '', filters))
        # There's a strange glitch under GNOME where, right after the dialog is gone, the main
        # window isn't the active window, but it will become active if we give it enough time. If we
        # start showing the import window before that happens, we'll end up with an import window
        # under the main window, which is bad. Therefore, we process events until this happens. We
        # do this in a big forloop instead of a while to avoid a possible infinite loop.
        for i in range(10000):
            if self.app.mainWindow.isActiveWindow():
                break
            QApplication.processEvents()
        if docpath:
            try:
                self.model.parse_file_for_import(docpath)
            except FileFormatError as e:
                QMessageBox.warning(self.app.mainWindow, tr("Cannot import file"), str(e))
    
    def new(self):
        if not self.confirmDestructiveAction():
            return
        self.close()
        self.documentPath = None
        self.model.clear()
        self.documentPathChanged.emit()
    
    def open(self, docpath):
        if not self.confirmDestructiveAction():
            return
        self.close()
        try:
            self.model.load_from_xml(docpath)
            self.documentPath = docpath
        except FileFormatError as e:
            QMessageBox.warning(self.app.mainWindow, tr("Cannot load file"), str(e))
        self.documentPathChanged.emit()
        self.documentOpened.emit(docpath)
    
    def openDocument(self):
        title = tr("Select a document to load")
        filters = tr("moneyGuru Documents (*.moneyguru)")
        docpath = str(QFileDialog.getOpenFileName(self.app.mainWindow, title, '', filters))
        if docpath:
            self.open(docpath)
    
    def openExampleDocument(self):
        if not self.confirmDestructiveAction():
            return
        self.close()
        dirpath = tempfile.mkdtemp()
        destpath = op.join(dirpath, 'example.moneyguru')
        QFile.copy(':/example.moneyguru', destpath)
        self.model.load_from_xml(destpath)
        self.model.adjust_example_file()
        self.documentPath = None # As if it was a new doc. Save As is required.
        self.documentPathChanged.emit()
    
    def save(self):
        if self.documentPath is not None:
            self.model.save_to_xml(self.documentPath)
        else:
            self.saveAs()
    
    def saveAs(self):
        title = tr("Save As")
        filters = tr("moneyGuru Documents (*.moneyguru)")
        docpath = str(QFileDialog.getSaveFileName(self.app.mainWindow, title, '', filters))
        if docpath:
            if not docpath.endswith('.moneyguru'):
                docpath += '.moneyguru'
            self.model.save_to_xml(docpath)
            self.documentPath = docpath
            self.documentPathChanged.emit()
            self.documentSavedAs.emit(docpath)
    
    # model --> view
    def query_for_schedule_scope(self):
        if QApplication.keyboardModifiers() & Qt.ShiftModifier:
            return ScheduleScope.Global
        if not self.app.model.show_schedule_scope_dialog:
            return ScheduleScope.Local
        dialog = ScheduleScopeDialog(self.app.mainWindow)
        return dialog.queryForScope()
    
    #--- Signals
    documentOpened = pyqtSignal(str)
    documentSavedAs = pyqtSignal(str)
    documentPathChanged = pyqtSignal()
Beispiel #8
0
 def __init__(self, app):
     QObject.__init__(self)
     self.app = app
     self.documentPath = None
     self.model = DocumentModel(app=app.model)
     self.model.view = self
Beispiel #9
0
class Document(QObject):
    def __init__(self, app):
        QObject.__init__(self)
        self.app = app
        self.documentPath = None
        self.model = DocumentModel(app=app.model)
        self.model.view = self

    # --- Public
    def close(self):
        if self.documentPath:
            self.model.close()

    def confirmDestructiveAction(self):
        # Asks whether the user wants to continue before continuing with an action that will replace
        # the current document. Will save the document as needed. Returns True if the action can
        # continue.
        if not self.model.is_dirty():
            return True
        title = tr("Unsaved Document")
        msg = tr("Do you want to save your changes before continuing?")
        buttons = QMessageBox.Save | QMessageBox.Cancel | QMessageBox.Discard
        result = QMessageBox.question(self.app.mainWindow, title, msg, buttons)
        if result == QMessageBox.Save:
            self.save()
            if self.model.is_dirty():  # "save as" was cancelled
                return False
            else:
                return True
        elif result == QMessageBox.Cancel:
            return False
        elif result == QMessageBox.Discard:
            return True

    def new(self):
        if not self.confirmDestructiveAction():
            return
        self.close()
        self.documentPath = None
        self.model.clear()
        self.documentPathChanged.emit()

    def open(self, docpath, initial=False):
        # initial flag is true when open() is called at the document's initialization. When that's
        # the case, we need to create a new document when we fail opening this one.
        if not self.confirmDestructiveAction():
            return
        self.close()
        try:
            self.model.load_from_xml(docpath)
            self.documentPath = docpath
        except FileFormatError as e:
            QMessageBox.warning(self.app.mainWindow, tr("Cannot load file"),
                                str(e))
            if initial:
                self.new()
        self.documentPathChanged.emit()
        self.documentOpened.emit(docpath)

    def openDocument(self):
        title = tr("Select a document to load")
        filters = tr("moneyGuru Documents (*.moneyguru)")
        docpath, filetype = QFileDialog.getOpenFileName(
            self.app.mainWindow, title, '', filters)
        if docpath:
            self.open(docpath)

    def openExampleDocument(self):
        if not self.confirmDestructiveAction():
            return
        self.close()
        dirpath = tempfile.mkdtemp()
        destpath = op.join(dirpath, 'example.moneyguru')
        QFile.copy(':/example.moneyguru', destpath)
        self.model.load_from_xml(destpath)
        self.model.adjust_example_file()
        self.documentPath = None  # As if it was a new doc. Save As is required.
        self.documentPathChanged.emit()

    def save(self):
        if self.documentPath is not None:
            self.model.save_to_xml(self.documentPath)
        else:
            self.saveAs()

    def saveAs(self):
        title = tr("Save As")
        filters = tr("moneyGuru Documents (*.moneyguru)")
        docpath = QFileDialog.getSaveFileName(self.app.mainWindow, title, '',
                                              filters)[0]
        if docpath:
            if not docpath.endswith('.moneyguru'):
                docpath += '.moneyguru'
            self.model.save_to_xml(docpath)
            self.documentPath = docpath
            self.documentPathChanged.emit()
            self.documentSavedAs.emit(docpath)

    # model --> view
    def query_for_schedule_scope(self):
        if QApplication.keyboardModifiers() & Qt.ShiftModifier:
            return ScheduleScope.Global
        if not self.app.model.show_schedule_scope_dialog:
            return ScheduleScope.Local
        dialog = ScheduleScopeDialog(self.app.mainWindow)
        return dialog.queryForScope()

    # --- Signals
    documentOpened = pyqtSignal(str)
    documentSavedAs = pyqtSignal(str)
    documentPathChanged = pyqtSignal()
Beispiel #10
0
class MainWindow(QMainWindow):
    def __init__(self, app):
        QMainWindow.__init__(self, None)
        self.documentPath = None
        self.doc = DocumentModel(app=app.model)
        self.app = app

        self._setupUi()

        # Create base elements
        self.model = MainWindowModel(document=self.doc)
        self.model2view = {}
        self.alookup = Lookup(self, model=self.model.account_lookup)
        self.clookup = Lookup(self, model=self.model.completion_lookup)
        self.drsel = DateRangeSelector(mainwindow=self,
                                       view=self.dateRangeSelectorView)
        self.sfield = SearchField(model=self.model.search_field,
                                  view=self.searchLineEdit)
        self.recentDocuments = Recent(self.app, 'recentDocuments')
        self.recentDocuments.addMenu(self.menuOpenRecent)

        self.doc.view = self
        self.model.view = self

        self._updateUndoActions()
        self._bindSignals()

    def _setupUi(self):  # has to take place *before* base elements creation
        self.setWindowTitle("moneyGuru")
        self.resize(700, 580)
        self.centralwidget = QWidget(self)
        self.verticalLayout = QVBoxLayout(self.centralwidget)
        self.verticalLayout.setSpacing(0)
        self.verticalLayout.setContentsMargins(0, 0, 0, 0)
        self.topBar = QWidget(self.centralwidget)
        self.horizontalLayout_2 = QHBoxLayout(self.topBar)
        self.horizontalLayout_2.setContentsMargins(2, 0, 2, 0)
        spacerItem = QSpacerItem(40, 20, QSizePolicy.Expanding,
                                 QSizePolicy.Minimum)
        self.horizontalLayout_2.addItem(spacerItem)
        self.dateRangeSelectorView = DateRangeSelectorView(self.topBar)
        self.dateRangeSelectorView.setMinimumSize(QSize(220, 0))
        self.horizontalLayout_2.addWidget(self.dateRangeSelectorView)
        spacerItem1 = QSpacerItem(40, 20, QSizePolicy.Expanding,
                                  QSizePolicy.Minimum)
        self.horizontalLayout_2.addItem(spacerItem1)
        self.searchLineEdit = SearchEdit(self.topBar)
        self.searchLineEdit.setMaximumSize(QSize(240, 16777215))
        self.horizontalLayout_2.addWidget(self.searchLineEdit)
        self.verticalLayout.addWidget(self.topBar)
        self.tabBar = TabBarPlus(self.centralwidget)
        self.tabBar.setMinimumSize(QSize(0, 20))
        self.verticalLayout.addWidget(self.tabBar)
        self.mainView = QStackedWidget(self.centralwidget)
        self.verticalLayout.addWidget(self.mainView)

        # Bottom buttons & status label
        self.bottomBar = QWidget(self.centralwidget)
        self.horizontalLayout = QHBoxLayout(self.bottomBar)
        self.horizontalLayout.setContentsMargins(2, 2, 2, 2)
        self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
        self.newItemButton = QPushButton(self.bottomBar)
        buttonSizePolicy = QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
        buttonSizePolicy.setHorizontalStretch(0)
        buttonSizePolicy.setVerticalStretch(0)
        buttonSizePolicy.setHeightForWidth(
            self.newItemButton.sizePolicy().hasHeightForWidth())
        self.newItemButton.setSizePolicy(buttonSizePolicy)
        self.newItemButton.setIcon(QIcon(QPixmap(':/plus_8')))
        self.horizontalLayout.addWidget(self.newItemButton)
        self.deleteItemButton = QPushButton(self.bottomBar)
        self.deleteItemButton.setSizePolicy(buttonSizePolicy)
        self.deleteItemButton.setIcon(QIcon(QPixmap(':/minus_8')))
        self.horizontalLayout.addWidget(self.deleteItemButton)
        self.editItemButton = QPushButton(self.bottomBar)
        self.editItemButton.setSizePolicy(buttonSizePolicy)
        self.editItemButton.setIcon(QIcon(QPixmap(':/info_gray_12')))
        self.horizontalLayout.addWidget(self.editItemButton)
        self.horizontalLayout.addItem(horizontalSpacer(size=20))
        self.graphVisibilityButton = QPushButton()
        self.graphVisibilityButton.setSizePolicy(buttonSizePolicy)
        self.graphVisibilityButton.setIcon(
            QIcon(QPixmap(':/graph_visibility_on_16')))
        self.horizontalLayout.addWidget(self.graphVisibilityButton)
        self.piechartVisibilityButton = QPushButton()
        self.piechartVisibilityButton.setSizePolicy(buttonSizePolicy)
        self.piechartVisibilityButton.setIcon(
            QIcon(QPixmap(':/piechart_visibility_on_16')))
        self.horizontalLayout.addWidget(self.piechartVisibilityButton)
        self.columnsVisibilityButton = QPushButton()
        self.columnsVisibilityButton.setSizePolicy(buttonSizePolicy)
        self.columnsVisibilityButton.setIcon(QIcon(QPixmap(':/columns_16')))
        self.horizontalLayout.addWidget(self.columnsVisibilityButton)

        self.statusLabel = QLabel(tr("Status"))
        self.statusLabel.setAlignment(Qt.AlignCenter)
        self.horizontalLayout.addWidget(self.statusLabel)
        self.verticalLayout.addWidget(self.bottomBar)

        self.setCentralWidget(self.centralwidget)
        self.menubar = QMenuBar(self)
        self.menubar.setGeometry(QRect(0, 0, 700, 20))
        self.menuFile = QMenu(tr("File"))
        self.menuOpenRecent = QMenu(tr("Open Recent"))
        self.menuView = QMenu(tr("View"))
        self.menuDateRange = QMenu(tr("Date Range"))
        self.menuEdit = QMenu(tr("Edit"))
        self.menuHelp = QMenu(tr("Help"))
        self.setMenuBar(self.menubar)
        self.actionOpenDocument = QAction(tr("Open..."), self)
        self.actionOpenDocument.setShortcut("Ctrl+O")
        self.actionShowNetWorth = QAction(tr("Net Worth"), self)
        self.actionShowNetWorth.setShortcut("Ctrl+1")
        self.actionShowNetWorth.setIcon(QIcon(QPixmap(':/balance_sheet_48')))
        self.actionShowProfitLoss = QAction(escapeamp(tr("Profit & Loss")),
                                            self)
        self.actionShowProfitLoss.setShortcut("Ctrl+2")
        self.actionShowProfitLoss.setIcon(
            QIcon(QPixmap(':/income_statement_48')))
        self.actionShowTransactions = QAction(tr("Transactions"), self)
        self.actionShowTransactions.setShortcut("Ctrl+3")
        self.actionShowTransactions.setIcon(
            QIcon(QPixmap(':/transaction_table_48')))
        self.actionShowSelectedAccount = QAction(tr("Show Account"), self)
        self.actionShowSelectedAccount.setShortcut("Ctrl+]")
        self.actionNewItem = QAction(tr("New Item"), self)
        self.actionNewItem.setShortcut("Ctrl+N")
        self.actionDeleteItem = QAction(tr("Remove Selected"), self)
        self.actionEditItem = QAction(tr("Show Info"), self)
        self.actionEditItem.setShortcut("Ctrl+I")
        self.actionToggleGraph = QAction(tr("Toggle Graph"), self)
        self.actionToggleGraph.setShortcut("Ctrl+Alt+G")
        self.actionTogglePieChart = QAction(tr("Toggle Pie Chart"), self)
        self.actionTogglePieChart.setShortcut("Ctrl+Alt+P")
        self.actionMoveUp = QAction(tr("Move Up"), self)
        self.actionMoveUp.setShortcut("Ctrl++")
        self.actionMoveDown = QAction(tr("Move Down"), self)
        self.actionMoveDown.setShortcut("Ctrl+-")
        self.actionNavigateBack = QAction(tr("Go Back"), self)
        self.actionNavigateBack.setShortcut("Ctrl+[")
        self.actionNewAccountGroup = QAction(tr("New Account Group"), self)
        self.actionNewAccountGroup.setShortcut("Ctrl+Shift+N")
        self.actionShowNextView = QAction(tr("Next View"), self)
        self.actionShowNextView.setShortcut("Ctrl+Shift+]")
        self.actionShowPreviousView = QAction(tr("Previous View"), self)
        self.actionShowPreviousView.setShortcut("Ctrl+Shift+[")
        self.actionNewDocument = QAction(tr("New Document"), self)
        self.actionImport = QAction(tr("Import..."), self)
        self.actionImport.setShortcut("Ctrl+Alt+I")
        self.actionExport = QAction(tr("Export..."), self)
        self.actionExport.setShortcut("Ctrl+Alt+E")
        self.actionSave = QAction(tr("Save"), self)
        self.actionSave.setShortcut("Ctrl+S")
        self.actionSaveAs = QAction(tr("Save As..."), self)
        self.actionSaveAs.setShortcut("Ctrl+Shift+S")
        self.actionAbout = QAction(tr("About moneyGuru"), self)
        self.actionToggleReconciliationMode = QAction(
            tr("Toggle Reconciliation Mode"), self)
        self.actionToggleReconciliationMode.setShortcut("Ctrl+Shift+R")
        self.actionToggleAccountExclusion = QAction(
            tr("Toggle Exclusion Status of Account"), self)
        self.actionToggleAccountExclusion.setShortcut("Ctrl+Shift+X")
        self.actionShowSchedules = QAction(tr("Schedules"), self)
        self.actionShowSchedules.setShortcut("Ctrl+4")
        self.actionShowSchedules.setIcon(QIcon(QPixmap(':/schedules_48')))
        self.actionReconcileSelected = QAction(tr("Reconcile Selection"), self)
        self.actionReconcileSelected.setShortcut("Ctrl+R")
        self.actionMakeScheduleFromSelected = QAction(
            tr("Make Schedule from Selected"), self)
        self.actionMakeScheduleFromSelected.setShortcut("Ctrl+M")
        self.actionShowPreferences = QAction(tr("Preferences..."), self)
        self.actionPrint = QAction(tr("Print..."), self)
        self.actionPrint.setShortcut("Ctrl+P")
        self.actionQuit = QAction(tr("Quit moneyGuru"), self)
        self.actionQuit.setShortcut("Ctrl+Q")
        self.actionUndo = QAction(tr("Undo"), self)
        self.actionUndo.setShortcut("Ctrl+Z")
        self.actionRedo = QAction(tr("Redo"), self)
        self.actionRedo.setShortcut("Ctrl+Y")
        self.actionShowHelp = QAction(tr("moneyGuru Help"), self)
        self.actionShowHelp.setShortcut("F1")
        self.actionDuplicateTransaction = QAction(tr("Duplicate Transaction"),
                                                  self)
        self.actionDuplicateTransaction.setShortcut("Ctrl+D")
        self.actionJumpToAccount = QAction(tr("Jump to Account..."), self)
        self.actionJumpToAccount.setShortcut("Ctrl+Shift+A")
        self.actionNewTab = QAction(tr("New Tab"), self)
        self.actionNewTab.setShortcut("Ctrl+T")
        self.actionCloseTab = QAction(tr("Close Tab"), self)
        self.actionCloseTab.setShortcut("Ctrl+W")

        self.menuFile.addAction(self.actionNewDocument)
        self.menuFile.addAction(self.actionNewTab)
        self.menuFile.addAction(self.actionOpenDocument)
        self.menuFile.addAction(self.menuOpenRecent.menuAction())
        self.menuFile.addAction(self.actionImport)
        self.menuFile.addSeparator()
        self.menuFile.addAction(self.actionCloseTab)
        self.menuFile.addAction(self.actionSave)
        self.menuFile.addAction(self.actionSaveAs)
        self.menuFile.addAction(self.actionExport)
        self.menuFile.addAction(self.actionPrint)
        self.menuFile.addAction(self.actionQuit)
        self.menuView.addAction(self.actionShowNetWorth)
        self.menuView.addAction(self.actionShowProfitLoss)
        self.menuView.addAction(self.actionShowTransactions)
        self.menuView.addAction(self.actionShowSchedules)
        self.menuView.addAction(self.actionShowPreviousView)
        self.menuView.addAction(self.actionShowNextView)
        self.menuView.addAction(self.menuDateRange.menuAction())
        self.menuView.addAction(self.actionShowPreferences)
        self.menuView.addAction(self.actionToggleGraph)
        self.menuView.addAction(self.actionTogglePieChart)
        self.menuEdit.addAction(self.actionNewItem)
        self.menuEdit.addAction(self.actionNewAccountGroup)
        self.menuEdit.addAction(self.actionDeleteItem)
        self.menuEdit.addAction(self.actionEditItem)
        self.menuEdit.addSeparator()
        self.menuEdit.addAction(self.actionMoveUp)
        self.menuEdit.addAction(self.actionMoveDown)
        self.menuEdit.addAction(self.actionDuplicateTransaction)
        self.menuEdit.addAction(self.actionMakeScheduleFromSelected)
        self.menuEdit.addAction(self.actionReconcileSelected)
        self.menuEdit.addAction(self.actionToggleReconciliationMode)
        self.menuEdit.addAction(self.actionToggleAccountExclusion)
        self.menuEdit.addSeparator()
        self.menuEdit.addAction(self.actionShowSelectedAccount)
        self.menuEdit.addAction(self.actionNavigateBack)
        self.menuEdit.addAction(self.actionJumpToAccount)
        self.menuEdit.addSeparator()
        self.menuEdit.addAction(self.actionUndo)
        self.menuEdit.addAction(self.actionRedo)
        self.menuHelp.addAction(self.actionShowHelp)
        self.menuHelp.addAction(self.actionAbout)
        mainmenus = [
            self.menuFile, self.menuEdit, self.menuView, self.menuHelp
        ]
        for menu in mainmenus:
            self.menubar.addAction(menu.menuAction())
            setAccelKeys(menu)
        setAccelKeys(self.menubar)
        self.tabBar.setMovable(True)
        self.tabBar.setTabsClosable(True)
        self.tabBar.setExpanding(False)

        seq = QKeySequence(Qt.CTRL + Qt.SHIFT + Qt.Key_Right)
        self._shortcutNextTab = QShortcut(seq, self)
        seq = QKeySequence(Qt.CTRL + Qt.SHIFT + Qt.Key_Left)
        self._shortcutPrevTab = QShortcut(seq, self)

    def _bindSignals(self):
        self.newItemButton.clicked.connect(self.actionNewItem.trigger)
        self.deleteItemButton.clicked.connect(self.actionDeleteItem.trigger)
        self.editItemButton.clicked.connect(self.actionEditItem.trigger)
        self.graphVisibilityButton.clicked.connect(
            self.actionToggleGraph.trigger)
        self.piechartVisibilityButton.clicked.connect(
            self.actionTogglePieChart.trigger)
        self.columnsVisibilityButton.clicked.connect(
            self.columnsVisibilityButtonClicked)
        self.recentDocuments.mustOpenItem.connect(self.open)
        self.tabBar.currentChanged.connect(self.currentTabChanged)
        self.tabBar.tabCloseRequested.connect(self.tabCloseRequested)
        self.tabBar.tabMoved.connect(self.tabMoved)
        self.tabBar.plusClicked.connect(self.model.new_tab)

        # Views
        self.actionShowNetWorth.triggered.connect(self.showNetWorthTriggered)
        self.actionShowProfitLoss.triggered.connect(
            self.showProfitLossTriggered)
        self.actionShowTransactions.triggered.connect(
            self.showTransactionsTriggered)
        self.actionShowSchedules.triggered.connect(self.showSchedulesTriggered)
        self.actionShowPreviousView.triggered.connect(
            self.showPreviousViewTriggered)
        self.actionShowNextView.triggered.connect(self.showNextViewTriggered)
        self.actionShowPreferences.triggered.connect(self.app.showPreferences)
        self.actionToggleGraph.triggered.connect(self.toggleGraphTriggered)
        self.actionTogglePieChart.triggered.connect(
            self.togglePieChartTriggered)

        # Document Edition
        self.actionNewItem.triggered.connect(self.newItemTriggered)
        self.actionNewAccountGroup.triggered.connect(
            self.newAccountGroupTriggered)
        self.actionDeleteItem.triggered.connect(self.deleteItemTriggered)
        self.actionEditItem.triggered.connect(self.editItemTriggered)
        self.actionMoveUp.triggered.connect(self.moveUpTriggered)
        self.actionMoveDown.triggered.connect(self.moveDownTriggered)
        self.actionDuplicateTransaction.triggered.connect(
            self.model.duplicate_item)
        self.actionUndo.triggered.connect(self.model.undo)
        self.actionRedo.triggered.connect(self.model.redo)

        # Open / Save / Import / Export / New
        self.actionNewDocument.triggered.connect(self.new)
        self.actionOpenDocument.triggered.connect(self.openDocument)
        self.actionImport.triggered.connect(self.importDocument)
        self.actionSave.triggered.connect(self.save)
        self.actionSaveAs.triggered.connect(self.saveAs)
        self.actionExport.triggered.connect(self.model.export)

        # Misc
        self.actionNewTab.triggered.connect(self.model.new_tab)
        self.actionCloseTab.triggered.connect(self.closeTabTriggered)
        self.actionShowSelectedAccount.triggered.connect(
            self.model.show_account)
        self.actionNavigateBack.triggered.connect(self.navigateBackTriggered)
        self.actionJumpToAccount.triggered.connect(self.jumpToAccountTriggered)
        self.actionMakeScheduleFromSelected.triggered.connect(
            self.makeScheduleFromSelectedTriggered)
        self.actionReconcileSelected.triggered.connect(
            self.reconcileSelectedTriggered)
        self.actionToggleReconciliationMode.triggered.connect(
            self.toggleReconciliationModeTriggered)
        self.actionToggleAccountExclusion.triggered.connect(
            self.toggleAccountExclusionTriggered)
        self.actionPrint.triggered.connect(self._print)
        self.actionShowHelp.triggered.connect(self.app.showHelp)
        self.actionAbout.triggered.connect(self.aboutTriggered)
        self.actionQuit.triggered.connect(self.close)

        # Extra Shortcuts
        self._shortcutNextTab.activated.connect(self.showNextViewTriggered)
        self._shortcutPrevTab.activated.connect(self.showPreviousViewTriggered)

    # --- QWidget overrides
    def closeEvent(self, event):
        if self.confirmDestructiveAction():
            event.accept()
        else:
            event.ignore()

    # --- Private
    def _print(self):
        dialog = QPrintDialog(self)
        if dialog.exec_() != QPrintDialog.Accepted:
            return
        printer = dialog.printer()
        currentView = self.mainView.currentWidget()
        viewPrinter = ViewPrinter(printer, currentView)
        currentView.fitViewsForPrint(viewPrinter)
        viewPrinter.render()

    def _getViewforPane(self, pane_type, pane_view):
        if pane_view in self.model2view:
            view = self.model2view[pane_view]
        else:
            view = PANETYPE2VIEWCLASS[pane_type](model=pane_view,
                                                 mainwindow=self)
            self.model2view[pane_view] = view
            self.mainView.addWidget(view)
            view.restoreSubviewsSize()
        return view

    def _setTabIndex(self, index):
        if not self.tabBar.count():
            return
        self.tabBar.setCurrentIndex(index)
        self._updateActionsState()
        pane_type = self.model.pane_type(index)
        pane_view = self.model.pane_view(index)
        view = self._getViewforPane(pane_type, pane_view)
        self.mainView.setCurrentWidget(view)
        view.setFocus()

    def _activeView(self):
        paneIndex = self.model.current_pane_index
        return self.model.pane_view(paneIndex)

    def _updateActionsState(self):
        # Updates enable/disable checked/unchecked state of all actions. These state can change
        # under various conditions: main view change, date range type change and when reconciliation
        # mode is toggled

        # Determine what actions are enabled
        view = self._activeView()
        viewType = view.VIEW_TYPE
        isSheet = viewType in {PaneType.NetWorth, PaneType.Profit}
        isTransactionOrEntryTable = viewType in {
            PaneType.Transaction, PaneType.Account
        }
        canToggleReconciliation = viewType == PaneType.Account and view.can_toggle_reconciliation_mode

        newItemLabel = {
            PaneType.NetWorth: tr("New Account"),
            PaneType.Profit: tr("New Account"),
            PaneType.Transaction: tr("New Transaction"),
            PaneType.Account: tr("New Transaction"),
            PaneType.Schedule: tr("New Schedule"),
            PaneType.GeneralLedger: tr("New Transaction"),
        }.get(viewType, tr("New Item"))  # XXX make "New Item" disabled
        self.actionNewItem.setText(newItemLabel)
        self.actionNewAccountGroup.setEnabled(isSheet)
        self.actionMoveDown.setEnabled(isTransactionOrEntryTable)
        self.actionMoveUp.setEnabled(isTransactionOrEntryTable)
        self.actionDuplicateTransaction.setEnabled(isTransactionOrEntryTable)
        self.actionMakeScheduleFromSelected.setEnabled(
            isTransactionOrEntryTable)
        self.actionReconcileSelected.setEnabled(viewType == PaneType.Account
                                                and view.reconciliation_mode)
        self.actionShowNextView.setEnabled(
            self.model.current_pane_index < self.model.pane_count - 1)
        self.actionShowPreviousView.setEnabled(
            self.model.current_pane_index > 0)
        self.actionShowSelectedAccount.setEnabled(isSheet
                                                  or isTransactionOrEntryTable)
        self.actionNavigateBack.setEnabled(viewType == PaneType.Account)
        self.actionToggleReconciliationMode.setEnabled(canToggleReconciliation)
        self.actionToggleAccountExclusion.setEnabled(isSheet)

    def _updateUndoActions(self):
        if self.doc.can_undo():
            self.actionUndo.setEnabled(True)
            self.actionUndo.setText(
                tr("Undo {0}").format(self.doc.undo_description()))
        else:
            self.actionUndo.setEnabled(False)
            self.actionUndo.setText(tr("Undo"))
        if self.doc.can_redo():
            self.actionRedo.setEnabled(True)
            self.actionRedo.setText(
                tr("Redo {0}").format(self.doc.redo_description()))
        else:
            self.actionRedo.setEnabled(False)
            self.actionRedo.setText(tr("Redo"))

    # --- Actions
    # Document open/save/close
    def confirmDestructiveAction(self):
        # Asks whether the user wants to continue before continuing with an action that will replace
        # the current document. Will save the document as needed. Returns True if the action can
        # continue.
        if not self.model.document.is_dirty():
            return True
        title = tr("Unsaved Document")
        msg = tr("Do you want to save your changes before continuing?")
        buttons = QMessageBox.Save | QMessageBox.Cancel | QMessageBox.Discard
        result = QMessageBox.question(self.app.mainWindow, title, msg, buttons)
        if result == QMessageBox.Save:
            self.save()
            if self.model.document.is_dirty():  # "save as" was cancelled
                return False
            else:
                return True
        elif result == QMessageBox.Cancel:
            return False
        elif result == QMessageBox.Discard:
            return True

    def new(self):
        if not self.confirmDestructiveAction():
            return
        self.model.close()
        self.documentPath = None
        self.model.clear()
        self.documentPathChanged()

    def open(self, docpath, initial=False):
        # initial flag is true when open() is called at the document's initialization. When that's
        # the case, we need to create a new document when we fail opening this one.
        if not self.confirmDestructiveAction():
            return
        self.model.close()
        try:
            self.model.load_from_xml(docpath)
            self.documentPath = docpath
        except FileFormatError as e:
            QMessageBox.warning(self.app.mainWindow, tr("Cannot load file"),
                                str(e))
            if initial:
                self.new()
        self.documentPathChanged()
        self.recentDocuments.insertItem(docpath)

    def openDocument(self):
        title = tr("Select a document to load")
        filters = tr("moneyGuru Documents (*.moneyguru)")
        docpath, filetype = QFileDialog.getOpenFileName(
            self.app.mainWindow, title, '', filters)
        if docpath:
            self.open(docpath)

    def save(self):
        if self.documentPath is not None:
            self.model.save_to_xml(self.documentPath)
        else:
            self.saveAs()

    def saveAs(self):
        title = tr("Save As")
        filters = tr("moneyGuru Documents (*.moneyguru)")
        docpath = QFileDialog.getSaveFileName(self.app.mainWindow, title, '',
                                              filters)[0]
        if docpath:
            if not docpath.endswith('.moneyguru'):
                docpath += '.moneyguru'
            self.model.save_to_xml(docpath)
            self.documentPath = docpath
            self.documentPathChanged()
            self.recentDocuments.insertItem(docpath)

    # Views
    def showNetWorthTriggered(self):
        self.model.select_pane_of_type(PaneType.NetWorth)

    def showProfitLossTriggered(self):
        self.model.select_pane_of_type(PaneType.Profit)

    def showTransactionsTriggered(self):
        self.model.select_pane_of_type(PaneType.Transaction)

    def showSchedulesTriggered(self):
        self.model.select_pane_of_type(PaneType.Schedule)

    def showPreviousViewTriggered(self):
        self.model.select_previous_view()

    def showNextViewTriggered(self):
        self.model.select_next_view()

    # Document Edition
    def newItemTriggered(self):
        self.model.new_item()

    def newAccountGroupTriggered(self):
        self.model.new_group()

    def deleteItemTriggered(self):
        self.model.delete_item()

    def editItemTriggered(self):
        self.model.edit_item()

    def moveUpTriggered(self):
        self.model.move_up()

    def moveDownTriggered(self):
        self.model.move_down()

    # Misc
    def closeTabTriggered(self):
        self.model.close_pane(self.model.current_pane_index)

    def navigateBackTriggered(self):
        self.model.navigate_back()

    def jumpToAccountTriggered(self):
        self.model.jump_to_account()

    def makeScheduleFromSelectedTriggered(self):
        self.model.make_schedule_from_selected()

    def reconcileSelectedTriggered(self):
        self._activeView().etable.toggle_reconciled()

    def toggleReconciliationModeTriggered(self):
        self._activeView().toggle_reconciliation_mode()
        self._updateActionsState()

    def toggleAccountExclusionTriggered(self):
        viewType = self.model.pane_type(self.model.current_pane_index)
        if viewType in {PaneType.NetWorth, PaneType.Profit}:
            self._activeView().sheet.toggle_excluded()

    def toggleGraphTriggered(self):
        self.model.toggle_area_visibility(PaneArea.BottomGraph)

    def togglePieChartTriggered(self):
        self.model.toggle_area_visibility(PaneArea.RightChart)

    def columnsVisibilityButtonClicked(self):
        items = self.model.column_menu_items()
        if not items:
            return
        menu = QMenu()
        for i, (display, marked) in enumerate(items):
            action = menu.addAction(display)
            action.setCheckable(True)
            action.setChecked(marked)
            action.setData(i)
            action.triggered.connect(self.columnsMenuItemWasClicked)
        self._columnMenuHolder = menu  # we need to hold a reference to it while it popups
        button = self.columnsVisibilityButton
        menu.popup(button.parentWidget().mapToGlobal(
            button.geometry().topLeft()))

    def columnsMenuItemWasClicked(self):
        action = self.sender()
        if action is not None:
            index = action.data()
            self.model.toggle_column_menu_item(index)

    def aboutTriggered(self):
        self.app.showAboutBox()

    def importDocument(self):
        title = tr("Select a document to import")
        filters = tr(
            "Supported files (*.moneyguru *.ofx *.qfx *.qif *.csv *.txt);;All files (*)"
        )
        docpath, filetype = QFileDialog.getOpenFileName(
            self.app.mainWindow, title, '', filters)
        # There's a strange glitch under GNOME where, right after the dialog is gone, the main
        # window isn't the active window, but it will become active if we give it enough time. If we
        # start showing the import window before that happens, we'll end up with an import window
        # under the main window, which is bad. Therefore, we process events until this happens. We
        # do this in a big forloop instead of a while to avoid a possible infinite loop.
        for i in range(10000):
            if self.app.mainWindow.isActiveWindow():
                break
            QApplication.processEvents()
        if docpath:
            try:
                self.model.parse_file_for_import(docpath)
            except FileFormatError as e:
                QMessageBox.warning(self.app.mainWindow,
                                    tr("Cannot import file"), str(e))

    # --- Other Signals
    def currentTabChanged(self, index):
        self.model.current_pane_index = index
        self._setTabIndex(index)

    def documentPathChanged(self):
        if self.documentPath:
            title = "moneyGuru ({})".format(self.documentPath)
        else:
            title = "moneyGuru"
        self.setWindowTitle(title)

    def tabCloseRequested(self, index):
        self.model.close_pane(index)

    def tabMoved(self, fromIndex, toIndex):
        # We don't refresh panes because tabMoved is apparently now called *during* drag operations.
        # If we start a full pane refresh during a drag operation, we segfault.
        self.model.move_pane(fromIndex, toIndex, refresh_panes=False)

    # --- document model --> view
    def query_for_schedule_scope(self):
        if QApplication.keyboardModifiers() & Qt.ShiftModifier:
            return ScheduleScope.Global
        if not self.app.model.show_schedule_scope_dialog:
            return ScheduleScope.Local
        dialog = ScheduleScopeDialog(self)
        return dialog.queryForScope()

    # --- model --> view
    def change_current_pane(self):
        self._setTabIndex(self.model.current_pane_index)

    def get_panel_view(self, model):
        if isinstance(model, CustomDateRangePanelModel):
            return CustomDateRangePanel(model, self)
        elif isinstance(model, CSVOptionsModel):
            return CSVOptionsWindow(model, self)
        elif isinstance(model, ImportWindowModel):
            return ImportWindow(model, self, self.app.prefs)
        else:
            return ExportPanel(model, self)

    def refresh_panes(self):
        while self.tabBar.count() < self.model.pane_count:
            self.tabBar.addTab('')
        for i in range(self.model.pane_count):
            pane_label = self.model.pane_label(i)
            pane_label = escapeamp(pane_label)
            self.tabBar.setTabText(i, pane_label)
            pane_type = self.model.pane_type(i)
            pane_view = self.model.pane_view(i)
            # Ensure that the view's "view" has been created and bound
            self._getViewforPane(pane_type, pane_view)
            iconname = PANETYPE2ICON.get(pane_type)
            icon = QIcon(QPixmap(
                ':/{0}'.format(iconname))) if iconname else QIcon()
            self.tabBar.setTabIcon(i, icon)
        # It's important that we proceed with tab removal *after* we've completed tab initialization.
        # We're walking on eggshells here. refresh_panes() can be called in multiple situations, one
        # of them is during the opening of a document. When that happens when another document was
        # previously opened, all views' model are uninitalized and don't have their "view" attribute
        # set yet. If we proceed with the setCurrentIndex() call below before _getViewforPane()
        # could be called above, we get a crash.
        if self.tabBar.currentIndex() >= self.model.pane_count:
            # Normally, we don't touch the tabBar index here and wait for change_current_pane,
            # but when we remove tabs, it's possible that currentTabChanged end up being called and
            # then the tab selection is bugged. I tried disconnecting/reconnecting the signal, but
            # this is buggy. So when a selected tab is about to be removed and is out of bounds,
            # we change the selection to the last index in the model. We don't use
            # self.model.current_pane_index because in some cases, it's -1 and prevents this crash
            # preventer from preventing its crash.
            self.tabBar.setCurrentIndex(self.model.pane_count - 1)
        while self.tabBar.count() > self.model.pane_count:
            self.tabBar.removeTab(self.tabBar.count() - 1)
        self.tabBar.setTabsClosable(self.model.pane_count > 1)

    def refresh_status_line(self):
        self.statusLabel.setText(self.model.status_line)

    def refresh_undo_actions(self):
        self._updateUndoActions()

    def restore_window_frame(self, frame):
        self.setGeometry(*frame)

    def save_window_frame(self):
        r = self.geometry()
        return (r.x(), r.y(), r.width(), r.height())

    def show_message(self, msg):
        title = tr("Warning")
        QMessageBox.warning(self, title, msg)

    def update_area_visibility(self):
        hidden = self.model.hidden_areas
        graphimg = ':/graph_visibility_{}_16'.format(
            'off' if PaneArea.BottomGraph in hidden else 'on')
        pieimg = ':/piechart_visibility_{}_16'.format(
            'off' if PaneArea.RightChart in hidden else 'on')
        self.graphVisibilityButton.setIcon(QIcon(QPixmap(graphimg)))
        self.piechartVisibilityButton.setIcon(QIcon(QPixmap(pieimg)))

    def view_closed(self, index):
        self.tabBar.removeTab(index)
        self.tabBar.setTabsClosable(self.model.pane_count > 1)