Exemplo n.º 1
0
 def setUp(self):
     import time
     self.feed = Feed()
     entry = Mock("Entry")
     entry.title = "Title1"
     entry.author = "Author1"
     entry.read = False
     entry.important = False
     entry.updated = time.gmtime(time.time())
     self.feed.entries.append(entry)
     entry = Mock("Entry")
     entry.title = "Title2"
     entry.author = "Author2"
     entry.read = True
     entry.important = True
     entry.updated = time.gmtime(time.time())
     self.feed.entries.append(entry)
     self.entryModel = EntryModel(self.feed)
     self.modeltest = ModelTest(self.entryModel, self.entryModel)
Exemplo n.º 2
0
    def __init__(self, parent=None):
        QtGui.QMainWindow.__init__(self, parent)

        # To be able to just hide to systray when closing the window and restore
        # the window via the systray icon need to set this to false
        QtGui.QApplication.setQuitOnLastWindowClosed(False)

        uic.loadUi("slimfeed.ui", self)
        self.feedMgr = FeedManager()

        # Restore settings, this includes feeds
        self._readSettings()

        self.feedModel = FeedModel(self.feedMgr, self)
        self.feedToolBar = QtGui.QToolBar(self.feedToolBarContainer)
        self.entryToolBar = QtGui.QToolBar(self.entryToolBarContainer)
        self.browserToolBar = QtGui.QToolBar(self.browserToolBarContainer)

        self.setupListToolBars()
        self.entryList.sortByColumn(2, QtCore.Qt.AscendingOrder)

        self.entryModel = EntryModel(parent=self)
        self.entryProxyModel = QtGui.QSortFilterProxyModel()
        self.entryProxyModel.setDynamicSortFilter(True)
        self.entryProxyModel.setSourceModel(self.entryModel)
        self.feedList.setModel(self.feedModel)
        self.entryList.setModel(self.entryProxyModel)

        self.entryModel.entriesChanged.connect(self.feedModel.entriesUpdated)

        # Directly call quit from our quit-action instead of going through the close
        # event since that just hides to systray
        self.actionQuit.triggered.connect(self.quit)
        self.actionAbout.triggered.connect(self.showAbout)
        self.actionAboutQt.triggered.connect(QtGui.qApp.aboutQt)
        self.actionPreferences.triggered.connect(self.showPreferences)
        self.actionAdd.triggered.connect(self.addFeed)
        self.actionDeleteFeed.triggered.connect(self.deleteSelectedFeed)
        self.actionDeleteEntry.triggered.connect(self.deleteSelectedEntry)
        self.actionMarkEntryAsImportant.triggered.connect(self.markSelectedEntriesImportant)
        self.actionMarkAllEntriesRead.triggered.connect(self.markAllEntriesRead)
        self.actionMarkAllFeedsRead.triggered.connect(self.markAllFeedsRead)
        self.actionMarkAsRead.triggered.connect(self.markArticleRead)
        self.actionMarkAsUnread.triggered.connect(self.markArticleUnread)
        self.feedList.selectionModel().selectionChanged.connect(self.feedSelectionChanged)
        self.entryList.selectionModel().selectionChanged.connect(self.entrySelectionChanged)
        self.feedList.addAction(self.actionDeleteFeed)
        self.updateTimer = QtCore.QTimer()
        self.updateTimer.timeout.connect(self.updateFeeds)
        self.updateThread = None
        self._startUpdateTimer()
        # Need to do this async, _readSettings is done too early and
        # hence the size is being overwritten somehow later on
        QtCore.QTimer.singleShot(0, self._restoreViews)

        self.entryList.selectionModel().currentChanged.connect(self.currentEntryChanged)
        self.markReadTimer = QtCore.QTimer()
        self.markReadTimer.setSingleShot(True)
        self.markReadTimer.timeout.connect(self.markEntryReadTimeout)

        # Start archive-timer if necessary
        self.archiveTimer = None
        self.checkArchiveTimer()

        self.updated.connect(self.feedsUpdated, QtCore.Qt.QueuedConnection)

        # Set up systray
        self.sysTray = QtGui.QSystemTrayIcon(QtGui.qApp.windowIcon(), self)
        self.updateSystrayIcon()
        self.sysTray.show()
        self.sysTray.activated.connect(self.sysTrayActivated)
        self.showAction = QtGui.QAction("Restore", self)
        self.showAction.triggered.connect(self.doShow)
        self.sysTrayMenu = QtGui.QMenu(self)
        self.sysTrayMenu.addAction(self.showAction)
        self.sysTrayMenu.addSeparator()
        self.sysTrayMenu.addAction(self.actionQuit)
        self.sysTray.setContextMenu(self.sysTrayMenu)
