diff --git a/lib/src/constants.dart b/lib/src/constants.dart index a8c9ea4aa..8414488cf 100644 --- a/lib/src/constants.dart +++ b/lib/src/constants.dart @@ -8,12 +8,13 @@ import 'state/grid.dart'; // WARNING: Do not modify line below, except for the version string // (and also add new version string to scadnano_versions_to_link). -const String CURRENT_VERSION = "0.17.4"; +const String CURRENT_VERSION = "0.17.5"; const String INITIAL_VERSION = "0.1.0"; // scadnano versions that we deploy so that older versions can be used. final scadnano_older_versions_to_link =[ // "0.17.3", // accidentally skipped this version + "0.17.4", "0.17.2", "0.17.1", "0.17.0", diff --git a/lib/src/middleware/helix_idxs_change.dart b/lib/src/middleware/helix_idxs_change.dart index df531743f..1aa369ceb 100644 --- a/lib/src/middleware/helix_idxs_change.dart +++ b/lib/src/middleware/helix_idxs_change.dart @@ -12,6 +12,35 @@ helix_idxs_change_middleware(Store store, dynamic action, NextDispatch Set existing_idxs = store.state.design.helices.keys.toSet(); Set old_idxs = action.idx_replacements.keys.toSet(); Set remaining_idxs = existing_idxs.difference(old_idxs); + var new_indices = action.idx_replacements.values.toSet(); + + if (new_indices.length != action.idx_replacements.length) { + Map> key_to_idxs = new Map>(); + + for (var old_index in old_idxs) { + var new_index = action.idx_replacements[old_index]; + if (key_to_idxs.containsKey(new_index)) + key_to_idxs[new_index].add(old_index); + else + key_to_idxs[new_index] = [ old_index ]; + } + + if (key_to_idxs.length != action.idx_replacements.length) { + var msg = 'You tried to assign existing helices '; + + msg += key_to_idxs.entries.where((element) => element.value.length != 1).map((element) { + return element.value.join(', ') + " to " + element.key.toString(); + }).join(" and helices "); + + msg += ". Each helix must have a unique new index; make sure all the integers you write are distinct from each other and do not appear elsewhere in the design"; + + window.alert(msg); + return; + } + + + } + for (int new_idx in action.idx_replacements.values) { if (remaining_idxs.contains(new_idx)) { var msg = 'Index ${new_idx} is already taken.'; diff --git a/lib/src/reducers/helices_reducer.dart b/lib/src/reducers/helices_reducer.dart index ba037a75b..d09d3cad0 100644 --- a/lib/src/reducers/helices_reducer.dart +++ b/lib/src/reducers/helices_reducer.dart @@ -89,12 +89,13 @@ Design helix_idx_change_reducer(Design design, AppState state, actions.HelixIdxs var strands = design.strands.toList(); Map new_groups = change_groups(action, helices, design); - + var copy_helices = design.helices.toMap(); // change helices + + helices.removeWhere((key, _) => action.idx_replacements.containsKey(key)); for (int old_idx in action.idx_replacements.keys) { int new_idx = action.idx_replacements[old_idx]; - var helix = helices[old_idx].rebuild((b) => b..idx = new_idx); - helices.remove(old_idx); + var helix = copy_helices[old_idx].rebuild((b) => b..idx = new_idx); helices[new_idx] = helix; } diff --git a/lib/src/serializers.dart b/lib/src/serializers.dart index ef921efa7..e2a554dea 100644 --- a/lib/src/serializers.dart +++ b/lib/src/serializers.dart @@ -288,6 +288,7 @@ part 'serializers.g.dart'; DialogShow, DialogHide, DialogLink, + DialogLabel, StrandOrder, StrandColorSet, StrandColorPickerShow, diff --git a/lib/src/state/dialog.dart b/lib/src/state/dialog.dart index 1fc637567..d633e31f5 100644 --- a/lib/src/state/dialog.dart +++ b/lib/src/state/dialog.dart @@ -305,6 +305,8 @@ abstract class DialogLink ..tooltip = tooltip); } + + /************************ end BuiltValue boilerplate ************************/ String get label; @@ -314,6 +316,33 @@ abstract class DialogLink String get value; } +abstract class DialogLabel + with BuiltJsonSerializable + implements DialogItem, Built { + DialogLabel._(); + + factory DialogLabel.from([void Function(DialogLabelBuilder) updates]) = _$DialogLabel; + + static Serializer get serializer => _$dialogLabelSerializer; + + @memoized + int get hashCode; + + factory DialogLabel({String label, String tooltip}) { + return DialogLabel.from((b) => b + ..label = label + ..value = "" + ..tooltip = tooltip); + } + + + /************************ end BuiltValue boilerplate ************************/ + + String get label; + + String get value; +} + // abstract class DialogLink // with BuiltJsonSerializable // implements DialogItem, Built { diff --git a/lib/src/view/design_dialog_form.dart b/lib/src/view/design_dialog_form.dart index 1fa764399..cfddd9b44 100644 --- a/lib/src/view/design_dialog_form.dart +++ b/lib/src/view/design_dialog_form.dart @@ -318,6 +318,13 @@ class DesignDialogFormComponent extends UiStatefulComponent2 with RedrawCounterMi ..disabled = props.groups.length == 1 ..on_click = ((ev) => app.dispatch(actions.GroupRemove(name: props.displayed_group_name))) ..key = 'remove-current-group')(), + (MenuDropdownItem() + ..display = 'adjust helix indices' + ..disabled = props.groups[props.displayed_group_name].helices_view_order.length == 0 + ..on_click = ((ev) => adjust_helix_indices_for_current_group()) + ..key = 'adjust-helix-indices')(), ]); return NavDropdown({ 'title': 'Group', @@ -125,6 +130,9 @@ class SideMenuComponent extends UiComponent2 with RedrawCounterMi set_new_parameters_for_current_group() => app.disable_keyboard_shortcuts_while(ask_new_parameters_for_current_group); + adjust_helix_indices_for_current_group() => + app.disable_keyboard_shortcuts_while(ask_new_helix_indices_for_current_group); + add_new_group(Iterable existing_names) => app.disable_keyboard_shortcuts_while(() => ask_about_new_group(existing_names)); @@ -262,4 +270,31 @@ class SideMenuComponent extends UiComponent2 with RedrawCounterMi app.dispatch( actions.GroupChange(old_name: props.displayed_group_name, new_name: new_name, new_group: new_group)); } + + Future ask_new_helix_indices_for_current_group() async { + var group = props.groups[props.displayed_group_name]; + var existing_grid = group.grid; + + List items = []; + items.add(DialogLabel(label: 'current view order: ' + group.helices_view_order.join(' '))); + + for (var helix_index in group.helices_view_order) { + items.add(DialogInteger(label: helix_index.toString(), value: helix_index)); + } + + var dialog = + Dialog(title: 'adjust Helix indices', items: items); + List results = await util.dialog(dialog); + if (results == null) return; + + Map new_indices_map = {}; + for (int i = 1; i < results.length; i++) { + new_indices_map[group.helices_view_order[i-1]] = (results[i] as DialogInteger).value; + } + + app.dispatch(actions.HelixIdxsChange(idx_replacements: new_indices_map)); + } + } + + diff --git a/web/scadnano-styles.css b/web/scadnano-styles.css index 8f86f8a2c..bf0c3b09f 100644 --- a/web/scadnano-styles.css +++ b/web/scadnano-styles.css @@ -1,5 +1,5 @@ - -html, body { +html, +body { width: 100%; height: 100%; margin: 0; @@ -11,12 +11,14 @@ html, body { ** BEGIN layout ************************************************/ -input[type = checkbox] { +input[type=checkbox] { margin-right: 4px; } -label + select { + +label+select { margin-left: 4px; } + .radio-left { text-align: left; } @@ -65,7 +67,7 @@ label + select { #design-and-modes-buttons-container { display: flex; height: 100%; - width: 100%; + width: 100%; overflow: hidden; } @@ -99,7 +101,8 @@ label + select { position: relative; } -#side-arrows, #main-arrows { +#side-arrows, +#main-arrows { position: absolute; left: 0; bottom: 0; @@ -108,11 +111,11 @@ label + select { filter: drop-shadow(0px 0px 5px rgb(255 255 255)); } -.arrow-group{ +.arrow-group { pointer-events: bounding-box; } -.axis-arrow{ +.axis-arrow { stroke-width: 3; stroke-linecap: round; } @@ -153,7 +156,8 @@ label + select { padding-right: 5px; } -.dialog-form-container, .dialog-loading-container { +.dialog-form-container, +.dialog-loading-container { position: fixed; top: 50%; left: 50%; @@ -170,6 +174,8 @@ label + select { border: 1px solid #B2B2B2; background: #F9F9F9; box-shadow: 3px 3px 2px #E9E9E9; + max-height: 80vh; + overflow-y: auto; } .dialog-form-item { @@ -205,7 +211,7 @@ label + select { font-style: italic; } -.dialog-design-loading span { +.dialog-design-loading span { animation: pulse 0.8s infinite alternate; } @@ -261,7 +267,7 @@ label + select { top: 0; } -.has-submenu:hover > .context-menu { +.has-submenu:hover>.context-menu { visibility: visible; } @@ -277,7 +283,7 @@ label + select { pointer-events: none; } -.has-submenu > .context-menu-item::after { +.has-submenu>.context-menu-item::after { content: ''; position: absolute; top: 50%; @@ -290,7 +296,7 @@ label + select { } .context-menu-item:hover, -.has-submenu:hover > .context-menu-item { +.has-submenu:hover>.context-menu-item { color: white; background: #284570; border-radius: 2px; @@ -315,6 +321,7 @@ label + select { background: #F9F9F9 !important; box-shadow: none !important; } + /*.panes-container {*/ /* display: flex;*/ /* width: 100%;*/ @@ -413,7 +420,7 @@ label + select { width: 100%; } -.edit-mode-toggle-button{ +.edit-mode-toggle-button { position: absolute; width: 25px; bottom: 24px; @@ -421,11 +428,11 @@ label + select { right: calc(100% + 20px); } -.edit-mode-toggle-button img.appear{ +.edit-mode-toggle-button img.appear { transform: rotate(180deg); } -.edit-mode-toggle-button img{ +.edit-mode-toggle-button img { pointer-events: none; max-width: 100%; @@ -444,7 +451,8 @@ label + select { padding: 5px 10px; border-top: 1px solid black; } -.mode-button>img{ + +.mode-button>img { width: 25px; pointer-events: none; } @@ -504,11 +512,16 @@ label + select { /* show open hand when cursor is over SVG background itself */ .panzoomable { - cursor: move; /* fallback: no `url()` support or images disabled */ - cursor: url(images/grab.png), auto; /* fallback: Internet Explorer */ - cursor: -webkit-grab; /* Chrome 1-21, Safari 4+ */ - cursor: -moz-grab; /* Firefox 1.5-26 */ - cursor: grab; /* W3C standards syntax, should come least */ + cursor: move; + /* fallback: no `url()` support or images disabled */ + cursor: url(images/grab.png), auto; + /* fallback: Internet Explorer */ + cursor: -webkit-grab; + /* Chrome 1-21, Safari 4+ */ + cursor: -moz-grab; + /* Firefox 1.5-26 */ + cursor: grab; + /* W3C standards syntax, should come least */ } /* show regular cursor arrow when cursor is over an element in the SVG, not the SVG background itself */ @@ -528,11 +541,13 @@ label + select { cursor: crosshair; } -.domain-name-text, .strand-name-text { +.domain-name-text, +.strand-name-text { text-anchor: middle; } -.selection-box,.selection-rope { +.selection-box, +.selection-rope { stroke: dimgray; fill: gray; fill-opacity: 0.5; @@ -569,11 +584,13 @@ label + select { stroke: #B0B0B0; } -.helix-horz-line, .helix-vert-minor-line { +.helix-horz-line, +.helix-vert-minor-line { stroke-width: 0.5; } -.helix-vert-end-line, .helix-vert-major-line { +.helix-vert-end-line, +.helix-vert-major-line { stroke-width: 3; } @@ -591,7 +608,8 @@ label + select { stroke: black; stroke-width: 0.5px; fill-opacity: 0.5; - pointer-events: none; /* XXX: need this so we can detect mouse up on non-moving end underneath */ + pointer-events: none; + /* XXX: need this so we can detect mouse up on non-moving end underneath */ } .five-prime-end, @@ -649,6 +667,7 @@ https://stackoverflow.com/questions/47758565/adding-fedropshadow-to-a-vertical-l stroke: hotpink; stroke-width: 5pt; } + .selected.five-prime-end, .selected.three-prime-end, .selected.five-prime-end-first-substrand, @@ -672,6 +691,7 @@ https://stackoverflow.com/questions/47758565/adding-fedropshadow-to-a-vertical-l stroke: hotpink; stroke-width: 5pt; } + .selected .five-prime-end, .selected .three-prime-end, .selected .five-prime-end-first-substrand, @@ -751,10 +771,15 @@ https://stackoverflow.com/questions/47758565/adding-fedropshadow-to-a-vertical-l .modification-text { text-anchor: middle; font-weight: bold; - text-shadow: /*-1px -1px 0 #fff,*/ /*1px -1px 0 #fff,*/ /*-1px 1px 0 #fff,*/ /*1px 1px 0 #fff;*/ -0.7px -0.7px 0 #fff, - 0.7px -0.7px 0 #fff, - -0.7px 0.7px 0 #fff, - 0.7px 0.7px 0 #fff; + text-shadow: + /*-1px -1px 0 #fff,*/ + /*1px -1px 0 #fff,*/ + /*-1px 1px 0 #fff,*/ + /*1px 1px 0 #fff;*/ + -0.7px -0.7px 0 #fff, + 0.7px -0.7px 0 #fff, + -0.7px 0.7px 0 #fff, + 0.7px 0.7px 0 #fff; } .dna-seq { @@ -764,12 +789,19 @@ https://stackoverflow.com/questions/47758565/adding-fedropshadow-to-a-vertical-l text-anchor: start; pointer-events: none; dominant-baseline: ideographic; - text-shadow: /*-1px -1px 0 #fff,*/ /*1px -1px 0 #fff,*/ /*-1px 1px 0 #fff,*/ /*1px 1px 0 #fff;*/ -0.7px -0.7px 0 #fff, - 0.7px -0.7px 0 #fff, - -0.7px 0.7px 0 #fff, - 0.7px 0.7px 0 #fff; - text-rendering: optimizeSpeed; /* doesn't seem to do much to improve rendering speed */ - white-space: pre; /* needed to display multiple space symbols in a row */ + text-shadow: + /*-1px -1px 0 #fff,*/ + /*1px -1px 0 #fff,*/ + /*-1px 1px 0 #fff,*/ + /*1px 1px 0 #fff;*/ + -0.7px -0.7px 0 #fff, + 0.7px -0.7px 0 #fff, + -0.7px 0.7px 0 #fff, + 0.7px 0.7px 0 #fff; + text-rendering: optimizeSpeed; + /* doesn't seem to do much to improve rendering speed */ + white-space: pre; + /* needed to display multiple space symbols in a row */ } .loopout-extension-length, @@ -780,10 +812,11 @@ https://stackoverflow.com/questions/47758565/adding-fedropshadow-to-a-vertical-l font-weight: bold; text-anchor: middle; pointer-events: none; - text-shadow: /*-1px -1px 0 #fff,*/ /*1px -1px 0 #fff,*/ /*-1px 1px 0 #fff,*/ /*1px 1px 0 #fff;*/ -0.7px -0.7px 0 #fff, - 0.7px -0.7px 0 #fff, - -0.7px 0.7px 0 #fff, - 0.7px 0.7px 0 #fff; + text-shadow: + -0.7px -0.7px 0 #fff, + 0.7px -0.7px 0 #fff, + -0.7px 0.7px 0 #fff, + 0.7px 0.7px 0 #fff; text-rendering: optimizeSpeed; /* doesn't seem to do much to improve rendering speed */ white-space: pre; /* needed to display multiple space symbols in a row */ } @@ -802,12 +835,19 @@ https://stackoverflow.com/questions/47758565/adding-fedropshadow-to-a-vertical-l text-anchor: middle; pointer-events: none; dominant-baseline: ideographic; - text-shadow: /*-1px -1px 0 #fff,*/ /*1px -1px 0 #fff,*/ /*-1px 1px 0 #fff,*/ /*1px 1px 0 #fff;*/ -0.7px -0.7px 0 #fff, - 0.7px -0.7px 0 #fff, - -0.7px 0.7px 0 #fff, - 0.7px 0.7px 0 #fff; - text-rendering: optimizeSpeed; /* doesn't seem to do much to improve rendering speed */ - white-space: pre; /* needed to display multiple space symbols in a row */ + text-shadow: + /*-1px -1px 0 #fff,*/ + /*1px -1px 0 #fff,*/ + /*-1px 1px 0 #fff,*/ + /*1px 1px 0 #fff;*/ + -0.7px -0.7px 0 #fff, + 0.7px -0.7px 0 #fff, + -0.7px 0.7px 0 #fff, + 0.7px 0.7px 0 #fff; + text-rendering: optimizeSpeed; + /* doesn't seem to do much to improve rendering speed */ + white-space: pre; + /* needed to display multiple space symbols in a row */ } /* next is needed so that we can rotate ends on extensions @@ -842,7 +882,8 @@ https://stackoverflow.com/questions/15138801/rotate-rectangle-around-its-own-cen font-family: Helvetica, serif; font-size: 32pt; fill: DarkBlue; - stroke: none; /* don't know why this is necessary to prevent a gray stoke from hiding most of the text */ + stroke: none; + /* don't know why this is necessary to prevent a gray stoke from hiding most of the text */ text-anchor: middle; dominant-baseline: central; } @@ -866,7 +907,9 @@ https://stackoverflow.com/questions/15138801/rotate-rectangle-around-its-own-cen ************************************************/ /* Thin the navbar */ -.navbar, .dropdown-menu, .form-control { +.navbar, +.dropdown-menu, +.form-control { font-size: 0.8rem; } @@ -891,12 +934,12 @@ https://stackoverflow.com/questions/15138801/rotate-rectangle-around-its-own-cen } /* File checkmarks appear on one line */ -#file-nav-dropdown + .dropdown-menu label { +#file-nav-dropdown+.dropdown-menu label { white-space: nowrap; } /* View checkmarks appear on one line */ -#view-nav-dropdown + .dropdown-menu label { +#view-nav-dropdown+.dropdown-menu label { white-space: nowrap; } @@ -954,7 +997,8 @@ https://stackoverflow.com/questions/15138801/rotate-rectangle-around-its-own-cen ************************************************/ /* Style dropdown item of selected grid as well as newly clicked on grid */ -#grid-nav-dropdown + .dropdown-menu .dropdown-item.disabled, .dropdown-item:active { +#grid-nav-dropdown+.dropdown-menu .dropdown-item.disabled, +.dropdown-item:active { color: #fff; text-decoration: none; background-color: #686868; @@ -966,11 +1010,12 @@ https://stackoverflow.com/questions/15138801/rotate-rectangle-around-its-own-cen } /* Styles the label like a DropdownItem */ -.form-file-dropdown .custom-file-input + .custom-file-label { +.form-file-dropdown .custom-file-input+.custom-file-label { display: block; width: 100%; padding: .25rem 1.5rem; - margin-bottom: 0%; /* Removes label browser default */ + margin-bottom: 0%; + /* Removes label browser default */ clear: both; font-weight: 400; color: #212529; @@ -981,14 +1026,14 @@ https://stackoverflow.com/questions/15138801/rotate-rectangle-around-its-own-cen } /* Styles the label like a DropdownItem (on hover) */ -.form-file-dropdown .custom-file-input + .custom-file-label:hover { +.form-file-dropdown .custom-file-input+.custom-file-label:hover { color: #16181b; text-decoration: none; background-color: #f8f9fa; } /* Hides the data-browse psuedo-element. */ -.form-file-dropdown .custom-file-input + .custom-file-label::after { +.form-file-dropdown .custom-file-input+.custom-file-label::after { content: none; } @@ -1007,7 +1052,7 @@ https://stackoverflow.com/questions/15138801/rotate-rectangle-around-its-own-cen ************************************************/ .slice-bar-rect rect { - fill:goldenrod; + fill: goldenrod; fill-opacity: 50%; cursor: move; stroke: rgb(131, 97, 3); @@ -1015,7 +1060,7 @@ https://stackoverflow.com/questions/15138801/rotate-rectangle-around-its-own-cen } .slice-bar-rect text { - fill:rgb(131, 97, 3); + fill: rgb(131, 97, 3); fill-opacity: 100%; font-size: 12px; text-anchor: middle;