shinestacker 1.1.0__py3-none-any.whl → 1.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of shinestacker might be problematic. Click here for more details.

Files changed (38) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/__init__.py +4 -1
  3. shinestacker/algorithms/align.py +149 -34
  4. shinestacker/algorithms/balance.py +364 -166
  5. shinestacker/algorithms/base_stack_algo.py +6 -0
  6. shinestacker/algorithms/depth_map.py +1 -1
  7. shinestacker/algorithms/multilayer.py +22 -13
  8. shinestacker/algorithms/noise_detection.py +7 -8
  9. shinestacker/algorithms/pyramid.py +3 -2
  10. shinestacker/algorithms/pyramid_auto.py +141 -0
  11. shinestacker/algorithms/pyramid_tiles.py +199 -44
  12. shinestacker/algorithms/stack.py +20 -20
  13. shinestacker/algorithms/stack_framework.py +136 -156
  14. shinestacker/algorithms/utils.py +175 -1
  15. shinestacker/algorithms/vignetting.py +26 -8
  16. shinestacker/config/constants.py +31 -6
  17. shinestacker/core/framework.py +12 -12
  18. shinestacker/gui/action_config.py +59 -7
  19. shinestacker/gui/action_config_dialog.py +427 -283
  20. shinestacker/gui/base_form_dialog.py +11 -6
  21. shinestacker/gui/gui_images.py +10 -10
  22. shinestacker/gui/gui_run.py +1 -1
  23. shinestacker/gui/main_window.py +6 -5
  24. shinestacker/gui/menu_manager.py +16 -2
  25. shinestacker/gui/new_project.py +26 -22
  26. shinestacker/gui/project_controller.py +43 -27
  27. shinestacker/gui/project_converter.py +2 -8
  28. shinestacker/gui/project_editor.py +50 -27
  29. shinestacker/gui/tab_widget.py +3 -3
  30. shinestacker/retouch/exif_data.py +5 -5
  31. shinestacker/retouch/shortcuts_help.py +4 -4
  32. shinestacker/retouch/vignetting_filter.py +12 -8
  33. {shinestacker-1.1.0.dist-info → shinestacker-1.2.1.dist-info}/METADATA +1 -1
  34. {shinestacker-1.1.0.dist-info → shinestacker-1.2.1.dist-info}/RECORD +38 -37
  35. {shinestacker-1.1.0.dist-info → shinestacker-1.2.1.dist-info}/WHEEL +0 -0
  36. {shinestacker-1.1.0.dist-info → shinestacker-1.2.1.dist-info}/entry_points.txt +0 -0
  37. {shinestacker-1.1.0.dist-info → shinestacker-1.2.1.dist-info}/licenses/LICENSE +0 -0
  38. {shinestacker-1.1.0.dist-info → shinestacker-1.2.1.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,5 @@
1
1
  # pylint: disable=C0114, C0115, C0116, R0902, E1101, W0718, W0640, R0913, R0917, R0914
2
+ import math
2
3
  import traceback
3
4
  import logging
4
5
  import numpy as np
@@ -49,16 +50,30 @@ def fit_sigmoid(radii, intensities):
49
50
  return res
50
51
 
51
52
 
53
+ def subsample_factor(subsample, image):
54
+ if subsample == 0:
55
+ h, w = image.shape[:2]
56
+ img_res = (float(h) / 1000) * (float(w) / 1000)
57
+ target_res = constants.DEFAULT_BALANCE_RES_TARGET_MPX
58
+ subsample = int(1 + math.floor(img_res / target_res))
59
+ return subsample
60
+
61
+
52
62
  def img_subsampled(image, subsample=constants.DEFAULT_VIGN_SUBSAMPLE,
53
63
  fast_subsampling=constants.DEFAULT_VIGN_FAST_SUBSAMPLING):
54
64
  image_bw = cv2.cvtColor(img_8bit(image), cv2.COLOR_BGR2GRAY)
55
- return image_bw if subsample == 1 else img_subsample(image_bw, subsample, fast_subsampling)
65
+ if subsample == 0:
66
+ subsample = subsample_factor(subsample, image)
67
+ img_sub = image_bw if subsample == 1 else img_subsample(image_bw, subsample, fast_subsampling)
68
+ return img_sub
56
69
 
57
70
 
58
71
  def compute_fit_parameters(
59
72
  image, r_steps, radii=None, intensities=None,
60
73
  subsample=constants.DEFAULT_VIGN_SUBSAMPLE,
61
74
  fast_subsampling=constants.DEFAULT_VIGN_FAST_SUBSAMPLING):
75
+ if subsample == 0:
76
+ subsample = subsample_factor(subsample, image)
62
77
  image_sub = img_subsampled(image, subsample, fast_subsampling)
63
78
  if radii is None and intensities is None:
64
79
  radii, intensities = radial_mean_intensity(image_sub, r_steps)
@@ -77,6 +92,8 @@ def correct_vignetting(
77
92
  if params is None:
78
93
  if r_steps is None:
79
94
  raise RuntimeError("Either r_steps or pars must not be None")
95
+ if subsample == 0:
96
+ subsample = subsample_factor(subsample, image)
80
97
  params = compute_fit_parameters(
81
98
  image, r_steps, subsample=subsample, fast_subsampling=fast_subsampling)
82
99
  if v0 is None:
@@ -121,11 +138,12 @@ class Vignetting(SubAction):
121
138
  h, w = img_0.shape[:2]
122
139
  self.w_2, self.h_2 = w / 2, h / 2
123
140
  self.r_max = np.sqrt((w / 2)**2 + (h / 2)**2)
124
- image_sub = img_subsampled(img_0, self.subsample, self.fast_subsampling)
141
+ subsample = subsample_factor(self.subsample, img_0)
142
+ image_sub = img_subsampled(img_0, subsample, self.fast_subsampling)
125
143
  radii, intensities = radial_mean_intensity(image_sub, self.r_steps)
126
144
  try:
127
145
  params = compute_fit_parameters(
128
- img_0, self.r_steps, radii, intensities, self.subsample, self.fast_subsampling)
146
+ img_0, self.r_steps, radii, intensities, subsample, self.fast_subsampling)
129
147
  except Exception as e:
130
148
  traceback.print_tb(e.__traceback__)
131
149
  self.process.sub_message(
@@ -142,9 +160,9 @@ class Vignetting(SubAction):
142
160
  "light_blue"),
143
161
  level=logging.DEBUG)