Exemplo n.º 3
0
class EntryModelTest(unittest2.TestCase):
    def setUp(self):
        import time
        self.feed = Feed()
        entry = Mock("Entry")
        entry.title = "Title1"
        entry.author = "Author1"
        entry.read = False
        entry.important = False
        entry.updated = time.gmtime(time.time())
        self.feed.entries.append(entry)
        entry = Mock("Entry")
        entry.title = "Title2"
        entry.author = "Author2"
        entry.read = True
        entry.important = True
        entry.updated = time.gmtime(time.time())
        self.feed.entries.append(entry)
        self.entryModel = EntryModel(self.feed)
        self.modeltest = ModelTest(self.entryModel, self.entryModel)

    def testUpdateImportantStatus(self):
        model = self.entryModel
        spyEntriesChanged = SignalSpy(model.entriesChanged)
        spyDataChanged = SignalSpy(model.dataChanged)
        idx = model.index(0, 0, QModelIndex())
        self.assertEqual(model.data(idx, Qt.DisplayRole), "")
        model.markImportant(idx)
        self.assertEqual(spyEntriesChanged.slotTriggered, 1)
        self.assertEqual(spyEntriesChanged.arguments[0][0], self.feed)
        self.assertEqual(spyDataChanged.slotTriggered, 1)
        self.assertEqual(spyDataChanged.arguments[0][0].row(), model.index(0, 0, QModelIndex()).row())
        self.assertEqual(spyDataChanged.arguments[0][1].row(), model.index(0, 2, QModelIndex()).row())
        self.assertEqual(model.data(idx, Qt.DisplayRole), "!")
        spyEntriesChanged = SignalSpy(model.entriesChanged)
        spyDataChanged = SignalSpy(model.dataChanged)
        idx = model.index(0, 0, QModelIndex())
        model.markImportant(idx)
        self.assertEqual(spyEntriesChanged.slotTriggered, 1)
        self.assertEqual(spyEntriesChanged.arguments[0][0], self.feed)
        self.assertEqual(spyDataChanged.slotTriggered, 1)
        self.assertEqual(spyDataChanged.arguments[0][0].row(), model.index(0, 0, QModelIndex()).row())
        self.assertEqual(spyDataChanged.arguments[0][1].row(), model.index(0, 2, QModelIndex()).row())
        self.assertEqual(model.data(idx, Qt.DisplayRole), "")


    def testUpdateReadStatus(self):
        model = self.entryModel
        spyEntriesChanged = SignalSpy(model.entriesChanged)
        spyDataChanged = SignalSpy(model.dataChanged)
        idx = model.index(0, 0, QModelIndex())
        model.markRead(idx)
        self.assertEqual(spyEntriesChanged.slotTriggered, 1)
        self.assertEqual(spyEntriesChanged.arguments[0][0], self.feed)
        self.assertEqual(spyDataChanged.slotTriggered, 1)
        self.assertEqual(spyDataChanged.arguments[0][0].row(), model.index(0, 0, QModelIndex()).row())
        self.assertEqual(spyDataChanged.arguments[0][1].row(), model.index(0, 2, QModelIndex()).row())
        spyEntriesChanged = SignalSpy(model.entriesChanged)
        spyDataChanged = SignalSpy(model.dataChanged)
        idx = model.index(0, 0, QModelIndex())
        model.markUnread(idx)
        self.assertEqual(spyEntriesChanged.slotTriggered, 1)
        self.assertEqual(spyEntriesChanged.arguments[0][0], self.feed)
        self.assertEqual(spyDataChanged.slotTriggered, 1)
        self.assertEqual(spyDataChanged.arguments[0][0].row(), model.index(0, 0, QModelIndex()).row())
        self.assertEqual(spyDataChanged.arguments[0][1].row(), model.index(0, 2, QModelIndex()).row())

    def testHeader(self):
        self.assertEqual(self.entryModel.headerData(0, Qt.Horizontal, Qt.DisplayRole), "")
        self.assertEqual(self.entryModel.headerData(1, Qt.Horizontal, Qt.DisplayRole), "Title")
        self.assertEqual(self.entryModel.headerData(2, Qt.Horizontal, Qt.DisplayRole), "Author")
        self.assertEqual(self.entryModel.headerData(3, Qt.Horizontal, Qt.DisplayRole), "Updated")

    def testData(self):
        self.assertEqual(self.entryModel.rowCount(), 2)
        self.assertEqual(self.entryModel.columnCount(), 4)
        self.assertEqual(self.entryModel.data(
                self.entryModel.index(0, 0, QModelIndex()), 
                    Qt.DisplayRole), "")
        self.assertEqual(self.entryModel.data(
                self.entryModel.index(0, 0, QModelIndex()),
                    Qt.FontRole).italic(), False)
        self.assertEqual(self.entryModel.data(
                self.entryModel.index(0, 1, QModelIndex()),
                    Qt.DisplayRole), "Title1")
        self.assertEqual(self.entryModel.data(
                self.entryModel.index(0, 2, QModelIndex()),
                    Qt.DisplayRole), "Author1")
        self.assertEqual(self.entryModel.data(
                self.entryModel.index(0, 3, QModelIndex()),
                    Qt.DisplayRole), qDateTimeFromTimeStruct(
                        list(self.feed.entries)[0].updated))
        self.assertEqual(self.entryModel.data(
                self.entryModel.index(0, 1, QModelIndex()),
                    Qt.FontRole).bold(), True)
        self.assertEqual(self.entryModel.data(
                self.entryModel.index(1, 1, QModelIndex()),
                    Qt.DisplayRole), "Title2")
        self.assertEqual(self.entryModel.data(
                self.entryModel.index(1, 0, QModelIndex()), 
                    Qt.DisplayRole), "!")
        self.assertEqual(self.entryModel.data(
                self.entryModel.index(1, 0, QModelIndex()),
                    Qt.FontRole).italic(), True)
        self.assertEqual(self.entryModel.data(
                self.entryModel.index(1, 2, QModelIndex()),
                    Qt.DisplayRole), "Author2")
        self.assertEqual(self.entryModel.data(
                self.entryModel.index(1, 3, QModelIndex()),
                    Qt.DisplayRole), qDateTimeFromTimeStruct(
                        list(self.feed.entries)[1].updated))
        self.assertEqual(self.entryModel.data(
                self.entryModel.index(1, 1, QModelIndex()),
                    Qt.FontRole).bold(), False)

    def testIndexFromEntry(self):
        self.assertEqual(self.entryModel.indexForEntry(self.feed.entries[0]), self.entryModel.index(0, 0, QModelIndex()))
        self.assertEqual(self.entryModel.indexForEntry(self.feed.entries[1]), self.entryModel.index(1, 0, QModelIndex()))
