class Reader (Gtk.Window, GObject.GObject): """ """ __gsignals__ = { "loaded" : ( GObject.SignalFlags.RUN_FIRST, None, ()), "next-article" : ( GObject.SignalFlags.RUN_FIRST, None, ()), "previous-article" : ( GObject.SignalFlags.RUN_FIRST, None, ()), "next-feed" : ( GObject.SignalFlags.RUN_FIRST, None, ()), "previous-feed" : ( GObject.SignalFlags.RUN_FIRST, None, ()), "no-engine" : ( GObject.SignalFlags.RUN_FIRST, None, ()), } def __repr__(self): return "Reader" def __init__(self): Gtk.Window.__init__(self) GObject.GObject.__init__(self) GObject.type_register(Reader) self.log = Logger("reader.log", "BRss-Reader") self.settings = Gio.Settings.new(BASE_KEY) self.settings.connect("changed::show-status", self.__toggle_status) # ui elements self.tree = Tree(self.log) self.ilist = ArticleList(self.log) self.ilist.set_property("height-request", 250) self.view = View(self.log) self.status = Status() # layout self.__layout_ui() #signals self.__connect_signals() self.__get_engine() # ready to go self.emit('loaded') def __check_engine(self): bus = dbus.SessionBus() try: engine = bus.get_object(ENGINE_DBUS_KEY, ENGINE_DBUS_PATH) except: return False return engine def __get_engine(self): self.engine = self.__check_engine() if not self.engine: self.log.critical("{0}: Couldn't get a DBus connection; quitting.".format(self)) self.alert.error(_("Could not connect to Engine"), _("BRss will now quit.\nPlease make sure that the engine is running and restart the application")) self.quit() self.create = self.engine.get_dbus_method('create', ENGINE_DBUS_KEY) self.edit = self.engine.get_dbus_method('edit', ENGINE_DBUS_KEY) self.update = self.engine.get_dbus_method('update', ENGINE_DBUS_KEY) self.delete = self.engine.get_dbus_method('delete', ENGINE_DBUS_KEY) self.get_menu_items = self.engine.get_dbus_method('get_menu_items', ENGINE_DBUS_KEY) self.get_articles_for = self.engine.get_dbus_method('get_articles_for', ENGINE_DBUS_KEY) self.search_for = self.engine.get_dbus_method('search_for', ENGINE_DBUS_KEY) self.get_article = self.engine.get_dbus_method('get_article', ENGINE_DBUS_KEY) self.toggle_starred = self.engine.get_dbus_method('toggle_starred', ENGINE_DBUS_KEY) self.toggle_read = self.engine.get_dbus_method('toggle_read', ENGINE_DBUS_KEY) self.stop_update = self.engine.get_dbus_method('stop_update', ENGINE_DBUS_KEY) self.count_special = self.engine.get_dbus_method('count_special', ENGINE_DBUS_KEY) self.get_configs = self.engine.get_dbus_method('get_configs', ENGINE_DBUS_KEY) self.set_configs = self.engine.get_dbus_method('set_configs', ENGINE_DBUS_KEY) self.import_opml = self.engine.get_dbus_method('import_opml', ENGINE_DBUS_KEY) self.export_opml = self.engine.get_dbus_method('export_opml', ENGINE_DBUS_KEY) self.rag.set_visible(False) self.ag.set_visible(True) self.__connect_engine_signals() self.log.debug(_("{0}: Connected to feed engine {1}".format(self, self.engine))) def __create_menu(self): ui_string = """<ui> <menubar name='Menubar'> <menu action='FeedMenu'> <menuitem action='New feed'/> <menuitem action='New category'/> <menuitem action='Delete'/> <separator/> <menuitem action='Import feeds'/> <menuitem action='Export feeds'/> <separator /> <menuitem name='Reconnect' action='Reconnect'/> <separator/> <menuitem action='Quit'/> </menu> <menu action='EditMenu'> <menuitem action='Edit'/> <menuitem action='Find'/> <separator/> <menuitem action='Star'/> <menuitem action='Read'/> <separator/> <menuitem action='Preferences'/> </menu> <menu action='NetworkMenu'> <menuitem action='Update'/> <menuitem action='Update all'/> </menu> <menu action='ViewMenu'> <menuitem action='NextArticle'/> <menuitem action='PreviousArticle'/> <menuitem action='NextFeed'/> <menuitem action='PreviousFeed'/> <separator /> <menuitem action='FullScreen'/> </menu> <menu action='HelpMenu'> <menuitem action='About'/> </menu> </menubar> <toolbar name='Toolbar'> <toolitem name='New feed' action='New feed'/> <toolitem name='New category' action='New category'/> <separator name='sep1'/> <toolitem name='Reconnect' action='Reconnect'/> <separator /> <toolitem name='Update all' action='Update all'/> <toolitem name='Stop' action='StopUpdate'/> <separator name='sep2'/> <toolitem name='PreviousFeed' action='PreviousFeed'/> <toolitem name='PreviousArticle' action='PreviousArticle'/> <toolitem name='Star' action='Star'/> <toolitem name='NextArticle' action='NextArticle'/> <toolitem name='NextFeed' action='NextFeed'/> <separator name='sep3'/> <toolitem name='FullScreen' action='FullScreen'/> <toolitem name='Find' action='Find'/> <toolitem name='Preferences' action='Preferences'/> </toolbar> </ui>""" self.mag = Gtk.ActionGroup('MenuActions') mactions = [ ('FeedMenu', None, _('_Feeds')), ('EditMenu', None, _('E_dit')), ('NetworkMenu', None, _('_Network')), ('ViewMenu', None, _('_View')), ('HelpMenu', None, _('_Help')), ('About', "gtk-about", _('_About'), None, _('About'), self.__about), ] self.mag.add_actions(mactions) self.ag = Gtk.ActionGroup('WindowActions') actions = [ ('New feed', 'feed', _('_New feed'), '<control><alt>n', _('Add a feed'), self.__add_feed), ('New category', "gtk-directory", _('New _category'), '<control><alt>c', _('Add a category'), self.__add_category), ('Delete', "gtk-clear", _('Delete'), 'Delete', _('Delete a feed or a category'), self.__delete_item), ('Import feeds', "gtk-redo", _('Import feeds'), None, _('Import a feedlist'), self.__import_feeds), ('Export feeds', "gtk-undo", _('Export feeds'), None, _('Export a feedlist'), self.__export_feeds), ('Quit', "gtk-quit", _('_Quit'), '<control>Q', _('Quits'), self.quit), ('Edit', "gtk-edit", _('_Edit'), '<control>E', _('Edit the selected element')), ('Star', "gtk-about", _('_Star'), 'x', _('Star the current article'), self.__star), ('Read', "gtk-ok", _('_Read'), 'r', _('Toggle the current article read status'), self.__read), ('Preferences', "gtk-preferences", _('_Preferences'), '<control>P', _('Configure the engine'), self.__edit_prefs), ('Update', None, _('_Update'), '<control>U', _('Update the selected feed'), self.__update_feed), ('Update all', "gtk-refresh", _('Update all'), '<control>R', _('Update all feeds'), self.__update_all), ('PreviousArticle', "gtk-go-back", _('Previous Article'), 'b', _('Go to the previous article'), self.__previous_article), ('NextArticle', "gtk-go-forward", _('Next Article'), 'n', _('Go to the next article'), self.__next_article), ('PreviousFeed', "gtk-goto-first", _('Previous Feed'), '<shift>b', _('Go to the previous news feed'), self.__previous_feed), ('NextFeed', "gtk-goto-last", _('Next Feed'), '<shift>n', _('Go to the next news feed'), self.__next_feed), ('StopUpdate', "gtk-stop", _('Stop'), None, _('Stop the current update'), self.__stop_updates), ] tactions = [ ('Find', "gtk-find", _('Find'), '<control>F', _('Search for a term in the articles'), self.__toggle_search), ('FullScreen', "gtk-fullscreen", _('Fullscreen'), 'F11', _('(De)Activate fullscreen'), self.__toggle_fullscreen), ] self.ag.add_actions(actions) self.ag.add_toggle_actions(tactions) # break reconnect into its own group self.rag = Gtk.ActionGroup("Rec") ractions = [ ('Reconnect', "gtk-disconnect", _('_Reconnect'), None, _('Try and reconnect to the feed engine'), self.__reconnect), ] self.rag.add_actions(ractions) self.ui = Gtk.UIManager() self.ui.insert_action_group(self.mag, 0) self.ui.insert_action_group(self.ag, 0) self.ui.insert_action_group(self.rag, 1) self.ui.add_ui_from_string(ui_string) self.add_accel_group(self.ui.get_accel_group()) def __reset_title(self, *args): self.set_title('BRss Reader') def __stop_updates(self, *args): self.stop_update( reply_handler=self.__to_log, error_handler=self.__to_log) self.ag.get_action('StopUpdate').set_sensitive(False) def __layout_ui(self): self.log.debug("{0}: Laying out User Interface".format(self)) self.__create_menu() opane = Gtk.VPaned() opane.pack1(self.ilist) opane.pack2(self.view) pane = Gtk.HPaned() pane.pack1(self.tree) pane.pack2(opane) al = Gtk.Alignment.new(0.5,0.5,1,1) al.set_padding(3,3,3,3) al.add(pane) box = Gtk.VBox(spacing=3) box.pack_start(self.ui.get_widget('/Menubar'), False, True, 0) box.pack_start(self.ui.get_widget('/Toolbar'), False, True, 0) #setting GTK3 style ctx= self.ui.get_widget('/Toolbar').get_style_context() print "UI Context: ",ctx ctx.add_class(Gtk.STYLE_CLASS_PRIMARY_TOOLBAR) widget = self.ag.get_action('StopUpdate') widget.set_sensitive(False) box.pack_start(al, True, True, 0) box.pack_start(self.status, False, False, 0) self.add(box) self.set_property("height-request", 700) self.set_property("width-request", 1024) self.is_fullscreen = False self.__reset_title() self.set_icon_from_file(make_path('icons/hicolor','brss.svg')) self.alert = Alerts(self) self.connect("destroy", self.quit) self.show_all() if not self.settings.get_boolean('show-status'): self.status.hide() def __connect_signals(self): # signals self.log.debug("{0}: Connecting all signals".format(self)) self.connect('next-article', self.ilist.next_item) self.connect('previous-article', self.ilist.previous_item) self.connect('next-feed', self.tree.next_item) self.connect('previous-feed', self.tree.previous_item)#TODO: implement self.connect_after('no-engine', self.__no_engine) self.connect_after('no-engine', self.view.no_engine) self.tree.connect('item-selected', self.__load_articles) self.tree.connect('dcall-request', self.__handle_dcall) self.ilist.connect('item-selected', self.__load_article) self.ilist.connect('item-selected', self.__update_title) self.ilist.connect('no-data', self.view.clear) self.ilist.connect('no-data', self.__reset_title) self.ilist.connect('star-toggled', self.__toggle_starred) self.ilist.connect('read-toggled', self.__toggle_read) self.ilist.connect('filter-ready', self.__connect_accels) self.ilist.connect('list_loaded', self.__hide_search,) self.ilist.connect_after('row-updated', self.tree.update_starred) self.ilist.connect_after('row-updated', self.tree.update_unread) self.ilist.connect('dcall-request', self.__handle_dcall) self.ilist.connect('search-requested', self.__search_articles) self.ilist.connect('search-requested', self.tree.deselect) self.view.connect('article-loaded', self.ilist.mark_read) self.view.connect('link-clicked', self.__to_browser) self.view.connect('link-hovered-in', self.__status_info) self.view.connect('link-hovered-out', self.__status_info) def __connect_engine_signals(self): self.engine.connect_to_signal('notice', self.status.message) self.engine.connect_to_signal('added', self.__handle_added) self.engine.connect_to_signal('updated', self.__handle_updated) self.engine.connect_to_signal('updating', self.__update_started) self.engine.connect_to_signal('complete', self.__update_done) self.engine.connect_to_signal('complete', self.tree.select_current) # might want to highlight these a bit more self.engine.connect_to_signal('warning', self.status.warning) def __import_feeds(self, *args): dialog = Gtk.FileChooserDialog(_("Open..."), self, Gtk.FileChooserAction.OPEN, (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK)) dialog.set_default_response(Gtk.ResponseType.OK) filter = Gtk.FileFilter() filter.set_name("opml/xml") filter.add_pattern("*.opml") filter.add_pattern("*.xml") dialog.add_filter(filter) filter = Gtk.FileFilter() filter.set_name(_("All files")) filter.add_pattern("*") dialog.add_filter(filter) response = dialog.run() filename = dialog.get_filename() dialog.destroy() if response == Gtk.ResponseType.OK: self.log.debug("{0}: Trying to import from OPML file {1}".format(self, filename)) self.import_opml(filename, reply_handler=self.__to_log, error_handler=self.__to_log) def __export_feeds(self, *args): dialog = Gtk.FileChooserDialog(_("Save..."), self, Gtk.FileChooserAction.SAVE, (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.OK)) dialog.set_default_response(Gtk.ResponseType.OK) dialog.set_do_overwrite_confirmation(True) dialog.set_current_name("brss.opml") filter = Gtk.FileFilter() filter.set_name("opml/xml") filter.add_pattern("*.opml") filter.add_pattern("*.xml") dialog.add_filter(filter) response = dialog.run() filename = dialog.get_filename() dialog.destroy() if response == Gtk.ResponseType.OK: self.log.debug("{0}: Trying to export to OPML file {1}".format(self, filename)) self.export_opml(filename, reply_handler=self.__to_log, error_handler=self.__to_log) def __populate_menu(self, *args): self.log.debug("{0}: Populating menu".format(self)) self.get_menu_items( reply_handler=self.tree.fill_menu, error_handler=self.__to_log) self.count_special( reply_handler=self.tree.make_special_folders, error_handler=self.__to_log) def __toggle_starred(self, ilist, item): self.toggle_starred(item, reply_handler=self.__to_log, error_handler=self.__to_log) def __toggle_read(self, ilist, item): self.toggle_read(item, reply_handler=self.__to_log, error_handler=self.__to_log) def __load_articles(self, tree, item): self.log.debug("{0}: Loading articles for feed {1}".format(self, item['name'])) self.get_articles_for(item, reply_handler=self.ilist.load_list, error_handler=self.__to_log) def __add_category(self, *args): args = [ {'type':'str','name':'name', 'header':_('Name') }, ] d = Dialog(self, _('Add a category'), args) r = d.run() item = d.response item['type'] = 'category' d.destroy() if r == Gtk.ResponseType.OK: self.__create(item) def __add_feed(self, *args): data = [ {'type':'str','name':'url', 'header':_('Link') }, ] d = Dialog(self, _('Add a feed'), data) r = d.run() item = d.response item['type'] = 'feed' d.destroy() if r == Gtk.ResponseType.OK: self.__create(item) def __edit_prefs(self, *args): kmap = { 'hide-read':'bool', 'update-interval':'int', 'max-articles':'int', 'use-notify':'bool', 'on-the-fly':'bool', 'enable-debug':'bool', 'auto-update':'bool', 'live-search':'bool', 'auto-hide-search':'bool', 'show-status':'bool', } hmap = { 'hide-read':_('Hide Read Items'), 'update-interval':_('Update interval (in minutes)'), 'max-articles':_('Maximum number of articles to keep (excluding starred)'), 'auto-update':_('Allow the engine to download new articles automatically.'), 'on-the-fly':_('Start downloading articles for new feeds on-the-fly'), 'use-notify':_('Show notification on updates'), 'enable-debug':_('Enable detailed logs'), 'live-search':_('Return search results as you type'), 'auto-hide-search':_('Hide Search form on results'), 'show-status':_('Show the bottom status bar'), } data = [] for k,v in kmap.iteritems(): data.append({ 'type':v, 'name':k, 'header':hmap.get(k),#FIXME: can this be gotten from gsettings? }) d = Dialog(self, _('Edit preferences'), data, self.settings) r = d.run() d.destroy() def __about(self, *args): """Shows the about message dialog""" LICENSE = """ This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. """ about = Gtk.AboutDialog() about.set_transient_for(self) about.set_program_name("BRss") about.set_version(__version__) about.set_authors(__maintainers__) about.set_artists(__maintainers__) about.set_copyright("(c) 2011 ITGears") about.set_license(LICENSE) about.set_comments(_("BRss is an offline DBus-based RSS reader")) about.set_logo(make_pixbuf('brss')) about.run() about.destroy() def __create(self, item): self.log.debug("About to create item: {0}".format(item)) self.create(item, reply_handler=self.__to_log, error_handler=self.__to_log) def __edit_item(self, *args): item = self.tree.current_item #FIXME: not good!! print item if item['type'] == 'category': args = [{'type':'str','name':'name', 'header':_('Name'), 'value':item['name'] },] d = Dialog(self, _('Edit this category'), args) elif item['type'] == 'feed': args = [{'type':'str','name':'url', 'header':_('Link'), 'value':item['url'] },] d = Dialog(self, _('Edit this feed'), args) r = d.run() o = d.response for k,v in o.iteritems(): item[k] = v d.destroy() if r == Gtk.ResponseType.OK: self.__edit(item) def __edit(self, item): self.log.debug("About to edit item: {0}".format(item)) self.edit(item, reply_handler=self.__to_log, error_handler=self.__to_log) def __update(self, item): self.log.debug("{0} Requesting update for: {1}".format(self, item)) self.update(item, reply_handler=self.__to_log, error_handler=self.__to_log) def __update_all(self, *args): self.__update('all') def __update_feed(self, item): self.__update(self.tree.current_item) def __delete_item(self, *args): self.__delete(self.tree.current_item) def __delete(self, item): self.log.debug("About to delete item: {0}".format(item)) self.alert.question(_("Are you sure you want to delete this {0} ?".format(item['type'])), _("All included feeds and articles will be deleted.") ) if self.alert.checksum: self.log.debug("Deletion confirmed") self.delete(item, reply_handler=self.__populate_menu, error_handler=self.__to_log) def __load_article(self, ilist, item): self.get_article(item, reply_handler=self.view.show_article, error_handler=self.__to_log) def __handle_added(self, item): self.log.debug("{0}: Added item: {1}".format(self, item['id'])) if item['type'] in ['feed', 'category']: return self.tree.insert_row(item) if item['type'] == 'article': return self.ilist.insert_row(item) def __handle_updated(self, item): #~ self.log.debug("{0}: Updated item: {1}".format(self, item['id'])) if item['type'] in ['feed', 'category']: return self.tree.update_row(item) if item['type'] == 'article': self.view.star_this(item) return self.ilist.update_row(item) def __handle_dcall(self, caller, name, item): if name in [_('Update'), _('Update all')]: self.log.debug("updating {0}".format(item)) self.update(item, reply_handler=self.__update_done, error_handler=self.__to_log) elif name == _('Mark all as read'): self.ilist.mark_all_read() elif name == _('Open in Browser'): self.__to_browser(caller, item['url']) elif name == _('Copy Url to Clipboard'): self.__to_clipboard(item['url']) elif name in [_('Delete'), _('Delete Feed'), _('Delete Category')]: self.__delete(item) elif name == _('Edit'): self.__edit_item(item) def __search_articles(self, caller, string): self.log.debug("Searching articles with: {0}".format(string.encode('utf-8'))) self.search_for(string, reply_handler=self.ilist.load_list, error_handler=self.__to_log) def __toggle_in_update(self, b): gmap = {True:False, False:True} a = self.ag.get_action('StopUpdate') a.set_sensitive(b) a = self.ag.get_action('Update all') a.set_sensitive(gmap.get(b)) def __toggle_search(self, *args): # show ilist if in fullscreen self.ilist.toggle_search() if self.ilist.search_on: if self.is_fullscreen: self.ilist.show() else: if self.is_fullscreen: self.ilist.hide() else: self.ilist.listview.grab_focus() def __hide_search(self, *args): if not self.settings.get_boolean('live-search'): if self.ilist.search_on and self.settings.get_boolean('auto-hide-search'): w = self.ag.get_action('Find') if w.get_sensitive(): w.activate() def __star(self, *args): self.ilist.mark_starred() def __read(self, *args): self.ilist.toggle_read() def __update_started(self, *args): self.log.debug("Toggling update status to True") self.__toggle_in_update(True) def __update_done(self, *args): self.log.debug("Toggling update status to False") self.__toggle_in_update(False) def __update_title(self, caller, item): self.set_title(item['title']) def __status_info(self, caller, message=None): if message: self.status.message('info', message) else: self.status.clear() def __to_log(self, *args): for a in args: self.log.warning(a) if type(a) == dbus.exceptions.DBusException: self.emit('no-engine') def __to_browser(self, caller, link): self.log.debug("Trying to open link '{0}' in browser".format(link)) orig_link = self.view.link_button.get_uri() self.view.link_button.set_uri(link) self.view.link_button.activate() self.view.link_button.set_uri(orig_link) def __previous_article(self, *args): self.emit('previous-article') def __next_article(self, *args): self.emit('next-article') def __previous_feed(self, *args): self.emit('previous-feed') def __next_feed(self, *args): self.emit('next-feed') def __to_clipboard(self, link): clipboard = Gtk.Clipboard() clipboard.set_text(link.encode("utf8"), -1) clipboard.store() def __connect_accels (self, widget): widget.filterentry.connect('focus-in-event', self.__toggle_accels, False) widget.filterentry.connect('focus-out-event', self.__toggle_accels, True) widget.filterentry.grab_focus() def __toggle_accels(self, widget, event, b): n = self.ag.get_action('NextArticle') nf = self.ag.get_action('NextFeed') p = self.ag.get_action('PreviousArticle') pf = self.ag.get_action('PreviousFeed') s = self.ag.get_action('Star') r = self.ag.get_action('Read') if b: self.log.debug("Toggling accels on") for acc in [n, nf, p, pf, s, r]: acc.connect_accelerator() else: self.log.debug("Toggling accels off") for acc in [n, nf, p, pf, s, r]: acc.disconnect_accelerator() def __toggle_status(self, settings, key=None): if settings.get_boolean(key): self.log.debug('{0}: showing the status bar'.format(self)) self.status.show() else: self.log.debug('{0}: hiding the status bar'.format(self)) self.status.hide() def __toggle_fullscreen(self, *args): if self.is_fullscreen == True: self.ilist.show() self.tree.show() self.unfullscreen() self.is_fullscreen = False else: self.ilist.hide() self.tree.hide() self.fullscreen() self.is_fullscreen = True def __reconnect(self, *args): self.log.warning("Trying to reconnect to engine!") self.__get_engine() self.emit('loaded') def __no_engine(self, *args): self.log.warning("Lost connection with engine!") self.status.message('critical', _("Cannot connect to the Feed Engine")) self.log.debug("Showing reconnect icon!") self.rag.set_visible(True) self.ag.set_visible(False) self.__update_done() #~ def __feed_selected(self, caller, item): #~ self.status.message('info', "{0}".format( #~ item['name'].encode('utf-8'))) def quit(self, *args): self.destroy() def start(self, *args): self.present() def do_loaded(self, *args): self.log.debug("Starting BRss Reader") self.__populate_menu() self.status.message('ok', _('Connected to engine'))