class GroupSelectParameterWidget(GenericParameterWidget): """Widget class for Group Select Parameter.""" def __init__(self, parameter, parent=None): """Constructor. :param parameter: A GroupSelectParameter object. :type parameter: GroupSelectParameter """ QWidget.__init__(self, parent) self._parameter = parameter # Store spin box self.spin_boxes = {} # Create elements # Label (name) self.label = QLabel(self._parameter.name) # Layouts self.main_layout = QVBoxLayout() self.input_layout = QVBoxLayout() # _inner_input_layout must be filled with widget in the child class self.inner_input_layout = QVBoxLayout() self.radio_button_layout = QGridLayout() # Create radio button group self.input_button_group = QButtonGroup() # List widget self.list_widget = QListWidget() self.list_widget.setSelectionMode(QAbstractItemView.ExtendedSelection) self.list_widget.setDragDropMode(QAbstractItemView.DragDrop) self.list_widget.setDefaultDropAction(Qt.MoveAction) self.list_widget.setEnabled(False) self.list_widget.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Expanding) for i, key in enumerate(self._parameter.options): value = self._parameter.options[key] radio_button = QRadioButton(value.get('label')) self.radio_button_layout.addWidget(radio_button, i, 0) if value.get('type') == SINGLE_DYNAMIC: double_spin_box = QDoubleSpinBox() self.radio_button_layout.addWidget(double_spin_box, i, 1) double_spin_box.setValue(value.get('value', 0)) double_spin_box.setMinimum( value.get('constraint', {}).get('min', 0)) double_spin_box.setMaximum( value.get('constraint', {}).get('max', 1)) double_spin_box.setSingleStep( value.get('constraint', {}).get('step', 0.01)) step = double_spin_box.singleStep() if step > 1: precision = 0 else: precision = len(str(step).split('.')[1]) if precision > 3: precision = 3 double_spin_box.setDecimals(precision) self.spin_boxes[key] = double_spin_box # Enable spin box depends on the selected option if self._parameter.selected == key: double_spin_box.setEnabled(True) else: double_spin_box.setEnabled(False) elif value.get('type') == STATIC: static_value = value.get('value', 0) if static_value is not None: self.radio_button_layout.addWidget( QLabel(str(static_value)), i, 1) elif value.get('type') == MULTIPLE_DYNAMIC: selected_fields = value.get('value', []) if self._parameter.selected == key: self.list_widget.setEnabled(True) else: self.list_widget.setEnabled(False) self.input_button_group.addButton(radio_button, i) if self._parameter.selected == key: radio_button.setChecked(True) # Help text self.help_label = QLabel(self._parameter.help_text) self.help_label.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Expanding) self.help_label.setWordWrap(True) self.help_label.setAlignment(Qt.AlignTop) self.inner_input_layout.addLayout(self.radio_button_layout) self.inner_input_layout.addWidget(self.list_widget) # Put elements into layouts self.input_layout.addWidget(self.label) self.input_layout.addLayout(self.inner_input_layout) self.help_layout = QVBoxLayout() self.help_layout.addWidget(self.help_label) self.main_layout.addLayout(self.input_layout) self.main_layout.addLayout(self.help_layout) self.setLayout(self.main_layout) self.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Expanding) # Update list widget self.update_list_widget() # Connect signal self.input_button_group.buttonClicked.connect( self.radio_buttons_clicked) def get_parameter(self): """Obtain list parameter object from the current widget state. :returns: A DefaultValueParameter from the current state of widget :rtype: DefaultValueParameter """ # Set value for each key for key, value in self._parameter.options.items(): if value.get('type') == STATIC: continue elif value.get('type') == SINGLE_DYNAMIC: new_value = self.spin_boxes.get(key).value() self._parameter.set_value_for_key(key, new_value) elif value.get('type') == MULTIPLE_DYNAMIC: # Need to iterate through all items items = [] for index in xrange(self.list_widget.count()): items.append(self.list_widget.item(index)) new_value = [i.text() for i in items] self._parameter.set_value_for_key(key, new_value) # Get selected radio button radio_button_checked_id = self.input_button_group.checkedId() # No radio button checked, then default value = None if radio_button_checked_id == -1: self._parameter.selected = None else: self._parameter.selected = self._parameter.options.keys( )[radio_button_checked_id] return self._parameter def update_list_widget(self): """Update list widget when radio button is clicked.""" # Get selected radio button radio_button_checked_id = self.input_button_group.checkedId() # No radio button checked, then default value = None if radio_button_checked_id > -1: selected_dict = self._parameter.options.values( )[radio_button_checked_id] if selected_dict.get('type') == MULTIPLE_DYNAMIC: for field in selected_dict.get('value'): # Update list widget field_item = QListWidgetItem(self.list_widget) field_item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled) field_item.setData(Qt.UserRole, field) field_item.setText(field) self.list_widget.addItem(field_item) def radio_buttons_clicked(self): """Handler when selected radio button changed.""" # Disable all spin boxes for spin_box in self.spin_boxes.values(): spin_box.setEnabled(False) # Disable list widget self.list_widget.setEnabled(False) # Get selected radio button radio_button_checked_id = self.input_button_group.checkedId() if radio_button_checked_id > -1: selected_value = self._parameter.options.values( )[radio_button_checked_id] if selected_value.get('type') == MULTIPLE_DYNAMIC: # Enable list widget self.list_widget.setEnabled(True) elif selected_value.get('type') == SINGLE_DYNAMIC: selected_key = self._parameter.options.keys( )[radio_button_checked_id] self.spin_boxes[selected_key].setEnabled(True) def select_radio_button(self, key): """Helper to select a radio button with key. :param key: The key of the radio button. :type key: str """ key_index = self._parameter.options.keys().index(key) radio_button = self.input_button_group.button(key_index) radio_button.click()
class StepKwMultiClassifications(WizardStep, FORM_CLASS): """InaSAFE Wizard Step Multi Classifications.""" def __init__(self, parent=None): """Constructor for the tab. :param parent: widget to use as parent (Wizard Dialog). :type parent: QWidget """ WizardStep.__init__(self, parent) self.exposures = [] self.exposure_labels = [] self.exposure_combo_boxes = [] self.exposure_edit_buttons = [] self.mode = CHOOSE_MODE self.layer_purpose = None self.layer_mode = None # Store the current representative state of the UI. # self.classifications = {} self.value_maps = {} self.thresholds = {} # Temporary attributes self.threshold_classes = OrderedDict() self.active_exposure = None self.list_unique_values = None self.tree_mapping_widget = None # GUI, good for testing self.save_button = None # Has default threshold # Trick for EQ raster for population #3853 self.use_default_thresholds = False # Index of the special case exposure classification self.special_case_index = None def is_ready_to_next_step(self): """Check if the step is complete. :returns: True if new step may be enabled. :rtype: bool """ # Still editing if self.mode == EDIT_MODE: return False for combo_box in self.exposure_combo_boxes: # Enable if there is one that has classification if combo_box.currentIndex() > 0: return True # Trick for EQ raster for population #3853 if self.use_default_thresholds: return True return False def get_next_step(self): """Find the proper step when user clicks the Next button. :returns: The step to be switched to. :rtype: WizardStep instance or None """ if self.layer_purpose != layer_purpose_aggregation: subcategory = self.parent.step_kw_subcategory.\ selected_subcategory() else: subcategory = {'key': None} if is_raster_layer(self.parent.layer): return self.parent.step_kw_source # Check if it can go to inasafe field step inasafe_fields = get_non_compulsory_fields( self.layer_purpose['key'], subcategory['key']) if not skip_inasafe_field(self.parent.layer, inasafe_fields): return self.parent.step_kw_inasafe_fields # Check if it can go to inasafe default field step default_inasafe_fields = get_fields( self.layer_purpose['key'], subcategory['key'], replace_null=True, in_group=False ) if default_inasafe_fields: return self.parent.step_kw_default_inasafe_fields # Any other case return self.parent.step_kw_source def set_wizard_step_description(self): """Set the text for description.""" subcategory = self.parent.step_kw_subcategory.selected_subcategory() field = self.parent.step_kw_field.selected_fields() is_raster = is_raster_layer(self.parent.layer) if is_raster: if self.layer_mode == layer_mode_continuous: text_label = multiple_continuous_hazard_classifications_raster else: text_label = multiple_classified_hazard_classifications_raster # noinspection PyAugmentAssignment text_label = text_label % ( subcategory['name'], self.layer_purpose['name']) else: if self.layer_mode == layer_mode_continuous: text_label = multiple_continuous_hazard_classifications_vector else: text_label = multiple_classified_hazard_classifications_vector # noinspection PyAugmentAssignment text_label = text_label % ( subcategory['name'], self.layer_purpose['name'], field) self.multi_classifications_label.setText(text_label) def setup_left_panel(self): """Setup the UI for left panel. Generate all exposure, combobox, and edit button. """ hazard = self.parent.step_kw_subcategory.selected_subcategory() left_panel_heading = QLabel(tr('Classifications')) left_panel_heading.setFont(big_font) self.left_layout.addWidget(left_panel_heading) inner_left_layout = QGridLayout() row = 0 for exposure in exposure_all: special_case = False # Filter out unsupported exposure for the hazard if exposure in hazard['disabled_exposures']: # Remove from the storage if the exposure is disabled if self.layer_mode == layer_mode_continuous: if exposure['key'] in self.thresholds: self.thresholds.pop(exposure['key']) else: if exposure['key'] in self.value_maps: self.value_maps.pop(exposure['key']) continue # Trick for EQ raster for population #3853 if exposure == exposure_population and hazard == hazard_earthquake: if is_raster_layer(self.parent.layer): if self.layer_mode == layer_mode_continuous: self.use_default_thresholds = True special_case = True # Set classification for EQ Raster for Population self.thresholds[exposure_population['key']] = { earthquake_mmi_scale['key']: { 'classes': default_classification_thresholds( earthquake_mmi_scale), 'active': True } } # Add label # Hazard on Exposure Classifications label = tr( '{hazard_name} on {exposure_name} Classifications').format( hazard_name=hazard['name'], exposure_name=exposure['name'] ) exposure_label = QLabel(label) # Add combo box exposure_combo_box = QComboBox() hazard_classifications = hazard.get('classifications') exposure_combo_box.addItem(tr('No classifications')) exposure_combo_box.setItemData( 0, None, Qt.UserRole) current_index = 0 i = 0 # Iterate through all available hazard classifications for hazard_classification in hazard_classifications: # Skip if the classification is not for the exposure if 'exposures' in hazard_classification: if exposure not in hazard_classification['exposures']: continue exposure_combo_box.addItem(hazard_classification['name']) exposure_combo_box.setItemData( i + 1, hazard_classification, Qt.UserRole) if self.layer_mode == layer_mode_continuous: current_hazard_classifications = self.thresholds.get( exposure['key']) else: current_hazard_classifications = self.value_maps.get( exposure['key']) if current_hazard_classifications: current_hazard_classification = \ current_hazard_classifications.get( hazard_classification['key']) if current_hazard_classification: is_active = current_hazard_classification.get('active') if is_active: current_index = i + 1 i += 1 # Set current classification exposure_combo_box.setCurrentIndex(current_index) # Add edit button exposure_edit_button = QPushButton(tr('Edit')) # For special case. Raster EQ on Population. if special_case: mmi_index = exposure_combo_box.findText( earthquake_mmi_scale['name']) exposure_combo_box.setCurrentIndex(mmi_index) exposure_combo_box.setEnabled(False) exposure_edit_button.setEnabled(False) tool_tip_message = tr( 'InaSAFE use default classification for Raster Earthquake ' 'hazard on population.') exposure_label.setToolTip(tool_tip_message) exposure_combo_box.setToolTip(tool_tip_message) exposure_edit_button.setToolTip(tool_tip_message) else: if current_index == 0: # Disable if there is no classification chosen. exposure_edit_button.setEnabled(False) exposure_edit_button.clicked.connect( partial(self.edit_button_clicked, edit_button=exposure_edit_button, exposure_combo_box=exposure_combo_box, exposure=exposure)) exposure_combo_box.currentIndexChanged.connect( partial( self.classifications_combo_box_changed, exposure=exposure, exposure_combo_box=exposure_combo_box, edit_button=exposure_edit_button)) # Arrange in layout inner_left_layout.addWidget(exposure_label, row, 0) inner_left_layout.addWidget(exposure_combo_box, row, 1) inner_left_layout.addWidget(exposure_edit_button, row, 2) # Adding to step's attribute self.exposures.append(exposure) self.exposure_combo_boxes.append(exposure_combo_box) self.exposure_edit_buttons.append(exposure_edit_button) self.exposure_labels.append(label) if special_case: self.special_case_index = len(self.exposures) - 1 row += 1 self.left_layout.addLayout(inner_left_layout) # To push the inner_left_layout up self.left_layout.addStretch(1) # noinspection PyUnusedLocal def edit_button_clicked(self, edit_button, exposure_combo_box, exposure): """Method to handle when an edit button is clicked. :param edit_button: The edit button. :type edit_button: QPushButton :param exposure_combo_box: The combo box of the exposure, contains list of classifications. :type exposure_combo_box: QComboBox :param exposure: Exposure definition. :type exposure: dict """ # Note(IS): Do not change the text of edit button for now until we # have better behaviour. classification = self.get_classification(exposure_combo_box) if self.mode == CHOOSE_MODE: # Change mode self.mode = EDIT_MODE # Set active exposure self.active_exposure = exposure # Disable all edit button for exposure_edit_button in self.exposure_edit_buttons: exposure_edit_button.setEnabled(False) # Except one that was clicked # edit_button.setEnabled(True) # Disable all combo box for exposure_combo_box in self.exposure_combo_boxes: exposure_combo_box.setEnabled(False) # Change the edit button to cancel # edit_button.setText(tr('Cancel')) # Clear right panel clear_layout(self.right_layout) # Show edit threshold or value mapping if self.layer_mode == layer_mode_continuous: self.setup_thresholds_panel(classification) else: self.setup_value_mapping_panels(classification) self.add_buttons(classification) elif self.mode == EDIT_MODE: # Behave the same as cancel button clicked. self.cancel_button_clicked() self.parent.pbnNext.setEnabled(self.is_ready_to_next_step()) def show_current_state(self): """Setup the UI for QTextEdit to show the current state.""" right_panel_heading = QLabel(tr('Status')) right_panel_heading.setFont(big_font) right_panel_heading.setSizePolicy( QSizePolicy.Maximum, QSizePolicy.Maximum) self.right_layout.addWidget(right_panel_heading) message = m.Message() if self.layer_mode == layer_mode_continuous: title = tr('Thresholds') else: title = tr('Value maps') message.add(m.Heading(title, **INFO_STYLE)) for i in range(len(self.exposures)): message.add(m.Text(self.exposure_labels[i])) classification = self.get_classification( self.exposure_combo_boxes[i]) if self.layer_mode == layer_mode_continuous: thresholds = self.thresholds.get(self.exposures[i]['key']) if not thresholds or not classification: message.add(m.Paragraph(tr('No classifications set.'))) continue table = m.Table( style_class='table table-condensed table-striped') header = m.Row() header.add(m.Cell(tr('Class name'))) header.add(m.Cell(tr('Minimum'))) header.add(m.Cell(tr('Maximum'))) table.add(header) classes = classification.get('classes') # Sort by value, put the lowest first classes = sorted(classes, key=lambda k: k['value']) for the_class in classes: threshold = thresholds[classification['key']]['classes'][ the_class['key']] row = m.Row() row.add(m.Cell(the_class['name'])) row.add(m.Cell(threshold[0])) row.add(m.Cell(threshold[1])) table.add(row) else: value_maps = self.value_maps.get(self.exposures[i]['key']) if not value_maps or not classification: message.add(m.Paragraph(tr('No classifications set.'))) continue table = m.Table( style_class='table table-condensed table-striped') header = m.Row() header.add(m.Cell(tr('Class name'))) header.add(m.Cell(tr('Value'))) table.add(header) classes = classification.get('classes') # Sort by value, put the lowest first classes = sorted(classes, key=lambda k: k['value']) for the_class in classes: value_map = value_maps[classification['key']][ 'classes'].get(the_class['key'], []) row = m.Row() row.add(m.Cell(the_class['name'])) row.add(m.Cell(', '.join([str(v) for v in value_map]))) table.add(row) message.add(table) # status_text_edit = QTextBrowser(None) status_text_edit = QWebView(None) status_text_edit.setSizePolicy( QSizePolicy.Ignored, QSizePolicy.Ignored) status_text_edit.page().mainFrame().setScrollBarPolicy( Qt.Horizontal, Qt.ScrollBarAlwaysOff) html_string = html_header() + message.to_html() + html_footer() status_text_edit.setHtml(html_string) self.right_layout.addWidget(status_text_edit) def set_widgets(self): """Set widgets on the Multi classification step.""" self.clear() self.layer_mode = self.parent.step_kw_layermode.selected_layermode() self.layer_purpose = self.parent.step_kw_purpose.selected_purpose() self.set_current_state() # Set the step description self.set_wizard_step_description() # Set the left panel self.setup_left_panel() # Set the right panel, for the beginning show the viewer self.show_current_state() def clear(self): """Clear current state.""" self.exposures = [] self.exposure_labels = [] self.exposure_combo_boxes = [] self.exposure_edit_buttons = [] self.mode = CHOOSE_MODE self.layer_purpose = None self.layer_mode = None self.special_case_index = None self.value_maps = {} self.thresholds = {} # Temporary attributes self.threshold_classes = OrderedDict() self.active_exposure = None self.list_unique_values = None self.tree_mapping_widget = None clear_layout(self.left_layout) clear_layout(self.right_layout) def get_current_state(self): """Obtain current classification and value map / threshold.""" def clean_state(dictionary): """Clean dictionary from bad value. :param dictionary: Dictionary of value maps or thresholds. :type dictionary: dict :returns: Clean state. :rtype: dict """ clean_dictionary = { k: v for k, v in dictionary.items() if isinstance(v, dict)} return clean_dictionary if self.layer_mode == layer_mode_continuous: output = {'thresholds': clean_state(self.thresholds)} key = 'thresholds' else: output = {'value_maps': clean_state(self.value_maps)} key = 'value_maps' # Clean non existing hazard class key empty_exposure_classifications = [] for the_exposure, the_hazard_classifications in output[key].items(): for the_hazard_classification in the_hazard_classifications.\ keys(): invalid_classifications = [] if not definition(the_hazard_classification): invalid_classifications.append( the_hazard_classification) for invalid_classification in invalid_classifications: the_hazard_classifications.pop(invalid_classification) if not the_hazard_classifications: empty_exposure_classifications.append(the_exposure) for empty_exposure_classification in empty_exposure_classifications: output[key].pop(empty_exposure_classification) return output @staticmethod def get_classification(combo_box): """Helper to obtain the classification from a combo box. :param combo_box: A classification combo box. :type combo_box: QComboBox. :returns: Classification definitions. :rtype: dict """ return combo_box.itemData(combo_box.currentIndex(), Qt.UserRole) def setup_thresholds_panel(self, classification): """Setup threshold panel in the right panel. :param classification: Classification definition. :type classification: dict """ # Set text in the label layer_purpose = self.parent.step_kw_purpose.selected_purpose() layer_subcategory = self.parent.step_kw_subcategory.\ selected_subcategory() if is_raster_layer(self.parent.layer): statistics = self.parent.layer.dataProvider().bandStatistics( 1, QgsRasterBandStats.All, self.parent.layer.extent(), 0) description_text = continuous_raster_question % ( layer_purpose['name'], layer_subcategory['name'], classification['name'], statistics.minimumValue, statistics.maximumValue) else: field_name = self.parent.step_kw_field.selected_fields() field_index = self.parent.layer.fieldNameIndex(field_name) min_value_layer = self.parent.layer.minimumValue(field_index) max_value_layer = self.parent.layer.maximumValue(field_index) description_text = continuous_vector_question % ( layer_purpose['name'], layer_subcategory['name'], field_name, classification['name'], min_value_layer, max_value_layer) # Set description description_label = QLabel(description_text) description_label.setWordWrap(True) self.right_layout.addWidget(description_label) if self.thresholds: thresholds = self.thresholds else: thresholds = self.parent.get_existing_keyword('thresholds') selected_unit = self.parent.step_kw_unit.selected_unit()['key'] self.threshold_classes = OrderedDict() classes = classification.get('classes') # Sort by value, put the lowest first classes = sorted(classes, key=lambda the_key: the_key['value']) grid_layout_thresholds = QGridLayout() for i, the_class in enumerate(classes): class_layout = QHBoxLayout() # Class label class_label = QLabel(the_class['name']) # Min label min_label = QLabel(tr('Min >')) # Min value as double spin min_value_input = QDoubleSpinBox() # TODO(IS) We can set the min and max depends on the unit, later min_value_input.setMinimum(0) min_value_input.setMaximum(999999) if thresholds.get(self.active_exposure['key']): exposure_thresholds = thresholds.get( self.active_exposure['key']) if exposure_thresholds.get(classification['key']): exposure_thresholds_classifications = exposure_thresholds\ .get(classification['key']) min_value_input.setValue( exposure_thresholds_classifications['classes'][ the_class['key']][0]) else: default_min = the_class['numeric_default_min'] if isinstance(default_min, dict): default_min = the_class[ 'numeric_default_min'][selected_unit] min_value_input.setValue(default_min) else: default_min = the_class['numeric_default_min'] if isinstance(default_min, dict): default_min = the_class[ 'numeric_default_min'][selected_unit] min_value_input.setValue(default_min) min_value_input.setSingleStep(0.1) # Max label max_label = QLabel(tr('Max <=')) # Max value as double spin max_value_input = QDoubleSpinBox() # TODO(IS) We can set the min and max depends on the unit, later max_value_input.setMinimum(0) max_value_input.setMaximum(999999) if thresholds.get(self.active_exposure['key']): exposure_thresholds = thresholds.get( self.active_exposure['key']) if exposure_thresholds.get(classification['key']): exposure_thresholds_classifications = exposure_thresholds \ .get(classification['key']) max_value_input.setValue( exposure_thresholds_classifications['classes'][ the_class['key']][1]) else: default_max = the_class['numeric_default_max'] if isinstance(default_max, dict): default_max = the_class[ 'numeric_default_max'][selected_unit] max_value_input.setValue(default_max) else: default_max = the_class['numeric_default_max'] if isinstance(default_max, dict): default_max = the_class[ 'numeric_default_max'][selected_unit] max_value_input.setValue(default_max) max_value_input.setSingleStep(0.1) # Add to class_layout class_layout.addWidget(min_label) class_layout.addWidget(min_value_input) class_layout.addWidget(max_label) class_layout.addWidget(max_value_input) class_layout.setStretch(0, 1) class_layout.setStretch(1, 2) class_layout.setStretch(2, 1) class_layout.setStretch(3, 2) # Add to grid_layout grid_layout_thresholds.addWidget(class_label, i, 0) grid_layout_thresholds.addLayout(class_layout, i, 1) self.threshold_classes[the_class['key']] = [ min_value_input, max_value_input] grid_layout_thresholds.setColumnStretch(0, 1) grid_layout_thresholds.setColumnStretch(0, 2) def min_max_changed(double_spin_index, mode): """Slot when min or max value change. :param double_spin_index: The index of the double spin. :type double_spin_index: int :param mode: The flag to indicate the min or max value. :type mode: int """ if mode == MAX_VALUE_MODE: current_max_value = self.threshold_classes.values()[ double_spin_index][1] target_min_value = self.threshold_classes.values()[ double_spin_index + 1][0] if current_max_value.value() != target_min_value.value(): target_min_value.setValue(current_max_value.value()) elif mode == MIN_VALUE_MODE: current_min_value = self.threshold_classes.values()[ double_spin_index][0] target_max_value = self.threshold_classes.values()[ double_spin_index - 1][1] if current_min_value.value() != target_max_value.value(): target_max_value.setValue(current_min_value.value()) # Set behaviour for k, v in self.threshold_classes.items(): index = self.threshold_classes.keys().index(k) if index < len(self.threshold_classes) - 1: # Max value changed v[1].valueChanged.connect(partial( min_max_changed, double_spin_index=index, mode=MAX_VALUE_MODE)) if index > 0: # Min value v[0].valueChanged.connect(partial( min_max_changed, double_spin_index=index, mode=MIN_VALUE_MODE)) grid_layout_thresholds.setSpacing(0) self.right_layout.addLayout(grid_layout_thresholds) def add_buttons(self, classification): """Helper to setup 3 buttons. :param classification: The current classification. :type classification: dict """ # Note(IS): Until we have good behaviour, we will disable load # default and cancel button. # Add 3 buttons: Load default, Cancel, Save # load_default_button = QPushButton(tr('Load Default')) # cancel_button = QPushButton(tr('Cancel')) self.save_button = QPushButton(tr('Save')) # Action for buttons # cancel_button.clicked.connect(self.cancel_button_clicked) self.save_button.clicked.connect( partial(self.save_button_clicked, classification=classification)) button_layout = QHBoxLayout() # button_layout.addWidget(load_default_button) button_layout.addStretch(1) # button_layout.addWidget(cancel_button) button_layout.addWidget(self.save_button) button_layout.setStretch(0, 3) button_layout.setStretch(1, 1) # button_layout.setStretch(2, 1) # button_layout.setStretch(3, 1) self.right_layout.addLayout(button_layout) def setup_value_mapping_panels(self, classification): """Setup value mapping panel in the right panel. :param classification: Classification definition. :type classification: dict """ # Set text in the label layer_purpose = self.parent.step_kw_purpose.selected_purpose() layer_subcategory = self.parent.step_kw_subcategory. \ selected_subcategory() if is_raster_layer(self.parent.layer): description_text = classify_raster_question % ( layer_subcategory['name'], layer_purpose['name'], classification['name']) dataset = gdal.Open(self.parent.layer.source(), GA_ReadOnly) active_band = self.parent.step_kw_band_selector.selected_band() unique_values = numpy.unique(numpy.array( dataset.GetRasterBand(active_band).ReadAsArray())) field_type = 0 # Convert datatype to a json serializable type if numpy.issubdtype(unique_values.dtype, float): unique_values = [float(i) for i in unique_values] else: unique_values = [int(i) for i in unique_values] else: field = self.parent.step_kw_field.selected_fields() field_index = self.parent.layer.dataProvider().fields(). \ indexFromName(field) field_type = self.parent.layer.dataProvider(). \ fields()[field_index].type() description_text = classify_vector_question % ( layer_subcategory['name'], layer_purpose['name'], classification['name'], field.upper()) unique_values = self.parent.layer.uniqueValues(field_index) # Set description description_label = QLabel(description_text) description_label.setWordWrap(True) self.right_layout.addWidget(description_label) self.list_unique_values = QListWidget() self.list_unique_values.setDragDropMode(QAbstractItemView.DragDrop) self.list_unique_values.setDefaultDropAction(Qt.MoveAction) self.tree_mapping_widget = QTreeWidget() self.tree_mapping_widget.setDragDropMode(QAbstractItemView.DragDrop) self.tree_mapping_widget.setDefaultDropAction(Qt.MoveAction) self.tree_mapping_widget.header().hide() self.tree_mapping_widget.itemChanged.connect( self.update_dragged_item_flags) value_mapping_layout = QHBoxLayout() value_mapping_layout.addWidget(self.list_unique_values) value_mapping_layout.addWidget(self.tree_mapping_widget) self.right_layout.addLayout(value_mapping_layout) default_classes = classification['classes'] # Assign unique values to classes (according to default) unassigned_values = list() assigned_values = dict() for default_class in default_classes: assigned_values[default_class['key']] = list() for unique_value in unique_values: if unique_value is None or isinstance( unique_value, QPyNullVariant): # Don't classify features with NULL value continue # Capitalization of the value and removing '_' (raw OSM data). value_as_string = unicode(unique_value).upper().replace('_', ' ') assigned = False for default_class in default_classes: if 'string_defaults' in default_class: condition_1 = ( field_type > 9 and value_as_string in [ c.upper() for c in default_class['string_defaults']]) else: condition_1 = False condition_2 = ( field_type < 10 and 'numeric_default_min' in default_class and 'numeric_default_max' in default_class and ( default_class['numeric_default_min'] <= unique_value < default_class['numeric_default_max'])) if condition_1 or condition_2: assigned_values[default_class['key']] += [unique_value] assigned = True if not assigned: # add to unassigned values list otherwise unassigned_values += [unique_value] self.populate_classified_values( unassigned_values, assigned_values, default_classes, self.list_unique_values, self.tree_mapping_widget ) # Current value map for exposure and classification available_classifications = self.value_maps.get( self.active_exposure['key']) if not available_classifications: return # Get active one current_classification = available_classifications.get( classification['key']) if not current_classification: return current_value_map = current_classification.get('classes') if not current_value_map: return unassigned_values = list() assigned_values = dict() for default_class in default_classes: assigned_values[default_class['key']] = list() for unique_value in unique_values: if unique_value is None or isinstance( unique_value, QPyNullVariant): # Don't classify features with NULL value continue # check in value map assigned = False for key, value_list in current_value_map.items(): if unique_value in value_list and key in assigned_values: assigned_values[key] += [unique_value] assigned = True if not assigned: unassigned_values += [unique_value] self.populate_classified_values( unassigned_values, assigned_values, default_classes, self.list_unique_values, self.tree_mapping_widget ) # noinspection PyMethodMayBeStatic def update_dragged_item_flags(self, item): """Fix the drop flag after the item is dropped. Check if it looks like an item dragged from QListWidget to QTreeWidget and disable the drop flag. For some reasons the flag is set when dragging. :param item: Item which is dragged. :type item: QTreeWidgetItem .. note:: This is a slot executed when the item change. """ if int(item.flags() & Qt.ItemIsDropEnabled) \ and int(item.flags() & Qt.ItemIsDragEnabled): item.setFlags(item.flags() & ~Qt.ItemIsDropEnabled) @staticmethod def populate_classified_values( unassigned_values, assigned_values, default_classes, list_unique_values, tree_mapping_widget): """Populate lstUniqueValues and treeClasses.from the parameters. :param unassigned_values: List of values that haven't been assigned to a class. It will be put in list_unique_values. :type unassigned_values: list :param assigned_values: Dictionary with class as the key and list of value as the value of the dictionary. It will be put in tree_mapping_widget. :type assigned_values: dict :param default_classes: Default classes from unit. :type default_classes: list :param list_unique_values: List Widget for unique values :type list_unique_values: QListWidget :param tree_mapping_widget: Tree Widget for classifying. :type tree_mapping_widget: QTreeWidget """ # Populate the unique values list list_unique_values.clear() list_unique_values.setSelectionMode( QAbstractItemView.ExtendedSelection) for value in unassigned_values: value_as_string = value is not None and unicode(value) or 'NULL' list_item = QListWidgetItem(list_unique_values) list_item.setFlags( Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled) list_item.setData(Qt.UserRole, value) list_item.setText(value_as_string) list_unique_values.addItem(list_item) # Populate assigned values tree tree_mapping_widget.clear() bold_font = QFont() bold_font.setItalic(True) bold_font.setBold(True) bold_font.setWeight(75) tree_mapping_widget.invisibleRootItem().setFlags( Qt.ItemIsEnabled) for default_class in default_classes: # Create branch for class tree_branch = QTreeWidgetItem(tree_mapping_widget) tree_branch.setFlags( Qt.ItemIsDropEnabled | Qt.ItemIsEnabled) tree_branch.setExpanded(True) tree_branch.setFont(0, bold_font) if 'name' in default_class: default_class_name = default_class['name'] else: default_class_name = default_class['key'] tree_branch.setText(0, default_class_name) tree_branch.setData(0, Qt.UserRole, default_class['key']) if 'description' in default_class: tree_branch.setToolTip(0, default_class['description']) # Assign known values for value in assigned_values[default_class['key']]: string_value = value is not None and unicode(value) or 'NULL' tree_leaf = QTreeWidgetItem(tree_branch) tree_leaf.setFlags( Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled) tree_leaf.setData(0, Qt.UserRole, value) tree_leaf.setText(0, string_value) def cancel_button_clicked(self): """Action for cancel button clicked.""" # Change mode self.mode = CHOOSE_MODE # Enable all edit buttons and combo boxes for i in range(len(self.exposures)): if i == self.special_case_index: self.exposure_edit_buttons[i].setEnabled(False) self.exposure_combo_boxes[i].setEnabled(False) continue if self.get_classification(self.exposure_combo_boxes[i]): self.exposure_edit_buttons[i].setEnabled(True) else: self.exposure_edit_buttons[i].setEnabled(False) # self.exposure_edit_buttons[i].setText(tr('Edit')) self.exposure_combo_boxes[i].setEnabled(True) # Clear right panel clear_layout(self.right_layout) # Show current state self.show_current_state() # Unset active exposure self.active_exposure = None self.parent.pbnNext.setEnabled(self.is_ready_to_next_step()) def save_button_clicked(self, classification): """Action for save button clicked. :param classification: The classification that being edited. :type classification: dict """ # Save current edit if self.layer_mode == layer_mode_continuous: thresholds = self.get_threshold() classification_class = { 'classes': thresholds, 'active': True } if self.thresholds.get(self.active_exposure['key']): # Set other class to not active for current_classification in self.thresholds.get( self.active_exposure['key']).values(): current_classification['active'] = False else: self.thresholds[self.active_exposure['key']] = {} self.thresholds[self.active_exposure['key']][ classification['key']] = classification_class else: value_maps = self.get_value_map() classification_class = { 'classes': value_maps, 'active': True } if self.value_maps.get(self.active_exposure['key']): # Set other class to not active for current_classification in self.value_maps.get( self.active_exposure['key']).values(): current_classification['active'] = False else: self.value_maps[self.active_exposure['key']] = {} self.value_maps[self.active_exposure['key']][ classification['key']] = classification_class # Back to choose mode self.cancel_button_clicked() def get_threshold(self): """Return threshold based on current state.""" value_map = dict() for key, value in self.threshold_classes.items(): value_map[key] = [ value[0].value(), value[1].value(), ] return value_map def get_value_map(self): """Obtain the value-to-class mapping set by user. :returns: The complete mapping as a dict of lists. :rtype: dict """ value_map = {} tree_clone = self.tree_mapping_widget.invisibleRootItem().clone() for tree_branch in tree_clone.takeChildren(): value_list = [] for tree_leaf in tree_branch.takeChildren(): value_list += [tree_leaf.data(0, Qt.UserRole)] if value_list: value_map[tree_branch.data(0, Qt.UserRole)] = value_list return value_map def set_current_state(self): """"Helper to set the state of the step from current keywords.""" if not self.thresholds: self.thresholds = self.parent.get_existing_keyword('thresholds') if not self.value_maps: self.value_maps = self.parent.get_existing_keyword('value_maps') def classifications_combo_box_changed( self, index, exposure, exposure_combo_box, edit_button): """Action when classification combo box changed. :param index: The index of the combo box. :type index: int :param exposure: The exposure associated with the combo box. :type exposure: dict :param exposure_combo_box: Combo box for the classification. :type exposure_combo_box: QComboBox :param edit_button: The edit button associate with combo box. :type edit_button: QPushButton """ # Disable button if it's no classification edit_button.setEnabled(bool(index)) classification = self.get_classification(exposure_combo_box) self.activate_classification(exposure, classification) clear_layout(self.right_layout) self.show_current_state() self.parent.pbnNext.setEnabled(self.is_ready_to_next_step()) # Open edit panel directly edit_button.click() def activate_classification(self, exposure, classification=None): """Set active to True for classification for the exposure. If classification = None, all classification set active = False. :param exposure: Exposure definition. :type exposure: dict :param classification: Classification definition. :type classification: dict """ if self.layer_mode == layer_mode_continuous: selected_unit = self.parent.step_kw_unit.selected_unit()['key'] target = self.thresholds.get(exposure['key']) if target is None: self.thresholds[exposure['key']] = {} target = self.thresholds.get(exposure['key']) else: selected_unit = None target = self.value_maps.get(exposure['key']) if target is None: self.value_maps[exposure['key']] = {} target = self.value_maps.get(exposure['key']) if classification is not None: if classification['key'] not in target: if self.layer_mode == layer_mode_continuous: default_classes = default_classification_thresholds( classification, selected_unit) target[classification['key']] = { 'classes': default_classes, 'active': True } else: default_classes = default_classification_value_maps( classification) target[classification['key']] = { 'classes': default_classes, 'active': True } return for classification_key, value in target.items(): if classification is None: value['active'] = False continue if classification_key == classification['key']: value['active'] = True else: value['active'] = False @property def step_name(self): """Get the human friendly name for the wizard step. :returns: The name of the wizard step. :rtype: str """ return tr('Multi Classification Step') def help_content(self): """Return the content of help for this step wizard. We only needs to re-implement this method in each wizard step. :returns: A message object contains help. :rtype: m.Message """ message = m.Message() message.add(m.Paragraph(tr( 'In this wizard step: {step_name}, you will be able to set the ' 'classification that you will use per exposure type. You can also ' 'set the threshold or value map for each classification.' ).format(step_name=self.step_name))) return message
class GroupSelectParameterWidget(GenericParameterWidget): """Widget class for Group Select Parameter.""" def __init__(self, parameter, parent=None): """Constructor. :param parameter: A GroupSelectParameter object. :type parameter: GroupSelectParameter """ QWidget.__init__(self, parent) self._parameter = parameter # Store spin box self.spin_boxes = {} # Create elements # Label (name) self.label = QLabel(self._parameter.name) # Layouts self.main_layout = QVBoxLayout() self.input_layout = QVBoxLayout() # _inner_input_layout must be filled with widget in the child class self.inner_input_layout = QVBoxLayout() self.radio_button_layout = QGridLayout() # Create radio button group self.input_button_group = QButtonGroup() # List widget self.list_widget = QListWidget() self.list_widget.setSelectionMode(QAbstractItemView.ExtendedSelection) self.list_widget.setDragDropMode(QAbstractItemView.DragDrop) self.list_widget.setDefaultDropAction(Qt.MoveAction) self.list_widget.setEnabled(False) self.list_widget.setSizePolicy( QSizePolicy.Maximum, QSizePolicy.Expanding) for i, key in enumerate(self._parameter.options): value = self._parameter.options[key] radio_button = QRadioButton(value.get('label')) self.radio_button_layout.addWidget(radio_button, i, 0) if value.get('type') == SINGLE_DYNAMIC: double_spin_box = QDoubleSpinBox() self.radio_button_layout.addWidget(double_spin_box, i, 1) double_spin_box.setValue(value.get('value', 0)) double_spin_box.setMinimum( value.get('constraint', {}).get('min', 0)) double_spin_box.setMaximum(value.get( 'constraint', {}).get('max', 1)) double_spin_box.setSingleStep( value.get('constraint', {}).get('step', 0.01)) step = double_spin_box.singleStep() if step > 1: precision = 0 else: precision = len(str(step).split('.')[1]) if precision > 3: precision = 3 double_spin_box.setDecimals(precision) self.spin_boxes[key] = double_spin_box # Enable spin box depends on the selected option if self._parameter.selected == key: double_spin_box.setEnabled(True) else: double_spin_box.setEnabled(False) elif value.get('type') == STATIC: static_value = value.get('value', 0) if static_value is not None: self.radio_button_layout.addWidget( QLabel(str(static_value)), i, 1) elif value.get('type') == MULTIPLE_DYNAMIC: selected_fields = value.get('value', []) if self._parameter.selected == key: self.list_widget.setEnabled(True) else: self.list_widget.setEnabled(False) self.input_button_group.addButton(radio_button, i) if self._parameter.selected == key: radio_button.setChecked(True) # Help text self.help_label = QLabel(self._parameter.help_text) self.help_label.setSizePolicy( QSizePolicy.Maximum, QSizePolicy.Expanding) self.help_label.setWordWrap(True) self.help_label.setAlignment(Qt.AlignTop) self.inner_input_layout.addLayout(self.radio_button_layout) self.inner_input_layout.addWidget(self.list_widget) # Put elements into layouts self.input_layout.addWidget(self.label) self.input_layout.addLayout(self.inner_input_layout) self.help_layout = QVBoxLayout() self.help_layout.addWidget(self.help_label) self.main_layout.addLayout(self.input_layout) self.main_layout.addLayout(self.help_layout) self.setLayout(self.main_layout) self.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Expanding) # Update list widget self.update_list_widget() # Connect signal self.input_button_group.buttonClicked.connect( self.radio_buttons_clicked) def get_parameter(self): """Obtain list parameter object from the current widget state. :returns: A DefaultValueParameter from the current state of widget :rtype: DefaultValueParameter """ # Set value for each key for key, value in self._parameter.options.items(): if value.get('type') == STATIC: continue elif value.get('type') == SINGLE_DYNAMIC: new_value = self.spin_boxes.get(key).value() self._parameter.set_value_for_key(key, new_value) elif value.get('type') == MULTIPLE_DYNAMIC: # Need to iterate through all items items = [] for index in xrange(self.list_widget.count()): items.append(self.list_widget.item(index)) new_value = [i.text() for i in items] self._parameter.set_value_for_key(key, new_value) # Get selected radio button radio_button_checked_id = self.input_button_group.checkedId() # No radio button checked, then default value = None if radio_button_checked_id == -1: self._parameter.selected = None else: self._parameter.selected = self._parameter.options.keys()[ radio_button_checked_id] return self._parameter def update_list_widget(self): """Update list widget when radio button is clicked.""" # Get selected radio button radio_button_checked_id = self.input_button_group.checkedId() # No radio button checked, then default value = None if radio_button_checked_id > -1: selected_dict = self._parameter.options.values()[ radio_button_checked_id] if selected_dict.get('type') == MULTIPLE_DYNAMIC: for field in selected_dict.get('value'): # Update list widget field_item = QListWidgetItem(self.list_widget) field_item.setFlags( Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled) field_item.setData(Qt.UserRole, field) field_item.setText(field) self.list_widget.addItem(field_item) def radio_buttons_clicked(self): """Handler when selected radio button changed.""" # Disable all spin boxes for spin_box in self.spin_boxes.values(): spin_box.setEnabled(False) # Disable list widget self.list_widget.setEnabled(False) # Get selected radio button radio_button_checked_id = self.input_button_group.checkedId() if radio_button_checked_id > -1: selected_value = self._parameter.options.values()[ radio_button_checked_id] if selected_value.get('type') == MULTIPLE_DYNAMIC: # Enable list widget self.list_widget.setEnabled(True) elif selected_value.get('type') == SINGLE_DYNAMIC: selected_key = self._parameter.options.keys()[ radio_button_checked_id] self.spin_boxes[selected_key].setEnabled(True) def select_radio_button(self, key): """Helper to select a radio button with key. :param key: The key of the radio button. :type key: str """ key_index = self._parameter.options.keys().index(key) radio_button = self.input_button_group.button(key_index) radio_button.click()
class FieldMappingTab(QWidget, object): """Widget class for field mapping.""" def __init__(self, field_group=None, parent=None, iface=None): """Constructor.""" # Init from parent class QWidget.__init__(self, parent) # Attributes self.layer = None self.metadata = {} self.parent = parent self.iface = iface self.field_group = field_group self.setting = QSettings() # TODO(IS): Make dynamic # Main container self.main_layout = QVBoxLayout() # Inner layout self.header_layout = QHBoxLayout() self.content_layout = QHBoxLayout() self.footer_layout = QHBoxLayout() # Header self.header_label = QLabel() self.header_label.setWordWrap(True) # Content self.field_layout = QVBoxLayout() self.parameter_layout = QHBoxLayout() self.field_description = QLabel(tr('List of fields')) self.field_list = QListWidget() self.field_list.setSelectionMode(QAbstractItemView.ExtendedSelection) self.field_list.setDragDropMode(QAbstractItemView.DragDrop) self.field_list.setDefaultDropAction(Qt.MoveAction) self.field_list.setSizePolicy( QSizePolicy.Maximum, QSizePolicy.Expanding) # noinspection PyUnresolvedReferences self.field_list.itemSelectionChanged.connect(self.update_footer) # Footer self.footer_label = QLabel() # Parameters self.extra_parameters = [ (GroupSelectParameter, GroupSelectParameterWidget) ] self.parameters = [] self.parameter_container = None # Adding to layout self.header_layout.addWidget(self.header_label) self.field_layout.addWidget(self.field_description) self.field_layout.addWidget(self.field_list) self.field_layout.setSizeConstraint(QLayout.SetMaximumSize) self.content_layout.addLayout(self.field_layout) self.content_layout.addLayout(self.parameter_layout) self.footer_layout.addWidget(self.footer_label) self.main_layout.addLayout(self.header_layout) self.main_layout.addLayout(self.content_layout) self.main_layout.addLayout(self.footer_layout) self.setLayout(self.main_layout) def set_layer(self, layer, keywords=None): """Set layer and update UI accordingly. :param layer: A vector layer that has been already patched with metadata. :type layer: QgsVectorLayer :param keywords: Custom keyword for the layer. :type keywords: dict, None """ self.layer = layer if keywords is not None: self.metadata = keywords else: # Check if it has keywords if not hasattr(layer, 'keywords'): message = 'Layer {layer_name} does not have keywords.'.format( layer_name=layer.name()) raise KeywordNotFoundError(message) self.metadata = layer.keywords self.populate_parameter() def populate_field_list(self, excluded_fields=None): """Helper to add field of the layer to the list. :param excluded_fields: List of field that want to be excluded. :type excluded_fields: list """ # Populate fields list if excluded_fields is None: excluded_fields = [] self.field_list.clear() for field in self.layer.dataProvider().fields(): # Skip if it's excluded if field.name() in excluded_fields: continue # Skip if it's not number (float, int, etc) if field.type() not in qvariant_numbers: continue field_item = QListWidgetItem(self.field_list) field_item.setFlags( Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled) field_item.setData(Qt.UserRole, field.name()) field_item.setText(field.name()) self.field_list.addItem(field_item) def populate_parameter(self): """Helper to setup the parameter widget.""" used_fields = [] self.parameters = [] for field in self.field_group.get('fields', []): selected_option = DO_NOT_USE options = OrderedDict([ (DO_NOT_USE, { 'label': tr('Do not use'), 'value': None, 'type': STATIC, 'constraint': {} }), ]) # Example: count if field['absolute']: # Used in field options field_label = tr('Count fields') else: # Example: ratio # Used in field options field_label = tr('Ratio fields') global_default_value = get_inasafe_default_value_qsetting( self.setting, GLOBAL, field['key']) options[GLOBAL_DEFAULT] = { 'label': tr('Global default'), 'value': global_default_value, 'type': STATIC, 'constraint': {} } default_custom_value = get_inasafe_default_value_qsetting( self.setting, RECENT, field['key']) custom_value = self.metadata.get( 'inasafe_default_values', {}).get( field['key'], default_custom_value) if field['key'] in self.metadata.get( 'inasafe_default_values', {}): if custom_value == global_default_value: selected_option = GLOBAL_DEFAULT else: selected_option = CUSTOM_VALUE min_value = field['default_value'].get('min_value', 0) max_value = field['default_value'].get('max_value', 100) default_step = (max_value - min_value) / 100.0 step = field['default_value'].get('increment', default_step) options[CUSTOM_VALUE] = { 'label': tr('Custom'), 'value': custom_value, 'type': SINGLE_DYNAMIC, 'constraint': { 'min': min_value, 'max': max_value, 'step': step } } custom_fields = self.metadata.get('inasafe_fields', {}).get( field['key'], []) if field['key'] in self.metadata.get('inasafe_fields', {}): selected_option = FIELDS if isinstance(custom_fields, basestring): custom_fields = [custom_fields] options[FIELDS] = { 'label': field_label, 'value': custom_fields, 'type': MULTIPLE_DYNAMIC, 'constraint': {} } used_fields.extend(custom_fields) parameter = GroupSelectParameter() parameter.guid = field['key'] parameter.name = field['name'] parameter.options = options parameter.selected = selected_option parameter.help_text = field['help_text'] parameter.description = field['description'] self.parameters.append(parameter) self.parameter_container = ParameterContainer( parameters=self.parameters, extra_parameters=self.extra_parameters, vertical=False ) self.parameter_container.setup_ui() constraints = self.field_group.get('constraints', {}) for key, value in constraints.items(): self.parameter_container.add_validator( validators[key], kwargs=value['kwargs'], validation_message=value['message']) self.parameter_layout.addWidget(self.parameter_container) # Set move or copy if self.field_group.get('exclusive', False): # If exclusive, do not add used field. self.populate_field_list(excluded_fields=used_fields) # Use move action since it's exclusive self.field_list.setDefaultDropAction(Qt.MoveAction) # Just make sure that the signal is disconnected try: # noinspection PyUnresolvedReferences self.field_list.itemChanged.disconnect(self.drop_remove) except TypeError: pass # Set header header_text = self.field_group['description'] header_text += '\n\n' + tr( 'You can only map one field to one concept.') else: # If not exclusive, add all field. self.populate_field_list() # Use copy action since it's not exclusive self.field_list.setDefaultDropAction(Qt.CopyAction) # noinspection PyUnresolvedReferences self.field_list.itemChanged.connect( partial(self.drop_remove, field_list=self.field_list)) self.connect_drop_remove_parameter() # Set header header_text = self.field_group['description'] header_text += '\n\n' + tr( 'You can map one field to more than one concepts.') self.header_label.setText(header_text) def get_parameter_value(self): """Get parameter of the tab. :returns: Dictionary of parameters by type in this format: {'fields': {}, 'values': {}}. :rtype: dict """ try: parameters = self.parameter_container.get_parameters(True) except InvalidValidationException as e: raise OriginalValidationException(e) field_parameters = {} value_parameters = {} for parameter in parameters: if parameter.selected_option_type() in [SINGLE_DYNAMIC, STATIC]: value_parameters[parameter.guid] = parameter.value elif parameter.selected_option_type() == MULTIPLE_DYNAMIC: field_parameters[parameter.guid] = parameter.value return { 'fields': field_parameters, 'values': value_parameters } def update_footer(self): """Update footer when the field list change.""" field_item = self.field_list.currentItem() if not field_item: self.footer_label.setText('') return field_name = field_item.data(Qt.UserRole) field = self.layer.fields().field(field_name) index = self.layer.fieldNameIndex(field_name) unique_values = self.layer.uniqueValues(index) pretty_unique_values = ', '.join([str(v) for v in unique_values[:10]]) footer_text = tr('Field type: {0}\n').format(field.typeName()) footer_text += tr('Unique values: {0}').format(pretty_unique_values) self.footer_label.setText(footer_text) def connect_drop_remove_parameter(self): parameter_widgets = self.parameter_container.get_parameter_widgets() for parameter_widget in parameter_widgets: field_list = parameter_widget.widget().list_widget field_list.itemChanged.connect( partial(self.drop_remove, field_list=field_list)) @staticmethod def drop_remove(*args, **kwargs): """Action when we need to remove dropped item. :param args: Position arguments. :type args: list :param kwargs: Keywords arguments. :type kwargs: dict """ dropped_item = args[0] field_list = kwargs['field_list'] num_duplicate = 0 for i in range(field_list.count()): if dropped_item.text() == field_list.item(i).text(): num_duplicate += 1 if num_duplicate > 1: # Notes(IS): For some reason, removeItemWidget is not working. field_list.takeItem(field_list.row(dropped_item))