Exemplo n.º 4
0
class MainWindow(QtGui.QMainWindow):
    updated = pyqtSignal(list)

    def __init__(self, parent=None):
        QtGui.QMainWindow.__init__(self, parent)

        # To be able to just hide to systray when closing the window and restore
        # the window via the systray icon need to set this to false
        QtGui.QApplication.setQuitOnLastWindowClosed(False)

        uic.loadUi("slimfeed.ui", self)
        self.feedMgr = FeedManager()

        # Restore settings, this includes feeds
        self._readSettings()

        self.feedModel = FeedModel(self.feedMgr, self)
        self.feedToolBar = QtGui.QToolBar(self.feedToolBarContainer)
        self.entryToolBar = QtGui.QToolBar(self.entryToolBarContainer)
        self.browserToolBar = QtGui.QToolBar(self.browserToolBarContainer)

        self.setupListToolBars()
        self.entryList.sortByColumn(2, QtCore.Qt.AscendingOrder)

        self.entryModel = EntryModel(parent=self)
        self.entryProxyModel = QtGui.QSortFilterProxyModel()
        self.entryProxyModel.setDynamicSortFilter(True)
        self.entryProxyModel.setSourceModel(self.entryModel)
        self.feedList.setModel(self.feedModel)
        self.entryList.setModel(self.entryProxyModel)

        self.entryModel.entriesChanged.connect(self.feedModel.entriesUpdated)

        # Directly call quit from our quit-action instead of going through the close
        # event since that just hides to systray
        self.actionQuit.triggered.connect(self.quit)
        self.actionAbout.triggered.connect(self.showAbout)
        self.actionAboutQt.triggered.connect(QtGui.qApp.aboutQt)
        self.actionPreferences.triggered.connect(self.showPreferences)
        self.actionAdd.triggered.connect(self.addFeed)
        self.actionDeleteFeed.triggered.connect(self.deleteSelectedFeed)
        self.actionDeleteEntry.triggered.connect(self.deleteSelectedEntry)
        self.actionMarkEntryAsImportant.triggered.connect(self.markSelectedEntriesImportant)
        self.actionMarkAllEntriesRead.triggered.connect(self.markAllEntriesRead)
        self.actionMarkAllFeedsRead.triggered.connect(self.markAllFeedsRead)
        self.actionMarkAsRead.triggered.connect(self.markArticleRead)
        self.actionMarkAsUnread.triggered.connect(self.markArticleUnread)
        self.feedList.selectionModel().selectionChanged.connect(self.feedSelectionChanged)
        self.entryList.selectionModel().selectionChanged.connect(self.entrySelectionChanged)
        self.feedList.addAction(self.actionDeleteFeed)
        self.updateTimer = QtCore.QTimer()
        self.updateTimer.timeout.connect(self.updateFeeds)
        self.updateThread = None
        self._startUpdateTimer()
        # Need to do this async, _readSettings is done too early and
        # hence the size is being overwritten somehow later on
        QtCore.QTimer.singleShot(0, self._restoreViews)

        self.entryList.selectionModel().currentChanged.connect(self.currentEntryChanged)
        self.markReadTimer = QtCore.QTimer()
        self.markReadTimer.setSingleShot(True)
        self.markReadTimer.timeout.connect(self.markEntryReadTimeout)

        # Start archive-timer if necessary
        self.archiveTimer = None
        self.checkArchiveTimer()

        self.updated.connect(self.feedsUpdated, QtCore.Qt.QueuedConnection)

        # Set up systray
        self.sysTray = QtGui.QSystemTrayIcon(QtGui.qApp.windowIcon(), self)
        self.updateSystrayIcon()
        self.sysTray.show()
        self.sysTray.activated.connect(self.sysTrayActivated)
        self.showAction = QtGui.QAction("Restore", self)
        self.showAction.triggered.connect(self.doShow)
        self.sysTrayMenu = QtGui.QMenu(self)
        self.sysTrayMenu.addAction(self.showAction)
        self.sysTrayMenu.addSeparator()
        self.sysTrayMenu.addAction(self.actionQuit)
        self.sysTray.setContextMenu(self.sysTrayMenu)

    def _startUpdateTimer(self):
        self.updateTimer.start(self.updateTimeout * 1000)

    def doShow(self):
        self.show()
        self.activateWindow()

    def markAllEntriesRead(self):
        for idx in self.feedList.selectionModel().selectedRows():
            feed = self.feedModel.feedFromIndex(idx)
            for entry in feed.entries:
                self.entryModel.markRead(self.entryModel.indexForEntry(entry))

    def markAllFeedsRead(self):
        for feed in self.feedMgr.feeds:
            idx = self.feedModel.indexForFeed(feed)
            # For the currently-selected feed update should be done through entry-model
            # so that the entry list updates correctly. For the other feeds we just need
            # to notify once all entries are marked
            if self.feedList.selectionModel().isSelected(idx):
                for entry in feed.entries:
                    self.entryModel.markRead(self.entryModel.indexForEntry(entry))
            else:
                for entry in feed.entries:
                    entry.read = True
                # Update the model
                self.feedModel.feedsUpdated([{"title": feed.title}])

    def checkArchiveTimer(self):
        if self.enableArticleDeletion:
            if self.archiveTimer is None:
                self.archiveTimer = QtCore.QTimer()
                self.archiveTimer.timeout.connect(self.archiveArticles)
            # First check after enabling is done after 60 seconds, the archive
            # function will then re-schedule for longer timeout
            self.archiveTimer.start(6000)
        elif self.archiveTimer is not None:
            self.archiveTimer.stop()
            self.archiveTimer.timeout.disconnect(self.archiveArticles)
            self.archiveTimer = None

    def archiveArticles(self):
        deleteableinfo = self.feedMgr.checkForArchiveableEntries(
            self.numberOfDaysEnabled, self.numberOfDays, self.numberOfArticlesEnabled, self.numberOfArticles
        )
        # Now update the feeds, this may need to go through the entrymodel so that the indexes are updated properly,
        # hence cannot do this directly in the archiving-method above
        for info in deleteableinfo:
            feed = info["feed"]
            entries = info["entries"]
            isentrymodelfeed = self.entryModel.feed == feed
            for entry in entries:
                if isentrymodelfeed:
                    self.entryModel.removeEntry(self.entryModel.indexForEntry(entry))
                else:
                    feed.entries.remove(entry)
            self.feedModel.entriesUpdated(feed)
        # Restart timer to run again after an hour
        self.archiveTimer.start(60 * 1000)

    def showPreferences(self):
        prefs = Preferences(self)
        prefs.setWindowTitle("Slimfeed Preferences")
        prefs.updateTimeout = self.updateTimeout
        prefs.markReadTimeout = self.markReadTimeout
        prefs.systrayFont = self.systrayFont
        prefs.systrayFontColor = self.systrayFontColor
        prefs.articleDeletionEnabled = self.enableArticleDeletion
        prefs.numberOfArticlesEnabled = self.numberOfArticlesEnabled
        prefs.numberOfArticles = self.numberOfArticles
        prefs.numberOfDaysEnabled = self.numberOfDaysEnabled
        prefs.numberOfDays = self.numberOfDays
        if prefs.exec_() == QtGui.QDialog.Accepted:
            self.updateTimeout = prefs.updateTimeout
            self.markReadTimeout = prefs.markReadTimeout
            self.systrayFont = prefs.systrayFont
            self.systrayFontColor = prefs.systrayFontColor
            self.enableArticleDeletion = prefs.articleDeletionEnabled
            self.numberOfArticlesEnabled = prefs.numberOfArticlesEnabled
            self.numberOfArticles = prefs.numberOfArticles
            self.numberOfDaysEnabled = prefs.numberOfDaysEnabled
            self.numberOfDays = prefs.numberOfDays
            self.updateSystrayIcon()
            self.checkArchiveTimer()

    def sysTrayActivated(self, reason):
        if reason == QtGui.QSystemTrayIcon.Trigger:
            if self.isVisible():
                self.hide()
            else:
                self.doShow()

    def markArticleRead(self):
        if self.feedList.selectionModel().hasSelection():
            for idx in self.entryList.selectionModel().selectedRows():
                self.entryModel.markRead(idx)

    def markArticleUnread(self):
        if self.feedList.selectionModel().hasSelection():
            for idx in self.entryList.selectionModel().selectedRows():
                self.entryModel.markUnread(idx)

    def markEntryReadTimeout(self):
        if self.markReadIdx is not None and self.markReadIdx.isValid():
            self.entryModel.markRead(self.markReadIdx)
        self.updateSystrayIcon()

    def currentEntryChanged(self, idx1, idx2):
        self.markReadIdx = self.entryProxyModel.mapToSource(idx1)
        self.markReadTimer.start(self.markReadTimeout)
        entry = self.entryModel.entryFromIndex(self.markReadIdx)
        self.browser.setUrl(QUrl(entry.url))

    def markSelectedEntriesImportant(self):
        selection = self.entryList.selectionModel().selectedRows()
        if len(selection) > 0:
            for idx in selection:
                srcIdx = self.entryProxyModel.mapToSource(idx)
                self.entryModel.markImportant(srcIdx)

    def updateFeeds(self):
        if self.updateThread is None or not self.updateThread.isAlive():
            self.updateThread = UpdateThread(self)
            self.updateThread.setDaemon(True)
            self.updateThread.start()

    def updateSystrayIcon(self):
        numUnread = 0
        for feed in self.feedMgr.feeds:
            numUnread += feed.unread
        pixmap = self.windowIcon().pixmap(self.sysTray.geometry().size())
        if not pixmap.isNull():
            painter = QtGui.QPainter()
            painter.begin(pixmap)
            painter.setFont(self.systrayFont)
            painter.setPen(self.systrayFontColor)
            painter.drawText(pixmap.rect(), QtCore.Qt.AlignCenter | QtCore.Qt.AlignHCenter, str(numUnread))
            painter.end()
            self.sysTrayIcon = QtGui.QIcon(pixmap)
            self.sysTray.setIcon(self.sysTrayIcon)

    @QtCore.pyqtSlot(list)
    def feedsUpdated(self, updated):
        self.updateThread = None
        self.feedModel.feedsUpdated(updated)
        self.entryModel.feedsUpdated(updated)
        self.updateSystrayIcon()
        self._startUpdateTimer()

    def _setupToolBar(self, toolbar, container, actions):
        container.setLayout(QtGui.QVBoxLayout())
        container.layout().setSpacing(0)
        container.layout().setContentsMargins(0, 0, 0, 0)
        container.layout().addWidget(toolbar)
        for action in actions:
            toolbar.addAction(action)
        toolbar.setIconSize(QtCore.QSize(16, 16))

    def setupListToolBars(self):
        self._setupToolBar(self.feedToolBar, self.feedToolBarContainer, [self.actionDeleteFeed])
        self._setupToolBar(
            self.entryToolBar,
            self.entryToolBarContainer,
            [self.actionMarkEntryAsImportant, self.actionMarkAsRead, self.actionMarkAsUnread, self.actionDeleteEntry],
        )
        self._setupToolBar(
            self.browserToolBar,
            self.browserToolBarContainer,
            [self.actionBack, self.actionForward, self.actionStop, self.actionReload],
        )

    def feedSelectionChanged(self):
        selection = self.feedList.selectionModel().selectedRows()
        self.actionDeleteFeed.setEnabled((len(selection) > 0))
        if len(selection) > 0:
            self.entryModel.feed = self.feedModel.feedFromIndex(selection[0])
        else:
            self.entryModel.feed = None

    def entrySelectionChanged(self):
        selection = self.entryList.selectionModel().selectedRows()
        self.actionDeleteEntry.setEnabled((len(selection) > 0))
        self.actionMarkEntryAsImportant.setEnabled((len(selection) > 0))

    def deleteSelectedEntry(self):
        selection = self.entryList.selectionModel().selectedRows()
        if len(selection) > 0:
            srcIdx = self.entryProxyModel.mapToSource(selection[0])
            entry = self.entryModel.entryFromIndex(srcIdx)
            if (
                QtGui.QMessageBox.question(
                    self,
                    "Delete Feed",
                    "Do you really want to delete the article '%s'" % (entry.title),
                    QtGui.QMessageBox.Yes | QtGui.QMessageBox.No,
                    QtGui.QMessageBox.No,
                )
                == QtGui.QMessageBox.Yes
            ):
                self.entryModel.removeEntry(srcIdx)

    def deleteSelectedFeed(self):
        selection = self.feedList.selectionModel().selectedRows()
        if len(selection) > 0:
            feed = self.feedModel.feedFromIndex(selection[0])
            if (
                QtGui.QMessageBox.question(
                    self,
                    "Delete Feed",
                    "Do you really want to delete the feed '%s'" % (feed.title),
                    QtGui.QMessageBox.Yes | QtGui.QMessageBox.No,
                    QtGui.QMessageBox.No,
                )
                == QtGui.QMessageBox.Yes
            ):
                self.feedModel.deleteFeed(feed)

    def _restoreHeaderView(self, settings, view):
        view.horizontalHeader().restoreState(settings.value("Horizontal Header State", QtCore.QByteArray()))
        for grp in settings.childGroups():
            if "Column" in grp:
                col = int(grp.split(" ")[1])
                settings.beginGroup(grp)
                size = view.horizontalHeader().sectionSize(col)
                newsize = int(settings.value("Width", size))
                view.horizontalHeader().resizeSection(col, newsize)
                settings.endGroup()

    def _restoreViews(self):
        settings = QtCore.QSettings("de.apaku", "Slimfeed")
        settings.beginGroup("FeedList")
        self._restoreHeaderView(settings, self.feedList)
        currentFeedId = settings.value("CurrentFeedUrl", None)
        currentFeed = None
        if currentFeedId is not None:
            for feed in self.feedMgr.feeds:
                if feed.url == currentFeedId:
                    currentFeed = feed
                    break
        if currentFeed is not None:
            # Make sure to select the complete row, to be consistent with what the user
            # can select
            topleft = self.feedModel.indexForFeed(currentFeed)
            self.feedList.selectionModel().select(
                topleft, QtGui.QItemSelectionModel.ClearAndSelect | QtGui.QItemSelectionModel.Rows
            )
            self.feedList.selectionModel().setCurrentIndex(topleft, QtGui.QItemSelectionModel.Current)
        settings.endGroup()
        settings.beginGroup("EntryList")
        self._restoreHeaderView(settings, self.entryList)
        # Ensure that the sort indicator is always shown on the entryList, even if restoring from
        # a state before this property was used
        self.entryList.horizontalHeader().setSortIndicatorShown(True)
        currentEntryId = settings.value("CurrentEntryId", None)
        currentEntry = None
        if currentEntryId is not None and currentFeed is not None:
            for entry in currentFeed.entries:
                if entry.identity == currentEntryId:
                    currentEntry = entry
                    break
        if currentEntry is not None:
            # Make sure to select the complete row, to be consistent with what the user
            # can select
            topleft = self.entryProxyModel.mapFromSource(self.entryModel.indexForEntry(currentEntry))
            self.entryList.selectionModel().select(
                topleft, QtGui.QItemSelectionModel.ClearAndSelect | QtGui.QItemSelectionModel.Rows
            )
            self.entryList.selectionModel().setCurrentIndex(topleft, QtGui.QItemSelectionModel.Current)
        settings.endGroup()

    def _readSettings(self):
        settings = QtCore.QSettings("de.apaku", "Slimfeed")
        self.restoreGeometry(settings.value("geometry", QtCore.QByteArray()))
        self.restoreState(settings.value("state", QtCore.QByteArray()))
        self.updateTimeout = _readQSettingsIntEntry(settings, "UpdateTimeout", 300)
        self.markReadTimeout = _readQSettingsIntEntry(settings, "MarkReadTimeout", 500)
        settings.beginGroup("ArticleDeletion")

        self.enableArticleDeletion = _readQSettingsBoolEntry(settings, "Enabled", False)
        self.numberOfArticlesEnabled = _readQSettingsBoolEntry(settings, "NumberOfArticlesEnabled", False)
        self.numberOfArticles = _readQSettingsIntEntry(settings, "NumberOfArticles", 100)
        self.numberOfDaysEnabled = _readQSettingsBoolEntry(settings, "NumberOfDaysEnabled", True)
        self.numberOfDays = _readQSettingsIntEntry(settings, "NumberOfDays", 60)
        settings.endGroup()

        self.systrayFontColor = settings.value(
            "SystrayFontColor", QtGui.qApp.palette().color(QtGui.QPalette.Active, QtGui.QPalette.Text)
        )
        # The default font for the systray overlay text should be bold
        defFont = QtGui.qApp.font()
        defFont.setBold(True)
        self.systrayFont = settings.value("SystrayFont", defFont)
        settings.beginGroup("Feeds")
        self.feedMgr.load(settings)
        settings.endGroup()

    def _writeSettings(self):
        settings = QtCore.QSettings("de.apaku", "Slimfeed")
        settings.setValue("geometry", self.saveGeometry())
        settings.setValue("state", self.saveState())
        settings.setValue("UpdateTimeout", self.updateTimeout)
        settings.setValue("MarkReadTimeout", self.markReadTimeout)

        settings.beginGroup("ArticleDeletion")
        settings.setValue("Enabled", self.enableArticleDeletion)
        settings.setValue("NumberOfArticlesEnabled", self.numberOfArticlesEnabled)
        settings.setValue("NumberOfArticles", self.numberOfArticles)
        settings.setValue("NumberOfDaysEnabled", self.numberOfDaysEnabled)
        settings.setValue("NumberOfDays", self.numberOfDays)
        settings.endGroup()

        settings.setValue("SystrayFont", self.systrayFont)
        settings.setValue("SystrayFontColor", self.systrayFontColor)
        settings.beginGroup("EntryList")
        currentEntry = self.entryModel.entryFromIndex(
            self.entryProxyModel.mapToSource(self.entryList.selectionModel().currentIndex())
        )
        if currentEntry is not None:
            settings.setValue("CurrentEntryId", currentEntry.identity)
        else:
            settings.setValue("CurrentEntryId", None)
        settings.setValue("Horizontal Header State", self.entryList.horizontalHeader().saveState())
        for col in range(0, self.entryList.horizontalHeader().count()):
            settings.beginGroup("Column %d" % col)
            settings.setValue("Width", self.entryList.horizontalHeader().sectionSize(col))
            settings.endGroup()
        settings.endGroup()
        settings.beginGroup("FeedList")
        currentFeed = self.feedModel.feedFromIndex(self.feedList.selectionModel().currentIndex())
        if currentFeed is not None:
            settings.setValue("CurrentFeedUrl", currentFeed.url)
        else:
            settings.setValue("CurrentFeedUrl", None)
        settings.setValue("Horizontal Header State", self.feedList.horizontalHeader().saveState())
        for col in range(0, self.feedList.horizontalHeader().count()):
            settings.beginGroup("Column %d" % col)
            settings.setValue("Width", self.feedList.horizontalHeader().sectionSize(col))
            settings.endGroup()
        settings.endGroup()
        settings.beginGroup("Feeds")
        # Clear out all stored feeds so we don't carry around
        # any that might have been deleted meanwhile
        for grp in settings.childGroups():
            settings.remove(grp)
        self.feedMgr.save(settings)
        settings.endGroup()
        settings.sync()

    def quit(self):
        # Really Quit from here
        self._writeSettings()
        QtGui.QApplication.quit()

    def closeEvent(self, event):
        # If we're closed by the user, lets just hide
        # but if the close comes due to session-shutdown lets really quit
        if not QtGui.qApp.isAboutToQuit():
            self.hide()
            event.ignore()
        else:
            event.accept()
            quit()

    def addFeed(self):
        dlg = uic.loadUi("addfeeddlg.ui", QtGui.QDialog(self))
        if dlg.exec_() == QtGui.QDialog.Accepted:
            data = feedparser.parse(dlg.url.text())
            feed = createFeedFromData(data)
            self.feedModel.addFeed(feed)

    def showAbout(self):
        txt = """
<b>Slimfeed</b><br />
<br />
Version: 0.9.0<br />
A samll and fast feed reader<br />
<br />
Copyright 2011 Andreas Pakulat <*****@*****.**>
"""
        QtGui.QMessageBox.about(self, "About Slimfeed", txt)