class ComicsViewer(ScalableWindow): """A program to display image feeds.""" __name__ = "Comics!" __version__ = "1.0" __author__ = "Moses" __gsignals__ = { "removed": (gobject.SIGNAL_RUN_FIRST, None, ()), "updated": (gobject.SIGNAL_RUN_FIRST, None, (str,)), } ######################################################################## # Helper methods # ######################################################################## def set_visibility(self, visible): """Hides or unhides the window. If Compiz is running and the Widget plugin is loaded, the window is sent to the widget layer. Makes sure that the window is visible on screen.""" if visible: screen = self.get_screen() screen_width, screen_height = (screen.get_width(), screen.get_height()) w, h = self.get_size() x = self.__settings.get_int("x", (screen_width - w) / 2) y = self.__settings.get_int("y", (screen_height - h) / 2) # Show the window first... self.show() x %= screen_width if x < 0: x += screen_width y %= screen_height if y < 0: y += screen_height # this must be between self.show() and self.move() # it calls self.get_position(), without it the comic # will not be painted the very first time self.save_settings() # ...and then move it self.move(x, y) if has_widget_layer(): compiz_widget_set(self, False) else: if self.get_window(): self.save_settings() if has_widget_layer(): self.show() compiz_widget_set(self, True) else: self.hide() def close(self): self.destroy() def get_menu_item_name(self, item): """Return the menu item name of item.""" if item[DATE] > 0: tt = time.localtime(item[DATE]) # Translators: This is a date/time format string. You can check the # output in terminal with 'date +"%%A %d %B"'. return time.strftime(_("%A %d %B"), tt) else: return item[TITLE] def get_link_name(self, item): """Return the link name of item.""" return item[TITLE] def get_link_dimensions(self): """Return the dimensions of the link.""" if self.__link and self.show_link: size = self.__link.rsize if -1 in size: return (0, 0) else: return size else: return (0, 0) def get_ticker_dimensions(self): """Return the dimensions of the ticker.""" if self.__ticker: return self.__ticker.rsize else: return (0, 0) def get_image_dimensions(self): """Return the dimensions of the current image.""" if self.__pixbuf: return (self.__pixbuf.get_width(), self.__pixbuf.get_height()) elif self.__is_error: return ERROR_IMAGE.get_dimension_data()[:2] else: return DEFAULT_IMAGE.get_dimension_data()[:2] def get_window_dimensions(self): """Get the required size of the window.""" l_dim = self.get_link_dimensions() t_dim = self.get_ticker_dimensions() i_dim = self.get_image_dimensions() c_dim = max(i_dim[0], l_dim[0] + 2.0 * (t_dim[0] + TICKER_LINK_SEPARATION)), i_dim[1] return (c_dim[0] + self.__border[0] + self.__border[2], c_dim[1] + self.__border[1] + self.__border[3]) def rescale_window(self): """Change the scale of the window so that the window will fit on screen.""" screen = self.get_screen() screen_size = screen.get_width(), screen.get_height() w_dim = self.canvas_size scale = None if w_dim[0] > screen_size[0]: scale = 0.95 * float(screen_size[0]) / w_dim[0] if w_dim[1] > screen_size[1]: if not scale or scale > screen_size[1] / w_dim[1]: scale = 0.95 * float(screen_size[1]) / w_dim[1] if scale: self.set_scale(scale) def update_size(self): """Update the size of the window and place controls.""" l_dim = self.get_link_dimensions() t_dim = self.get_ticker_dimensions() i_dim = self.get_image_dimensions() c_dim = (max(i_dim[0], l_dim[0] + 2.0 * (t_dim[0] + TICKER_LINK_SEPARATION)), i_dim[1]) w_dim = (c_dim[0] + self.__border[0] + self.__border[2], c_dim[1] + self.__border[1] + self.__border[3]) # Resize window self.set_canvas_size(w_dim) self.rescale_window() # Place link if self.__link: self.move_child(self.__link, (w_dim[0] - l_dim[0]) / 2, w_dim[1] - (self.__border[3] + l_dim[1]) / 2 - 1) # Place ticker if self.__ticker: self.move_child(self.__ticker, TICKER_DISTANCE, w_dim[1] - t_dim[1] - TICKER_DISTANCE) def select_item(self, item): """Select a strip for downloading.""" self.__ticker.set_ticking(True) if self.__download_id and self.__downloader: self.__downloader.disconnect(self.__download_id) self.__download_id = None self.__downloader = Downloader(item[URL], self.__settings["cache-file"]) self.__download_id = self.__downloader.connect("completed", self.on_download_completed, item) self.__downloader.download() def draw_frame(self, ctx, p1, p2, rad): """Trace a rectangle with rounded corners.""" ctx.set_line_cap(cairo.LINE_CAP_SQUARE) # Top-left if rad[0] > 0: ctx.arc(p1[0] + rad[0], p1[1] + rad[0], rad[0], pi, 3 * pi / 2) else: ctx.move_to(*p1) # Top-right if rad[1] > 0: ctx.arc(p2[0] - rad[1], p1[1] + rad[1], rad[1], 3 * pi / 2, 0) else: ctx.line_to(p2[0], p1[1]) # Bottom-right if rad[2] > 0: ctx.arc(p2[0] - rad[2], p2[1] - rad[2], rad[2], 0, pi / 2) else: ctx.line_to(*p2) # Bottom-left if rad[3] > 0: ctx.arc(p1[0] + rad[3], p2[1] - rad[3], rad[3], pi / 2, pi) else: ctx.line_to(p1[0], p2[1]) # Close the path to draw the last line ctx.close_path() def draw_window(self, ctx): """Draw the window on which the image is painted.""" ctx.save() # Define shape w_dim = self.get_window_dimensions() self.draw_frame(ctx, (1, 1), (w_dim[0] - 1, w_dim[1] - 1), self.__border_radii) ctx.set_source_rgb(*WINDOW_COLOR) ctx.fill_preserve() ctx.set_source_rgb(*BORDER_COLOR) ctx.stroke() ctx.restore() def draw_image(self, ctx): """Draw the image.""" ctx.save() w_dim = self.get_window_dimensions() i_dim = self.get_image_dimensions() x = (w_dim[0] - i_dim[0]) / 2 # Has an image been downloaded? if self.__pixbuf: dim = self.get_image_dimensions() ctx.set_source_pixbuf(self.__pixbuf, x, self.__border[1]) ctx.rectangle(x, self.__border[1], dim[0], dim[1]) ctx.clip() ctx.paint() # Has an error occurred? elif self.__is_error: ctx.set_operator(cairo.OPERATOR_OVER) ctx.translate(x, self.__border[1]) ERROR_IMAGE.render_cairo(ctx) # Otherwise draw the default image else: ctx.set_operator(cairo.OPERATOR_OVER) ctx.translate(x, self.__border[1]) DEFAULT_IMAGE.render_cairo(ctx) ctx.restore() def make_menu(self): """Create the context menu.""" menu = gtk.Menu() # Generate history menu if len(self.feeds.feeds[self.feed_name].items) > 1: history_container = gtk.ImageMenuItem(gtk.STOCK_JUMP_TO) history_menu = gtk.Menu() history_menu.foreach(lambda child: history_menu.remove(child)) items = self.feeds.feeds[self.feed_name].items.items() items.sort(reverse=True) for date, item in items: label = gtk.Label() text = self.get_menu_item_name(item) if self.__current_timestamp == date: label.set_markup("<b>" + text + "</b>") else: label.set_markup(text) align = gtk.Alignment(xalign=0.0) align.add(label) menu_item = gtk.MenuItem() menu_item.data = item menu_item.connect("activate", self.on_history_activated) menu_item.add(align) history_menu.append(menu_item) history_container.set_submenu(history_menu) menu.append(history_container) size_container = gtk.MenuItem(_("Size")) size_menu = gtk.Menu() zoom_normal_item = gtk.ImageMenuItem(gtk.STOCK_ZOOM_100) zoom_normal_item.connect("activate", self.on_normal_activated) zoom_in_item = gtk.ImageMenuItem(gtk.STOCK_ZOOM_IN) zoom_in_item.connect("activate", self.on_larger_activated) zoom_out_item = gtk.ImageMenuItem(gtk.STOCK_ZOOM_OUT) zoom_out_item.connect("activate", self.on_smaller_activated) size_menu.append(zoom_normal_item) size_menu.append(zoom_in_item) size_menu.append(zoom_out_item) size_container.set_submenu(size_menu) menu.append(size_container) show_link_item = gtk.CheckMenuItem(_("Show link")) show_link_item.set_active(self.show_link) show_link_item.connect("toggled", self.on_show_link_toggled) menu.append(show_link_item) save_as_item = gtk.ImageMenuItem(stock_id="gtk-save-as") save_as_item.set_sensitive(not self.__pixbuf is None) save_as_item.connect("activate", self.on_save_as_activated) menu.append(save_as_item) close_item = gtk.ImageMenuItem(stock_id="gtk-close") close_item.connect("activate", self.on_close_activated) menu.append(close_item) menu.show_all() return menu ######################################################################## # Standard python methods # ######################################################################## def __init__(self, applet, settings, visible=False): """Create a new ComicsView instance.""" super(ComicsViewer, self).__init__() self.applet = applet self.__settings = settings # Initialize fields self.feeds = applet.feeds self.__update_id = None self.__download_id = None self.__downloader = None try: self.__pixbuf = gdk.pixbuf_new_from_file(settings["cache-file"]) except Exception: self.__pixbuf = None self.__is_error = False self.__link = WWWLink("", "", LINK_FONTSIZE) self.__link.connect("size-allocate", self.on_link_size_allocate) self.__ticker = Ticker((20.0, 20.0)) self.__current_timestamp = 0.0 self.__border = BORDER self.__border_radii = BORDER_RADII # Connect events self.connect("destroy", self.on_destroy) self.feeds.connect("feed-changed", self.on_feed_changed) # Build UI self.__link.connect("button-press-event", self.on_link_clicked) self.put_child(self.__link, 0, 0) self.put_child(self.__ticker, 0, 0) self.__ticker.show() self.set_skip_taskbar_hint(True) self.set_skip_pager_hint(True) self.load_settings() if visible: self.set_visibility(visible) ######################################################################## # Property updating methods # ######################################################################## def load_settings(self): """Load the settings.""" self.set_show_link(self.__settings.get_bool("show_link", False)) self.set_feed_name(self.__settings.get_string("feed_name", "")) def save_settings(self): """Save the settings.""" x, y = self.get_position() self.__settings["x"] = x self.__settings["y"] = y self.__settings.save() def set_feed_name(self, new_feed_name): """Set the name of the feed to use.""" if self.__update_id: self.feeds.feeds[self.feed_name].disconnect(self.__update_id) self.__update_id = None if new_feed_name in self.feeds.feeds: self.feed_name = new_feed_name self.__update_id = self.feeds.feeds[self.feed_name].connect("updated", self.on_feed_updated) if self.feeds.feeds[self.feed_name].status == Feed.DOWNLOAD_OK: self.on_feed_updated(self.feeds.feeds[self.feed_name], Feed.DOWNLOAD_OK) self.__settings["feed_name"] = str(new_feed_name) elif len(self.feeds.feeds) > 0: self.set_feed_name(self.feeds.feeds.keys()[0]) def set_show_link(self, new_show_link): """Show or hide the link label.""" self.show_link = new_show_link if self.show_link: self.__link.show() self.__border = LINK_BORDER self.__border_radii = LINK_BORDER_RADII else: self.__link.hide() self.__border = BORDER self.__border_radii = BORDER_RADII self.__settings["show_link"] = str(new_show_link) self.update_size() ######################################################################## # Event hooks # ######################################################################## def on_destroy(self, widget): self.emit("removed") self.__settings.delete() if self.__update_id: self.feeds.feeds[self.feed_name].disconnect(self.__update_id) self.__update_id = None if self.__download_id and self.__downloader: self.__downloader.disconnect(self.__download_id) self.__download_id = None del self.__pixbuf def on_link_clicked(self, widget, e): # Start the web browser in another process os.system("%s %s &" % (BROWSER_COMMAND, widget.url)) def on_history_activated(self, widget): # The widget is a GtkMenuItem, where data is a str self.select_item(widget.data) def on_normal_activated(self, widget): self.set_scale(1.0) def on_larger_activated(self, widget): self.set_scale(self.scale * 1.2) def on_smaller_activated(self, widget): self.set_scale(self.scale / 1.2) def on_show_link_toggled(self, widget): self.set_show_link(widget.get_active()) def on_close_activated(self, widget): self.close() def on_save_as_activated(self, widget): """Run FileChooserDialog and save file.""" self.dialog = gtk.FileChooserDialog( _("Save comic as…"), buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_SAVE, gtk.RESPONSE_OK), action=gtk.FILE_CHOOSER_ACTION_SAVE, ) self.dialog.set_icon_from_file(os.path.join(UI_DIR, "comics.svg")) self.dialog.set_do_overwrite_confirmation(True) self.dialog.set_current_name(self.__link.text + ".jpg") # Set filters, default jpg, without ico for format in gdk.pixbuf_get_formats(): if format["is_writable"] and format["name"] != "ico": ff = gtk.FileFilter() ff.set_name(format["description"]) for i in format["mime_types"]: ff.add_mime_type(i) self.dialog.add_filter(ff) if format["name"] == "jpeg": self.dialog.set_filter(ff) self.dialog.connect("notify::filter", self.on_filter_changed) if self.dialog.run() == gtk.RESPONSE_OK: ff = gtk.FileFilter() ff = self.dialog.get_filter() name = ff.get_name() for format in gdk.pixbuf_get_formats(): if format["description"] == name: name = format["name"] break try: self.__pixbuf.save(self.dialog.get_filename(), name) except Exception: self.applet.show_message( _("Failed to save <i>%s</i>.") % self.dialog.get_filename(), gtk.STOCK_DIALOG_ERROR ) self.dialog.destroy() def on_filter_changed(self, pspec, data): """Change filename extension.""" current_name = self.dialog.get_filename().rsplit(".", 1) if len(current_name) == 2: ff = gtk.FileFilter() ff = self.dialog.get_filter() for i in gdk.pixbuf_get_formats(): if i["description"] == ff.get_name(): for ext in i["extensions"]: if current_name[1] == ext: return self.dialog.set_current_name("%s.%s" % (os.path.basename(current_name[0]), i["extensions"][0])) def on_link_size_allocate(self, widget, e): i_dim = self.get_image_dimensions() l_dim = self.get_link_dimensions() t_dim = self.get_ticker_dimensions() c_dim = (max(i_dim[0], l_dim[0] + 2.0 * (t_dim[0] + TICKER_LINK_SEPARATION)), i_dim[1]) w_dim = (c_dim[0] + self.__border[0] + self.__border[2], c_dim[1] + self.__border[1] + self.__border[3]) # Place link if self.show_link: self.move_child(self.__link, (w_dim[0] - l_dim[0]) / 2, w_dim[1] - (self.__border[3] + l_dim[1]) / 2 - 1) # Does the window need to be resized? if l_dim[0] > i_dim[0]: self.set_canvas_size(w_dim) def on_widget_show(self, widget): """Set the compiz widget property on all top-level windows.""" compiz_widget_set(widget, compiz_widget_get(self)) def on_draw_background(self, ctx): """Draw the window.""" self.draw_window(ctx) self.draw_image(ctx) def on_feed_updated(self, feed, result): """The feed has been updated.""" if result == Feed.DOWNLOAD_OK: # Only emit the updated signal when there actually is an update if self.__current_timestamp != 0.0: self.emit("updated", feed.items[feed.newest][TITLE]) if feed.newest in feed.items: self.select_item(feed.items[feed.newest]) def on_download_completed(self, o, code, item): """A new image has been downloaded.""" self.__ticker.set_ticking(False) self.__is_error = code != Downloader.OK self.__current_timestamp = item[DATE] self.__link.set_text(self.get_link_name(item)) self.__link.set_url(item[LINK]) self.__downloader = None self.__download_id = None if not self.__is_error: try: del self.__pixbuf except AttributeError: # Received destroy signal after download was initiated return try: self.__pixbuf = gdk.pixbuf_new_from_file(o.filename) except gobject.GError: self.__pixbuf = None self.__is_error = True self.update_size() def on_feed_changed(self, feeds, feed_name, action): """A feed has been changed.""" if action == FeedContainer.FEED_REMOVED: if self.feed_name == feed_name: self.__settings.delete() self.destroy()