def test_signal_count(self): m = ObjectStore() def handler(model, path, iter_, result): result[0] += 1 inserted = [0] m.connect("row-inserted", handler, inserted) changed = [0] m.connect("row-changed", handler, changed) m.append([1]) m.prepend([8]) m.insert(0, [1]) m.insert_before(None, [1]) m.insert_after(None, [1]) m.insert_many(0, [1, 2, 3]) m.append_many([1, 2, 3]) list(m.iter_append_many([1, 2, 3])) list(m.iter_append_many(xrange(3))) self.assertEqual(changed[0], 0) self.assertEqual(inserted[0], len(m))
class SearchWindow(Dialog): def __init__(self, parent, album): self.album = album self.album.sort(key=lambda s: sort_key(s)) self._resultlist = ObjectStore() self._releasecache = {} self._qthread = QueryThread() self.current_release = None super(SearchWindow, self).__init__(_("MusicBrainz lookup")) self.add_button(_("_Cancel"), Gtk.ResponseType.REJECT) self.add_icon_button(_("_Save"), Icons.DOCUMENT_SAVE, Gtk.ResponseType.ACCEPT) self.set_default_size(650, 500) self.set_border_width(5) self.set_transient_for(parent) save_button = self.get_widget_for_response(Gtk.ResponseType.ACCEPT) save_button.set_sensitive(False) vb = Gtk.VBox() vb.set_spacing(8) hb = Gtk.HBox() hb.set_spacing(8) sq = self.search_query = Gtk.Entry() sq.connect('activate', self._do_query) sq.set_text(build_query(album)) lbl = Gtk.Label(label=_("_Query:")) lbl.set_use_underline(True) lbl.set_mnemonic_widget(sq) stb = self.search_button = Gtk.Button(_('S_earch'), use_underline=True) stb.connect('clicked', self._do_query) hb.pack_start(lbl, False, True, 0) hb.pack_start(sq, True, True, 0) hb.pack_start(stb, False, True, 0) vb.pack_start(hb, False, True, 0) self.result_combo = ResultComboBox(self._resultlist) self.result_combo.connect('changed', self._result_changed) vb.pack_start(self.result_combo, False, True, 0) rhb = Gtk.HBox() rl = Gtk.Label() rl.set_markup(_("Results <i>(drag to reorder)</i>")) rl.set_alignment(0, 0.5) rhb.pack_start(rl, False, True, 0) rl = self.result_label = Gtk.Label(label="") rhb.pack_end(rl, False, True, 0) vb.pack_start(rhb, False, True, 0) sw = Gtk.ScrolledWindow() sw.set_shadow_type(Gtk.ShadowType.IN) sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.ALWAYS) rtv = self.result_treeview = ResultTreeView(self.album) rtv.set_border_width(8) sw.add(rtv) vb.pack_start(sw, True, True, 0) self.get_action_area().set_border_width(4) self.get_content_area().pack_start(vb, True, True, 0) self.connect('response', self._on_response) self.connect("destroy", self._on_destroy) stb.emit('clicked') self.get_child().show_all() def _on_destroy(self, *args): self._qthread.stop() def _on_response(self, widget, response): if response != Gtk.ResponseType.ACCEPT: self.destroy() return self._save() def _save(self): """Writes values to Song objects.""" year_only = pconfig.getboolean("year_only") albumartist = pconfig.getboolean("albumartist") artistsort = pconfig.getboolean("artist_sort") musicbrainz = pconfig.getboolean("standard") labelid = pconfig.getboolean("labelid2") for release, track, song in self.result_treeview.iter_tracks(): meta = build_song_data(release, track) apply_options(meta, year_only, albumartist, artistsort, musicbrainz, labelid) apply_to_song(meta, song) self.destroy() def _do_query(self, *args): """Search for album using the query text.""" query = util.gdecode(self.search_query.get_text()) if not query: self.result_label.set_markup("<b>%s</b>" % _("Please enter a query.")) self.search_button.set_sensitive(True) return self.result_label.set_markup("<i>%s</i>" % _(u"Searching…")) self._qthread.add(self._process_results, search_releases, query) def _process_results(self, results): """Called when a query result is returned. `results` is None if an error occurred. """ self._resultlist.clear() self.search_button.set_sensitive(True) if results is None: self.result_label.set_text(_("Error encountered. Please retry.")) self.search_button.set_sensitive(True) return self._resultlist.append_many(results) if len(results) > 0: self.result_label.set_markup("<i>%s</i>" % _(u"Loading result…")) self.result_combo.set_active(0) else: self.result_label.set_markup(_("No results found.")) def _result_changed(self, combo): """Called when a release is chosen from the result combo.""" idx = combo.get_active() if idx == -1: return release = self._resultlist[idx][0] if release.id in self._releasecache: self._update_result(self._releasecache[release.id]) else: self.result_label.set_markup("<i>%s</i>" % _(u"Loading result…")) self.result_treeview.update_release(None) self._qthread.add(self._update_result, release.fetch_full) def _update_result(self, full_release): """Callback for release detail download from result combo.""" if full_release is None: self.result_label.set_text(_("Error encountered. Please retry.")) return self.result_label.set_text(u"") self._releasecache.setdefault(full_release.id, full_release) self.result_treeview.update_release(full_release) self.current_release = full_release save_button = self.get_widget_for_response(Gtk.ResponseType.ACCEPT) save_button.set_sensitive(True)
class ResultTreeView(HintedTreeView, MultiDragTreeView): """The result treeview""" def __init__(self, album): self.album = album self._release = None self.model = ObjectStore() self.model.append_many(album) super(ResultTreeView, self).__init__(self.model) self.set_headers_clickable(True) self.set_rules_hint(True) self.set_reorderable(True) self.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE) mode = Pango.EllipsizeMode cols = [ (_('Filename'), self.__name_datafunc, True, mode.MIDDLE), (_('Disc'), self.__disc_datafunc, False, mode.END), (_('Track'), self.__track_datafunc, False, mode.END), (_('Title'), self.__title_datafunc, True, mode.END), (_('Artist'), self.__artist_datafunc, True, mode.END), ] for title, func, resize, mode in cols: render = Gtk.CellRendererText() render.set_property('ellipsize', mode) col = Gtk.TreeViewColumn(title, render) col.set_cell_data_func(render, func) col.set_resizable(resize) col.set_expand(resize) self.append_column(col) def iter_tracks(self): """Yields tuples of (release, track, song) combinations as they are shown in the list. """ tracks = self._tracks for idx, (song, ) in enumerate(self.model): if song is None: continue if idx >= len(tracks): continue track = tracks[idx] yield (self._release, track, song) def update_release(self, full_release): """Updates the TreeView, handling results with a different number of tracks than the album being tagged. Passing in None will reset the list. """ if full_release is not None: tracks = full_release.tracks else: tracks = [] for i in range(len(self.model), len(tracks)): self.model.append((None, )) for i in range(len(self.model), len(tracks), -1): if self.model[-1][0] is not None: break itr = self.model.get_iter_from_string(str(len(self.model) - 1)) self.model.remove(itr) self._release = full_release for row in self.model: self.model.row_changed(row.path, row.iter) # Only show artists if we have any has_artists = bool(filter(lambda t: t.artists, tracks)) col = self.get_column(4) col.set_visible(has_artists) # Only show discs column if we have more than one disc col = self.get_column(1) col.set_visible( bool(full_release) and bool(full_release.disc_count > 1)) self.columns_autosize() @property def _tracks(self): if self._release is None: return [] return self._release.tracks def __name_datafunc(self, col, cell, model, itr, data): song = model[itr][0] if song: cell.set_property('text', fsn2text(song("~basename"))) else: cell.set_property('text', '') def __track_datafunc(self, col, cell, model, itr, data): idx = model.get_path(itr)[0] if idx >= len(self._tracks): cell.set_property('text', '') else: cell.set_property('text', self._tracks[idx].tracknumber) def __disc_datafunc(self, col, cell, model, itr, data): idx = model.get_path(itr)[0] if idx >= len(self._tracks): cell.set_property('text', '') else: cell.set_property('text', self._tracks[idx].discnumber) def __title_datafunc(self, col, cell, model, itr, data): idx = model.get_path(itr)[0] if idx >= len(self._tracks): cell.set_property('text', '') else: cell.set_property('text', self._tracks[idx].title) def __artist_datafunc(self, col, cell, model, itr, data): idx = model.get_path(itr)[0] if idx >= len(self._tracks): cell.set_property('text', '') else: names = [a.name for a in self._tracks[idx].artists] cell.set_property('text', ", ".join(names))
class SearchWindow(Dialog): def __init__(self, parent, album): self.album = album self.album.sort(key=lambda s: sort_key(s)) self._resultlist = ObjectStore() self._releasecache = {} self._qthread = QueryThread() self.current_release = None super(SearchWindow, self).__init__(_("MusicBrainz lookup")) self.add_button(_("_Cancel"), Gtk.ResponseType.REJECT) self.add_icon_button(_("_Save"), Icons.DOCUMENT_SAVE, Gtk.ResponseType.ACCEPT) self.set_default_size(650, 500) self.set_border_width(5) self.set_transient_for(parent) save_button = self.get_widget_for_response(Gtk.ResponseType.ACCEPT) save_button.set_sensitive(False) vb = Gtk.VBox() vb.set_spacing(8) hb = Gtk.HBox() hb.set_spacing(8) sq = self.search_query = Gtk.Entry() sq.connect('activate', self._do_query) sq.set_text(build_query(album)) lbl = Gtk.Label(label=_("_Query:")) lbl.set_use_underline(True) lbl.set_mnemonic_widget(sq) stb = self.search_button = Gtk.Button(_('S_earch'), use_underline=True) stb.connect('clicked', self._do_query) hb.pack_start(lbl, False, True, 0) hb.pack_start(sq, True, True, 0) hb.pack_start(stb, False, True, 0) vb.pack_start(hb, False, True, 0) self.result_combo = ResultComboBox(self._resultlist) self.result_combo.connect('changed', self._result_changed) vb.pack_start(self.result_combo, False, True, 0) rhb = Gtk.HBox() rl = Gtk.Label() rl.set_markup(_("Results <i>(drag to reorder)</i>")) rl.set_alignment(0, 0.5) rhb.pack_start(rl, False, True, 0) rl = self.result_label = Gtk.Label(label="") rhb.pack_end(rl, False, True, 0) vb.pack_start(rhb, False, True, 0) sw = Gtk.ScrolledWindow() sw.set_shadow_type(Gtk.ShadowType.IN) sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.ALWAYS) rtv = self.result_treeview = ResultTreeView(self.album) rtv.set_border_width(8) sw.add(rtv) vb.pack_start(sw, True, True, 0) self.get_action_area().set_border_width(4) self.get_content_area().pack_start(vb, True, True, 0) self.connect('response', self._on_response) self.connect("destroy", self._on_destroy) stb.emit('clicked') self.get_child().show_all() def _on_destroy(self, *args): self._qthread.stop() def _on_response(self, widget, response): if response != Gtk.ResponseType.ACCEPT: self.destroy() return self._save() def _save(self): """Writes values to Song objects.""" year_only = pconfig.getboolean("year_only") albumartist = pconfig.getboolean("albumartist") artistsort = pconfig.getboolean("artist_sort") musicbrainz = pconfig.getboolean("standard") labelid = pconfig.getboolean("labelid2") for release, track, song in self.result_treeview.iter_tracks(): meta = build_song_data(release, track) apply_options( meta, year_only, albumartist, artistsort, musicbrainz, labelid) apply_to_song(meta, song) self.destroy() def _do_query(self, *args): """Search for album using the query text.""" query = util.gdecode(self.search_query.get_text()) if not query: self.result_label.set_markup( "<b>%s</b>" % _("Please enter a query.")) self.search_button.set_sensitive(True) return self.result_label.set_markup("<i>%s</i>" % _(u"Searching…")) self._qthread.add(self._process_results, search_releases, query) def _process_results(self, results): """Called when a query result is returned. `results` is None if an error occurred. """ self._resultlist.clear() self.search_button.set_sensitive(True) if results is None: self.result_label.set_text(_("Error encountered. Please retry.")) self.search_button.set_sensitive(True) return self._resultlist.append_many(results) if len(results) > 0: self.result_label.set_markup("<i>%s</i>" % _(u"Loading result…")) self.result_combo.set_active(0) else: self.result_label.set_markup(_("No results found.")) def _result_changed(self, combo): """Called when a release is chosen from the result combo.""" idx = combo.get_active() if idx == -1: return release = self._resultlist[idx][0] if release.id in self._releasecache: self._update_result(self._releasecache[release.id]) else: self.result_label.set_markup("<i>%s</i>" % _(u"Loading result…")) self.result_treeview.update_release(None) self._qthread.add(self._update_result, release.fetch_full) def _update_result(self, full_release): """Callback for release detail download from result combo.""" if full_release is None: self.result_label.set_text(_("Error encountered. Please retry.")) return self.result_label.set_text(u"") self._releasecache.setdefault(full_release.id, full_release) self.result_treeview.update_release(full_release) self.current_release = full_release save_button = self.get_widget_for_response(Gtk.ResponseType.ACCEPT) save_button.set_sensitive(True)
class ResultTreeView(HintedTreeView, MultiDragTreeView): """The result treeview""" def __init__(self, album): self.album = album self._release = None self.model = ObjectStore() self.model.append_many(album) super(ResultTreeView, self).__init__(self.model) self.set_headers_clickable(True) self.set_rules_hint(True) self.set_reorderable(True) self.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE) mode = Pango.EllipsizeMode cols = [ (_('Filename'), self.__name_datafunc, True, mode.MIDDLE), (_('Disc'), self.__disc_datafunc, False, mode.END), (_('Track'), self.__track_datafunc, False, mode.END), (_('Title'), self.__title_datafunc, True, mode.END), (_('Artist'), self.__artist_datafunc, True, mode.END), ] for title, func, resize, mode in cols: render = Gtk.CellRendererText() render.set_property('ellipsize', mode) col = Gtk.TreeViewColumn(title, render) col.set_cell_data_func(render, func) col.set_resizable(resize) col.set_expand(resize) self.append_column(col) def iter_tracks(self): """Yields tuples of (release, track, song) combinations as they are shown in the list. """ tracks = self._tracks for idx, (song, ) in enumerate(self.model): if song is None: continue if idx >= len(tracks): continue track = tracks[idx] yield (self._release, track, song) def update_release(self, full_release): """Updates the TreeView, handling results with a different number of tracks than the album being tagged. Passing in None will reset the list. """ if full_release is not None: tracks = full_release.tracks else: tracks = [] for i in range(len(self.model), len(tracks)): self.model.append((None, )) for i in range(len(self.model), len(tracks), -1): if self.model[-1][0] is not None: break itr = self.model.get_iter_from_string(str(len(self.model) - 1)) self.model.remove(itr) self._release = full_release for row in self.model: self.model.row_changed(row.path, row.iter) # Only show artists if we have any has_artists = bool(filter(lambda t: t.artists, tracks)) col = self.get_column(4) col.set_visible(has_artists) # Only show discs column if we have more than one disc col = self.get_column(1) col.set_visible( bool(full_release) and bool(full_release.disc_count > 1)) self.columns_autosize() @property def _tracks(self): if self._release is None: return [] return self._release.tracks def __name_datafunc(self, col, cell, model, itr, data): song = model[itr][0] if song: cell.set_property('text', path.fsdecode(song("~basename"))) else: cell.set_property('text', '') def __track_datafunc(self, col, cell, model, itr, data): idx = model.get_path(itr)[0] if idx >= len(self._tracks): cell.set_property('text', '') else: cell.set_property('text', self._tracks[idx].tracknumber) def __disc_datafunc(self, col, cell, model, itr, data): idx = model.get_path(itr)[0] if idx >= len(self._tracks): cell.set_property('text', '') else: cell.set_property('text', self._tracks[idx].discnumber) def __title_datafunc(self, col, cell, model, itr, data): idx = model.get_path(itr)[0] if idx >= len(self._tracks): cell.set_property('text', '') else: cell.set_property('text', self._tracks[idx].title) def __artist_datafunc(self, col, cell, model, itr, data): idx = model.get_path(itr)[0] if idx >= len(self._tracks): cell.set_property('text', '') else: names = [a.name for a in self._tracks[idx].artists] cell.set_property('text', ", ".join(names))
def test_append_many_set(self): m = ObjectStore() m.append_many(set()) m.append_many(set(range(10))) self.failUnlessEqual({r[0] for r in m}, set(range(10)))
def test_append_many(self): m = ObjectStore() m.append_many(range(10)) self.failUnlessEqual([r[0] for r in m], range(10))
class MatchListsTreeView(HintedTreeView, Generic[T]): _b_order: List[Optional[int]] def __init__(self, a_items: List[T], b_items: List[T], columns: List[ColumnSpec[T]]): self.model = ObjectStore() self.model.append_many(a_items) self._b_items = b_items super().__init__(self.model) self.set_headers_clickable(False) self.set_rules_hint(True) self.set_reorderable(False) self.get_selection().set_mode(Gtk.SelectionMode.NONE) def show_id(col, cell, model, itr, data): idx = model.get_path(itr)[0] imp_idx = self._b_order[idx] num = '_' if imp_idx is None else imp_idx + 1 cell.set_property('markup', f'<span weight="bold">{num}</span>') def df_for_a_items(a_attr_getter): def data_func(col, cell, model, itr, data): a_item = model[itr][0] text = '' if a_item is not None: text = a_attr_getter(a_item) cell.set_property('text', text) return data_func def df_for_b_items(b_attr_getter): def data_func(col, cell, model, itr, data): self._set_text(model, itr, cell, b_attr_getter) return data_func for c in columns: self._add_col(c.title, df_for_a_items(c.cell_text_getter), c.is_resizable) self._add_col('#', show_id, False) for c in columns: self._add_col(c.title, df_for_b_items(c.cell_text_getter), c.is_resizable) self._b_order = [] # Initialize the backing field of b_order self.b_order = list(range(len(b_items))) # Update it and rows self.update_b_items(b_items) def _add_col(self, title, func, resize): render = Gtk.CellRendererText() render.set_property('ellipsize', Pango.EllipsizeMode.END) col = Gtk.TreeViewColumn(title, render) col.set_cell_data_func(render, func) col.set_resizable(resize) col.set_expand(resize) self.append_column(col) def _set_text(self, model, itr, cell, get_attr): idx = model.get_path(itr)[0] text = '' if idx < len(self._b_order): it_idx = self._b_order[idx] if it_idx is not None: text = get_attr(self._b_items[it_idx]) cell.set_property('text', text) def update_b_items(self, b_items: List[T]): """ Updates the TreeView, handling results with a different number of b_items than there are a_items. """ self._b_items = b_items for i in range(len(self.model), len(b_items)): self.model.append((None, )) for i in range(len(self.model), len(b_items), -1): if self.model[-1][0] is not None: break itr = self.model.get_iter_from_string(str(len(self.model) - 1)) self.model.remove(itr) self._rows_changed() self.columns_autosize() def _rows_changed(self): for row in self.model: self.model.row_changed(row.path, row.iter) @property def b_order(self) -> List[Optional[int]]: return list(self._b_order) @b_order.setter def b_order(self, order: List[Optional[int]]): """ Supports a partial order list. For example, if there are 5 elements in the b_items list, you could supply [4, 1, 2]. This will result in an ascending order for the last 2 rows, so [0, 3]. """ if order == self._b_order: return b_len = len(self._b_items) if len(order) < b_len: # add missing indices for i in range(b_len): if i not in order: order.append(i) while len(order) < len(self.model): order.append(None) self._b_order = order self._rows_changed()