diff --git a/EasyReflectometryApp/Backends/Mock/Plotting.qml b/EasyReflectometryApp/Backends/Mock/Plotting.qml index 537eb221..57e49717 100644 --- a/EasyReflectometryApp/Backends/Mock/Plotting.qml +++ b/EasyReflectometryApp/Backends/Mock/Plotting.qml @@ -23,7 +23,22 @@ QtObject { property int modelCount: 1 + // Plot mode properties + property bool plotRQ4: false + property string yMainAxisTitle: 'R(q)' + property bool xAxisLog: false + property string xAxisType: 'linear' + property bool sldXDataReversed: false + property bool scaleShown: false + property bool bkgShown: false + + // Signals for plot mode changes + signal plotModeChanged() + signal axisTypeChanged() + signal sldAxisReversedChanged() + signal referenceLineVisibilityChanged() signal samplePageDataChanged() + signal samplePageResetAxes() function setQtChartsSerieRef(value1, value2, value3) { console.debug(`setQtChartsSerieRef ${value1}, ${value2}, ${value3}`) @@ -52,4 +67,70 @@ QtObject { return '#0000FF' } + // Plot mode toggle functions + function togglePlotRQ4() { + plotRQ4 = !plotRQ4 + yMainAxisTitle = plotRQ4 ? 'R(q)×q⁴' : 'R(q)' + plotModeChanged() + } + + function toggleXAxisType() { + xAxisLog = !xAxisLog + xAxisType = xAxisLog ? 'log' : 'linear' + axisTypeChanged() + } + + function reverseSldXData() { + sldXDataReversed = !sldXDataReversed + sldAxisReversedChanged() + } + + function flipScaleShown() { + scaleShown = !scaleShown + referenceLineVisibilityChanged() + } + + function flipBkgShown() { + bkgShown = !bkgShown + referenceLineVisibilityChanged() + } + + // Reference line data accessors (mock implementation) + function getBackgroundData() { + if (!bkgShown) return [] + // Return mock horizontal line at background level + return [ + { 'x': 0.01, 'y': -7.0 }, + { 'x': 0.30, 'y': -7.0 } + ] + } + + function getScaleData() { + if (!scaleShown) return [] + // Return mock horizontal line at scale level (log10(1.0) = 0) + return [ + { 'x': 0.01, 'y': 0.0 }, + { 'x': 0.30, 'y': 0.0 } + ] + } + + // Analysis-specific reference line data accessors (use sample/calculated x-range) + function getBackgroundDataForAnalysis() { + if (!bkgShown) return [] + // Return mock horizontal line at background level using sample x-range + return [ + { 'x': sampleMinX, 'y': -7.0 }, + { 'x': sampleMaxX, 'y': -7.0 } + ] + } + + function getScaleDataForAnalysis() { + if (!scaleShown) return [] + // Return mock horizontal line at scale level using sample x-range + return [ + { 'x': sampleMinX, 'y': 0.0 }, + { 'x': sampleMaxX, 'y': 0.0 } + ] + } + } diff --git a/EasyReflectometryApp/Backends/Py/analysis.py b/EasyReflectometryApp/Backends/Py/analysis.py index b14d351d..0d3049bb 100644 --- a/EasyReflectometryApp/Backends/Py/analysis.py +++ b/EasyReflectometryApp/Backends/Py/analysis.py @@ -292,7 +292,8 @@ def modelNamesForExperiment(self) -> list: mapped_models = [] experiments = self._experiments_logic._project_lib._experiments for ind in experiments: - mapped_models.append(experiments[ind].model.name) + name = experiments[ind].model.user_data.get('original_name', experiments[ind].model.name) + mapped_models.append(name) return mapped_models @Property('QVariantList', notify=experimentsChanged) diff --git a/EasyReflectometryApp/Backends/Py/logic/models.py b/EasyReflectometryApp/Backends/Py/logic/models.py index cf2777d2..0dee4dea 100644 --- a/EasyReflectometryApp/Backends/Py/logic/models.py +++ b/EasyReflectometryApp/Backends/Py/logic/models.py @@ -21,7 +21,10 @@ def index(self, new_value: Union[int, str]) -> None: @property def name_at_current_index(self) -> str: - return self._models[self.index].name + if self._models[self.index].user_data.get('original_name'): + return self._models[self.index].user_data['original_name'] + else: + return self._models[self.index].name @property def scaling_at_current_index(self) -> float: @@ -128,7 +131,7 @@ def _from_models_collection_to_list_of_dicts(models_collection: ModelCollection) for model in models_collection: models_list.append( { - 'label': model.name, + 'label': model.user_data.get('original_name', model.name), # Use original name if available 'color': str(model.color), } ) diff --git a/EasyReflectometryApp/Backends/Py/logic/parameters.py b/EasyReflectometryApp/Backends/Py/logic/parameters.py index bc36f539..2eef21ad 100644 --- a/EasyReflectometryApp/Backends/Py/logic/parameters.py +++ b/EasyReflectometryApp/Backends/Py/logic/parameters.py @@ -226,7 +226,7 @@ def _is_layer_parameter(param: Parameter) -> bool: # Process parameters for each model for model_idx, model in enumerate(models): model_unique_name = model.unique_name - model_prefix = f'M{model_idx + 1}' + model_prefix = model.user_data.get('original_name', model.name) for parameter in parameters: # Skip parameters not in this model's path diff --git a/EasyReflectometryApp/Backends/Py/logic/project.py b/EasyReflectometryApp/Backends/Py/logic/project.py index adf8976c..47ee5be7 100644 --- a/EasyReflectometryApp/Backends/Py/logic/project.py +++ b/EasyReflectometryApp/Backends/Py/logic/project.py @@ -122,6 +122,11 @@ def add_sample_from_orso(self, sample) -> None: new_model_index = len(self._project_lib.models) - 1 self._update_enablement_of_fixed_layers_for_model(new_model_index) + def replace_models_from_orso(self, sample) -> None: + """Replace all existing models with a single model built from the loaded sample.""" + self._project_lib.replace_models_from_orso(sample) + self._update_enablement_of_fixed_layers_for_model(0) + def reset(self) -> None: self._project_lib.reset() self._project_lib.default_model() diff --git a/EasyReflectometryApp/Backends/Py/plotting_1d.py b/EasyReflectometryApp/Backends/Py/plotting_1d.py index e06fdf6a..f376564a 100644 --- a/EasyReflectometryApp/Backends/Py/plotting_1d.py +++ b/EasyReflectometryApp/Backends/Py/plotting_1d.py @@ -19,6 +19,13 @@ class Plotting1d(QObject): experimentChartRangesChanged = Signal() experimentDataChanged = Signal() samplePageDataChanged = Signal() # Signal for QML to refresh sample page charts + samplePageResetAxes = Signal() # Signal for QML to reset chart axes after data load + + # New signals for plot mode properties + plotModeChanged = Signal() + axisTypeChanged = Signal() + sldAxisReversedChanged = Signal() + referenceLineVisibilityChanged = Signal() def __init__(self, project_lib: ProjectLib, parent=None): super().__init__(parent) @@ -27,6 +34,13 @@ def __init__(self, project_lib: ProjectLib, parent=None): self._currentLib1d = 'QtCharts' self._sample_data = {} self._sld_data = {} + + # Plot mode state + self._plot_rq4 = False + self._x_axis_log = False + self._sld_x_reversed = False + self._scale_shown = False + self._bkg_shown = False self._chartRefs = { 'QtCharts': { 'samplePage': { @@ -51,6 +65,191 @@ def reset_data(self): self._sld_data = {} console.debug(IO.formatMsg('sub', 'Sample and SLD data cleared')) + # R(q)×q⁴ mode + @Property(bool, notify=plotModeChanged) + def plotRQ4(self) -> bool: + """Return whether R(q)×q⁴ mode is enabled.""" + return self._plot_rq4 + + @Slot() + def togglePlotRQ4(self) -> None: + """Toggle R(q)×q⁴ plotting mode.""" + self._plot_rq4 = not self._plot_rq4 + self.plotModeChanged.emit() + # Refresh all charts with new mode + self.sampleChartRangesChanged.emit() + self.experimentChartRangesChanged.emit() + self.samplePageDataChanged.emit() + + @Property(str, notify=plotModeChanged) + def yMainAxisTitle(self) -> str: + """Return Y-axis title based on current plot mode.""" + return 'R(q)×q⁴' if self._plot_rq4 else 'R(q)' + + # X-axis type (log/linear) + @Property(bool, notify=axisTypeChanged) + def xAxisLog(self) -> bool: + """Return whether X-axis is logarithmic.""" + return self._x_axis_log + + @Slot() + def toggleXAxisType(self) -> None: + """Toggle between linear and logarithmic X-axis.""" + self._x_axis_log = not self._x_axis_log + self.axisTypeChanged.emit() + + @Property(str, notify=axisTypeChanged) + def xAxisType(self) -> str: + """Return X-axis type as string for QML.""" + return 'log' if self._x_axis_log else 'linear' + + # SLD X-axis reversal + @Property(bool, notify=sldAxisReversedChanged) + def sldXDataReversed(self) -> bool: + """Return whether SLD X-axis is reversed.""" + return self._sld_x_reversed + + @Slot() + def reverseSldXData(self) -> None: + """Toggle SLD X-axis reversal.""" + self._sld_x_reversed = not self._sld_x_reversed + self.sldAxisReversedChanged.emit() + self.sldChartRangesChanged.emit() + + # Reference line visibility + @Property(bool, notify=referenceLineVisibilityChanged) + def scaleShown(self) -> bool: + """Return whether scale reference line is shown.""" + return self._scale_shown + + @Slot() + def flipScaleShown(self) -> None: + """Toggle scale line visibility.""" + self._scale_shown = not self._scale_shown + self.referenceLineVisibilityChanged.emit() + + @Property(bool, notify=referenceLineVisibilityChanged) + def bkgShown(self) -> bool: + """Return whether background reference line is shown.""" + return self._bkg_shown + + @Slot() + def flipBkgShown(self) -> None: + """Toggle background line visibility.""" + self._bkg_shown = not self._bkg_shown + self.referenceLineVisibilityChanged.emit() + + @Slot(result='QVariantList') + def getBackgroundData(self) -> list: + """Return background reference line data for plotting. + + Returns a horizontal line at the model's background value. + Note: Reference lines are always horizontal, even in R×q⁴ mode. + """ + if not self._bkg_shown: + return [] + try: + # Capture indices atomically to prevent race conditions + model_idx = self._project_lib.current_model_index + exp_idx = self._project_lib.current_experiment_index + model = self._project_lib.models[model_idx] + exp_data = self._project_lib.experimental_data_for_model_at_index(exp_idx) + if exp_data.x is None or len(exp_data.x) == 0: + return [] + x = exp_data.x + bkg_value = model.background.value + # For log scale plotting, convert background value + bkg_log = float(np.log10(bkg_value)) if bkg_value > 0 else -10.0 + # Reference lines are always horizontal (no R×q⁴ transformation) + return [{'x': float(x[0]), 'y': bkg_log}, {'x': float(x[-1]), 'y': bkg_log}] + except (IndexError, AttributeError, TypeError) as e: + console.debug(f'Error getting background data: {e}') + return [] + + @Slot(result='QVariantList') + def getScaleData(self) -> list: + """Return scale reference line data for plotting. + + Returns a horizontal line at the model's scale value. + Note: Scale is a multiplicative factor, typically close to 1.0. + For reflectometry plots, the scale line at y=scale (log10) shows + where R=scale, i.e., where the reflectivity equals the scale factor. + Reference lines are always horizontal, even in R×q⁴ mode. + """ + if not self._scale_shown: + return [] + try: + # Capture indices atomically to prevent race conditions + model_idx = self._project_lib.current_model_index + exp_idx = self._project_lib.current_experiment_index + model = self._project_lib.models[model_idx] + exp_data = self._project_lib.experimental_data_for_model_at_index(exp_idx) + if exp_data.x is None or len(exp_data.x) == 0: + return [] + x = exp_data.x + scale_value = model.scale.value + # For log scale plotting, convert scale value + scale_log = float(np.log10(scale_value)) if scale_value > 0 else 0.0 + # Reference lines are always horizontal (no R×q⁴ transformation) + return [{'x': float(x[0]), 'y': scale_log}, {'x': float(x[-1]), 'y': scale_log}] + except (IndexError, AttributeError, TypeError) as e: + console.debug(f'Error getting scale data: {e}') + return [] + + @Slot(result='QVariantList') + def getBackgroundDataForAnalysis(self) -> list: + """Return background reference line data for the Analysis chart. + + Uses the analysis/sample x-range (calculated model data range) instead of + experimental data range to ensure the line spans the full chart width. + Reference lines are always horizontal, even in R×q⁴ mode. + """ + if not self._bkg_shown: + return [] + try: + # Capture index atomically to prevent race conditions + model_idx = self._project_lib.current_model_index + model = self._project_lib.models[model_idx] + # Use sample/analysis x-range instead of experimental data + x_min, x_max = self._get_all_models_sample_range()[0:2] + if x_min == float('inf') or x_max == float('-inf'): + return [] + bkg_value = model.background.value + # For log scale plotting, convert background value + bkg_log = float(np.log10(bkg_value)) if bkg_value > 0 else -10.0 + # Reference lines are always horizontal (no R×q⁴ transformation) + return [{'x': float(x_min), 'y': bkg_log}, {'x': float(x_max), 'y': bkg_log}] + except (IndexError, AttributeError, TypeError) as e: + console.debug(f'Error getting background data for analysis: {e}') + return [] + + @Slot(result='QVariantList') + def getScaleDataForAnalysis(self) -> list: + """Return scale reference line data for the Analysis chart. + + Uses the analysis/sample x-range (calculated model data range) instead of + experimental data range to ensure the line spans the full chart width. + Reference lines are always horizontal, even in R×q⁴ mode. + """ + if not self._scale_shown: + return [] + try: + # Capture index atomically to prevent race conditions + model_idx = self._project_lib.current_model_index + model = self._project_lib.models[model_idx] + # Use sample/analysis x-range instead of experimental data + x_min, x_max = self._get_all_models_sample_range()[0:2] + if x_min == float('inf') or x_max == float('-inf'): + return [] + scale_value = model.scale.value + # For log scale plotting, convert scale value + scale_log = float(np.log10(scale_value)) if scale_value > 0 else 0.0 + # Reference lines are always horizontal (no R×q⁴ transformation) + return [{'x': float(x_min), 'y': scale_log}, {'x': float(x_max), 'y': scale_log}] + except (IndexError, AttributeError, TypeError) as e: + console.debug(f'Error getting scale data for analysis: {e}') + return [] + @property def sample_data(self) -> DataSet1D: idx = self._project_lib.current_model_index @@ -154,8 +353,13 @@ def _get_all_models_sample_range(self): min_x = min(min_x, data.x.min()) max_x = max(max_x, data.x.max()) if data.y.size > 0: - valid_y = data.y[data.y > 0] + valid_mask = data.y > 0 + valid_y = data.y[valid_mask] + valid_x = data.x[valid_mask] if valid_y.size > 0: + # Apply R×q⁴ transformation if enabled + if self._plot_rq4: + valid_y = valid_y * (valid_x**4) min_y = min(min_y, np.log10(valid_y.min())) max_y = max(max_y, np.log10(valid_y.max())) except (IndexError, ValueError): @@ -233,13 +437,29 @@ def experimentMinX(self): @Property(float, notify=experimentChartRangesChanged) def experimentMaxY(self): data = self.experiment_data - return np.log10(data.y.max()) if data.y.size > 0 else 1.0 + if data.y.size == 0: + return 1.0 + y_values = data.y + # Apply R×q⁴ transformation if enabled + if self._plot_rq4: + y_values = y_values * (data.x**4) + return np.log10(y_values.max()) @Property(float, notify=experimentChartRangesChanged) def experimentMinY(self): data = self.experiment_data valid_y = data.y[data.y > 0] if data.y.size > 0 else np.array([1e-10]) - return np.log10(valid_y.min()) if valid_y.size > 0 else -10.0 + if valid_y.size == 0: + return -10.0 + # Apply R×q⁴ transformation if enabled + if self._plot_rq4: + valid_x = data.x[data.y > 0] if data.y.size > 0 else np.array([1.0]) + valid_y = valid_y * (valid_x**4) + # Filter again after transformation to avoid log of zero/negative + valid_y = valid_y[valid_y > 0] + if valid_y.size == 0: + return -10.0 + return np.log10(valid_y.min()) @Property('QVariant', notify=chartRefsChanged) def chartRefs(self): @@ -284,7 +504,13 @@ def getSampleDataPointsForModel(self, model_index: int) -> list: data = self._project_lib.sample_data_for_model_at_index(model_index) points = [] for point in data.data_points(): - points.append({'x': float(point[0]), 'y': float(np.log10(point[1])) if point[1] > 0 else -10.0}) + x_val = float(point[0]) + y_val = float(point[1]) + # Apply R×q⁴ transformation if enabled + if self._plot_rq4 and y_val > 0: + y_val = y_val * (x_val**4) + y_log = float(np.log10(y_val)) if y_val > 0 else -10.0 + points.append({'x': x_val, 'y': y_log}) return points except Exception as e: console.debug(f'Error getting sample data points for model {model_index}: {e}') @@ -324,12 +550,27 @@ def getExperimentDataPoints(self, experiment_index: int) -> list: points = [] for point in data.data_points(): if point[0] < self._project_lib.q_max and self._project_lib.q_min < point[0]: + q = point[0] + r = point[1] + error_var = point[2] + # Apply R×q⁴ transformation if enabled + # Clamp error_lower before transformation to ensure positive values + error_lower_linear = max(r - np.sqrt(error_var), 1e-20) + if self._plot_rq4: + q4 = q**4 + r_val = r * q4 + error_upper = (r + np.sqrt(error_var)) * q4 + error_lower = error_lower_linear * q4 + else: + r_val = r + error_upper = r + np.sqrt(error_var) + error_lower = error_lower_linear points.append( { - 'x': float(point[0]), - 'y': float(np.log10(point[1])), - 'errorUpper': float(np.log10(point[1] + np.sqrt(point[2]))), - 'errorLower': float(np.log10(max(point[1] - np.sqrt(point[2]), 1e-10))), # Avoid log(0) + 'x': float(q), + 'y': float(np.log10(r_val)), + 'errorUpper': float(np.log10(error_upper)), + 'errorLower': float(np.log10(error_lower)), } ) return points @@ -373,11 +614,18 @@ def getAnalysisDataPoints(self, experiment_index: int) -> list: calc_idx = 0 for point in exp_points: if point[0] < self._project_lib.q_max and self._project_lib.q_min < point[0]: - calc_y_val = calc_y[calc_idx] if calc_idx < len(calc_y) else point[1] + q = point[0] + r_meas = point[1] + calc_y_val = calc_y[calc_idx] if calc_idx < len(calc_y) else r_meas + # Apply R×q⁴ transformation if enabled + if self._plot_rq4: + q4 = q**4 + r_meas = r_meas * q4 + calc_y_val = calc_y_val * q4 points.append( { - 'x': float(point[0]), - 'measured': float(np.log10(point[1])), + 'x': float(q), + 'measured': float(np.log10(r_meas)), 'calculated': float(np.log10(calc_y_val)), } ) @@ -446,6 +694,7 @@ def qtchartsReplaceCalculatedOnSldChartAndRedraw(self): nr_points = nr_points + 1 console.debug(IO.formatMsg('sub', 'Sld curve', f'{nr_points} points', 'on analysis page', 'replaced')) + @Slot() def drawMeasuredOnExperimentChart(self): if PLOT_BACKEND == 'QtCharts': if self.is_multi_experiment_mode: @@ -463,9 +712,24 @@ def qtchartsReplaceMeasuredOnExperimentChartAndRedraw(self): nr_points = 0 for point in self.experiment_data.data_points(): if point[0] < self._project_lib.q_max and self._project_lib.q_min < point[0]: - series_measured.append(point[0], np.log10(point[1])) - series_error_upper.append(point[0], np.log10(point[1] + np.sqrt(point[2]))) - series_error_lower.append(point[0], np.log10(max(point[1] - np.sqrt(point[2]), 1e-10))) + q = point[0] + r = point[1] + error_var = point[2] + # Apply R×q⁴ transformation if enabled + # Clamp error_lower before transformation to ensure positive values + error_lower_linear = max(r - np.sqrt(error_var), 1e-20) + if self._plot_rq4: + q4 = q**4 + r_val = r * q4 + error_upper = (r + np.sqrt(error_var)) * q4 + error_lower = error_lower_linear * q4 + else: + r_val = r + error_upper = r + np.sqrt(error_var) + error_lower = error_lower_linear + series_measured.append(q, np.log10(r_val)) + series_error_upper.append(q, np.log10(error_upper)) + series_error_lower.append(q, np.log10(error_lower)) nr_points = nr_points + 1 console.debug(IO.formatMsg('sub', 'Measured curve', f'{nr_points} points', 'on experiment page', 'replaced')) @@ -486,6 +750,7 @@ def qtchartsReplaceMultiExperimentChartAndRedraw(self): # This method is called to trigger the refresh, actual drawing is handled by QML self.experimentDataChanged.emit() + @Slot() def drawCalculatedAndMeasuredOnAnalysisChart(self): if PLOT_BACKEND == 'QtCharts': if self.is_multi_experiment_mode: @@ -515,11 +780,21 @@ def qtchartsReplaceCalculatedAndMeasuredOnAnalysisChartAndRedraw(self): nr_points = 0 for point in self.experiment_data.data_points(): if point[0] < self._project_lib.q_max and self._project_lib.q_min < point[0]: - series_measured.append(point[0], np.log10(point[1])) + q = point[0] + r_meas = point[1] + # Apply R×q⁴ transformation if enabled + if self._plot_rq4: + r_meas = r_meas * (q**4) + series_measured.append(q, np.log10(r_meas)) nr_points = nr_points + 1 - console.debug(IO.formatMsg('sub', 'Measurede curve', f'{nr_points} points', 'on analysis page', 'replaced')) + console.debug(IO.formatMsg('sub', 'Measured curve', f'{nr_points} points', 'on analysis page', 'replaced')) for point in self.sample_data.data_points(): - series_calculated.append(point[0], np.log10(point[1])) + q = point[0] + r_calc = point[1] + # Apply R×q⁴ transformation if enabled + if self._plot_rq4: + r_calc = r_calc * (q**4) + series_calculated.append(q, np.log10(r_calc)) nr_points = nr_points + 1 console.debug(IO.formatMsg('sub', 'Calculated curve', f'{nr_points} points', 'on analysis page', 'replaced')) diff --git a/EasyReflectometryApp/Backends/Py/project.py b/EasyReflectometryApp/Backends/Py/project.py index 5217c0df..4fba7149 100644 --- a/EasyReflectometryApp/Backends/Py/project.py +++ b/EasyReflectometryApp/Backends/Py/project.py @@ -1,3 +1,5 @@ +import warnings + from EasyApp.Logic.Utils.Utils import generalizePath from easyreflectometry import Project as ProjectLib from easyreflectometry.orso_utils import load_orso_model @@ -20,6 +22,7 @@ class Project(QObject): externalNameChanged = Signal() externalProjectLoaded = Signal() externalProjectReset = Signal() + sampleLoadWarning = Signal(str) def __init__(self, project_lib: ProjectLib, parent=None): super().__init__(parent) @@ -104,13 +107,25 @@ def reset(self) -> None: self.externalNameChanged.emit() self.externalProjectReset.emit() - @Slot(str) - def sampleLoad(self, url: str) -> None: + @Slot(str, bool) + def sampleLoad(self, url: str, append: bool = True) -> None: # Load ORSO file content orso_data = orso.load_orso(generalizePath(url)) # Load the sample model - sample = load_orso_model(orso_data) - # Add the sample as a new model in the project - self._logic.add_sample_from_orso(sample) + with warnings.catch_warnings(record=True) as caught_warnings: + warnings.simplefilter('always') + sample = load_orso_model(orso_data) + if sample is None: + warning_msg = 'The ORSO file does not contain a valid sample model definition. No sample was loaded.' + for w in caught_warnings: + warning_msg = str(w.message) + self.sampleLoadWarning.emit(warning_msg) + return + if append: + # Add the sample as a new model in the project + self._logic.add_sample_from_orso(sample) + else: + # Replace all existing models with the loaded sample + self._logic.replace_models_from_orso(sample) # notify listeners self.externalProjectLoaded.emit() diff --git a/EasyReflectometryApp/Backends/Py/py_backend.py b/EasyReflectometryApp/Backends/Py/py_backend.py index ce924fe8..efd5da50 100644 --- a/EasyReflectometryApp/Backends/Py/py_backend.py +++ b/EasyReflectometryApp/Backends/Py/py_backend.py @@ -179,6 +179,7 @@ def _relay_project_page_created(self): def _relay_project_page_project_changed(self): self._sample.materialsTableChanged.emit() self._sample.modelsTableChanged.emit() + self._sample.modelsIndexChanged.emit() self._sample.assembliesTableChanged.emit() self._sample._clearCacheAndEmitLayersChanged() self._experiment.experimentChanged.emit() @@ -188,6 +189,7 @@ def _relay_project_page_project_changed(self): self._summary.summaryChanged.emit() self._plotting_1d.reset_data() self._refresh_plots() + self._plotting_1d.samplePageResetAxes.emit() def _relay_sample_page_sample_changed(self): self._plotting_1d.reset_data() diff --git a/EasyReflectometryApp/Gui/Globals/BackendWrapper.qml b/EasyReflectometryApp/Gui/Globals/BackendWrapper.qml index 15e23f18..b8479c89 100644 --- a/EasyReflectometryApp/Gui/Globals/BackendWrapper.qml +++ b/EasyReflectometryApp/Gui/Globals/BackendWrapper.qml @@ -78,7 +78,17 @@ QtObject { function projectReset() { activeBackend.project.reset() } function projectSave() { activeBackend.project.save() } function projectLoad(value) { activeBackend.project.load(value) } - function sampleFileLoad(value) { activeBackend.project.sampleLoad(value) } + function sampleFileLoad(value, append) { activeBackend.project.sampleLoad(value, append) } + + // Sample load warning signal - forwarded from backend + signal sampleLoadWarning(string message) + + property var _sampleLoadWarningConnection: { + if (activeBackend && activeBackend.project && activeBackend.project.sampleLoadWarning) { + activeBackend.project.sampleLoadWarning.connect(sampleLoadWarning) + } + return null + } /////////////// @@ -328,9 +338,61 @@ QtObject { readonly property var plottingAnalysisMaxY: activeBackend.plotting.sampleMaxY readonly property var calcSerieColor: activeBackend.plotting.calcSerieColor + // Plot mode properties + readonly property bool plottingPlotRQ4: activeBackend.plotting.plotRQ4 + readonly property string plottingYAxisTitle: activeBackend.plotting.yMainAxisTitle + readonly property bool plottingXAxisLog: activeBackend.plotting.xAxisLog + readonly property string plottingXAxisType: activeBackend.plotting.xAxisType + readonly property bool plottingSldXReversed: activeBackend.plotting.sldXDataReversed + + // Reference line visibility + readonly property bool plottingScaleShown: activeBackend.plotting.scaleShown + readonly property bool plottingBkgShown: activeBackend.plotting.bkgShown + + // Plot mode toggle functions + function plottingTogglePlotRQ4() { activeBackend.plotting.togglePlotRQ4() } + function plottingToggleXAxisType() { activeBackend.plotting.toggleXAxisType() } + function plottingReverseSldXData() { activeBackend.plotting.reverseSldXData() } + function plottingFlipScaleShown() { activeBackend.plotting.flipScaleShown() } + function plottingFlipBkgShown() { activeBackend.plotting.flipBkgShown() } + + // Reference line data accessors + function plottingGetBackgroundData() { + try { + return activeBackend.plotting.getBackgroundData() + } catch (e) { + return [] + } + } + function plottingGetScaleData() { + try { + return activeBackend.plotting.getScaleData() + } catch (e) { + return [] + } + } + + // Analysis-specific reference line data accessors (use sample/calculated x-range) + function plottingGetBackgroundDataForAnalysis() { + try { + return activeBackend.plotting.getBackgroundDataForAnalysis() + } catch (e) { + return [] + } + } + function plottingGetScaleDataForAnalysis() { + try { + return activeBackend.plotting.getScaleDataForAnalysis() + } catch (e) { + return [] + } + } + function plottingSetQtChartsSerieRef(value1, value2, value3) { activeBackend.plotting.setQtChartsSerieRef(value1, value2, value3) } function plottingRefreshSample() { activeBackend.plotting.drawCalculatedOnSampleChart() } function plottingRefreshSLD() { activeBackend.plotting.drawCalculatedOnSldChart() } + function plottingRefreshExperiment() { activeBackend.plotting.drawMeasuredOnExperimentChart() } + function plottingRefreshAnalysis() { activeBackend.plotting.drawCalculatedAndMeasuredOnAnalysisChart() } // Multi-model sample page plotting support readonly property int plottingModelCount: activeBackend.plotting.modelCount @@ -358,12 +420,27 @@ QtObject { // Signal for sample page data changes - forward from backend signal samplePageDataChanged() + // Signal for resetting chart axes after data load + signal samplePageResetAxes() + // Signal for plot mode changes - forward from backend + signal plotModeChanged() + // Signal to request QML to reset chart axes (e.g., after model load) + signal chartAxesResetRequested() // Connect to backend signal (called from Component.onCompleted in QML items) function connectSamplePageDataChanged() { if (activeBackend && activeBackend.plotting && activeBackend.plotting.samplePageDataChanged) { activeBackend.plotting.samplePageDataChanged.connect(samplePageDataChanged) } + if (activeBackend && activeBackend.plotting && activeBackend.plotting.samplePageResetAxes) { + activeBackend.plotting.samplePageResetAxes.connect(samplePageResetAxes) + } + if (activeBackend && activeBackend.plotting && activeBackend.plotting.plotModeChanged) { + activeBackend.plotting.plotModeChanged.connect(plotModeChanged) + } + if (activeBackend && activeBackend.plotting && activeBackend.plotting.chartAxesResetRequested) { + activeBackend.plotting.chartAxesResetRequested.connect(chartAxesResetRequested) + } } Component.onCompleted: { diff --git a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/AnalysisView.qml b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/AnalysisView.qml index 0baf81cd..13a686dd 100644 --- a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/AnalysisView.qml +++ b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/AnalysisView.qml @@ -31,6 +31,59 @@ Rectangle { useOpenGL: EaGlobals.Vars.useOpenGL + // Background reference line series + LineSeries { + id: backgroundRefLine + axisX: chartView.axisX + axisY: chartView.axisY + useOpenGL: chartView.useOpenGL + color: "#888888" + width: 1 + style: Qt.DashLine + visible: Globals.BackendWrapper.plottingBkgShown + } + + // Scale reference line series + LineSeries { + id: scaleRefLine + axisX: chartView.axisX + axisY: chartView.axisY + useOpenGL: chartView.useOpenGL + color: "#666666" + width: 1 + style: Qt.DotLine + visible: Globals.BackendWrapper.plottingScaleShown + } + + // Update reference lines when visibility changes + Connections { + target: Globals.BackendWrapper.activeBackend?.plotting ?? null + enabled: target !== null + function onReferenceLineVisibilityChanged() { + chartView.updateReferenceLines() + } + } + + function updateReferenceLines() { + // Update background line (use analysis-specific method for correct x-range) + backgroundRefLine.clear() + if (Globals.BackendWrapper.plottingBkgShown) { + var bkgData = Globals.BackendWrapper.plottingGetBackgroundDataForAnalysis() + for (var i = 0; i < bkgData.length; i++) { + backgroundRefLine.append(bkgData[i].x, bkgData[i].y) + } + } + + // Update scale line (use analysis-specific method for correct x-range) + scaleRefLine.clear() + if (Globals.BackendWrapper.plottingScaleShown) { + var scaleData = Globals.BackendWrapper.plottingGetScaleDataForAnalysis() + for (var j = 0; j < scaleData.length; j++) { + scaleRefLine.append(scaleData[j].x, scaleData[j].y) + } + } + } + // Multi-experiment support property var multiExperimentSeries: [] property bool isMultiExperimentMode: { @@ -49,12 +102,35 @@ Rectangle { // Watch for changes in multi-experiment selection Connections { - target: Globals.BackendWrapper.activeBackend + target: Globals.BackendWrapper.activeBackend ?? null + enabled: target !== null function onMultiExperimentSelectionChanged() { console.log("Analysis: Multi-experiment selection changed - updating series") chartView.updateMultiExperimentSeries() } } + + // Watch for plot mode changes (R(q)×q⁴ toggle) + Connections { + target: Globals.BackendWrapper + function onPlotModeChanged() { + console.debug("AnalysisView: Plot mode changed, refreshing chart") + Globals.BackendWrapper.plottingRefreshAnalysis() + // Delay resetAxes to allow axis range properties to update first + analysisResetAxesTimer.start() + } + function onChartAxesResetRequested() { + // Reset axes when model is loaded (e.g., from ORSO file) + analysisResetAxesTimer.start() + } + } + + Timer { + id: analysisResetAxesTimer + interval: 50 + repeat: false + onTriggered: chartView.resetAxes() + } property double xRange: Globals.BackendWrapper.plottingAnalysisMaxX - Globals.BackendWrapper.plottingAnalysisMinX axisX.title: "q (Å⁻¹)" @@ -64,7 +140,7 @@ Rectangle { axisX.maxAfterReset: Globals.BackendWrapper.plottingAnalysisMaxX + xRange * 0.01 property double yRange: Globals.BackendWrapper.plottingAnalysisMaxY - Globals.BackendWrapper.plottingAnalysisMinY - axisY.title: "Log10 R(q)" + axisY.title: "Log10 " + Globals.BackendWrapper.plottingYAxisTitle axisY.min: Globals.BackendWrapper.plottingAnalysisMinY - yRange * 0.01 axisY.max: Globals.BackendWrapper.plottingAnalysisMaxY + yRange * 0.01 axisY.minAfterReset: Globals.BackendWrapper.plottingAnalysisMinY - yRange * 0.01 @@ -364,6 +440,9 @@ Rectangle { // Initialize multi-experiment support updateMultiExperimentSeries() + + // Initialize reference lines + updateReferenceLines() } // Update series when chart becomes visible @@ -371,6 +450,9 @@ Rectangle { if (visible && isMultiExperimentMode) { updateMultiExperimentSeries() } + if (visible) { + updateReferenceLines() + } } } diff --git a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml index 305fe01c..580d8368 100644 --- a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml +++ b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml @@ -63,12 +63,88 @@ Rectangle { // Watch for changes in multi-experiment selection Connections { - target: Globals.BackendWrapper.activeBackend + target: Globals.BackendWrapper.activeBackend ?? null + enabled: target !== null function onMultiExperimentSelectionChanged() { analysisChartView.updateMultiExperimentSeries() } } + // Watch for plot mode changes (R(q)×q⁴ toggle) + Connections { + target: Globals.BackendWrapper + function onPlotModeChanged() { + console.debug("CombinedView Analysis: Plot mode changed, refreshing chart") + Globals.BackendWrapper.plottingRefreshAnalysis() + // Delay resetAxes to allow axis range properties to update first + combinedAnalysisResetAxesTimer.start() + } + function onChartAxesResetRequested() { + // Reset axes when model is loaded (e.g., from ORSO file) + combinedAnalysisResetAxesTimer.start() + } + } + + Timer { + id: combinedAnalysisResetAxesTimer + interval: 50 + repeat: false + onTriggered: analysisChartView.resetAxes() + } + + // Background reference line series + LineSeries { + id: backgroundRefLine + axisX: analysisChartView.axisX + axisY: analysisChartView.axisY + useOpenGL: analysisChartView.useOpenGL + color: "#888888" + width: 1 + style: Qt.DashLine + visible: Globals.BackendWrapper.plottingBkgShown + } + + // Scale reference line series + LineSeries { + id: scaleRefLine + axisX: analysisChartView.axisX + axisY: analysisChartView.axisY + useOpenGL: analysisChartView.useOpenGL + color: "#666666" + width: 1 + style: Qt.DotLine + visible: Globals.BackendWrapper.plottingScaleShown + } + + // Update reference lines when visibility changes + Connections { + target: Globals.BackendWrapper.activeBackend?.plotting ?? null + enabled: target !== null + function onReferenceLineVisibilityChanged() { + analysisChartView.updateReferenceLines() + } + } + + function updateReferenceLines() { + // Update background line (use analysis-specific method for correct x-range) + backgroundRefLine.clear() + if (Globals.BackendWrapper.plottingBkgShown) { + var bkgData = Globals.BackendWrapper.plottingGetBackgroundDataForAnalysis() + for (var i = 0; i < bkgData.length; i++) { + backgroundRefLine.append(bkgData[i].x, bkgData[i].y) + } + } + + // Update scale line (use analysis-specific method for correct x-range) + scaleRefLine.clear() + if (Globals.BackendWrapper.plottingScaleShown) { + var scaleData = Globals.BackendWrapper.plottingGetScaleDataForAnalysis() + for (var j = 0; j < scaleData.length; j++) { + scaleRefLine.append(scaleData[j].x, scaleData[j].y) + } + } + } + // Multi-experiment series management function updateMultiExperimentSeries() { // Always get the latest value from backend @@ -183,7 +259,7 @@ Rectangle { axisX.maxAfterReset: Globals.BackendWrapper.plottingAnalysisMaxX + xRange * 0.01 property double yRange: Globals.BackendWrapper.plottingAnalysisMaxY - Globals.BackendWrapper.plottingAnalysisMinY - axisY.title: "Log10 R(q)" + axisY.title: "Log10 " + Globals.BackendWrapper.plottingYAxisTitle axisY.min: Globals.BackendWrapper.plottingAnalysisMinY - yRange * 0.01 axisY.max: Globals.BackendWrapper.plottingAnalysisMaxY + yRange * 0.01 axisY.minAfterReset: Globals.BackendWrapper.plottingAnalysisMinY - yRange * 0.01 @@ -381,6 +457,9 @@ Rectangle { // Initialize multi-experiment support updateMultiExperimentSeries() + + // Initialize reference lines + updateReferenceLines() } } } diff --git a/EasyReflectometryApp/Gui/Pages/Analysis/Sidebar/Advanced/Groups/PlotControl.qml b/EasyReflectometryApp/Gui/Pages/Analysis/Sidebar/Advanced/Groups/PlotControl.qml new file mode 100644 index 00000000..0b2e0ce2 --- /dev/null +++ b/EasyReflectometryApp/Gui/Pages/Analysis/Sidebar/Advanced/Groups/PlotControl.qml @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2025 EasyReflectometry contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2025 Contributors to the EasyReflectometry project + +import QtQuick +import QtQuick.Controls + +import EasyApp.Gui.Style as EaStyle +import EasyApp.Gui.Elements as EaElements + +import Gui.Globals as Globals + +EaElements.GroupBox { + title: qsTr("Plot control") + collapsed: true + + Column { + spacing: EaStyle.Sizes.fontPixelSize * 0.5 + + EaElements.CheckBox { + topPadding: 0 + checked: Globals.BackendWrapper.plottingPlotRQ4 + text: qsTr("Plot R(q)×q⁴") + ToolTip.text: qsTr("Checking this box will plot R(q) multiplied by q⁴") + onToggled: { + Globals.BackendWrapper.plottingTogglePlotRQ4() + } + } + + EaElements.CheckBox { + topPadding: 0 + checked: Globals.BackendWrapper.plottingScaleShown + text: qsTr("Show scale line") + ToolTip.text: qsTr("Checking this box will show the scale reference line on the plot") + onToggled: { + Globals.BackendWrapper.plottingFlipScaleShown() + } + } + + EaElements.CheckBox { + topPadding: 0 + checked: Globals.BackendWrapper.plottingBkgShown + text: qsTr("Show background line") + ToolTip.text: qsTr("Checking this box will show the background reference line on the plot") + onToggled: { + Globals.BackendWrapper.plottingFlipBkgShown() + } + } + } +} diff --git a/EasyReflectometryApp/Gui/Pages/Analysis/Sidebar/Advanced/Layout.qml b/EasyReflectometryApp/Gui/Pages/Analysis/Sidebar/Advanced/Layout.qml index 2571f4d9..deeee520 100644 --- a/EasyReflectometryApp/Gui/Pages/Analysis/Sidebar/Advanced/Layout.qml +++ b/EasyReflectometryApp/Gui/Pages/Analysis/Sidebar/Advanced/Layout.qml @@ -17,4 +17,6 @@ EaComponents.SideBarColumn { Groups.Calculator {} Groups.Minimizer {} + + Groups.PlotControl {} } diff --git a/EasyReflectometryApp/Gui/Pages/Experiment/Layout.qml b/EasyReflectometryApp/Gui/Pages/Experiment/Layout.qml index f56ede8a..7c6f1c87 100644 --- a/EasyReflectometryApp/Gui/Pages/Experiment/Layout.qml +++ b/EasyReflectometryApp/Gui/Pages/Experiment/Layout.qml @@ -22,13 +22,13 @@ EaComponents.ContentPage { sideBar: EaComponents.SideBar { tabs: [ - EaElements.TabButton { text: qsTr('Basic controls') } -// EaElements.TabButton { text: qsTr('Advanced controls') } + EaElements.TabButton { text: qsTr('Basic controls') }, + EaElements.TabButton { text: qsTr('Advanced controls') } ] items: [ - Loader { source: 'Sidebar/Basic/Layout.qml' } - // Loader { source: 'Sidebar/Advanced/Layout.qml' } + Loader { source: 'Sidebar/Basic/Layout.qml' }, + Loader { source: 'Sidebar/Advanced/Layout.qml' } ] continueButton.text: qsTr('Continue') diff --git a/EasyReflectometryApp/Gui/Pages/Experiment/MainContent/ExperimentView.qml b/EasyReflectometryApp/Gui/Pages/Experiment/MainContent/ExperimentView.qml index 905dc7c9..3bd40092 100644 --- a/EasyReflectometryApp/Gui/Pages/Experiment/MainContent/ExperimentView.qml +++ b/EasyReflectometryApp/Gui/Pages/Experiment/MainContent/ExperimentView.qml @@ -32,6 +32,59 @@ Rectangle { useOpenGL: EaGlobals.Vars.useOpenGL + // Background reference line series + LineSeries { + id: backgroundRefLine + axisX: chartView.axisX + axisY: chartView.axisY + useOpenGL: chartView.useOpenGL + color: "#888888" + width: 1 + style: Qt.DashLine + visible: Globals.BackendWrapper.plottingBkgShown + } + + // Scale reference line series + LineSeries { + id: scaleRefLine + axisX: chartView.axisX + axisY: chartView.axisY + useOpenGL: chartView.useOpenGL + color: "#666666" + width: 1 + style: Qt.DotLine + visible: Globals.BackendWrapper.plottingScaleShown + } + + // Update reference lines when visibility changes + Connections { + target: Globals.BackendWrapper.activeBackend?.plotting ?? null + enabled: target !== null + function onReferenceLineVisibilityChanged() { + chartView.updateReferenceLines() + } + } + + function updateReferenceLines() { + // Update background line + backgroundRefLine.clear() + if (Globals.BackendWrapper.plottingBkgShown) { + var bkgData = Globals.BackendWrapper.plottingGetBackgroundData() + for (var i = 0; i < bkgData.length; i++) { + backgroundRefLine.append(bkgData[i].x, bkgData[i].y) + } + } + + // Update scale line + scaleRefLine.clear() + if (Globals.BackendWrapper.plottingScaleShown) { + var scaleData = Globals.BackendWrapper.plottingGetScaleData() + for (var j = 0; j < scaleData.length; j++) { + scaleRefLine.append(scaleData[j].x, scaleData[j].y) + } + } + } + // Multi-experiment support property var multiExperimentSeries: [] property bool isMultiExperimentMode: { @@ -137,7 +190,8 @@ Rectangle { // Watch for changes in multi-experiment selection Connections { - target: Globals.BackendWrapper.activeBackend + target: Globals.BackendWrapper.activeBackend ?? null + enabled: target !== null function onMultiExperimentSelectionChanged() { // Update series when selection changes // The function will handle showing/hiding appropriate series @@ -145,6 +199,28 @@ Rectangle { chartView.updateMultiExperimentSeries() } } + + // Watch for plot mode changes (R(q)×q⁴ toggle) + Connections { + target: Globals.BackendWrapper + function onPlotModeChanged() { + console.debug("ExperimentView: Plot mode changed, refreshing chart") + Globals.BackendWrapper.plottingRefreshExperiment() + // Delay resetAxes to allow axis range properties to update first + experimentResetAxesTimer.start() + } + function onChartAxesResetRequested() { + // Reset axes when model is loaded (e.g., from ORSO file) + experimentResetAxesTimer.start() + } + } + + Timer { + id: experimentResetAxesTimer + interval: 50 + repeat: false + onTriggered: chartView.resetAxes() + } property double xRange: Globals.BackendWrapper.plottingExperimentMaxX - Globals.BackendWrapper.plottingExperimentMinX axisX.title: "q (Å⁻¹)" @@ -154,7 +230,7 @@ Rectangle { axisX.maxAfterReset: Globals.BackendWrapper.plottingExperimentMaxX + xRange * 0.01 property double yRange: Globals.BackendWrapper.plottingExperimentMaxY - Globals.BackendWrapper.plottingExperimentMinY - axisY.title: "Log10 R(q)" + axisY.title: "Log10 " + Globals.BackendWrapper.plottingYAxisTitle axisY.min: Globals.BackendWrapper.plottingExperimentMinY - yRange * 0.01 axisY.max: Globals.BackendWrapper.plottingExperimentMaxY + yRange * 0.01 axisY.minAfterReset: Globals.BackendWrapper.plottingExperimentMinY - yRange * 0.01 @@ -491,6 +567,9 @@ Rectangle { // Initialize multi-experiment support // console.log("ExperimentView initialized - checking multi-experiment mode...") updateMultiExperimentSeries() + + // Initialize reference lines + updateReferenceLines() } // Update series when chart becomes visible @@ -498,6 +577,9 @@ Rectangle { if (visible && isMultiExperimentMode) { updateMultiExperimentSeries() } + if (visible) { + updateReferenceLines() + } } } diff --git a/EasyReflectometryApp/Gui/Pages/Experiment/Sidebar/Advanced/Groups/PlotControl.qml b/EasyReflectometryApp/Gui/Pages/Experiment/Sidebar/Advanced/Groups/PlotControl.qml new file mode 100644 index 00000000..0b2e0ce2 --- /dev/null +++ b/EasyReflectometryApp/Gui/Pages/Experiment/Sidebar/Advanced/Groups/PlotControl.qml @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2025 EasyReflectometry contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2025 Contributors to the EasyReflectometry project + +import QtQuick +import QtQuick.Controls + +import EasyApp.Gui.Style as EaStyle +import EasyApp.Gui.Elements as EaElements + +import Gui.Globals as Globals + +EaElements.GroupBox { + title: qsTr("Plot control") + collapsed: true + + Column { + spacing: EaStyle.Sizes.fontPixelSize * 0.5 + + EaElements.CheckBox { + topPadding: 0 + checked: Globals.BackendWrapper.plottingPlotRQ4 + text: qsTr("Plot R(q)×q⁴") + ToolTip.text: qsTr("Checking this box will plot R(q) multiplied by q⁴") + onToggled: { + Globals.BackendWrapper.plottingTogglePlotRQ4() + } + } + + EaElements.CheckBox { + topPadding: 0 + checked: Globals.BackendWrapper.plottingScaleShown + text: qsTr("Show scale line") + ToolTip.text: qsTr("Checking this box will show the scale reference line on the plot") + onToggled: { + Globals.BackendWrapper.plottingFlipScaleShown() + } + } + + EaElements.CheckBox { + topPadding: 0 + checked: Globals.BackendWrapper.plottingBkgShown + text: qsTr("Show background line") + ToolTip.text: qsTr("Checking this box will show the background reference line on the plot") + onToggled: { + Globals.BackendWrapper.plottingFlipBkgShown() + } + } + } +} diff --git a/EasyReflectometryApp/Gui/Pages/Experiment/Sidebar/Advanced/Layout.qml b/EasyReflectometryApp/Gui/Pages/Experiment/Sidebar/Advanced/Layout.qml new file mode 100644 index 00000000..600ca675 --- /dev/null +++ b/EasyReflectometryApp/Gui/Pages/Experiment/Sidebar/Advanced/Layout.qml @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2025 EasyReflectometry contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2025 Contributors to the EasyReflectometry project + +import QtQuick + +import EasyApp.Gui.Elements as EaElements +import EasyApp.Gui.Components as EaComponents + +import "./Groups" as Groups + + +EaComponents.SideBarColumn { + + Groups.PlotControl {} + +} diff --git a/EasyReflectometryApp/Gui/Pages/Sample/MainContent/CombinedView.qml b/EasyReflectometryApp/Gui/Pages/Sample/MainContent/CombinedView.qml index fb2f6d4e..c86713f8 100644 --- a/EasyReflectometryApp/Gui/Pages/Sample/MainContent/CombinedView.qml +++ b/EasyReflectometryApp/Gui/Pages/Sample/MainContent/CombinedView.qml @@ -101,7 +101,7 @@ Rectangle { ValueAxis { id: sampleAxisY - titleText: "Log10 R(q)" + titleText: "Log10 " + Globals.BackendWrapper.plottingYAxisTitle // min/max set imperatively to avoid binding reset during zoom property double minAfterReset: Globals.BackendWrapper.plottingSampleMinY - sampleChartView.yRange * 0.01 property double maxAfterReset: Globals.BackendWrapper.plottingSampleMaxY + sampleChartView.yRange * 0.01 @@ -591,6 +591,36 @@ Rectangle { function onSamplePageDataChanged() { refreshAllCharts() } + function onSamplePageResetAxes() { + sampleCombinedResetAxesTimer.start() + sldCombinedResetAxesTimer.start() + } + function onPlotModeChanged() { + refreshAllCharts() + // Delay resetAxes to allow axis range properties to update first + sampleCombinedResetAxesTimer.start() + } + function onChartAxesResetRequested() { + // Reset axes when model is loaded (e.g., from ORSO file) + sampleCombinedResetAxesTimer.start() + } + } + + Timer { + id: sampleCombinedResetAxesTimer + interval: 50 + repeat: false + onTriggered: { + sampleChartView.resetAxes() + sldChartView.resetAxes() + } + } + + Timer { + id: sldCombinedResetAxesTimer + interval: 50 + repeat: false + onTriggered: sldChartView.resetAxes() } Component.onCompleted: { diff --git a/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SampleView.qml b/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SampleView.qml index 01645bfa..e492bbd4 100644 --- a/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SampleView.qml +++ b/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SampleView.qml @@ -87,7 +87,7 @@ Rectangle { ValueAxis { id: axisY - titleText: "Log10 R(q)" + titleText: "Log10 " + Globals.BackendWrapper.plottingYAxisTitle // min/max set imperatively to avoid binding reset during zoom property double minAfterReset: Globals.BackendWrapper.plottingSampleMinY - chartView.yRange * 0.01 property double maxAfterReset: Globals.BackendWrapper.plottingSampleMaxY + chartView.yRange * 0.01 @@ -347,6 +347,25 @@ Rectangle { function onSamplePageDataChanged() { refreshAllCharts() } + function onSamplePageResetAxes() { + sampleResetAxesTimer.start() + } + function onPlotModeChanged() { + refreshAllCharts() + // Delay resetAxes to allow axis range properties to update first + sampleResetAxesTimer.start() + } + function onChartAxesResetRequested() { + // Reset axes when model is loaded (e.g., from ORSO file) + sampleResetAxesTimer.start() + } + } + + Timer { + id: sampleResetAxesTimer + interval: 50 + repeat: false + onTriggered: chartView.resetAxes() } Component.onCompleted: { diff --git a/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SldView.qml b/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SldView.qml index e27d98f5..9c034fb1 100644 --- a/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SldView.qml +++ b/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SldView.qml @@ -317,6 +317,16 @@ Rectangle { function onSamplePageDataChanged() { refreshAllCharts() } + function onSamplePageResetAxes() { + sldResetAxesTimer.start() + } + } + + Timer { + id: sldResetAxesTimer + interval: 50 + repeat: false + onTriggered: chartView.resetAxes() } Component.onCompleted: { diff --git a/EasyReflectometryApp/Gui/Pages/Sample/Sidebar/Advanced/Groups/PlotControl.qml b/EasyReflectometryApp/Gui/Pages/Sample/Sidebar/Advanced/Groups/PlotControl.qml index f7d77177..b448193c 100644 --- a/EasyReflectometryApp/Gui/Pages/Sample/Sidebar/Advanced/Groups/PlotControl.qml +++ b/EasyReflectometryApp/Gui/Pages/Sample/Sidebar/Advanced/Groups/PlotControl.qml @@ -17,6 +17,16 @@ EaElements.GroupBox { Column { spacing: EaStyle.Sizes.fontPixelSize * 0.5 + EaElements.CheckBox { + topPadding: 0 + checked: Globals.BackendWrapper.plottingPlotRQ4 + text: qsTr("Plot R(q)×q⁴") + ToolTip.text: qsTr("Checking this box will plot R(q) multiplied by q⁴") + onToggled: { + Globals.BackendWrapper.plottingTogglePlotRQ4() + } + } + EaElements.CheckBox { topPadding: 0 checked: Globals.Variables.reverseSldZAxis diff --git a/EasyReflectometryApp/Gui/Pages/Sample/Sidebar/Basic/Groups/Assemblies/MultiLayer.qml b/EasyReflectometryApp/Gui/Pages/Sample/Sidebar/Basic/Groups/Assemblies/MultiLayer.qml index 213ab528..8697204f 100644 --- a/EasyReflectometryApp/Gui/Pages/Sample/Sidebar/Basic/Groups/Assemblies/MultiLayer.qml +++ b/EasyReflectometryApp/Gui/Pages/Sample/Sidebar/Basic/Groups/Assemblies/MultiLayer.qml @@ -78,6 +78,7 @@ EaElements.GroupColumn { horizontalAlignment: Text.AlignHCenter enabled: Globals.BackendWrapper.sampleLayers[index].thickness_enabled === "True" text: (isNaN(Globals.BackendWrapper.sampleLayers[index].thickness)) ? '--' : Number(Globals.BackendWrapper.sampleLayers[index].thickness).toFixed(2) + onActiveFocusChanged: if (activeFocus && Globals.BackendWrapper.sampleCurrentLayerIndex !== index) Globals.BackendWrapper.sampleSetCurrentLayerIndex(index) onEditingFinished: Globals.BackendWrapper.sampleSetCurrentLayerThickness(text) } @@ -85,6 +86,7 @@ EaElements.GroupColumn { horizontalAlignment: Text.AlignHCenter enabled: Globals.BackendWrapper.sampleLayers[index].roughness_enabled === "True" text: (isNaN(Globals.BackendWrapper.sampleLayers[index].roughness)) ? '--' : Number(Globals.BackendWrapper.sampleLayers[index].roughness).toFixed(2) + onActiveFocusChanged: if (activeFocus && Globals.BackendWrapper.sampleCurrentLayerIndex !== index) Globals.BackendWrapper.sampleSetCurrentLayerIndex(index) onEditingFinished: Globals.BackendWrapper.sampleSetCurrentLayerRoughness(text) } diff --git a/EasyReflectometryApp/Gui/Pages/Sample/Sidebar/Basic/Groups/Assemblies/SurfactantLayer.qml b/EasyReflectometryApp/Gui/Pages/Sample/Sidebar/Basic/Groups/Assemblies/SurfactantLayer.qml index dd33ea8d..231e02f1 100644 --- a/EasyReflectometryApp/Gui/Pages/Sample/Sidebar/Basic/Groups/Assemblies/SurfactantLayer.qml +++ b/EasyReflectometryApp/Gui/Pages/Sample/Sidebar/Basic/Groups/Assemblies/SurfactantLayer.qml @@ -60,6 +60,7 @@ EaElements.GroupColumn { EaComponents.TableViewTextInput { horizontalAlignment: Text.AlignHCenter text: Globals.BackendWrapper.sampleLayers[index].formula + onActiveFocusChanged: if (activeFocus && Globals.BackendWrapper.sampleCurrentLayerIndex !== index) Globals.BackendWrapper.sampleSetCurrentLayerIndex(index) onEditingFinished: Globals.BackendWrapper.sampleSetCurrentLayerFormula(text) } @@ -67,6 +68,7 @@ EaElements.GroupColumn { horizontalAlignment: Text.AlignHCenter enabled: Globals.BackendWrapper.sampleLayers[index].thickness_enabled === "True" text: (isNaN(Globals.BackendWrapper.sampleLayers[index].thickness)) ? '--' : Number(Globals.BackendWrapper.sampleLayers[index].thickness).toFixed(2) + onActiveFocusChanged: if (activeFocus && Globals.BackendWrapper.sampleCurrentLayerIndex !== index) Globals.BackendWrapper.sampleSetCurrentLayerIndex(index) onEditingFinished: Globals.BackendWrapper.sampleSetCurrentLayerThickness(text) } @@ -74,12 +76,14 @@ EaElements.GroupColumn { horizontalAlignment: Text.AlignHCenter enabled: Globals.BackendWrapper.sampleLayers[index].roughness_enabled === "True" text: (isNaN(Globals.BackendWrapper.sampleLayers[index].roughness)) ? '--' : Number(Globals.BackendWrapper.sampleLayers[index].roughness).toFixed(2) + onActiveFocusChanged: if (activeFocus && Globals.BackendWrapper.sampleCurrentLayerIndex !== index) Globals.BackendWrapper.sampleSetCurrentLayerIndex(index) onEditingFinished: Globals.BackendWrapper.sampleSetCurrentLayerRoughness(text) } EaComponents.TableViewTextInput { horizontalAlignment: Text.AlignHCenter text: (isNaN(Globals.BackendWrapper.sampleLayers[index].solvation)) ? '--' : Number(Globals.BackendWrapper.sampleLayers[index].solvation).toFixed(2) + onActiveFocusChanged: if (activeFocus && Globals.BackendWrapper.sampleCurrentLayerIndex !== index) Globals.BackendWrapper.sampleSetCurrentLayerIndex(index) onEditingFinished: Globals.BackendWrapper.sampleSetCurrentLayerSolvation(text) } @@ -87,6 +91,7 @@ EaElements.GroupColumn { horizontalAlignment: Text.AlignHCenter enabled: Globals.BackendWrapper.sampleLayers[index].apm_enabled === "True" text: (isNaN(Globals.BackendWrapper.sampleLayers[index].apm)) ? '--' : Number(Globals.BackendWrapper.sampleLayers[index].apm).toFixed(2) + onActiveFocusChanged: if (activeFocus && Globals.BackendWrapper.sampleCurrentLayerIndex !== index) Globals.BackendWrapper.sampleSetCurrentLayerIndex(index) onEditingFinished: Globals.BackendWrapper.sampleSetCurrentLayerAPM(text) } @@ -108,7 +113,7 @@ EaElements.GroupColumn { } } mouseArea.onPressed: { - if (Globals.BackendWrapper.samplCurrentLayerIndex !== index) { + if (Globals.BackendWrapper.sampleCurrentLayerIndex !== index) { Globals.BackendWrapper.sampleSetCurrentLayerIndex(index) } } diff --git a/EasyReflectometryApp/Gui/Pages/Sample/Sidebar/Basic/Groups/LoadSample.qml b/EasyReflectometryApp/Gui/Pages/Sample/Sidebar/Basic/Groups/LoadSample.qml index 14973e97..ec9cba86 100644 --- a/EasyReflectometryApp/Gui/Pages/Sample/Sidebar/Basic/Groups/LoadSample.qml +++ b/EasyReflectometryApp/Gui/Pages/Sample/Sidebar/Basic/Groups/LoadSample.qml @@ -13,6 +13,13 @@ EaElements.GroupBox { collapsed: false EaElements.GroupColumn { + EaElements.CheckBox { + id: appendCheckBox + text: qsTr("Append to existing models") + checked: true + width: EaStyle.Sizes.sideBarContentWidth + } + EaElements.SideBarButton { width: EaStyle.Sizes.sideBarContentWidth fontIcon: "folder-open" @@ -24,7 +31,31 @@ EaElements.GroupBox { id: fileDialog title: qsTr("Select a sample file") nameFilters: [ "ORT files (*.ort)", "ORSO files (*.orso)", "All files (*.*)" ] - onAccepted: Globals.BackendWrapper.sampleFileLoad(selectedFiles[0]) + onAccepted: Globals.BackendWrapper.sampleFileLoad(selectedFiles[0], appendCheckBox.checked) + } + } + + // Warning dialog for sample load issues + EaElements.Dialog { + id: sampleLoadWarningDialog + title: qsTr("Sample Load Warning") + standardButtons: Dialog.Ok + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + + property string warningMessage: "" + + EaElements.Label { + text: sampleLoadWarningDialog.warningMessage + wrapMode: Text.WordWrap + width: Math.min(implicitWidth, EaStyle.Sizes.sideBarContentWidth * 1.5) + } + } + + Connections { + target: Globals.BackendWrapper + function onSampleLoadWarning(message) { + sampleLoadWarningDialog.warningMessage = message + sampleLoadWarningDialog.open() } } } \ No newline at end of file