From 82badb989c20d254a5012029feed1653c55459dd Mon Sep 17 00:00:00 2001 From: Shawn Douglas Date: Fri, 27 Oct 2023 17:31:37 -0700 Subject: [PATCH] Enhanced support for scaffold colors - File encoder now saves scaffold oligo colors to a "scaf_colors" dict within relevant the virtual helix. - Similar to staples, scaffold color information is saved once per oligo, at the idx5p of the strand5p. This also applies to circular oligos (`oligo.isLoop() == True`). - File decoder will load any saved "scaf_colors" values and restore them upon opening. - Added keyboard shortcut `Shift+P` to cycle through scaffold color list when the Paint Tool is active. - The "scaf" selection filter (shortcut key `c`) must be enabled to apply colors to scaffold, and can be disabled to limit coloring to staples. Bug fixes: - cadnano.py Removed import of deprecated `imp` package. It was only used for Maya compatibility. This change should prevent import errors in python 3.12, as in issue #52 - legacydecoder: Use QDialogButtonBox.StandardButton enum to avoid crash on unrecognized file format. --- cadnano2/cadnano.py | 42 ++++++++++--------- cadnano2/model/io/legacydecoder.py | 13 ++++-- cadnano2/model/io/legacyencoder.py | 12 ++++-- cadnano2/model/oligo.py | 2 +- cadnano2/model/strandset.py | 4 +- cadnano2/ui/mainwindow/ui_mainwindow.py | 2 +- cadnano2/views/pathview/colorpanel.py | 23 +++++++---- cadnano2/views/pathview/tools/painttool.py | 7 ++-- cadnano2/views/styles.py | 48 ++++++++++++++-------- setup.py | 2 +- 10 files changed, 96 insertions(+), 59 deletions(-) diff --git a/cadnano2/cadnano.py b/cadnano2/cadnano.py index dd2db3e..c62c3de 100755 --- a/cadnano2/cadnano.py +++ b/cadnano2/cadnano.py @@ -1,4 +1,4 @@ -import sys, imp +import sys import os.path from glob import glob from code import interact @@ -87,23 +87,25 @@ def unloadedPlugins(): results.append(f) return [x for x in results if x not in loadedPlugins] -def loadPlugin(f): - path, fname = os.path.split(f) - name, ext = os.path.splitext(fname) - pluginKey = os.path.join(path, name) - try: - mod = loadedPlugins[pluginKey] - return mod - except KeyError: - pass - file, filename, data = imp.find_module(name, [path]) - mod = imp.load_module(name, file, filename, data) - loadedPlugins[pluginKey] = mod - return mod +# Plugins are no longer supported, so needn't import imp + +# def loadPlugin(f): +# path, fname = os.path.split(f) +# name, ext = os.path.splitext(fname) +# pluginKey = os.path.join(path, name) +# try: +# mod = loadedPlugins[pluginKey] +# return mod +# except KeyError: +# pass +# file, filename, data = imp.find_module(name, [path]) +# mod = imp.load_module(name, file, filename, data) +# loadedPlugins[pluginKey] = mod +# return mod -def loadAllPlugins(): - loadedAPlugin = False - for p in unloadedPlugins(): - loadPlugin(p) - loadedAPlugin = True - return loadedAPlugin \ No newline at end of file +# def loadAllPlugins(): +# loadedAPlugin = False +# for p in unloadedPlugins(): +# loadPlugin(p) +# loadedAPlugin = True +# return loadedAPlugin \ No newline at end of file diff --git a/cadnano2/model/io/legacydecoder.py b/cadnano2/model/io/legacydecoder.py index 3df7b88..d15cbb6 100755 --- a/cadnano2/model/io/legacydecoder.py +++ b/cadnano2/model/io/legacydecoder.py @@ -174,7 +174,7 @@ def import_legacy_dict(document, obj, latticeType=LatticeType.Honeycomb): print("Unrecognized file format.") else: dialogLT.label.setText("Unrecognized file format.") - dialogLT.buttonBox.setStandardButtons(QDialogButtonBox.Ok) + dialogLT.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Ok) dialog.exec() # INSTALL XOVERS @@ -234,13 +234,20 @@ def import_legacy_dict(document, obj, latticeType=LatticeType.Honeycomb): scaf_strand.addInsertion(baseIdx, sumOfInsertSkip, useUndoStack=False) elif stap_strand: stap_strand.addInsertion(baseIdx, sumOfInsertSkip, useUndoStack=False) - # end for - # populate colors + + # populate staple colors for baseIdx, colorNumber in helix['stap_colors']: color = QColor((colorNumber>>16)&0xFF, (colorNumber>>8)&0xFF, colorNumber&0xFF).name() strand = stapStrandSet.getStrand(baseIdx) strand.oligo().applyColor(color, useUndoStack=False) + # populate scaffold colors, if any + if 'scaf_colors' in helix: + for baseIdx, colorNumber in helix['scaf_colors']: + color = QColor((colorNumber>>16)&0xFF, (colorNumber>>8)&0xFF, colorNumber&0xFF).name() + strand = scafStrandSet.getStrand(baseIdx) + strand.oligo().applyColor(color, useUndoStack=False) + def isSegmentStartOrEnd(strandType, vhNum, baseIdx, fiveVH, fiveIdx, threeVH, threeIdx): """Returns True if the base is a breakpoint or crossover.""" if strandType == StrandType.Scaffold: diff --git a/cadnano2/model/io/legacyencoder.py b/cadnano2/model/io/legacyencoder.py index ac8c950..d02e2f2 100755 --- a/cadnano2/model/io/legacyencoder.py +++ b/cadnano2/model/io/legacyencoder.py @@ -21,11 +21,16 @@ def legacy_dict_from_doc(document, fname, helixOrderList): insts[idx] = insertion.length() # colors stapColors = [] - stapStrandSet = vh.stapleStrandSet() - for strand in stapStrandSet: + for strand in vh.stapleStrandSet(): if strand.connection5p() == None: c = str(strand.oligo().color())[1:] # drop the hash stapColors.append([strand.idx5Prime(), int(c, 16)]) + scafColors = set() + for strand in vh.scaffoldStrandSet(): + if strand.connection5p() == None or \ + (strand == strand.oligo().strand5p() and strand.oligo().isLoop()): + c = str(strand.oligo().color())[1:] # drop the hash + scafColors.add((strand.idx5Prime(), int(c, 16))) vhDict = {"row": row, "col": col, @@ -36,7 +41,8 @@ def legacy_dict_from_doc(document, fname, helixOrderList): "skip": skips, "scafLoop": [], "stapLoop": [], - "stap_colors": stapColors} + "stap_colors": stapColors, + "scaf_colors": list(scafColors)} vhList.append(vhDict) bname = basename(str(fname)) obj = {"name": bname, "vstrands": vhList} diff --git a/cadnano2/model/oligo.py b/cadnano2/model/oligo.py index 7c42040..4e11796 100755 --- a/cadnano2/model/oligo.py +++ b/cadnano2/model/oligo.py @@ -90,7 +90,7 @@ def undoStack(self): # end def ### PUBLIC METHODS FOR QUERYING THE MODEL ### - def isLoop(self): + def isLoop(self): # isCircular return self._isLoop def isStaple(self): diff --git a/cadnano2/model/strandset.py b/cadnano2/model/strandset.py index b97caa7..feff680 100755 --- a/cadnano2/model/strandset.py +++ b/cadnano2/model/strandset.py @@ -737,7 +737,7 @@ def __init__(self, strandSet, baseIdxLow, baseIdxHigh, strandSetIdx): self._strandSet = strandSet self._sSetIdx = strandSetIdx self._strand = Strand(strandSet, baseIdxLow, baseIdxHigh) - colorList = styles.stapColors if strandSet.isStaple() else styles.scafColors + colorList = styles.stapColors if strandSet.isStaple() else [styles.scafColors[0]] # default to classic 0066cc color = random.choice(colorList).name() self._newOligo = Oligo(None, color) # redo will set part self._newOligo.setLength(self._strand.totalLength()) @@ -803,7 +803,7 @@ def __init__(self, strandSet, strand, strandSetIdx, solo=True): else: self._newOligo3p = olg3p = olg.shallowCopy() olg3p.setStrand5p(self._oldStrand3p) - colorList = styles.stapColors if strandSet.isStaple() else styles.scafColors + colorList = styles.stapColors if strandSet.isStaple() else [styles.scafColors[0]] color = random.choice(colorList).name() olg3p.setColor(color) olg3p.refreshLength() diff --git a/cadnano2/ui/mainwindow/ui_mainwindow.py b/cadnano2/ui/mainwindow/ui_mainwindow.py index 9b6a1c9..fd1b071 100644 --- a/cadnano2/ui/mainwindow/ui_mainwindow.py +++ b/cadnano2/ui/mainwindow/ui_mainwindow.py @@ -398,7 +398,7 @@ def retranslateUi(self, MainWindow): self.actionRenumber.setToolTip(_translate("MainWindow", "Renumber Slice helices according to helix ordering in Path panel.")) self.actionPathPaint.setText(_translate("MainWindow", "Paint")) self.actionPathPaint.setToolTip(_translate("MainWindow", "(P)aint Tool")) - self.actionPathPaint.setShortcut(_translate("MainWindow", "P")) + self.actionPathPaint.setShortcuts([_translate("MainWindow", "P"), _translate("MainWindow", "Shift+P")]) self.actionPathAddSeq.setText(_translate("MainWindow", "Seq")) self.actionPathAddSeq.setToolTip(_translate("MainWindow", "(A)dd Sequence Tool")) self.actionPathAddSeq.setShortcut(_translate("MainWindow", "A")) diff --git a/cadnano2/views/pathview/colorpanel.py b/cadnano2/views/pathview/colorpanel.py index ab92aa4..95a88c9 100755 --- a/cadnano2/views/pathview/colorpanel.py +++ b/cadnano2/views/pathview/colorpanel.py @@ -2,7 +2,8 @@ import cadnano2.util as util util.qtWrapImport('QtCore', globals(), ['QRectF', 'Qt']) util.qtWrapImport('QtGui', globals(), ['QBrush', 'QFont']) -util.qtWrapImport('QtWidgets', globals(), ['QColorDialog', +util.qtWrapImport('QtWidgets', globals(), ['QApplication', + 'QColorDialog', 'QGraphicsItem', 'QGraphicsSimpleTextItem']) @@ -20,7 +21,7 @@ def __init__(self, parent=None): self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIgnoresTransformations) self.colordialog = QColorDialog() # self.colordialog.setOption(QColorDialog.DontUseNativeDialog) - self._scafColorIndex = -1 # init on -1, painttool will cycle to 0 + self._scafColorIndex = 0 # init on -1, painttool will cycle to 0 self._stapColorIndex = -1 # init on -1, painttool will cycle to 0 self._scafColor = self._scafColors[self._scafColorIndex] self._stapColor = self._stapColors[self._stapColorIndex] @@ -47,14 +48,22 @@ def paint(self, painter, option, widget=None): painter.drawRect(0, 15, 30, 15) def nextColor(self): - self._stapColorIndex += 1 - if self._stapColorIndex == len(self._stapColors): - self._stapColorIndex = 0 - self._stapColor = self._stapColors[self._stapColorIndex] - self._stapBrush.setColor(self._stapColor) + if QApplication.keyboardModifiers() & Qt.KeyboardModifier.ShiftModifier: + self._scafColorIndex += 1 + if self._scafColorIndex == len(self._scafColors): + self._scafColorIndex = 0 + self._scafColor = self._scafColors[self._scafColorIndex] + self._scafBrush.setColor(self._scafColor) + else: + self._stapColorIndex += 1 + if self._stapColorIndex == len(self._stapColors): + self._stapColorIndex = 0 + self._stapColor = self._stapColors[self._stapColorIndex] + self._stapBrush.setColor(self._stapColor) self.update() def prevColor(self): + self._scafColorIndex -= 1 self._stapColorIndex -= 1 def color(self): diff --git a/cadnano2/views/pathview/tools/painttool.py b/cadnano2/views/pathview/tools/painttool.py index a2cd316..50d4602 100755 --- a/cadnano2/views/pathview/tools/painttool.py +++ b/cadnano2/views/pathview/tools/painttool.py @@ -3,6 +3,7 @@ import cadnano2.util as util util.qtWrapImport('QtCore', globals(), []) util.qtWrapImport('QtGui', globals(), []) +util.qtWrapImport('QtWidgets', globals(), ['QApplication']) class PaintTool(AbstractPathTool): @@ -27,17 +28,17 @@ def setActive(self, willBeActive): def widgetClicked(self): """Cycle through colors on 'p' keypress""" self._window.pathColorPanel.nextColor() - + def customMouseRelease(self, event): if self._isMacrod: self._isMacrod = False self._window.undoStack().endMacro() # end def - + def isMacrod(self): return self._isMacrod # end def - + def setMacrod(self): self._isMacrod = True self._window.undoStack().beginMacro("Group Paint") diff --git a/cadnano2/views/styles.py b/cadnano2/views/styles.py index b5cead9..d2858cc 100755 --- a/cadnano2/views/styles.py +++ b/cadnano2/views/styles.py @@ -70,26 +70,38 @@ breakfill = QColor(204, 0, 0, 255) colorbox_fill = QColor(204, 0, 0) colorbox_stroke = QColor(102, 102, 102) -stapColors = [QColor(204, 0, 0), - QColor(247, 67, 8), - QColor(247, 147, 30), - QColor(170, 170, 0), - QColor(87, 187, 0), - QColor(0, 114, 0), - QColor(3, 182, 162), - QColor(23, 0, 222), - QColor(115, 0, 222), - QColor(184, 5, 108), - QColor(51, 51, 51), - QColor(136, 136, 136)] -scafColors = [QColor(0, 102, 204)] - # QColor(64, 138, 212), - # QColor(0, 38, 76), - # QColor(23, 50, 76), - # QColor(0, 76, 153)] +stapColors = [ + QColor(204, 0, 0), #cc0000 + QColor(247, 67, 8), #f74308 + QColor(247, 147, 30), #f7931e + QColor(170, 170, 0), #aaaa00 + QColor( 87, 187, 0), #57bb00 + QColor( 0, 114, 0), #007200 + QColor( 3, 182, 162), #03b6a2 + QColor( 23, 0, 222), #1700de + QColor(115, 0, 222), #7300de + QColor(184, 5, 108), #b8056c + QColor( 51, 51, 51), #333333 + QColor(136, 136, 136) #888888 +] + +scafColors = [ + QColor( 0, 102, 204), #0066cc + QColor(102, 0, 0), #990000 + QColor(139, 48, 6), #b83006 + QColor(198, 125, 23), #c67d17 + QColor(136, 136, 0), #888800 + QColor( 68, 119, 0), #447700 + QColor( 0, 85, 0), #005500 + QColor( 15, 0, 183), #0f00b7 + QColor( 91, 0, 171), #5b00ab + QColor(157, 5, 108), #9d034f + QColor( 2, 126, 130), #027e82 +] + DEFAULT_STAP_COLOR = "#888888" DEFAULT_SCAF_COLOR = "#0066cc" -selected_color = QColor(255, 51, 51) +selected_color = QColor(255, 51, 51) #ff3333 # brightColors = [QColor() for i in range(10)] # for i in range(len(brightColors)): diff --git a/setup.py b/setup.py index de536e8..bf6e4c7 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='cadnano2', - version='2.4.10', + version='2.4.11', description='Cadnano2 for PyQt6', long_description=long_description, long_description_content_type="text/markdown",