Merge branch 'master' into artists-tag

Conflicts:
	NEWS.txt
This commit is contained in:
Michael Wiencek
2014-01-09 14:21:04 -06:00
48 changed files with 978 additions and 674 deletions

View File

@@ -1,8 +1,8 @@
[main]
host = https://www.transifex.net
host = https://www.transifex.com
[musicbrainz.picard]
file_filter = po/<lang>.po
source_file = po/picard.pot
source_lang = en
type = PO

View File

@@ -16,6 +16,8 @@
* Main window is now emitting a "selection_updated" signal, plugin api version bumps to 1.3.0
* Append system information to user-agent string
* Compilation tag/variable aligned with iTunes, set only for Various Artists type compilations.
* Ignore directories and files while indexing when show_hidden_files option is set to False (PICARD-528)
* Add ignore_regex option which allows one to ignore matching paths, can be set in Options > Advanced (PICARD-528)
* Added an "artists" tag to track metadata, based on the one in Jaikoz, which
contains the individual artist names from the artist credit.

14
README.md Normal file
View File

@@ -0,0 +1,14 @@
MusicBrainz Picard
==================
[MusicBrainz Picard](http://musicbrainz.org/doc/MusicBrainz_Picard) is a cross-platform (Linux/Mac OS X/Windows) application written in Python and is the official [MusicBrainz](http://musicbrainz.org) tagger.
Picard supports the majority of audio file formats, is capable of using audio fingerprints ([AcoustIDs](http://musicbrainz.org/doc/AcoustID)), performing CD lookups and [disc ID](http://musicbrainz.org/doc/Disc_ID) submissions, and it has excellent Unicode support. Additionally, there are several plugins available that extend Picard's features.
When tagging files, Picard uses an album-oriented approach. This approach allows it to utilize the MusicBrainz data as effectively as possible and correctly tag your music. For more information, [see the illustrated quick start guide to tagging](http://musicbrainz.org/doc/How_To_Tag_Files_With_Picard).
Picard is named after Captain Jean-Luc Picard from the TV series Star Trek: The Next Generation.
Binary downloads are available [here](http://musicbrainz.org/doc/MusicBrainz_Picard).
To submit bugs or improvements, please use [Picard bug tracker](http://tickets.musicbrainz.org/browse/PICARD).

View File

@@ -25,22 +25,42 @@ PICARD_ORG_NAME = "MusicBrainz"
PICARD_VERSION = (1, 3, 0, 'dev', 2)
def version_to_string(version_tuple, short=False):
assert len(version_tuple) == 5
assert version_tuple[3] in ('final', 'dev')
if short and version_tuple[3] == 'final':
if version_tuple[2] == 0:
version_str = '%d.%d' % version_tuple[:2]
class VersionError(Exception):
pass
def version_to_string(version, short=False):
if len(version) != 5:
raise VersionError("Length != 5")
if version[3] not in ('final', 'dev'):
raise VersionError("Should be either 'final' or 'dev'")
_version = []
for p in version:
try:
n = int(p)
except ValueError:
n = p
pass
_version.append(n)
version = tuple(_version)
if short and version[3] == 'final':
if version[2] == 0:
version_str = '%d.%d' % version[:2]
else:
version_str = '%d.%d.%d' % version_tuple[:3]
version_str = '%d.%d.%d' % version[:3]
else:
version_str = '%d.%d.%d%s%d' % version_tuple
version_str = '%d.%d.%d%s%d' % version
return version_str
_version_re = re.compile("(\d+)[._](\d+)[._](\d+)[._]?(dev|final)[._]?(\d+)$")
def version_from_string(version_str):
g = re.match(r"^(\d+).(\d+).(\d+)(dev|final)(\d+)$", version_str).groups()
return (int(g[0]), int(g[1]), int(g[2]), g[3], int(g[4]))
m = _version_re.search(version_str)
if m:
g = m.groups()
return (int(g[0]), int(g[1]), int(g[2]), g[3], int(g[4]))
raise VersionError("String '%s' do not match regex '%s'" % (version_str,
_version_re.pattern))
__version__ = PICARD_VERSION_STR = version_to_string(PICARD_VERSION)

View File

@@ -21,7 +21,7 @@ from collections import deque
from functools import partial
from PyQt4 import QtCore
from picard import config, log
from picard.const import ACOUSTID_KEY, FPCALC_NAMES
from picard.const import FPCALC_NAMES
from picard.util import find_executable
from picard.webservice import XmlNode

View File

@@ -24,7 +24,7 @@ from heapq import heappush, heappop
from PyQt4 import QtCore
from picard import config
from picard.metadata import Metadata
from picard.similarity import similarity2, similarity
from picard.similarity import similarity
from picard.ui.item import Item
from picard.util import format_time
@@ -189,10 +189,10 @@ class Cluster(QtCore.QObject, Item):
albumDict.add(album)))
artist_cluster_engine = ClusterEngine(artistDict)
artist_cluster = artist_cluster_engine.cluster(threshold)
artist_cluster_engine.cluster(threshold)
album_cluster_engine = ClusterEngine(albumDict)
album_cluster = album_cluster_engine.cluster(threshold)
album_cluster_engine.cluster(threshold)
# Arrange tracks into albums
albums = {}
@@ -458,7 +458,5 @@ class ClusterEngine(object):
self.idClusterIndex[match] = match0
del self.clusterBins[match1]
return self.clusterBins
def can_refresh(self):
return False

View File

@@ -17,10 +17,11 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import re
from operator import itemgetter
from PyQt4 import QtCore
from picard import (PICARD_APP_NAME, PICARD_ORG_NAME, PICARD_VERSION,
version_to_string, version_from_string, log)
version_to_string, version_from_string)
from picard.util import LockableObject, rot13
@@ -84,7 +85,7 @@ class Config(QtCore.QSettings):
TextOption("application", "version", '0.0.0dev0')
self._version = version_from_string(self.application["version"])
self._upgrade_hooks = []
self._upgrade_hooks = dict()
def switchProfile(self, profilename):
"""Sets the current profile."""
@@ -94,17 +95,15 @@ class Config(QtCore.QSettings):
else:
raise KeyError("Unknown profile '%s'" % (profilename,))
def register_upgrade_hook(self, to_version_str, func, *args):
def register_upgrade_hook(self, func, *args):
"""Register a function to upgrade from one config version to another"""
to_version = version_from_string(to_version_str)
to_version = version_from_string(func.__name__)
assert to_version <= PICARD_VERSION, "%r > %r !!!" % (to_version, PICARD_VERSION)
hook = {
'to': to_version,
self._upgrade_hooks[to_version] = {
'func': func,
'args': args,
'done': False
}
self._upgrade_hooks.append(hook)
def run_upgrade_hooks(self):
"""Executes registered functions to upgrade config version to the latest"""
@@ -118,29 +117,30 @@ class Config(QtCore.QSettings):
version_to_string(PICARD_VERSION)
))
return
# sort upgrade hooks by version
self._upgrade_hooks.sort(key=itemgetter("to"))
for hook in self._upgrade_hooks:
if self._version < hook['to']:
for version in sorted(self._upgrade_hooks):
hook = self._upgrade_hooks[version]
if self._version < version:
try:
hook['func'](*hook['args'])
except Exception as e:
except:
import traceback
raise ConfigUpgradeError(
"Error during config upgrade from version %s to %s "
"using %s(): %s" % (
"using %s():\n%s" % (
version_to_string(self._version),
version_to_string(hook['to']),
hook['func'].__name__, e
version_to_string(version),
hook['func'].__name__,
traceback.format_exc()
))
else:
hook['done'] = True
self._version = hook['to']
self._version = version
self._write_version()
else:
# hook is not applicable, mark as done
hook['done'] = True
if all(map(itemgetter("done"), self._upgrade_hooks)):
if all(map(itemgetter("done"), self._upgrade_hooks.values())):
# all hooks were executed, ensure config is marked with latest version
self._version = PICARD_VERSION
self._write_version()

110
picard/config_upgrade.py Normal file
View File

@@ -0,0 +1,110 @@
# -*- coding: utf-8 -*-
#
# Picard, the next-generation MusicBrainz tagger
# Copyright (C) 2013-2014 Laurent Monin
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
from PyQt4 import QtGui
import re
from picard import (log, config)
# TO ADD AN UPGRADE HOOK:
# ----------------------
# add a function here, named after the version you want upgrade to
# ie. upgrade_to_v1_0_0_dev_1() for 1.0.0dev1
# register it in upgrade_config()
# and modify PICARD_VERSION to match it
#
_s = config.setting
# In version 1.0, the file naming formats for single and various
# artist releases were merged.
def upgrade_to_v1_0_0_final_0():
def remove_va_file_naming_format(merge=True):
if merge:
_s["file_naming_format"] = (
"$if($eq(%%compilation%%,1),\n$noop(Various Artist "
"albums)\n%s,\n$noop(Single Artist Albums)\n%s)" % (
_s["va_file_naming_format"].toString(),
_s["file_naming_format"]
))
_s.remove("va_file_naming_format")
_s.remove("use_va_format")
if ("va_file_naming_format" in _s and
"use_va_format" in _s):
msgbox = QtGui.QMessageBox()
if _s["use_va_format"].toBool():
remove_va_file_naming_format()
msgbox.information(msgbox,
_("Various Artists file naming scheme removal"),
_("The separate file naming scheme for various artists "
"albums has been removed in this version of Picard.\n"
"Your file naming scheme has automatically been "
"merged with that of single artist albums."),
QtGui.QMessageBox.Ok)
elif (_s["va_file_naming_format"].toString() !=
r"$if2(%albumartist%,%artist%)/%album%/$if($gt(%totaldis"
"cs%,1),%discnumber%-,)$num(%tracknumber%,2) %artist% - "
"%title%"):
answer = msgbox.question(msgbox,
_("Various Artists file naming scheme removal"),
_("The separate file naming scheme for various artists "
"albums has been removed in this version of Picard.\n"
"You currently do not use this option, but have a "
"separate file naming scheme defined.\n"
"Do you want to remove it or merge it with your file "
"naming scheme for single artist albums?"),
_("Merge"), _("Remove"))
if answer:
remove_va_file_naming_format(merge=False)
else:
remove_va_file_naming_format()
else:
# default format, disabled
remove_va_file_naming_format(merge=False)
def upgrade_to_v1_3_0_dev_1():
if "windows_compatible_filenames" in _s:
_s["windows_compatibility"] = _s["windows_compatible_filenames"]
_s.remove("windows_compatible_filenames")
log.info(_('Config upgrade: option "windows_compatible_filenames" '
' was renamed "windows_compatibility" (PICARD-110).'))
def upgrade_to_v1_3_0_dev_2():
if "preserved_tags" in _s:
_s["preserved_tags"] = re.sub(r"\s+", ",", _s["preserved_tags"].strip())
log.info(_('Config upgrade: option "preserved_tags" is now using '
'comma instead of spaces as tag separator (PICARD-536).'))
def upgrade_config():
cfg = config._config
cfg.register_upgrade_hook(upgrade_to_v1_0_0_final_0)
cfg.register_upgrade_hook(upgrade_to_v1_3_0_dev_1)
cfg.register_upgrade_hook(upgrade_to_v1_3_0_dev_2)
cfg.run_upgrade_hooks()

View File

@@ -19,7 +19,6 @@
import os
import sys
import re
# Install gettext "noop" function in case const.py gets imported directly.
import __builtin__

View File

@@ -24,10 +24,9 @@
import json
import re
import traceback
import picard.webservice
from functools import partial
from picard import config, log
from picard.metadata import Metadata, is_front_image
from picard.metadata import is_front_image
from picard.util import mimetype, parse_amazon_url
from picard.const import CAA_HOST, CAA_PORT
from PyQt4.QtCore import QUrl, QObject

View File

@@ -29,11 +29,9 @@ from operator import itemgetter
from collections import defaultdict
from PyQt4 import QtCore
from picard import config, log
from picard.track import Track
from picard.metadata import Metadata
from picard.ui.item import Item
from picard.script import ScriptParser
from picard.similarity import similarity2
from picard.util import (
decode_filename,
encode_filename,
@@ -144,7 +142,7 @@ class File(QtCore.QObject, Item):
def has_error(self):
return self.state == File.ERROR
def _load(self):
def _load(self, filename):
"""Load metadata from the file."""
raise NotImplementedError
@@ -281,6 +279,8 @@ class File(QtCore.QObject, Item):
new_filename = os.path.basename(new_filename)
new_filename = make_short_filename(new_dirname, new_filename,
config.setting['windows_compatibility'], config.setting['windows_compatibility_drive_root'])
# TODO: move following logic under util.filenaming
# (and reconsider its necessity)
# win32 compatibility fixes
if settings['windows_compatibility'] or sys.platform == 'win32':
new_filename = new_filename.replace('./', '_/').replace('.\\', '_\\')
@@ -359,8 +359,8 @@ class File(QtCore.QObject, Item):
# image multiple times
if (os.path.exists(new_filename) and
os.path.getsize(new_filename) == len(data)):
log.debug("Identical file size, not saving %r", image_filename)
continue
log.debug("Identical file size, not saving %r", image_filename)
continue
log.debug("Saving cover images to %r", image_filename)
new_dirname = os.path.dirname(image_filename)
if not os.path.isdir(new_dirname):

View File

@@ -22,7 +22,7 @@ import struct
from struct import pack, unpack
import mutagen
from mutagen._util import insert_bytes
from mutagen.id3 import ID3, Frame, Frames, Frames_2_2, TextFrame, TORY, \
from mutagen.id3 import ID3, Frames, Frames_2_2, TextFrame, TORY, \
TYER, TIME, APIC, IPLS, TDAT, BitPaddedInt, MakeID3v1

View File

@@ -1,3 +1,22 @@
# -*- coding: utf-8 -*-
#
# Picard, the next-generation MusicBrainz tagger
# Copyright (C) 2013 Laurent Monin
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import gettext
import locale
import os.path

View File

@@ -62,7 +62,7 @@ class Logger(object):
for func in self._receivers:
try:
thread.to_main(func, level, time, message)
except Exception as e:
except:
import traceback
traceback.print_exc()

View File

@@ -21,8 +21,7 @@ import traceback
from collections import defaultdict
from functools import partial
from itertools import combinations
from PyQt4 import QtCore
from picard import config, log
from picard import log
from picard.metadata import Metadata
from picard.dataobj import DataObject
from picard.mbxml import media_formats_from_node, label_info_from_node

View File

@@ -22,11 +22,9 @@ from PyQt4 import QtGui, QtCore
import getopt
import os.path
import re
import shutil
import signal
import sys
from collections import deque
from functools import partial
from itertools import chain
@@ -61,16 +59,17 @@ from picard.track import Track, NonAlbumTrack
from picard.releasegroup import ReleaseGroup
from picard.collection import load_user_collections
from picard.ui.mainwindow import MainWindow
from picard.ui.itemviews import BaseTreeView
from picard.plugin import PluginManager
from picard.acoustidmanager import AcoustIDManager
from picard.config_upgrade import upgrade_config
from picard.util import (
decode_filename,
encode_filename,
thread,
mbid_validate,
check_io_encoding,
uniqify
uniqify,
is_hidden_path,
)
from picard.webservice import XmlWebService
@@ -127,10 +126,12 @@ class Tagger(QtGui.QApplication):
check_io_encoding()
self._upgrade_config()
# Must be before config upgrade because upgrade dialogs need to be
# translated
setup_gettext(localedir, config.setting["ui_language"], log.debug)
upgrade_config()
self.xmlws = XmlWebService()
load_user_collections()
@@ -162,64 +163,6 @@ class Tagger(QtGui.QApplication):
self.nats = None
self.window = MainWindow()
def _upgrade_config(self):
cfg = config._config
# In version 1.0, the file naming formats for single and various
# artist releases were merged.
def upgrade_to_v1_0():
def remove_va_file_naming_format(merge=True):
if merge:
config.setting["file_naming_format"] = (
"$if($eq(%compilation%,1),\n$noop(Various Artist "
"albums)\n%s,\n$noop(Single Artist Albums)\n%s)" % (
config.setting["va_file_naming_format"].toString(),
config.setting["file_naming_format"]
))
config.setting.remove("va_file_naming_format")
config.setting.remove("use_va_format")
if ("va_file_naming_format" in config.setting and
"use_va_format" in config.setting):
if config.setting["use_va_format"].toBool():
remove_va_file_naming_format()
self.window.show_va_removal_notice()
elif (config.setting["va_file_naming_format"].toString() !=
r"$if2(%albumartist%,%artist%)/%album%/$if($gt(%totaldis"
"cs%,1),%discnumber%-,)$num(%tracknumber%,2) %artist% - "
"%title%"):
if self.window.confirm_va_removal():
remove_va_file_naming_format(merge=False)
else:
remove_va_file_naming_format()
else:
# default format, disabled
remove_va_file_naming_format(merge=False)
def upgrade_to_v1_3():
_s = config.setting
# the setting `windows_compatible_filenames` has been renamed
# to `windows_compatibility`
if "windows_compatible_filenames" in _s:
_s["windows_compatibility"] = _s["windows_compatible_filenames"]
_s.remove("windows_compatible_filenames")
log.debug("Config upgrade: windows_compatible_filenames "
"renamed windows_compatibility")
# preserved_tags spaces to comma separator, PICARD-536
if "preserved_tags" in _s:
_s["preserved_tags"] = re.sub(r"\s+", ",", _s["preserved_tags"].strip())
log.debug("Config upgrade: convert preserved_tags separator "
"from spaces to comma")
cfg.register_upgrade_hook("1.0.0final0", upgrade_to_v1_0)
cfg.register_upgrade_hook("1.3.0dev2", upgrade_to_v1_3)
cfg.run_upgrade_hooks()
def move_files_to_album(self, files, albumid=None, album=None):
"""Move `files` to tracks on album `albumid`."""
if album is None:
@@ -329,9 +272,20 @@ class Tagger(QtGui.QApplication):
def add_files(self, filenames, target=None):
"""Add files to the tagger."""
log.debug("Adding files %r", filenames)
ignoreregex = None
pattern = config.setting['ignore_regex']
if pattern:
ignoreregex = re.compile(pattern)
ignore_hidden = not config.persist["show_hidden_files"]
new_files = []
for filename in filenames:
filename = os.path.normpath(os.path.realpath(filename))
if ignore_hidden and is_hidden_path(filename):
log.debug("File ignored (hidden): %s" % (filename))
continue
if ignoreregex is not None and ignoreregex.search(filename):
log.info("File ignored (matching %s): %s" % (pattern, filename))
continue
if filename not in self.files:
file = open_file(filename)
if file:

View File

@@ -50,7 +50,8 @@ class CDLookupDialog(QtGui.QDialog):
item.setData(0, QtCore.Qt.UserRole, QtCore.QVariant(release.id))
self.ui.release_list.setCurrentItem(self.ui.release_list.topLevelItem(0))
self.ui.ok_button.setEnabled(True)
[self.ui.release_list.resizeColumnToContents(i) for i in range(self.ui.release_list.columnCount() - 1)]
for i in range(self.ui.release_list.columnCount() - 1):
self.ui.release_list.resizeColumnToContents(i)
# Sort by descending date, then ascending country
self.ui.release_list.sortByColumn(3, QtCore.Qt.AscendingOrder)
self.ui.release_list.sortByColumn(2, QtCore.Qt.DescendingOrder)

View File

@@ -1,4 +1,4 @@
# -*- coding: UTF-8 -*-
# -*- coding: utf-8 -*-
#
# Picard, the next-generation MusicBrainz tagger
# Copyright (C) 2006-2007 Lukáš Lalinský

View File

@@ -18,7 +18,6 @@
from PyQt4 import QtCore, QtGui
from PyQt4.QtGui import QIcon
from picard import config
from picard.util import icontheme
from picard.ui.ui_infostatus import Ui_InfoStatus

View File

@@ -308,7 +308,9 @@ class BaseTreeView(QtGui.QTreeWidget):
action.setChecked(True)
action.triggered.connect(partial(obj.switch_release_version, version["id"]))
_add_other_versions() if obj.release_group.loaded else \
if obj.release_group.loaded:
_add_other_versions()
else:
obj.release_group.load_versions(_add_other_versions)
releases_menu.setEnabled(True)
else:

View File

@@ -22,7 +22,6 @@ from PyQt4 import QtCore, QtGui
import sys
import os.path
from functools import partial
from picard import config, log
from picard.file import File
from picard.track import Track
@@ -669,23 +668,6 @@ class MainWindow(QtGui.QMainWindow):
from picard.ui.logview import HistoryView
HistoryView(self).show()
def confirm_va_removal(self):
return QtGui.QMessageBox.question(self,
_("Various Artists file naming scheme removal"),
_("""The separate file naming scheme for various artists albums has been
removed in this version of Picard. You currently do not use the this option,
but have a separate file naming scheme defined. Do you want to remove it or
merge it with your file naming scheme for single artist albums?"""),
_("Merge"), _("Remove"))
def show_va_removal_notice(self):
QtGui.QMessageBox.information(self,
_("Various Artists file naming scheme removal"),
_("""The separate file naming scheme for various artists albums has been
removed in this version of Picard. Your file naming scheme has automatically
been merged with that of single artist albums."""),
QtGui.QMessageBox.Ok)
def open_bug_report(self):
webbrowser2.goto('troubleshooting')

View File

@@ -290,7 +290,9 @@ class MetadataBox(QtGui.QTableWidget):
self.set_tag_values(tag, [""])
def remove_selected_tags(self):
(self.remove_tag(tag) for tag in self.selected_tags() if self.tag_is_removable(tag))
for tag in self.selected_tags():
if self.tag_is_removable(tag):
self.remove_tag(tag)
def tag_is_removable(self, tag):
return self.tag_diff.status[tag] & TagStatus.NotRemovable == 0

View File

@@ -33,6 +33,7 @@ class OptionsPage(QtGui.QWidget):
PARENT = None
SORT_ORDER = 1000
ACTIVE = True
STYLESHEET_ERROR = "QWidget { background-color: #f55; color: white; font-weight:bold }"
def info(self):
raise NotImplementedError

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# Picard, the next-generation MusicBrainz tagger
# Copyright (C) 2006 Lukáš Lalinský
# Copyright (C) 2006-2014 Lukáš Lalinský
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -75,7 +75,7 @@ Discid %(discid-version)s
Thank you for using Picard. Picard relies on the MusicBrainz database, which is operated by the MetaBrainz Foundation with the help of thousands of volunteers. If you like this application please consider donating to the MetaBrainz Foundation to keep the service running.</p>
<p align="center"><a href="%(picard-donate-url)s">Donate now!</a></p>
<p align="center"><strong>Credits</strong><br/>
<small>Copyright © 2004-2011 Robert Kaye, Lukáš Lalinský and others%(translator-credits)s</small></p>
<small>Copyright © 2004-2014 Robert Kaye, Lukáš Lalinský and others%(translator-credits)s</small></p>
<p align="center"><a href="%(picard-doc-url)s">%(picard-doc-url)s</a></p>
""") % args
self.ui.label.setOpenExternalLinks(True)

View File

@@ -17,7 +17,12 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
from picard.ui.options import OptionsPage, register_options_page
from PyQt4.QtGui import QPalette, QColor
import re
from picard import config
from picard.ui.options import OptionsPage, OptionsCheckError, register_options_page
from picard.ui.ui_options_advanced import Ui_AdvancedOptionsPage
class AdvancedOptionsPage(OptionsPage):
@@ -26,7 +31,39 @@ class AdvancedOptionsPage(OptionsPage):
TITLE = N_("Advanced")
PARENT = None
SORT_ORDER = 90
ACTIVE = False
ACTIVE = True
options = [
config.TextOption("setting", "ignore_regex", ""),
]
def __init__(self, parent=None):
super(AdvancedOptionsPage, self).__init__(parent)
self.ui = Ui_AdvancedOptionsPage()
self.ui.setupUi(self)
self.ui.ignore_regex.textChanged.connect(self.live_checker)
def load(self):
self.ui.ignore_regex.setText(config.setting["ignore_regex"])
def save(self):
config.setting["ignore_regex"] = unicode(self.ui.ignore_regex.text())
def live_checker(self, text):
self.ui.regex_error.setStyleSheet("")
self.ui.regex_error.setText("")
try:
self.check()
except OptionsCheckError as e:
self.ui.regex_error.setStyleSheet(self.STYLESHEET_ERROR)
self.ui.regex_error.setText(e.info)
return
def check(self):
try:
re.compile(unicode(self.ui.ignore_regex.text()))
except re.error as e:
raise OptionsCheckError(_("Regex Error"), str(e))
register_options_page(AdvancedOptionsPage)

View File

@@ -19,7 +19,6 @@
from PyQt4 import QtCore, QtGui
from picard import config
from picard.plugin import ExtensionPoint
from picard.util import webbrowser2
from picard.ui.util import StandardButton
from picard.ui.options import (

View File

@@ -18,7 +18,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import os
from PyQt4 import QtCore, QtGui
from PyQt4 import QtGui
from picard import config
from picard.util import webbrowser2, find_executable
from picard.const import FPCALC_NAMES

View File

@@ -17,7 +17,6 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
from PyQt4 import QtCore
from picard import config
from picard.ui.options import OptionsPage, register_options_page
from picard.ui.ui_options_metadata import Ui_MetadataOptionsPage

View File

@@ -270,8 +270,6 @@ class RenamingOptionsPage(OptionsPage):
file.metadata['musicbrainz_releasetrackid'] = 'eac99807-93d4-3668-9714-fa0c1b487ccf'
return file
STYLESHEET_ERROR = "QWidget { background-color: #f55; color: white; font-weight:bold }"
def move_files_to_browse(self):
path = QtGui.QFileDialog.getExistingDirectory(self, "", self.ui.move_files_to.text())
if path:

View File

@@ -70,8 +70,6 @@ class ScriptingOptionsPage(OptionsPage):
config.TextOption("setting", "tagger_script", ""),
]
STYLESHEET_ERROR = "QWidget { background-color: #f55; color: white; font-weight:bold }"
def __init__(self, parent=None):
super(ScriptingOptionsPage, self).__init__(parent)
self.ui = Ui_ScriptingOptionsPage()

View File

@@ -17,7 +17,7 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
from PyQt4 import QtCore, QtGui
from PyQt4 import QtGui
from picard import config
from picard.ui.ui_passworddialog import Ui_PasswordDialog
from picard.util import rot13

View File

@@ -66,6 +66,6 @@ class Ui_Dialog(object):
Dialog.setWindowTitle(_("CD Lookup"))
self.label.setText(_("The following releases on MusicBrainz match the CD:"))
self.ok_button.setText(_("OK"))
self.lookup_button.setText(_(" Lookup manually "))
self.lookup_button.setText(_("Lookup manually"))
self.cancel_button.setText(_("Cancel"))

View File

@@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'ui/options_advanced.ui'
#
# Created: Wed Dec 25 02:35:20 2013
# by: PyQt4 UI code generator 4.9.3
#
# WARNING! All changes made in this file will be lost!
from PyQt4 import QtCore, QtGui
try:
_fromUtf8 = QtCore.QString.fromUtf8
except AttributeError:
_fromUtf8 = lambda s: s
class Ui_AdvancedOptionsPage(object):
def setupUi(self, AdvancedOptionsPage):
AdvancedOptionsPage.setObjectName(_fromUtf8("AdvancedOptionsPage"))
AdvancedOptionsPage.resize(338, 435)
self.vboxlayout = QtGui.QVBoxLayout(AdvancedOptionsPage)
self.vboxlayout.setObjectName(_fromUtf8("vboxlayout"))
self.groupBox = QtGui.QGroupBox(AdvancedOptionsPage)
self.groupBox.setObjectName(_fromUtf8("groupBox"))
self.gridlayout = QtGui.QGridLayout(self.groupBox)
self.gridlayout.setSpacing(2)
self.gridlayout.setObjectName(_fromUtf8("gridlayout"))
self.label_ignore_regex = QtGui.QLabel(self.groupBox)
self.label_ignore_regex.setObjectName(_fromUtf8("label_ignore_regex"))
self.gridlayout.addWidget(self.label_ignore_regex, 1, 0, 1, 1)
self.ignore_regex = QtGui.QLineEdit(self.groupBox)
self.ignore_regex.setObjectName(_fromUtf8("ignore_regex"))
self.gridlayout.addWidget(self.ignore_regex, 2, 0, 1, 1)
self.regex_error = QtGui.QLabel(self.groupBox)
self.regex_error.setText(_fromUtf8(""))
self.regex_error.setObjectName(_fromUtf8("regex_error"))
self.gridlayout.addWidget(self.regex_error, 3, 0, 1, 1)
self.vboxlayout.addWidget(self.groupBox)
spacerItem = QtGui.QSpacerItem(181, 21, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
self.vboxlayout.addItem(spacerItem)
self.retranslateUi(AdvancedOptionsPage)
QtCore.QMetaObject.connectSlotsByName(AdvancedOptionsPage)
def retranslateUi(self, AdvancedOptionsPage):
self.groupBox.setTitle(_("Advanced options"))
self.label_ignore_regex.setText(_("Ignore file paths matching the following regular expression:"))

View File

@@ -18,7 +18,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import sys
from PyQt4 import QtGui
from PyQt4 import QtCore, QtGui
from picard import config
from picard.util import find_existing_path

View File

@@ -365,3 +365,9 @@ if sys.platform == 'win32':
return ap1 == ap2
else:
os_path_samefile = os.path.samefile
def is_hidden_path(path):
"""Returns true if at least one element of the path starts with a dot"""
path = os.path.normpath(path) # we need to ignore /./ and /a/../ cases
return any(s.startswith('.') for s in path.split(os.sep))

View File

@@ -18,8 +18,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import locale
import picard.i18n
"""
Helper class to convert bytes to human-readable form
@@ -46,26 +44,26 @@ _BYTES_STRINGS_I18N = (
)
def decimal(number, prec=1):
def decimal(number, scale=1):
"""
Convert bytes to short human-readable string, decimal mode
>>> [decimal(n) for n in [1000, 1024, 15500]]
['1 kB', '1 kB', '15.5 kB']
"""
return short_string(int(number), 1000)
return short_string(int(number), 1000, scale)
def binary(number, prec=1):
def binary(number, scale=1):
"""
Convert bytes to short human-readable string, binary mode
>>> [binary(n) for n in [1000, 1024, 15500]]
['1000 B', '1 KiB', '15.1 KiB']
"""
return short_string(int(number), 1024, prec)
return short_string(int(number), 1024, scale)
def short_string(number, multiple, prec=1):
def short_string(number, multiple, scale=1):
"""
Returns short human-readable string for `number` bytes
>>> [short_string(n, 1024, 2) for n in [1000, 1100, 15500]]
@@ -75,12 +73,12 @@ def short_string(number, multiple, prec=1):
"""
num, unit = calc_unit(number, multiple)
n = int(num)
nr = round(num, prec)
nr = round(num, scale)
if n == nr or unit == 'B':
fmt = '%d'
num = n
else:
fmt = '%%0.%df' % prec
fmt = '%%0.%df' % scale
num = nr
fmtnum = locale.format(fmt, num)
return _("%s " + unit) % fmtnum
@@ -118,8 +116,3 @@ def calc_unit(number, multiple=1000):
return (sign * n, suffix)
else:
n /= multiple
if __name__ == "__main__":
import doctest
doctest.testmod()

View File

@@ -79,10 +79,11 @@ elif sys.platform == 'linux2' and QFile.exists(LINUX_CDROM_INFO):
elif key == 'Can play audio':
drive_audio_caps = [v == '1' for v in
QString(values).trimmed().split(QRegExp("\\s+"), QString.SkipEmptyParts)]
break # no need to continue passed this line
line = cdinfo.readLine()
# Show only drives that are capable of playing audio
for drive in drive_names:
if drive_audio_caps[drive_names.indexOf(drive)]:
for index, drive in enumerate(drive_names):
if drive_audio_caps[index]:
device = u'/dev/%s' % drive
symlink_target = QFile.symLinkTarget(device)
if symlink_target != '':

View File

@@ -311,7 +311,7 @@ def make_short_filename(basedir, relpath, win_compat=False, relative_to=""):
reserved = len(basedir)
if not basedir.endswith(os.path.sep):
reserved += 1
return os.path.join(basedir, _make_win_short_filename(relpath, reserved))
return _make_win_short_filename(relpath, reserved)
# if we're being windows compatible, figure out how much
# needs to be reserved for the basedir part
if win_compat:
@@ -337,4 +337,4 @@ def make_short_filename(basedir, relpath, win_compat=False, relative_to=""):
# and filesystem-dependent
limit = _get_filename_limit(basedir)
relpath = shorten_path(relpath, limit, mode=SHORTEN_BYTES)
return os.path.join(basedir, relpath)
return relpath

View File

@@ -19,7 +19,7 @@
import sys
import traceback
from PyQt4.QtCore import QThreadPool, QRunnable, QCoreApplication, QEvent
from PyQt4.QtCore import QRunnable, QCoreApplication, QEvent
class ProxyToMainEvent(QEvent):

View File

@@ -28,7 +28,6 @@ import re
import time
import os.path
import platform
import urllib
from collections import deque, defaultdict
from functools import partial
from PyQt4 import QtCore, QtNetwork
@@ -55,8 +54,9 @@ USER_AGENT_STRING = '%s-%s/%s (%s;%s-%s)' % (PICARD_ORG_NAME, PICARD_APP_NAME,
platform.platform(),
platform.python_implementation(),
platform.python_version())
CLIENT_STRING = urllib.quote('%s %s-%s' % (PICARD_ORG_NAME, PICARD_APP_NAME,
PICARD_VERSION_STR))
CLIENT_STRING = str(QUrl.toPercentEncoding('%s %s-%s' % (PICARD_ORG_NAME,
PICARD_APP_NAME,
PICARD_VERSION_STR)))
def _escape_lucene_query(text):
@@ -392,7 +392,7 @@ class XmlWebService(QtCore.QObject):
query = []
for name, value in kwargs.items():
if name == 'limit':
filters.append((name, value))
filters.append((name, str(value)))
else:
value = _escape_lucene_query(value).strip().lower()
if value:

File diff suppressed because it is too large Load Diff

View File

@@ -89,8 +89,6 @@ class picard_test(Command):
self.verbosity = int(self.verbosity)
def run(self):
import os.path
import glob
import unittest
names = []

View File

@@ -30,8 +30,11 @@ class Testbytes2human(unittest.TestCase):
self.assertEqual(bytes2human.binary(45682), '44.6 KiB')
self.assertEqual(bytes2human.binary(-45682), '-44.6 KiB')
self.assertEqual(bytes2human.binary(-45682, 2), '-44.61 KiB')
self.assertEqual(bytes2human.decimal(45682), '45.7 kB')
self.assertEqual(bytes2human.decimal(45682, 2), '45.68 kB')
self.assertEqual(bytes2human.decimal(9223372036854775807), '9223.4 PB')
self.assertEqual(bytes2human.decimal(9223372036854775807, 3), '9223.372 PB')
self.assertEqual(bytes2human.decimal(123.6), '123 B')
self.assertRaises(ValueError, bytes2human.decimal, 'xxx')
self.assertRaises(ValueError, bytes2human.decimal, '123.6')

View File

@@ -17,7 +17,7 @@ class ShortFilenameTest(unittest.TestCase):
def test_bmp_unicode_on_unicode_fs(self):
char = u"\N{LATIN SMALL LETTER SHARP S}"
fn = make_short_filename(self.root, os.path.join(*[char * 120] * 2))
self.assertEqual(fn, os.path.join(self.root, *[char * 120] * 2))
self.assertEqual(fn, os.path.join(*[char * 120] * 2))
@unittest.skipUnless(sys.platform not in ("win32", "darwin"), "non-windows, non-osx test")
def test_bmp_unicode_on_nix(self):
@@ -25,28 +25,28 @@ class ShortFilenameTest(unittest.TestCase):
max_len = 255
divisor = len(char.encode(sys.getfilesystemencoding()))
fn = make_short_filename(self.root, os.path.join(*[char * 200] * 2))
self.assertEqual(fn, os.path.join(self.root, *[char * (max_len // divisor)] * 2))
self.assertEqual(fn, os.path.join(*[char * (max_len // divisor)] * 2))
@unittest.skipUnless(sys.platform == "darwin", "os x test")
def test_precomposed_unicode_on_osx(self):
char = u"\N{LATIN SMALL LETTER A WITH BREVE}"
max_len = 255
fn = make_short_filename(self.root, os.path.join(*[char * 200] * 2))
self.assertEqual(fn, os.path.join(self.root, *[char * (max_len // 2)] * 2))
self.assertEqual(fn, os.path.join(*[char * (max_len // 2)] * 2))
@unittest.skipUnless(sys.platform == "win32", "windows test")
def test_nonbmp_unicode_on_windows(self):
char = u"\N{MUSICAL SYMBOL G CLEF}"
remaining = 259 - (3 + 10 + 1 + 200 + 1)
fn = make_short_filename(self.root, os.path.join(*[char * 100] * 2))
self.assertEqual(fn, os.path.join(self.root, char * 100, char * (remaining // 2)))
self.assertEqual(fn, os.path.join(char * 100, char * (remaining // 2)))
@unittest.skipUnless(sys.platform == "darwin", "os x test")
def test_nonbmp_unicode_on_osx(self):
char = u"\N{MUSICAL SYMBOL G CLEF}"
max_len = 255
fn = make_short_filename(self.root, os.path.join(*[char * 200] * 2))
self.assertEqual(fn, os.path.join(self.root, *[char * (max_len // 2)] * 2))
self.assertEqual(fn, os.path.join(*[char * (max_len // 2)] * 2))
@unittest.skipUnless(sys.platform not in ("win32", "darwin"), "non-windows, non-osx test")
def test_nonbmp_unicode_on_nix(self):
@@ -54,7 +54,7 @@ class ShortFilenameTest(unittest.TestCase):
max_len = 255
divisor = len(char.encode(sys.getfilesystemencoding()))
fn = make_short_filename(self.root, os.path.join(*[char * 100] * 2))
self.assertEqual(fn, os.path.join(self.root, *[char * (max_len // divisor)] * 2))
self.assertEqual(fn, os.path.join(*[char * (max_len // divisor)] * 2))
@unittest.skipUnless(sys.platform not in ("win32", "darwin"), "non-windows, non-osx test")
def test_nonbmp_unicode_on_nix_with_windows_compat(self):
@@ -63,45 +63,49 @@ class ShortFilenameTest(unittest.TestCase):
remaining = 259 - (3 + 10 + 1 + 200 + 1)
divisor = len(char.encode(sys.getfilesystemencoding()))
fn = make_short_filename(self.root, os.path.join(*[char * 100] * 2), win_compat=True)
self.assertEqual(fn, os.path.join(self.root, char * (max_len // divisor), char * (remaining // 2)))
self.assertEqual(fn, os.path.join(char * (max_len // divisor), char * (remaining // 2)))
def test_windows_shortening(self):
fn = make_short_filename(self.root, os.path.join("a" * 200, "b" * 200, "c" * 200 + ".ext"), win_compat=True)
self.assertEqual(fn, os.path.join(self.root, "a" * 116, "b" * 116, "c" * 7 + ".ext"))
self.assertEqual(fn, os.path.join("a" * 116, "b" * 116, "c" * 7 + ".ext"))
@unittest.skipUnless(sys.platform != "win32", "non-windows test")
def test_windows_shortening_with_ancestor_on_nix(self):
root = os.path.join(self.root, "w" * 10, "x" * 10, "y" * 9, "z" * 9)
fn = make_short_filename(
os.path.join(self.root, "w" * 10, "x" * 10, "y" * 9, "z" * 9), os.path.join("b" * 200, "c" * 200, "d" * 200 + ".ext"),
root, os.path.join("b" * 200, "c" * 200, "d" * 200 + ".ext"),
win_compat=True, relative_to = self.root)
self.assertEqual(fn, os.path.join(self.root, "w" * 10, "x" * 10, "y" * 9, "z" * 9, "b" * 100, "c" * 100, "d" * 7 + ".ext"))
self.assertEqual(fn, os.path.join("b" * 100, "c" * 100, "d" * 7 + ".ext"))
def test_windows_node_maxlength_shortening(self):
max_len = 226
remaining = 259 - (3 + 10 + 1 + max_len + 1)
fn = make_short_filename(self.root, os.path.join("a" * 300, "b" * 100 + ".ext"), win_compat=True)
self.assertEqual(fn, os.path.join(self.root, "a" * max_len, "b" * (remaining - 4) + ".ext"))
self.assertEqual(fn, os.path.join("a" * max_len, "b" * (remaining - 4) + ".ext"))
def test_windows_selective_shortening(self):
root = self.root + "x" * (44 - 10 - 3)
fn = make_short_filename(root, os.path.join(
os.path.join(*["a" * 9] * 10 + ["b" * 15] * 10), "c" * 10), win_compat=True)
self.assertEqual(fn, os.path.join(root, os.path.join(*["a" * 9] * 10 + ["b" * 9] * 10), "c" * 10))
self.assertEqual(fn, os.path.join(os.path.join(*["a" * 9] * 10 + ["b" * 9] * 10), "c" * 10))
def test_windows_shortening_not_needed(self):
fn = make_short_filename(self.root + "x" * 33, os.path.join(
root = self.root + "x" * 33
fn = make_short_filename(root, os.path.join(
os.path.join(*["a" * 9] * 20), "b" * 10), win_compat=True)
self.assertEqual(fn, os.path.join(self.root + "x" * 33, os.path.join(*["a" * 9] * 20), "b" * 10))
self.assertEqual(fn, os.path.join(os.path.join(*["a" * 9] * 20), "b" * 10))
def test_windows_path_too_long(self):
root = self.root + "x" * 230
self.assertRaises(IOError, make_short_filename,
self.root + "x" * 230, os.path.join("a", "b", "c", "d"), win_compat=True)
root, os.path.join("a", "b", "c", "d"), win_compat=True)
def test_windows_path_not_too_long(self):
fn = make_short_filename(self.root + "x" * 230, os.path.join("a", "b", "c"), win_compat=True)
self.assertEqual(fn, os.path.join(self.root + "x" * 230, "a", "b", "c"))
root = self.root + "x" * 230
fn = make_short_filename(root, os.path.join("a", "b", "c"), win_compat=True)
self.assertEqual(fn, os.path.join("a", "b", "c"))
def test_whitespace(self):
fn = make_short_filename(self.root, os.path.join("a1234567890 ", " b1234567890 "))
self.assertEqual(fn, os.path.join(self.root, "a1234567890", "b1234567890"))
self.assertEqual(fn, os.path.join("a1234567890", "b1234567890"))

View File

@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
import os.path
import unittest
from picard import util
@@ -119,3 +120,18 @@ class SaveReleaseTypeScoresTest(unittest.TestCase):
self.assertTrue("Single 0.50" in saved_scores)
self.assertTrue("Other 0.00" in saved_scores)
self.assertEqual(6, len(saved_scores.split()))
class HiddenPathTest(unittest.TestCase):
def test(self):
self.assertEqual(util.is_hidden_path('/a/.b/c.mp3'), True)
self.assertEqual(util.is_hidden_path('/a/b/c.mp3'), False)
self.assertEqual(util.is_hidden_path('/a/.b/.c.mp3'), True)
self.assertEqual(util.is_hidden_path('/a/b/.c.mp3'), True)
self.assertEqual(util.is_hidden_path('c.mp3'), False)
self.assertEqual(util.is_hidden_path('.c.mp3'), True)
self.assertEqual(util.is_hidden_path('/a/./c.mp3'), False)
self.assertEqual(util.is_hidden_path('/a/./.c.mp3'), True)
self.assertEqual(util.is_hidden_path('/a/../c.mp3'), False)
self.assertEqual(util.is_hidden_path('/a/../.c.mp3'), True)

View File

@@ -1,7 +1,9 @@
# -*- coding: utf-8 -*-
import unittest
from picard import version_to_string, version_from_string
from picard import (version_to_string,
version_from_string,
VersionError)
class VersionsTest(unittest.TestCase):
@@ -23,8 +25,8 @@ class VersionsTest(unittest.TestCase):
def test_version_conv_4(self):
l, s = (1, 0, 2, '', 0), '1.0.2'
self.assertRaises(AssertionError, version_to_string, (l))
self.assertRaises(AttributeError, version_from_string, (s))
self.assertRaises(VersionError, version_to_string, (l))
self.assertRaises(VersionError, version_from_string, (s))
def test_version_conv_5(self):
l, s = (999, 999, 999, 'dev', 999), '999.999.999dev999'
@@ -32,9 +34,8 @@ class VersionsTest(unittest.TestCase):
self.assertEqual(l, version_from_string(s))
def test_version_conv_6(self):
self.assertRaises(TypeError, version_to_string, ('1', 0, 2, 'final', 0))
self.assertRaises(AssertionError, version_to_string, (1, 0))
self.assertRaises(TypeError, version_from_string, 1)
l = (1, 0, 2, 'xx', 0)
self.assertRaises(VersionError, version_to_string, (l))
def test_version_conv_7(self):
l, s = (1, 1, 0, 'final', 0), '1.1'
@@ -51,3 +52,19 @@ class VersionsTest(unittest.TestCase):
def test_version_conv_10(self):
l, s = (1, 1, 0, 'dev', 0), '1.1.0dev0'
self.assertEqual(version_to_string(l, short=True), s)
def test_version_conv_11(self):
l, s = ('1', '1', '0', 'dev', '0'), '1.1.0dev0'
self.assertEqual(version_to_string(l), s)
def test_version_conv_12(self):
l, s = (1, 1, 0, 'dev', 0), '1_1_0_dev_0'
self.assertEqual(l, version_from_string(s))
def test_version_conv_13(self):
l, s = (1, 1, 0, 'dev', 0), 'anything_28_1_1_0_dev_0'
self.assertEqual(l, version_from_string(s))
def test_version_conv_14(self):
l = 'anything_28x_1_0_dev_0'
self.assertRaises(VersionError, version_to_string, (l))

View File

@@ -68,7 +68,7 @@
<item>
<widget class="QPushButton" name="lookup_button">
<property name="text">
<string> Lookup manually </string>
<string>Lookup manually</string>
</property>
</widget>
</item>

63
ui/options_advanced.ui Normal file
View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AdvancedOptionsPage</class>
<widget class="QWidget" name="AdvancedOptionsPage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>338</width>
<height>435</height>
</rect>
</property>
<layout class="QVBoxLayout">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Advanced options</string>
</property>
<layout class="QGridLayout">
<property name="spacing">
<number>2</number>
</property>
<item row="1" column="0">
<widget class="QLabel" name="label_ignore_regex">
<property name="text">
<string>Ignore file paths matching the following regular expression:</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLineEdit" name="ignore_regex"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="regex_error" >
<property name="text" >
<string/>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer>
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>181</width>
<height>21</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<tabstops>
<tabstop>ignore_regex</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>