class AlbumArtWindow(qltk.Window, PluginConfigMixin): """The main window including the search list""" CONFIG_SECTION = PLUGIN_CONFIG_SECTION THUMB_SIZE = 50 def __init__(self, songs): super().__init__() self.image_cache = [] self.image_cache_size = 10 self.search_lock = False self.set_title(_('Album Art Downloader')) self.set_icon_name(Icons.EDIT_FIND) self.set_default_size(800, 550) image = CoverArea(self, songs[0]) self.liststore = Gtk.ListStore(object, object) self.treeview = treeview = AllTreeView(model=self.liststore) self.treeview.set_headers_visible(False) self.treeview.set_rules_hint(True) targets = [("text/uri-list", 0, 0)] targets = [Gtk.TargetEntry.new(*t) for t in targets] treeview.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, targets, Gdk.DragAction.COPY) treeselection = self.treeview.get_selection() treeselection.set_mode(Gtk.SelectionMode.SINGLE) treeselection.connect('changed', self.__select_callback, image) self.treeview.connect("drag-data-get", self.__drag_data_get, treeselection) rend_pix = Gtk.CellRendererPixbuf() img_col = Gtk.TreeViewColumn('Thumb') img_col.pack_start(rend_pix, False) def cell_data_pb(column, cell, model, iter_, *args): surface = model[iter_][0] cell.set_property("surface", surface) img_col.set_cell_data_func(rend_pix, cell_data_pb, None) treeview.append_column(img_col) rend_pix.set_property('xpad', 2) rend_pix.set_property('ypad', 2) border_width = self.get_scale_factor() * 2 rend_pix.set_property('width', self.THUMB_SIZE + 4 + border_width) rend_pix.set_property('height', self.THUMB_SIZE + 4 + border_width) def escape_data(data): for rep in ('\n', '\t', '\r', '\v'): data = data.replace(rep, ' ') return util.escape(' '.join(data.split())) def cell_data(column, cell, model, iter, data): cover = model[iter][1] esc = escape_data txt = '<b><i>%s</i></b>' % esc(cover['name']) txt += "\n<small>%s</small>" % ( _('from %(source)s') % { "source": util.italic(esc(cover['source'])) }) if 'resolution' in cover: txt += "\n" + _('Resolution: %s') % util.italic( esc(cover['resolution'])) if 'size' in cover: txt += "\n" + _('Size: %s') % util.italic(esc(cover['size'])) cell.markup = txt cell.set_property('markup', cell.markup) rend = Gtk.CellRendererText() rend.set_property('ellipsize', Pango.EllipsizeMode.END) info_col = Gtk.TreeViewColumn('Info', rend) info_col.set_cell_data_func(rend, cell_data) treeview.append_column(info_col) sw_list = Gtk.ScrolledWindow() sw_list.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) sw_list.set_shadow_type(Gtk.ShadowType.IN) sw_list.add(treeview) search_labelraw = Gtk.Label('raw') search_labelraw.set_alignment(xalign=1.0, yalign=0.5) self.search_fieldraw = Gtk.Entry() self.search_fieldraw.connect('activate', self.start_search) self.search_fieldraw.connect('changed', self.__searchfieldchanged) search_labelclean = Gtk.Label('clean') search_labelclean.set_alignment(xalign=1.0, yalign=0.5) self.search_fieldclean = Gtk.Label() self.search_fieldclean.set_can_focus(False) self.search_fieldclean.set_alignment(xalign=0.0, yalign=0.5) self.search_radioraw = Gtk.RadioButton(group=None, label=None) self.search_radioraw.connect("toggled", self.__searchtypetoggled, "raw") self.search_radioclean = Gtk.RadioButton(group=self.search_radioraw, label=None) self.search_radioclean.connect("toggled", self.__searchtypetoggled, "clean") #note: set_active(False) appears to have no effect #self.search_radioraw.set_active( # self.config_get_bool('searchraw', False)) if self.config_get_bool('searchraw', False): self.search_radioraw.set_active(True) else: self.search_radioclean.set_active(True) search_labelresultsmax = Gtk.Label('limit') search_labelresultsmax.set_alignment(xalign=1.0, yalign=0.5) search_labelresultsmax.set_tooltip_text( _("Per engine 'at best' results limit")) search_adjresultsmax = Gtk.Adjustment(value=int( self.config_get("resultsmax", 3)), lower=1, upper=REQUEST_LIMIT_MAX, step_incr=1, page_incr=0, page_size=0) self.search_spinresultsmax = Gtk.SpinButton( adjustment=search_adjresultsmax, climb_rate=0.2, digits=0) self.search_spinresultsmax.set_alignment(xalign=0.5) self.search_spinresultsmax.set_can_focus(False) self.search_button = Button(_("_Search"), Icons.EDIT_FIND) self.search_button.connect('clicked', self.start_search) search_button_box = Gtk.Alignment() search_button_box.set(1, 0, 0, 0) search_button_box.add(self.search_button) search_table = Gtk.Table(rows=3, columns=4, homogeneous=False) search_table.attach(search_labelraw, 0, 1, 0, 1, xoptions=Gtk.AttachOptions.FILL, xpadding=6) search_table.attach(self.search_radioraw, 1, 2, 0, 1, xoptions=0, xpadding=0) search_table.attach(self.search_fieldraw, 2, 4, 0, 1) search_table.attach(search_labelclean, 0, 1, 1, 2, xoptions=Gtk.AttachOptions.FILL, xpadding=6) search_table.attach(self.search_radioclean, 1, 2, 1, 2, xoptions=0, xpadding=0) search_table.attach(self.search_fieldclean, 2, 4, 1, 2, xpadding=4) search_table.attach(search_labelresultsmax, 0, 2, 2, 3, xoptions=Gtk.AttachOptions.FILL, xpadding=6) search_table.attach(self.search_spinresultsmax, 2, 3, 2, 3, xoptions=Gtk.AttachOptions.FILL, xpadding=0) search_table.attach(search_button_box, 3, 4, 2, 3) widget_space = 5 self.progress = Gtk.ProgressBar() left_vbox = Gtk.VBox(spacing=widget_space) left_vbox.pack_start(search_table, False, True, 0) left_vbox.pack_start(sw_list, True, True, 0) hpaned = Paned() hpaned.set_border_width(widget_space) hpaned.pack1(left_vbox, shrink=False) hpaned.pack2(image, shrink=False) hpaned.set_position(275) self.add(hpaned) self.show_all() left_vbox.pack_start(self.progress, False, True, 0) self.connect('destroy', self.__save_config) song = songs[0] text = SEARCH_PATTERN.format(song) self.set_text(text) self.start_search() def __save_config(self, widget): self.config_set('searchraw', self.search_radioraw.get_active()) self.config_set('resultsmax', self.search_spinresultsmax.get_value_as_int()) def __drag_data_get(self, view, ctx, sel, tid, etime, treeselection): model, iter = treeselection.get_selected() if not iter: return cover = model.get_value(iter, 1) sel.set_uris([cover['cover']]) def __searchfieldchanged(self, *data): search = data[0].get_text() clean = cleanup_query(search, ' ') self.search_fieldclean.set_text('<b>' + clean + '</b>') self.search_fieldclean.set_use_markup(True) def __searchtypetoggled(self, *data): self.config_set('searchraw', self.search_radioraw.get_active()) def start_search(self, *data): """Start the search using the text from the text entry""" text = self.search_fieldraw.get_text() if not text or self.search_lock: return self.search_lock = True self.search_button.set_sensitive(False) self.progress.set_fraction(0) self.progress.set_text(_(u'Searching…')) self.progress.show() self.liststore.clear() self.search = search = CoverSearch(self.__search_callback) for eng in ENGINES: if self.config_get_bool(CONFIG_ENG_PREFIX + eng['config_id'], True): search.add_engine(eng['class'], eng['replace']) raw = self.search_radioraw.get_active() limit = self.search_spinresultsmax.get_value_as_int() search.start(text, raw, limit) # Focus the list self.treeview.grab_focus() self.connect("destroy", self.__destroy) def __destroy(self, *args): self.search.stop() def set_text(self, text): """set the text and move the cursor to the end""" self.search_fieldraw.set_text(text) self.search_fieldraw.emit('move-cursor', Gtk.MovementStep.BUFFER_ENDS, 0, False) def __select_callback(self, selection, image): model, iter = selection.get_selected() if not iter: return cover = model.get_value(iter, 1) image.set_cover(cover['cover']) def __add_cover_to_list(self, cover): try: pbloader = GdkPixbuf.PixbufLoader() pbloader.write(get_url(cover['thumbnail'])) pbloader.close() scale_factor = self.get_scale_factor() size = self.THUMB_SIZE * scale_factor - scale_factor * 2 pixbuf = pbloader.get_pixbuf().scale_simple( size, size, GdkPixbuf.InterpType.BILINEAR) pixbuf = add_border_widget(pixbuf, self) surface = get_surface_for_pixbuf(self, pixbuf) except (GLib.GError, IOError): pass else: def append(data): self.liststore.append(data) GLib.idle_add(append, [surface, cover]) def __search_callback(self, covers, progress): for cover in covers: self.__add_cover_to_list(cover) if self.progress.get_fraction() < progress: self.progress.set_fraction(progress) if progress >= 1: self.progress.set_text(_('Done')) GLib.timeout_add(700, self.progress.hide) self.search_button.set_sensitive(True) self.search_lock = False
def __init__(self, parent, library): super(EditTags, self).__init__(spacing=12) self.title = _("Edit Tags") self.set_border_width(12) model = ObjectStore() view = RCMHintedTreeView(model=model) self._view = view selection = view.get_selection() render = Gtk.CellRendererPixbuf() column = TreeViewColumn() column.pack_start(render, True) column.set_fixed_width(24) column.set_expand(False) def cdf_write(col, rend, model, iter_, *args): entry = model.get_value(iter_) rend.set_property('sensitive', entry.edited or entry.deleted) if entry.canedit or entry.deleted: if entry.deleted: rend.set_property('icon-name', Icons.EDIT_DELETE) else: rend.set_property('icon-name', Icons.EDIT) else: rend.set_property('icon-name', Icons.CHANGES_PREVENT) column.set_cell_data_func(render, cdf_write) view.append_column(column) render = Gtk.CellRendererText() column = TreeViewColumn(title=_('Tag')) column.pack_start(render, True) def cell_data_tag(column, cell, model, iter_, data): entry = model.get_value(iter_) cell.set_property("text", entry.tag) cell.set_property("strikethrough", entry.deleted) column.set_cell_data_func(render, cell_data_tag) column.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) render.set_property('editable', True) render.connect('edited', self.__edit_tag_name, model) render.connect('editing-started', self.__tag_editing_started, model, library) view.append_column(column) render = Gtk.CellRendererText() render.set_property('ellipsize', Pango.EllipsizeMode.END) render.set_property('editable', True) render.connect('edited', self.__edit_tag, model) render.connect('editing-started', self.__value_editing_started, model, library) column = TreeViewColumn(title=_('Value')) column.pack_start(render, True) def cell_data_value(column, cell, model, iter_, data): entry = model.get_value(iter_) markup = entry.value.get_markup() cell.markup = markup cell.set_property("markup", markup) cell.set_property("editable", entry.canedit) cell.set_property("strikethrough", entry.deleted) column.set_cell_data_func(render, cell_data_value) column.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) view.append_column(column) sw = Gtk.ScrolledWindow() sw.set_shadow_type(Gtk.ShadowType.IN) sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) sw.add(view) self.pack_start(sw, True, True, 0) cb = ConfigCheckButton( _("Show _programmatic tags"), 'editing', 'alltags', populate=True, tooltip=_("Access all tags, including machine-generated " "ones e.g. MusicBrainz or Replay Gain tags")) cb.connect('toggled', self.__all_tags_toggled) self.pack_start(cb, False, True, 0) # Add and Remove [tags] buttons buttonbox = Gtk.HBox(spacing=18) bbox1 = Gtk.HButtonBox() bbox1.set_spacing(6) bbox1.set_layout(Gtk.ButtonBoxStyle.START) add = qltk.Button(_("_Add"), Icons.LIST_ADD) add.set_focus_on_click(False) self._add = add add.connect('clicked', self.__add_tag, model, library) bbox1.pack_start(add, True, True, 0) # Remove button remove = qltk.Button(_("_Remove"), Icons.LIST_REMOVE) remove.set_focus_on_click(False) remove.connect('clicked', self.__remove_tag, view) remove.set_sensitive(False) self._remove = remove bbox1.pack_start(remove, True, True, 0) # Revert and save buttons # Both can have customised translated text (and thus accels) bbox2 = Gtk.HButtonBox() bbox2.set_spacing(6) bbox2.set_layout(Gtk.ButtonBoxStyle.END) # Translators: Revert button in the tag editor revert = Button(C_("edittags", "_Revert"), Icons.DOCUMENT_REVERT) self._revert = revert revert.set_sensitive(False) # Translators: Save button in the tag editor save = Button(C_("edittags", "_Save"), Icons.DOCUMENT_SAVE) save.set_sensitive(False) self._save = save bbox2.pack_start(revert, True, True, 0) bbox2.pack_start(save, True, True, 0) buttonbox.pack_start(bbox1, True, True, 0) buttonbox.pack_start(bbox2, True, True, 0) self.pack_start(buttonbox, False, True, 0) self._buttonbox = buttonbox parent.connect('changed', self.__parent_changed) revert.connect('clicked', lambda *x: self._update()) connect_obj(revert, 'clicked', parent.set_pending, None) save.connect('clicked', self.__save_files, revert, model, library) connect_obj(save, 'clicked', parent.set_pending, None) for sig in ['row-inserted', 'row-deleted', 'row-changed']: model.connect(sig, self.__enable_save, [save, revert]) connect_obj(model, sig, parent.set_pending, save) view.connect('popup-menu', self.__popup_menu, parent) view.connect('button-press-event', self.__button_press) view.connect('key-press-event', self.__view_key_press_event) selection.connect('changed', self.__tag_select, remove) selection.set_mode(Gtk.SelectionMode.MULTIPLE) self._parent = parent for child in self.get_children(): child.show_all()
def __init__(self, parent, library): super(EditTags, self).__init__(spacing=12) self.title = _("Edit Tags") self.set_border_width(12) model = ObjectStore() view = RCMHintedTreeView(model=model) self._view = view selection = view.get_selection() render = Gtk.CellRendererPixbuf() column = TreeViewColumn() column.pack_start(render, True) column.set_fixed_width(24) column.set_expand(False) def cdf_write(col, rend, model, iter_, *args): entry = model.get_value(iter_) rend.set_property('sensitive', entry.edited or entry.deleted) if entry.canedit or entry.deleted: if entry.deleted: rend.set_property('icon-name', Icons.EDIT_DELETE) else: rend.set_property('icon-name', Icons.EDIT) else: rend.set_property('icon-name', Icons.CHANGES_PREVENT) column.set_cell_data_func(render, cdf_write) view.append_column(column) render = Gtk.CellRendererText() column = TreeViewColumn(title=_('Tag')) column.pack_start(render, True) def cell_data_tag(column, cell, model, iter_, data): entry = model.get_value(iter_) cell.set_property("text", entry.tag) cell.set_property("strikethrough", entry.deleted) column.set_cell_data_func(render, cell_data_tag) column.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) render.set_property('editable', True) render.connect('edited', self.__edit_tag_name, model) render.connect( 'editing-started', self.__tag_editing_started, model, library) view.append_column(column) render = Gtk.CellRendererText() render.set_property('ellipsize', Pango.EllipsizeMode.END) render.set_property('editable', True) render.connect('edited', self.__edit_tag, model) render.connect( 'editing-started', self.__value_editing_started, model, library) column = TreeViewColumn(title=_('Value')) column.pack_start(render, True) def cell_data_value(column, cell, model, iter_, data): entry = model.get_value(iter_) markup = entry.value.get_markup() cell.markup = markup cell.set_property("markup", markup) cell.set_property("editable", entry.canedit) cell.set_property("strikethrough", entry.deleted) column.set_cell_data_func(render, cell_data_value) column.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) view.append_column(column) sw = Gtk.ScrolledWindow() sw.set_shadow_type(Gtk.ShadowType.IN) sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) sw.add(view) self.pack_start(sw, True, True, 0) cb = ConfigCheckButton( _("Show _programmatic tags"), 'editing', 'alltags', populate=True, tooltip=_("Access all tags, including machine-generated " "ones e.g. MusicBrainz or Replay Gain tags")) cb.connect('toggled', self.__all_tags_toggled) self.pack_start(cb, False, True, 0) # Add and Remove [tags] buttons buttonbox = Gtk.HBox(spacing=18) bbox1 = Gtk.HButtonBox() bbox1.set_spacing(6) bbox1.set_layout(Gtk.ButtonBoxStyle.START) add = qltk.Button(_("_Add"), Icons.LIST_ADD) add.set_focus_on_click(False) self._add = add add.connect('clicked', self.__add_tag, model, library) bbox1.pack_start(add, True, True, 0) # Remove button remove = qltk.Button(_("_Remove"), Icons.LIST_REMOVE) remove.set_focus_on_click(False) remove.connect('clicked', self.__remove_tag, view) remove.set_sensitive(False) self._remove = remove bbox1.pack_start(remove, True, True, 0) # Revert and save buttons # Both can have customised translated text (and thus accels) bbox2 = Gtk.HButtonBox() bbox2.set_spacing(6) bbox2.set_layout(Gtk.ButtonBoxStyle.END) # Translators: Revert button in the tag editor revert = Button(C_("edittags", "_Revert"), Icons.DOCUMENT_REVERT) self._revert = revert revert.set_sensitive(False) # Translators: Save button in the tag editor save = Button(C_("edittags", "_Save"), Icons.DOCUMENT_SAVE) save.set_sensitive(False) self._save = save bbox2.pack_start(revert, True, True, 0) bbox2.pack_start(save, True, True, 0) buttonbox.pack_start(bbox1, True, True, 0) buttonbox.pack_start(bbox2, True, True, 0) self.pack_start(buttonbox, False, True, 0) self._buttonbox = buttonbox parent.connect('changed', self.__parent_changed) revert.connect('clicked', lambda *x: self._update()) connect_obj(revert, 'clicked', parent.set_pending, None) save.connect('clicked', self.__save_files, revert, model, library) connect_obj(save, 'clicked', parent.set_pending, None) for sig in ['row-inserted', 'row-deleted', 'row-changed']: model.connect(sig, self.__enable_save, [save, revert]) connect_obj(model, sig, parent.set_pending, save) view.connect('popup-menu', self.__popup_menu, parent) view.connect('button-press-event', self.__button_press) view.connect('key-press-event', self.__view_key_press_event) selection.connect('changed', self.__tag_select, remove) selection.set_mode(Gtk.SelectionMode.MULTIPLE) self._parent = parent for child in self.get_children(): child.show_all()
class CoverArea(Gtk.VBox, PluginConfigMixin): """The image display and saving part.""" CONFIG_SECTION = PLUGIN_CONFIG_SECTION def __init__(self, parent, song): super().__init__() self.song = song self.dirname = song("~dirname") self.main_win = parent self.data_cache = [] self.current_data = None self.current_pixbuf = None self.image = Gtk.Image() self.button = Button(_("_Save"), Icons.DOCUMENT_SAVE_AS) self.button.set_sensitive(False) self.button.connect('clicked', self.__save) close_button = Button(_("_Close"), Icons.WINDOW_CLOSE) close_button.connect('clicked', lambda x: self.main_win.destroy()) self.window_fit = self.ConfigCheckButton(_('Fit image to _window'), 'fit', True) self.window_fit.connect('toggled', self.__scale_pixbuf) self.name_combo = Gtk.ComboBoxText() self.name_combo.set_tooltip_text( _("See '[plugins] cover_filenames' config entry " + "for image filename strings")) self.cmd = qltk.entry.ValidatingEntry(iscommand) # Both labels label_open = Gtk.Label(label=_('_Program:')) label_open.set_use_underline(True) label_open.set_mnemonic_widget(self.cmd) label_open.set_justify(Gtk.Justification.LEFT) self.open_check = self.ConfigCheckButton(_('_Edit image after saving'), 'edit', False) label_name = Gtk.Label(label=_('File_name:'), use_underline=True) label_name.set_use_underline(True) label_name.set_mnemonic_widget(self.name_combo) label_name.set_justify(Gtk.Justification.LEFT) self.cmd.set_text(self.config_get('edit_cmd', 'gimp')) # populate the filename combo box fn_list = self.config_get_stringlist( 'filenames', ["cover.jpg", "folder.jpg", ".folder.jpg"]) # Issue 374 - add dynamic file names fn_dynlist = [] artist = song("artist") alartist = song("albumartist") album = song("album") labelid = song("labelid") if album: fn_dynlist.append("<album>.jpg") if alartist: fn_dynlist.append("<albumartist> - <album>.jpg") else: fn_dynlist.append("<artist> - <album>.jpg") else: print_w(u"No album for \"%s\". Could be difficult " u"finding art…" % song("~filename")) title = song("title") if title and artist: fn_dynlist.append("<artist> - <title>.jpg") if labelid: fn_dynlist.append("<labelid>.jpg") # merge unique fn_list.extend(s for s in fn_dynlist if s not in fn_list) set_fn = self.config_get('filename', fn_list[0]) for i, fn in enumerate(fn_list): self.name_combo.append_text(fn) if fn == set_fn: self.name_combo.set_active(i) if self.name_combo.get_active() < 0: self.name_combo.set_active(0) self.config_set('filename', self.name_combo.get_active_text()) table = Gtk.Table(n_rows=2, n_columns=2, homogeneous=False) table.props.expand = False table.set_row_spacing(0, 5) table.set_row_spacing(1, 5) table.set_col_spacing(0, 5) table.set_col_spacing(1, 5) table.attach(label_open, 0, 1, 0, 1) table.attach(label_name, 0, 1, 1, 2) table.attach(self.cmd, 1, 2, 0, 1) table.attach(self.name_combo, 1, 2, 1, 2) self.scrolled = Gtk.ScrolledWindow() self.scrolled.add_with_viewport(self.image) self.scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) bbox = Gtk.HButtonBox() bbox.set_spacing(6) bbox.set_layout(Gtk.ButtonBoxStyle.END) bbox.pack_start(self.button, True, True, 0) bbox.pack_start(close_button, True, True, 0) bb_align = Align(valign=Gtk.Align.END, right=6) bb_align.add(bbox) main_hbox = Gtk.HBox() main_hbox.pack_start(table, False, True, 6) main_hbox.pack_start(bb_align, True, True, 0) top_hbox = Gtk.HBox() top_hbox.pack_start(self.open_check, True, True, 0) top_hbox.pack_start(self.window_fit, False, True, 0) main_vbox = Gtk.VBox() main_vbox.pack_start(top_hbox, True, True, 2) main_vbox.pack_start(main_hbox, True, True, 0) self.pack_start(self.scrolled, True, True, 0) self.pack_start(main_vbox, False, True, 5) # 5 MB image cache size self.max_cache_size = 1024 * 1024 * 5 # For managing fast selection switches of covers.. self.stop_loading = False self.loading = False self.current_job = 0 self.connect('destroy', self.__save_config) def __save(self, *data): """Save the cover and spawn the program to edit it if selected""" save_format = self.name_combo.get_active_text() # Allow use of patterns in creating cover filenames pattern = ArbitraryExtensionFileFromPattern(save_format) filename = pattern.format(self.song) print_d("Using '%s' as filename based on %s" % (filename, save_format)) file_path = os.path.join(self.dirname, filename) if os.path.exists(file_path): resp = ConfirmFileReplace(self, file_path).run() if resp != ConfirmFileReplace.RESPONSE_REPLACE: return try: f = open(file_path, 'wb') f.write(self.current_data) f.close() except IOError: qltk.ErrorMessage(None, _('Saving failed'), _('Unable to save "%s".') % file_path).run() else: if self.open_check.get_active(): try: util.spawn([self.cmd.get_text(), file_path]) except: pass app.cover_manager.cover_changed([self.song._song]) self.main_win.destroy() def __save_config(self, widget): self.config_set('edit_cmd', self.cmd.get_text()) self.config_set('filename', self.name_combo.get_active_text()) def __update(self, loader, *data): """Update the picture while it's loading""" if self.stop_loading: return pixbuf = loader.get_pixbuf() def idle_set(): if pixbuf is not None: surface = get_surface_for_pixbuf(self, pixbuf) self.image.set_from_surface(surface) GLib.idle_add(idle_set) def __scale_pixbuf(self, *data): if not self.current_pixbuf: return pixbuf = self.current_pixbuf if self.window_fit.get_active(): alloc = self.scrolled.get_allocation() width = alloc.width height = alloc.height scale_factor = self.get_scale_factor() boundary = (width * scale_factor, height * scale_factor) pixbuf = scale(pixbuf, boundary, scale_up=False) surface = get_surface_for_pixbuf(self, pixbuf) self.image.set_from_surface(surface) def __close(self, loader, *data): if self.stop_loading: return self.current_pixbuf = loader.get_pixbuf() GLib.idle_add(self.__scale_pixbuf) def set_cover(self, url): thr = threading.Thread(target=self.__set_async, args=(url, )) thr.setDaemon(True) thr.start() def __set_async(self, url): """Manages various things: Fast switching of covers (aborting old HTTP requests), The image cache, etc.""" self.current_job += 1 job = self.current_job self.stop_loading = True while self.loading: time.sleep(0.05) self.stop_loading = False if job != self.current_job: return self.loading = True GLib.idle_add(self.button.set_sensitive, False) self.current_pixbuf = None pbloader = GdkPixbuf.PixbufLoader() pbloader.connect('closed', self.__close) # Look for cached images raw_data = None for entry in self.data_cache: if entry[0] == url: raw_data = entry[1] break if not raw_data: pbloader.connect('area-updated', self.__update) data_store = BytesIO() try: request = Request(url) request.add_header('User-Agent', USER_AGENT) url_sock = urlopen(request) except EnvironmentError: print_w(_("[albumart] HTTP Error: %s") % url) else: while not self.stop_loading: tmp = url_sock.read(1024 * 10) if not tmp: break pbloader.write(tmp) data_store.write(tmp) url_sock.close() if not self.stop_loading: raw_data = data_store.getvalue() self.data_cache.insert(0, (url, raw_data)) while 1: cache_sizes = [ len(data[1]) for data in self.data_cache ] if sum(cache_sizes) > self.max_cache_size: del self.data_cache[-1] else: break data_store.close() else: # Sleep for fast switching of cached images time.sleep(0.05) if not self.stop_loading: pbloader.write(raw_data) try: pbloader.close() except GLib.GError: pass self.current_data = raw_data if not self.stop_loading: GLib.idle_add(self.button.set_sensitive, True) self.loading = False
class TagListEditor(qltk.Window): """Dialog to edit a list of tag names.""" _WIDTH = 600 _HEIGHT = 300 def __init__(self, title, values=None): super(TagListEditor, self).__init__() self.use_header_bar() self.data = values or [] self.set_border_width(12) self.set_title(title) self.set_default_size(self._WIDTH, self._HEIGHT) vbox = Gtk.VBox(spacing=12) hbox = Gtk.HBox(spacing=12) # Set up the model for this widget self.model = Gtk.ListStore(str) self.__fill_values() # Main view view = self.view = HintedTreeView(model=self.model) view.set_fixed_height_mode(True) view.set_headers_visible(False) sw = Gtk.ScrolledWindow() sw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) sw.set_shadow_type(Gtk.ShadowType.IN) sw.add(view) sw.set_size_request(-1, max(sw.size_request().height, 100)) hbox.pack_start(sw, True, True, 0) self.__setup_column(view) # Context menu menu = Gtk.Menu() remove_item = MenuItem(_("_Remove"), Icons.LIST_REMOVE) menu.append(remove_item) menu.show_all() view.connect('popup-menu', self.__popup, menu) connect_obj(remove_item, 'activate', self.__remove, view) # Add and Remove buttons vbbox = Gtk.VButtonBox() vbbox.set_layout(Gtk.ButtonBoxStyle.START) vbbox.set_spacing(6) add = Button(_("_Add"), Icons.LIST_ADD) add.connect("clicked", self.__add) vbbox.pack_start(add, False, True, 0) remove = Button(_("_Remove"), Icons.LIST_REMOVE) remove.connect("clicked", self.__remove) vbbox.pack_start(remove, False, True, 0) hbox.pack_start(vbbox, False, True, 0) vbox.pack_start(hbox, True, True, 0) # Close buttons bbox = Gtk.HButtonBox() self.remove_but = Button(_("_Remove"), Icons.LIST_REMOVE) self.remove_but.set_sensitive(False) close = Button(_("_Close"), Icons.WINDOW_CLOSE) connect_obj(close, 'clicked', qltk.Window.destroy, self) bbox.set_layout(Gtk.ButtonBoxStyle.END) if not self.has_close_button(): bbox.pack_start(close, True, True, 0) vbox.pack_start(bbox, False, True, 0) # Finish up self.add(vbox) self.get_child().show_all() def __setup_column(self, view): def tag_cdf(column, cell, model, iter, data): row = model[iter] if row: cell.set_property('text', row[0]) def desc_cdf(column, cell, model, iter, data): row = model[iter] if row: name = re.sub(':[a-z]+$', '', row[0].strip('~#')) try: t = _TAGS[name] valid = (not t.hidden and t.numeric == row[0].startswith('~#')) val = t.desc if valid else name except KeyError: val = name cell.set_property('text', util.title(val.replace('~', ' / '))) render = Gtk.CellRendererText() column = Gtk.TreeViewColumn(_("Tag expression"), render) column.set_cell_data_func(render, tag_cdf) column.set_sizing(Gtk.TreeViewColumnSizing.FIXED) column.set_expand(True) view.append_column(column) render = Gtk.CellRendererText() render.set_property('ellipsize', Pango.EllipsizeMode.END) column = Gtk.TreeViewColumn(_("Description"), render) column.set_cell_data_func(render, desc_cdf) column.set_sizing(Gtk.TreeViewColumnSizing.FIXED) column.set_expand(True) view.append_column(column) view.set_headers_visible(True) def __fill_values(self): for s in self.data: self.model.append(row=[s]) def get_strings(self): strings = [row[0] for row in self.model if row] return strings def __remove(self, *args): self.view.remove_selection() def __add(self, *args): tooltip = _('Tag expression e.g. people:real or ~album~year.') dialog = GetStringDialog(self, _("Enter new tag"), "", button_icon=None, tooltip=tooltip) new = dialog.run() if new: self.model.append(row=[new]) def __popup(self, view, menu): return view.popup_menu(menu, 0, Gtk.get_current_event_time()).show()
def __init__(self, library): super(MediaDevices, self).__init__(spacing=6) self.set_orientation(Gtk.Orientation.VERTICAL) self._register_instance() self.__cache = {} # Device list on the left pane swin = ScrolledWindow() swin.set_shadow_type(Gtk.ShadowType.IN) swin.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) self.pack_start(swin, True, True, 0) self.__view = view = AllTreeView() view.set_model(self.__devices) view.set_rules_hint(True) view.set_headers_visible(False) view.get_selection().set_mode(Gtk.SelectionMode.BROWSE) connect_obj(view.get_selection(), 'changed', self.__refresh, False) view.connect('popup-menu', self.__popup_menu, library) view.connect('row-activated', lambda *a: self.songs_activated()) swin.add(view) col = Gtk.TreeViewColumn("Devices") view.append_column(col) render = Gtk.CellRendererPixbuf() col.pack_start(render, False) col.add_attribute(render, 'icon-name', 1) self.__render = render = Gtk.CellRendererText() render.set_property('ellipsize', Pango.EllipsizeMode.END) render.connect('edited', self.__edited) col.pack_start(render, True) col.set_cell_data_func(render, MediaDevices.cell_data) hbox = Gtk.HBox(spacing=6) hbox.set_homogeneous(True) self.pack_start(Align(hbox, left=3, bottom=3), False, True, 0) # refresh button refresh = Button(_("_Refresh"), Gtk.STOCK_REFRESH, Gtk.IconSize.MENU) self.__refresh_button = refresh connect_obj(refresh, 'clicked', self.__refresh, True) refresh.set_sensitive(False) hbox.pack_start(refresh, True, True, 0) # eject button eject = Button(_("_Eject"), "media-eject", Gtk.IconSize.MENU) self.__eject_button = eject eject.connect('clicked', self.__eject) eject.set_sensitive(False) hbox.pack_start(eject, True, True, 0) # Device info on the right pane self.__header = table = Gtk.Table() table.set_col_spacings(8) self.__device_icon = icon = Gtk.Image() icon.set_size_request(48, 48) table.attach(icon, 0, 1, 0, 2, 0) self.__device_name = label = Gtk.Label() label.set_ellipsize(Pango.EllipsizeMode.END) label.set_alignment(0, 0) table.attach(label, 1, 3, 0, 1) self.__device_space = label = Gtk.Label() label.set_ellipsize(Pango.EllipsizeMode.END) label.set_alignment(0, 0.5) table.attach(label, 1, 2, 1, 2) self.__progress = progress = Gtk.ProgressBar() progress.set_size_request(150, -1) table.attach(progress, 2, 3, 1, 2, xoptions=0, yoptions=0) self.accelerators = Gtk.AccelGroup() key, mod = Gtk.accelerator_parse('F2') self.accelerators.connect(key, mod, 0, self.__rename) self.__statusbar = WaitLoadBar() for child in self.get_children(): child.show_all()
class JSONBasedEditor(qltk.UniqueWindow): """ Flexible editor for objects extending `JSONObject` (held in a `JSONObjectDict`) TODO: validation, especially for name. """ _WIDTH = 800 _HEIGHT = 400 def __init__(self, Prototype, values, filename, title): if self.is_not_unique(): return super(JSONBasedEditor, self).__init__() self.Prototype = Prototype self.current = None self.filename = filename self.name = Prototype.NAME or Prototype.__name__ self.input_entries = {} self.set_border_width(12) self.set_title(title) self.set_default_size(self._WIDTH, self._HEIGHT) self.add(Gtk.HBox(spacing=6)) self.get_child().set_homogeneous(True) self.accels = Gtk.AccelGroup() # Set up the model for this widget self.model = Gtk.ListStore(object) self._fill_values(values) # The browser for existing data self.view = view = RCMHintedTreeView(model=self.model) view.set_headers_visible(False) view.set_reorderable(True) view.set_rules_hint(True) render = Gtk.CellRendererText() render.set_padding(3, 6) render.props.ellipsize = Pango.EllipsizeMode.END column = Gtk.TreeViewColumn("", render) column.set_cell_data_func(render, self.__cdf) view.append_column(column) sw = Gtk.ScrolledWindow() sw.set_shadow_type(Gtk.ShadowType.IN) sw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) sw.add(view) self.get_child().pack_start(sw, True, True, 0) vbox = Gtk.VBox(spacing=6) # Input for new ones. frame = self.__build_input_frame() vbox.pack_start(frame, False, True, 0) # Add context menu menu = Gtk.Menu() rem = MenuItem(_("_Remove"), Icons.LIST_REMOVE) keyval, mod = Gtk.accelerator_parse("Delete") rem.add_accelerator('activate', self.accels, keyval, mod, Gtk.AccelFlags.VISIBLE) connect_obj(rem, 'activate', self.__remove, view) menu.append(rem) menu.show_all() view.connect('popup-menu', self.__popup, menu) view.connect('key-press-event', self.__view_key_press) connect_obj(self, 'destroy', Gtk.Menu.destroy, menu) # New and Close buttons bbox = Gtk.HButtonBox() self.remove_but = Button(_("_Remove"), Icons.LIST_REMOVE) self.remove_but.set_sensitive(False) self.new_but = Button(_("_New"), Icons.DOCUMENT_NEW) self.new_but.connect('clicked', self._new_item) bbox.pack_start(self.new_but, True, True, 0) close = Button(_("_Close"), Icons.WINDOW_CLOSE) connect_obj(close, 'clicked', qltk.Window.destroy, self) bbox.pack_start(close, True, True, 0) vbox.pack_end(bbox, False, True, 0) self.get_child().pack_start(vbox, True, True, 0) # Initialise self.selection = view.get_selection() self.selection.connect('changed', self.__select) self.connect('destroy', self.__finish) self.get_child().show_all() def _find(self, name): for row in self.model: if row[0].name == name: return row[0] def _new_item(self, button): current_name = name = _("New %s") % self.name n = 2 while True: if self._find(current_name): current_name = "%s (%d)" % (name, n) n += 1 continue break self.model.append(row=(self.Prototype(name=current_name), )) def _new_widget(self, key, val): """ Creates a Gtk.Entry subclass appropriate for a field named `key` with value `val` """ callback = signal = None if isinstance(val, bool): entry = Gtk.CheckButton() callback = self.__toggled_widget signal = "toggled" elif isinstance(val, int): adj = Gtk.Adjustment.new(0, 0, 9999, 1, 10, 0) entry = Gtk.SpinButton(adjustment=adj) entry.set_numeric(True) callback = self.__changed_numeric_widget elif "pattern" in key: entry = ValidatingEntry(validator=Query.validator) else: entry = UndoEntry() entry.connect(signal or "changed", callback or self.__changed_widget, key) return entry def __refresh_view(self): model, iter = self.selection.get_selected() self.model.emit("row-changed", model[iter].path, iter) def __changed_widget(self, entry, key): if self.current: setattr(self.current, key, str(entry.get_text())) self.__refresh_view() def __changed_numeric_widget(self, entry, key): if self.current: setattr(self.current, key, int(entry.get_text() or 0)) self.__refresh_view() def __toggled_widget(self, entry, key): if self.current: setattr(self.current, key, bool(entry.get_active())) self.__refresh_view() def _populate_fields(self, obj): """Populates the input fields based on the `JSONData` object `obj`""" for fn, val in obj.data: widget = self.input_entries[fn] widget.set_sensitive(True) # TODO: link this logic better with the creational stuff if isinstance(val, bool): widget.set_active(val) elif isinstance(val, int): widget.set_value(int(val)) elif isinstance(val, basestring): widget.set_text(val or "") def __build_input_frame(self): t = Gtk.Table(n_rows=2, n_columns=3) t.set_row_spacings(6) t.set_col_spacing(0, 3) t.set_col_spacing(1, 12) empty = self.Prototype("empty") for i, (key, val) in enumerate(empty.data): field = empty.field(key) field_name = self.get_field_name(field, key) l = Gtk.Label(label=field_name + ":") entry = self._new_widget(key, val) entry.set_sensitive(False) if field.doc: entry.set_tooltip_text(field.doc) # Store these away in a map for later access self.input_entries[key] = entry l.set_mnemonic_widget(entry) l.set_use_underline(True) l.set_alignment(0.0, 0.5) if isinstance(val, int) or isinstance(val, bool): align = Align(entry, halign=Gtk.Align.START) t.attach(align, 1, 2, i, i + 1) else: t.attach(entry, 1, 2, i, i + 1) t.attach(l, 0, 1, i, i + 1, xoptions=Gtk.AttachOptions.FILL) frame = qltk.Frame(label=self.name, child=t) self.input_entries["name"].grab_focus() return frame @staticmethod def get_field_name(field, key): field_name = (field.human_name or (key and key.replace("_", " "))) return field_name and field_name.title() or _("(unknown)") def _fill_values(self, data): if not data: return for (name, obj) in data.items(): self.model.prepend(row=[obj]) def _update_current(self, new_selection=None): if new_selection: self.selection = new_selection model, iter = self.selection.get_selected() if iter: self.current = model[iter][0] def __select(self, selection): self._update_current(selection) self.remove_but.set_sensitive(bool(iter)) if iter is not None: self._populate_fields(self.current) def __remove(self, view): view.remove_selection() def __popup(self, view, menu): return view.popup_menu(menu, 0, Gtk.get_current_event_time()) def __view_key_press(self, view, event): if event.keyval == Gtk.accelerator_parse("Delete")[0]: self.__remove(view) def __cdf(self, column, cell, model, iter, data): row = model[iter] obj = row[0] obj_name = util.escape(obj.name) obj_description = util.escape(str(obj)) markup = '<b>%s</b>\n%s' % (obj_name, obj_description) cell.markup = markup cell.set_property('markup', markup) def __finish(self, widget): # TODO: Warn about impending deletion of nameless items, or something all = JSONObjectDict.from_list( [row[0] for row in self.model if row[0].name]) all.save(filename=self.filename)
class AlbumArtWindow(qltk.Window, PluginConfigMixin): """The main window including the search list""" CONFIG_SECTION = PLUGIN_CONFIG_SECTION THUMB_SIZE = 50 def __init__(self, songs): super(AlbumArtWindow, self).__init__() self.image_cache = [] self.image_cache_size = 10 self.search_lock = False self.set_title(_('Album Art Downloader')) self.set_icon_name(Icons.EDIT_FIND) self.set_default_size(800, 550) image = CoverArea(self, songs[0]) self.liststore = Gtk.ListStore(object, object) self.treeview = treeview = AllTreeView(model=self.liststore) self.treeview.set_headers_visible(False) self.treeview.set_rules_hint(True) targets = [("text/uri-list", 0, 0)] targets = [Gtk.TargetEntry.new(*t) for t in targets] treeview.drag_source_set( Gdk.ModifierType.BUTTON1_MASK, targets, Gdk.DragAction.COPY) treeselection = self.treeview.get_selection() treeselection.set_mode(Gtk.SelectionMode.SINGLE) treeselection.connect('changed', self.__select_callback, image) self.treeview.connect("drag-data-get", self.__drag_data_get, treeselection) rend_pix = Gtk.CellRendererPixbuf() img_col = Gtk.TreeViewColumn('Thumb') img_col.pack_start(rend_pix, False) def cell_data_pb(column, cell, model, iter_, *args): pbosf = model[iter_][0] set_renderer_from_pbosf(cell, pbosf) img_col.set_cell_data_func(rend_pix, cell_data_pb, None) treeview.append_column(img_col) rend_pix.set_property('xpad', 2) rend_pix.set_property('ypad', 2) border_width = get_scale_factor(self) * 2 rend_pix.set_property('width', self.THUMB_SIZE + 4 + border_width) rend_pix.set_property('height', self.THUMB_SIZE + 4 + border_width) def escape_data(data): for rep in ('\n', '\t', '\r', '\v'): data = data.replace(rep, ' ') return util.escape(' '.join(data.split())) def cell_data(column, cell, model, iter, data): cover = model[iter][1] esc = escape_data txt = '<b><i>%s</i></b>' % esc(cover['name']) txt += "\n<small>%s</small>" % ( _('from %(source)s') % { "source": util.italic(esc(cover['source']))}) if 'resolution' in cover: txt += "\n" + _('Resolution: %s') % util.italic( esc(cover['resolution'])) if 'size' in cover: txt += "\n" + _('Size: %s') % util.italic(esc(cover['size'])) cell.markup = txt cell.set_property('markup', cell.markup) rend = Gtk.CellRendererText() rend.set_property('ellipsize', Pango.EllipsizeMode.END) info_col = Gtk.TreeViewColumn('Info', rend) info_col.set_cell_data_func(rend, cell_data) treeview.append_column(info_col) sw_list = Gtk.ScrolledWindow() sw_list.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) sw_list.set_shadow_type(Gtk.ShadowType.IN) sw_list.add(treeview) self.search_field = Gtk.Entry() self.search_button = Button(_("_Search"), Icons.EDIT_FIND) self.search_button.connect('clicked', self.start_search) self.search_field.connect('activate', self.start_search) widget_space = 5 search_hbox = Gtk.HBox(spacing=widget_space) search_hbox.pack_start(self.search_field, True, True, 0) search_hbox.pack_start(self.search_button, False, True, 0) self.progress = Gtk.ProgressBar() left_vbox = Gtk.VBox(spacing=widget_space) left_vbox.pack_start(search_hbox, False, True, 0) left_vbox.pack_start(sw_list, True, True, 0) hpaned = Paned() hpaned.set_border_width(widget_space) hpaned.pack1(left_vbox, shrink=False) hpaned.pack2(image, shrink=False) hpaned.set_position(275) self.add(hpaned) self.show_all() left_vbox.pack_start(self.progress, False, True, 0) if songs[0]('albumartist'): text = songs[0]('albumartist') else: text = songs[0]('artist') text += ' - ' + songs[0]('album') self.set_text(text) self.start_search() def __drag_data_get(self, view, ctx, sel, tid, etime, treeselection): model, iter = treeselection.get_selected() if not iter: return cover = model.get_value(iter, 1) sel.set_uris([cover['cover']]) def start_search(self, *data): """Start the search using the text from the text entry""" text = self.search_field.get_text() if not text or self.search_lock: return self.search_lock = True self.search_button.set_sensitive(False) self.progress.set_fraction(0) self.progress.set_text(_(u'Searching…')) self.progress.show() self.liststore.clear() self.search = search = CoverSearch(self.__search_callback) for eng in engines: if self.config_get(CONFIG_ENG_PREFIX + eng['config_id'], True): search.add_engine(eng['class'], eng['replace']) search.start(text) # Focus the list self.treeview.grab_focus() self.connect("destroy", self.__destroy) def __destroy(self, *args): self.search.stop() def set_text(self, text): """set the text and move the cursor to the end""" self.search_field.set_text(text) self.search_field.emit('move-cursor', Gtk.MovementStep.BUFFER_ENDS, 0, False) def __select_callback(self, selection, image): model, iter = selection.get_selected() if not iter: return cover = model.get_value(iter, 1) image.set_cover(cover['cover']) def __add_cover_to_list(self, cover): try: pbloader = GdkPixbuf.PixbufLoader() pbloader.write(get_url(cover['thumbnail'])[0]) pbloader.close() scale_factor = get_scale_factor(self) size = self.THUMB_SIZE * scale_factor - scale_factor * 2 pixbuf = pbloader.get_pixbuf().scale_simple(size, size, GdkPixbuf.InterpType.BILINEAR) pixbuf = add_border_widget(pixbuf, self, None, round=True) thumb = get_pbosf_for_pixbuf(self, pixbuf) except (GLib.GError, IOError): pass else: def append(data): self.liststore.append(data) GLib.idle_add(append, [thumb, cover]) def __search_callback(self, covers, progress): for cover in covers: self.__add_cover_to_list(cover) if self.progress.get_fraction() < progress: self.progress.set_fraction(progress) if progress >= 1: self.progress.set_text(_('Done')) GLib.timeout_add(700, self.progress.hide) self.search_button.set_sensitive(True) self.search_lock = False
class JSONBasedEditor(qltk.UniqueWindow): """ Flexible editor for objects extending `JSONObject` (held in a `JSONObjectDict`) TODO: validation, especially for name. """ _WIDTH = 800 _HEIGHT = 400 def __init__(self, Prototype, values, filename, title): if self.is_not_unique(): return super(JSONBasedEditor, self).__init__() self.Prototype = Prototype self.current = None self.filename = filename self.name = Prototype.NAME or Prototype.__name__ self.input_entries = {} self.set_border_width(12) self.set_title(title) self.set_default_size(self._WIDTH, self._HEIGHT) self.add(Gtk.HBox(spacing=6)) self.get_child().set_homogeneous(True) self.accels = Gtk.AccelGroup() # Set up the model for this widget self.model = Gtk.ListStore(object) self._fill_values(values) # The browser for existing data self.view = view = RCMHintedTreeView(model=self.model) view.set_headers_visible(False) view.set_reorderable(True) view.set_rules_hint(True) render = Gtk.CellRendererText() render.set_padding(3, 6) render.props.ellipsize = Pango.EllipsizeMode.END column = Gtk.TreeViewColumn("", render) column.set_cell_data_func(render, self.__cdf) view.append_column(column) sw = Gtk.ScrolledWindow() sw.set_shadow_type(Gtk.ShadowType.IN) sw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) sw.add(view) self.get_child().pack_start(sw, True, True, 0) vbox = Gtk.VBox(spacing=6) # Input for new ones. frame = self.__build_input_frame() vbox.pack_start(frame, False, True, 0) # Add context menu menu = Gtk.Menu() rem = MenuItem(_("_Remove"), Icons.LIST_REMOVE) keyval, mod = Gtk.accelerator_parse("Delete") rem.add_accelerator( 'activate', self.accels, keyval, mod, Gtk.AccelFlags.VISIBLE) connect_obj(rem, 'activate', self.__remove, view) menu.append(rem) menu.show_all() view.connect('popup-menu', self.__popup, menu) view.connect('key-press-event', self.__view_key_press) connect_obj(self, 'destroy', Gtk.Menu.destroy, menu) # New and Close buttons bbox = Gtk.HButtonBox() self.remove_but = Button(_("_Remove"), Icons.LIST_REMOVE) self.remove_but.set_sensitive(False) self.new_but = Button(_("_New"), Icons.DOCUMENT_NEW) self.new_but.connect('clicked', self._new_item) bbox.pack_start(self.new_but, True, True, 0) close = Button(_("_Close"), Icons.WINDOW_CLOSE) connect_obj(close, 'clicked', qltk.Window.destroy, self) bbox.pack_start(close, True, True, 0) vbox.pack_end(bbox, False, True, 0) self.get_child().pack_start(vbox, True, True, 0) # Initialise self.selection = view.get_selection() self.selection.connect('changed', self.__select) self.connect('destroy', self.__finish) self.get_child().show_all() def _find(self, name): for row in self.model: if row[0].name == name: return row[0] def _new_item(self, button): current_name = name = _("New %s") % self.name n = 2 while True: if self._find(current_name): current_name = "%s (%d)" % (name, n) n += 1 continue break self.model.append(row=(self.Prototype(name=current_name),)) def _new_widget(self, key, val): """ Creates a Gtk.Entry subclass appropriate for a field named `key` with value `val` """ callback = signal = None if isinstance(val, bool): entry = Gtk.CheckButton() callback = self.__toggled_widget signal = "toggled" elif isinstance(val, int): adj = Gtk.Adjustment.new(0, 0, 9999, 1, 10, 0) entry = Gtk.SpinButton(adjustment=adj) entry.set_numeric(True) callback = self.__changed_numeric_widget elif "pattern" in key: entry = ValidatingEntry(validator=Query.validator) else: entry = UndoEntry() entry.connect(signal or "changed", callback or self.__changed_widget, key) return entry def __refresh_view(self): model, iter = self.selection.get_selected() self.model.emit("row-changed", model[iter].path, iter) def __changed_widget(self, entry, key): if self.current: setattr(self.current, key, str(entry.get_text())) self.__refresh_view() def __changed_numeric_widget(self, entry, key): if self.current: setattr(self.current, key, int(entry.get_text() or 0)) self.__refresh_view() def __toggled_widget(self, entry, key): if self.current: setattr(self.current, key, bool(entry.get_active())) self.__refresh_view() def _populate_fields(self, obj): """Populates the input fields based on the `JSONData` object `obj`""" for fn, val in obj.data: widget = self.input_entries[fn] widget.set_sensitive(True) # TODO: link this logic better with the creational stuff if isinstance(val, bool): widget.set_active(val) elif isinstance(val, int): widget.set_value(int(val)) elif isinstance(val, basestring): widget.set_text(val or "") def __build_input_frame(self): t = Gtk.Table(n_rows=2, n_columns=3) t.set_row_spacings(6) t.set_col_spacing(0, 3) t.set_col_spacing(1, 12) empty = self.Prototype("empty") for i, (key, val) in enumerate(empty.data): field = empty.field(key) field_name = self.get_field_name(field, key) l = Gtk.Label(label=field_name + ":") entry = self._new_widget(key, val) entry.set_sensitive(False) if field.doc: entry.set_tooltip_text(field.doc) # Store these away in a map for later access self.input_entries[key] = entry l.set_mnemonic_widget(entry) l.set_use_underline(True) l.set_alignment(0.0, 0.5) if isinstance(val, int) or isinstance(val, bool): align = Align(entry, halign=Gtk.Align.START) t.attach(align, 1, 2, i, i + 1) else: t.attach(entry, 1, 2, i, i + 1) t.attach(l, 0, 1, i, i + 1, xoptions=Gtk.AttachOptions.FILL) frame = qltk.Frame(label=self.name, child=t) self.input_entries["name"].grab_focus() return frame @staticmethod def get_field_name(field, key): field_name = (field.human_name or (key and key.replace("_", " "))) return field_name and field_name.title() or _("(unknown)") def _fill_values(self, data): if not data: return for (name, obj) in data.items(): self.model.prepend(row=[obj]) def _update_current(self, new_selection=None): if new_selection: self.selection = new_selection model, iter = self.selection.get_selected() if iter: self.current = model[iter][0] def __select(self, selection): self._update_current(selection) self.remove_but.set_sensitive(bool(iter)) if iter is not None: self._populate_fields(self.current) def __remove(self, view): view.remove_selection() def __popup(self, view, menu): return view.popup_menu(menu, 0, Gtk.get_current_event_time()) def __view_key_press(self, view, event): if event.keyval == Gtk.accelerator_parse("Delete")[0]: self.__remove(view) def __cdf(self, column, cell, model, iter, data): row = model[iter] obj = row[0] obj_name = util.escape(obj.name) obj_description = util.escape(str(obj)) markup = '<b>%s</b>\n%s' % (obj_name, obj_description) cell.markup = markup cell.set_property('markup', markup) def __finish(self, widget): # TODO: Warn about impending deletion of nameless items, or something all = JSONObjectDict.from_list( [row[0] for row in self.model if row[0].name]) all.save(filename=self.filename)
class CoverArea(Gtk.VBox, PluginConfigMixin): """The image display and saving part.""" CONFIG_SECTION = PLUGIN_CONFIG_SECTION def __init__(self, parent, song): super(CoverArea, self).__init__() self.song = song self.dirname = song("~dirname") self.main_win = parent self.data_cache = [] self.current_data = None self.current_pixbuf = None self.image = Gtk.Image() self.button = Button(_("_Save"), Icons.DOCUMENT_SAVE) self.button.set_sensitive(False) self.button.connect('clicked', self.__save) close_button = Button(_("_Close"), Icons.WINDOW_CLOSE) close_button.connect('clicked', lambda x: self.main_win.destroy()) self.window_fit = self.ConfigCheckButton(_('Fit image to _window'), 'fit', True) self.window_fit.connect('toggled', self.__scale_pixbuf) self.name_combo = Gtk.ComboBoxText() self.cmd = qltk.entry.ValidatingEntry(iscommand) # Both labels label_open = Gtk.Label(label=_('_Program:')) label_open.set_use_underline(True) label_open.set_mnemonic_widget(self.cmd) label_open.set_justify(Gtk.Justification.LEFT) self.open_check = self.ConfigCheckButton(_('_Edit image after saving'), 'edit', False) label_name = Gtk.Label(label=_('File_name:'), use_underline=True) label_name.set_use_underline(True) label_name.set_mnemonic_widget(self.name_combo) label_name.set_justify(Gtk.Justification.LEFT) self.cmd.set_text(self.config_get('edit_cmd', 'gimp')) # Create the filename combo box fn_list = ['cover.jpg', 'folder.jpg', '.folder.jpg'] # Issue 374 - add dynamic file names artist = song("artist") alartist = song("albumartist") album = song("album") labelid = song("labelid") if album: fn_list.append("<album>.jpg") if alartist: fn_list.append("<albumartist> - <album>.jpg") else: fn_list.append("<artist> - <album>.jpg") else: print_w(u"No album for \"%s\". Could be difficult " u"finding art…" % song("~filename")) title = song("title") if title and artist: fn_list.append("<artist> - <title>.jpg") if labelid: fn_list.append("<labelid>.jpg") set_fn = self.config_get('fn', fn_list[0]) for i, fn in enumerate(fn_list): self.name_combo.append_text(fn) if fn == set_fn: self.name_combo.set_active(i) if self.name_combo.get_active() < 0: self.name_combo.set_active(0) table = Gtk.Table(n_rows=2, n_columns=2, homogeneous=False) table.set_row_spacing(0, 5) table.set_row_spacing(1, 5) table.set_col_spacing(0, 5) table.set_col_spacing(1, 5) table.attach(label_open, 0, 1, 0, 1) table.attach(label_name, 0, 1, 1, 2) table.attach(self.cmd, 1, 2, 0, 1) table.attach(self.name_combo, 1, 2, 1, 2) self.scrolled = Gtk.ScrolledWindow() self.scrolled.add_with_viewport(self.image) self.scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) bbox = Gtk.HButtonBox() bbox.set_spacing(6) bbox.set_layout(Gtk.ButtonBoxStyle.END) bbox.pack_start(self.button, True, True, 0) bbox.pack_start(close_button, True, True, 0) bb_align = Align(valign=Gtk.Align.END, right=6) bb_align.add(bbox) main_hbox = Gtk.HBox() main_hbox.pack_start(table, False, True, 6) main_hbox.pack_start(bb_align, True, True, 0) top_hbox = Gtk.HBox() top_hbox.pack_start(self.open_check, True, True, 0) top_hbox.pack_start(self.window_fit, False, True, 0) main_vbox = Gtk.VBox() main_vbox.pack_start(top_hbox, True, True, 2) main_vbox.pack_start(main_hbox, True, True, 0) self.pack_start(self.scrolled, True, True, 0) self.pack_start(main_vbox, False, True, 5) # 5 MB image cache size self.max_cache_size = 1024 * 1024 * 5 # For managing fast selection switches of covers.. self.stop_loading = False self.loading = False self.current_job = 0 self.connect('destroy', self.__save_config) def __save(self, *data): """Save the cover and spawn the program to edit it if selected""" save_format = self.name_combo.get_active_text() # Allow use of patterns in creating cover filenames pattern = ArbitraryExtensionFileFromPattern( save_format.decode("utf-8")) filename = pattern.format(self.song) print_d("Using '%s' as filename based on %s" % (filename, save_format)) file_path = os.path.join(self.dirname, filename) if os.path.exists(file_path): resp = ConfirmFileReplace(self, file_path).run() if resp != ConfirmFileReplace.RESPONSE_REPLACE: return try: f = open(file_path, 'wb') f.write(self.current_data) f.close() except IOError: qltk.ErrorMessage(None, _('Saving failed'), _('Unable to save "%s".') % file_path).run() else: if self.open_check.get_active(): try: util.spawn([self.cmd.get_text(), file_path]) except: pass app.cover_manager.cover_changed([self.song]) self.main_win.destroy() def __save_config(self, widget): self.config_set('edit_cmd', self.cmd.get_text()) self.config_set('fn', self.name_combo.get_active_text()) def __update(self, loader, *data): """Update the picture while it's loading""" if self.stop_loading: return pixbuf = loader.get_pixbuf() def idle_set(): set_image_from_pbosf(self.image, pixbuf) GLib.idle_add(idle_set) def __scale_pixbuf(self, *data): if not self.current_pixbuf: return pixbuf = self.current_pixbuf if not self.window_fit.get_active(): pbosf = pixbuf else: alloc = self.scrolled.get_allocation() width = alloc.width height = alloc.height scale_factor = get_scale_factor(self) boundary = (width * scale_factor, height * scale_factor) pixbuf = scale(pixbuf, boundary, scale_up=False) pbosf = get_pbosf_for_pixbuf(self, pixbuf) set_image_from_pbosf(self.image, pbosf) def __close(self, loader, *data): if self.stop_loading: return self.current_pixbuf = loader.get_pixbuf() GLib.idle_add(self.__scale_pixbuf) def set_cover(self, url): thr = threading.Thread(target=self.__set_async, args=(url,)) thr.setDaemon(True) thr.start() def __set_async(self, url): """Manages various things: Fast switching of covers (aborting old HTTP requests), The image cache, etc.""" self.current_job += 1 job = self.current_job self.stop_loading = True while self.loading: time.sleep(0.05) self.stop_loading = False if job != self.current_job: return self.loading = True GLib.idle_add(self.button.set_sensitive, False) self.current_pixbuf = None pbloader = GdkPixbuf.PixbufLoader() pbloader.connect('closed', self.__close) # Look for cached images raw_data = None for entry in self.data_cache: if entry[0] == url: raw_data = entry[1] break if not raw_data: pbloader.connect('area-updated', self.__update) data_store = StringIO() try: request = urllib2.Request(url) request.add_header('User-Agent', USER_AGENT) url_sock = urllib2.urlopen(request) except urllib2.HTTPError: print_w(_("[albumart] HTTP Error: %s") % url) else: while not self.stop_loading: tmp = url_sock.read(1024 * 10) if not tmp: break pbloader.write(tmp) data_store.write(tmp) url_sock.close() if not self.stop_loading: raw_data = data_store.getvalue() self.data_cache.insert(0, (url, raw_data)) while 1: cache_sizes = [len(data[1]) for data in self.data_cache] if sum(cache_sizes) > self.max_cache_size: del self.data_cache[-1] else: break data_store.close() else: # Sleep for fast switching of cached images time.sleep(0.05) if not self.stop_loading: pbloader.write(raw_data) try: pbloader.close() except GLib.GError: pass self.current_data = raw_data if not self.stop_loading: GLib.idle_add(self.button.set_sensitive, True) self.loading = False
def __init__(self, library): super(MediaDevices, self).__init__(spacing=6) self.set_orientation(Gtk.Orientation.VERTICAL) self._register_instance() self.__cache = {} # Device list on the left pane swin = ScrolledWindow() swin.set_shadow_type(Gtk.ShadowType.IN) swin.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) self.pack_start(swin, True, True, 0) self.__view = view = AllTreeView() view.set_model(self.__devices) view.set_rules_hint(True) view.set_headers_visible(False) view.get_selection().set_mode(Gtk.SelectionMode.BROWSE) connect_obj(view.get_selection(), 'changed', self.__refresh, False) view.connect('popup-menu', self.__popup_menu, library) view.connect('row-activated', lambda *a: self.songs_activated()) swin.add(view) col = Gtk.TreeViewColumn("Devices") view.append_column(col) render = Gtk.CellRendererPixbuf() col.pack_start(render, False) col.add_attribute(render, 'icon-name', 1) self.__render = render = Gtk.CellRendererText() render.set_property('ellipsize', Pango.EllipsizeMode.END) render.connect('edited', self.__edited) col.pack_start(render, True) col.set_cell_data_func(render, MediaDevices.cell_data) hbox = Gtk.HBox(spacing=6) hbox.set_homogeneous(True) self.pack_start(Align(hbox, left=3, bottom=3), False, True, 0) # refresh button refresh = Button(_("_Refresh"), Icons.VIEW_REFRESH, Gtk.IconSize.MENU) self.__refresh_button = refresh connect_obj(refresh, 'clicked', self.__refresh, True) refresh.set_sensitive(False) hbox.pack_start(refresh, True, True, 0) # eject button eject = Button(_("_Eject"), Icons.MEDIA_EJECT, Gtk.IconSize.MENU) self.__eject_button = eject eject.connect('clicked', self.__eject) eject.set_sensitive(False) hbox.pack_start(eject, True, True, 0) # Device info on the right pane self.__header = table = Gtk.Table() table.set_col_spacings(8) self.__device_icon = icon = Gtk.Image() icon.set_size_request(48, 48) table.attach(icon, 0, 1, 0, 2, 0) self.__device_name = label = Gtk.Label() label.set_ellipsize(Pango.EllipsizeMode.END) label.set_alignment(0, 0) table.attach(label, 1, 3, 0, 1) self.__device_space = label = Gtk.Label() label.set_ellipsize(Pango.EllipsizeMode.END) label.set_alignment(0, 0.5) table.attach(label, 1, 2, 1, 2) self.__progress = progress = Gtk.ProgressBar() progress.set_size_request(150, -1) table.attach(progress, 2, 3, 1, 2, xoptions=0, yoptions=0) self.accelerators = Gtk.AccelGroup() key, mod = Gtk.accelerator_parse('F2') self.accelerators.connect(key, mod, 0, self.__rename) self.__statusbar = WaitLoadBar() for child in self.get_children(): child.show_all()
class AlbumArtWindow(qltk.Window, PluginConfigMixin): """The main window including the search list""" CONFIG_SECTION = PLUGIN_CONFIG_SECTION THUMB_SIZE = 50 def __init__(self, songs): super(AlbumArtWindow, self).__init__() self.image_cache = [] self.image_cache_size = 10 self.search_lock = False self.set_title(_('Album Art Downloader')) self.set_icon_name(Icons.EDIT_FIND) self.set_default_size(800, 550) image = CoverArea(self, songs[0]) self.liststore = Gtk.ListStore(object, object) self.treeview = treeview = AllTreeView(model=self.liststore) self.treeview.set_headers_visible(False) self.treeview.set_rules_hint(True) targets = [("text/uri-list", 0, 0)] targets = [Gtk.TargetEntry.new(*t) for t in targets] treeview.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, targets, Gdk.DragAction.COPY) treeselection = self.treeview.get_selection() treeselection.set_mode(Gtk.SelectionMode.SINGLE) treeselection.connect('changed', self.__select_callback, image) self.treeview.connect("drag-data-get", self.__drag_data_get, treeselection) rend_pix = Gtk.CellRendererPixbuf() img_col = Gtk.TreeViewColumn('Thumb') img_col.pack_start(rend_pix, False) def cell_data_pb(column, cell, model, iter_, *args): surface = model[iter_][0] cell.set_property("surface", surface) img_col.set_cell_data_func(rend_pix, cell_data_pb, None) treeview.append_column(img_col) rend_pix.set_property('xpad', 2) rend_pix.set_property('ypad', 2) border_width = self.get_scale_factor() * 2 rend_pix.set_property('width', self.THUMB_SIZE + 4 + border_width) rend_pix.set_property('height', self.THUMB_SIZE + 4 + border_width) def escape_data(data): for rep in ('\n', '\t', '\r', '\v'): data = data.replace(rep, ' ') return util.escape(' '.join(data.split())) def cell_data(column, cell, model, iter, data): cover = model[iter][1] esc = escape_data txt = '<b><i>%s</i></b>' % esc(cover['name']) txt += "\n<small>%s</small>" % ( _('from %(source)s') % { "source": util.italic(esc(cover['source'])) }) if 'resolution' in cover: txt += "\n" + _('Resolution: %s') % util.italic( esc(cover['resolution'])) if 'size' in cover: txt += "\n" + _('Size: %s') % util.italic(esc(cover['size'])) cell.markup = txt cell.set_property('markup', cell.markup) rend = Gtk.CellRendererText() rend.set_property('ellipsize', Pango.EllipsizeMode.END) info_col = Gtk.TreeViewColumn('Info', rend) info_col.set_cell_data_func(rend, cell_data) treeview.append_column(info_col) sw_list = Gtk.ScrolledWindow() sw_list.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) sw_list.set_shadow_type(Gtk.ShadowType.IN) sw_list.add(treeview) self.search_field = Gtk.Entry() self.search_button = Button(_("_Search"), Icons.EDIT_FIND) self.search_button.connect('clicked', self.start_search) self.search_field.connect('activate', self.start_search) widget_space = 5 search_hbox = Gtk.HBox(spacing=widget_space) search_hbox.pack_start(self.search_field, True, True, 0) search_hbox.pack_start(self.search_button, False, True, 0) self.progress = Gtk.ProgressBar() left_vbox = Gtk.VBox(spacing=widget_space) left_vbox.pack_start(search_hbox, False, True, 0) left_vbox.pack_start(sw_list, True, True, 0) hpaned = Paned() hpaned.set_border_width(widget_space) hpaned.pack1(left_vbox, shrink=False) hpaned.pack2(image, shrink=False) hpaned.set_position(275) self.add(hpaned) self.show_all() left_vbox.pack_start(self.progress, False, True, 0) song = songs[0] text = SEARCH_PATTERN.format(song) self.set_text(text) self.start_search() def __drag_data_get(self, view, ctx, sel, tid, etime, treeselection): model, iter = treeselection.get_selected() if not iter: return cover = model.get_value(iter, 1) sel.set_uris([cover['cover']]) def start_search(self, *data): """Start the search using the text from the text entry""" text = self.search_field.get_text() if not text or self.search_lock: return self.search_lock = True self.search_button.set_sensitive(False) self.progress.set_fraction(0) self.progress.set_text(_(u'Searching…')) self.progress.show() self.liststore.clear() self.search = search = CoverSearch(self.__search_callback) for eng in engines: if self.config_get_bool(CONFIG_ENG_PREFIX + eng['config_id'], True): search.add_engine(eng['class'], eng['replace']) search.start(text) # Focus the list self.treeview.grab_focus() self.connect("destroy", self.__destroy) def __destroy(self, *args): self.search.stop() def set_text(self, text): """set the text and move the cursor to the end""" self.search_field.set_text(text) self.search_field.emit('move-cursor', Gtk.MovementStep.BUFFER_ENDS, 0, False) def __select_callback(self, selection, image): model, iter = selection.get_selected() if not iter: return cover = model.get_value(iter, 1) image.set_cover(cover['cover']) def __add_cover_to_list(self, cover): try: pbloader = GdkPixbuf.PixbufLoader() pbloader.write(get_url(cover['thumbnail'])) pbloader.close() scale_factor = self.get_scale_factor() size = self.THUMB_SIZE * scale_factor - scale_factor * 2 pixbuf = pbloader.get_pixbuf().scale_simple( size, size, GdkPixbuf.InterpType.BILINEAR) pixbuf = add_border_widget(pixbuf, self) surface = get_surface_for_pixbuf(self, pixbuf) except (GLib.GError, IOError): pass else: def append(data): self.liststore.append(data) GLib.idle_add(append, [surface, cover]) def __search_callback(self, covers, progress): for cover in covers: self.__add_cover_to_list(cover) if self.progress.get_fraction() < progress: self.progress.set_fraction(progress) if progress >= 1: self.progress.set_text(_('Done')) GLib.timeout_add(700, self.progress.hide) self.search_button.set_sensitive(True) self.search_lock = False
class AlbumArtWindow(qltk.Window, PluginConfigMixin): """The main window including the search list""" CONFIG_SECTION = PLUGIN_CONFIG_SECTION THUMB_SIZE = 50 def __init__(self, songs): super(AlbumArtWindow, self).__init__() self.image_cache = [] self.image_cache_size = 10 self.search_lock = False self.set_title(_('Album Art Downloader')) self.set_icon_name(Icons.EDIT_FIND) self.set_default_size(800, 550) image = CoverArea(self, songs[0]) self.liststore = Gtk.ListStore(object, object) self.treeview = treeview = AllTreeView(model=self.liststore) self.treeview.set_headers_visible(False) self.treeview.set_rules_hint(True) targets = [("text/uri-list", 0, 0)] targets = [Gtk.TargetEntry.new(*t) for t in targets] treeview.drag_source_set( Gdk.ModifierType.BUTTON1_MASK, targets, Gdk.DragAction.COPY) treeselection = self.treeview.get_selection() treeselection.set_mode(Gtk.SelectionMode.SINGLE) treeselection.connect('changed', self.__select_callback, image) self.treeview.connect("drag-data-get", self.__drag_data_get, treeselection) rend_pix = Gtk.CellRendererPixbuf() img_col = Gtk.TreeViewColumn('Thumb') img_col.pack_start(rend_pix, False) def cell_data_pb(column, cell, model, iter_, *args): surface = model[iter_][0] cell.set_property("surface", surface) img_col.set_cell_data_func(rend_pix, cell_data_pb, None) treeview.append_column(img_col) rend_pix.set_property('xpad', 2) rend_pix.set_property('ypad', 2) border_width = self.get_scale_factor() * 2 rend_pix.set_property('width', self.THUMB_SIZE + 4 + border_width) rend_pix.set_property('height', self.THUMB_SIZE + 4 + border_width) def escape_data(data): for rep in ('\n', '\t', '\r', '\v'): data = data.replace(rep, ' ') return util.escape(' '.join(data.split())) def cell_data(column, cell, model, iter, data): cover = model[iter][1] esc = escape_data txt = '<b><i>%s</i></b>' % esc(cover['name']) txt += "\n<small>%s</small>" % ( _('from %(source)s') % { "source": util.italic(esc(cover['source']))}) if 'resolution' in cover: txt += "\n" + _('Resolution: %s') % util.italic( esc(cover['resolution'])) if 'size' in cover: txt += "\n" + _('Size: %s') % util.italic(esc(cover['size'])) cell.markup = txt cell.set_property('markup', cell.markup) rend = Gtk.CellRendererText() rend.set_property('ellipsize', Pango.EllipsizeMode.END) info_col = Gtk.TreeViewColumn('Info', rend) info_col.set_cell_data_func(rend, cell_data) treeview.append_column(info_col) sw_list = Gtk.ScrolledWindow() sw_list.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) sw_list.set_shadow_type(Gtk.ShadowType.IN) sw_list.add(treeview) search_labelraw = Gtk.Label('raw') search_labelraw.set_alignment(xalign=1.0, yalign=0.5) self.search_fieldraw = Gtk.Entry() self.search_fieldraw.connect('activate', self.start_search) self.search_fieldraw.connect('changed', self.__searchfieldchanged) search_labelclean = Gtk.Label('clean') search_labelclean.set_alignment(xalign=1.0, yalign=0.5) self.search_fieldclean = Gtk.Label() self.search_fieldclean.set_can_focus(False) self.search_fieldclean.set_alignment(xalign=0.0, yalign=0.5) self.search_radioraw = Gtk.RadioButton(group=None, label=None) self.search_radioraw.connect("toggled", self.__searchtypetoggled, "raw") self.search_radioclean = Gtk.RadioButton(group=self.search_radioraw, label=None) self.search_radioclean.connect("toggled", self.__searchtypetoggled, "clean") #note: set_active(False) appears to have no effect #self.search_radioraw.set_active( # self.config_get_bool('searchraw', False)) if self.config_get_bool('searchraw', False): self.search_radioraw.set_active(True) else: self.search_radioclean.set_active(True) search_labelresultsmax = Gtk.Label('limit') search_labelresultsmax.set_alignment(xalign=1.0, yalign=0.5) search_labelresultsmax.set_tooltip_text( _("Per engine 'at best' results limit")) search_adjresultsmax = Gtk.Adjustment( value=int(self.config_get("resultsmax", 3)), lower=1, upper=REQUEST_LIMIT_MAX, step_incr=1, page_incr=0, page_size=0) self.search_spinresultsmax = Gtk.SpinButton( adjustment=search_adjresultsmax, climb_rate=0.2, digits=0) self.search_spinresultsmax.set_alignment(xalign=0.5) self.search_spinresultsmax.set_can_focus(False) self.search_button = Button(_("_Search"), Icons.EDIT_FIND) self.search_button.connect('clicked', self.start_search) search_button_box = Gtk.Alignment() search_button_box.set(1, 0, 0, 0) search_button_box.add(self.search_button) search_table = Gtk.Table(rows=3, columns=4, homogeneous=False) search_table.attach(search_labelraw, 0, 1, 0, 1, xoptions=Gtk.AttachOptions.FILL, xpadding=6) search_table.attach(self.search_radioraw, 1, 2, 0, 1, xoptions=0, xpadding=0) search_table.attach(self.search_fieldraw, 2, 4, 0, 1) search_table.attach(search_labelclean, 0, 1, 1, 2, xoptions=Gtk.AttachOptions.FILL, xpadding=6) search_table.attach(self.search_radioclean, 1, 2, 1, 2, xoptions=0, xpadding=0) search_table.attach(self.search_fieldclean, 2, 4, 1, 2, xpadding=4) search_table.attach(search_labelresultsmax, 0, 2, 2, 3, xoptions=Gtk.AttachOptions.FILL, xpadding=6) search_table.attach(self.search_spinresultsmax, 2, 3, 2, 3, xoptions=Gtk.AttachOptions.FILL, xpadding=0) search_table.attach(search_button_box, 3, 4, 2, 3) widget_space = 5 self.progress = Gtk.ProgressBar() left_vbox = Gtk.VBox(spacing=widget_space) left_vbox.pack_start(search_table, False, True, 0) left_vbox.pack_start(sw_list, True, True, 0) hpaned = Paned() hpaned.set_border_width(widget_space) hpaned.pack1(left_vbox, shrink=False) hpaned.pack2(image, shrink=False) hpaned.set_position(275) self.add(hpaned) self.show_all() left_vbox.pack_start(self.progress, False, True, 0) self.connect('destroy', self.__save_config) song = songs[0] text = SEARCH_PATTERN.format(song) self.set_text(text) self.start_search() def __save_config(self, widget): self.config_set('searchraw', self.search_radioraw.get_active()) self.config_set('resultsmax', self.search_spinresultsmax.get_value_as_int()) def __drag_data_get(self, view, ctx, sel, tid, etime, treeselection): model, iter = treeselection.get_selected() if not iter: return cover = model.get_value(iter, 1) sel.set_uris([cover['cover']]) def __searchfieldchanged(self, *data): search = data[0].get_text() clean = cleanup_query(search, ' ') self.search_fieldclean.set_text('<b>' + clean + '</b>') self.search_fieldclean.set_use_markup(True) def __searchtypetoggled(self, *data): self.config_set('searchraw', self.search_radioraw.get_active()) def start_search(self, *data): """Start the search using the text from the text entry""" text = self.search_fieldraw.get_text() if not text or self.search_lock: return self.search_lock = True self.search_button.set_sensitive(False) self.progress.set_fraction(0) self.progress.set_text(_(u'Searching…')) self.progress.show() self.liststore.clear() self.search = search = CoverSearch(self.__search_callback) for eng in ENGINES: if self.config_get_bool( CONFIG_ENG_PREFIX + eng['config_id'], True): search.add_engine(eng['class'], eng['replace']) raw = self.search_radioraw.get_active() limit = self.search_spinresultsmax.get_value_as_int() search.start(text, raw, limit) # Focus the list self.treeview.grab_focus() self.connect("destroy", self.__destroy) def __destroy(self, *args): self.search.stop() def set_text(self, text): """set the text and move the cursor to the end""" self.search_fieldraw.set_text(text) self.search_fieldraw.emit('move-cursor', Gtk.MovementStep.BUFFER_ENDS, 0, False) def __select_callback(self, selection, image): model, iter = selection.get_selected() if not iter: return cover = model.get_value(iter, 1) image.set_cover(cover['cover']) def __add_cover_to_list(self, cover): try: pbloader = GdkPixbuf.PixbufLoader() pbloader.write(get_url(cover['thumbnail'])) pbloader.close() scale_factor = self.get_scale_factor() size = self.THUMB_SIZE * scale_factor - scale_factor * 2 pixbuf = pbloader.get_pixbuf().scale_simple(size, size, GdkPixbuf.InterpType.BILINEAR) pixbuf = add_border_widget(pixbuf, self) surface = get_surface_for_pixbuf(self, pixbuf) except (GLib.GError, IOError): pass else: def append(data): self.liststore.append(data) GLib.idle_add(append, [surface, cover]) def __search_callback(self, covers, progress): for cover in covers: self.__add_cover_to_list(cover) if self.progress.get_fraction() < progress: self.progress.set_fraction(progress) if progress >= 1: self.progress.set_text(_('Done')) GLib.timeout_add(700, self.progress.hide) self.search_button.set_sensitive(True) self.search_lock = False
class MultiStringEditor(qltk.UniqueWindow): """Dialog to edit a list of strings""" _WIDTH = 400 _HEIGHT = 300 def __init__(self, title, values=None): super(MultiStringEditor, self).__init__() self.data = values or [] self.set_border_width(12) self.set_title(title) self.set_default_size(self._WIDTH, self._HEIGHT) vbox = Gtk.VBox(spacing=12) hbox = Gtk.HBox(spacing=12) # Set up the model for this widget self.model = Gtk.ListStore(str) self.__fill_values() # Main view view = self.view = HintedTreeView(model=self.model) view.set_fixed_height_mode(True) view.set_headers_visible(False) sw = Gtk.ScrolledWindow() sw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) sw.set_shadow_type(Gtk.ShadowType.IN) sw.add(view) sw.set_size_request(-1, max(sw.size_request().height, 100)) hbox.pack_start(sw, True, True, 0) self.__setup_column(view) # Context menu menu = Gtk.Menu() remove_item = MenuItem(_("_Remove"), Icons.LIST_REMOVE) menu.append(remove_item) menu.show_all() view.connect('popup-menu', self.__popup, menu) connect_obj(remove_item, 'activate', self.__remove, view) # Add and Remove buttons vbbox = Gtk.VButtonBox() vbbox.set_layout(Gtk.ButtonBoxStyle.START) vbbox.set_spacing(6) add = Button(_("_Add"), Icons.LIST_ADD) add.connect("clicked", self.__add) vbbox.pack_start(add, False, True, 0) remove = Button(_("_Remove"), Icons.LIST_REMOVE) remove.connect("clicked", self.__remove) vbbox.pack_start(remove, False, True, 0) hbox.pack_start(vbbox, False, True, 0) vbox.pack_start(hbox, True, True, 0) # Close buttons bbox = Gtk.HButtonBox() self.remove_but = Button(_("_Remove"), Icons.LIST_REMOVE) self.remove_but.set_sensitive(False) close = Button(_("_Close"), Icons.WINDOW_CLOSE) connect_obj(close, 'clicked', qltk.Window.destroy, self) bbox.set_layout(Gtk.ButtonBoxStyle.END) bbox.pack_start(close, True, True, 0) vbox.pack_start(bbox, False, True, 0) # Finish up self.add(vbox) self.get_child().show_all() def __setup_column(self, view): def cdf(column, cell, model, iter, data): row = model[iter] if row: cell.set_property('text', row[0]) render = Gtk.CellRendererText() render.set_property('ellipsize', Pango.EllipsizeMode.END) column = Gtk.TreeViewColumn(None, render) column.set_cell_data_func(render, cdf) column.set_sizing(Gtk.TreeViewColumnSizing.FIXED) view.append_column(column) def __fill_values(self): for s in self.data: self.model.append(row=[s]) def get_strings(self): strings = [row[0] for row in self.model if row] return strings def __remove(self, *args): self.view.remove_selection() def __add(self, *args): dialog = GetStringDialog(self, _("Enter new value"), "", button_label=_("_Add"), button_icon=Icons.LIST_ADD) new = dialog.run() if new: self.model.append(row=[new]) def __popup(self, view, menu): return view.popup_menu(menu, 0, Gtk.get_current_event_time()).show()