class EntityEditorDialog(MapperMixin): """ Dialog for editing entity attributes. """ addedModel = pyqtSignal(object) def __init__(self, entity, model=None, parent=None, manage_documents=True, collect_model=False, parent_entity=None, exclude_columns=None, plugin=None, allow_str_creation=True): """ Class constructor. :param entity: Entity object corresponding to a table object. :type entity: Entity :param model: Data object for loading data into the form widgets. If the model is set, then the editor dialog is assumed to be in edit mode. :type model: object :param parent: Parent widget that the form belongs to. :type parent: QWidget :param manage_documents: True if the dialog should provide controls for managing supporting documents. Only applicable if the entity allows for supporting documents to be attached. :type manage_documents: bool :param collect_model: If set to True only returns the filled form model without saving it to the database. :type collect_model: Boolean :param parent_entity: The parent entity of the editor :type parent_entity: Object :param exclude_columns: List of columns to be excluded if in a list. :type exclude_columns: List :return: If collect_model, returns SQLAlchemy Model """ super().__init__(parent=parent, model=model, entity=entity) QgsGui.enableAutoGeometryRestore(self) self.collection_suffix = self.tr('Collection') # Set minimum width self.setMinimumWidth(450) self.plugin = plugin # Flag for mandatory columns self.has_mandatory = False self.reload_form = False self._entity = entity self.edit_model = model self.column_widgets = OrderedDict() self.columns = {} self._parent = parent self.exclude_columns = exclude_columns or [] self.entity_tab_widget = None self._disable_collections = False self.filter_val = None self.parent_entity = parent_entity self.child_models = OrderedDict() self.entity_scroll_area = None self.entity_editor_widgets = OrderedDict() self.details_tree_view = None # Set notification layout bar self.vlNotification = QVBoxLayout() self.vlNotification.setObjectName('vlNotification') self._notifBar = NotificationBar(self.vlNotification) self.do_not_check_dirty = False # Set manage documents only if the entity supports documents if self._entity.supports_documents: self._manage_documents = manage_documents else: self._manage_documents = False # Setup entity model self._ent_document_model = None if self._entity.supports_documents: self.ent_model, self._ent_document_model = entity_model( self._entity, with_supporting_document=True) else: self.ent_model = entity_model(self._entity) if model is not None: self.ent_model = model MapperMixin.__init__(self, self.ent_model, entity) self.collect_model = collect_model self.register_column_widgets() try: if isinstance(parent._parent, EntityEditorDialog): # hide collections form child editor self._disable_collections = True except AttributeError: self._parent._parent = None # Set title editor_trans = self.tr('Editor') if self._entity.label is not None: if self._entity.label != '': title_str = self._entity.label else: title_str = format_name(self._entity.short_name) else: title_str = format_name(self._entity.short_name) self.title = '{0} {1}'.format(title_str, editor_trans) self.setWindowTitle(self.title) # determine whether the entity is part of the STR relationship curr_profile = current_profile() self.participates_in_str, self.is_party_unit = curr_profile.social_tenure.entity_participates_in_str( self.entity) self._init_gui(show_str_tab=allow_str_creation and self.participates_in_str, is_party_unit=self.is_party_unit) self.adjustSize() self._get_entity_editor_widgets() if isinstance(parent._parent, EntityEditorDialog): self.parent_entity = parent.parent_entity self.set_parent_values() # make the size smaller to differentiate from parent and as it # only has few tabs. self.adjustSize() self.attribute_mappers = self._attr_mapper_collection # Exception title for editor extension exceptions self._ext_exc_msg = self.tr( 'An error has occured while executing Python code in the editor ' 'extension:') # Register custom editor extension if specified self._editor_ext = entity_dlg_extension(self) if self._editor_ext is not None: self._editor_ext.post_init() # Initialize CascadingFieldContext objects self._editor_ext.connect_cf_contexts() def _init_gui(self, show_str_tab: bool, is_party_unit: bool): # Setup base elements self.gridLayout = QGridLayout(self) self.gridLayout.setObjectName('glMain') self.gridLayout.addLayout(self.vlNotification, 0, 0, 1, 1) # set widgets values column_widget_area = self._setup_columns_content_area() self.gridLayout.addWidget(column_widget_area, 1, 0, 1, 1) if show_str_tab: self._setup_str_tab(is_party_unit=is_party_unit) # Add notification for mandatory columns if applicable next_row = 2 if self.has_mandatory: self.required_fields_lbl = QLabel(self) msg = self.tr('Please fill out all required (*) fields.') msg = self._highlight_asterisk(msg) self.required_fields_lbl.setText(msg) self.gridLayout.addWidget(self.required_fields_lbl, next_row, 0, 1, 2) # Bump up row reference next_row += 1 self.buttonBox = QDialogButtonBox(self) self.buttonBox.setObjectName('buttonBox') self.gridLayout.addWidget(self.buttonBox, next_row, 0, 1, 1) self.buttonBox.setOrientation(Qt.Horizontal) self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel | QDialogButtonBox.Save) if self.edit_model is None: if not self.collect_model: self.save_new_button = QPushButton( QApplication.translate('EntityEditorDialog', 'Save and New')) self.buttonBox.addButton(self.save_new_button, QDialogButtonBox.ActionRole) # edit model, collect model # adding new record for child # Saving in parent editor if not isinstance(self._parent._parent, EntityEditorDialog): # adding a new record if self.edit_model is None: # saving when digitizing. if self.collect_model: self.buttonBox.accepted.connect(self.on_model_added) # saving parent editor else: self.buttonBox.accepted.connect(self.save_parent_editor) self.save_new_button.clicked.connect(self.save_and_new) # updating existing record else: if not self.collect_model: # updating existing record of the parent editor self.buttonBox.accepted.connect(self.save_parent_editor) else: self.buttonBox.accepted.connect(self.on_model_added) # Saving in child editor else: # save and new record if self.edit_model is None: self.buttonBox.accepted.connect(self.on_child_saved) self.save_new_button.clicked.connect( lambda: self.on_child_saved(True)) else: # When updating an existing child editor save to the db self.buttonBox.accepted.connect(self.on_child_saved) # self.buttonBox.accepted.connect(self.submit) self.buttonBox.rejected.connect(self.cancel) @property def notification_bar(self): """ :return: Returns the dialog's notification bar. :rtype: NotificationBar """ return self._notifBar def save_parent_editor(self): """ Saves the parent editor and its children. """ self.submit() self.save_children() def set_parent_values(self): """ Sets the parent display column for the child. """ if self.parent_entity is None: return for col in self._entity.columns.values(): if col.TYPE_INFO == 'FOREIGN_KEY': parent_entity = col.parent if parent_entity == self.parent_entity: self.parent_widgets_value_setter(self._parent._parent, col) def parent_widgets_value_setter(self, parent, col): """ Finds and sets the value from parent widget and set it to the column widget of a child using the child column. :param parent: The parent widget :type parent: QWidget :param col: The child column object :type col: Object """ for parent_col_name, parent_widget in parent.column_widgets.items(): parent_col = parent.columns[parent_col_name] if parent_col.name == col.name: self.single_parent_value_setter(col, parent_widget) break if parent_col.name in col.entity_relation.display_cols: self.single_parent_value_setter(col, parent_widget) break def single_parent_value_setter(self, col, parent_widget): """ Gets value from parent widget and set it to the column widget of a child using the child column. :param parent: The parent widget :type parent: QWidget :param col: The child column object :type col: Object """ local_widget = self.column_widgets[col.name] local_widget.show_clear_button() self.filter_val = parent_widget.text() local_widget.setText(self.filter_val) def save_and_new(self): """ A slot raised when Save and New button is click. It saves the form without showing a success message. Then it sets reload_form property to True so that entity_browser can re-load the form. """ from stdm.ui.entity_browser import (EntityBrowserWithEditor) self.submit(False, True) self.save_children() if self.is_valid: self.addedModel.emit(self.model()) self.setModel(self.ent_model()) self.clear() self.child_models.clear() for index in range(0, self.entity_tab_widget.count() - 1): if isinstance(self.entity_tab_widget.widget(index), EntityBrowserWithEditor): child_browser = self.entity_tab_widget.widget(index) child_browser.remove_rows() def on_model_added(self): """ A slot raised when a form is submitted with collect model set to True. There will be no success message and the form does not close. """ self.submit(True) self.addedModel.emit(self.model()) def closeEvent(self, event): """ Raised when a request to close the window is received. Check the dirty state of input controls and prompt user to save if dirty. """ if self.do_not_check_dirty: event.accept() return isDirty, userResponse = self.checkDirty() if isDirty: if userResponse == QMessageBox.Yes: # We need to ignore the event so that validation and # saving operations can be executed event.ignore() self.submit() elif userResponse == QMessageBox.No: event.accept() elif userResponse == QMessageBox.Cancel: event.ignore() else: event.accept() def on_child_saved(self, save_and_new=False): """ A slot raised when the save or save and new button is clicked. It sets the child_models dictionary of the parent when saved. :param save_and_new: A boolean indicating the save and new button is clicked to trigger the slot. :type save_and_new: Boolean """ if self.parent_entity is None: return self.submit(True) insert_pos = self._parent.tbEntity.model().rowCount() + 1 # Save to parent editor so that it is persistent. self._parent._parent.child_models[insert_pos, self._entity.name] = \ (self._entity, self.model()) self.addedModel.emit(self.model()) if not save_and_new: self.accept() else: if self.is_valid: # self.addedModel.emit(self.model()) self.setModel(self.ent_model()) self.clear() self.set_parent_values() def save_children(self): """ Saves children models into the database by assigning the the id of the parent for foreign key column. """ if len(self.child_models) < 1: return children_obj = [] for row_entity, row_value in self.child_models.items(): entity, model = row_value ent_model = entity_model(entity) entity_obj = ent_model() for col in entity.columns.values(): if col.TYPE_INFO == 'FOREIGN_KEY': if col.parent.name == self._entity.name: setattr(model, col.name, self.model().id) children_obj.append(model) entity_obj.saveMany(children_obj) def register_column_widgets(self): """ Registers the column widgets. """ # Append column labels and widgets table_name = self._entity.name columns = table_column_names(table_name) self.scroll_widget_contents = QWidget() self.scroll_widget_contents.setObjectName('scrollAreaWidgetContents') for c in self._entity.columns.values(): if c.name in self.exclude_columns: continue if c.name not in columns and not isinstance(c, VirtualColumn): continue # Get widget factory column_widget = ColumnWidgetRegistry.create( c, self.scroll_widget_contents, host=self) self.columns[c.name] = c self.column_widgets[c.name] = column_widget def _setup_columns_content_area(self): # Only use this if entity supports documents # self.entity_tab_widget = None self.doc_widget = None self.entity_scroll_area = QScrollArea(self) self.entity_scroll_area.setFrameShape(QFrame.NoFrame) self.entity_scroll_area.setWidgetResizable(True) self.entity_scroll_area.setObjectName('scrollArea') # Grid layout for controls self.gl = QGridLayout(self.scroll_widget_contents) self.gl.setObjectName('gl_widget_contents') # Append column labels and widgets table_name = self._entity.name columns = table_column_names(table_name) # Iterate entity column and assert if they exist row_id = 0 for column_name, column_widget in self.column_widgets.items(): c = self.columns[column_name] if c.name in self.exclude_columns: continue if c.name not in columns and not isinstance(c, VirtualColumn): continue if column_widget is not None: header = c.ui_display() self.c_label = QLabel(self.scroll_widget_contents) # Format label text if it is a mandatory field if c.mandatory: header = '{0} *'.format(c.ui_display()) # Highlight asterisk header = self._highlight_asterisk(header) self.c_label.setText(header) self.gl.addWidget(self.c_label, row_id, 0, 1, 1) self.column_widget = column_widget self.gl.addWidget(self.column_widget, row_id, 1, 1, 1) # Add user tip if specified for the column configuration if c.user_tip: self.tip_lbl = UserTipLabel(user_tip=c.user_tip) self.gl.addWidget(self.tip_lbl, row_id, 2, 1, 1) if c.mandatory and not self.has_mandatory: self.has_mandatory = True col_name = c.name # Replace name accordingly based on column type if isinstance(c, MultipleSelectColumn): col_name = c.model_attribute_name # Add widget to MapperMixin collection self.addMapping(col_name, self.column_widget, c.mandatory, pseudoname=c.ui_display()) # Bump up row_id row_id += 1 self.entity_scroll_area.setWidget(self.scroll_widget_contents) if self.entity_tab_widget is None: self.entity_tab_widget = QTabWidget(self) # Check if there are children and add foreign key browsers # Add primary tab if necessary self._add_primary_attr_widget() if not self._disable_collections: ch_entities = self.children_entities() for col, ch in ch_entities: if hasattr(col.entity_relation, 'show_in_parent'): if col.entity_relation.show_in_parent != '0': self._add_fk_browser(ch, col) else: self._add_fk_browser(ch, col) # Add tab widget if entity supports documents if self._entity.supports_documents: self.doc_widget = SupportingDocumentsWidget( self._entity.supporting_doc, self._ent_document_model, self) # Map the source document manager object self.addMapping('documents', self.doc_widget.source_document_manager) # # # Add attribute tab # self._add_primary_attr_widget() # Add supporting documents tab self.entity_tab_widget.addTab(self.doc_widget, self.tr('Supporting Documents')) # Return the correct widget if self.entity_tab_widget is not None: return self.entity_tab_widget return self.entity_scroll_area def _add_primary_attr_widget(self): # Check if the primary entity # exists and add if it does not pr_txt = self.tr('Primary') if self.entity_tab_widget is not None: tab_txt = self.entity_tab_widget.tabText(0) if not tab_txt == pr_txt: self.entity_tab_widget.addTab(self.entity_scroll_area, pr_txt) def _setup_str_tab(self, is_party_unit: bool): """ Creates the STR relationship tab """ from stdm.ui.feature_details import DetailsTreeView layout = QVBoxLayout() hl = QHBoxLayout() add_btn = QToolButton(self) add_btn.setText(self.tr('Create STR')) add_btn.setIcon(GuiUtils.get_icon('add.png')) hl.addWidget(add_btn) add_btn.clicked.connect(self._create_str) edit_btn = QToolButton(self) edit_btn.setText(self.tr('Edit')) edit_btn.setIcon(GuiUtils.get_icon('edit.png')) edit_btn.setDisabled(True) hl.addWidget(edit_btn) view_document_btn = QToolButton(self) view_document_btn.setText(self.tr('View Supporting Documents')) view_document_btn.setIcon(GuiUtils.get_icon('document.png')) view_document_btn.setDisabled(True) hl.addWidget(view_document_btn) hl.addStretch() layout.addLayout(hl) self.details_tree_view = DetailsTreeView( parent=self, plugin=self.plugin, edit_button=edit_btn, view_document_button=view_document_btn) self.details_tree_view.activate_feature_details( True, follow_layer_selection=False) self.details_tree_view.model.clear() if is_party_unit: self.details_tree_view.search_party(self.entity, [self.ent_model.id]) else: self.details_tree_view.search_spatial_unit(self.entity, [self.ent_model.id]) layout.addWidget(self.details_tree_view) w = QWidget() w.setLayout(layout) self.entity_tab_widget.addTab(w, self.tr('STR')) def _create_str(self): """ Opens the dialog to create a new STR """ from stdm.ui.social_tenure.str_editor import STREditor add_str_window = STREditor() #if is_party_unit: # add_str.set_party_data(self.entity_model_obj) if add_str_window.exec_(): # STR created - refresh STR view if self.is_party_unit: self.details_tree_view.search_party(self.entity, [self.ent_model.id]) else: self.details_tree_view.search_spatial_unit( self.entity, [self.ent_model.id]) def _add_fk_browser(self, child_entity, column): # Create and add foreign key # browser to the collection from stdm.ui.entity_browser import (ContentGroupEntityBrowser) attr = '{0}_collection'.format(child_entity.name) # Return if the attribute does not exist if not hasattr(self._model, attr): return table_content = TableContentGroup(User.CURRENT_USER.UserName, child_entity.short_name) if self.edit_model is not None: parent_id = self.edit_model.id else: parent_id = 0 entity_browser = ContentGroupEntityBrowser(child_entity, table_content, rec_id=parent_id, parent=self, plugin=self.plugin, load_recs=False) # entity_browser = EntityBrowserWithEditor( # child_entity, # self, # MANAGE, # False, # plugin=self.plugin # ) entity_browser.buttonBox.setVisible(False) entity_browser.record_filter = [] if len(child_entity.label) > 2: column_label = child_entity.label else: # Split and join to filter out entity name prefix # e.g. 'lo_parcel' to 'parcel' column_label = format_name(" ".join( child_entity.name.split("_", 1)[1:])) self.set_filter(child_entity, entity_browser) self.entity_tab_widget.addTab(entity_browser, '{0}'.format(column_label)) def set_filter(self, entity, browser): col = self.filter_col(entity) child_model = entity_model(entity) child_model_obj = child_model() col_obj = getattr(child_model, col.name) browser.filtered_records = [] if self.model() is not None: if self.model().id is not None: browser.filtered_records = child_model_obj.queryObject( ).filter(col_obj == self.model().id).all() if self.edit_model is not None: browser.filtered_records = child_model_obj.queryObject().filter( col_obj == self.edit_model.id).all() def filter_col(self, child_entity): for col in child_entity.columns.values(): if col.TYPE_INFO == 'FOREIGN_KEY': parent_entity = col.parent if parent_entity == self._entity: return col def children_entities(self): """ :return: Returns a list of children entities (by name) that refer to the main entity as the parent. :rtype: OrderedDict """ child_columns = [] for ch in self._entity.children(): if ch.TYPE_INFO == Entity.TYPE_INFO: for col in ch.columns.values(): if hasattr(col, 'entity_relation'): if col.parent.name == self._entity.name: child_columns.append((col, ch)) return child_columns def document_widget(self): """ :return: Returns the widget for managing the supporting documents for an entity if enabled. :rtype: SupportingDocumentsWidget """ return self.doc_widget def source_document_manager(self): """ :return: Returns an instance of the SourceDocumentManager only if supporting documents are enabled for the given entity. Otherwise, None if supporting documents are not enabled. :rtype: SourceDocumentManager """ if self.doc_widget is None: return None return self.doc_widget.source_document_manager def _highlight_asterisk(self, text): # Highlight asterisk in red c = '*' # Do not format if there is no asterisk if text.find(c) == -1: return text asterisk_highlight = '<span style=\" color:#ff0000;\">*</span>' text = text.replace(c, asterisk_highlight) return '<html><head/><body><p>{0}</p></body></html>'.format(text) def _custom_validate(self): """ Override of the MapperMixin which enables custom editor extensions to inject additional validation before saving form data. :return: Return True if the validation was successful, otherwise False. :rtype: bool """ if self._editor_ext is not None: return self._editor_ext.validate() # Return True if there is no custom editor extension specified return True def _post_save(self, model): """ Include additional post-save logic by custom extensions. :param model: SQLAlchemy model :type model: object """ if self._editor_ext is not None: self._editor_ext.post_save(model) def _get_entity_editor_widgets(self): """ Gets entity editor widgets and appends them to a dictionary """ if self.entity_tab_widget: tab_count = self.entity_tab_widget.count() for i in range(tab_count): tab_object = self.entity_tab_widget.widget(i) tab_text = self.entity_tab_widget.tabText(i) self.entity_editor_widgets[tab_text] = tab_object else: self.entity_editor_widgets['no_tab'] = self.entity_scroll_area
class DebuggerWidget(QMainWindow): def __init__(self, parent=None): QMainWindow.__init__(self, parent) self.setWindowTitle("First Aid - Debugger") self.text_edits = {} # fully expanded path of the file -> associated SourceWidget self.toolbar = self.addToolBar("General") self.toolbar.setObjectName("ToolbarGeneral") self.tab_widget = QTabWidget() self.tab_widget.setTabsClosable(True) self.tab_widget.tabCloseRequested.connect(self.on_tab_close_requested) self.tab_widget.currentChanged.connect(self.on_pos_changed) self.setCentralWidget(self.tab_widget) _icon = lambda x: QIcon(os.path.join(os.path.dirname(__file__), "icons", x + ".svg")) self.action_load = self.toolbar.addAction(_icon("folder-outline"), "Load Python file (Ctrl+O)", self.on_load) self.action_load.setShortcut("Ctrl+O") self.action_run = self.toolbar.addAction(_icon("run"), "Run Python file (Ctrl+R)", self.on_run) self.action_run.setShortcut("Ctrl+R") self.action_bp = self.toolbar.addAction(_icon("record"), "Toggle breakpoint (F9)", self.on_toggle_breakpoint) self.action_bp.setShortcut("F9") self.toolbar.addSeparator() self.action_continue = self.toolbar.addAction(_icon("play"), "Continue (F5)", self.on_continue) self.action_continue.setShortcut("F5") self.action_step_into = self.toolbar.addAction(_icon("debug-step-into"), "Step into (F11)", self.on_step_into) self.action_step_into.setShortcut("F11") self.action_step_over = self.toolbar.addAction(_icon("debug-step-over"), "Step over (F10)", self.on_step_over) self.action_step_over.setShortcut("F10") self.action_step_out = self.toolbar.addAction(_icon("debug-step-out"), "Step out (Shift+F11)", self.on_step_out) self.action_step_out.setShortcut("Shift+F11") self.action_run_to_cursor = self.toolbar.addAction(_icon("cursor-default-outline"), "Run to cursor (Ctrl+F10)", self.on_run_to_cursor) self.action_run_to_cursor.setShortcut("Ctrl+F10") self.vars_view = VariablesView() self.frames_view = FramesView() self.dock_frames = QDockWidget("Frames", self) self.dock_frames.setObjectName("DockFrames") self.dock_frames.setWidget(self.frames_view) self.addDockWidget(Qt.BottomDockWidgetArea, self.dock_frames) self.dock_vars = QDockWidget("Variables", self) self.dock_vars.setObjectName("DockVariables") self.dock_vars.setWidget(self.vars_view) self.addDockWidget(Qt.BottomDockWidgetArea, self.dock_vars) self.resize(800, 800) self.debugger = Debugger(self) self.update_buttons() settings = QSettings() self.restoreGeometry(settings.value("/plugins/firstaid/debugger-geometry", b'')) self.restoreState(settings.value("/plugins/firstaid/debugger-windowstate", b'')) filenames = settings.value("/plugins/firstaid/debugger-files", []) if filenames is None: filenames = [] # load files from previous session for filename in filenames: self.load_file(filename) if self.tab_widget.count() > 1: self.tab_widget.setCurrentIndex(0) # start tracing self.start_tracing() def start_tracing(self): """ called from constructor or when the debugger window is opened again """ sys.settrace(self.debugger.trace_function) def closeEvent(self, event): # disable tracing sys.settrace(None) settings = QSettings() settings.setValue("/plugins/firstaid/debugger-geometry", self.saveGeometry()) settings.setValue("/plugins/firstaid/debugger-windowstate", self.saveState()) filenames = list(self.text_edits.keys()) settings.setValue("/plugins/firstaid/debugger-files", filenames) QMainWindow.closeEvent(self, event) def load_file(self, filename): filename = os.path.normpath(os.path.realpath(filename)) if filename in self.text_edits: self.switch_to_file(filename) return # already there... try: self.text_edits[filename] = SourceWidget(filename) except IOError: # TODO: display warning we failed to read the file return tab_text = os.path.basename(filename) self.tab_widget.addTab(self.text_edits[filename], tab_text) self.tab_widget.setTabToolTip(self.tab_widget.count() - 1, filename) self.tab_widget.setCurrentWidget(self.text_edits[filename]) self.text_edits[filename].cursorPositionChanged.connect(self.on_pos_changed) self.on_pos_changed() def switch_to_file(self, filename): if filename in self.text_edits: self.tab_widget.setCurrentWidget(self.text_edits[filename]) def unload_file(self, filename): for index in range(self.tab_widget.count()): if self.text_edits[filename] == self.tab_widget.widget(index): self.tab_widget.removeTab(index) del self.text_edits[filename] break def get_file_name(self, args): if isinstance(args, tuple): return args[0] elif isinstance(args, str): return args return "" def on_load(self): settings = QSettings() folder = settings.value("firstaid/lastFolder", '') args = QFileDialog.getOpenFileName(self, "Load", folder, "Python files (*.py)") filename = self.get_file_name(args) if not filename: return settings.setValue("firstaid/lastFolder", os.path.dirname(filename)) self.load_file(filename) def on_tab_close_requested(self, index): self.unload_file(self.tab_widget.widget(index).filename) def on_pos_changed(self): if not self.current_text_edit(): self.statusBar().showMessage("[no file]") return c = self.current_text_edit().textCursor() line = c.blockNumber() + 1 col = c.positionInBlock() + 1 self.statusBar().showMessage("%d:%d" % (line, col)) def on_run(self): globals = None locals = None if globals is None: import __main__ globals = __main__.__dict__ if locals is None: locals = globals execfile(self.tab_widget.currentWidget().filename, globals, locals) def current_text_edit(self): return self.tab_widget.currentWidget() def on_toggle_breakpoint(self): if self.current_text_edit(): self.current_text_edit().toggle_breakpoint() def update_buttons(self): active = self.debugger.stopped self.action_step_into.setEnabled(active) self.action_step_over.setEnabled(active) self.action_step_out.setEnabled(active) self.action_run_to_cursor.setEnabled(active) self.action_continue.setEnabled(active) def on_step_into(self): self.debugger.stepping = True self.debugger.next_step = None self.debugger.ev_loop.exit(0) def on_step_over(self): self.debugger.stepping = True self.debugger.next_step = ( 'over', self.debugger.current_frame.f_code.co_filename, self.debugger.current_frame.f_lineno) self.debugger.ev_loop.exit(0) def on_step_out(self): self.debugger.stepping = True self.debugger.next_step = ('out', frame_depth(self.debugger.current_frame)) self.debugger.ev_loop.exit(0) def on_run_to_cursor(self): self.debugger.stepping = True filename = self.tab_widget.currentWidget().filename line_no = self.tab_widget.currentWidget().textCursor().blockNumber() + 1 self.debugger.next_step = ('at', filename, line_no) self.debugger.ev_loop.exit(0) def on_continue(self): self.debugger.stepping = False self.current_text_edit().debug_line = -1 self.current_text_edit().update_highlight() self.vars_view.setVariables({}) self.frames_view.setTraceback(None) self.debugger.ev_loop.exit(0)
class AttributesTable(QWidget): def __init__(self, iface): QWidget.__init__(self) self.setWindowTitle(self.tr('Search results')) self.resize(480,320) self.setMinimumSize(320,240) self.center() # Results export button self.btn_saveTab = QAction(QIcon(':/plugins/qgeric/resources/icon_save.png'), self.tr('Save this tab\'s results'), self) self.btn_saveTab.triggered.connect(lambda : self.saveAttributes(True)) self.btn_saveAllTabs = QAction(QIcon(':/plugins/qgeric/resources/icon_saveAll.png'), self.tr('Save all results'), self) self.btn_saveAllTabs.triggered.connect(lambda : self.saveAttributes(False)) self.btn_export = QAction(QIcon(':/plugins/qgeric/resources/icon_export.png'), self.tr('Export the selection as a memory layer'), self) self.btn_export.triggered.connect(self.exportLayer) self.btn_zoom = QAction(QIcon(':/plugins/qgeric/resources/icon_Zoom.png'), self.tr('Zoom to selected attributes'), self) self.btn_zoom.triggered.connect(self.zoomToFeature) self.btn_selectGeom = QAction(QIcon(':/plugins/qgeric/resources/icon_HlG.png'), self.tr('Highlight feature\'s geometry'), self) self.btn_selectGeom.triggered.connect(self.selectGeomChanged) self.btn_rename = QAction(QIcon(':/plugins/qgeric/resources/icon_Settings.png'), self.tr('Settings'), self) self.btn_rename.triggered.connect(self.renameWindow) self.tabWidget = QTabWidget() # Tab container self.tabWidget.setTabsClosable(True) self.tabWidget.currentChanged.connect(self.highlight_features) self.tabWidget.tabCloseRequested.connect(self.closeTab) self.loadingWindow = QProgressDialog() self.loadingWindow.setWindowTitle(self.tr('Loading...')) self.loadingWindow.setRange(0,100) self.loadingWindow.setAutoClose(False) self.loadingWindow.setCancelButton(None) self.canvas = iface.mapCanvas() self.canvas.extentsChanged.connect(self.highlight_features) self.highlight = [] self.highlight_rows = [] toolbar = QToolBar() toolbar.addAction(self.btn_saveTab) toolbar.addAction(self.btn_saveAllTabs) toolbar.addAction(self.btn_export) toolbar.addSeparator() toolbar.addAction(self.btn_zoom) toolbar.addSeparator() toolbar.addAction(self.btn_selectGeom) toolbar.addAction(self.btn_rename) vbox = QVBoxLayout() vbox.setContentsMargins(0,0,0,0) vbox.addWidget(toolbar) vbox.addWidget(self.tabWidget) self.setLayout(vbox) self.mb = iface.messageBar() self.selectGeom = False # False for point, True for geometry def renameWindow(self): title, ok = QInputDialog.getText(self, self.tr('Rename window'), self.tr('Enter a new title:')) if ok: self.setWindowTitle(title) def closeTab(self, index): self.tabWidget.widget(index).deleteLater() self.tabWidget.removeTab(index) def selectGeomChanged(self): if self.selectGeom: self.selectGeom = False self.btn_selectGeom.setText(self.tr('Highlight feature\'s geometry')) self.btn_selectGeom.setIcon(QIcon(':/plugins/qgeric/resources/icon_HlG.png')) else: self.selectGeom = True self.btn_selectGeom.setText(self.tr('Highlight feature\'s centroid')) self.btn_selectGeom.setIcon(QIcon(':/plugins/qgeric/resources/icon_HlC.png')) self.highlight_features() def exportLayer(self): if self.tabWidget.count() != 0: index = self.tabWidget.currentIndex() table = self.tabWidget.widget(index).findChildren(QTableWidget)[0] items = table.selectedItems() if len(items) > 0: type = '' if items[0].feature.geometry().type() == QgsWkbTypes.PointGeometry: type = 'Point' elif items[0].feature.geometry().type() == QgsWkbTypes.LineGeometry: type = 'LineString' else: type = 'Polygon' features = [] for item in items: if item.feature not in features: features.append(item.feature) name = '' ok = True while not name.strip() and ok == True: name, ok = QInputDialog.getText(self, self.tr('Layer name'), self.tr('Give a name to the layer:')) if ok: layer = QgsVectorLayer(type+"?crs="+table.crs.authid(),name,"memory") layer.startEditing() layer.dataProvider().addAttributes(features[0].fields().toList()) layer.dataProvider().addFeatures(features) layer.commitChanges() QgsProject.instance().addMapLayer(layer) else: self.mb.pushWarning(self.tr('Warning'), self.tr('There is no selected feature !')) def highlight_features(self): for item in self.highlight: self.canvas.scene().removeItem(item) del self.highlight[:] del self.highlight_rows[:] index = self.tabWidget.currentIndex() tab = self.tabWidget.widget(index) if self.tabWidget.count() != 0: table = self.tabWidget.widget(index).findChildren(QTableWidget)[0] nb = 0 area = 0 length = 0 items = table.selectedItems() for item in items: if item.row() not in self.highlight_rows: if self.selectGeom: highlight = QgsHighlight(self.canvas, item.feature.geometry(), self.tabWidget.widget(index).layer) else: highlight = QgsHighlight(self.canvas, item.feature.geometry().centroid(), self.tabWidget.widget(index).layer) highlight.setColor(QColor(255,0,0)) self.highlight.append(highlight) self.highlight_rows.append(item.row()) g = QgsGeometry(item.feature.geometry()) g.transform(QgsCoordinateTransform(tab.layer.crs(), QgsCoordinateReferenceSystem(2154), QgsProject.instance())) # geometry reprojection to get meters nb += 1 area += g.area() length += g.length() if tab.layer.geometryType()==QgsWkbTypes.PolygonGeometry: tab.sb.showMessage(self.tr('Selected features')+': '+str(nb)+' '+self.tr('Area')+': '+"%.2f"%area+' m'+u'²') elif tab.layer.geometryType()==QgsWkbTypes.LineGeometry: tab.sb.showMessage(self.tr('Selected features')+': '+str(nb)+' '+self.tr('Length')+': '+"%.2f"%length+' m') else: tab.sb.showMessage(self.tr('Selected features')+': '+str(nb)) def tr(self, message): return QCoreApplication.translate('Qgeric', message) def zoomToFeature(self): index = self.tabWidget.currentIndex() table = self.tabWidget.widget(index).findChildren(QTableWidget)[0] items = table.selectedItems() feat_id = [] for item in items: feat_id.append(item.feature.id()) if len(feat_id) >= 1: if len(feat_id) == 1: self.canvas.setExtent(items[0].feature.geometry().buffer(5, 0).boundingBox()) # in case of a single point, it will still zoom to it else: self.canvas.zoomToFeatureIds(self.tabWidget.widget(self.tabWidget.currentIndex()).layer, feat_id) self.canvas.refresh() # Add a new tab def addLayer(self, layer, headers, types, features): tab = QWidget() tab.layer = layer p1_vertical = QVBoxLayout(tab) p1_vertical.setContentsMargins(0,0,0,0) table = QTableWidget() table.itemSelectionChanged.connect(self.highlight_features) table.title = layer.name() table.crs = layer.crs() table.setColumnCount(len(headers)) if len(features) > 0: table.setRowCount(len(features)) nbrow = len(features) self.loadingWindow.show() self.loadingWindow.setLabelText(table.title) self.loadingWindow.activateWindow() self.loadingWindow.showNormal() # Table population m = 0 for feature in features: n = 0 for cell in feature.attributes(): item = QTableWidgetItem() item.setData(Qt.DisplayRole, cell) item.setFlags(item.flags() ^ Qt.ItemIsEditable) item.feature = feature table.setItem(m, n, item) n += 1 m += 1 self.loadingWindow.setValue(int((float(m)/nbrow)*100)) QApplication.processEvents() else: table.setRowCount(0) table.setHorizontalHeaderLabels(headers) table.horizontalHeader().setSectionsMovable(True) table.types = types table.filter_op = [] table.filters = [] for i in range(0, len(headers)): table.filters.append('') table.filter_op.append(0) header = table.horizontalHeader() header.setContextMenuPolicy(Qt.CustomContextMenu) header.customContextMenuRequested.connect(partial(self.filterMenu, table)) table.setSortingEnabled(True) p1_vertical.addWidget(table) # Status bar to display informations (ie: area) tab.sb = QStatusBar() p1_vertical.addWidget(tab.sb) title = table.title # We reduce the title's length to 20 characters if len(title)>20: title = title[:20]+'...' # We add the number of elements to the tab's title. title += ' ('+str(len(features))+')' self.tabWidget.addTab(tab, title) # Add the tab to the conatiner self.tabWidget.setTabToolTip(self.tabWidget.indexOf(tab), table.title) # Display a tooltip with the layer's full name def filterMenu(self, table, pos): index = table.columnAt(pos.x()) menu = QMenu() filter_operation = QComboBox() if table.types[index] in [10]: filter_operation.addItems([self.tr('Contains'),self.tr('Equals')]) else: filter_operation.addItems(['=','>','<']) filter_operation.setCurrentIndex(table.filter_op[index]) action_filter_operation = QWidgetAction(self) action_filter_operation.setDefaultWidget(filter_operation) if table.types[index] in [14]: if not isinstance(table.filters[index], QDate): filter_value = QDateEdit() else: filter_value = QDateEdit(table.filters[index]) elif table.types[index] in [15]: if not isinstance(table.filters[index], QTime): filter_value = QTimeEdit() else: filter_value = QTimeEdit(table.filters[index]) elif table.types[index] in [16]: if not isinstance(table.filters[index], QDateTime): filter_value = QDateTimeEdit() else: filter_value = QDateTimeEdit(table.filters[index]) else: filter_value = QLineEdit(table.filters[index]) action_filter_value = QWidgetAction(self) action_filter_value.setDefaultWidget(filter_value) menu.addAction(action_filter_operation) menu.addAction(action_filter_value) action_filter_apply = QAction(self.tr('Apply'), self) action_filter_apply.triggered.connect(partial(self.applyFilter, table, index, filter_value, filter_operation)) action_filter_cancel = QAction(self.tr('Cancel'), self) action_filter_cancel.triggered.connect(partial(self.applyFilter, table, index, None, filter_operation)) menu.addAction(action_filter_apply) menu.addAction(action_filter_cancel) menu.exec_(QtGui.QCursor.pos()) def applyFilter(self, table, index, filter_value, filter_operation): if filter_value == None: table.filters[index] = None else: if isinstance(filter_value, QDateEdit): table.filters[index] = filter_value.date() elif isinstance(filter_value, QTimeEdit): table.filters[index] = filter_value.time() elif isinstance(filter_value, QDateTimeEdit): table.filters[index] = filter_value.dateTime() else: table.filters[index] = filter_value.text() table.filter_op[index] = filter_operation.currentIndex() nb_elts = 0 for i in range(0, table.rowCount()): table.setRowHidden(i, False) nb_elts += 1 hidden_rows = [] for nb_col in range(0, table.columnCount()): filtered = False header = table.horizontalHeaderItem(nb_col).text() valid = False if table.filters[nb_col] is not None: if type(table.filters[nb_col]) in [QDate, QTime, QDateTime]: valid = True else: if table.filters[nb_col].strip(): valid = True if valid: filtered = True items = None if table.types[nb_col] in [10]:# If it's a string filter_type = None if table.filter_op[nb_col] == 0: # Contain filter_type = Qt.MatchContains if table.filter_op[nb_col] == 1: # Equal filter_type = Qt.MatchFixedString items = table.findItems(table.filters[nb_col], filter_type) elif table.types[nb_col] in [14, 15, 16]: # If it's a date/time items = [] for nb_row in range(0, table.rowCount()): item = table.item(nb_row, nb_col) if table.filter_op[nb_col] == 0: # = if item.data(QTableWidgetItem.Type) == table.filters[nb_col]: items.append(item) if table.filter_op[nb_col] == 1: # > if item.data(QTableWidgetItem.Type) > table.filters[nb_col]: items.append(item) if table.filter_op[nb_col] == 2: # < if item.data(QTableWidgetItem.Type) < table.filters[nb_col]: items.append(item) else: # If it's a number items = [] for nb_row in range(0, table.rowCount()): item = table.item(nb_row, nb_col) if item.text().strip(): if table.filter_op[nb_col] == 0: # = if float(item.text()) == float(table.filters[nb_col]): items.append(item) if table.filter_op[nb_col] == 1: # > if float(item.text()) > float(table.filters[nb_col]): items.append(item) if table.filter_op[nb_col] == 2: # < if float(item.text()) < float(table.filters[nb_col]): items.append(item) rows = [] for item in items: if item.column() == nb_col: rows.append(item.row()) for i in range(0, table.rowCount()): if i not in rows: if i not in hidden_rows: nb_elts -= 1 table.setRowHidden(i, True) hidden_rows.append(i) if filtered: if header[len(header)-1] != '*': table.setHorizontalHeaderItem(nb_col, QTableWidgetItem(header+'*')) else: if header[len(header)-1] == '*': header = header[:-1] table.setHorizontalHeaderItem(nb_col, QTableWidgetItem(header)) title = self.tabWidget.tabText(self.tabWidget.currentIndex()) for i in reversed(range(len(title))): if title[i] == ' ': break title = title[:-1] title += '('+str(nb_elts)+')' self.tabWidget.setTabText(self.tabWidget.currentIndex(), title) # Save tables in OpenDocument format # Use odswriter library def saveAttributes(self, active): file = QFileDialog.getSaveFileName(self, self.tr('Save in...'),'', self.tr('OpenDocument Spreadsheet (*.ods)')) if file[0]: try: with ods.writer(open(file[0],"wb")) as odsfile: tabs = None if active: tabs = self.tabWidget.currentWidget().findChildren(QTableWidget) else: tabs = self.tabWidget.findChildren(QTableWidget) for table in reversed(tabs): sheet = odsfile.new_sheet(table.title[:20]+'...') # For each tab in the container, a new sheet is created sheet.writerow([table.title]) # As the tab's title's lenght is limited, the full name of the layer is written in the first row nb_row = table.rowCount() nb_col = table.columnCount() # Fetching and writing of the table's header header = [] for i in range(0,nb_col): header.append(table.horizontalHeaderItem(i).text()) sheet.writerow(header) # Fetching and writing of the table's items for i in range(0,nb_row): row = [] for j in range(0,nb_col): row.append(table.item(i,j).text()) if not table.isRowHidden(i): sheet.writerow(row) return True except IOError: QMessageBox.critical(self, self.tr('Error'), self.tr('The file can\'t be written.')+'\n'+self.tr('Maybe you don\'t have the rights or are trying to overwrite an opened file.')) return False def center(self): screen = QDesktopWidget().screenGeometry() size = self.geometry() self.move((screen.width()-size.width())/2, (screen.height()-size.height())/2) def clear(self): self.tabWidget.clear() for table in self.tabWidget.findChildren(QTableWidget): table.setParent(None) def closeEvent(self, e): result = QMessageBox.question(self, self.tr("Saving ?"), self.tr("Would you like to save results before exit ?"), buttons = QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel) if result == QMessageBox.Yes: if self.saveAttributes(False): self.clear() e.accept() else: e.ignore() elif result == QMessageBox.No: self.clear() e.accept() else: e.ignore()