diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml new file mode 100644 index 0000000..c11a31d --- /dev/null +++ b/.github/workflows/build-test.yml @@ -0,0 +1,45 @@ +name: Build and Test Branch + +on: + push: + branches: + - '**' # Runs on every branch + - '!main' # exclude main + pull_request: + +jobs: + build-artifact: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Install uv + uses: astral-sh/setup-uv@v2 + + - name: Set up Python + run: uv python install 3.12 + + - name: Prepare Plugin directory + run: | + mkdir -p release/qflowcrate + cp -r qflowcrate/* release/qflowcrate/ + + - name: Install dependencies to libs + run: | + mkdir -p release/qflowcrate/libs + uv pip install --target release/qflowcrate/libs rocrate + + - name: Create ZIP + run: | + cd release + # Use the branch name or commit hash for the zip name + zip -r qflowcrate-test-build.zip qflowcrate/ -x "*/__pycache__/*" + + + - name: Upload Artifact for Testing + uses: actions/upload-artifact@v4 + with: + name: qflowcrate-test-build + path: release/qflowcrate-test-build.zip + retention-days: 3 \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3fdbf44..6de5b39 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,7 +24,7 @@ jobs: - name: Prepare Plugin directory run: | mkdir -p release/qflowcrate - cp -r plugin/* release/qflowcrate/ + cp -r qflowcrate/* release/qflowcrate/ - name: Install dependencies to libs run: | diff --git a/.gitignore b/.gitignore index e44a1cb..5a2f87f 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,4 @@ __pycache__/ .vscode/ .idea/ -plugin/libs/ \ No newline at end of file +qflowcrate/libs/ \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 9ff070e..3429913 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ dependencies = ["rocrate>=0.14.0"] [tool.ruff] line-length = 120 target-version = "py312" -exclude = ["plugin/libs/*"] +exclude = ["qflowcrate/libs/*"] [tool.ruff.lint] select = ["E", "F", "I", "N", "W"] diff --git a/plugin/.gitignore b/qflowcrate/.gitignore similarity index 100% rename from plugin/.gitignore rename to qflowcrate/.gitignore diff --git a/plugin/Plugin/Export/export_tab.py b/qflowcrate/Plugin/Export/export_tab.py similarity index 99% rename from plugin/Plugin/Export/export_tab.py rename to qflowcrate/Plugin/Export/export_tab.py index 418a12e..fa6e8d4 100644 --- a/plugin/Plugin/Export/export_tab.py +++ b/qflowcrate/Plugin/Export/export_tab.py @@ -31,7 +31,7 @@ from qgis.utils import Qgis from rocrate.rocrate import ROCrate -from ..utility import Logger, display_error_message, get_logger +from qflowcrate.Plugin.utility import Logger, display_error_message, get_logger class ExportTab(QWidget): diff --git a/plugin/Plugin/Graph/connection_arrow.py b/qflowcrate/Plugin/Graph/connection_arrow.py similarity index 98% rename from plugin/Plugin/Graph/connection_arrow.py rename to qflowcrate/Plugin/Graph/connection_arrow.py index 1639153..cc3e76b 100644 --- a/plugin/Plugin/Graph/connection_arrow.py +++ b/qflowcrate/Plugin/Graph/connection_arrow.py @@ -9,8 +9,8 @@ from qgis.PyQt.QtGui import QBrush, QPen, QPolygonF from qgis.PyQt.QtWidgets import QGraphicsLineItem, QGraphicsPolygonItem -from .layer_node import LayerNode -from .process_node import ProcessNode +from qflowcrate.Plugin.Graph.layer_node import LayerNode +from qflowcrate.Plugin.Graph.process_node import ProcessNode class ConnectionArrow(QGraphicsLineItem): diff --git a/plugin/Plugin/Graph/graph_tab.py b/qflowcrate/Plugin/Graph/graph_tab.py similarity index 96% rename from plugin/Plugin/Graph/graph_tab.py rename to qflowcrate/Plugin/Graph/graph_tab.py index c6c57db..e359132 100644 --- a/plugin/Plugin/Graph/graph_tab.py +++ b/qflowcrate/Plugin/Graph/graph_tab.py @@ -21,14 +21,14 @@ QWidget, ) -from ..Layer.layer_factory import LayerFactory -from ..Layer.layer_metadata_dialog import LayerMetadataDialog -from ..Process.process import Process -from ..Process.process_metadata_dialog import ProcessMetadataDialog -from ..utility import get_logger -from .graph_view import GraphView -from .layer_node import LayerNode -from .process_node import ProcessNode +from qflowcrate.Plugin.Layer.layer_factory import LayerFactory +from qflowcrate.Plugin.Layer.layer_metadata_dialog import LayerMetadataDialog +from qflowcrate.Plugin.Process.process import Process +from qflowcrate.Plugin.Process.process_metadata_dialog import ProcessMetadataDialog +from qflowcrate.Plugin.utility import get_logger +from qflowcrate.Plugin.Graph.graph_view import GraphView +from qflowcrate.Plugin.Graph.layer_node import LayerNode +from qflowcrate.Plugin.Graph.process_node import ProcessNode class GraphTab(QWidget): diff --git a/plugin/Plugin/Graph/graph_view.py b/qflowcrate/Plugin/Graph/graph_view.py similarity index 97% rename from plugin/Plugin/Graph/graph_view.py rename to qflowcrate/Plugin/Graph/graph_view.py index a9a664d..e2b633c 100644 --- a/plugin/Plugin/Graph/graph_view.py +++ b/qflowcrate/Plugin/Graph/graph_view.py @@ -12,9 +12,9 @@ QMessageBox, ) -from .connection_arrow import ConnectionArrow -from .layer_node import LayerNode -from .process_node import ProcessNode +from qflowcrate.Plugin.Graph.connection_arrow import ConnectionArrow +from qflowcrate.Plugin.Graph.layer_node import LayerNode +from qflowcrate.Plugin.Graph.process_node import ProcessNode class GraphView(QGraphicsView): diff --git a/plugin/Plugin/Graph/layer_node.py b/qflowcrate/Plugin/Graph/layer_node.py similarity index 99% rename from plugin/Plugin/Graph/layer_node.py rename to qflowcrate/Plugin/Graph/layer_node.py index 828e2b7..91f6e50 100644 --- a/plugin/Plugin/Graph/layer_node.py +++ b/qflowcrate/Plugin/Graph/layer_node.py @@ -12,7 +12,7 @@ QMenu, ) -from ..Layer.layer_metadata_dialog import LayerMetadataDialog +from qflowcrate.Plugin.Layer.layer_metadata_dialog import LayerMetadataDialog class LayerNode(QGraphicsRectItem): diff --git a/plugin/Plugin/Graph/process_node.py b/qflowcrate/Plugin/Graph/process_node.py similarity index 98% rename from plugin/Plugin/Graph/process_node.py rename to qflowcrate/Plugin/Graph/process_node.py index ab69f5b..2b4da4b 100644 --- a/plugin/Plugin/Graph/process_node.py +++ b/qflowcrate/Plugin/Graph/process_node.py @@ -12,8 +12,8 @@ QMenu, ) -from ..Process.process_metadata_dialog import ProcessMetadataDialog -from ..utility import get_logger +from qflowcrate.Plugin.Process.process_metadata_dialog import ProcessMetadataDialog +from qflowcrate.Plugin.utility import get_logger class ProcessNode(QGraphicsEllipseItem): diff --git a/qflowcrate/Plugin/Import/import_tab.py b/qflowcrate/Plugin/Import/import_tab.py new file mode 100644 index 0000000..07f0fda --- /dev/null +++ b/qflowcrate/Plugin/Import/import_tab.py @@ -0,0 +1,410 @@ +# -*- coding: utf-8 -*- +""" +Import Tab Widget - Widget for importing and visualizing RO-Crate workflows +""" + +import ast +import json +import re +import zipfile +from pathlib import Path + +from qgis.PyQt.QtCore import Qt +from qgis.PyQt.QtWidgets import ( + QFileDialog, + QHBoxLayout, + QLabel, + QLineEdit, + QMenu, + QMessageBox, + QPushButton, + QVBoxLayout, + QWidget, +) + +from qflowcrate.Plugin.Graph.connection_arrow import ConnectionArrow +from qflowcrate.Plugin.Graph.graph_view import GraphView +from qflowcrate.Plugin.Graph.layer_node import LayerNode +from qflowcrate.Plugin.Graph.process_node import ProcessNode +from qflowcrate.Plugin.utility import get_logger + + +# ============================================================================ +# MOCK OBJECTS FOR READ-ONLY METADATA DIALOGS +# ============================================================================ + +class ImportedLayer: + """Mock layer object rich enough to satisfy LayerMetadataDialog""" + def __init__(self, dataset_entity, entity_map): + self.id = dataset_entity["@id"] + self.name = dataset_entity["name"] + self.clean_name = self.name + self.description = dataset_entity["description"] + + visible_val = dataset_entity["layerVisible"] + self.visible = str(visible_val).lower() == "true" if isinstance(visible_val, str) else bool(visible_val) + + # Prepare default values + self.type = "Unknown" + self.source = self.id + self.external = False + self.source_title = "" + self.source_url = "" + self.source_date = "" + self.source_comment = "" + + # Find geometry entity which contains technical & external metadata + geometry_entity = None + has_parts = dataset_entity["hasPart"] + if isinstance(has_parts, dict): + has_parts = [has_parts] + + for part_ref in has_parts: + part_id = part_ref if isinstance(part_ref, str) else part_ref["@id"] + if part_id in entity_map and "geometry" in part_id.lower(): + geometry_entity = entity_map[part_id] + break + + # Extract metadata from geometry entity + if geometry_entity: + self.source = geometry_entity["@id"] + + # Extract layer type (fall back to additionalType if layerType is somehow missing) + raw_type = geometry_entity.get("layerType", geometry_entity.get("additionalType", "Unknown")) + if isinstance(raw_type, list): + raw_type = raw_type[0] + + if "Vector" in raw_type or "Point" in raw_type or "Line" in raw_type or "Polygon" in raw_type: + self.type = "Vector" + elif "Raster" in raw_type: + self.type = "Raster" + else: + self.type = raw_type + + # Extract optional external source info + self.source_title = geometry_entity.get("sourceTitle", "") + self.source_url = geometry_entity.get("sourceURL", "") + self.source_date = geometry_entity.get("sourceDate", "") + self.source_comment = geometry_entity.get("sourceComment", "") + + # Enable 'External Source' flag if data is present + if self.source_title or self.source_url or self.source_comment: + self.external = True + + +class ImportedProcess: + """Mock process object rich enough to satisfy ProcessMetadataDialog""" + def __init__(self, action_entity, instrument_entity=None): + self.id = action_entity["@id"] + self.name = action_entity["name"] + self.description = action_entity["description"] + + if instrument_entity: + self.algorithm_id = instrument_entity["name"] + else: + self.algorithm_id = "Unknown Algorithm" + + # Reconstruct timestamp from ID since the process exporter doesn't write it explicitly + self.timestamp = action_entity.get("endTime", action_entity.get("startTime", "")) + if not self.timestamp: + match = re.search(r'(\d{2})(\d{2})(\d{4})(\d{2})(\d{2})(\d{2})$', self.id) + if match: + dd, mm, yyyy, hh, mnt, ss = match.groups() + self.timestamp = f"{dd}.{mm}.{yyyy} {hh}:{mnt}:{ss}" + else: + self.timestamp = "Unknown Time" + + # Read custom QGIS properties + self.log = action_entity["qgisLog"] + raw_params = action_entity["qgisParameters"] + raw_results = action_entity["qgisResults"] + + # Convert JSON strings to dictionaries for UI rendering in the metadata dialog + try: + self.parameters = ast.literal_eval(raw_params) if isinstance(raw_params, str) else raw_params + except Exception: + self.parameters = raw_params + + try: + self.results = ast.literal_eval(raw_results) if isinstance(raw_results, str) else raw_results + except Exception: + self.results = raw_results + + def set_input(self, ids): pass + def set_result(self, ids): pass + + +# ============================================================================ +# MAIN IMPORT TAB WIDGET +# ============================================================================ + +class ImportTab(QWidget): + """Widget for importing and visualizing RO-Crate workflows""" + + def __init__(self, parent=None): + super().__init__(parent) + self.logger = get_logger("ImportTab") + self.nodes_map = {} # Map of NORMALIZED @id -> Node instance + self.entity_map = {} # Map of raw @id -> JSON entity + self.normalized_datasets = {} # Map of NORMALIZED @id -> Dataset JSON + + self.setup_ui() + + def setup_ui(self): + """Setup the user interface programmatically""" + main_layout = QVBoxLayout() + main_layout.setContentsMargins(10, 10, 10, 10) + main_layout.setSpacing(12) + + instruction_label = QLabel( + "Select an exported RO-Crate (.zip) file to visualize its workflow graph. " + "This view is read-only. Right-click any node to inspect its metadata." + ) + instruction_label.setWordWrap(True) + main_layout.addWidget(instruction_label) + + file_layout = QHBoxLayout() + file_layout.setSpacing(8) + + self.file_path_lineedit = QLineEdit() + self.file_path_lineedit.setPlaceholderText("Select RO-Crate .zip file...") + self.file_path_lineedit.setReadOnly(True) + file_layout.addWidget(self.file_path_lineedit) + + self.browse_btn = QPushButton("Browse...") + self.browse_btn.clicked.connect(self.browse_file) + file_layout.addWidget(self.browse_btn) + + self.clear_btn = QPushButton("Clear Graph") + self.clear_btn.clicked.connect(self.clear_graph) + file_layout.addWidget(self.clear_btn) + + main_layout.addLayout(file_layout) + + self.graph_view = GraphView() + self.graph_view.setMinimumHeight(400) + self.graph_view.setAcceptDrops(False) + main_layout.addWidget(self.graph_view) + + self.setLayout(main_layout) + + def browse_file(self): + """Open file dialog to select RO-Crate zip""" + file_path, _ = QFileDialog.getOpenFileName( + self, "Select RO-Crate Archive", "", "ZIP Files (*.zip);;All Files (*)" + ) + + if file_path: + self.file_path_lineedit.setText(file_path) + self.load_graph_from_crate(file_path) + + def clear_graph(self): + """Clear all items from graph""" + self.graph_view.scene.clear() + self.nodes_map.clear() + self.entity_map.clear() + self.normalized_datasets.clear() + self.file_path_lineedit.clear() + + def _normalize_id(self, raw_id): + """Normalizes IDs by stripping './' prefix and '/' suffix.""" + if not raw_id: + return "" + return raw_id.lstrip('./').rstrip('/') + + def load_graph_from_crate(self, zip_path): + """Parse RO-Crate zip and build the visual graph.""" + self.graph_view.scene.clear() + self.nodes_map.clear() + self.entity_map.clear() + self.normalized_datasets.clear() + + try: + with zipfile.ZipFile(zip_path, "r") as zf: + if "ro-crate-metadata.json" not in zf.namelist(): + raise ValueError("ro-crate-metadata.json missing in archive.") + with zf.open("ro-crate-metadata.json") as f: + metadata = json.load(f) + + graph_data = metadata["@graph"] + + # 1. Map all entities + self.entity_map = {item["@id"]: item for item in graph_data} + + # 2. Map Datasets specially to handle ID normalization + for item in graph_data: + types = item["@type"] + if not isinstance(types, list): + types = [types] + + if "Dataset" in types and item["@id"] not in ["./", ".\\"]: + norm_id = self._normalize_id(item["@id"]) + self.normalized_datasets[norm_id] = item + + # 3. Identify Processing Steps + actions = [] + for item in graph_data: + types = item["@type"] + if not isinstance(types, list): + types = [types] + if "CreateAction" in types: + actions.append(item) + + # 4. Reconstruct Nodes and Connections + connections_to_make = [] + + for action in actions: + action_id = action["@id"] + + # Fetch instrument entity to pass to our Mock Process + instrument_refs = action["instrument"] + instrument_entity = None + if instrument_refs: + ref_id = instrument_refs[0]["@id"] if isinstance(instrument_refs, list) else instrument_refs["@id"] + instrument_entity = self.entity_map[ref_id] + + # Create Process Node + process_obj = ImportedProcess(action, instrument_entity) + p_node = ProcessNode(process_obj) + self._disable_interactions(p_node) + self.nodes_map[action_id] = p_node + self.graph_view.scene.addItem(p_node) + + # Process Inputs (Data -> Process) + inputs = action["object"] + if isinstance(inputs, dict): + inputs = [inputs] + + for input_ref in inputs: + in_id = input_ref["@id"] + if in_id: + norm_in_id = self._ensure_layer_node_exists(in_id) + connections_to_make.append((norm_in_id, action_id)) + + # Process Outputs (Process -> Data) + outputs = action["result"] + if isinstance(outputs, dict): + outputs = [outputs] + + for output_ref in outputs: + out_id = output_ref["@id"] + if out_id: + norm_out_id = self._ensure_layer_node_exists(out_id) + connections_to_make.append((action_id, norm_out_id)) + + # 5. Draw Connections + for source_id, target_id in connections_to_make: + source_node = self.nodes_map.get(source_id) + target_node = self.nodes_map.get(target_id) + + if source_node and target_node: + arrow = ConnectionArrow(source_node, target_node) + self.graph_view.scene.addItem(arrow) + + # 6. Apply auto-layout + self._apply_auto_layout(connections_to_make) + + except Exception as e: + self.logger.error(f"Failed to load RO-Crate: {e}") + QMessageBox.critical(self, "Import Error", f"Failed to parse RO-Crate:\n{str(e)}") + self.clear_graph() + + def _ensure_layer_node_exists(self, raw_layer_ref): + """Create a LayerNode. Handles the ID mismatch between inputs/outputs and datasets.""" + norm_id = self._normalize_id(raw_layer_ref) + + if norm_id not in self.nodes_map: + # Get the correct dataset via normalized ID + dataset_entity = self.normalized_datasets.get(norm_id) + + # Fallback if it is not a dataset + if not dataset_entity: + dataset_entity = self.entity_map.get(raw_layer_ref, {"@id": raw_layer_ref}) + + layer_obj = ImportedLayer(dataset_entity, self.entity_map) + l_node = LayerNode(layer_obj) + self._disable_interactions(l_node) + + # Register node under NORMALIZED id for connections + self.nodes_map[norm_id] = l_node + self.graph_view.scene.addItem(l_node) + + return norm_id + + def _disable_interactions(self, node): + """Replace standard context menu with a read-only inspect menu.""" + node.setToolTip(node.toolTip() + "\n(Right-click to Inspect)") + + def custom_context_menu(event): + menu = QMenu() + inspect_action = menu.addAction("Inspect Details") + action = menu.exec_(event.screenPos()) + + if action == inspect_action: + if isinstance(node, LayerNode): + node._inspect_layer() + elif isinstance(node, ProcessNode): + node._inspect_process() + + node.contextMenuEvent = custom_context_menu + + # ============================================================================ + # GRAPH LAYOUT ENGINE + # ============================================================================ + + def _apply_auto_layout(self, edges): + """A simple left-to-right topological layout algorithm.""" + if not self.nodes_map: + return + + in_degrees = {node_id: 0 for node_id in self.nodes_map.keys()} + out_edges = {node_id: [] for node_id in self.nodes_map.keys()} + + for source, target in edges: + in_degrees[target] = in_degrees.get(target, 0) + 1 + out_edges[source].append(target) + + queue = [node_id for node_id, degree in in_degrees.items() if degree == 0] + + if not queue and self.nodes_map: + queue = [list(self.nodes_map.keys())[0]] + + levels = {} + while queue: + current_id = queue.pop(0) + current_level = levels.get(current_id, 0) + + for neighbor in out_edges.get(current_id, []): + levels[neighbor] = max(levels.get(neighbor, 0), current_level + 1) + in_degrees[neighbor] -= 1 + if in_degrees[neighbor] <= 0: + queue.append(neighbor) + + for node_id in self.nodes_map: + if node_id not in levels: + levels[node_id] = 0 + + level_groups = {} + for node_id, lvl in levels.items(): + level_groups.setdefault(lvl, []).append(node_id) + + horizontal_spacing = 250 + vertical_spacing = 150 + + for lvl, node_ids in level_groups.items(): + x_pos = 50 + (lvl * horizontal_spacing) + + total_height = len(node_ids) * vertical_spacing + start_y = (self.graph_view.scene.sceneRect().height() - total_height) / 2 + + for index, node_id in enumerate(node_ids): + y_pos = max(50, start_y + (index * vertical_spacing)) + self.nodes_map[node_id].setPos(x_pos, y_pos) + + for node in self.nodes_map.values(): + if isinstance(node, LayerNode): + for arrow in node.connections + node.input_arrows: + arrow.update_position() + elif isinstance(node, ProcessNode): + for arrow in node.input_arrows + node.output_arrows: + arrow.update_position() \ No newline at end of file diff --git a/plugin/Plugin/Instruction/instruction_tab.py b/qflowcrate/Plugin/Instruction/instruction_tab.py similarity index 94% rename from plugin/Plugin/Instruction/instruction_tab.py rename to qflowcrate/Plugin/Instruction/instruction_tab.py index 2609cff..618fbf9 100644 --- a/plugin/Plugin/Instruction/instruction_tab.py +++ b/qflowcrate/Plugin/Instruction/instruction_tab.py @@ -89,6 +89,12 @@ def setup_ui(self): "also create a log file in the same directory. After you have done that, " "hit Export RO-Crate.", ) + self._add_section( + main_layout, + "Import Tab - Importing an Existing RO-Crate", + "This tab allows you to import an existing RO-Crate. This can be used to " + "recreate the workflow graph from a previously exported RO-Crate.", + ) main_layout.addWidget(scroll_area) self.setLayout(main_layout) diff --git a/plugin/Plugin/Layer/gpkg_layer.py b/qflowcrate/Plugin/Layer/gpkg_layer.py similarity index 89% rename from plugin/Plugin/Layer/gpkg_layer.py rename to qflowcrate/Plugin/Layer/gpkg_layer.py index bf6fc3f..a1863c0 100644 --- a/plugin/Plugin/Layer/gpkg_layer.py +++ b/qflowcrate/Plugin/Layer/gpkg_layer.py @@ -1,5 +1,5 @@ -from ..Layer.layer import Layer -from ..utility import get_logger +from qflowcrate.Plugin.Layer.layer import Layer +from qflowcrate.Plugin.utility import get_logger class GPKGLayer(Layer): diff --git a/plugin/Plugin/Layer/layer.py b/qflowcrate/Plugin/Layer/layer.py similarity index 99% rename from plugin/Plugin/Layer/layer.py rename to qflowcrate/Plugin/Layer/layer.py index 680b365..d62977e 100644 --- a/plugin/Plugin/Layer/layer.py +++ b/qflowcrate/Plugin/Layer/layer.py @@ -6,7 +6,7 @@ from qgis.core import QgsMapLayer, QgsMapLayerType, QgsProject, QgsWkbTypes -from ..utility import get_logger, get_mimetype +from qflowcrate.Plugin.utility import get_logger, get_mimetype class Layer: diff --git a/plugin/Plugin/Layer/layer_factory.py b/qflowcrate/Plugin/Layer/layer_factory.py similarity index 83% rename from plugin/Plugin/Layer/layer_factory.py rename to qflowcrate/Plugin/Layer/layer_factory.py index f32ad17..f33fd18 100644 --- a/plugin/Plugin/Layer/layer_factory.py +++ b/qflowcrate/Plugin/Layer/layer_factory.py @@ -1,9 +1,9 @@ -from .gpkg_layer import GPKGLayer -from .layer import Layer -from .memory_layer import MemoryLayer -from .shp_layer import SHPLayer -from .wfs_layer import WFSLayer -from .wms_layer import WMSLayer +from qflowcrate.Plugin.Layer.gpkg_layer import GPKGLayer +from qflowcrate.Plugin.Layer.layer import Layer +from qflowcrate.Plugin.Layer.memory_layer import MemoryLayer +from qflowcrate.Plugin.Layer.shp_layer import SHPLayer +from qflowcrate.Plugin.Layer.wfs_layer import WFSLayer +from qflowcrate.Plugin.Layer.wms_layer import WMSLayer class LayerFactory: diff --git a/plugin/Plugin/Layer/layer_metadata_dialog.py b/qflowcrate/Plugin/Layer/layer_metadata_dialog.py similarity index 99% rename from plugin/Plugin/Layer/layer_metadata_dialog.py rename to qflowcrate/Plugin/Layer/layer_metadata_dialog.py index 1482e66..fa85433 100644 --- a/plugin/Plugin/Layer/layer_metadata_dialog.py +++ b/qflowcrate/Plugin/Layer/layer_metadata_dialog.py @@ -18,7 +18,7 @@ QVBoxLayout, ) -from ..utility import display_error_message +from qflowcrate.Plugin.utility import display_error_message class LayerMetadataDialog(QDialog): diff --git a/plugin/Plugin/Layer/memory_layer.py b/qflowcrate/Plugin/Layer/memory_layer.py similarity index 97% rename from plugin/Plugin/Layer/memory_layer.py rename to qflowcrate/Plugin/Layer/memory_layer.py index 60ebb5a..397f45b 100644 --- a/plugin/Plugin/Layer/memory_layer.py +++ b/qflowcrate/Plugin/Layer/memory_layer.py @@ -2,8 +2,8 @@ from qgis.core import QgsRasterFileWriter, QgsRasterPipe, QgsVectorFileWriter -from ..Layer.layer import Layer -from ..utility import get_logger, get_mimetype +from qflowcrate.Plugin.Layer.layer import Layer +from qflowcrate.Plugin.utility import get_logger, get_mimetype class MemoryLayer(Layer): diff --git a/plugin/Plugin/Layer/shp_layer.py b/qflowcrate/Plugin/Layer/shp_layer.py similarity index 95% rename from plugin/Plugin/Layer/shp_layer.py rename to qflowcrate/Plugin/Layer/shp_layer.py index 53a2829..7c8fa28 100644 --- a/plugin/Plugin/Layer/shp_layer.py +++ b/qflowcrate/Plugin/Layer/shp_layer.py @@ -3,8 +3,8 @@ import tempfile import zipfile -from ..Layer.layer import Layer -from ..utility import get_logger +from qflowcrate.Plugin.Layer.layer import Layer +from qflowcrate.Plugin.utility import get_logger class SHPLayer(Layer): diff --git a/plugin/Plugin/Layer/wfs_layer.py b/qflowcrate/Plugin/Layer/wfs_layer.py similarity index 97% rename from plugin/Plugin/Layer/wfs_layer.py rename to qflowcrate/Plugin/Layer/wfs_layer.py index 6ff941f..78cbe4f 100644 --- a/plugin/Plugin/Layer/wfs_layer.py +++ b/qflowcrate/Plugin/Layer/wfs_layer.py @@ -1,8 +1,8 @@ import re from urllib.parse import urlencode -from ..Layer.layer import Layer -from ..utility import get_logger, get_mimetype +from qflowcrate.Plugin.Layer.layer import Layer +from qflowcrate.Plugin.utility import get_logger, get_mimetype class WFSLayer(Layer): diff --git a/plugin/Plugin/Layer/wms_layer.py b/qflowcrate/Plugin/Layer/wms_layer.py similarity index 97% rename from plugin/Plugin/Layer/wms_layer.py rename to qflowcrate/Plugin/Layer/wms_layer.py index fe37a55..693c7cd 100644 --- a/plugin/Plugin/Layer/wms_layer.py +++ b/qflowcrate/Plugin/Layer/wms_layer.py @@ -1,7 +1,7 @@ from urllib.parse import unquote -from ..Layer.layer import Layer -from ..utility import get_logger, get_mimetype +from qflowcrate.Plugin.Layer.layer import Layer +from qflowcrate.Plugin.utility import get_logger, get_mimetype class WMSLayer(Layer): diff --git a/plugin/Plugin/Process/instrument.py b/qflowcrate/Plugin/Process/instrument.py similarity index 96% rename from plugin/Plugin/Process/instrument.py rename to qflowcrate/Plugin/Process/instrument.py index 719be69..e762082 100644 --- a/plugin/Plugin/Process/instrument.py +++ b/qflowcrate/Plugin/Process/instrument.py @@ -3,7 +3,7 @@ Instrument - Represents a QGIS processing algorithm as a software instrument """ -from ..utility import get_logger +from qflowcrate.Plugin.utility import get_logger class Instrument: diff --git a/plugin/Plugin/Process/process.py b/qflowcrate/Plugin/Process/process.py similarity index 97% rename from plugin/Plugin/Process/process.py rename to qflowcrate/Plugin/Process/process.py index 367fdd4..5464960 100644 --- a/plugin/Plugin/Process/process.py +++ b/qflowcrate/Plugin/Process/process.py @@ -5,8 +5,8 @@ import re -from ..utility import get_logger -from .instrument import Instrument +from qflowcrate.Plugin.utility import get_logger +from qflowcrate.Plugin.Process.instrument import Instrument class Process: diff --git a/plugin/Plugin/Process/process_metadata_dialog.py b/qflowcrate/Plugin/Process/process_metadata_dialog.py similarity index 100% rename from plugin/Plugin/Process/process_metadata_dialog.py rename to qflowcrate/Plugin/Process/process_metadata_dialog.py diff --git a/plugin/Plugin/main_dialog.py b/qflowcrate/Plugin/main_dialog.py similarity index 84% rename from plugin/Plugin/main_dialog.py rename to qflowcrate/Plugin/main_dialog.py index 5e7c66f..1d220da 100644 --- a/plugin/Plugin/main_dialog.py +++ b/qflowcrate/Plugin/main_dialog.py @@ -8,6 +8,7 @@ QDialog, QDialogButtonBox, QTabWidget, + QWidget, QVBoxLayout, ) from qgis.PyQt import QtGui @@ -15,9 +16,10 @@ import os # Import individual tab widgets -from .Export.export_tab import ExportTab -from .Graph.graph_tab import GraphTab -from .Instruction.instruction_tab import InstructionTab +from qflowcrate.Plugin.Export.export_tab import ExportTab +from qflowcrate.Plugin.Graph.graph_tab import GraphTab +from qflowcrate.Plugin.Instruction.instruction_tab import InstructionTab +from qflowcrate.Plugin.Import.import_tab import ImportTab class MainDialog(QDialog): @@ -103,11 +105,31 @@ def _initialize_tab_widgets(self): self.instruction_tab = InstructionTab(parent=self) self.graph_tab = GraphTab(parent=self) self.export_tab = ExportTab(parent=self) + self.import_tab = ImportTab(parent=self) # Add tabs to the main tab widget self.tab_widget.addTab(self.instruction_tab, "Instructions") + + # Keep your existing setup code + self.tab_widget.addTab(QWidget(), "") + self.tab_widget.setTabEnabled(1, False) + self.tab_widget.addTab(self.graph_tab, "Graph") self.tab_widget.addTab(self.export_tab, "Export") + self.tab_widget.addTab(QWidget(), "") + self.tab_widget.setTabEnabled(4, False) + + self.tab_widget.addTab(self.import_tab, "Import") + + # Apply this stylesheet to the tab bar to hide index 3 + self.tab_widget.setStyleSheet(""" + QTabBar::tab:disabled { + width: 10px; + color: transparent; + background: transparent; + border: none; + } + """) # Set initial tab self.tab_widget.setCurrentIndex(0) # Start with Instructions tab diff --git a/plugin/Plugin/mimetypes.json b/qflowcrate/Plugin/mimetypes.json similarity index 100% rename from plugin/Plugin/mimetypes.json rename to qflowcrate/Plugin/mimetypes.json diff --git a/plugin/Plugin/utility.py b/qflowcrate/Plugin/utility.py similarity index 100% rename from plugin/Plugin/utility.py rename to qflowcrate/Plugin/utility.py diff --git a/plugin/__init__.py b/qflowcrate/__init__.py similarity index 95% rename from plugin/__init__.py rename to qflowcrate/__init__.py index 5f7d21d..25c79b5 100644 --- a/plugin/__init__.py +++ b/qflowcrate/__init__.py @@ -39,6 +39,6 @@ def classFactory(iface): # pylint: disable=invalid-name # noqa: N802 if libs_path not in sys.path: sys.path.insert(0, libs_path) # - from .automated_workflow_documentation import AutomatedWorkflowDocumentation + from qflowcrate.automated_workflow_documentation import AutomatedWorkflowDocumentation return AutomatedWorkflowDocumentation(iface) diff --git a/plugin/automated_workflow_documentation.py b/qflowcrate/automated_workflow_documentation.py similarity index 99% rename from plugin/automated_workflow_documentation.py rename to qflowcrate/automated_workflow_documentation.py index ca572e0..f1b81c4 100644 --- a/plugin/automated_workflow_documentation.py +++ b/qflowcrate/automated_workflow_documentation.py @@ -32,7 +32,7 @@ from qgis.PyQt.QtWidgets import QAction, QMessageBox # Import the code for the dialog -from .Plugin.main_dialog import MainDialog +from qflowcrate.Plugin.main_dialog import MainDialog class AutomatedWorkflowDocumentation: diff --git a/plugin/icon.svg b/qflowcrate/icon.svg similarity index 100% rename from plugin/icon.svg rename to qflowcrate/icon.svg diff --git a/plugin/metadata.txt b/qflowcrate/metadata.txt similarity index 100% rename from plugin/metadata.txt rename to qflowcrate/metadata.txt diff --git a/scripts/create_release.sh b/scripts/create_release.sh index bcde1a5..f33568e 100755 --- a/scripts/create_release.sh +++ b/scripts/create_release.sh @@ -2,7 +2,7 @@ set -e VERSION=$1 -METADATA_FILE="plugin/metadata.txt" +METADATA_FILE="qflowcrate/metadata.txt" # Show help if no version provided if [ -z "$VERSION" ]; then diff --git a/tests/test_export_tab.py b/tests/test_export_tab.py index d64bd03..ade0cf2 100644 --- a/tests/test_export_tab.py +++ b/tests/test_export_tab.py @@ -11,7 +11,7 @@ QVBoxLayout, ) -from plugin.Plugin.Export.export_tab import ExportTab +from qflowcrate.Plugin.Export.export_tab import ExportTab # ============================================================================ # FIXTURES diff --git a/tests/test_graph_tab.py b/tests/test_graph_tab.py index bae52f1..ca4c30e 100644 --- a/tests/test_graph_tab.py +++ b/tests/test_graph_tab.py @@ -8,7 +8,7 @@ QVBoxLayout ) -from plugin.Plugin.Graph.graph_tab import GraphTab +from qflowcrate.Plugin.Graph.graph_tab import GraphTab # ========================================================================== # FIXTURES diff --git a/tests/test_instruction_tab.py b/tests/test_instruction_tab.py index 4a278eb..46c9aec 100644 --- a/tests/test_instruction_tab.py +++ b/tests/test_instruction_tab.py @@ -6,7 +6,7 @@ QScrollArea, QVBoxLayout, ) -from plugin.Plugin.Instruction.instruction_tab import InstructionTab +from qflowcrate.Plugin.Instruction.instruction_tab import InstructionTab # ============================================================================ # FIXTURES