def __edit_tag_name(self, renderer, path, new_tag, model): new_tag = ' '.join(new_tag.splitlines()).lower() row = model[path] if new_tag == row[TAG]: return elif not self.__songinfo.can_change(row[TAG]): # Can't remove the old tag. title = _("Invalid tag") msg = _( "Invalid tag <b>%s</b>\n\nThe files currently" " selected do not support editing this tag.") % util.escape( row[TAG]) qltk.ErrorMessage(self, title, msg).run() elif not self.__songinfo.can_change(new_tag): # Can't add the new tag. title = _("Invalid tag") msg = _("Invalid tag <b>%s</b>\n\nThe files currently" " selected do not support editing this tag." ) % util.escape(new_tag) qltk.ErrorMessage(self, title, msg).run() else: if new_tag in massagers.tags: fmt = massagers.tags[new_tag] v = util.unescape(row[VALUE]) if not fmt.is_valid(v): qltk.WarningMessage( self, _("Invalid value"), _("Invalid value: <b>%(value)s</b>\n\n%(error)s") % { "value": row[VALUE], "error": fmt.error }).run() return value = fmt.validate(v) else: value = row[VALUE] value = util.unescape(value) if row[ORIGVALUE] is None: # The tag hasn't been saved yet, so we can just update # the name in the model, and the value, since it # may have been re-validated. row[TAG] = new_tag row[VALUE] = value else: # The tag has been saved, so delete the old tag and # add a new one with the old (or sanitized) value. row[RENAMED] = row[EDITED] = True row[ORIGTAG] = row[TAG] row[TAG] = new_tag
def __popup_menu(self, view, parent): menu = Gtk.Menu() view.ensure_popup_selection() model, rows = view.get_selection().get_selected_rows() can_change = min([model[path][CANEDIT] for path in rows]) items = [ SplitDisc, SplitTitle, SplitPerformer, SplitArranger, SplitValues, SplitPerformerFromTitle, SplitOriginalArtistFromTitle ] items.extend(self.handler.plugins) items.sort(key=lambda item: (item._order, item.__name__)) if len(rows) == 1: row = model[rows[0]] value = row[VALUE].decode('utf-8') text = util.unescape(value) multi = (value.split("<")[0] != value) for Item in items: if Item.tags and row[TAG] not in Item.tags: continue try: b = Item(row[TAG], text) except: util.print_exc() else: b.connect('activate', self.__menu_activate, view) if (not min( map(self.__songinfo.can_change, b.needs) + [1]) or multi): b.set_sensitive(False) menu.append(b) if menu.get_children(): menu.append(SeparatorMenuItem()) b = Gtk.ImageMenuItem.new_from_stock(Gtk.STOCK_REMOVE, None) b.connect('activate', self.__remove_tag, view) keyval, mod = Gtk.accelerator_parse("Delete") menu.__accels = Gtk.AccelGroup() b.add_accelerator('activate', menu.__accels, keyval, mod, Gtk.AccelFlags.VISIBLE) menu.append(b) menu.show_all() # Setting the menu itself to be insensitive causes it to not # be dismissed; see #473. for c in menu.get_children(): c.set_sensitive(can_change and c.get_property('sensitive')) menu.connect('selection-done', lambda m: m.destroy()) # XXX: Keep reference self.__menu = menu return view.popup_menu(menu, 3, Gtk.get_current_event_time())
def __edit_tag_name(self, renderer, path, new_tag, model): new_tag = " ".join(new_tag.splitlines()).lower() row = model[path] if new_tag == row[TAG]: return elif not self.__songinfo.can_change(row[TAG]): # Can't remove the old tag. title = _("Invalid tag") msg = _( "Invalid tag <b>%s</b>\n\nThe files currently" " selected do not support editing this tag." ) % util.escape(row[TAG]) qltk.ErrorMessage(self, title, msg).run() elif not self.__songinfo.can_change(new_tag): # Can't add the new tag. title = _("Invalid tag") msg = _( "Invalid tag <b>%s</b>\n\nThe files currently" " selected do not support editing this tag." ) % util.escape(new_tag) qltk.ErrorMessage(self, title, msg).run() else: if new_tag in massagers.tags: fmt = massagers.tags[new_tag] v = util.unescape(row[VALUE]) if not fmt.is_valid(v): qltk.WarningMessage( self, _("Invalid value"), _("Invalid value: <b>%(value)s</b>\n\n%(error)s") % {"value": row[VALUE], "error": fmt.error}, ).run() return value = fmt.validate(v) else: value = row[VALUE] value = util.unescape(value) if row[ORIGVALUE] is None: # The tag hasn't been saved yet, so we can just update # the name in the model, and the value, since it # may have been re-validated. row[TAG] = new_tag row[VALUE] = value else: # The tag has been saved, so delete the old tag and # add a new one with the old (or sanitized) value. row[RENAMED] = row[EDITED] = True row[ORIGTAG] = row[TAG] row[TAG] = new_tag
def __popup_menu(self, view, parent): menu = gtk.Menu() view.ensure_popup_selection() model, rows = view.get_selection().get_selected_rows() can_change = min([model[path][CANEDIT] for path in rows]) items = [ SplitDisc, SplitTitle, SplitPerformer, SplitArranger, SplitValues, SplitPerformerFromTitle, SplitOriginalArtistFromTitle, ] items.extend(self.handler.plugins) items.sort(key=lambda item: (item._order, item.__name__)) if len(rows) == 1: row = model[rows[0]] value = row[VALUE].decode("utf-8") text = util.unescape(value) multi = value.split("<")[0] != value for Item in items: if Item.tags and row[TAG] not in Item.tags: continue try: b = Item(row[TAG], text) except: util.print_exc() else: b.connect("activate", self.__menu_activate, view) if not min(map(self.__songinfo.can_change, b.needs) + [1]) or multi: b.set_sensitive(False) menu.append(b) if menu.get_children(): menu.append(gtk.SeparatorMenuItem()) b = gtk.ImageMenuItem(gtk.STOCK_REMOVE, gtk.ICON_SIZE_MENU) b.connect("activate", self.__remove_tag, view) keyval, mod = gtk.accelerator_parse("Delete") menu.__accels = gtk.AccelGroup() b.add_accelerator("activate", menu.__accels, keyval, mod, gtk.ACCEL_VISIBLE) menu.append(b) menu.show_all() # Setting the menu itself to be insensitive causes it to not # be dismissed; see #473. for c in menu.get_children(): c.set_sensitive(can_change and c.get_property("sensitive")) menu.connect("selection-done", lambda m: m.destroy()) return view.popup_menu(menu, 3, gtk.get_current_event_time())
def __value_editing_started(self, render, editable, path, model, library): try: if not editable.get_completion(): tag = model[path][TAG] completion = LibraryValueCompletion(tag, library) editable.set_completion(completion) except AttributeError: pass if isinstance(editable, gtk.Entry): editable.set_text(util.unescape(model[path][VALUE].split("<")[0]))
def __value_editing_started(self, render, editable, path, model, library): try: if not editable.get_completion(): tag = model[path][TAG] completion = LibraryValueCompletion(tag, library) editable.set_completion(completion) except AttributeError: pass if isinstance(editable, Gtk.Entry): editable.set_text(util.unescape(model[path][VALUE].split('<')[0]))
def __menu_activate(self, activator, view): model, (iter, ) = view.get_selection().get_selected_rows() row = model[iter] tag = row[TAG] value = util.unescape(row[VALUE].decode('utf-8')) vals = activator.activated(tag, value) replaced = False if vals and (len(vals) != 1 or vals[0][1] != value): for atag, aval in vals: if atag == tag and not replaced: replaced = True row[VALUE] = util.escape(aval) row[EDITED] = True else: self.__add_new_tag(model, atag, aval) elif vals: replaced = True if not replaced: row[EDITED] = row[DELETED] = True
def __menu_activate(self, activator, view): model, (iter,) = view.get_selection().get_selected_rows() row = model[iter] tag = row[TAG] value = util.unescape(row[VALUE].decode("utf-8")) vals = activator.activated(tag, value) replaced = False if vals and (len(vals) != 1 or vals[0][1] != value): for atag, aval in vals: if atag == tag and not replaced: replaced = True row[VALUE] = util.escape(aval) row[EDITED] = True else: self.__add_new_tag(model, atag, aval) elif vals: replaced = True if not replaced: row[EDITED] = row[DELETED] = True
def __save_files(self, save, revert, model, library): updated = {} deleted = {} added = {} renamed = {} for row in model: if row[EDITED] and not (row[DELETED] or row[RENAMED]): if row[ORIGVALUE] is not None: updated.setdefault(row[TAG], []) updated[row[TAG]].append( (decode(row[VALUE]), decode(row[ORIGVALUE]))) else: added.setdefault(row[TAG], []) added[row[TAG]].append(decode(row[VALUE])) if row[EDITED] and row[DELETED]: if row[ORIGVALUE] is not None: deleted.setdefault(row[TAG], []) deleted[row[TAG]].append(decode(row[ORIGVALUE])) if row[EDITED] and row[RENAMED] and not row[DELETED]: renamed.setdefault(row[TAG], []) renamed[row[TAG]].append( (decode(row[ORIGTAG]), decode(row[VALUE]), decode(row[ORIGVALUE]))) was_changed = set() songs = self.__songinfo.songs win = WritingWindow(self, len(songs)) win.show() for song in songs: if not song.valid() and not qltk.ConfirmAction( self, _("Tag may not be accurate"), _("<b>%s</b> changed while the program was running. " "Saving without refreshing your library may " "overwrite other changes to the song.\n\n" "Save this song anyway?") % util.escape(util.fsdecode(song("~basename")))).run(): break changed = False for key, values in updated.iteritems(): for (new_value, old_value) in values: new_value = util.unescape(new_value) if song.can_change(key): if old_value is None: song.add(key, new_value) else: song.change(key, old_value, new_value) changed = True for key, values in added.iteritems(): for value in values: value = util.unescape(value) if song.can_change(key): song.add(key, value) changed = True for key, values in deleted.iteritems(): for value in values: value = util.unescape(value) if song.can_change(key) and key in song: song.remove(key, value) changed = True save_rename = [] for new_tag, values in renamed.iteritems(): for old_tag, new_value, old_value in values: old_tag = util.unescape(old_tag) old_value = util.unescape(old_value) new_value = util.unescape(new_value) if (song.can_change(new_tag) and song.can_change(old_tag) and old_tag in song): if not is_special(new_value): song.remove(old_tag, old_value) save_rename.append((new_tag, new_value)) elif is_missing(new_value): value = strip_missing(old_value) song.remove(old_tag, old_value) save_rename.append((new_tag, new_value)) else: save_rename.append((new_tag, song[old_tag])) song.remove(old_tag, None) changed = True for tag, value in save_rename: song.add(tag, value) if changed: try: song.write() except: util.print_exc() qltk.ErrorMessage( self, _("Unable to save song"), _("Saving <b>%s</b> failed. The file " "may be read-only, corrupted, or you " "do not have permission to edit it.") % (util.escape(fsdecode(song('~basename'))))).run() library.reload(song, changed=was_changed) break was_changed.add(song) if win.step(): break win.destroy() library.changed(was_changed) for b in [save, revert]: b.set_sensitive(False)
def show_notification(self, song): """Returns True if showing the notification was successful""" if not song: return True try: if self.__enabled: # we are enabled try to work with the data we have and # keep it fresh if not self.__interface: iface, caps, spec = self.__get_interface() self.__interface = iface self.__caps = caps self.__spec_version = spec if "actions" in caps: self.__action_sig = iface.connect( 'g-signal', self._on_signal) else: iface = self.__interface caps = self.__caps spec = self.__spec_version else: # not enabled, just get everything temporary, # probably preview iface, caps, spec = self.__get_interface() except GLib.Error: print_w("[notify] %s" % _("Couldn't connect to notification daemon.")) self.__disconnect() return False strip_markup = lambda t: re.subn(r"\</?[iub]\>", "", t)[0] strip_links = lambda t: re.subn(r"\</?a.*?\>", "", t)[0] strip_images = lambda t: re.subn(r"\<img.*?\>", "", t)[0] title = XMLFromPattern(pconfig.gettext("titlepattern")) % song title = unescape(strip_markup(strip_links(strip_images(title)))) body = "" if "body" in caps: body = XMLFromPattern(pconfig.gettext("bodypattern")) % song if "body-markup" not in caps: body = strip_markup(body) if "body-hyperlinks" not in caps: body = strip_links(body) if "body-images" not in caps: body = strip_images(body) actions = [] if pconfig.getboolean("show_next_button") and "actions" in caps: actions = ["next", _("Next")] hints = { "desktop-entry": GLib.Variant('s', "io.github.quodlibet.QuodLibet"), } image_uri = self._get_image_uri(song) if image_uri: hints["image_path"] = GLib.Variant('s', image_uri) hints["image-path"] = GLib.Variant('s', image_uri) try: self.__last_id = iface.Notify('(susssasa{sv}i)', "Quod Libet", self.__last_id, image_uri, title, body, actions, hints, pconfig.getint("timeout")) except GLib.Error: print_w("[notify] %s" % _("Couldn't connect to notification daemon.")) self.__disconnect() return False # preview done, remove all references again if not self.__enabled: self.__disconnect() return True
def test_unescape_empty(self): self.failUnlessEqual(util.unescape(""), "")
def test_roundtrip(self): for s in ["foo&", "<&>", "&", "&", "<&testing&>amp;"]: esc = util.escape(s) self.failIfEqual(s, esc) self.failUnlessEqual(s, util.unescape(esc))
def show_notification(self, song): """Returns True if showing the notification was successful""" if not song: return True try: if self.__enabled: # we are enabled try to work with the data we have and # keep it fresh if not self.__interface: iface, caps, spec = self.__get_interface() self.__interface = iface self.__caps = caps self.__spec_version = spec if "actions" in caps: self.__action_sig = iface.connect_to_signal( "ActionInvoked", self.on_dbus_action) else: iface = self.__interface caps = self.__caps spec = self.__spec_version else: # not enabled, just get everything temporary, # propably preview iface, caps, spec = self.__get_interface() except dbus.DBusException: print_w("[notify] %s" % _("Couldn't connect to notification daemon.")) self.__disconnect() return False strip_markup = lambda t: re.subn("\</?[iub]\>", "", t)[0] strip_links = lambda t: re.subn("\</?a.*?\>", "", t)[0] strip_images = lambda t: re.subn("\<img.*?\>", "", t)[0] title = XMLFromPattern(get_conf_value("titlepattern")) % song title = unescape(strip_markup(strip_links(strip_images(title)))) body = "" if "body" in caps: body = XMLFromPattern(get_conf_value("bodypattern")) % song if "body-markup" not in caps: body = strip_markup(body) if "body-hyperlinks" not in caps: body = strip_links(body) if "body-images" not in caps: body = strip_images(body) actions = [] if get_conf_bool("show_next_button") and "actions" in caps: actions = ["next", _("Next")] hints = { "desktop-entry": "quodlibet", } image_uri = self._get_image_uri(song) if image_uri: hints["image_path"] = image_uri hints["image-path"] = image_uri try: self.__last_id = iface.Notify("Quod Libet", self.__last_id, image_uri, title, body, actions, hints, get_conf_int("timeout")) except dbus.DBusException: print_w("[notify] %s" % _("Couldn't connect to notification daemon.")) self.__disconnect() return False # preview done, remove all references again if not self.__enabled: self.__disconnect() return True
def __save_files(self, save, revert, model, library): updated = {} deleted = {} added = {} renamed = {} for row in model: if row[EDITED] and not (row[DELETED] or row[RENAMED]): if row[ORIGVALUE] is not None: updated.setdefault(row[TAG], []) updated[row[TAG]].append((util.decode(row[VALUE]), util.decode(row[ORIGVALUE]))) else: added.setdefault(row[TAG], []) added[row[TAG]].append(util.decode(row[VALUE])) if row[EDITED] and row[DELETED]: if row[ORIGVALUE] is not None: deleted.setdefault(row[TAG], []) deleted[row[TAG]].append(util.decode(row[ORIGVALUE])) if row[EDITED] and row[RENAMED] and not row[DELETED]: renamed.setdefault(row[TAG], []) renamed[row[TAG]].append( (util.decode(row[ORIGTAG]), util.decode(row[VALUE]), util.decode(row[ORIGVALUE])) ) was_changed = [] songs = self.__songinfo.songs win = WritingWindow(self, len(songs)) for song in songs: if ( not song.valid() and not qltk.ConfirmAction( self, _("Tag may not be accurate"), _( "<b>%s</b> changed while the program was running. " "Saving without refreshing your library may " "overwrite other changes to the song.\n\n" "Save this song anyway?" ) % util.escape(util.fsdecode(song("~basename"))), ).run() ): break changed = False for key, values in updated.iteritems(): for (new_value, old_value) in values: new_value = util.unescape(new_value) if song.can_change(key): if old_value is None: song.add(key, new_value) else: song.change(key, old_value, new_value) changed = True for key, values in added.iteritems(): for value in values: value = util.unescape(value) if song.can_change(key): song.add(key, value) changed = True for key, values in deleted.iteritems(): for value in values: value = util.unescape(value) if song.can_change(key) and key in song: song.remove(key, value) changed = True save_rename = [] for new_tag, values in renamed.iteritems(): for old_tag, new_value, old_value in values: old_tag = util.unescape(old_tag) old_value = util.unescape(old_value) new_value = util.unescape(new_value) if song.can_change(new_tag) and song.can_change(old_tag) and old_tag in song: if not is_special(new_value): song.remove(old_tag, old_value) save_rename.append((new_tag, new_value)) elif is_missing(new_value): value = strip_missing(old_value) song.remove(old_tag, old_value) save_rename.append((new_tag, new_value)) else: save_rename.append((new_tag, song[old_tag])) song.remove(old_tag, None) changed = True for tag, value in save_rename: song.add(tag, value) if changed: try: song.write() except: util.print_exc() qltk.ErrorMessage( self, _("Unable to save song"), _( "Saving <b>%s</b> failed. The file " "may be read-only, corrupted, or you " "do not have permission to edit it." ) % (util.escape(util.fsdecode(song("~basename")))), ).run() library.reload(song, changed=was_changed) break was_changed.append(song) if win.step(): break win.destroy() library.changed(was_changed) for b in [save, revert]: b.set_sensitive(False)
def show_notification(self, song): """Returns True if showing the notification was successful""" if not song: return True try: if self.__enabled: # we are enabled try to work with the data we have and # keep it fresh if not self.__interface: iface, caps, spec = self.__get_interface() self.__interface = iface self.__caps = caps self.__spec_version = spec if "actions" in caps: self.__action_sig = iface.connect_to_signal( "ActionInvoked", self.on_dbus_action) else: iface = self.__interface caps = self.__caps spec = self.__spec_version else: # not enabled, just get everything temporary, # propably preview iface, caps, spec = self.__get_interface() except dbus.DBusException: print_w("[notify] %s" % _("Couldn't connect to notification daemon.")) self.__disconnect() return False strip_markup = lambda t: re.subn("\</?[iub]\>", "", t)[0] strip_links = lambda t: re.subn("\</?a.*?\>", "", t)[0] strip_images = lambda t: re.subn("\<img.*?\>", "", t)[0] title = XMLFromPattern(pconfig.gettext("titlepattern")) % song title = unescape(strip_markup(strip_links(strip_images(title)))) body = "" if "body" in caps: body = XMLFromPattern(pconfig.gettext("bodypattern")) % song if "body-markup" not in caps: body = strip_markup(body) if "body-hyperlinks" not in caps: body = strip_links(body) if "body-images" not in caps: body = strip_images(body) actions = [] if pconfig.getboolean("show_next_button") and "actions" in caps: actions = ["next", _("Next")] hints = { "desktop-entry": "quodlibet", } image_uri = self._get_image_uri(song) if image_uri: hints["image_path"] = image_uri hints["image-path"] = image_uri try: self.__last_id = iface.Notify( "Quod Libet", self.__last_id, image_uri, title, body, actions, hints, pconfig.getint("timeout")) except dbus.DBusException: print_w("[notify] %s" % _("Couldn't connect to notification daemon.")) self.__disconnect() return False # preview done, remove all references again if not self.__enabled: self.__disconnect() return True
def show_notification(self, song): """Returns True if showing the notification was successful""" if not song: return True try: if self.__enabled: # we are enabled try to work with the data we have and # keep it fresh if not self.__interface: iface, caps, spec = self.__get_interface() self.__interface = iface self.__caps = caps self.__spec_version = spec if "actions" in caps: self.__action_sig = iface.connect_to_signal( "ActionInvoked", self.on_dbus_action) else: iface = self.__interface caps = self.__caps spec = self.__spec_version else: # not enabled, just get everything temporary, # propably preview iface, caps, spec = self.__get_interface() except dbus.DBusException: print_w("[notify] %s" % _("Couldn't connect to notification daemon.")) self.__disconnect() return False strip_markup = lambda t: re.subn("\</?[iub]\>", "", t)[0] strip_links = lambda t: re.subn("\</?a.*?\>", "", t)[0] strip_images = lambda t: re.subn("\<img.*?\>", "", t)[0] title = XMLFromPattern(get_conf_value("titlepattern")) % song title = unescape(strip_markup(strip_links(strip_images(title)))) body = "" if "body" in caps: body = XMLFromPattern(get_conf_value("bodypattern")) % song if "body-markup" not in caps: body = strip_markup(body) if "body-hyperlinks" not in caps: body = strip_links(body) if "body-images" not in caps: body = strip_images(body) image_path = "" if "icon-static" in caps: self.__image_fp = song.find_cover() if self.__image_fp: image_path = self.__image_fp.name is_temp = image_path.startswith(tempfile.gettempdir()) # If it is not an embeded cover, drop the file handle if not is_temp: self.__image_fp = None # spec recommends it, and it seems to work if image_path and spec >= (1, 1): image_path = URI.frompath(image_path) actions = [] if "actions" in caps: actions = ["next", _("Next")] hints = { "desktop-entry": "quodlibet", } try: self.__last_id = iface.Notify("Quod Libet", self.__last_id, image_path, title, body, actions, hints, get_conf_int("timeout")) except dbus.DBusException: print_w("[notify] %s" % _("Couldn't connect to notification daemon.")) self.__disconnect() return False # preview done, remove all references again if not self.__enabled: self.__disconnect() return True