2323
2424from .helper import BrowseWidget , MultiInput , BorderedWidget , RadioButtonGroup , resizeLineEdit
2525from .quick_dialog import getLayout
26- from PyReconstruct .modules .gui .utils import notify
26+ from PyReconstruct .modules .gui .utils import notify , notifyConfirm
2727from PyReconstruct .modules .datatypes import Series
2828
2929
@@ -143,9 +143,10 @@ def __init__(self, parent : QWidget, series : Series, other : Series):
143143 "alignments" ,
144144 MultiImportAs (
145145 self ,
146- other .alignments ,
147- series .alignments ,
148- "alignment"
146+ other .alignments ,
147+ series .alignments ,
148+ "alignment" ,
149+ allow_overwrite = True ,
149150 )
150151 ))
151152
@@ -229,6 +230,71 @@ def exec(self):
229230 return None , False
230231
231232
233+ class ImportAlignmentsDialog (QDialog ):
234+ """Import alignments, and nothing else, from another series.
235+
236+ The same alignments are reachable through ImportSeriesDialog's Alignments
237+ tab, which is the whole-series merge: seven tabs, six of them about
238+ something else. A user who only wants a colleague's alignment has to find it
239+ in there, so Alignments > Import alignments offers this one-tab version too.
240+ Both run the same MultiImportAs widget and therefore the same overwrite
241+ prompt.
242+ """
243+
244+ def __init__ (self , parent : QWidget , series : Series , other : Series ):
245+ """Create the import-alignments dialog.
246+
247+ Params:
248+ parent (QWidget): the parent widget
249+ series (Series): the current series obj
250+ other (Series): the series importing from
251+ """
252+ super ().__init__ (parent )
253+ self .setWindowTitle ("Import Alignments" )
254+
255+ vlayout = QVBoxLayout ()
256+ vlayout .setSpacing (10 )
257+
258+ vlayout .addWidget (QLabel (
259+ f"Import alignments from { os .path .basename (other .jser_fp )} :" ,
260+ self ,
261+ ))
262+
263+ self .import_widget = MultiImportAs (
264+ self ,
265+ other .alignments ,
266+ series .alignments ,
267+ "alignment" ,
268+ allow_overwrite = True ,
269+ default_to_source = True ,
270+ )
271+ vlayout .addWidget (self .import_widget )
272+
273+ QBtn = QDialogButtonBox .Ok | QDialogButtonBox .Cancel
274+ buttonbox = QDialogButtonBox (QBtn )
275+ buttonbox .accepted .connect (self .accept )
276+ buttonbox .rejected .connect (self .reject )
277+ vlayout .addWidget (buttonbox )
278+
279+ self .setLayout (vlayout )
280+ self .response = None
281+
282+ def accept (self ):
283+ """Run when user clicks OK."""
284+ response , confirmed = self .import_widget .getResponse ()
285+ if not confirmed :
286+ return # stay open: the widget already said why
287+ self .response = response
288+ super ().accept ()
289+
290+ def exec (self ):
291+ """Run the dialog."""
292+ confirmed = super ().exec ()
293+ if confirmed :
294+ return self .response , True
295+ return None , False
296+
297+
232298class ImportTracesWidget (QWidget ):
233299
234300 def __init__ (self , parent , series : Series , other : Series ):
@@ -494,17 +560,53 @@ def getResponse(self):
494560 return None , False
495561
496562
563+ def collidingImportNames (entries , existing ) -> list :
564+ """Return the target names in entries that already exist, in order.
565+
566+ Kept as a plain function (no Qt) because it is the whole decision behind the
567+ overwrite prompt: warn only about names that really are already taken, and
568+ name them. Duplicates are collapsed so a two-row import onto one existing
569+ name reads as one collision.
570+
571+ Params:
572+ entries (list): (source name, target name) pairs
573+ existing (iterable): the names the series already uses
574+ Returns:
575+ (list): the colliding target names, first-seen order, deduped
576+ """
577+ taken = set (existing )
578+ collisions = []
579+ for _source , target in entries :
580+ if target in taken and target not in collisions :
581+ collisions .append (target )
582+ return collisions
583+
584+
497585class ImportAs (QWidget ):
498586
499- def __init__ (self , parent = None , combo_items = []):
500- """Create the import __ as __ widget."""
587+ def __init__ (self , parent = None , combo_items = [], default_to_source = False ):
588+ """Create the import __ as __ widget.
589+
590+ Params:
591+ parent (QWidget): the parent widget
592+ combo_items (list): the names available to import
593+ default_to_source (bool): True if the target name should
594+ prefill with (and follow) the selected source name
595+ """
501596 super ().__init__ (parent )
502597
503598 # set up the inputs
504599 self .input_1 = QComboBox (self )
505600 self .input_1 .addItems (combo_items )
506601 self .input_2 = QLineEdit ("" , self )
507602
603+ if default_to_source :
604+ # importing an alignment under its own name is the common case, so
605+ # prefill it and keep following the combo until the user types
606+ self .input_2 .setText (self .input_1 .currentText ())
607+ self .input_1 .currentTextChanged .connect (self ._followSource )
608+ self .input_2 .textEdited .connect (self ._stopFollowingSource )
609+
508610 # set up the layout
509611 hlayout = QHBoxLayout ()
510612 hlayout .addWidget (QLabel ("Import" , self ))
@@ -513,7 +615,18 @@ def __init__(self, parent=None, combo_items=[]):
513615 hlayout .addWidget (self .input_2 )
514616
515617 self .setLayout (hlayout )
516-
618+
619+ def _followSource (self , text ):
620+ """Mirror the selected source name into the target field."""
621+ self .input_2 .setText (text )
622+
623+ def _stopFollowingSource (self , _text ):
624+ """Stop mirroring once the user has typed a name of their own."""
625+ try :
626+ self .input_1 .currentTextChanged .disconnect (self ._followSource )
627+ except RuntimeError :
628+ pass
629+
517630 def getResponse (self ):
518631 """Get the user response."""
519632 return (
@@ -524,19 +637,42 @@ def getResponse(self):
524637
525638class MultiImportAs (QWidget ):
526639
527- def __init__ (self , parent : QWidget , other_items , self_items , name : str ):
528- """Create the multi line edit widget."""
640+ def __init__ (
641+ self ,
642+ parent : QWidget ,
643+ other_items ,
644+ self_items ,
645+ name : str ,
646+ allow_overwrite : bool = False ,
647+ default_to_source : bool = False ,
648+ ):
649+ """Create the multi line edit widget.
650+
651+ Params:
652+ parent (QWidget): the parent widget
653+ other_items (iterable): names available in the other series
654+ self_items (iterable): names already in the current series
655+ name (str): what one item is called, for messages
656+ allow_overwrite (bool): True if a name that already exists may
657+ be replaced after the user confirms it. False rejects the
658+ name outright, which is the behavior for palettes and
659+ brightness/contrast profiles.
660+ default_to_source (bool): True if each row's target name should
661+ prefill from its source name
662+ """
529663 super ().__init__ (parent )
530664
531665 vbl = QVBoxLayout ()
532666 self .input_layout = QVBoxLayout ()
533667 self .other_items = other_items
534668 self .self_items = self_items
535669 self .name = name
670+ self .allow_overwrite = allow_overwrite
671+ self .default_to_source = default_to_source
536672
537673 # create the inputs
538674 self .inputs = []
539- w = ImportAs (self , self .other_items )
675+ w = ImportAs (self , self .other_items , self . default_to_source )
540676 self .input_layout .addWidget (w )
541677 self .inputs .append (w )
542678 vbl .addLayout (self .input_layout )
@@ -556,7 +692,7 @@ def __init__(self, parent : QWidget, other_items, self_items, name : str):
556692
557693 def add (self ):
558694 """Add a line edit row to the field."""
559- w = ImportAs (self , self .other_items )
695+ w = ImportAs (self , self .other_items , self . default_to_source )
560696 self .input_layout .addWidget (w )
561697 self .inputs .append (w )
562698
@@ -587,16 +723,37 @@ def accept(self):
587723 if not new_name :
588724 notify ("Please enter a valid name." )
589725 return False
590- if new_name in self .self_items :
726+ if new_name in self .self_items and not self . allow_overwrite :
591727 notify (f"{ self .name .capitalize ()} name already exists in current series." )
592728 return False
593729 if new_name in new_names :
594730 notify (f"Cannot import multiple { self .name } s as the same name." )
595731 return False
596732 new_names .add (new_name )
597-
733+
734+ collisions = collidingImportNames (entries , self .self_items )
735+ if collisions and not self .confirmOverwrite (collisions ):
736+ return False
737+
598738 return True
599-
739+
740+ def confirmOverwrite (self , collisions : list ) -> bool :
741+ """Ask before replacing names that already exist in this series.
742+
743+ Only reached when allow_overwrite is set and a target name really is
744+ taken, so the prompt never fires on an import that adds new names.
745+ """
746+ listed = "\n " .join (f" { name } " for name in collisions )
747+ plural = "s" if len (collisions ) > 1 else ""
748+ return notifyConfirm (
749+ f"This series already has the following { self .name } { plural } :\n \n "
750+ f"{ listed } \n \n "
751+ f"Importing under { 'these names' if plural else 'this name' } will "
752+ f"replace the existing { self .name } { plural } on every section.\n \n "
753+ "Continue?" ,
754+ yn = True ,
755+ )
756+
600757 def getResponse (self ):
601758 """Get the user response"""
602759 if self .accept ():
0 commit comments