144
162
  if self.plot_correction:
145
- plt.figure(figsize=(10, 5))
163
+ plt.figure(figsize=constants.PLT_FIG_SIZE)
146
164
  plt.plot(radii, intensities, label="image mean intensity")
147
- plt.plot(radii, sigmoid_model(radii * self.subsample, *params), label="sigmoid fit")
165
+ plt.plot(radii, sigmoid_model(radii * subsample, *params), label="sigmoid fit")
148
166
  plt.xlabel('radius (pixels)')
149
167
  plt.ylabel('mean intensity')
150
168
  plt.legend()
@@ -165,16 +183,16 @@ class Vignetting(SubAction):
165
183
  self.process.sub_message_r(color_str(": correct vignetting", "cyan"))
166
184
  return correct_vignetting(
167
185
  img_0, self.max_correction, self.black_threshold, None, params, self.v0,
168
- self.subsample, self.fast_subsampling)
186
+ subsample, self.fast_subsampling)
169
187
 
170
188
  def begin(self, process):
171
189
  self.process = process
172
- self.corrections = [np.full(self.process.counts, None, dtype=float)
190
+ self.corrections = [np.full(self.process.total_action_counts, None, dtype=float)
173
191
  for p in self.percentiles]
174
192
 
175
193
  def end(self):
176
194
  if self.plot_summary:
177
- plt.figure(figsize=(10, 5))
195
+ plt.figure(figsize=constants.PLT_FIG_SIZE)
178
196
  xs = np.arange(1, len(self.corrections[0]) + 1, dtype=int)
179
197
  for i, p in enumerate(self.percentiles):
180
198
  linestyle = 'solid'
@@ -1,6 +1,7 @@
1
1
  # pylint: disable=C0114, C0115, C0116, C0103, R0903
2
2
  import sys
3
3
  import re
4
+ import os
4
5
 
5
6
 
6
7
  class _Constants:
@@ -12,12 +13,17 @@ class _Constants:
12
13
  NUM_UINT16 = 65536
13
14
  MAX_UINT8 = 255
14
15
  MAX_UINT16 = 65535
16
+ ONE_KILO = 1024
17
+ ONE_MEGA = ONE_KILO**2
18
+ ONE_GIGA = ONE_KILO**3
15
19
 
16
20
  LOG_FONTS = ['Monaco', 'Menlo', ' Lucida Console', 'Courier New', 'Courier', 'monospace']
17
21
  LOG_FONTS_STR = ", ".join(LOG_FONTS)
18
22
 
19
23
  ANSI_ESCAPE = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
20
24
 
25
+ PLT_FIG_SIZE = (10, 5)
26
+
21
27
  ACTION_JOB = "Job"
22
28
  ACTION_COMBO = "CombinedActions"
23
29
  ACTION_NOISEDETECTION = "NoiseDetection"
@@ -34,12 +40,17 @@ class _Constants:
34
40
  SUB_ACTION_TYPES = [ACTION_MASKNOISE, ACTION_VIGNETTING, ACTION_ALIGNFRAMES,
35
41
  ACTION_BALANCEFRAMES]
36
42
  STACK_ALGO_PYRAMID = 'Pyramid'
37
- STACK_ALGO_PYRAMID_TILES = 'Pyramid Tiles'
38
43
  STACK_ALGO_DEPTH_MAP = 'Depth map'
39
- STACK_ALGO_OPTIONS = [STACK_ALGO_PYRAMID, STACK_ALGO_PYRAMID_TILES, STACK_ALGO_DEPTH_MAP]
44
+ STACK_ALGO_OPTIONS = [STACK_ALGO_PYRAMID, STACK_ALGO_DEPTH_MAP]
40
45
  STACK_ALGO_DEFAULT = STACK_ALGO_PYRAMID
41
46
  DEFAULT_PLOTS_PATH = 'plots'
42
47
 
48
+ FIELD_SUBSAMPLE_VALUES_1 = [2, 3, 4, 6, 8, 12, 16, 24, 32]
49
+ FIELD_SUBSAMPLE_OPTIONS_1 = [f"1/{n} × 1/{n}" for n in FIELD_SUBSAMPLE_VALUES_1]
50
+ FIELD_SUBSAMPLE_VALUES = [0, 1] + FIELD_SUBSAMPLE_VALUES_1
51
+ FIELD_SUBSAMPLE_OPTIONS = ['Auto', 'Full resolution'] + FIELD_SUBSAMPLE_OPTIONS_1
52
+ FIELD_SUBSAMPLE_DEFAULT = FIELD_SUBSAMPLE_VALUES[0]
53
+
43
54
  PATH_SEPARATOR = ';'
44
55
 
45
56
  LOG_COLOR_ALERT = 'red'
@@ -51,8 +62,10 @@ class _Constants:
51
62
 
52
63
  DEFAULT_FILE_REVERSE_ORDER = False
53
64
  DEFAULT_MULTILAYER_FILE_REVERSE_ORDER = True
65
+ MULTILAYER_WARNING_MEM_GB = 1
54
66
 
55
67
  DEFAULT_NOISE_MAP_FILENAME = "noise-map/hot_pixels.png"
68
+ DEFAULT_NOISE_MAX_FRAMES = 10
56
69
  DEFAULT_MN_KERNEL_SIZE = 3
57
70
  INTERPOLATE_MEAN = 'MEAN'
58
71
  INTERPOLATE_MEDIAN = 'MEDIAN'
@@ -105,9 +118,11 @@ class _Constants:
105
118
  DEFAULT_REFINE_ITERS = 100
106
119
  DEFAULT_ALIGN_CONFIDENCE = 99.9
107
120
  DEFAULT_ALIGN_MAX_ITERS = 2000
121
+ DEFAULT_ALIGN_ABORT_ABNORMAL = False
108
122
  DEFAULT_BORDER_VALUE = [0] * 4
109
123
  DEFAULT_BORDER_BLUR = 50
110
- DEFAULT_ALIGN_SUBSAMPLE = 2
124
+ DEFAULT_ALIGN_SUBSAMPLE = 0
125
+ DEFAULT_ALIGN_RES_TARGET_MPX = 2
111
126
  DEFAULT_ALIGN_FAST_SUBSAMPLING = False
112
127
  DEFAULT_ALIGN_MIN_GOOD_MATCHES = 50
113
128
 
@@ -120,9 +135,12 @@ class _Constants:
120
135
  BALANCE_RGB = "RGB"
121
136
  BALANCE_HSV = "HSV"
122
137
  BALANCE_HLS = "HLS"
123
- VALID_BALANCE_CHANNELS = [BALANCE_LUMI, BALANCE_RGB, BALANCE_HSV, BALANCE_HLS]
138
+ BALANCE_LAB = "LAB"
139
+ VALID_BALANCE_CHANNELS = [BALANCE_LUMI, BALANCE_RGB, BALANCE_HSV, BALANCE_HLS,
140
+ BALANCE_LAB]
124
141
 
125
- DEFAULT_BALANCE_SUBSAMPLE = 8
142
+ DEFAULT_BALANCE_SUBSAMPLE = 0
143
+ DEFAULT_BALANCE_RES_TARGET_MPX = 2
126
144
  DEFAULT_BALANCE_FAST_SUBSAMPLING = False
127
145
  DEFAULT_CORR_MAP = BALANCE_LINEAR
128
146
  DEFAULT_CHANNEL = BALANCE_LUMI
@@ -131,7 +149,8 @@ class _Constants:
131
149
  DEFAULT_R_STEPS = 100
132
150
  DEFAULT_BLACK_THRESHOLD = 1.0
133
151
  DEFAULT_MAX_CORRECTION = 1
134
- DEFAULT_VIGN_SUBSAMPLE = 8
152
+ DEFAULT_VIGN_SUBSAMPLE = 0
153
+ DEFAULT_VIGN_RES_TARGET_MPX = 2
135
154
  DEFAULT_VIGN_FAST_SUBSAMPLING = False
136
155
 
137
156
  FLOAT_32 = 'float-32'
@@ -163,6 +182,12 @@ class _Constants:
163
182
  DEFAULT_PY_KERNEL_SIZE = 5
164
183
  DEFAULT_PY_GEN_KERNEL = 0.4
165
184
  DEFAULT_PY_TILE_SIZE = 512
185
+ DEFAULT_PY_N_TILED_LAYERS = 2
186
+ DEFAULT_PY_MEMORY_LIMIT_GB = 8
187
+ DEFAULT_PY_MAX_THREADS = min(os.cpu_count() or 4, 8)
188
+ DEFAULT_PY_MODE = 'auto'
189
+ PY_VALID_MODES = ['auto', 'memory', 'tiled']
190
+ MIN_PY_TILE_SIZE = 256
166
191
 
167
192
  DEFAULT_PLOT_STACK_BUNCH = False
168
193
  DEFAULT_PLOT_STACK = True
@@ -24,7 +24,7 @@ class TqdmCallbacks:
24
24
 
25
25
  def __init__(self):
26
26
  self.tbar = None
27
- self.counts = -1
27
+ self.total_action_counts = -1
28
28
 
29
29
  @classmethod
30
30
  def instance(cls):
@@ -33,8 +33,8 @@ class TqdmCallbacks:
33
33
  return cls._instance
34
34
 
35
35
  def step_counts(self, name, counts):
36
- self.counts = counts
37
- self.tbar = make_tqdm_bar(name, self.counts)
36
+ self.total_action_counts = counts
37
+ self.tbar = make_tqdm_bar(name, self.total_action_counts)
38
38
 
39
39
  def begin_steps(self, name):
40
40
  pass
@@ -191,12 +191,12 @@ class Job(JobBase):
191
191
  class ActionList(JobBase):
192
192
  def __init__(self, name, enabled=True, **kwargs):
193
193
  JobBase.__init__(self, name, enabled, **kwargs)
194
- self.counts = None
195
- self.count = None
194
+ self.total_action_counts = None
195
+ self.current_action_count = None
196
196
 
197
197
  def set_counts(self, counts):
198
- self.counts = counts
199
- self.callback('step_counts', self.id, self.name, self.counts)
198
+ self.total_action_counts = counts
199
+ self.callback('step_counts', self.id, self.name, self.total_action_counts)
200
200
 
201
201
  def begin(self):
202
202
  self.callback('begin_steps', self.id, self.name)
@@ -205,17 +205,17 @@ class ActionList(JobBase):
205
205
  self.callback('end_steps', self.id, self.name)
206
206
 
207
207
  def __iter__(self):
208
- self.count = 0
208
+ self.current_action_count = 0
209
209
  return self
210
210
 
211
211
  def run_step(self):
212
212
  pass
213
213
 
214
214
  def __next__(self):
215
- if self.count < self.counts:
215
+ if self.current_action_count < self.total_action_counts:
216
216
  self.run_step()
217
- x = self.count
218
- self.count += 1
217
+ x = self.current_action_count
218
+ self.current_action_count += 1
219
219
  return x
220
220
  raise StopIteration
221
221
 
@@ -223,7 +223,7 @@ class ActionList(JobBase):
223
223
  self.print_message(color_str('begin run', constants.LOG_COLOR_LEVEL_2), end='\n')
224
224
  self.begin()
225
225
  for _ in iter(self):
226
- self.callback('after_step', self.id, self.name, self.count)
226
+ self.callback('after_step', self.id, self.name, self.current_action_count)
227
227
  if self.callback('check_running', self.id, self.name) is False:
228
228
  raise RunStopException(self.name)
229
229
  self.end()
@@ -10,19 +10,22 @@ from PySide6.QtWidgets import (QPushButton, QHBoxLayout, QFileDialog, QLabel, QC
10
10
  from .. config.constants import constants
11
11
  from .select_path_widget import (create_select_file_paths_widget, create_layout_widget_no_margins,
12
12
  create_layout_widget_and_connect)
13
- from .project_model import ActionConfig
14
13
 
15
14
  FIELD_TEXT = 'text'
16
15
  FIELD_ABS_PATH = 'abs_path'
17
16
  FIELD_REL_PATH = 'rel_path'
18
17
  FIELD_FLOAT = 'float'
19
18
  FIELD_INT = 'int'
19
+ FIELD_REF_IDX = 'ref_idx'
20
20
  FIELD_INT_TUPLE = 'int_tuple'
21
21
  FIELD_BOOL = 'bool'
22
22
  FIELD_COMBO = 'combo'
23
23
  FIELD_TYPES = [FIELD_TEXT, FIELD_ABS_PATH, FIELD_REL_PATH, FIELD_FLOAT,
24
24
  FIELD_INT, FIELD_INT_TUPLE, FIELD_BOOL, FIELD_COMBO]
25
25
 
26
+ FIELD_REF_IDX_OPTIONS = ['Median frame', 'First frame', 'Last frame', 'Specify index']
27
+ FIELD_REF_IDX_MAX = 1000
28
+
26
29
 
27
30
  class ActionConfigurator(ABC):
28
31
  def __init__(self, expert, current_wd):
@@ -30,23 +33,23 @@ class ActionConfigurator(ABC):
30
33
  self.current_wd = current_wd
31
34
 
32
35
  @abstractmethod
33
- def create_form(self, layout, action: ActionConfig, tag="Action"):
36
+ def create_form(self, layout, action, tag="Action"):
34
37
  pass
35
38
 
36
39
  @abstractmethod
37
- def update_params(self, params: Dict[str, Any]) -> bool:
40
+ def update_params(self, params):
38
41
  pass
39
42
 
40
43
 
41
44
  class FieldBuilder:
42
45
  def __init__(self, layout, action, current_wd):
43
- self.layout = layout
46
+ self.main_layout = layout
44
47
  self.action = action
45
48
  self.current_wd = current_wd
46
49
  self.fields = {}
47
50
 
48
- def add_field(self, tag: str, field_type: str, label: str,
49
- required: bool = False, add_to_layout=None, **kwargs):
51
+ def add_field(self, tag, field_type, label,
52
+ required=False, add_to_layout=None, **kwargs):
50
53
  if field_type == FIELD_TEXT:
51
54
  widget = self.create_text_field(tag, **kwargs)
52
55
  elif field_type == FIELD_ABS_PATH:
@@ -57,6 +60,8 @@ class FieldBuilder:
57
60
  widget = self.create_float_field(tag, **kwargs)
58
61
  elif field_type == FIELD_INT:
59
62
  widget = self.create_int_field(tag, **kwargs)
63
+ elif field_type == FIELD_REF_IDX:
64
+ widget = self.create_ref_idx_field(tag, **kwargs)
60
65
  elif field_type == FIELD_INT_TUPLE:
61
66
  widget = self.create_int_tuple_field(tag, **kwargs)
62
67
  elif field_type == FIELD_BOOL:
@@ -76,6 +81,8 @@ class FieldBuilder:
76
81
  default_value = kwargs.get('default', 0.0)
77
82
  elif field_type == FIELD_INT:
78
83
  default_value = kwargs.get('default', 0)
84
+ elif field_type == FIELD_REF_IDX:
85
+ default_value = kwargs.get('default', 0)
79
86
  elif field_type == FIELD_INT_TUPLE:
80
87
  default_value = kwargs.get('default', [0] * kwargs.get('size', 1))
81
88
  elif field_type == FIELD_BOOL:
@@ -93,7 +100,7 @@ class FieldBuilder:
93
100
  **kwargs
94
101
  }
95
102
  if add_to_layout is None:
96
- add_to_layout = self.layout
103
+ add_to_layout = self.main_layout
97
104
  add_to_layout.addRow(f"{label}:", widget)
98
105
  return widget
99
106
 
@@ -113,6 +120,9 @@ class FieldBuilder:
113
120
  widget.setChecked(default)
114
121
  elif field['type'] == FIELD_INT:
115
122
  widget.setValue(default)
123
+ elif field['type'] == FIELD_REF_IDX:
124
+ widget.layout().itemAt(2).widget().setValue(default)
125
+ widget.layout().itemAt(0).widget().setCurrentText(FIELD_REF_IDX_OPTIONS[0])
116
126
  elif field['type'] == FIELD_INT_TUPLE:
117
127
  for i in range(field['size']):
118
128
  spinbox = widget.layout().itemAt(1 + i * 2).widget()
@@ -147,6 +157,17 @@ class FieldBuilder:
147
157
  params[tag] = field['widget'].isChecked()
148
158
  elif field['type'] == FIELD_INT:
149
159
  params[tag] = field['widget'].value()
160
+ elif field['type'] == FIELD_REF_IDX:
161
+ wl = field['widget'].layout()
162
+ txt = wl.itemAt(0).widget().currentText()
163
+ if txt == FIELD_REF_IDX_OPTIONS[0]:
164
+ params[tag] = 0
165
+ elif txt == FIELD_REF_IDX_OPTIONS[1]:
166
+ params[tag] = 1
167
+ elif txt == FIELD_REF_IDX_OPTIONS[2]:
168
+ params[tag] = -1
169
+ else:
170
+ params[tag] = wl.itemAt(2).widget().value()
150
171
  elif field['type'] == FIELD_INT_TUPLE:
151
172
  params[tag] = [field['widget'].layout().itemAt(1 + i * 2).widget().value()
152
173
  for i in range(field['size'])]
@@ -328,6 +349,37 @@ class FieldBuilder:
328
349
  spin.setValue(self.action.params.get(tag, default))
329
350
  return spin
330
351
 
352
+ def create_ref_idx_field(self, tag, default=0):
353
+ layout = QHBoxLayout()
354
+ combo = QComboBox()
355
+ combo.addItems(FIELD_REF_IDX_OPTIONS)
356
+ label = QLabel("index [1, ..., N]: ")
357
+ spin = QSpinBox()
358
+ spin.setRange(1, FIELD_REF_IDX_MAX)
359
+ value = self.action.params.get(tag, default)
360
+ if value == 0:
361
+ combo.setCurrentText(FIELD_REF_IDX_OPTIONS[0])
362
+ spin.setValue(1)
363
+ elif value == 1:
364
+ combo.setCurrentText(FIELD_REF_IDX_OPTIONS[1])
365
+ spin.setValue(1)
366
+ elif value == -1:
367
+ combo.setCurrentText(FIELD_REF_IDX_OPTIONS[2])
368
+ spin.setValue(1)
369
+ else:
370
+ combo.setCurrentText(FIELD_REF_IDX_OPTIONS[3])
371
+ spin.setValue(value)
372
+
373
+ def set_enabled():
374
+ spin.setEnabled(combo.currentText() == FIELD_REF_IDX_OPTIONS[-1])
375
+
376
+ combo.currentTextChanged.connect(set_enabled)
377
+ set_enabled()
378
+ layout.addWidget(combo)
379
+ layout.addWidget(label)
380
+ layout.addWidget(spin)
381
+ return create_layout_widget_no_margins(layout)
382
+
331
383
  def create_int_tuple_field(self, tag, size=1,
332
384
  default=[0] * 100, min_val=[0] * 100, max_val=[100] * 100,
333
385
  **kwargs):