PICARD-993: Have different error icons for different file errors

Separate icons for file not found, access denied and anything else
This commit is contained in:
Philipp Wolfer
2022-03-09 14:06:22 +01:00
parent 903ed4b59d
commit aaee5e4f37
4 changed files with 95 additions and 6 deletions

View File

@@ -44,6 +44,10 @@
from collections import Counter
from enum import (
Enum,
auto,
)
import fnmatch
from functools import partial
import os
@@ -73,6 +77,7 @@ from picard.plugin import (
)
from picard.script import get_file_naming_script
from picard.util import (
any_exception_isinstance,
decode_filename,
emptydir,
find_best_match,
@@ -95,6 +100,13 @@ from picard.util.tags import PRESERVED_TAGS
from picard.ui.item import Item
class FileErrorType(Enum):
UNKNOWN = auto()
NOTFOUND = auto()
NOACCESS = auto()
class File(QtCore.QObject, Item):
metadata_images_changed = QtCore.pyqtSignal()
@@ -144,6 +156,7 @@ class File(QtCore.QObject, Item):
self.base_filename = os.path.basename(filename)
self._state = File.UNDEFINED
self.state = File.PENDING
self.error_type = FileErrorType.UNKNOWN
self.orig_metadata = Metadata()
self.metadata = Metadata()
@@ -189,6 +202,16 @@ class File(QtCore.QObject, Item):
copy[name] = self.format_specific_metadata(metadata, name, settings)
return copy
def _set_error(self, error):
self.state = File.ERROR
self.error_append(str(error))
if any_exception_isinstance(error, FileNotFoundError):
self.error_type = FileErrorType.NOTFOUND
elif any_exception_isinstance(error, PermissionError):
self.error_type = FileErrorType.NOACCESS
else:
self.error_type = FileErrorType.UNKNOWN
def load(self, callback):
thread.run_task(
partial(self._load_check, self.filename),
@@ -215,8 +238,7 @@ class File(QtCore.QObject, Item):
return
config = get_config()
if error is not None:
self.state = self.ERROR
self.error_append(str(error))
self._set_error(error)
# If loading failed, force format guessing and try loading again
from picard.formats.util import guess_format
@@ -390,8 +412,7 @@ class File(QtCore.QObject, Item):
return
old_filename = new_filename = self.filename
if error is not None:
self.state = File.ERROR
self.error_append(str(error))
self._set_error(error)
else:
self.filename = new_filename = result
self.base_filename = os.path.basename(new_filename)

View File

@@ -70,7 +70,10 @@ from picard.config import (
Option,
get_config,
)
from picard.file import File
from picard.file import (
File,
FileErrorType,
)
from picard.plugin import ExtensionPoint
from picard.track import (
NonAlbumTrack,
@@ -258,6 +261,8 @@ class MainPanel(QtWidgets.QSplitter):
FileItem.icon_file = QtGui.QIcon(":/images/file.png")
FileItem.icon_file_pending = QtGui.QIcon(":/images/file-pending.png")
FileItem.icon_error = icontheme.lookup('dialog-error', icontheme.ICON_SIZE_MENU)
FileItem.icon_error_not_found = icontheme.lookup('error-not-found', icontheme.ICON_SIZE_MENU)
FileItem.icon_error_no_access = icontheme.lookup('error-no-access', icontheme.ICON_SIZE_MENU)
FileItem.icon_saved = QtGui.QIcon(":/images/track-saved.png")
FileItem.icon_fingerprint = icontheme.lookup('fingerprint', icontheme.ICON_SIZE_MENU)
FileItem.icon_fingerprint_gray = icontheme.lookup('fingerprint-gray', icontheme.ICON_SIZE_MENU)
@@ -1130,7 +1135,12 @@ class FileItem(TreeItem):
@staticmethod
def decide_file_icon(file):
if file.state == File.ERROR:
return FileItem.icon_error
if file.error_type == FileErrorType.NOTFOUND:
return FileItem.icon_error_not_found
elif file.error_type == FileErrorType.NOACCESS:
return FileItem.icon_error_no_access
else:
return FileItem.icon_error
elif isinstance(file.parent, Track):
if file.state == File.NORMAL:
return FileItem.icon_saved

View File

@@ -1037,3 +1037,18 @@ def get_base_title(title):
"""
suffix = _(DEFAULT_COPY_TEXT)
return get_base_title_with_suffix(title, suffix)
def iter_exception_chain(err):
"""Iterate over the exception chain.
Yields this exception and all __context__ and __cause__ exceptions"""
yield err
if hasattr(err, '__context__'):
yield from iter_exception_chain(err.__context__)
if hasattr(err, '__cause__'):
yield from iter_exception_chain(err.__cause__)
def any_exception_isinstance(error, type_):
"""Returns True, if any exception in the exception chain is instance of type_."""
return any(isinstance(err, type_) for err in iter_exception_chain(error))

View File

@@ -52,10 +52,12 @@ from picard.const.sys import (
)
from picard.util import (
album_artist_from_path,
any_exception_isinstance,
build_qurl,
extract_year_from_date,
find_best_match,
is_absolute_path,
iter_exception_chain,
iter_files_from_objects,
iter_unique,
limited_join,
@@ -781,3 +783,44 @@ class SystemSupportsLongPathsTest(PicardTestCase):
self.assertTrue(system_supports_long_paths())
mock_open_key.assert_called_once()
mock_query_value.assert_called_once()
class IterExceptionChainTest(PicardTestCase):
def test_iter_exception_chain(self):
e1 = Mock(name='e1')
e2 = Mock(name='e2')
e3 = Mock(name='e3')
e4 = Mock(name='e4')
e5 = Mock(name='e5')
e1.__context__ = e2
e2.__context__ = e3
e2.__cause__ = e4
e1.__cause__ = e5
self.assertEqual([e1, e2, e3, e4, e5], list(iter_exception_chain(e1)))
class AnyExceptionIsinstanceTest(PicardTestCase):
def test_any_exception_isinstance_itself(self):
ex = RuntimeError()
self.assertTrue(any_exception_isinstance(ex, RuntimeError))
def test_any_exception_isinstance_context(self):
ex = Mock()
self.assertFalse(any_exception_isinstance(ex, RuntimeError))
ex.__context__ = RuntimeError()
self.assertTrue(any_exception_isinstance(ex, RuntimeError))
def test_any_exception_isinstance_cause(self):
ex = Mock()
self.assertFalse(any_exception_isinstance(ex, RuntimeError))
ex.__cause__ = RuntimeError()
self.assertTrue(any_exception_isinstance(ex, RuntimeError))
def test_any_exception_isinstance_nested(self):
ex = Mock()
self.assertFalse(any_exception_isinstance(ex, RuntimeError))
ex.__cause__ = Mock()
ex.__cause__.__context__ = RuntimeError()
self.assertTrue(any_exception_isinstance(ex, RuntimeError))