diff --git a/picard/album.py b/picard/album.py index 112569a0e..4e3895b95 100644 --- a/picard/album.py +++ b/picard/album.py @@ -709,11 +709,13 @@ class Album(DataObject, Item): config = get_config() threshold = config.setting['track_matching_threshold'] moves = self._match_files(files, self.tracks, self.unmatched_files, threshold=threshold) - for file, target in process_events_iter(moves): - file.move(target) + with self.tagger.window.metadata_box.ignore_updates: + for file, target in process_events_iter(moves): + file.move(target) else: - for file in process_events_iter(list(files)): - file.move(self.unmatched_files) + with self.tagger.window.metadata_box.ignore_updates: + for file in process_events_iter(list(files)): + file.move(self.unmatched_files) def can_save(self): return self._files_count > 0 diff --git a/picard/tagger.py b/picard/tagger.py index 66dc2ab95..f428b5239 100644 --- a/picard/tagger.py +++ b/picard/tagger.py @@ -522,22 +522,23 @@ class Tagger(QtWidgets.QApplication): log.debug("Aborting move since target is invalid") return self.window.set_sorting(False) - if isinstance(target, Cluster): - for file in process_events_iter(files): - file.move(target) - elif isinstance(target, Track): - album = target.album - for file in process_events_iter(files): - file.move(target) - if move_to_multi_tracks: # Assign next file to following track - target = album.get_next_track(target) or album.unmatched_files - elif isinstance(target, File): - for file in process_events_iter(files): - file.move(target.parent) - elif isinstance(target, Album): - self.move_files_to_album(files, album=target) - elif isinstance(target, ClusterList): - self.cluster(files) + with self.window.metadata_box.ignore_updates: + if isinstance(target, Cluster): + for file in process_events_iter(files): + file.move(target) + elif isinstance(target, Track): + album = target.album + for file in process_events_iter(files): + file.move(target) + if move_to_multi_tracks: # Assign next file to following track + target = album.get_next_track(target) or album.unmatched_files + elif isinstance(target, File): + for file in process_events_iter(files): + file.move(target.parent) + elif isinstance(target, Album): + self.move_files_to_album(files, album=target) + elif isinstance(target, ClusterList): + self.cluster(files) self.window.set_sorting(True) def add_files(self, filenames, target=None): diff --git a/picard/ui/mainwindow.py b/picard/ui/mainwindow.py index 398118cb2..1447bc9d2 100644 --- a/picard/ui/mainwindow.py +++ b/picard/ui/mainwindow.py @@ -87,6 +87,7 @@ from picard.plugin import ExtensionPoint from picard.script import get_file_naming_script_presets from picard.track import Track from picard.util import ( + IgnoreUpdatesContext, icontheme, iter_files_from_objects, iter_unique, @@ -147,35 +148,6 @@ def register_ui_init(function): ui_init.register(function.__module__, function) -class IgnoreSelectionContext: - """Context manager for holding a boolean value, indicating whether selection changes are performed or not. - By default the context resolves to False. If entered it is True. This allows - to temporarily set a state on a block of code like: - - ignore_changes = IgnoreSelectionContext() - # Initially ignore_changes is True - with ignore_changes: - # Perform some tasks with ignore_changes now being True - ... - # ignore_changes is False again - """ - - def __init__(self, onexit=None): - self._entered = 0 - self._onexit = onexit - - def __enter__(self): - self._entered += 1 - - def __exit__(self, type, value, tb): - self._entered -= 1 - if self._onexit: - self._onexit() - - def __bool__(self): - return self._entered > 0 - - class MainWindowActions: _create_actions = [] @@ -214,7 +186,7 @@ class MainWindow(QtWidgets.QMainWindow, PreserveGeometry): super().__init__(parent) self.__shown = False self.selected_objects = [] - self.ignore_selection_changes = IgnoreSelectionContext(self.update_selection) + self.ignore_selection_changes = IgnoreUpdatesContext(self.update_selection) self.toolbar = None self.player = None self.status_indicators = [] diff --git a/picard/ui/metadatabox.py b/picard/ui/metadatabox.py index a94fd77c1..3b3a7715e 100644 --- a/picard/ui/metadatabox.py +++ b/picard/ui/metadatabox.py @@ -55,6 +55,7 @@ from picard.file import File from picard.metadata import MULTI_VALUED_JOINER from picard.track import Track from picard.util import ( + IgnoreUpdatesContext, format_time, icontheme, restore_method, @@ -253,6 +254,7 @@ class MetadataBox(QtWidgets.QTableWidget): self.preserved_tags = PreservedTags() self._single_file_album = False self._single_track_album = False + self.ignore_updates = IgnoreUpdatesContext(onexit=self.update) self.tagger.clipboard().dataChanged.connect(self.update_clipboard) def get_file_lookup(self): @@ -537,7 +539,7 @@ class MetadataBox(QtWidgets.QTableWidget): @throttle(100) def update(self, drop_album_caches=False): - if self.editing: + if self.editing or self.ignore_updates: return new_selection = self.selection_dirty if self.selection_dirty: diff --git a/picard/util/__init__.py b/picard/util/__init__.py index a9df13bad..c39eed65f 100644 --- a/picard/util/__init__.py +++ b/picard/util/__init__.py @@ -474,6 +474,38 @@ def throttle(interval): return decorator +class IgnoreUpdatesContext: + """Context manager for holding a boolean value, indicating whether updates are performed or not. + By default the context resolves to False. If entered it is True. This allows + to temporarily set a state on a block of code like: + + ignore_changes = IgnoreUpdatesContext() + # Initially ignore_changes is False + with ignore_changes: + # Perform some tasks with ignore_changes now being True + ... + # ignore_changes is False again + + The code actually doing updates can check `ignore_changes` and only perform + updates if it is `False`. + """ + + def __init__(self, onexit=None): + self._entered = 0 + self._onexit = onexit + + def __enter__(self): + self._entered += 1 + + def __exit__(self, type, value, tb): + self._entered -= 1 + if self._onexit: + self._onexit() + + def __bool__(self): + return self._entered > 0 + + def uniqify(seq): """Uniqify a list, preserving order""" return list(iter_unique(seq)) diff --git a/test/test_ui_mainwindow.py b/test/test_ui_mainwindow.py deleted file mode 100644 index 31de27773..000000000 --- a/test/test_ui_mainwindow.py +++ /dev/null @@ -1,52 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Picard, the next-generation MusicBrainz tagger -# -# Copyright (C) 2020 Philipp Wolfer -# Copyright (C) 2021 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 unittest.mock import Mock - -from test.picardtestcase import PicardTestCase - -from picard.ui.mainwindow import IgnoreSelectionContext - - -class IgnoreSelectionContextTest(PicardTestCase): - - def test_enter_exit(self): - context = IgnoreSelectionContext() - self.assertFalse(context) - with context: - self.assertTrue(context) - self.assertFalse(context) - - def test_run_onexit(self): - onexit = Mock() - context = IgnoreSelectionContext(onexit=onexit) - with context: - onexit.assert_not_called() - onexit.assert_called_once_with() - - def test_nested_with(self): - context = IgnoreSelectionContext() - with context: - with context: - self.assertTrue(context) - self.assertTrue(context) - self.assertFalse(context) diff --git a/test/test_utils.py b/test/test_utils.py index 83d15856c..e23c2b39e 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -51,6 +51,7 @@ from picard.const.sys import ( IS_WIN, ) from picard.util import ( + IgnoreUpdatesContext, album_artist_from_path, any_exception_isinstance, build_qurl, @@ -824,3 +825,28 @@ class AnyExceptionIsinstanceTest(PicardTestCase): ex.__cause__ = Mock() ex.__cause__.__context__ = RuntimeError() self.assertTrue(any_exception_isinstance(ex, RuntimeError)) + + +class IgnoreUpdatesContextTest(PicardTestCase): + + def test_enter_exit(self): + context = IgnoreUpdatesContext() + self.assertFalse(context) + with context: + self.assertTrue(context) + self.assertFalse(context) + + def test_run_onexit(self): + onexit = Mock() + context = IgnoreUpdatesContext(onexit=onexit) + with context: + onexit.assert_not_called() + onexit.assert_called_once_with() + + def test_nested_with(self): + context = IgnoreUpdatesContext() + with context: + with context: + self.assertTrue(context) + self.assertTrue(context) + self.assertFalse(context)