From: abn Date: Mon, 8 Aug 2016 08:52:30 +0000 (+0200) Subject: Integration of CurvePlot tool in GUI. X-Git-Tag: V8_1_0rc1~14 X-Git-Url: http://git.salome-platform.org/gitweb/?a=commitdiff_plain;h=2d1cabfe4280f1d34737a3adcba9e16619a5e600;p=modules%2Fgui.git Integration of CurvePlot tool in GUI. --- diff --git a/tools/CurvePlot/CMakeLists.txt b/tools/CurvePlot/CMakeLists.txt new file mode 100755 index 000000000..41cd1dc67 --- /dev/null +++ b/tools/CurvePlot/CMakeLists.txt @@ -0,0 +1,225 @@ +# Copyright (C) 2012-2014 CEA/DEN, EDF R&D +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# +CMAKE_MINIMUM_REQUIRED(VERSION 2.8.8 FATAL_ERROR) +INCLUDE(CMakeDependentOption) + +PROJECT(SalomeCURVEPLOT C CXX) + +# Ensure a proper linker behavior: +CMAKE_POLICY(SET CMP0003 NEW) + +# Versioning +# =========== +# Project name, upper case +STRING(TOUPPER ${PROJECT_NAME} PROJECT_NAME_UC) + +SET(${PROJECT_NAME_UC}_MAJOR_VERSION 7) +SET(${PROJECT_NAME_UC}_MINOR_VERSION 6) +SET(${PROJECT_NAME_UC}_PATCH_VERSION 0) +SET(${PROJECT_NAME_UC}_VERSION + ${${PROJECT_NAME_UC}_MAJOR_VERSION}.${${PROJECT_NAME_UC}_MINOR_VERSION}.${${PROJECT_NAME_UC}_PATCH_VERSION}) +SET(${PROJECT_NAME_UC}_VERSION_DEV 1) + +# User options +# ============ +OPTION(SALOME_CURVEPLOT_STANDALONE "Standalone installation of CURVEPLOT" OFF) +OPTION(SALOME_BUILD_DOC "Generate SALOME CURVEPLOT documentation" ON) +OPTION(SALOME_BUILD_TESTS "Generate SALOME CURVEPLOT tests" ON) + +# Common CMake macros +# =================== +SET(CONFIGURATION_ROOT_DIR $ENV{CONFIGURATION_ROOT_DIR} CACHE PATH "Path to the Salome CMake configuration files") +IF(EXISTS ${CONFIGURATION_ROOT_DIR}) + LIST(APPEND CMAKE_MODULE_PATH "${CONFIGURATION_ROOT_DIR}/cmake") + INCLUDE(SalomeMacros) +ELSE() + MESSAGE(FATAL_ERROR "We absolutely need the Salome CMake configuration files, please define CONFIGURATION_ROOT_DIR !") +ENDIF() + +IF(NOT SALOME_CURVEPLOT_STANDALONE) + # Find KERNEL + # ============== + SET(KERNEL_ROOT_DIR $ENV{KERNEL_ROOT_DIR} CACHE PATH "Path to the Salome KERNEL") + IF( EXISTS ${KERNEL_ROOT_DIR} ) + LIST(APPEND CMAKE_MODULE_PATH "${KERNEL_ROOT_DIR}/salome_adm/cmake_files") + FIND_PACKAGE(SalomeKERNEL REQUIRED) + KERNEL_WITH_CORBA() # check whether KERNEL builded with CORBA + ADD_DEFINITIONS(${KERNEL_DEFINITIONS} -DSALOME_KERNEL) + INCLUDE_DIRECTORIES(${KERNEL_INCLUDE_DIRS}) + ELSE( EXISTS ${KERNEL_ROOT_DIR} ) + MESSAGE(FATAL_ERROR "We absolutely need a Salome KERNEL, please define KERNEL_ROOT_DIR or turn option SALOME_CURVEPLOT_STANDALONE to ON!") + ENDIF( EXISTS ${KERNEL_ROOT_DIR} ) + + # Find SALOME GUI (needed for the C++ part - PyUtils) + # ============== + SET(GUI_ROOT_DIR $ENV{GUI_ROOT_DIR} CACHE PATH "Path to the Salome GUI") + IF(EXISTS ${GUI_ROOT_DIR}) + LIST(APPEND CMAKE_MODULE_PATH "${GUI_ROOT_DIR}/adm_local/cmake_files") + FIND_PACKAGE(SalomeGUI) + ELSE(EXISTS ${GUI_ROOT_DIR}) + MESSAGE(FATAL_ERROR "We absolutely need a Salome GUI, please define GUI_ROOT_DIR or turn option SALOME_CURVEPLOT_STANDALONE to ON!") + ENDIF(EXISTS ${GUI_ROOT_DIR}) + + # Platform setup + # ============== + INCLUDE(SalomeSetupPlatform) # From KERNEL +ENDIF(NOT SALOME_CURVEPLOT_STANDALONE) + +# Always build libraries as shared objects: +SET(BUILD_SHARED_LIBS TRUE) +# Local macros: +LIST(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/adm_local/cmake_files") + +IF(SALOME_CURVEPLOT_STANDALONE) + INCLUDE(SalomeMacros) +ENDIF() + + +# Prerequisites +# ============= +# Find "big" prerequisites first - they reference themselves many others +# -> this can helps to find the smaller prerequisites and detect conflicts. +# In our case KERNEL may have already loaded many prerequisites which are also used in OSCAR_POST: + +## +## From KERNEL: +## + +# Mandatory products +FIND_PACKAGE(SalomePythonInterp REQUIRED) +FIND_PACKAGE(SalomePythonLibs REQUIRED) +FIND_PACKAGE(SalomeNumPySciPy REQUIRED) + +# Qt4 +FIND_PACKAGE(SalomeQt4 REQUIRED COMPONENTS QtCore QtGui) +INCLUDE(${QT_USE_FILE}) + +# Optional products: + +IF(SALOME_BUILD_DOC) +# FIND_PACKAGE(SalomeDoxygen) +# FIND_PACKAGE(SalomeSphinx) +# SALOME_LOG_OPTIONAL_PACKAGE(Doxygen SALOME_BUILD_DOC) +# SALOME_LOG_OPTIONAL_PACKAGE(Sphinx SALOME_BUILD_DOC) +# ADD_DEFINITIONS(-DDOXYGEN_IS_OK) +ENDIF() + +IF(SALOME_BUILD_TESTS) + ENABLE_TESTING() +ENDIF() + +# Detection summary: +SALOME_PACKAGE_REPORT_AND_CHECK() + +# Directories +# =========== +IF(SALOME_CURVEPLOT_STANDALONE) + SET(SALOME_INSTALL_LIBS lib/salome CACHE PATH "Install path: SALOME libs") + SET(_pydir lib/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/site-packages) + SET(SALOME_INSTALL_PYTHON ${_pydir}/salome CACHE PATH "Install path: SALOME Python stuff") + SET(SALOME_INSTALL_SCRIPT_PYTHON bin/salome CACHE PATH + "Install path: SALOME Python scripts") + SET(SALOME_INSTALL_CMAKE_LOCAL adm_local/cmake_files CACHE PATH + "Install path: local SALOME CMake files") + SET(SALOME_INSTALL_RES share/salome/resources CACHE PATH "Install path: SALOME resources") + SET(SALOME_INSTALL_DOC share/doc/resources CACHE PATH "Install path: SALOME documentation") +ELSE() + SET(SALOME_INSTALL_LIBS "${SALOME_INSTALL_LIBS}" CACHE PATH "Install path: SALOME libs") + SET(SALOME_INSTALL_PYTHON "${SALOME_INSTALL_PYTHON}" CACHE PATH + "Install path: SALOME Python scripts") + SET(SALOME_INSTALL_SCRIPT_PYTHON "${SALOME_INSTALL_SCRIPT_PYTHON}" CACHE PATH + "Install path: SALOME Python scripts") + SET(SALOME_INSTALL_CMAKE_LOCAL ${SALOME_INSTALL_CMAKE_LOCAL} CACHE PATH + "Install path: local SALOME CMake files") + SET(SALOME_INSTALL_RES "${SALOME_INSTALL_RES}" CACHE PATH "Install path: SALOME resources") + SET(SALOME_INSTALL_DOC "${SALOME_INSTALL_DOC}" CACHE PATH "Install path: SALOME documentation") + SET(SALOME_INSTALL_HEADERS ${SALOME_INSTALL_HEADERS} CACHE PATH "Install path: SALOME headers") +ENDIF() + +# Specific to CURVEPLOT: +SET(SALOME_CURVEPLOT_INSTALL_RES_DATA "${SALOME_INSTALL_RES}/curveplot" CACHE PATH + "Install path: SALOME CURVEPLOT specific data") +# Package installation path (lib/python2.7/...) +SET(SALOME_CURVEPLOT_INSTALL_PYTHON ${SALOME_INSTALL_PYTHON}/curveplot CACHE INTERNAL + "Install path: SALOME CURVEPLOT Python packages" FORCE) +SET(SALOME_CURVEPLOT_INSTALL_SCRIPT_PYTHON ${SALOME_INSTALL_SCRIPT_PYTHON} CACHE INTERNAL + "Install path: SALOME CURVEPLOT Python main entry points" FORCE) + +# Sources +# ======== +ADD_SUBDIRECTORY(src) +ADD_SUBDIRECTORY(resources) +ADD_SUBDIRECTORY(cmake_files) +IF(SALOME_BUILD_DOC) +# ADD_SUBDIRECTORY(doc) +ENDIF() + +# Configuration export +# (here only the level 1 prerequisites are exposed) +# ==================== +INCLUDE(CMakePackageConfigHelpers) + +# List of targets in this project we want to make visible to the rest of the world. +# They all have to be INSTALL'd with the option "EXPORT ${PROJECT_NAME}TargetGroup" +SET(_${PROJECT_NAME}_exposed_targets) +IF(NOT SALOME_CURVEPLOT_STANDALONE) + SET(_${PROJECT_NAME}_exposed_targets CurvePlot) +ENDIF() + +# Add all targets to the build-tree export set +EXPORT(TARGETS ${_${PROJECT_NAME}_exposed_targets} + FILE ${PROJECT_BINARY_DIR}/${PROJECT_NAME}Targets.cmake) + +# Create the configuration files: +# - in the build tree: + +# Ensure the variables are always defined for the configure: +SET(KERNEL_ROOT_DIR "${KERNEL_ROOT_DIR}") +SET(GUI_ROOT_DIR "${GUI_ROOT_DIR}") +SET(QT4_ROOT_DIR "${QT4_ROOT_DIR}") +SET(PYQT4_ROOT_DIR "${PYQT4_ROOT_DIR}") +SET(PYTHON_ROOT_DIR "${PYTHON_ROOT_DIR}") + +SET(CONF_INCLUDE_DIRS "${PROJECT_SOURCE_DIR}/include" "${PROJECT_BINARY_DIR}/include") + +# Build variables that will be expanded when configuring SalomeConfig.cmake: +SALOME_CONFIGURE_PREPARE(PyQt4 Qt4 Python) + +CONFIGURE_PACKAGE_CONFIG_FILE(${PROJECT_NAME}Config.cmake.in + ${PROJECT_BINARY_DIR}/${PROJECT_NAME}Config.cmake + INSTALL_DESTINATION "${SALOME_INSTALL_CMAKE_LOCAL}" + PATH_VARS CONF_INCLUDE_DIRS SALOME_INSTALL_CMAKE_LOCAL CMAKE_INSTALL_PREFIX + KERNEL_ROOT_DIR GUI_ROOT_DIR QT4_ROOT_DIR PYQT4_ROOT_DIR PYTHON_ROOT_DIR) + +WRITE_BASIC_PACKAGE_VERSION_FILE(${PROJECT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake + VERSION ${${PROJECT_NAME_UC}_VERSION} + COMPATIBILITY AnyNewerVersion) + +# Install the CMake configuration files: +INSTALL(FILES + "${PROJECT_BINARY_DIR}/${PROJECT_NAME}Config.cmake" + "${PROJECT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake" + DESTINATION "${SALOME_INSTALL_CMAKE_LOCAL}") + +# Install the export set for use with the install-tree +IF(NOT SALOME_CURVEPLOT_STANDALONE) + INSTALL(EXPORT ${PROJECT_NAME}TargetGroup DESTINATION "${SALOME_INSTALL_CMAKE_LOCAL}" + FILE ${PROJECT_NAME}Targets.cmake) +ENDIF() + diff --git a/tools/CurvePlot/SalomeCURVEPLOTConfig.cmake.in b/tools/CurvePlot/SalomeCURVEPLOTConfig.cmake.in new file mode 100644 index 000000000..76b16dda8 --- /dev/null +++ b/tools/CurvePlot/SalomeCURVEPLOTConfig.cmake.in @@ -0,0 +1,100 @@ +# Copyright (C) 2013-2015 CEA/DEN, EDF R&D, OPEN CASCADE +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# + +# - Config file for the @PROJECT_NAME@ package +# It defines the following variables. +# Specific to the pacakge @PROJECT_NAME@ itself: +# @PROJECT_NAME_UC@_ROOT_DIR_EXP - the root path of the installation providing this CMake file +# + +### Initialisation performed by CONFIGURE_PACKAGE_CONFIG_FILE: +@PACKAGE_INIT@ + +# Options exported by the package: +SET(SALOME_CURVEPLOT_BUILD_DOC @SALOME_BUILD_DOC@) +SET(SALOME_CURVEPLOT_BUILD_TESTS @SALOME_BUILD_TESTS@) +SET(SALOME_CURVEPLOT_STANDALONE @SALOME_CURVEPLOT_STANDALONE@) + +# Load the dependencies for the libraries of @PROJECT_NAME@ +# (contains definitions for IMPORTED targets). This is only +# imported if we are not built as a subproject (in this case targets are already there) +IF(NOT TARGET CurvePlot AND NOT @PROJECT_NAME@_BINARY_DIR AND NOT SALOME_CURVEPLOT_STANDALONE) + INCLUDE("@PACKAGE_SALOME_INSTALL_CMAKE_LOCAL@/@PROJECT_NAME@Targets.cmake") +ENDIF() + +# Package root dir: +SET_AND_CHECK(CURVEPLOT_ROOT_DIR_EXP "@PACKAGE_CMAKE_INSTALL_PREFIX@") + +# Include directories +SET_AND_CHECK(CURVEPLOT_INCLUDE_DIRS "${CURVEPLOT_ROOT_DIR_EXP}/@SALOME_INSTALL_HEADERS@") + +#### Now the specificities + + +# Level 1 prerequisites: +IF(SALOME_CURVEPLOT_STANDALONE) + SET_AND_CHECK(KERNEL_ROOT_DIR_EXP "@PACKAGE_KERNEL_ROOT_DIR@") + SET_AND_CHECK(GUI_ROOT_DIR_EXP "@PACKAGE_GUI_ROOT_DIR@") +ENDIF() + +SET_AND_CHECK(QT4_ROOT_DIR_EXP "@PACKAGE_QT4_ROOT_DIR@") +SET_AND_CHECK(PYQT4_ROOT_DIR_EXP "@PACKAGE_PYQT4_ROOT_DIR@") +SET_AND_CHECK(PYTHON_ROOT_DIR_EXP "@PACKAGE_PYTHON_ROOT_DIR@") + + +# For all prerequisites, load the corresponding targets if the package was used +# in CONFIG mode. This ensures dependent projects link correctly +# without having to set LD_LIBRARY_PATH: +SET(_PREREQ @_PREREQ_LIST@) +SET(_PREREQ_CONFIG_DIR @_PREREQ_DIR_LIST@) +SET(_PREREQ_COMPONENTS "@_PREREQ_COMPO_LIST@") +LIST(LENGTH _PREREQ_CONFIG_DIR _list_len) +IF(NOT _list_len EQUAL 0) + # Another CMake stupidity - FOREACH(... RANGE r) generates r+1 numbers ... + MATH(EXPR _range "${_list_len}-1") + FOREACH(_p RANGE ${_range}) + LIST(GET _PREREQ ${_p} _pkg ) + LIST(GET _PREREQ_CONFIG_DIR ${_p} _pkg_dir) + LIST(GET _PREREQ_COMPONENTS ${_p} _pkg_compo) + MESSAGE(STATUS "===> Reloading targets from ${_pkg} ...") + IF(NOT _pkg_compo) + FIND_PACKAGE(${_pkg} REQUIRED NO_MODULE + PATHS "${_pkg_dir}" + NO_DEFAULT_PATH) + ELSE() + STRING(REPLACE "," ";" _compo_lst "${_pkg_compo}") + MESSAGE(STATUS "===> (components: ${_pkg_compo})") + FIND_PACKAGE(${_pkg} REQUIRED NO_MODULE + COMPONENTS ${_compo_lst} + PATHS "${_pkg_dir}" + NO_DEFAULT_PATH) + ENDIF() + ENDFOREACH() +ENDIF() + +# Installation directories +SET(SALOME_INSTALL_LIBS "@SALOME_INSTALL_LIBS@") +SET(SALOME_INSTALL_SCRIPT_PYTHON "@SALOME_INSTALL_SCRIPT_PYTHON@") +SET(SALOME_INSTALL_CMAKE_LOCAL "@SALOME_INSTALL_CMAKE_LOCAL@") +SET(SALOME_INSTALL_PYTHON "@SALOME_INSTALL_PYTHON@") +SET(SALOME_INSTALL_RES "@SALOME_INSTALL_RES@") +SET(SALOME_INSTALL_DOC "@SALOME_INSTALL_DOC@") + +# Exposed CURVEPLOT targets: +SET(CURVEPLOT_CurvePlot CurvePlot) diff --git a/tools/CurvePlot/cmake_files/CMakeLists.txt b/tools/CurvePlot/cmake_files/CMakeLists.txt new file mode 100755 index 000000000..c2580cf60 --- /dev/null +++ b/tools/CurvePlot/cmake_files/CMakeLists.txt @@ -0,0 +1,24 @@ +# Copyright (C) 2012-2015 CEA/DEN, EDF R&D, OPEN CASCADE +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# + +SET(_adm_data + FindSalomeCURVEPLOT.cmake +) + +INSTALL(FILES ${_adm_data} DESTINATION ${SALOME_INSTALL_CMAKE_LOCAL}) diff --git a/tools/CurvePlot/cmake_files/FindSalomeCURVEPLOT.cmake b/tools/CurvePlot/cmake_files/FindSalomeCURVEPLOT.cmake new file mode 100644 index 000000000..ef6c14482 --- /dev/null +++ b/tools/CurvePlot/cmake_files/FindSalomeCURVEPLOT.cmake @@ -0,0 +1,38 @@ +# Copyright (C) 2007-2015 CEA/DEN, EDF R&D, OPEN CASCADE +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# +# Author: Adrien Bruneton +# + +# CURVEPLOT detection for Salome - this is typically called by dependent modules +# +# The detection is simpler than for other prerequisites. +# See explanation in FindSalomeKERNEL.cmake. +# + +IF(NOT SalomeCURVEPLOT_FIND_QUIETLY) + MESSAGE(STATUS "Looking for Salome CURVEPLOT ...") +ENDIF() + +SET(CMAKE_PREFIX_PATH "${CURVEPLOT_ROOT_DIR}") +SALOME_FIND_PACKAGE(SalomeCURVEPLOT SalomeCURVEPLOT CONFIG) + +IF(NOT SalomeCURVEPLOT_FIND_QUIETLY) + MESSAGE(STATUS "Found Salome CURVEPLOT: ${CURVEPLOT_ROOT_DIR}") +ENDIF() + diff --git a/tools/CurvePlot/resources/CMakeLists.txt b/tools/CurvePlot/resources/CMakeLists.txt new file mode 100644 index 000000000..5abf728b6 --- /dev/null +++ b/tools/CurvePlot/resources/CMakeLists.txt @@ -0,0 +1,45 @@ +# Copyright (C) 2010-2015 CEA/DEN, EDF R&D +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# +INCLUDE(UseQtExt) + +SET(_res + CURVEPLOT_msg_en.ts + CURVEPLOT_msg_fr.ts +) + +QT_INSTALL_TS_RESOURCES("${_res}" "${SALOME_CURVEPLOT_INSTALL_RES_DATA}") + +SET(dist_salomeres_DATA + dump_view.png + fit_all.png + fit_area.png + zoom_pan.png + draw_points.png + draw_lines.png + legend.png + hor_linear.png + ver_linear.png + hor_logarithmic.png + ver_logarithmic.png + settings.png + ) + +FOREACH(f ${dist_salomeres_DATA}) + INSTALL(FILES ${f} DESTINATION ${SALOME_CURVEPLOT_INSTALL_RES_DATA}) +ENDFOREACH() diff --git a/tools/CurvePlot/resources/CURVEPLOT_msg_en.ts b/tools/CurvePlot/resources/CURVEPLOT_msg_en.ts new file mode 100644 index 000000000..dff128c36 --- /dev/null +++ b/tools/CurvePlot/resources/CURVEPLOT_msg_en.ts @@ -0,0 +1,83 @@ + + + + + CURVEPLOT + + DUMP_VIEW_TXT + Dump view + + + FIT_ALL_TXT + Fit all + + + FIT_AREA_TXT + Fit area + + + ZOOM_TXT + Zoom + + + ZOOM_PAN_TXT + Panning + + + DRAW_POINTS_TXT + Draw points + + + DRAW_LINES_TXT + Draw lines + + + HOR_LINEAR_TXT + Horizontal axis: linear + + + HOR_LOGARITHMIC_TXT + Horizontal axis: logarithmic + + + VER_LINEAR_TXT + Vertical axis: linear + + + VER_LOGARITHMIC_TXT + Vertical axis: logarithmic + + + SHOW_LEGEND_TXT + Show legend + + + HIDE_LEGEND_TXT + Hide legend + + + SETTINGS_TXT + Settings + + + DUMP_VIEW_FILE + Dump view to file + + + IMAGES_FILES + Images Files (*.png) + + + PDF_FILES + PDF files (*.pdf) + + + POSTSCRIPT_FILES + PostScript files (*.ps) + + + ENCAPSULATED_POSTSCRIPT_FILES + Encapsulated PostScript files (*.eps) + + + \ No newline at end of file diff --git a/tools/CurvePlot/resources/CURVEPLOT_msg_fr.ts b/tools/CurvePlot/resources/CURVEPLOT_msg_fr.ts new file mode 100644 index 000000000..2e72cc2bd --- /dev/null +++ b/tools/CurvePlot/resources/CURVEPLOT_msg_fr.ts @@ -0,0 +1,83 @@ + + + + + CURVEPLOT + + DUMP_VIEW_TXT + Sauvegarder la scène + + + FIT_ALL_TXT + Tout afficher + + + FIT_AREA_TXT + Afficher la zone + + + ZOOM_TXT + Zoomer + + + PAN_TXT + Déplacer + + + DRAW_POINTS_TXT + Dessiner des points + + + DRAW_LINES_TXT + Dessiner des lignes + + + HOR_LINEAR_TXT + Axe horizontal : linéaire + + + HOR_LOGARITHMIC_TXT + Axe horizontal : logarithmique + + + VER_LINEAR_TXT + Axe vertical : linéaire + + + VER_LOGARITHMIC_TXT + Axe vertical : logarithmique + + + SHOW_LEGEND_TXT + Afficher la légende + + + HIDE_LEGEND_TXT + Cacher la légende + + + SETTINGS_TXT + Paramètres + + + DUMP_VIEW_FILE + Enregistrer la vue dans le fichier + + + IMAGES_FILES + Fichiers images (*.png) + + + PDF_FILES + Fichiers PDF (*.pdf) + + + POSTSCRIPT_FILES + Fichiers PostScript (*.ps) + + + ENCAPSULATED_POSTSCRIPT_FILES + Fichiers PostScript encapsulés (*.eps) + + + \ No newline at end of file diff --git a/tools/CurvePlot/resources/draw_lines.png b/tools/CurvePlot/resources/draw_lines.png new file mode 100644 index 000000000..978945284 Binary files /dev/null and b/tools/CurvePlot/resources/draw_lines.png differ diff --git a/tools/CurvePlot/resources/draw_points.png b/tools/CurvePlot/resources/draw_points.png new file mode 100644 index 000000000..3cdea33dd Binary files /dev/null and b/tools/CurvePlot/resources/draw_points.png differ diff --git a/tools/CurvePlot/resources/dump_view.png b/tools/CurvePlot/resources/dump_view.png new file mode 100644 index 000000000..b02616f29 Binary files /dev/null and b/tools/CurvePlot/resources/dump_view.png differ diff --git a/tools/CurvePlot/resources/fit_all.png b/tools/CurvePlot/resources/fit_all.png new file mode 100644 index 000000000..87e001dd8 Binary files /dev/null and b/tools/CurvePlot/resources/fit_all.png differ diff --git a/tools/CurvePlot/resources/fit_area.png b/tools/CurvePlot/resources/fit_area.png new file mode 100644 index 000000000..450dc56c6 Binary files /dev/null and b/tools/CurvePlot/resources/fit_area.png differ diff --git a/tools/CurvePlot/resources/hor_linear.png b/tools/CurvePlot/resources/hor_linear.png new file mode 100644 index 000000000..0140fcc98 Binary files /dev/null and b/tools/CurvePlot/resources/hor_linear.png differ diff --git a/tools/CurvePlot/resources/hor_logarithmic.png b/tools/CurvePlot/resources/hor_logarithmic.png new file mode 100644 index 000000000..fb62cd772 Binary files /dev/null and b/tools/CurvePlot/resources/hor_logarithmic.png differ diff --git a/tools/CurvePlot/resources/legend.png b/tools/CurvePlot/resources/legend.png new file mode 100644 index 000000000..81c06277b Binary files /dev/null and b/tools/CurvePlot/resources/legend.png differ diff --git a/tools/CurvePlot/resources/settings.png b/tools/CurvePlot/resources/settings.png new file mode 100755 index 000000000..64df2460b Binary files /dev/null and b/tools/CurvePlot/resources/settings.png differ diff --git a/tools/CurvePlot/resources/ver_linear.png b/tools/CurvePlot/resources/ver_linear.png new file mode 100644 index 000000000..7acc8fe79 Binary files /dev/null and b/tools/CurvePlot/resources/ver_linear.png differ diff --git a/tools/CurvePlot/resources/ver_logarithmic.png b/tools/CurvePlot/resources/ver_logarithmic.png new file mode 100644 index 000000000..825d59edb Binary files /dev/null and b/tools/CurvePlot/resources/ver_logarithmic.png differ diff --git a/tools/CurvePlot/resources/zoom_pan.png b/tools/CurvePlot/resources/zoom_pan.png new file mode 100644 index 000000000..ec56cacc7 Binary files /dev/null and b/tools/CurvePlot/resources/zoom_pan.png differ diff --git a/tools/CurvePlot/src/CMakeLists.txt b/tools/CurvePlot/src/CMakeLists.txt new file mode 100644 index 000000000..247339edb --- /dev/null +++ b/tools/CurvePlot/src/CMakeLists.txt @@ -0,0 +1,25 @@ +# Copyright (C) 2012-2014 CEA/DEN, EDF R&D +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# + +ADD_SUBDIRECTORY(python) + +IF(NOT SALOME_CURVEPLOT_STANDALONE) + ADD_SUBDIRECTORY(cpp) +ENDIF() + diff --git a/tools/CurvePlot/src/cpp/CMakeLists.txt b/tools/CurvePlot/src/cpp/CMakeLists.txt new file mode 100755 index 000000000..edf5c6692 --- /dev/null +++ b/tools/CurvePlot/src/cpp/CMakeLists.txt @@ -0,0 +1,56 @@ +# Copyright (C) 2012-2015 CEA/DEN, EDF R&D, OPEN CASCADE +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# + +ADD_SUBDIRECTORY(test) + +# --- options --- + +# additional include directories +INCLUDE_DIRECTORIES( + ${PYTHON_INCLUDE_DIRS} + ${NUMPY_INCLUDE_DIR} + ${GUI_INCLUDE_DIRS} +) + +# additional preprocessor / compiler flags +ADD_DEFINITIONS(${PYTHON_DEFINITIONS}) + +# libraries to link to +SET(_link_LIBRARIES ${PYTHON_LIBRARIES} ${GUI_PyInterp}) + +# --- headers --- + +# header files / no moc processing +SET(_other_HEADERS + CurvePlot.hxx +# ColumnVector.hxx +) + +# sources / static +SET(_other_SOURCES + CurvePlot.cxx +# ColumnVector.cxx +) +# --- rules --- + +ADD_LIBRARY(CurvePlot ${_other_SOURCES}) +TARGET_LINK_LIBRARIES(CurvePlot ${_link_LIBRARIES}) +INSTALL(TARGETS CurvePlot EXPORT ${PROJECT_NAME}TargetGroup DESTINATION ${SALOME_INSTALL_LIBS}) + +INSTALL(FILES ${_other_HEADERS} DESTINATION ${SALOME_INSTALL_HEADERS}) diff --git a/tools/CurvePlot/src/cpp/CurvePlot.cxx b/tools/CurvePlot/src/cpp/CurvePlot.cxx new file mode 100644 index 000000000..fe32da67a --- /dev/null +++ b/tools/CurvePlot/src/cpp/CurvePlot.cxx @@ -0,0 +1,442 @@ +// Copyright (C) 2010-2015 CEA/DEN, EDF R&D +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +// +// Author : Adrien BRUNETON +// + +#include + +#define PY_ARRAY_UNIQUE_SYMBOL CURVEPLOT_ARRAY_API // see initializeCurvePlot() +#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION +#include +#include +#include // GUI + +#include "CurvePlot.hxx" +#include "CurvePlot_Exception.hxx" + +namespace +{ + PyObject * strToPyUnicode(std::string s) + { + return PyUnicode_DecodeUTF8(s.c_str(), s.size(), (char*)"strict"); + } + + void HandleAndPrintPyError(std::string msg) + { + if(PyErr_Occurred()) + { + PyErr_Print(); + throw CURVEPLOT::Exception(msg); + } + } +}; + +namespace CURVEPLOT +{ + /** + * To be called before doing anything + */ + void InitializeCurvePlot() + { + PyLockWrapper lock; + // TODO: discuss where the below should really happen: + // doc: http://docs.scipy.org/doc/numpy/reference/c-api.array.html#importing-the-api + import_array(); // a macro really! + } + + class ColumnVector::Internal + { + public: + Internal() : _npArray(0) {} + ~Internal() {} + + PyArrayObject * _npArray; + }; + + ColumnVector::ColumnVector() + { + _impl = new Internal(); + } + + ColumnVector::~ColumnVector() + { + delete _impl; + } + + int ColumnVector::size() const + { + if (!_impl->_npArray) + return 0; + { + PyLockWrapper lock; + int ndim = PyArray_NDIM(_impl->_npArray); + if (ndim != 1) + throw Exception("ColumnVector::size() : wrong number of dimensions for internal array!!"); + npy_intp * dims = PyArray_DIMS(_impl->_npArray); + return dims[0]; + } + } + + ColumnVector ColumnVector::BuildFromCMemory(double * data, int size) + { + ColumnVector ret; + if (size <= 0) + return ret; + npy_intp dims[1] = {size}; + + { + PyLockWrapper lock; + PyObject * obj = PyArray_SimpleNewFromData(1,dims,NPY_DOUBLE, data); + PyArrayObject * aobj = (PyArrayObject * )obj; + + // Make Numpy responsible of the memory of the array (the memory will be freed + // as soon as the array is released in NumPy) + PyArray_ENABLEFLAGS(aobj, NPY_ARRAY_OWNDATA); + + ret._impl->_npArray = aobj; + return ret; + } + } + + ColumnVector ColumnVector::BuildFromStdVector(const std::vector & vec) + { + ColumnVector ret; + if (vec.size() == 0) + return ret; + + double * c_mem = (double *)malloc(sizeof(double) * vec.size()); + if (!c_mem) + throw Exception("ColumnVector::BuildFromStdVector() : memory allocation error!"); + const double * data = &vec.front(); + std::copy(data, data+vec.size(), c_mem); + npy_intp dims[1] = {(intptr_t) vec.size()}; + + { + PyLockWrapper lock; + PyObject * obj = PyArray_SimpleNewFromData(1,dims,NPY_DOUBLE, c_mem); + PyArrayObject * aobj = (PyArrayObject * )obj; + + // Make Numpy responsible of the memory of the array (the memory will be freed + // as soon as the array is released in NumPy) + PyArray_ENABLEFLAGS(aobj, NPY_ARRAY_OWNDATA); + + ret._impl->_npArray = aobj; + return ret; + } + } + + std::string ColumnVector::toStdString() const + { + std::string ret_str = "(None)"; + if (!_impl->_npArray) + return ret_str; + + { + PyLockWrapper lock; + PyObjWrapper ret_py( + PyObject_CallMethod((PyObject *)_impl->_npArray, (char *)"__str__", NULL) + ); + // Now extract the returned string + if(!PyString_Check(ret_py)) + throw Exception("CurvePlot::toStdString(): Unexpected returned type!"); + ret_str = std::string(PyString_AsString(ret_py)); + } + return ret_str; + } + + void ColumnVector::createPythonVar(std::string varName) const + { + PyObject* main_module = PyImport_AddModule((char*)"__main__"); + PyObject* global_dict = PyModule_GetDict(main_module); + PyDict_SetItemString(global_dict, varName.c_str(), (PyObject *)_impl->_npArray); + } + + void ColumnVector::cleanPythonVar(std::string varName) const + { + // Could be a static method really ... + + std::string s = std::string("del ") + varName; + const char * cmd = s.c_str(); + PyRun_SimpleString(cmd); + } + + CurvePlot * CurvePlot::_instance = NULL; + + class CurvePlot::Internal + { + public: + Internal() : _controller(0) {} + ///! Plot2d controller from Python: + PyObject * _controller; + }; + + CurvePlot::CurvePlot(bool test_mode) + { + // TODO: do use an intermediate variable '__cont', but use directly Py***CallMethod() + _impl = new Internal(); + { + PyLockWrapper lock; + std::string code; + if (test_mode) + code = std::string("import curveplot; from SalomePyQt_MockUp import SalomePyQt;") + + std::string("__cont=curveplot.PlotController.GetInstance(sgPyQt=SalomePyQt())"); + else + code = std::string("import curveplot;")+ + std::string("__cont=curveplot.PlotController.GetInstance()"); + + int ret = PyRun_SimpleString(const_cast(code.c_str())); + if (ret == -1) + throw Exception("CurvePlot::CurvePlot(): Unable to load curveplot Python module!"); + + // Now get the reference to __engine and save the pointer. + // All the calls below returns *borrowed* references + PyObject* main_module = PyImport_AddModule((char*)"__main__"); + PyObject* global_dict = PyModule_GetDict(main_module); + PyObject* tmp = PyDict_GetItemString(global_dict, "__cont"); + + _impl->_controller = tmp; + Py_INCREF(_impl->_controller); + PyRun_SimpleString(const_cast("del __cont")); + } + } + + CurvePlot::~CurvePlot() + { + if(_impl->_controller != NULL) + { + PyLockWrapper lock; + Py_XDECREF(_impl->_controller); + } + delete _impl; + } + + CurvePlot * CurvePlot::GetInstance(bool test_mode) + { + if(!_instance) + _instance = new CurvePlot(test_mode); + return _instance; + } + + void CurvePlot::ToggleCurveBrowser(bool with_curve_browser) + { + if(_instance) + throw Exception("CurvePlot::ToggleCurveBrowser() must be invoked before anything else!"); + + PyLockWrapper lock; + std::string bool_s = with_curve_browser ? "True" : "False"; + std::string cod = std::string("import curveplot; curveplot.PlotController.WITH_CURVE_BROWSER=") + bool_s; + PyRun_SimpleString(const_cast(cod.c_str())); + HandleAndPrintPyError("CurvePlot::ToggleCurveBrowser(): Unable to toggle Curve Browser!"); + } + + PlotID CurvePlot::AddCurve(const ColumnVector & x, const ColumnVector & y, + PlotID & plot_set_id, + std::string curve_label/*=""*/, std::string x_label/*=""*/, std::string y_label/*=""*/, + bool append/*=true*/) + { + PyLockWrapper lock; + PyObject * cont = GetInstance()->_impl->_controller; + + PyObject * xx = (PyObject *)x._impl->_npArray; + PyObject * yy = (PyObject *)y._impl->_npArray; + + PyObjWrapper ret( + PyObject_CallMethod(cont, (char *)"AddCurve", (char *)"OOOOOi", xx, yy, + strToPyUnicode(curve_label), strToPyUnicode(x_label), strToPyUnicode(y_label), + append ? 1 : 0) + ); + HandleAndPrintPyError("CurvePlot::AddCurve(): unexpected error!"); + // Now extract curve_id and plot_set_id from the returned tuple: + if(!PyTuple_Check(ret)) + throw Exception("CurvePlot::AddCurve(): Unexpected returned type!"); + PyObject * o1 = PyTuple_GetItem(ret, 0); + if (!PyInt_Check(o1)) + throw Exception("CurvePlot::AddCurve(): Unexpected returned type!"); + PlotID curveId = PyInt_AsLong(o1); + PyObject * o2 = PyTuple_GetItem(ret, 1); + if (!PyInt_Check(o2)) + throw Exception("CurvePlot::AddCurve(): Unexpected returned type!"); + plot_set_id = PyInt_AsLong(o2); + return curveId; + } + + PlotID CurvePlot::AddPlotSet(std::string title/*=""*/) + { + PyLockWrapper lock; + PyObject * cont = GetInstance()->_impl->_controller; + + PyObjWrapper ret( + PyObject_CallMethod(cont, (char *)"AddPlotSet", (char *)"O", strToPyUnicode(title)) + ); + HandleAndPrintPyError("CurvePlot::AddPlotSet(): unexpected error!"); + return PyLong_AsLong(ret); + } + + PlotID CurvePlot::DeleteCurve(PlotID curve_id/*=-1*/) + { + PyLockWrapper lock; + PyObject * cont = GetInstance()->_impl->_controller; + + PyObjWrapper ret( + PyObject_CallMethod(cont, (char *)"DeleteCurve", (char *)"i", curve_id) + ); + HandleAndPrintPyError("CurvePlot::DeleteCurve(): unexpected error!"); + return PyLong_AsLong(ret); + } + + PlotID CurvePlot::DeletePlotSet(PlotID plot_set_id/*=-1*/) + { + PyLockWrapper lock; + PyObject * cont = GetInstance()->_impl->_controller; + + PyObjWrapper ret( + PyObject_CallMethod(cont, (char *)"DeletePlotSet", (char *)"i", plot_set_id) + ); + HandleAndPrintPyError("CurvePlot::DeletePlotSet(): unexpected error!"); + return PyLong_AsLong(ret); + } + + PlotID CurvePlot::ClearPlotSet(PlotID plot_set_id/*=-1*/) + { + PyLockWrapper lock; + PyObject * cont = GetInstance()->_impl->_controller; + + PyObjWrapper ret( + PyObject_CallMethod(cont, (char *)"ClearPlotSet", (char *)"i", plot_set_id) + ); + HandleAndPrintPyError("CurvePlot::ClearPlotSet(): unexpected error!"); + return PyLong_AsLong(ret); + } + + bool CurvePlot::SetXLabel(std::string x_label, PlotID plot_set_id/*=-1*/) + { + PyLockWrapper lock; + PyObject * cont = GetInstance()->_impl->_controller; + + PyObjWrapper ret( + PyObject_CallMethod(cont, (char *)"SetXLabel", (char *)"Oi", strToPyUnicode(x_label), plot_set_id) + ); + HandleAndPrintPyError("CurvePlot::SetXLabel(): unexpected error!"); + return ((PyObject *)ret == Py_True); + } + + bool CurvePlot::SetYLabel(std::string y_label, PlotID plot_set_id/*=-1*/) + { + PyLockWrapper lock; + PyObject * cont = GetInstance()->_impl->_controller; + + PyObjWrapper ret( + PyObject_CallMethod(cont, (char *)"SetYLabel", (char *)"Oi", strToPyUnicode(y_label), plot_set_id) + ); + HandleAndPrintPyError("CurvePlot::SetYLabel(): unexpected error!"); + return ((PyObject *)ret == Py_True); + } + + bool CurvePlot::SetPlotSetTitle(std::string title, PlotID plot_set_id/*=-1*/) + { + PyLockWrapper lock; + PyObject * cont = GetInstance()->_impl->_controller; + + PyObjWrapper ret( + PyObject_CallMethod(cont, (char *)"SetPlotSetTitle", (char *)"Oi", strToPyUnicode(title), plot_set_id)); + HandleAndPrintPyError("CurvePlot::SetPlotSetTitle(): unexpected error!"); + return ((PyObject *)ret == Py_True); + } + + PlotID CurvePlot::GetPlotSetID(PlotID curve_id) + { + PyLockWrapper lock; + PyObject * cont = GetInstance()->_impl->_controller; + + PyObjWrapper ret( + PyObject_CallMethod(cont, (char *)"GetPlotSetID", (char *)"i", curve_id) + ); + HandleAndPrintPyError("CurvePlot::GetPlotSetID(): unexpected error!"); + return PyLong_AsLong(ret); + } + + PlotID CurvePlot::GetPlotSetIDByName(std::string name) + { + PyLockWrapper lock; + PyObject * cont = GetInstance()->_impl->_controller; + + PyObjWrapper ret( + PyObject_CallMethod(cont, (char *)"GetPlotSetIDByName", (char *)"O", strToPyUnicode(name)) + ); + HandleAndPrintPyError("CurvePlot::GetPlotSetIDByName(): unexpected error!"); + return PyLong_AsLong(ret); + } + + PlotID CurvePlot::GetCurrentCurveID() + { + PyLockWrapper lock; + PyObject * cont = GetInstance()->_impl->_controller; + + PyObjWrapper ret( + PyObject_CallMethod(cont, (char *)"GetCurrentCurveID", (char *)"") + ); + HandleAndPrintPyError("CurvePlot::GetCurrentCurveID(): unexpected error!"); + return PyLong_AsLong(ret); + } + + PlotID CurvePlot::GetCurrentPlotSetID() + { + PyLockWrapper lock; + PyObject * cont = GetInstance()->_impl->_controller; + + PyObjWrapper ret( + PyObject_CallMethod(cont, (char *)"GetCurrentPlotSetID", (char *)"") + ); + HandleAndPrintPyError("CurvePlot::GetCurrentPlotSetID(): unexpected error!"); + return PyLong_AsLong(ret); + } + + bool CurvePlot::IsValidPlotSetID(PlotID plot_set_id) + { + PyLockWrapper lock; + PyObject * cont = GetInstance()->_impl->_controller; + + PyObjWrapper ret( + PyObject_CallMethod(cont, (char *)"IsValidPlotSetID", (char *)"i", plot_set_id)); + HandleAndPrintPyError("CurvePlot::IsValidPlotSetID(): unexpected error!"); + return ((PyObject *)ret == Py_True); + } + + int CurvePlot::GetSalomeViewID(PlotID plot_set_id) + { + PyLockWrapper lock; + PyObject * cont = GetInstance()->_impl->_controller; + + PyObjWrapper ret( + PyObject_CallMethod(cont, (char *)"GetSalomeViewID", (char *)"i", plot_set_id)); + HandleAndPrintPyError("CurvePlot::GetSalomeViewID(): unexpected error!"); + return PyLong_AsLong(ret); + } + + void CurvePlot::OnSalomeViewTryClose(int salome_view_id) + { + PyLockWrapper lock; + PyObject * cont = GetInstance()->_impl->_controller; + + PyObjWrapper ret( + PyObject_CallMethod(cont, (char *)"OnSalomeViewTryClose", (char *)"i", salome_view_id)); + HandleAndPrintPyError("CurvePlot::OnSalomeViewTryClose(): unexpected error!"); + } + +} diff --git a/tools/CurvePlot/src/cpp/CurvePlot.hxx b/tools/CurvePlot/src/cpp/CurvePlot.hxx new file mode 100644 index 000000000..2b3fbb23a --- /dev/null +++ b/tools/CurvePlot/src/cpp/CurvePlot.hxx @@ -0,0 +1,133 @@ +// Copyright (C) 2010-2015 CEA/DEN, EDF R&D +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +// +// Author : Adrien BRUNETON +// + +#ifndef SRC_CPP_CURVEPLOT_HXX_ +#define SRC_CPP_CURVEPLOT_HXX_ + +#include +#include + +namespace CURVEPLOT +{ + typedef int PlotID; + + /** + * This function should be called before doing anything in the CURVEPLOT namespace. + */ + void InitializeCurvePlot(); + + class ColumnVector + { + public: + friend class CurvePlot; + + virtual ~ColumnVector(); + + /** + * Build a ColumnVector from a std::vector() of double. The memory is copied for now (TODO: optimize this). + */ + static ColumnVector BuildFromStdVector(const std::vector & vec); + + /** + * Build a ColumnVector from a block of memory which was malloc'ed. + * The memory is not copied, and the array thus created becomes responsible of the block. + * So do NOT free the memory that you pass. + */ + static ColumnVector BuildFromCMemory(double * data, int size); + + /** + * Get the current size of the vector. + */ + int size() const; + + /** Get a string representation */ + std::string toStdString() const; + + private: + class Internal; + ColumnVector(); + + void createPythonVar(std::string varName) const; + void cleanPythonVar(std::string varName) const; + + Internal * _impl; + }; + + + /** + * C++ wrapping of the public API exposed in the Python package curveplot. See doc there. + */ + class CurvePlot + { + public: + static PlotID AddCurve(const ColumnVector & x, const ColumnVector & y, + PlotID & plot_set_id, + std::string curve_label="", std::string x_label="", std::string y_label="", + bool append=true); + + static PlotID AddPlotSet(std::string title=""); + + static PlotID DeleteCurve(PlotID curve_id=-1); + + static PlotID DeletePlotSet(PlotID plot_set_id=-1); + + static PlotID ClearPlotSet(PlotID plot_set_id=-1); + + static bool SetXLabel(std::string x_label, PlotID plot_set_id=-1); + + static bool SetYLabel(std::string y_label, PlotID plot_set_id=-1); + + static bool SetPlotSetTitle(std::string title, PlotID plot_set_id=-1); + + static PlotID GetPlotSetID(PlotID curve_id); + + static PlotID GetPlotSetIDByName(std::string name); + + static PlotID GetCurrentCurveID(); + + static PlotID GetCurrentPlotSetID(); + + static void ToggleCurveBrowser(bool with_curve_browser); + + static bool IsValidPlotSetID(PlotID plot_set_id=-1); + + static int GetSalomeViewID(PlotID plot_set_id); + + static CurvePlot * GetInstance(bool test_mode=false); + + /**! Temporary ... */ + static void OnSalomeViewTryClose(int salome_view_id); + + protected: + + private: + class Internal; + + static CurvePlot * _instance; + + CurvePlot(bool testMode); + virtual ~CurvePlot(); + + Internal * _impl; + }; +} + +#endif /* SRC_CPP_CURVEPLOT_HXX_ */ diff --git a/tools/CurvePlot/src/cpp/CurvePlot_Exception.hxx b/tools/CurvePlot/src/cpp/CurvePlot_Exception.hxx new file mode 100644 index 000000000..f8f50579f --- /dev/null +++ b/tools/CurvePlot/src/cpp/CurvePlot_Exception.hxx @@ -0,0 +1,40 @@ +// Copyright (C) 2010-2015 CEA/DEN, EDF R&D +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +// +// Author : Adrien BRUNETON +// + +#include +#include + +namespace CURVEPLOT +{ + class Exception : public std::exception + { + public: + Exception(const std::string & what) : _what(what) {} + const char* what() const throw() { return _what.c_str(); } + + ~Exception() throw () {} + private: + const std::string _what; + }; +} + + + diff --git a/tools/CurvePlot/src/cpp/test/CMakeLists.txt b/tools/CurvePlot/src/cpp/test/CMakeLists.txt new file mode 100755 index 000000000..17e1c157a --- /dev/null +++ b/tools/CurvePlot/src/cpp/test/CMakeLists.txt @@ -0,0 +1,51 @@ +# Copyright (C) 2012-2015 CEA/DEN, EDF R&D, OPEN CASCADE +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# + +# additional include directories +INCLUDE_DIRECTORIES( + ${PYTHON_INCLUDE_DIRS} + ${GUI_INCLUDE_DIRS} + ${QT_INCLUDES} + ${CMAKE_CURRENT_SOURCE_DIR}/.. +) + +# additional preprocessor / compiler flags +ADD_DEFINITIONS(${PYTHON_DEFINITIONS} ${QT_DEFINITIONS}) + +# libraries to link to +SET(_link_LIBRARIES ${PYTHON_LIBRARIES} ${GUI_PyInterp} ${QT_LIBRARIES} CurvePlot) + +# --- headers --- + +SET(_moc_HEADERS + test_curveplot.hxx +) + +# sources / static +SET(_other_SOURCES + test_curveplot.cxx +) +# --- rules --- + +# sources / moc wrappings +QT4_WRAP_CPP(_moc_SOURCES ${_moc_HEADERS}) + +ADD_EXECUTABLE(test_curveplot ${_other_SOURCES} ${_moc_SOURCES}) +TARGET_LINK_LIBRARIES(test_curveplot ${_link_LIBRARIES}) + diff --git a/tools/CurvePlot/src/cpp/test/test_curveplot.cxx b/tools/CurvePlot/src/cpp/test/test_curveplot.cxx new file mode 100644 index 000000000..3954b7d1c --- /dev/null +++ b/tools/CurvePlot/src/cpp/test/test_curveplot.cxx @@ -0,0 +1,139 @@ +// Copyright (C) 2010-2015 CEA/DEN, EDF R&D +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +// +// Author : Adrien BRUNETON +// + +#include "test_curveplot.hxx" + +#include // GUI +#include +#include +#include "CurvePlot.hxx" + +#include // GUI + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace CURVEPLOT; + +/* The real test is in this function ! */ +void TestCurvePlot::onClicked() +{ + int ps_id = -1; + std::cout << "click\n" << std::endl; + + /* Now the real test: */ +// std::vector x = {1.0,2.0,3.0,4.0,5.0,6.0,7.0}; + std::vector x; + for (int i=1; i <= 7; i++) + x.push_back(double(i)); + //std::vector y = {1.0,4.0,9.0,16.0,25.0,36.0,49.0}; + std::vector y; + for (int i=0; i < 7; i++) + y.push_back(double(i*i)); +// std::vector x(2000); +// std::vector y(2000); +// for(int i = 0 ; i < x.size(); i++) +// { +// x[i] = i*1.0; +// y[i] = i*2.0; +// } + ColumnVector xx(ColumnVector::BuildFromStdVector(x)); + ColumnVector yy(ColumnVector::BuildFromStdVector(y)); +// std::string s = xx.toStdString(); +// std::cout << "test xx: " << s << std::endl; + std::cout << "setting X label " << CurvePlot::SetXLabel("tôtô") << std::endl; + PlotID crv_id = CurvePlot::AddCurve(xx, yy, ps_id, "the cérve", "th x", "the y-s", false); + std::cout << "setting X label " << CurvePlot::SetXLabel("tôtô") << std::endl; +} + +void initPython() +{ + if (!Py_IsInitialized()){ + // Python is not initialized + Py_Initialize(); // Initialize the interpreter + + PyEval_InitThreads(); // Create (and acquire) the Python global interpreter lock (GIL) + PyEval_ReleaseLock(); + } +} + +/* Little hack to gather widgets created on the Python side */ +void getWigdets(QApplication * app, QWidget *& crvBrowser, QWidget *& tabWidget) +{ + QList lst(app->topLevelWidgets()); + crvBrowser = NULL; + tabWidget = NULL; + foreach(QWidget * w, lst) + { + if (w->objectName() == QString("TabWidget")) + tabWidget = w; + if (w->objectName() == QString("CurveTreeDockWidget")) + crvBrowser = w; + } +} + +int main(int argc, char ** argv) +{ + int ret = -1; + /* The below part is done automatically in SALOME context */ + QApplication app (argc, argv); + QDesktopWidget * dw = app.desktop(); + + QMainWindow mw; + mw.resize((int)(dw->width()*0.25), (int)(dw->height()*0.7)); + mw.show(); + + initPython(); + InitializeCurvePlot(); + + { + // Make sure the first instanciation of CurvePlot is made in test mode! + CurvePlot::ToggleCurveBrowser(false); + CurvePlot::GetInstance(true); + + QWidget * crvBrowser = 0, * tabWidget = 0; + getWigdets(&app, crvBrowser, tabWidget); + QDockWidget * dock = new QDockWidget(&mw); + QPushButton * but = new QPushButton("Hello"); + TestCurvePlot * t2d = new TestCurvePlot(); + but->connect(but, SIGNAL(clicked()), t2d, SLOT(onClicked())); + QWidget * w = new QWidget(dock); + dock->setWidget(w); + QVBoxLayout * vbl = new QVBoxLayout(w); + vbl->addWidget(but); + if (crvBrowser) + vbl->addWidget(crvBrowser); + mw.addDockWidget(Qt::LeftDockWidgetArea, dock); + mw.setCentralWidget(tabWidget); + + /* Finalization */ + ret = app.exec(); + } + + Py_Finalize(); // must be after GIL release + return ret; +} diff --git a/tools/CurvePlot/src/cpp/test/test_curveplot.hxx b/tools/CurvePlot/src/cpp/test/test_curveplot.hxx new file mode 100644 index 000000000..7d646bc63 --- /dev/null +++ b/tools/CurvePlot/src/cpp/test/test_curveplot.hxx @@ -0,0 +1,10 @@ +#include + +class TestCurvePlot : public QObject +{ + Q_OBJECT + +public slots: + void onClicked(); + +}; diff --git a/tools/CurvePlot/src/python/CMakeLists.txt b/tools/CurvePlot/src/python/CMakeLists.txt new file mode 100644 index 000000000..779111c0b --- /dev/null +++ b/tools/CurvePlot/src/python/CMakeLists.txt @@ -0,0 +1,27 @@ +# Copyright (C) 2012-2014 CEA/DEN, EDF R&D +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# + +ADD_SUBDIRECTORY(controller) +ADD_SUBDIRECTORY(ui) +ADD_SUBDIRECTORY(model) +ADD_SUBDIRECTORY(views) +ADD_SUBDIRECTORY(pyqtside) +IF(SALOME_BUILD_TESTS) + ADD_SUBDIRECTORY(test) +ENDIF() diff --git a/tools/CurvePlot/src/python/controller/CMakeLists.txt b/tools/CurvePlot/src/python/controller/CMakeLists.txt new file mode 100644 index 000000000..1c51e37f5 --- /dev/null +++ b/tools/CurvePlot/src/python/controller/CMakeLists.txt @@ -0,0 +1,28 @@ +# Copyright (C) 2012-2014 CEA/DEN, EDF R&D +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# + +SALOME_CONFIGURE_FILE(utils.py.in ${CMAKE_CURRENT_BINARY_DIR}/utils.py) + +SET(_all_lib_SCRIPTS + PlotController.py + __init__.py + ${CMAKE_CURRENT_BINARY_DIR}/utils.py +) + +SALOME_INSTALL_SCRIPTS("${_all_lib_SCRIPTS}" ${SALOME_CURVEPLOT_INSTALL_PYTHON}) diff --git a/tools/CurvePlot/src/python/controller/PlotController.py b/tools/CurvePlot/src/python/controller/PlotController.py new file mode 100644 index 000000000..76be5e6a6 --- /dev/null +++ b/tools/CurvePlot/src/python/controller/PlotController.py @@ -0,0 +1,710 @@ +from CurveBrowserView import CurveBrowserView +from PlotManager import PlotManager +from CurveTabsView import CurveTabsView +from CurveModel import CurveModel +from TableModel import TableModel +from utils import Logger +import numpy as np + +class PlotController(object): + """ Controller for 2D curve plotting functionalities. + """ + __UNIQUE_INSTANCE = None # my poor impl. of a singleton + + ## For testing purposes: + WITH_CURVE_BROWSER = True + WITH_CURVE_TABS = True + + def __init__(self, sgPyQt=None): + if self.__UNIQUE_INSTANCE is None: + self.__trueInit(sgPyQt) + else: + raise Exception("The PlotController must be a singleton - use GetInstance()") + + def __trueInit(self, sgPyQt=None): + if sgPyQt is None: + import SalomePyQt + sgPyQt = SalomePyQt.SalomePyQt() + self._sgPyQt = sgPyQt + self._modelViews = {} + self._browserContextualMenu = None + self._blockNotifications = False + self._blockViewClosing = False + self._callbacks = [] + + self._plotManager = PlotManager(self) + + if self.WITH_CURVE_BROWSER: + self._curveBrowserView = CurveBrowserView(self) + self.associate(self._plotManager, self._curveBrowserView) + else: + self._curveBrowserView = None + if self.WITH_CURVE_TABS: + self._curveTabsView = CurveTabsView(self) + self.associate(self._plotManager, self._curveTabsView) + else: + self._curveTabsView = None + PlotController.__UNIQUE_INSTANCE = self + + @classmethod + def GetInstance(cls, sgPyQt=None): + if cls.__UNIQUE_INSTANCE is None: + # First instanciation: + PlotController(sgPyQt) + return cls.__UNIQUE_INSTANCE + + @classmethod + def Destroy(cls): + cls.__UNIQUE_INSTANCE = None + + def setFixedSizeWidget(self): + """ For testing purposes - ensure visible Qt widgets have a fixed size. + """ + if self.WITH_CURVE_BROWSER: + self._curveBrowserView.treeWidget.resize(100,200) + if self.WITH_CURVE_TABS: + self._sgPyQt._tabWidget.resize(600,600) + + def associate(self, model, view): + """ + Associates a model to a view, and sets the view to listen to this model + changes. + + :param model: Model -- The model to be associated to the view. + :param view: View -- The view. + + """ + if model is None or view is None: + return + + view.setModel(model) + self.setModelListener(model, view) + + def setModelListener(self, model, view): + """ + Sets a view to listen to all changes of the given model + """ + l = self._modelViews.setdefault(model, []) + if not view in l and view is not None: + l.append(view) + + def removeModelListeners(self, model): + """ + Removes the given model from the list of listeners. All views previously connected to this model + won't receive its update notification anymore. + """ + self._modelViews.pop(model) + + def notify(self, model, what=""): + """ + Notifies the view when model changes. + + :param model: Model -- The updated model. + """ + if model is None or self._blockNotifications: + return + + if not self._modelViews.has_key(model): + return + + for view in self._modelViews[model]: + method = "on%s" % what + if what != "" and what is not None and hasattr(view, method): + exec "view.%s()" % method + elif hasattr(view, "update"): + # Generic update: + view.update() + + def setBrowserContextualMenu(self, menu): + """ Provide a menu to be contextually shown in the curve browser """ + self._browserContextualMenu = menu + + def setCurvePlotRequestingClose(self, bool): + self._blockViewClosing = bool + + def onCurrentCurveChange(self): + ps = self._plotManager.getCurrentPlotSet() + if not ps is None: + crv = ps.getCurrentCurve() + if crv is not None: + crv_id = crv.getID() + for c in self._callbacks: + c(crv_id) + + ##### + ##### Public static API + ##### + + @classmethod + def AddCurve(cls, x, y, curve_label="", x_label="", y_label="", append=True): + """ Add a new curve and make the plot set where it is drawn the active one. + If no plot set exists, or none is active, a new plot set will be created, even if append is True. + @param x x data + @param y y data + @param curve_label label of the curve being ploted (optional, default to empty string). This is what is + shown in the legend. + @param x_label label for the X axis + @param y_label label for the Y axis + @param append whether to add the curve to the active plot set (default) or into a new one. + @return the id of the created curve, and the id of the corresponding plot set. + """ + from XYView import XYView + control = cls.GetInstance() + pm = control._plotManager + t = TableModel(control) + data = np.transpose(np.vstack([x, y])) + t.setData(data) + # ensure a single Matplotlib repaint for all operations to come in AddCurve + prevLock = pm.isRepaintLocked() + if not prevLock: + pm.lockRepaint() + curveID, plotSetID = control.plotCurveFromTable(t, x_col_index=0, y_col_index=1, + curve_label=curve_label, append=append) + ps = pm._plotSets[plotSetID] + if x_label != "": + ps.setXLabel(x_label) + if y_label != "": + ps.setYLabel(y_label) + if not prevLock: + pm.unlockRepaint() + return curveID, plotSetID + + @classmethod + def ExtendCurve(cls, crv_id, x, y): + """ Add new points to an already created curve + @raise if invalid plot set ID is given + """ + control = cls.GetInstance() + ps = control._plotManager.getPlotSetContainingCurve(crv_id) + if ps is None: + raise ValueError("Curve ID (%d) not found for extension!" % crv_id) + crv_mod = ps._curves[crv_id] + data = np.transpose(np.vstack([x, y])) + crv_mod.extendData(data) + + @classmethod + def ResetCurve(cls, crv_id): + """ Reset a given curve: all data are cleared, but the curve is still + alive with all its attributes (color, etc ...). Mostly used in conjunction + with ExtendCurve above + @raise if invalid plot set ID is given + """ + control = cls.GetInstance() + ps = control._plotManager.getPlotSetContainingCurve(crv_id) + if ps is None: + raise ValueError("Curve ID (%d) not found for reset!" % crv_id) + crv_mod = ps._curves[crv_id] + crv_mod.resetData() + + @classmethod + def AddPlotSet(cls, title=""): + """ Creates a new plot set (a tab with several curves) and returns its ID. A title can be passed, + otherwise a default one will be created. + By default this new plot set becomes the active one. + """ + control = cls.GetInstance() + ps = control._plotManager.createXYPlotSet() + control.setModelListener(ps, control._curveBrowserView) + # Controller itself must be notified for curve picking: + control.setModelListener(ps, control) + if title != "": + ps.setTitle(title) + return ps.getID() + + @classmethod + def CopyCurve(cls, curve_id, plot_set_id): + """ Copy a given curve to a given plot set ID + @return ID of the newly created curve + """ + control = cls.GetInstance() + psID = cls.GetPlotSetID(curve_id) + if psID == -1: + raise ValueError("Curve ID (%d) not found for duplication!" % curve_id) + plot_set_src = control._plotManager._plotSets[psID] + plot_set_tgt = control._plotManager._plotSets.get(plot_set_id, None) + if plot_set_tgt is None: + raise ValueError("Plot set ID (%d) invalid for duplication!" % plot_set_id) + crv = plot_set_src._curves[curve_id] + new_crv = crv.clone() + control.setModelListener(new_crv, control._curveBrowserView) + plot_set_tgt.addCurve(new_crv) + return new_crv.getID() + + @classmethod + def DeleteCurve(cls, curve_id=-1): + """ By default, delete the current curve, if any. Otherwise do nothing. + @return the id of the deleted curve or -1 + """ + Logger.Debug("Delete curve") + control = cls.GetInstance() + # Find the right plot set: + if curve_id == -1: + curve_id = cls.GetCurrentCurveID() + if curve_id == -1: + # No current curve, do nothing + return -1 + + psID = cls.GetPlotSetID(curve_id) + if psID == -1: + raise ValueError("Curve ID (%d) not found for deletion!" % curve_id) + crv = control._plotManager._plotSets[psID]._curves[curve_id] + control._plotManager._plotSets[psID].removeCurve(curve_id) + control.removeModelListeners(crv) + return curve_id + + @classmethod + def DeletePlotSet(cls, plot_set_id=-1): + """ By default, delete the current plot set, if any. Otherwise do nothing. + This will automatically make the last added plot set the current one. + @return the id of the deleted plot set or -1 + """ + Logger.Debug("PlotController::DeletePlotSet %d" % plot_set_id) + control = cls.GetInstance() + # Find the right plot set: + if plot_set_id == -1: + plot_set_id = cls.GetCurrentPlotSetID() + if plot_set_id == -1: + # No current, do nothing + return -1 + + ps = control._plotManager.removeXYPlotSet(plot_set_id) + for _, crv in ps._curves.items(): + control.removeModelListeners(crv) + control.removeModelListeners(ps) + psets = control._plotManager._plotSets + if len(psets): + control._plotManager.setCurrentPlotSet(psets.keys()[-1]) + return plot_set_id + + @classmethod + def usedMem(cls): + import gc + gc.collect() + import resource + m = resource.getrusage(resource.RUSAGE_SELF)[2]*resource.getpagesize()/1e6 + print "** Used memory: %.2f Mb" % m + + @classmethod + def DeleteCurrentItem(cls): + """ Delete currently active item, be it a plot set or a curve. + @return (True, plot_sed_id) if a plot set was deleted or (False, curve_id) if a curve was deleted, or (True, -1) + if nothing was deleted. + """ + c_id = cls.GetCurrentCurveID() + ps_id = cls.GetCurrentPlotSetID() + ret = True, -1 + if ps_id == -1: + Logger.Info("PlotController.DeleteCurrentItem(): nothing selected, nothing to delete!") + return True,-1 + # Do we delete a curve or a full plot set + if c_id == -1: + cls.DeletePlotSet(ps_id) + ret = True, ps_id + else: + cls.DeleteCurve(c_id) + ret = False, c_id + return ret + + @classmethod + def ClearPlotSet(cls, ps_id=-1): + """ Clear all curves in a given plot set. By default clear the current plot set without deleting it, + if no default plot set is currently active, do nothing. + @return id of the cleared plot set + @raise if invalid plot set ID is given + """ + pm = cls.GetInstance()._plotManager + if ps_id == -1: + ps_id = cls.GetCurrentPlotSetID() + if ps_id == -1: + return ps_id + ps = pm._plotSets.get(ps_id, None) + if ps is None: + raise ValueError("Invalid plot set ID (%d)!" % ps_id) + ps.eraseAll() + return ps_id + +# @classmethod +# def ClearAll(cls): +# # TODO: optimize +# pm = cls.GetInstance()._plotManager +# ids = pm._plotSets.keys() +# for i in ids: +# cls.DeletePlotSet(i) + + @classmethod + def SetXLabel(cls, x_label, plot_set_id=-1): + """ By default set the X axis label for the current plot set, if any. Otherwise do nothing. + @return True if the label was set + """ + pm = cls.GetInstance()._plotManager + if plot_set_id == -1: + plot_set_id = cls.GetCurrentPlotSetID() + if plot_set_id == -1: + # Do nothing + return False + ps = pm._plotSets.get(plot_set_id, None) + if ps is None: + raise Exception("Invalid plot set ID (%d)!" % plot_set_id) + ps.setXLabel(x_label) + return True + + @classmethod + def SetYLabel(cls, y_label, plot_set_id=-1): + """ By default set the Y axis label for the current plot set, if any. Otherwise do nothing. + @return True if the label was set + """ + pm = cls.GetInstance()._plotManager + if plot_set_id == -1: + plot_set_id = cls.GetCurrentPlotSetID() + if plot_set_id == -1: + # Do nothing + return False + ps = pm._plotSets.get(plot_set_id, None) + if ps is None: + raise Exception("Invalid plot set ID (%d)!" % plot_set_id) + ps.setYLabel(y_label) + return True + + @classmethod + def SetPlotSetTitle(cls, title, plot_set_id=-1): + """ By default set the title for the current plot set, if any. Otherwise do nothing. + @return True if the title was set + """ + pm = cls.GetInstance()._plotManager + if plot_set_id == -1: + plot_set_id = cls.GetCurrentPlotSetID() + if plot_set_id == -1: + # Do nothing + return False + ps = pm._plotSets.get(plot_set_id, None) + if ps is None: + raise Exception("Invalid plot set ID (%d)!" % plot_set_id) + ps.setTitle(title) + return True + + @classmethod + def GetPlotSetID(cls, curve_id): + """ @return plot set id for a given curve or -1 if invalid curve ID + """ + control = cls.GetInstance() + cps = control._plotManager.getPlotSetContainingCurve(curve_id) + if cps is None: + return -1 + return cps.getID() + + @classmethod + def GetPlotSetIDByName(cls, name): + """ @return the first plot set whose name matches the provided name. Otherwise returns -1 + """ + pm = cls.GetInstance()._plotManager + for _, ps in pm._plotSets.items(): + if ps._title == name: + return ps.getID() + return -1 + + @classmethod + def GetAllPlotSets(cls): + """ @return two lists: plot set names, and corresponding plot set IDs + """ + pm = cls.GetInstance()._plotManager + it = pm._plotSets.items() + ids, inst, titles = [], [], [] + if len(it): + ids, inst = zip(*it) + if len(inst): + titles = [i.getTitle() for i in inst] + return list(ids), titles + + @classmethod + def GetCurrentCurveID(cls): + """ @return current curve ID or -1 if no curve is currently active + """ + control = cls.GetInstance() + crv = control._plotManager.getCurrentCurve() + if crv is None: + return -1 + return crv.getID() + + @classmethod + def GetCurrentPlotSetID(cls): + """ @return current plot set ID or -1 if no plot set is currently active + """ + control = cls.GetInstance() + cps = control._plotManager.getCurrentPlotSet() + if cps is None: + return -1 + return cps.getID() + + @classmethod + def SetCurrentPlotSet(cls, ps_id): + """ Set the current active plot set. Use -1 to unset any current plot set. + @throw if invalid ps_id + """ + control = cls.GetInstance() + control._plotManager.setCurrentPlotSet(ps_id) + + @classmethod + def SetCurrentCurve(cls, crv_id): + """ Set the current active curve. + @return corresponding plot set ID + @throw if invalid crv_id + """ + control = cls.GetInstance() + ps_id = control._plotManager.setCurrentCurve(crv_id) + return ps_id + + @classmethod + def ActiveViewChanged(cls, viewID): + """ This method is to be plugged direclty in the activeViewChanged() slot of a standard + Python SALOME module so that the curve browser stays in sync with the selected SALOME view + """ + control = cls.GetInstance() + # Get XYView from SALOME view ID + xyview = control._curveTabsView._XYViews.get(viewID, None) + if not xyview is None: + plotSetID = xyview.getModel().getID() + control._plotManager.setCurrentPlotSet(plotSetID) + + @classmethod + def ToggleCurveBrowser(cls, active): + if cls.__UNIQUE_INSTANCE is not None: + raise Exception("ToggleCurveBrowser() must be invoked before doing anything in plot2D!") + cls.WITH_CURVE_BROWSER = active + + @classmethod + def IsValidPlotSetID(cls, plot_set_id): + """ + @return True if plot_set_id is the identifier of a valid and existing plot set. + """ + control = cls.GetInstance() + return control._plotManager._plotSets.has_key(plot_set_id) + + @classmethod + def GetSalomeViewID(cls, plot_set_id): + """ + @return the salome view ID associated to a given plot set. -1 if invalid plot_set_id + """ + control = cls.GetInstance() + d = control._curveTabsView.mapModId2ViewId() + return d.get(plot_set_id, -1) + + @classmethod + def OnSalomeViewTryClose(cls, salome_view_id): + control = cls.GetInstance() + if not control._blockViewClosing: + Logger.Debug("PlotController::OnSalomeViewTryClose %d" % salome_view_id) +# control._sgPyQt.setViewClosable(salome_view_id, False) + # Get XYView from SALOME view ID + xyview = control._curveTabsView._XYViews.get(salome_view_id, None) + if not xyview is None: + plotSetID = xyview.getModel().getID() + Logger.Debug("PlotController::OnSalomeViewTryClose internal CurvePlot view ID is %d" % plotSetID) + control._plotManager.removeXYPlotSet(plotSetID) + else: + Logger.Warning("Internal error - could not match SALOME view ID %d with CurvePlot view!" % salome_view_id) + + @classmethod + def SetCurveMarker(cls, crv_id, marker): + """ Change curve marker. Available markers are: + CURVE_MARKERS = [ "o" ,# circle + "*", # star + "+", # plus + "x", # x + "s", # square + "p", # pentagon + "h", # hexagon1 + "8", # octagon + "D", # diamond + "^", # triangle_up + "<", # triangle_left + ">", # triangle_right + "1", # tri_down + "2", # tri_up + "3", # tri_left + "4", # tri_right + "v", # triangle_down + "H", # hexagon2 + "d", # thin diamond + "", # NO MARKER + ] + @raise if invalid curve ID or marker + """ + from XYView import XYView + from CurveView import CurveView + if not marker in XYView.CURVE_MARKERS: + raise ValueError("Invalid marker: '%s'" % marker) + + cont = cls.GetInstance() + for mod, views in cont._modelViews.items(): + if isinstance(mod, CurveModel) and mod.getID() == crv_id: + for v in views: + if isinstance(v, CurveView): + v.setMarker(marker) + # Update curve display and legend: + v._parentXYView.repaint() + v._parentXYView.showHideLegend() + found = True + + if not found: + raise Exception("Invalid curve ID or curve currently not displayed (curve_id=%d)!" % crv_id) + + @classmethod + def SetCurveLabel(cls, crv_id, label): + """ Change curve label + @raise if invalid curve id + """ + cont = cls.GetInstance() + cps = cont._plotManager.getPlotSetContainingCurve(crv_id) + if cps is None: + raise ValueError("Invalid curve ID: %d" % crv_id) + cps._curves[crv_id].setTitle(label) + + @classmethod + def __XYViewOperation(cls, func, ps_id, args, kwargs): + """ Private. To factorize methods accessing the XYView to change a display element. """ + from XYPlotSetModel import XYPlotSetModel + from XYView import XYView + + cont = cls.GetInstance() + for mod, views in cont._modelViews.items(): + if isinstance(mod, XYPlotSetModel) and mod.getID() == ps_id: + for v in views: + if isinstance(v, XYView): + exec "v.%s(*args, **kwargs)" % func + found = True + if not found: + raise Exception("Invalid plot set ID or plot set currently not displayed (ps_id=%d)!" % ps_id) + + + @classmethod + def SetXLog(cls, ps_id, log=True): + """ Toggle the X axis into logarithmic scale. + @param ps_id plot set ID + @param log if set to True, log scale is used, otherwise linear scale is used + @raise if invalid plot set ID + """ + args, kwargs = [log], {} + cls.__XYViewOperation("setXLog", ps_id, args, kwargs) + + @classmethod + def SetYLog(cls, ps_id, log=True): + """ Toggle the Y axis into logarithmic scale. + @param ps_id plot set ID + @param log if set to True, log scale is used, otherwise linear scale is used + @raise if invalid plot set ID + """ + args, kwargs = [log], {} + cls.__XYViewOperation("setYLog", ps_id, args, kwargs) + + @classmethod + def SetXSciNotation(cls, ps_id, sciNotation=False): + """ Change the format (scientific notation or not) of the X axis. + @param ps_id plot set ID + @param sciNotation if set to True, scientific notation is used, otherwise plain notation is used + @raise if invalid plot set ID + """ + args, kwargs = [sciNotation], {} + cls.__XYViewOperation("setXSciNotation", ps_id, args, kwargs) + + @classmethod + def SetYSciNotation(cls, ps_id, sciNotation=False): + """ Change the format (scientific notation or not) of the Y axis. + @param ps_id plot set ID + @param sciNotation if set to True, scientific notation is used, otherwise plain notation is used + @raise if invalid plot set ID + """ + args, kwargs = [sciNotation], {} + cls.__XYViewOperation("setYSciNotation", ps_id, args, kwargs) + + @classmethod + def SetLegendVisible(cls, ps_id, visible=True): + """ Change the visibility of the legend. + @param ps_id plot set ID + @param visible if set to True, show legend, otherwise hide it. + @raise if invalid plot set ID + """ + args, kwargs = [visible], {} + cls.__XYViewOperation("setLegendVisible", ps_id, args, kwargs) + + + ### + ### More advanced functions + ### + @classmethod + def RegisterCallback(cls, callback): + cont = cls.GetInstance() + cont._callbacks.append(callback) + + @classmethod + def ClearCallbacks(cls): + cont = cls.GetInstance() + cont._callbacks = [] + + @classmethod + def LockRepaint(cls): + control = cls.GetInstance() + control._plotManager.lockRepaint() + + @classmethod + def UnlockRepaint(cls): + control = cls.GetInstance() + control._plotManager.unlockRepaint() + + def createTable(self, data, table_name="table"): + t = TableModel(self) + t.setData(data) + t.setTitle(table_name) + return t + + def plotCurveFromTable(self, table, x_col_index=0, y_col_index=1, curve_label="", append=True): + """ + :returns: a tuple containing the unique curve ID and the plot set ID + """ + # Regardless of 'append', we must create a view if none there: + if self._plotManager.getCurrentPlotSet() is None or not append: + ps = self._plotManager.createXYPlotSet() + self.setModelListener(ps, self._curveBrowserView) + # For curve picking, controller must listen: + self.setModelListener(ps, self) + cps_title = table.getTitle() + else: + cps_title = None + + cps = self._plotManager.getCurrentPlotSet() + + cm = CurveModel(self, table, y_col_index) + cm.setXAxisIndex(x_col_index) + + # X axis label + tix = table.getColumnTitle(x_col_index) + if tix != "": + cps.setXLabel(tix) + + # Curve label + if curve_label != "": + cm.setTitle(curve_label) + else: + ti = table.getColumnTitle(y_col_index) + if ti != "": + cm.setTitle(ti) + + # Plot set title + if cps_title != "" and cps_title is not None: + Logger.Debug("about to set title to: " + cps_title) + cps.setTitle(cps_title) + + cps.addCurve(cm) + mp = self._curveTabsView.mapModId2ViewId() + xyview_id = mp[cps.getID()] + xyview = self._curveTabsView._XYViews[xyview_id] + + if cps_title is None: # no plot set was created above + self._plotManager.setCurrentPlotSet(cps.getID()) + + # Make CurveBrowser and CurveView depend on changes in the curve itself: + self.setModelListener(cm, self._curveBrowserView) + self.setModelListener(cm, xyview._curveViews[cm.getID()]) + # Upon change on the curve also update the full plot, notably for the auto-fit and the legend: + self.setModelListener(cm, xyview) + + return cm.getID(),cps.getID() diff --git a/tools/CurvePlot/src/python/controller/__init__.py b/tools/CurvePlot/src/python/controller/__init__.py new file mode 100644 index 000000000..fd0b521e3 --- /dev/null +++ b/tools/CurvePlot/src/python/controller/__init__.py @@ -0,0 +1,58 @@ +""" +CurvePlot Python package. +""" +try: + # Some unicode chars are improperly rendered with the default 'tkagg' backend (delta for ex) + # and the Bitstream font which is the first one by default. Try to use DejaVu which is more + # comprehensive. + ## !!Order of the sequence below is highly sensitive!! + import pyqtside # will trigger the PySide/PyQt4 switch + import matplotlib + matplotlib.use('Qt4Agg') + import matplotlib.pyplot as plt # must come after the PySide/PyQt4 switch! + plt.rcParams['font.sans-serif'].insert(0, u"DejaVu Sans") +except: + print "Warning: could not switch matplotlib to 'Qt4agg' backend. Some characters might be displayed improperly!" + +from PlotController import PlotController +from TableModel import TableModel +from CurveModel import CurveModel +from PlotManager import PlotManager +from XYPlotSetModel import XYPlotSetModel + +## The static API of PlotController is the main interface of the package and is hence exposed at package level: +AddCurve = PlotController.AddCurve +AddPlotSet = PlotController.AddPlotSet +ExtendCurve = PlotController.ExtendCurve +ResetCurve = PlotController.ResetCurve +CopyCurve = PlotController.CopyCurve +DeleteCurve = PlotController.DeleteCurve +DeletePlotSet = PlotController.DeletePlotSet +ClearPlotSet = PlotController.ClearPlotSet +SetXLabel = PlotController.SetXLabel +SetYLabel = PlotController.SetYLabel +GetPlotSetID = PlotController.GetPlotSetID +GetPlotSetIDByName = PlotController.GetPlotSetIDByName +GetAllPlotSets = PlotController.GetAllPlotSets +GetCurrentCurveID = PlotController.GetCurrentCurveID +GetCurrentPlotSetID = PlotController.GetCurrentPlotSetID +SetCurrentCurve = PlotController.SetCurrentCurve +SetCurrentPlotSet = PlotController.SetCurrentPlotSet +DeleteCurrentItem = PlotController.DeleteCurrentItem +SetCurveMarker = PlotController.SetCurveMarker +SetCurveLabel = PlotController.SetCurveLabel +SetXLog = PlotController.SetXLog +SetYLog = PlotController.SetYLog +SetXSciNotation = PlotController.SetXSciNotation +SetYSciNotation = PlotController.SetYSciNotation +RegisterCallback = PlotController.RegisterCallback +ClearCallbacks = PlotController.ClearCallbacks +GetSalomeViewID = PlotController.GetSalomeViewID +SetLegendVisible = PlotController.SetLegendVisible +SetPlotSetTitle = PlotController.SetPlotSetTitle + +# For advanced usage only: +GetInstance = PlotController.GetInstance +LockRepaint = PlotController.LockRepaint +UnlockRepaint = PlotController.UnlockRepaint + diff --git a/tools/CurvePlot/src/python/controller/utils.py.in b/tools/CurvePlot/src/python/controller/utils.py.in new file mode 100644 index 000000000..87f8e76c2 --- /dev/null +++ b/tools/CurvePlot/src/python/controller/utils.py.in @@ -0,0 +1,85 @@ +class Logger(object): + """ + Debug Info. + """ + LOG_LEVEL = 1 # 0 means all, 1 means all but DEBUG, 2 means all but INFO and DEBUG, 3 only FATAL + + @classmethod + def Debug(cls, msg): + """ + Prints an information message to the standard output. + + :param msg: str -- The message to be printed. + + """ + if cls.LOG_LEVEL <= 0: + cls.__log("[DEBUG]", msg) + + + @classmethod + def Info(cls, msg): + """ + Prints an information message to the standard output. + + :param msg: str -- The message to be printed. + + """ + if cls.LOG_LEVEL <= 1: + cls.__log("[INFO]", msg) + + + @classmethod + def Warning(cls, msg): + """ + Prints a warning message to the standard output. + + :param msg: str -- The message to be printed. + + """ + if cls.LOG_LEVEL <= 2: + cls.__log("[WARNING]", msg) + + + @classmethod + def FatalError(cls, msg): + """ + Prints an error message to the standard output. + + :param msg: str -- The message to be printed. + :raises: Exception. + + """ + if cls.LOG_LEVEL <= 3: + cls.__log("[FATAL]", msg) + raise Exception(msg) + + @classmethod + def __log(cls, typ, msg): + print "%s: %s" % (typ, msg) + +def trQ(tag, context="CURVEPLOT"): + """ @return a QString read from the translation file """ + from pyqtside.QtGui import QApplication + return QApplication.translate(context, tag) + +def trU(tag, context="CURVEPLOT"): + """ @return same as above, but returns a Python unicode string. """ + qs = trQ(tag, context) + return unicode(qs, 'utf-8') + +def toUnicodeWithWarning(s, method_name): + try: + s = unicode(s) + except: + Logger.Warning("%s - warning, passing non-unicode, non-ASCII string '%s'! Trying to convert myself to UTF-8 ..." % (method_name, s)) + s = unicode(s, 'utf-8') + return s + +def completeResPath(fileName): + import os + subPath = "@SALOME_CURVEPLOT_INSTALL_PYTHON@" + rd = os.environ.get("CURVEPLOT_ROOT_DIR", None) + if rd is None: + raise Exception("CURVEPLOT_ROOT_DIR is not defined!") + filePath = os.path.join(rd, subPath, fileName) + return filePath diff --git a/tools/CurvePlot/src/python/model/CMakeLists.txt b/tools/CurvePlot/src/python/model/CMakeLists.txt new file mode 100644 index 000000000..964d05ebf --- /dev/null +++ b/tools/CurvePlot/src/python/model/CMakeLists.txt @@ -0,0 +1,28 @@ +# Copyright (C) 2012-2014 CEA/DEN, EDF R&D +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# + +SET(_all_lib_SCRIPTS + Model.py + CurveModel.py + TableModel.py + PlotManager.py + XYPlotSetModel.py +) + +SALOME_INSTALL_SCRIPTS("${_all_lib_SCRIPTS}" ${SALOME_CURVEPLOT_INSTALL_PYTHON}) diff --git a/tools/CurvePlot/src/python/model/CurveModel.py b/tools/CurvePlot/src/python/model/CurveModel.py new file mode 100644 index 000000000..c11179679 --- /dev/null +++ b/tools/CurvePlot/src/python/model/CurveModel.py @@ -0,0 +1,68 @@ +from Model import Model +from utils import toUnicodeWithWarning + +class CurveModel(Model): + def __init__(self, controller, table=None, index=-1): + Model.__init__(self, controller) + self._title = "Curve %d" % self.getID() + self._table = table + self._yaxisIndex = index # column index in the table + self._xaxisIndex = 0 # By default the first column of the table is used for the X-s + + def clone(self): + ret = CurveModel(self._controller) + ret._title = self._title + ret._table = self._table # TO CHECK: deep copy here? (I think not needed in Python ...) + ret._yaxisIndex = self._yaxisIndex + ret._xaxisIndex = self._xaxisIndex + return ret + + def setTable(self, t, silent=False): + self._table = t + if not silent: + self.notifyChange("DataChange") + + def getTable(self): + return self._table + + def extendData(self, t, silent=False): + self._table.extend(t) + if not silent: + self.notifyChange("DataChange") + + def resetData(self): + self._table.clear() + self.notifyChange("DataChange") + + def setTitle(self, ti, silent=False): + ti = toUnicodeWithWarning(ti, "CurveModel::setTitle()") + self._title = ti + if not silent: + self.notifyChange("CurveTitleChange") + + def getTitle(self): + return self._title + + def getYAxisIndex(self): + return self._yaxisIndex + + def setYAxisIndex(self, idx, silent=False): + self._yaxisIndex = idx + if not silent: + self.notifyChange("YAxisIndexChange") + + def getXAxisIndex(self): + return self._xaxisIndex + + def setXAxisIndex(self, idx, silent=False): + sh = self._table.getShape() + if idx >= sh[1]: + raise ValueError("Index out of bound (is %d, but max is %d)" % (idx, sh[1])) + self._xaxisIndex = idx + if not silent: + self.notifyChange("XAxisIndexChange") + + def getID(self): + return self._id + + diff --git a/tools/CurvePlot/src/python/model/Model.py b/tools/CurvePlot/src/python/model/Model.py new file mode 100644 index 000000000..0db911bb6 --- /dev/null +++ b/tools/CurvePlot/src/python/model/Model.py @@ -0,0 +1,44 @@ +class Model(object): + START_ID = -1 + + @classmethod + def __GenerateID(cls): + cls.START_ID += 1 + return cls.START_ID + + def __init__( self, controller ): + """Constructor""" + + self._name = None + self._controller = controller + self._id = self.__GenerateID() # A unique ID for this class of object + + def getID(self): + return self._id + + def getController(self): + """ + :returns: Controller -- This model's controller. + """ + return self._controller + + def setController(self, controller): + """ + Sets the controller of this model. + + :param controller: Controller -- The controller of the model. + + """ + self._controller = controller + + def notifyChange( self, what="" ) : + """ + Notifies the controller that this model's data has changed. + """ + if self._controller != None: + self._controller.notify(self, what) + + def updateTimeStamps (self, modifiesList): + raise NotImplementedError + +pass diff --git a/tools/CurvePlot/src/python/model/PlotManager.py b/tools/CurvePlot/src/python/model/PlotManager.py new file mode 100644 index 000000000..665c2a763 --- /dev/null +++ b/tools/CurvePlot/src/python/model/PlotManager.py @@ -0,0 +1,99 @@ +from Model import Model +from XYPlotSetModel import XYPlotSetModel +from utils import Logger + +class PlotManager(Model): + def __init__(self, controller): + from collections import OrderedDict + Model.__init__(self, controller) + self._currentPlotSet = None + self._plotSets = OrderedDict() # key: plotSet ID, value: instance of XYPlotSetModel. We use an OrderedDict so that + # when removing elemetns, we can easily re-select the last-but-one. + self._lockRepaint = False # if True, repaint routines are blocked. + self._plotSetsRepaint = set() # plot waiting for repaint/update while repaint is locked + + def isEmpty(self): + return len(self._plotSets) == 0 + + def setCurrentPlotSet(self, plotSetID, silent=False): + if not self._plotSets.has_key(plotSetID) and plotSetID != -1: + raise ValueError("Invalid plot set ID (%d)!" % plotSetID) + self._currentPlotSet = self._plotSets.get(plotSetID, None) + if not silent: + self.notifyChange("CurrentPlotSetChange") + + def getCurrentPlotSet(self): + return self._currentPlotSet + + def getPlotSetContainingCurve(self, curveID): + for ps in self._plotSets.values(): + if ps._curves.has_key(curveID): + return ps + return None + + def setCurrentCurve(self, curveId): + ps = self.getPlotSetContainingCurve(curveId) + if ps is None and curveId != -1: + raise ValueError("Invalid curve ID (%d)!" % curveId) + self.clearAllCurrentCurve() + if curveId == -1: + return -1 + ps_id = ps.getID() + currPs = self.getCurrentPlotSet() + if currPs is None or currPs.getID() != ps_id: + self.setCurrentPlotSet(ps_id) + ps.setCurrentCurve(curveId) + return ps_id + + def getCurrentCurve(self): + ps = self.getCurrentPlotSet() + if ps is None: + return None + return ps.getCurrentCurve() + + def clearAllCurrentCurve(self, silent=False): + for psID in self._plotSets: + self._plotSets[psID].setCurrentCurve(-1) + if not silent: + self.notifyChange("CurrentCurveChange") + + def createXYPlotSet(self, silent=False): + cv = XYPlotSetModel(self._controller) + self._plotSets[cv.getID()] = cv + self._currentPlotSet = cv + if not silent: + self.notifyChange("NewPlotSet") + return cv + + def removeXYPlotSet(self, plotSetID): + Logger.Debug("====> PlotManager::removeXYPlotSet() %d" % plotSetID) + if not self._plotSets.has_key(plotSetID): + print self._plotSets + raise ValueError("Plot set ID (%d) not found for deletion!" % plotSetID) + ps = self._plotSets.pop(plotSetID) + if self._currentPlotSet is ps: + self._currentPlotSet = None + self.notifyChange("RemovePlotSet") + return ps + + def clearAll(self): + self._plotSets = {} + self._currentPlotSet = None + self.notifyChange("ClearAll") + + def lockRepaint(self): + self._lockRepaint = True + self._plotSetsRepaint = set() + + def isRepaintLocked(self): + return self._lockRepaint + + def registerRepaint(self, ps_id): + self._plotSetsRepaint.add(ps_id) + + def unlockRepaint(self): + self._lockRepaint = False + for obj in self._plotSetsRepaint: + obj.notifyChange() + self._plotSetsRepaint = set() + \ No newline at end of file diff --git a/tools/CurvePlot/src/python/model/TableModel.py b/tools/CurvePlot/src/python/model/TableModel.py new file mode 100644 index 000000000..2f30aa7db --- /dev/null +++ b/tools/CurvePlot/src/python/model/TableModel.py @@ -0,0 +1,93 @@ +import numpy as np +from Model import Model +from utils import toUnicodeWithWarning, Logger + +class TableModel(Model): + def __init__(self, controller): + Model.__init__(self, controller) + self._columnTitles = {} + self._data = None + self._title = "" + + def getData(self): + return self._data + + def __checkAndFormatData(self, data): + if isinstance(data, np.ndarray): + if len(data.shape) == 1: + data = np.resize(data, (data.shape[0], 1)) + elif len(data.shape) == 2: + pass + else: + raise ValueError("Invalid shape! Must be a vector or a rank-2 tensor (i.e. a matrix)!") + elif isinstance(data, list): + data = np.array((len(data), 1), dtype=np.float64) + data[:] = data + return data + + def setData(self, data): + data = self.__checkAndFormatData(data) + self._data = data + self.notifyChange("DataChange") + + def extend(self, data): + data = self.__checkAndFormatData(data) + if data.shape[1] != self._data.shape[1]: + raise ValueError("Invalid shape! Must have the same number of columns than already existing data!") + self._data = np.vstack([self._data, data]) + self.notifyChange("DataChange") + + def clear(self): + sh = self.getShape() + # Void data but keeping same number of cols: + self._data = np.zeros((0, sh[1])) + self.notifyChange("DataChange") + + def getShape(self): + if self._data is not None: + return self._data.shape + else: + return (0,0) + + def setTitle(self, ti): + ti = toUnicodeWithWarning(ti, "TableModel::setTitle()") + self._title = ti + self.notifyChange("TitleChange") + + def getTitle(self): + return self._title + + def addColumn(self, lst): + sh = self.getShape() + if sh != (0,0): + if len(lst) != sh[0]: + raise ValueError("Invalid number of rows in added column! (is %d, should be %d)" % (len(lst), sh[0])) + # Add a column + tmp = self._data + self._data = np.zeros((sh[0],sh[1]+1)) + self._data[:,:-1] = tmp + idx = -1 + else: + # First assignation + self._data = np.zeros((len(lst), 1), dtype=np.float64) + idx = 0 + self._data[:, idx] = lst + self.notifyChange("DataChange") + + def setColumnTitle(self, index, txt): + self._columnTitles[index] = txt + self.notifyChange("ColumnTitleChange") + + def getColumnTitle(self, index): + return self._columnTitles.get(index, "") + + def removeValue(self, nrow, ncol): + sh = self.getShape() + if nrow >= sh[0] or ncol >= sh[1]: + raise ValueError("Specified row and column (%d, %d) invalid with current data size (%d, %d)" % (nrow, ncol, sh[0], sh[1])) + self._data[nrow, ncol] = np.NaN + self.notifyChange("DataChange") + + def __str__(self): + return self._data.__str__() + diff --git a/tools/CurvePlot/src/python/model/XYPlotSetModel.py b/tools/CurvePlot/src/python/model/XYPlotSetModel.py new file mode 100644 index 000000000..18bb60e2f --- /dev/null +++ b/tools/CurvePlot/src/python/model/XYPlotSetModel.py @@ -0,0 +1,63 @@ +from Model import Model +from utils import toUnicodeWithWarning + +class XYPlotSetModel(Model): + + def __init__(self, controller): + Model.__init__(self, controller) + self._title = "Plot set %d" % self.getID() + self._curves = {} # Key: model ID, value: CurveModel + self._currentCurve = None + self._xlabel = "" + self._ylabel = "" + + def eraseAll(self): + self._curves = {} + self._currentCurve = None + self.notifyChange("ClearAll") + + def setTitle(self, title, silent=False): + title = toUnicodeWithWarning(title, "XYPlotSetModel::setTitle()") + self._title = title + if not silent: + self.notifyChange("TitleChange") + + def getTitle(self): + return self._title + + def setCurrentCurve(self, curveID, silent=False): + if not self._curves.has_key(curveID) and curveID != -1: + raise ValueError("Invalid curve ID (%d)!" % curveID) + self._currentCurve = self._curves.get(curveID, None) + if not silent: + self.notifyChange("CurrentCurveChange") + + def getCurrentCurve(self): + return self._currentCurve + + def addCurve(self, curve, silent=False): + self._curves[curve.getID()] = curve + if not silent: + self.notifyChange("AddCurve") + + def removeCurve(self, curveID, silent=False): + if not self._curves.has_key(curveID): + raise ValueError("Curve ID (%d) not found for deletion!" % curveID) + c = self._curves.pop(curveID) + if self._currentCurve is c: + self._currentCurve = None + if not silent: + self.notifyChange("RemoveCurve") + + def setXLabel(self, x_label, silent=False): + x_label = toUnicodeWithWarning(x_label, "XYPlotSetModel::setXLabel()") + self._xlabel = x_label + if not silent: + self.notifyChange("XLabelChange") + + def setYLabel(self, y_label, silent=False): + y_label = toUnicodeWithWarning(y_label, "XYPlotSetModel::setYLabel()") + self._ylabel = y_label + if not silent: + self.notifyChange("YLabelChange") + \ No newline at end of file diff --git a/tools/CurvePlot/src/python/pyqtside/CMakeLists.txt b/tools/CurvePlot/src/python/pyqtside/CMakeLists.txt new file mode 100644 index 000000000..9ec5d8758 --- /dev/null +++ b/tools/CurvePlot/src/python/pyqtside/CMakeLists.txt @@ -0,0 +1,28 @@ +# Copyright (C) 2012-2014 CEA/DEN, EDF R&D +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# + +SET(_pyqtside_SCRIPTS + __init__.py + QtCore.py + QtGui.py + pyside_dynamic.py + uic.py +) + +SALOME_INSTALL_SCRIPTS("${_pyqtside_SCRIPTS}" ${SALOME_INSTALL_PYTHON}/pyqtside) diff --git a/tools/CurvePlot/src/python/pyqtside/QtCore.py b/tools/CurvePlot/src/python/pyqtside/QtCore.py new file mode 100644 index 000000000..6e90c29e8 --- /dev/null +++ b/tools/CurvePlot/src/python/pyqtside/QtCore.py @@ -0,0 +1,7 @@ +from . import _use_pyqt +if _use_pyqt: + from PyQt4.QtCore import * + Slot = pyqtSlot + Signal = pyqtSignal +else: + from PySide.QtCore import * diff --git a/tools/CurvePlot/src/python/pyqtside/QtGui.py b/tools/CurvePlot/src/python/pyqtside/QtGui.py new file mode 100644 index 000000000..15c7ee1f1 --- /dev/null +++ b/tools/CurvePlot/src/python/pyqtside/QtGui.py @@ -0,0 +1,37 @@ +from . import _use_pyqt +if _use_pyqt: + from PyQt4.QtGui import * + + # Make QVariant invisible in PyQt4 since they don't exist in + # PySide ... + __original_itemData = QComboBox.itemData + def new_itemData(*args, **kargs): + from PyQt4.QtCore import QVariant + variant = __original_itemData(*args, **kargs) + funcS = lambda : (str(variant.toString()), True) + dico = {QVariant.Int: variant.toInt, QVariant.String: funcS, + QVariant.Bool: variant.toBool, QVariant.Double: variant.toDouble} + conv = dico.get(variant.type(), None) + if conv is None: + raise Exception("Unsupported variant type in pyqtside: '%s'!" % variant.typeName()) + return conv()[0] + + QComboBox.itemData = new_itemData +else: + from PySide.QtGui import * + + __original_ofn = QFileDialog.getOpenFileName + __original_sfn = QFileDialog.getSaveFileName + + # In PySide, getOpenFileName and co returns 2 values, and only one in PyQt4 ... + def newOfn(cls,*args, **kargs): + tup = __original_ofn(*args, **kargs) + return tup[0] + + def newSfn(cls,*args, **kargs): + tup = __original_sfn(*args, **kargs) + return tup[0] + + QFileDialog.getOpenFileName = classmethod(newOfn) + QFileDialog.getSaveFileName = classmethod(newSfn) + diff --git a/tools/CurvePlot/src/python/pyqtside/__init__.py b/tools/CurvePlot/src/python/pyqtside/__init__.py new file mode 100644 index 000000000..ef46e4b6d --- /dev/null +++ b/tools/CurvePlot/src/python/pyqtside/__init__.py @@ -0,0 +1,30 @@ +""" +Group under one hat PySide and PyQt4. PyQt4 is tried first. +""" + +try: + import os + if os.getenv("CURVEPLOT_FORCE_PYSIDE") is not None: + raise Exception + import PyQt4 + _use_pyqt = True + print "Using PyQt4 run-time ..." +except: + try: + import PySide + _use_pyqt = False + print "Using PySide run-time ..." + except: + raise Exception("Neither PyQt4 nor PySide could be imported!") + +# Matplotlib has to be handled very early, otherwise it will switch to whatever it +# finds first on the machine +try: + import matplotlib + if _use_pyqt: back = 'PyQt4' + else: back = 'PySide' + matplotlib.rcParams['backend.qt4'] = back + print "Matplotlib found - Set matplotlib backend to '%s'!" % back +except: + # No matplotlib, silently discard err message. + pass diff --git a/tools/CurvePlot/src/python/pyqtside/pyside_dynamic.py b/tools/CurvePlot/src/python/pyqtside/pyside_dynamic.py new file mode 100644 index 000000000..a3ab6046d --- /dev/null +++ b/tools/CurvePlot/src/python/pyqtside/pyside_dynamic.py @@ -0,0 +1,86 @@ +""" + How to load a user interface dynamically with PySide. + .. moduleauthor:: Sebastian Wiesner +""" +from PySide.QtCore import Slot, QMetaObject +from PySide.QtUiTools import QUiLoader + +class UiLoader(QUiLoader): + """ + Subclass :class:`~PySide.QtUiTools.QUiLoader` to create the user interface + in a base instance. + + Unlike :class:`~PySide.QtUiTools.QUiLoader` itself this class does not + create a new instance of the top-level widget, but creates the user + interface in an existing instance of the top-level class. + + This mimics the behaviour of :func:`PyQt4.uic.loadUi`. + """ + def __init__(self, baseinstance, customWidgets=None): + """ + Create a loader for the given ``baseinstance``. + + The user interface is created in ``baseinstance``, which must be an + instance of the top-level class in the user interface to load, or a + subclass thereof. + + ``customWidgets`` is a dictionary mapping from class name to class object + for widgets that you've promoted in the Qt Designer interface. Usually, + this should be done by calling registerCustomWidget on the QUiLoader, but + with PySide 1.1.2 on Ubuntu 12.04 x86_64 this causes a segfault. + + ``parent`` is the parent object of this loader. + """ + QUiLoader.__init__(self, baseinstance) + self.baseinstance = baseinstance + self.customWidgets = customWidgets + + def createWidget(self, class_name, parent=None, name=''): + """ + Function that is called for each widget defined in ui file, + overridden here to populate baseinstance instead. + """ + if parent is None and self.baseinstance: + return self.baseinstance + else: + if class_name in self.availableWidgets(): + # create a new widget for child widgets + widget = QUiLoader.createWidget(self, class_name, parent, name) + else: + try: + widget = self.customWidgets[class_name](parent) + except (TypeError, KeyError) as e: + raise Exception('No custom widget ' + class_name + ' found in customWidgets param of UiLoader __init__.') + + if self.baseinstance: + setattr(self.baseinstance, name, widget) + + return widget + +def loadUi(uifile, baseinstance=None, customWidgets=None): + """ + Dynamically load a user interface from the given ``uifile``. + ``uifile`` is a string containing a file name of the UI file to load. + + If ``baseinstance`` is ``None``, the a new instance of the top-level widget + will be created. Otherwise, the user interface is created within the given + ``baseinstance``. In this case ``baseinstance`` must be an instance of the + top-level widget class in the UI file to load, or a subclass thereof. . + + ``customWidgets`` is a dictionary mapping from class name to class object + for widgets that you've promoted in the Qt Designer interface. Usually, + this should be done by calling registerCustomWidget on the QUiLoader, but + with PySide 1.1.2 on Ubuntu 12.04 x86_64 this causes a segfault. + + :method:`~PySide.QtCore.QMetaObject.connectSlotsByName()` is called on the + created user interface, so you can implemented your slots according to its + conventions in your widget class. + + Return ``baseinstance``, if ``baseinstance`` is not ``None``. Otherwise + return the newly created instance of the user interface. + """ + + loader = UiLoader(baseinstance, customWidgets) + widget = loader.load(uifile) + QMetaObject.connectSlotsByName(widget) + return widget diff --git a/tools/CurvePlot/src/python/pyqtside/uic.py b/tools/CurvePlot/src/python/pyqtside/uic.py new file mode 100644 index 000000000..3f9565b8e --- /dev/null +++ b/tools/CurvePlot/src/python/pyqtside/uic.py @@ -0,0 +1,7 @@ +from . import _use_pyqt +if _use_pyqt: + from PyQt4.uic import loadUi as loadUiGen +else: + from pyside_dynamic import loadUi as loadUiGen + + diff --git a/tools/CurvePlot/src/python/test/CMakeLists.txt b/tools/CurvePlot/src/python/test/CMakeLists.txt new file mode 100644 index 000000000..24f9eca14 --- /dev/null +++ b/tools/CurvePlot/src/python/test/CMakeLists.txt @@ -0,0 +1,45 @@ +# Copyright (C) 2012-2014 CEA/DEN, EDF R&D +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# + +SET(SGPYQT_RES_DIR "${SALOME_INSTALL_RES}") +SALOME_CONFIGURE_FILE(SalomePyQt_MockUp.py.in mockup/SalomePyQt_MockUp.py) + +SET(_all_SCRIPTS + PlotCurve_Standalone.py + plot_test.py + TestDesktop.py +) + +SALOME_INSTALL_SCRIPTS("${_all_SCRIPTS}" ${SALOME_INSTALL_SCRIPT_PYTHON}/tests) +SALOME_INSTALL_SCRIPTS(${CMAKE_CURRENT_BINARY_DIR}/mockup/SalomePyQt_MockUp.py ${SALOME_INSTALL_SCRIPT_PYTHON}/tests) + +INSTALL(DIRECTORY baselines DESTINATION ${SALOME_INSTALL_SCRIPT_PYTHON}/tests) + +SALOME_ACCUMULATE_ENVIRONMENT(PYTHONPATH ${CMAKE_CURRENT_SOURCE_DIR}/../model) +SALOME_ACCUMULATE_ENVIRONMENT(PYTHONPATH ${CMAKE_CURRENT_SOURCE_DIR}/../ui) +SALOME_ACCUMULATE_ENVIRONMENT(PYTHONPATH ${CMAKE_CURRENT_SOURCE_DIR}/../views) +SALOME_ACCUMULATE_ENVIRONMENT(PYTHONPATH ${CMAKE_CURRENT_SOURCE_DIR}/../controller) +SALOME_ACCUMULATE_ENVIRONMENT(PYTHONPATH ${CMAKE_CURRENT_SOURCE_DIR}/..) +SALOME_ACCUMULATE_ENVIRONMENT(PYTHONPATH ${PROJECT_BINARY_DIR}/src/python/ui) # for the generated PY files (from UI files) +SALOME_ACCUMULATE_ENVIRONMENT(PYTHONPATH ${PROJECT_BINARY_DIR}/src/python/controller) # for utils.py +SALOME_GENERATE_TESTS_ENVIRONMENT(tests_env) + +ADD_TEST(CurvePlotUnitTests ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/plot_test.py) +SET_TESTS_PROPERTIES(CurvePlotUnitTests PROPERTIES ENVIRONMENT "${tests_env}") + diff --git a/tools/CurvePlot/src/python/test/PlotCurve_Standalone.py b/tools/CurvePlot/src/python/test/PlotCurve_Standalone.py new file mode 100755 index 000000000..ad2886efd --- /dev/null +++ b/tools/CurvePlot/src/python/test/PlotCurve_Standalone.py @@ -0,0 +1,75 @@ +# -*- coding: latin-1 -*- +# Copyright (C) 2007-2010 CEA/DEN, EDF R&D, OPEN CASCADE +# +# Copyright (C) 2003-2007 OPEN CASCADE, EADS/CCR, LIP6, CEA/DEN, +# CEDRAT, EDF R&D, LEG, PRINCIPIA R&D, BUREAU VERITAS +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# +# Author : A. Bruneton +# +from pyqtside.QtGui import QApplication +from pyqtside.QtCore import SIGNAL, SLOT, QTimer, QTranslator + +from TestDesktop import TestDesktop +import SalomePyQt_MockUp + +desktop = None + +def activate(): + """This method mimicks SALOME's module activation """ + global desktop + + desktop.showCurveTreeView() + return True + +def activeViewChanged( viewID ): + from curveplot import PlotController + PlotController.ActiveViewChanged(viewID) + +def main(args) : + global desktop + + app = QApplication(args) + ts_files = ["/export/home/adrien/Projets/salome/modules/V7_main/CURVEPLOT_INSTALL/share/salome/resources/curveplot/CURVEPLOT_msg_fr.qm", + "/export/home/adrien/Projets/salome/modules/V7_main/CURVEPLOT_INSTALL/share/salome/resources/curveplot/CURVEPLOT_msg_en.qm" + ] + trans = QTranslator() + for f in ts_files: + if not trans.load(f): + print "could not load translation %s!" % f + app.installTranslator(trans) + dw = app.desktop() + x, y = dw.width()*0.25, dw.height()*0.7 + + desktop = TestDesktop(None) + sgPyQt = SalomePyQt_MockUp.SalomePyQt(desktop) + sgPyQt.currentTabChanged.connect(activeViewChanged) + desktop._sgPyQt = sgPyQt + desktop.initialize() + desktop.resize(x,y) + desktop.show() + activate() + # + QTimer.singleShot(200, desktop, SLOT("curveSameFig()")) + # + app.connect(app,SIGNAL("lastWindowClosed()"),app,SLOT("quit()")) + app.exec_() + +if __name__ == "__main__" : + import sys + main(sys.argv) diff --git a/tools/CurvePlot/src/python/test/PlotTestBase.py b/tools/CurvePlot/src/python/test/PlotTestBase.py new file mode 100644 index 000000000..361b0d1e7 --- /dev/null +++ b/tools/CurvePlot/src/python/test/PlotTestBase.py @@ -0,0 +1,159 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2007-2010 CEA/DEN, EDF R&D, OPEN CASCADE +# +# Copyright (C) 2003-2007 OPEN CASCADE, EADS/CCR, LIP6, CEA/DEN, +# CEDRAT, EDF R&D, LEG, PRINCIPIA R&D, BUREAU VERITAS +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# +# Author : A. Bruneton +# +import unittest, sys, os, filecmp, shutil, tempfile +from pyqtside.QtGui import QApplication, QPixmap, QPainter +from PlotController import PlotController +from XYView import XYView + +def runOnly(func): + func.__runOnly__ = True + return func + +def processDecorator(mod_name): + """ Little trick to be able to mark a test with the decorator + @runOnly + If one or more tests bear this decorator, only them will be run + """ + import inspect + someRunOnly = False + for name, obj in inspect.getmembers(sys.modules[mod_name]): + if name == "PlotTest" and inspect.isclass(obj): + for p in dir(obj): + if p.startswith("test") and hasattr(obj.__dict__[p], "__runOnly__"): + someRunOnly = True + break + if someRunOnly: + for name, obj in inspect.getmembers(sys.modules[mod_name]): + if name == "PlotTest" and inspect.isclass(obj): + for p in dir(obj): + # Note the "not": + if p.startswith("test") and not hasattr(obj.__dict__[p], "__runOnly__"): + delattr(obj, p) + +class PlotTestBase(unittest.TestCase): + """ Unit test suite for the curve plotter. This class deals with the set up and the screenshot generation/ + comparison. The tests themselves are stored in the derived class PlotTest below. + + The class variable below can be turned on to regenerate base line files (reference images). + All baselines start with the name of the corresponding test, plus a possible suffix. + The baselines directory is set relative to the path of this script. + """ + REBUILD_BASELINES = False + + __BASE_LINE_DIR = "baselines" + __FORMAT = "png" + + def __init__(self, methodName): + unittest.TestCase.__init__(self, methodName) + + if self.REBUILD_BASELINES: + self.tmpBaselineDir = os.path.join(tempfile.gettempdir(), "curveplot_baselines") + if not os.path.isdir(self.tmpBaselineDir): + os.mkdir(self.tmpBaselineDir) + print "### Rebuilding base lines. Reference files will be saved to '%s'" % self.tmpBaselineDir + + PlotController.WITH_CURVE_BROWSER = True + XYView._DEFAULT_LEGEND_STATE = True # always show legend by default + self._this_dir = os.path.dirname(os.path.realpath(__file__)) + +# import matplotlib as mpl +# mpl.use('Agg') + + def setUp(self): + from SalomePyQt_MockUp import SalomePyQt + from TableModel import TableModel + from CurveModel import CurveModel + from XYPlotSetModel import XYPlotSetModel + + self.qpixmap = None + self.keepDir = False + if not self.REBUILD_BASELINES: + self.tmpDir = tempfile.mkdtemp(prefix="curveplot_tests") + else: + self.tmpDir = None + # Minimal UI setup: + self.sgPyQt = SalomePyQt() + # Reinstanciate from scratch the PlotController: + self.plotController = PlotController.GetInstance(self.sgPyQt) + self.plotController.setFixedSizeWidget() + # Reset some class var to make sure IDs appearing in screenshots do not depend on test seq order: + CurveModel.START_ID = -1 + TableModel.START_ID = -1 + XYPlotSetModel.START_ID = -1 + + def tearDown(self): + if not self.REBUILD_BASELINES: + # Clean up temp dir where the file comparison has been made: + if not self.keepDir: + shutil.rmtree(self.tmpDir, False) + PlotController.Destroy() + + def getTestName(self): + return self.id().split(".")[-1] + + def saveCurrentPix(self, direct, suffix): + fileName = os.path.join(direct, self.getTestName() + suffix + "." + self.__FORMAT) + self.qpixmap.save(fileName, self.__FORMAT) + return fileName + + def areScreenshotEqual(self, widget, suffix=""): + """ Test equality between a reference file saved in the baseline directory, and whose name is built as + ".png" + and the file generated on the fly by taking a snapshot of the widget provided in argument. + The comparison is made in a temp dir which is kept if the file differ. + """ + import glob + # Smiiiile :-) + self.qpixmap = QPixmap.grabWidget(widget) + + # Nothing to compare if rebuilding base lines, just saving file: + if self.REBUILD_BASELINES: + self.saveCurrentPix(self.tmpBaselineDir, suffix) + return True + + gen_path = self.saveCurrentPix(self.tmpDir, suffix) + base_ref = os.path.join(self._this_dir, self.__BASE_LINE_DIR, self.getTestName() + suffix) + ret = False + for ref_path in glob.glob("%s_*.%s" % (base_ref, self.__FORMAT)): + try: + ret = filecmp.cmp(ref_path, gen_path, shallow=False) + if ret: + break + except OSError: + ret = False + if not ret: + # Keep file if assert is false + self.keepDir = True + print "[%s] -- Failed screenshot equality, or unable to open baseline file - directory is kept alive: %s" % (self.getTestName(), self.tmpDir) + return ret + + def showTabWidget(self): + tabW = self.plotController._sgPyQt._tabWidget + # No simpler way found so far: + tabW.show() + return tabW + + def getBrowserWidget(self): + return self.plotController._curveBrowserView._treeWidget diff --git a/tools/CurvePlot/src/python/test/README.txt b/tools/CurvePlot/src/python/test/README.txt new file mode 100644 index 000000000..bdb40251f --- /dev/null +++ b/tools/CurvePlot/src/python/test/README.txt @@ -0,0 +1,5 @@ +Unit tests are in plot_test.py + +PlotCurve_Standalone is a standalone Python executable that shows the +various functionalities of the package. + diff --git a/tools/CurvePlot/src/python/test/SalomePyQt_MockUp.py.in b/tools/CurvePlot/src/python/test/SalomePyQt_MockUp.py.in new file mode 100644 index 000000000..70f4f1b22 --- /dev/null +++ b/tools/CurvePlot/src/python/test/SalomePyQt_MockUp.py.in @@ -0,0 +1,150 @@ +# Copyright (C) 2007-2010 CEA/DEN, EDF R&D, OPEN CASCADE +# +# Copyright (C) 2003-2007 OPEN CASCADE, EADS/CCR, LIP6, CEA/DEN, +# CEDRAT, EDF R&D, LEG, PRINCIPIA R&D, BUREAU VERITAS +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# +# Author : A. Bruneton +# + +from pyqtside.QtGui import QApplication, QTabWidget +from pyqtside.QtGui import QAction, QMenu, QIcon, QPixmap, QDesktopWidget, QFileDialog +from pyqtside.QtCore import SIGNAL, SLOT, QObject, Slot, Signal + +RESOURCE_DIR = "@SGPYQT_RES_DIR@" + +class SalomePyQt(QObject): + """ A pure Qt implementation of the SgPyQt API (usually provided in the SALOME context) + This class can be used to mimick the true SALOME object without having to launch + SALOME + """ + currentTabChanged = Signal(int) + + START_VIEW_ID = 0 + + def __init__(self, mainWindow=None): + QObject.__init__(self) + self._mainWindow = mainWindow + self._tabWidget = QTabWidget() + self._tabWidget.setObjectName("TabWidget") + self._viewIDs = {} + self._menuBar = None + if self._mainWindow: + self._menuBar = self._mainWindow.menuBar() + self._mainWindow.setCentralWidget(self._tabWidget) + self.connect(self._tabWidget, SIGNAL("currentChanged(int)"), self, SLOT("onTabChanged(int)")) + self._blockSignal = False + + def getDesktop(self): + return self._mainWindow + + def getFileName(self, parent_widget, initial, filters, caption, do_open): + fil = ";;".join([str(f) for f in filters]) + return QFileDialog.getOpenFileName(parent=parent_widget, + caption=caption, directory=initial, filter=fil); + + @Slot(int) + def onTabChanged(self, index): + if self._blockSignal: + return + invDict = dict([(v, k) for k,v in self._viewIDs.items()]) + if invDict.has_key(index): + self._blockSignal = True + self.currentTabChanged.emit(invDict[index]) + self._blockSignal = False + + def createView(self, name, widget): + self.START_VIEW_ID += 1 + idx = self._tabWidget.insertTab(-1, widget, name) + self._viewIDs[self.START_VIEW_ID] = idx + return self.START_VIEW_ID + + def activateView(self, viewID): + idx = self._viewIDs[viewID] + self._tabWidget.setCurrentIndex(idx) + + def setViewVisible(self, viewID, isVis): + ## TODO: improve to really remove tab + if isVis: + self.activateView(viewID) + + def closeView(self, viewID): + self._blockSignal = True + idxClosed = self._viewIDs[viewID] + # QTabWidget doesn't clean after itself when removing a tab + w = self._tabWidget.widget(idxClosed) + self._tabWidget.removeTab(idxClosed) + try: + w.clearAll() + except: + pass + # Update the other tab indices which are now shifted: + for k, idx in self._viewIDs.items(): + if idx > idxClosed: + self._viewIDs[k] -= 1 + self._blockSignal = False + + def setViewTitle(self, viewID, title): + idx = self._viewIDs[viewID] + self._tabWidget.setTabText(idx, title) + + def createAction(self, id, short_name, long_name, tooltip, icon): + import os + return QAction(QIcon(QPixmap(os.path.normpath(icon))),short_name, None) + + def defaultMenuGroup(self): + return None + + def createMenu(self, name_or_action, pos_or_menu, menuId=-1, menuGroup=None): + if not self._mainWindow is None: + if isinstance(name_or_action, str): + return self.__createMenu1( name_or_action, pos_or_menu, menuId, menuGroup) + else: + return self.__createMenu2(name_or_action, pos_or_menu) + + def __createMenu1(self, name, pos, menuId, menuGroup): + menu = QMenu(name, self._menuBar) + self._menuBar.addMenu(menu) + return menu + + def __createMenu2(self, action, menu): + menu.addAction(action) + + def createSeparator(self): + return None + + def createTool(self, toolbar_name_or_action, toolbar=None): + if not self._mainWindow is None: + if isinstance(toolbar_name_or_action, str): + return self.__createTool1(toolbar_name_or_action) + else: + return self.__createTool2(toolbar_name_or_action, toolbar) + + def __createTool1(self, toolbar_name): + return None + + def __createTool2(self, action, toolbar): + return None + + def loadIcon(self, module_name, file_name): + import os + mod_dir = os.getenv("%s_ROOT_DIR" % module_name) + mod_lc = module_name.lower() + res_path = os.path.join(mod_dir, RESOURCE_DIR, mod_lc, file_name) + # e.g. MODULE_ROOT_DIR/share/resource/module/image.png + return QIcon(res_path) diff --git a/tools/CurvePlot/src/python/test/TestDesktop.py b/tools/CurvePlot/src/python/test/TestDesktop.py new file mode 100644 index 000000000..8d72fd675 --- /dev/null +++ b/tools/CurvePlot/src/python/test/TestDesktop.py @@ -0,0 +1,227 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2007-2010 CEA/DEN, EDF R&D, OPEN CASCADE +# +# Copyright (C) 2003-2007 OPEN CASCADE, EADS/CCR, LIP6, CEA/DEN, +# CEDRAT, EDF R&D, LEG, PRINCIPIA R&D, BUREAU VERITAS +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# +# Author : A. Bruneton +# + +from pyqtside.QtCore import SIGNAL, SLOT, Slot, Qt, QTimer +from pyqtside.QtGui import QMainWindow,QMenu +import numpy as np + +import curveplot + +try: + import curveplot + from curveplot.utils import Logger +except: + from PlotController import PlotController as curveplot + from utils import Logger + +class TestDesktop(QMainWindow): + """ Dummy desktop for testing purposes. + """ + def __init__(self, sgPyQt): + QMainWindow.__init__(self) + self.cnt = -1 + self.MAX_CNT = 20 + self.timeLap = 100 + self._sgPyQt = sgPyQt + + def initialize(self): + """ Initialize is called later than __init__() so that the Desktop and the SgPyQt + objects can be properly initialized. + """ + self._currID = 1234 + + self._sgDesktop = self + try: + self._plotController = curveplot.PlotController.GetInstance(self._sgPyQt) + except: + self._plotController = curveplot.GetInstance(self._sgPyQt) + self.createIDs() + self.createActions() + + context_actions = [self.itemDelAction] + menu = QMenu(self) + for a in context_actions: + menu.addAction(a) + self._plotController.setBrowserContextualMenu(menu) + + self.createToolbars() + self.createMenus() + self.createView() + + self.connect(self.curveSameFigAction,SIGNAL("activated()"),self.curveSameFig) + self.connect(self.curveNewFigAction,SIGNAL("activated()"),self.curveNewFig) + self.connect(self.itemDelAction,SIGNAL("activated()"),self.itemDel) + self.connect(self.cpsAction,SIGNAL("activated()"),self.clearPlotSet) + self.connect(self.plotTableAction,SIGNAL("activated()"),self.plotTable) + self.connect(self.addPSAction,SIGNAL("activated()"),self.addPS) + self.connect(self.addTabAction,SIGNAL("activated()"),self.addTab) + self.connect(self.memAction,SIGNAL("activated()"),self.memPrint) + self.connect(self.perfTestAction,SIGNAL("activated()"),self.perfTest) + + def generateID(self): + self._currID += 1 + return self._currID + + def createIDs(self): + # Actions + self.curveSameFigActionID = self.generateID() + self.curveNewFigActionID = self.generateID() + self.itemDelActionID = self.generateID() + self.cpsActionID = self.generateID() + self.plotTableActionID = self.generateID() + self.addPSActionID = self.generateID() + self.addTabActionID = self.generateID() + self.memActionID = self.generateID() + self.perfActionID = self.generateID() + + # Menus + self.etudeMenuID = self.generateID() + self.dummyMenuID = self.generateID() + + def createActions(self): + ca = self._sgPyQt.createAction + self.curveSameFigAction = ca(self.curveSameFigActionID, "Curve on same fig", "Curve on same fig", "", "") + self.curveNewFigAction = ca(self.curveNewFigActionID, "Curve on new fig", "Curve on new fig", "", "") + self.itemDelAction = ca(self.itemDelActionID, "Delete selected", "Delete selected", "", "") + self.cpsAction = ca(self.cpsActionID, "Clear plot set", "Clear plot set", "", "") + self.plotTableAction = ca(self.plotTableActionID, "Plot from table", "Plot from table", "", "") + self.addPSAction = ca(self.addPSActionID, "Add plot set", "Add plot set", "", "") + self.addTabAction = ca(self.addTabActionID, "Add tab", "Add tab", "", "") + self.memAction = ca(self.memActionID, "Display used mem", "Display used mem", "", "") + self.perfTestAction = ca(self.perfActionID, "Perf test", "Perf test", "", "") + + def createToolbars(self): + pass +# self.Toolbar = self._sgPyQt.createTool(self.tr("Toolbar")) +# self._sgPyQt.createTool(self.fileNewAction, self.Toolbar) +# self._sgPyQt.createTool(self.filePrintAction, self.Toolbar) +# sep = self._sgPyQt.createSeparator() +# self._sgPyQt.createTool(sep, self.Toolbar) +# self._sgPyQt.createTool(self.editUndoAction, self.Toolbar) +# self._sgPyQt.createTool(self.editRedoAction, self.Toolbar) + + def createMenus(self): + curveMenu = self._sgPyQt.createMenu( "Curve test", -1, self.etudeMenuID, self._sgPyQt.defaultMenuGroup() ) + self._sgPyQt.createMenu(self.curveSameFigAction, curveMenu) + self._sgPyQt.createMenu(self.curveNewFigAction, curveMenu) + self._sgPyQt.createMenu(self.itemDelAction, curveMenu) + self._sgPyQt.createMenu(self.cpsAction, curveMenu) + self._sgPyQt.createMenu(self.plotTableAction, curveMenu) + self._sgPyQt.createMenu(self.addPSAction, curveMenu) + self._sgPyQt.createMenu(self.memAction, curveMenu) + self._sgPyQt.createMenu(self.perfTestAction, curveMenu) + + dummyMenu = self._sgPyQt.createMenu( "Dummy", -1, self.dummyMenuID, self._sgPyQt.defaultMenuGroup() ) + self._sgPyQt.createMenu(self.addTabAction, dummyMenu) + + def createView(self): + pass + + def showCurveTreeView(self) : + self._dockCurveBrowserView = self._plotController._curveBrowserView + self._sgDesktop.addDockWidget(Qt.LeftDockWidgetArea, self._dockCurveBrowserView) + + def __generateRandomData(self, nPoints=100): + from random import random + x = np.arange(nPoints) / 5.0 + ampl = 20.0*random() + 1.0 + y = ampl * np.sin(x*random()) +# x = np.arange(5e5) +# y = x + return x, y + + @Slot() + def curveSameFig(self): + x, y = self.__generateRandomData() + _, ps_id = curveplot.AddCurve(x, y, x_label="the x axis", y_label="the y axis", append=True) + curveplot.SetLegendVisible(ps_id, True) + if self.cnt >= 0: + QTimer.singleShot(self.timeLap, self, SLOT("itemDel()")) + + def curveNewFig(self): + x, y = self.__generateRandomData() + curveplot.AddCurve(x, y, x_label="the x axis", y_label="the y axis", append=False) + + @Slot() + def itemDel(self): + curveplot.DeleteCurrentItem() + if self.cnt >= 0: + QTimer.singleShot(self.timeLap, self, SLOT("memPrint()")) + + @Slot() + def perfTest(self): + lx, ly = [], [] + nC = 200 + for _ in range(nC): + x, y = self.__generateRandomData(1000) + lx.append(x); ly.append(y) + print "Done generating" + from time import time + t0 = time() + curveplot.LockRepaint() + for i in range(nC): + curveplot.AddCurve(lx[i], ly[i], append=True) + curveplot.UnlockRepaint() + print "Elapsed: %.2f" % ( time() - t0) + + def clearPlotSet(self): + curveplot.ClearPlotSet() + + def addPS(self): + # Also a test for unicode! + curveplot.AddPlotSet(u'ça m embête') + + def addTab(self): + pass +# from PyQt4.QtGui import QPushButton +# self.qp = QPushButton("Hi!") +# self._sgPyQt.createView("Dummy", self.qp) + + def plotTable(self): + from curveplot import TableModel + t = TableModel(None) + t.setTitle("coucou") + t.addColumn([1.0,2.0,3.0,4.0]) + t.addColumn([1.0,2.0,3.0,4.0]) + t.addColumn([1.0,4.0,9.0,16.0]) + t.setColumnTitle(0, "X-s") + t.setColumnTitle(1, "Identity") + t.setColumnTitle(2, "Square") + cont = curveplot.PlotController.GetInstance() + cont.plotCurveFromTable(t, y_col_index=1, append=False) + cont.plotCurveFromTable(t, y_col_index=2, append=True) + + @Slot() + def memPrint(self): + i, t = curveplot.GetAllPlotSets() + print zip(i, t) + new_id = curveplot.CopyCurve(curve_id=0, plot_set_id=1) + print "created curve: %d" % new_id + import resource + m = resource.getrusage(resource.RUSAGE_SELF)[2]*resource.getpagesize()/1e6 + print "** Used memory: %.2f Mb" % m + if self.cnt >= 0 and self.cnt < self.MAX_CNT: + self.cnt += 1 + QTimer.singleShot(self.timeLap, self, SLOT("curveSameFig()")) diff --git a/tools/CurvePlot/src/python/test/baselines/testAddCurveAppend_a.png b/tools/CurvePlot/src/python/test/baselines/testAddCurveAppend_a.png new file mode 100644 index 000000000..be7de68bb Binary files /dev/null and b/tools/CurvePlot/src/python/test/baselines/testAddCurveAppend_a.png differ diff --git a/tools/CurvePlot/src/python/test/baselines/testAddCurve_a.png b/tools/CurvePlot/src/python/test/baselines/testAddCurve_a.png new file mode 100644 index 000000000..69de6919b Binary files /dev/null and b/tools/CurvePlot/src/python/test/baselines/testAddCurve_a.png differ diff --git a/tools/CurvePlot/src/python/test/baselines/testAddPlotSet_a.png b/tools/CurvePlot/src/python/test/baselines/testAddPlotSet_a.png new file mode 100644 index 000000000..4a7417bd8 Binary files /dev/null and b/tools/CurvePlot/src/python/test/baselines/testAddPlotSet_a.png differ diff --git a/tools/CurvePlot/src/python/test/baselines/testClearPlotSet2_a.png b/tools/CurvePlot/src/python/test/baselines/testClearPlotSet2_a.png new file mode 100644 index 000000000..4a7417bd8 Binary files /dev/null and b/tools/CurvePlot/src/python/test/baselines/testClearPlotSet2_a.png differ diff --git a/tools/CurvePlot/src/python/test/baselines/testClearPlotSet_a.png b/tools/CurvePlot/src/python/test/baselines/testClearPlotSet_a.png new file mode 100644 index 000000000..c9144c243 Binary files /dev/null and b/tools/CurvePlot/src/python/test/baselines/testClearPlotSet_a.png differ diff --git a/tools/CurvePlot/src/python/test/baselines/testCopyCurve_a.png b/tools/CurvePlot/src/python/test/baselines/testCopyCurve_a.png new file mode 100644 index 000000000..a08cf1b22 Binary files /dev/null and b/tools/CurvePlot/src/python/test/baselines/testCopyCurve_a.png differ diff --git a/tools/CurvePlot/src/python/test/baselines/testDeleteCurrentItem_curve_a.png b/tools/CurvePlot/src/python/test/baselines/testDeleteCurrentItem_curve_a.png new file mode 100644 index 000000000..2b16e2ce2 Binary files /dev/null and b/tools/CurvePlot/src/python/test/baselines/testDeleteCurrentItem_curve_a.png differ diff --git a/tools/CurvePlot/src/python/test/baselines/testDeleteCurrentItem_plotSet_a.png b/tools/CurvePlot/src/python/test/baselines/testDeleteCurrentItem_plotSet_a.png new file mode 100644 index 000000000..b84993bbb Binary files /dev/null and b/tools/CurvePlot/src/python/test/baselines/testDeleteCurrentItem_plotSet_a.png differ diff --git a/tools/CurvePlot/src/python/test/baselines/testDeleteCurve1_a.png b/tools/CurvePlot/src/python/test/baselines/testDeleteCurve1_a.png new file mode 100644 index 000000000..1043c947d Binary files /dev/null and b/tools/CurvePlot/src/python/test/baselines/testDeleteCurve1_a.png differ diff --git a/tools/CurvePlot/src/python/test/baselines/testDeleteCurve2_a.png b/tools/CurvePlot/src/python/test/baselines/testDeleteCurve2_a.png new file mode 100644 index 000000000..1043c947d Binary files /dev/null and b/tools/CurvePlot/src/python/test/baselines/testDeleteCurve2_a.png differ diff --git a/tools/CurvePlot/src/python/test/baselines/testDeleteCurve3_a.png b/tools/CurvePlot/src/python/test/baselines/testDeleteCurve3_a.png new file mode 100644 index 000000000..e9f8ed691 Binary files /dev/null and b/tools/CurvePlot/src/python/test/baselines/testDeleteCurve3_a.png differ diff --git a/tools/CurvePlot/src/python/test/baselines/testDeletePlotSet1_a.png b/tools/CurvePlot/src/python/test/baselines/testDeletePlotSet1_a.png new file mode 100644 index 000000000..24895028c Binary files /dev/null and b/tools/CurvePlot/src/python/test/baselines/testDeletePlotSet1_a.png differ diff --git a/tools/CurvePlot/src/python/test/baselines/testDeletePlotSet2_a.png b/tools/CurvePlot/src/python/test/baselines/testDeletePlotSet2_a.png new file mode 100644 index 000000000..24895028c Binary files /dev/null and b/tools/CurvePlot/src/python/test/baselines/testDeletePlotSet2_a.png differ diff --git a/tools/CurvePlot/src/python/test/baselines/testExtendCurve_a.png b/tools/CurvePlot/src/python/test/baselines/testExtendCurve_a.png new file mode 100644 index 000000000..1476e781b Binary files /dev/null and b/tools/CurvePlot/src/python/test/baselines/testExtendCurve_a.png differ diff --git a/tools/CurvePlot/src/python/test/baselines/testLockRepaint_a.png b/tools/CurvePlot/src/python/test/baselines/testLockRepaint_a.png new file mode 100644 index 000000000..98b167091 Binary files /dev/null and b/tools/CurvePlot/src/python/test/baselines/testLockRepaint_a.png differ diff --git a/tools/CurvePlot/src/python/test/baselines/testPlotCurveFromTable_a.png b/tools/CurvePlot/src/python/test/baselines/testPlotCurveFromTable_a.png new file mode 100644 index 000000000..91bfc4012 Binary files /dev/null and b/tools/CurvePlot/src/python/test/baselines/testPlotCurveFromTable_a.png differ diff --git a/tools/CurvePlot/src/python/test/baselines/testResetCurve_a.png b/tools/CurvePlot/src/python/test/baselines/testResetCurve_a.png new file mode 100644 index 000000000..ba52552a1 Binary files /dev/null and b/tools/CurvePlot/src/python/test/baselines/testResetCurve_a.png differ diff --git a/tools/CurvePlot/src/python/test/baselines/testSetCurrentCurve2_a.png b/tools/CurvePlot/src/python/test/baselines/testSetCurrentCurve2_a.png new file mode 100644 index 000000000..01ea93a6a Binary files /dev/null and b/tools/CurvePlot/src/python/test/baselines/testSetCurrentCurve2_a.png differ diff --git a/tools/CurvePlot/src/python/test/baselines/testSetCurrentCurve3_a.png b/tools/CurvePlot/src/python/test/baselines/testSetCurrentCurve3_a.png new file mode 100644 index 000000000..2b16e2ce2 Binary files /dev/null and b/tools/CurvePlot/src/python/test/baselines/testSetCurrentCurve3_a.png differ diff --git a/tools/CurvePlot/src/python/test/baselines/testSetCurrentCurve_a.png b/tools/CurvePlot/src/python/test/baselines/testSetCurrentCurve_a.png new file mode 100644 index 000000000..1d5c92196 Binary files /dev/null and b/tools/CurvePlot/src/python/test/baselines/testSetCurrentCurve_a.png differ diff --git a/tools/CurvePlot/src/python/test/baselines/testSetCurrentPlotSet_a.png b/tools/CurvePlot/src/python/test/baselines/testSetCurrentPlotSet_a.png new file mode 100644 index 000000000..e890d57d3 Binary files /dev/null and b/tools/CurvePlot/src/python/test/baselines/testSetCurrentPlotSet_a.png differ diff --git a/tools/CurvePlot/src/python/test/baselines/testSetCurveLabel_a.png b/tools/CurvePlot/src/python/test/baselines/testSetCurveLabel_a.png new file mode 100644 index 000000000..8d4ffb317 Binary files /dev/null and b/tools/CurvePlot/src/python/test/baselines/testSetCurveLabel_a.png differ diff --git a/tools/CurvePlot/src/python/test/baselines/testSetCurveMarker_a.png b/tools/CurvePlot/src/python/test/baselines/testSetCurveMarker_a.png new file mode 100644 index 000000000..9f6eebc49 Binary files /dev/null and b/tools/CurvePlot/src/python/test/baselines/testSetCurveMarker_a.png differ diff --git a/tools/CurvePlot/src/python/test/baselines/testSetLabelX_a.png b/tools/CurvePlot/src/python/test/baselines/testSetLabelX_a.png new file mode 100644 index 000000000..f261e736d Binary files /dev/null and b/tools/CurvePlot/src/python/test/baselines/testSetLabelX_a.png differ diff --git a/tools/CurvePlot/src/python/test/baselines/testSetLabelY_a.png b/tools/CurvePlot/src/python/test/baselines/testSetLabelY_a.png new file mode 100644 index 000000000..4a7417bd8 Binary files /dev/null and b/tools/CurvePlot/src/python/test/baselines/testSetLabelY_a.png differ diff --git a/tools/CurvePlot/src/python/test/baselines/testSetLegendVisible_a.png b/tools/CurvePlot/src/python/test/baselines/testSetLegendVisible_a.png new file mode 100644 index 000000000..0dae71d25 Binary files /dev/null and b/tools/CurvePlot/src/python/test/baselines/testSetLegendVisible_a.png differ diff --git a/tools/CurvePlot/src/python/test/baselines/testSetPlotSetTitle_a.png b/tools/CurvePlot/src/python/test/baselines/testSetPlotSetTitle_a.png new file mode 100644 index 000000000..e805f17cd Binary files /dev/null and b/tools/CurvePlot/src/python/test/baselines/testSetPlotSetTitle_a.png differ diff --git a/tools/CurvePlot/src/python/test/baselines/testSetXSciNotation_a.png b/tools/CurvePlot/src/python/test/baselines/testSetXSciNotation_a.png new file mode 100644 index 000000000..336bbeb93 Binary files /dev/null and b/tools/CurvePlot/src/python/test/baselines/testSetXSciNotation_a.png differ diff --git a/tools/CurvePlot/src/python/test/baselines/testSetYSciNotation_a.png b/tools/CurvePlot/src/python/test/baselines/testSetYSciNotation_a.png new file mode 100644 index 000000000..44e29e678 Binary files /dev/null and b/tools/CurvePlot/src/python/test/baselines/testSetYSciNotation_a.png differ diff --git a/tools/CurvePlot/src/python/test/baselines/testSettingsCurveColor_a.png b/tools/CurvePlot/src/python/test/baselines/testSettingsCurveColor_a.png new file mode 100644 index 000000000..5dbdbd7e1 Binary files /dev/null and b/tools/CurvePlot/src/python/test/baselines/testSettingsCurveColor_a.png differ diff --git a/tools/CurvePlot/src/python/test/baselines/testSettingsCurveMarker_a.png b/tools/CurvePlot/src/python/test/baselines/testSettingsCurveMarker_a.png new file mode 100644 index 000000000..e9df517a7 Binary files /dev/null and b/tools/CurvePlot/src/python/test/baselines/testSettingsCurveMarker_a.png differ diff --git a/tools/CurvePlot/src/python/test/baselines/testToggleXLog_a.png b/tools/CurvePlot/src/python/test/baselines/testToggleXLog_a.png new file mode 100644 index 000000000..5fb1fcdf9 Binary files /dev/null and b/tools/CurvePlot/src/python/test/baselines/testToggleXLog_a.png differ diff --git a/tools/CurvePlot/src/python/test/baselines/testToggleYLog_a.png b/tools/CurvePlot/src/python/test/baselines/testToggleYLog_a.png new file mode 100644 index 000000000..0db30fb2b Binary files /dev/null and b/tools/CurvePlot/src/python/test/baselines/testToggleYLog_a.png differ diff --git a/tools/CurvePlot/src/python/test/plot_test.py b/tools/CurvePlot/src/python/test/plot_test.py new file mode 100644 index 000000000..204406cd4 --- /dev/null +++ b/tools/CurvePlot/src/python/test/plot_test.py @@ -0,0 +1,526 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2007-2010 CEA/DEN, EDF R&D, OPEN CASCADE +# +# Copyright (C) 2003-2007 OPEN CASCADE, EADS/CCR, LIP6, CEA/DEN, +# CEDRAT, EDF R&D, LEG, PRINCIPIA R&D, BUREAU VERITAS +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# +# Author : A. Bruneton +# + +from PlotTestBase import PlotTestBase, runOnly, processDecorator + +from PlotController import PlotController +from PlotSettings import PlotSettings + +from pyqtside.QtGui import QApplication +import sys +qapp = QApplication(sys.argv) + +class PlotTest(PlotTestBase): + """ Unit test suite for the curve plotter. The tests themselves are stored in this class, + the screenshot comparison logic is in PlotTestBase. + + The class variable below can be turned on to regenerate base line files (reference images). + All baselines start with the name of the corresponding test, plus a possible suffix. + The baselines directory is set relative to the path of this script. + + The decorator @runOnly can be used to run/rebuild a single test. + """ + REBUILD_BASELINES = False + + def __init__(self, methodName): + PlotTestBase.__init__(self, methodName) + + ### + ### Data generation + ### + def generateSine(self, alpha=1.0): + import numpy as np + x = np.arange(100) + y = np.sin(x*alpha/np.pi) + return x, y + + def generateExp(self, alpha=1.0): + import numpy as np + x = np.arange(20) + 1.0 + y = np.exp(x*alpha) + return x, y + + ### + ### The tests themselves + ### + + # + # Non GUI tests (some of them still need to show the widget to work properly but no + # screenshot comparison is made). + # + def testTableModel(self): + import numpy as np + from TableModel import TableModel + t = TableModel(None) + t.setTitle("coucou") + t.addColumn([1.0,2.0,3.0,4.0]) + self.assertRaises(ValueError, t.addColumn, [1.0,2.0]) + t.addColumn([1.0,2.0,3.0,4.0]) + t.addColumn([1.0, 4.0, 9.0, 16.0]) + self.assertEqual((4,3), t.getShape()) + t.removeValue(0, 1) + self.assertTrue(np.isnan(t.getData()[0,1])) + t.setColumnTitle(1, "a title") + self.assertEqual("a title", t.getColumnTitle(1)) + self.assertEqual("", t.getColumnTitle(0)) + + def testGetAllPlotSets(self): + self.showTabWidget() + ids, titles = PlotController.GetAllPlotSets() + self.assertEqual([], ids) + self.assertEqual([], titles) + + id1 = PlotController.AddPlotSet("toto") + id2 = PlotController.AddPlotSet("tutu") + id3 = PlotController.AddPlotSet("titi") + ids, titles = PlotController.GetAllPlotSets() + self.assertEqual([id1,id2,id3], ids) + self.assertEqual(["toto","tutu","titi"], titles) + + def testGetCurrentXX(self): + self.showTabWidget() + self.assertEqual(-1, PlotController.GetCurrentCurveID()) + self.assertEqual(-1, PlotController.GetCurrentPlotSetID()) + + x, y = self.generateSine() + _, psID1 = PlotController.AddCurve(x, y, append=False) + self.assertEqual(psID1, PlotController.GetCurrentPlotSetID()) + _, psID2 = PlotController.AddCurve(x, y, append=True) + self.assertEqual(psID1, psID2) # doesn't hurt! + self.assertEqual(psID2, PlotController.GetCurrentPlotSetID()) + psID3 = PlotController.AddPlotSet("ps") + self.assertEqual(psID3, PlotController.GetCurrentPlotSetID()) + PlotController.DeletePlotSet(psID3) + PlotController.DeletePlotSet(psID2) + self.assertEqual(-1, PlotController.GetCurrentCurveID()) + self.assertEqual(-1, PlotController.GetCurrentPlotSetID()) + + def testGetPlotSetID(self): + self.showTabWidget() + x, y = self.generateSine() + crvID, psID = PlotController.AddCurve(x, y, append=False) + self.assertEqual(psID, PlotController.GetPlotSetID(crvID)) + self.assertEqual(-1, PlotController.GetPlotSetID(145)) # invalid ID + PlotController.DeletePlotSet(psID) + self.assertEqual(-1, PlotController.GetPlotSetID(crvID)) # invalid ID + + def testGetPlotSetIDByName(self): + self.showTabWidget() + self.assertEqual(-1,PlotController.GetPlotSetIDByName("invalid")) + psID = PlotController.AddPlotSet("ps") + self.assertEqual(psID,PlotController.GetPlotSetIDByName("ps")) + PlotController.DeletePlotSet(psID) + self.assertEqual(-1,PlotController.GetPlotSetIDByName("ps")) + + def testIsValidPlotSetID(self): + self.showTabWidget() + self.assertEqual(False,PlotController.IsValidPlotSetID(0)) + psID = PlotController.AddPlotSet("ps") + self.assertEqual(True,PlotController.IsValidPlotSetID(psID)) + PlotController.DeletePlotSet(psID) + self.assertEqual(False,PlotController.IsValidPlotSetID(psID)) + + # + # GUI tests + # + def testAddCurve(self): + x, y = self.generateSine() + tw = self.showTabWidget() + PlotController.AddCurve(x, y, curve_label="My curve", x_label=u"Lèés X (unicode!)", y_label=u"Et des ŷ", append=False) + self.assertTrue(self.areScreenshotEqual(tw)) + + def testAddCurveAppend(self): + x, y = self.generateSine() + tw = self.showTabWidget() + PlotController.AddCurve(x, y, curve_label="My curve", x_label="The X-s", y_label="The Y-s", append=False) + PlotController.AddCurve(x, y*1.5, curve_label="My curve 2", append=True) + self.assertTrue(self.areScreenshotEqual(tw)) + + def testAddPlotSet(self): + tw = self.showTabWidget() + PlotController.AddPlotSet("My plotset") + self.assertTrue(self.areScreenshotEqual(tw)) + + def testClearPlotSet(self): + x, y = self.generateSine() + tw = self.showTabWidget() + PlotController.AddCurve(x, y, curve_label="My curve", x_label="The X-s", y_label="The Y-s", append=False) + _, psID = PlotController.AddCurve(x, y, curve_label="My curve 2", append=True) + clearedID = PlotController.ClearPlotSet() + self.assertEqual(clearedID, psID) + self.assertTrue(self.areScreenshotEqual(tw)) + + def testClearPlotSet2(self): + tw = self.showTabWidget() + self.assertRaises(ValueError, PlotController.ClearPlotSet, -789) + psID = PlotController.AddPlotSet("My plotset") + clearedID = PlotController.ClearPlotSet(psID) + self.assertEqual(psID, clearedID) + self.assertTrue(self.areScreenshotEqual(tw)) + + def testCopyCurve(self): + x, y = self.generateSine() + tw = self.showTabWidget() + crvID, _ = PlotController.AddCurve(x, y, curve_label="My curve", x_label="The X-s", y_label="The Y-s", append=False) + psID = PlotController.AddPlotSet("Another plotset") + newID = PlotController.CopyCurve(crvID, psID) + PlotController.SetCurrentPlotSet(psID) + self.assertNotEqual(crvID, newID) + self.assertTrue(self.areScreenshotEqual(tw)) + + def testDeleteCurrentItem_curve(self): + x, y = self.generateSine() + tw = self.showTabWidget() + PlotController.AddCurve(x, y, append=False) + crvID, _ = PlotController.AddCurve(x, y*1.5, append=True) + PlotController.SetCurrentCurve(crvID) + b, anID = PlotController.DeleteCurrentItem() # currently selected curve + self.assertFalse(b) + self.assertEqual(crvID, anID) + self.assertTrue(self.areScreenshotEqual(tw)) + + def testDeleteCurrentItem_plotSet(self): + tw = self.showTabWidget() + PlotController.AddPlotSet("tutu") + psID = PlotController.AddPlotSet("tata") + b, anID = PlotController.DeleteCurrentItem() + self.assertTrue(b) + self.assertEqual(psID, anID) + self.assertTrue(self.areScreenshotEqual(tw)) + + def testDeleteCurrentItem_void(self): + self.showTabWidget() + b, anID = PlotController.DeleteCurrentItem() # nothing selected + self.assertTrue(b) + self.assertEqual(-1, anID) + + def testDeleteCurve1(self): + tw = self.showTabWidget() + x, y = self.generateSine() + crvID, _ = PlotController.AddCurve(x, y, append=False) + PlotController.AddCurve(x, y*1.5, append=True) + cID = PlotController.DeleteCurve(crvID) + self.assertEqual(crvID, cID) + self.assertTrue(self.areScreenshotEqual(tw)) + + def testDeleteCurve2(self): + tw = self.showTabWidget() + x, y = self.generateSine() + crvID, _ = PlotController.AddCurve(x, y, append=False) + PlotController.AddCurve(x, y*1.5, append=True) + PlotController.SetCurrentCurve(crvID) + cID = PlotController.DeleteCurve() # current curve + self.assertEqual(crvID, cID) + self.assertTrue(self.areScreenshotEqual(tw)) + + def testDeleteCurve3(self): + """ resulting in an empty plot set, legend should be hidden """ + tw = self.showTabWidget() + x, y = self.generateSine() + crvID, _ = PlotController.AddCurve(x, y, append=False) + cID = PlotController.DeleteCurve(crvID) + self.assertEqual(crvID, cID) + self.assertTrue(self.areScreenshotEqual(tw)) + + def testDeletePlotSet1(self): + tw = self.showTabWidget() + psID = PlotController.AddPlotSet("tutu") + PlotController.AddPlotSet("tata") + psID2 = PlotController.DeletePlotSet(psID) + self.assertEqual(psID2, psID) + self.assertTrue(self.areScreenshotEqual(tw)) + + def testDeletePlotSet2(self): + tw = self.showTabWidget() + psID1 = PlotController.DeletePlotSet() + self.assertEqual(-1, psID1) # nothing selected yet + psID2 = PlotController.AddPlotSet("tutu") + PlotController.AddPlotSet("tata") + PlotController.SetCurrentPlotSet(psID2) + psID3 = PlotController.DeletePlotSet() # current plot set + self.assertEqual(psID3, psID2) + self.assertTrue(self.areScreenshotEqual(tw)) + + def testSetCurrentCurve(self): + tw = self.showTabWidget() + self.assertRaises(ValueError, PlotController.SetCurrentCurve, 23) + x, y = self.generateSine() + crvID, psID = PlotController.AddCurve(x, y, append=False) + _, _ = PlotController.AddCurve(x, y, append=False) # in a new plot set + psID2 = PlotController.SetCurrentCurve(crvID) + self.assertEqual(psID, psID2) + self.assertTrue(self.areScreenshotEqual(tw)) + + def testSetCurrentCurve2(self): + tw = self.showTabWidget() + x, y = self.generateSine() + crvID, psID = PlotController.AddCurve(x, y, append=False) + PlotController.SetCurrentCurve(crvID) + _, crvID2 = PlotController.AddCurve(x, y, append=False) # in a new plot set + PlotController.SetCurrentCurve(crvID2) + # on first plot set curve should not be selected anymore + PlotController.SetCurrentPlotSet(psID) + self.assertTrue(self.areScreenshotEqual(tw)) + + def testSetCurrentCurve3(self): + tw = self.showTabWidget() + x, y = self.generateSine() + crvID, _ = PlotController.AddCurve(x, y, append=False) + # Selecting and de-selecting + PlotController.SetCurrentCurve(crvID) + PlotController.SetCurrentCurve(-1) + self.assertTrue(self.areScreenshotEqual(tw)) + + def testSetCurrentPlotSet(self): + tw = self.showTabWidget() + psID = PlotController.AddPlotSet("tutu") + PlotController.AddPlotSet("tata") + PlotController.SetCurrentPlotSet(psID) + self.assertTrue(self.areScreenshotEqual(tw)) + self.assertRaises(ValueError, PlotController.SetCurrentPlotSet, 124) # invalid ps_id + + def testSetLabelX(self): + tw = self.showTabWidget() + ps_id = PlotController.AddPlotSet("My plotset") + PlotController.SetXLabel(u"The X-s éà", ps_id) + self.assertTrue(self.areScreenshotEqual(tw)) + + def testSetLabelY(self): + tw = self.showTabWidget() + ps_id = PlotController.AddPlotSet("My plotset") + PlotController.SetYLabel(u"Tutu", ps_id) + PlotController.SetYLabel(u"The Y-s uûàç", ps_id) + self.assertTrue(self.areScreenshotEqual(tw)) + + def testSetPlotSetTitle(self): + tw = self.showTabWidget() + ps_id = PlotController.AddPlotSet("tutu") + PlotController.AddPlotSet("tata") + PlotController.SetPlotSetTitle(u"un titre àé", ps_id) + PlotController.SetCurrentPlotSet(ps_id) + self.assertTrue(self.areScreenshotEqual(tw)) + +# def testToggleCurveBrowser(self): +# # hard to test ... +# raise NotImplementedError + + def testPlotCurveFromTable(self): + tw = self.showTabWidget() + from TableModel import TableModel + t = TableModel(None) + t.setTitle("coucou") + t.addColumn([1.0,2.0,3.0,4.0]) + t.addColumn([1.0,2.0,3.0,4.0]) + t.addColumn([1.0,4.0,9.0,16.0]) + t.setColumnTitle(0, "X-s") + t.setColumnTitle(1, "Identity") + t.setColumnTitle(2, "Square") + cont = PlotController.GetInstance() + cont.plotCurveFromTable(t, y_col_index=1, append=False) + cont.plotCurveFromTable(t, y_col_index=2, append=True) + self.assertTrue(self.areScreenshotEqual(tw)) + + def testSettingsCurveColor(self): + tw = self.showTabWidget() + x, y = self.generateSine() + crvID, _ = PlotController.AddCurve(x, y, append=False) + PlotController.SetCurrentCurve(crvID) + # Emulate changing the curve color from the settings box: + dlg_test = PlotSettings() + def fun(): + dlg_test.setRGB(0,0,0) + dlg_test.showLegendCheckBox.setChecked(True) + return True + dlg_test.exec_ = fun + t = PlotController.GetInstance()._curveTabsView._XYViews.items() + t[0][1].onSettings(dlg_test=dlg_test) + self.assertTrue(self.areScreenshotEqual(tw)) + + def testExtendCurve(self): + tw = self.showTabWidget() + x, y = self.generateSine() + crvID, _ = PlotController.AddCurve(x, y, append=False) + PlotController.SetCurrentCurve(crvID) + PlotController.ExtendCurve(crvID, x+100.0, y*2.0) + # Curve must remain blue, bold and with first marker: + self.assertTrue(self.areScreenshotEqual(tw)) + + def testResetCurve(self): + tw = self.showTabWidget() + x, y = self.generateSine() + crvID, _ = PlotController.AddCurve(x, y, append=False) + PlotController.SetCurrentCurve(crvID) + PlotController.ResetCurve(crvID) + PlotController.ExtendCurve(crvID, x+100.0, y*x) + # Curve must remain blue, bold and with first marker: + self.assertTrue(self.areScreenshotEqual(tw)) + + def testSettingsCurveMarker(self): + tw = self.showTabWidget() + x, y = self.generateSine() + crvID, _ = PlotController.AddCurve(x, y, append=False) + PlotController.SetCurrentCurve(crvID) + # Emulate changing the marker from the settings box: + dlg_test = PlotSettings() + def fun(): + dlg_test.markerCurve.setCurrentIndex(2) + dlg_test.showLegendCheckBox.setChecked(True) + return True + dlg_test.exec_ = fun + t = PlotController.GetInstance()._curveTabsView._XYViews.items() + t[0][1].onSettings(dlg_test=dlg_test) + self.assertTrue(self.areScreenshotEqual(tw)) + + def testSetCurveMarker(self): + tw = self.showTabWidget() + x, y = self.generateSine() + crvID, _ = PlotController.AddCurve(x, y, append=False) + PlotController.SetCurveMarker(crvID, "v") + self.assertTrue(self.areScreenshotEqual(tw)) + + def testSetCurveLabel(self): + tw = self.showTabWidget() + x, y = self.generateSine() + crvID, _ = PlotController.AddCurve(x, y, curve_label="titi", append=False) + _, _ = PlotController.AddCurve(x, y, curve_label="toto", append=True) + PlotController.SetCurrentCurve(crvID) + PlotController.SetCurveLabel(crvID, "tata") + self.assertTrue(self.areScreenshotEqual(tw)) + + def testToggleXLog(self): + tw = self.showTabWidget() + x, y = self.generateExp() + _, psID = PlotController.AddCurve(x, y, curve_label="titi", append=False) + PlotController.SetXLog(psID, True) + PlotController.SetYSciNotation(psID, True) + self.assertTrue(self.areScreenshotEqual(tw)) + + def testToggleYLog(self): + tw = self.showTabWidget() + x, y = self.generateExp() + _, psID = PlotController.AddCurve(x, y, curve_label="titi", append=False) + PlotController.SetYLog(psID, True) + PlotController.SetYSciNotation(psID, True) + self.assertTrue(self.areScreenshotEqual(tw)) + + def testSetXSciNotation(self): + tw = self.showTabWidget() + x, y = self.generateSine() + _, psID = PlotController.AddCurve(x*1.0e6, y*1.0e6, curve_label="titi", append=False) + PlotController.SetXSciNotation(psID, True) + self.assertTrue(self.areScreenshotEqual(tw)) + + def testSetYSciNotation(self): + tw = self.showTabWidget() + x, y = self.generateSine() + _, psID = PlotController.AddCurve(x*1.0e6, y*1.0e6, curve_label="titi", append=False) + PlotController.SetYSciNotation(psID, True) + self.assertTrue(self.areScreenshotEqual(tw)) + + def testRegisterCallback(self): + global a_callb + a_callb = 0 + def fun(crv_id): + global a_callb + a_callb = crv_id + self.showTabWidget() + x, y = self.generateExp() + crvId, _ = PlotController.AddCurve(x, y) + PlotController.RegisterCallback(fun) + PlotController.SetCurrentCurve(crvId) + self.assertEqual(crvId, a_callb) + + def testDeleteCallback(self): + global a_callb + a_callb = 0 + def fun(crv_id): + global a_callb + a_callb = crv_id + self.showTabWidget() + x, y = self.generateExp() + crvId, _ = PlotController.AddCurve(x, y) + PlotController.RegisterCallback(fun) + PlotController.ClearCallbacks() + PlotController.SetCurrentCurve(crvId) + _, _ = PlotController.AddCurve(x, y) + self.assertEqual(crvId, a_callb) + + def testAddCurveEmptyPs(self): + """ Adding a curve when no ps was active was buggy """ + self.showTabWidget() + x, y = self.generateSine() + PlotController.AddPlotSet("toto") + # No current plot set: + PlotController.SetCurrentPlotSet(-1) + # Should create a new plot set: + PlotController.AddCurve(x, y, append=True) + l, _ = PlotController.GetAllPlotSets() + self.assertEqual(2, len(l)) + + def test_onCurrentCurveChange(self): + self.showTabWidget() + x, y = self.generateSine() + crvID, _ = PlotController.AddCurve(x, y) + PlotController.SetCurrentCurve(crvID) + PlotController.DeleteCurve(crvID) + crvID2, _ = PlotController.AddCurve(x, y) + # was throwing: + PlotController.SetCurrentCurve(crvID2) + + def testSetLegendVisible(self): + tw = self.showTabWidget() + x, y = self.generateSine() + _, psID = PlotController.AddCurve(x, y, curve_label="titi", append=False) + PlotController.SetLegendVisible(psID, False) # by default legend is always visible in the tests + self.assertTrue(self.areScreenshotEqual(tw)) + + def testLockRepaint(self): + tw = self.showTabWidget() + x, y = self.generateSine() + PlotController.LockRepaint() + for i in range(10): + _, psID = PlotController.AddCurve(x, y*float(i+1), append=True) + PlotController.UnlockRepaint() + self.assertTrue(self.areScreenshotEqual(tw)) + + def testDelPlotSetSelectPrev(self): + """ When deleting a full plot set, the previous plot set should become active """ + self.showTabWidget() + x, y = self.generateSine() + _, psID0 = PlotController.AddCurve(x, y, append=True) # creates a new plot set + _, psID1 = PlotController.AddCurve(x, y, append=False) # creates a new plot set + PlotController.DeletePlotSet(psID1) + PlotController.AddCurve(x, y, append=True) # should NOT create a new plot set + psID2 = PlotController.GetCurrentPlotSetID() + self.assertEqual(psID0, psID2) + l, _ = PlotController.GetAllPlotSets() + self.assertEqual(1, len(l)) + +# Even if not in main: +processDecorator(__name__) + +if __name__ == "__main__": + import unittest + unittest.main() diff --git a/tools/CurvePlot/src/python/ui/CMakeLists.txt b/tools/CurvePlot/src/python/ui/CMakeLists.txt new file mode 100644 index 000000000..a52511cd2 --- /dev/null +++ b/tools/CurvePlot/src/python/ui/CMakeLists.txt @@ -0,0 +1,40 @@ +# Copyright (C) 2012-2014 CEA/DEN, EDF R&D, OPEN CASCADE +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# + +# --- resources --- + +# uic files +SET(_pyuic_files + CurveTreeDockWidget.ui + PlotWidget.ui + PlotSettings.ui +) + +# --- scripts --- +SET(_all_lib_SCRIPTS + CurveTreeDockWidget.py + PlotWidget.py + PlotSettings.py +) + +# --- rules --- +SALOME_INSTALL_SCRIPTS("${_pyuic_SCRIPTS}" ${SALOME_CURVEPLOT_INSTALL_PYTHON}) +SALOME_INSTALL_SCRIPTS("${_all_lib_SCRIPTS}" ${SALOME_CURVEPLOT_INSTALL_PYTHON}) + +INSTALL(FILES ${_pyuic_files} DESTINATION ${SALOME_CURVEPLOT_INSTALL_PYTHON}) diff --git a/tools/CurvePlot/src/python/ui/CurveTreeDockWidget.py b/tools/CurvePlot/src/python/ui/CurveTreeDockWidget.py new file mode 100644 index 000000000..bda0f5dec --- /dev/null +++ b/tools/CurvePlot/src/python/ui/CurveTreeDockWidget.py @@ -0,0 +1,18 @@ +from pyqtside import QtGui, QtCore +from pyqtside.uic import loadUiGen +from utils import completeResPath + +class CurveTreeDockWidget(QtGui.QDockWidget): + def __init__(self): + QtGui.QDockWidget.__init__(self) + loadUiGen(completeResPath("CurveTreeDockWidget.ui"), self) + self.treeWidget.setHeaderLabel ("Plots") + self.treeWidget.sortByColumn(0, QtCore.Qt.AscendingOrder) + self.treeWidget.setSortingEnabled(True); + self.treeWidget.setColumnHidden(1, True); + + def getTreeWidget(self): + """ + :returns: QTreeWidget -- the (curve) browser + """ + return self.treeWidget diff --git a/tools/CurvePlot/src/python/ui/CurveTreeDockWidget.ui b/tools/CurvePlot/src/python/ui/CurveTreeDockWidget.ui new file mode 100644 index 000000000..b13925a3a --- /dev/null +++ b/tools/CurvePlot/src/python/ui/CurveTreeDockWidget.ui @@ -0,0 +1,43 @@ + + + CurveTreeDockWidget + + + + 0 + 0 + 342 + 471 + + + + Plot browser + + + + + + + true + + + 2 + + + + 1 + + + + + 2 + + + + + + + + + + diff --git a/tools/CurvePlot/src/python/ui/PlotSettings.py b/tools/CurvePlot/src/python/ui/PlotSettings.py new file mode 100644 index 000000000..bd6aac256 --- /dev/null +++ b/tools/CurvePlot/src/python/ui/PlotSettings.py @@ -0,0 +1,89 @@ +from pyqtside import QtGui, QtCore +from pyqtside.uic import loadUiGen +from utils import completeResPath + +class PlotSettings(QtGui.QDialog): + def __init__(self): + QtGui.QDialog.__init__(self) + loadUiGen(completeResPath("PlotSettings.ui"), self) + self.initialize() + + def initialize(self): + self.legendPositionComboBox.addItem("Bottom") + self.legendPositionComboBox.addItem("Right") + self._r = 0 + self._g = 0 + self._b = 1 + + @QtCore.Slot(int) + def onShowLegend(self, index): + if index > 0 : + self.legendPositionComboBox.setEnabled(True) + else : + self.legendPositionComboBox.setEnabled(False) + + @QtCore.Slot() + def onChangeColor(self): + col = QtGui.QColorDialog.getColor() + + if col.isValid(): + r, g, b = [c/255.0 for c in col.getRgb()[:3]] + self.setRGB(r, g, b) + + def setSelectedCurveName(self, name): + if name : + self.selectedCurvePanel.setTitle("Selected curve : " + name) + self.selectedCurvePanel.show() + else : + self.selectedCurvePanel.hide() + + def setRGB(self, r, g, b): + self._r = r + self._g = g + self._b = b + self.colorCurve.setIcon(QtGui.QIcon(self.drawColorPixmap(int(r*255), int(g*255), int(b*255)))) + + def getRGB(self): + return self._r, self._g, self._b + + def drawColorPixmap(self, r, g, b): + pix = QtGui.QPixmap( 16, 16 ) + color = QtGui.QColor(r, g, b) + pix.fill(color) + return pix + + def accept(self): + xminText = unicode(self.axisXMinEdit.text()) + xmaxText = unicode(self.axisXMaxEdit.text()) + yminText = unicode(self.axisYMinEdit.text()) + ymaxText = unicode(self.axisYMaxEdit.text()) + if (yminText == "" or ymaxText == "") : + QtGui.QMessageBox.critical(self, "Plot settings", "A field \"YMin\" or \"YMax\" is empty") + else : + try: + xmin = float(xminText) + except ValueError: + QtGui.QMessageBox.critical(self, "Plot settings", "It is not possible to convert XMin") + try: + xmax = float(xmaxText) + except ValueError: + QtGui.QMessageBox.critical(self, "Plot settings", "It is not possible to convert XMax") + try: + ymin = float(yminText) + except ValueError: + QtGui.QMessageBox.critical(self, "Plot settings", "It is not possible to convert YMin") + try: + ymax = float(ymaxText) + except ValueError: + QtGui.QMessageBox.critical(self, "Plot settings", "It is not possible to convert YMax") + if ((xmax-xmin) == 0) : + QtGui.QMessageBox.critical(self, "Plot settings", "XMax is is equal to XMin.") + return + if ((ymax-ymin) == 0) : + QtGui.QMessageBox.critical(self, "Plot settings", "YMax is is equal to YMin.") + return + if ((xmax-xmin) < 0) : + QtGui.QMessageBox.warning(self, "Plot settings", "XMax is less than XMin.") + if ((ymax-ymin) < 0) : + QtGui.QMessageBox.warning(self, "Plot settings", "YMax is less than YMin.") + super(PlotSettings, self).accept() diff --git a/tools/CurvePlot/src/python/ui/PlotSettings.ui b/tools/CurvePlot/src/python/ui/PlotSettings.ui new file mode 100644 index 000000000..687e47a20 --- /dev/null +++ b/tools/CurvePlot/src/python/ui/PlotSettings.ui @@ -0,0 +1,548 @@ + + + PlotSettings + + + + 0 + 0 + 688 + 583 + + + + + 0 + 0 + + + + CurvePlot settings + + + + + + QLayout::SetDefaultConstraint + + + + + + + Main title : + + + + + + + + + + + + + 0 + 50 + + + + Grid + + + false + + + + + 10 + 20 + 771 + 21 + + + + Show grid + + + + + + + + + 0 + 50 + + + + Legend + + + + + + + + Show legend + + + false + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 40 + 20 + + + + + + + + Position : + + + + + + + + 0 + 0 + + + + + + + + + + + + + + 0 + 100 + + + + Selected curve + + + + + 11 + 35 + 291 + 57 + + + + + + + + + Name : + + + + + + + + + + + + 145 + 145 + 145 + + + + + + + 105 + 105 + 105 + + + + + + + + + 145 + 145 + 145 + + + + + + + 105 + 105 + 105 + + + + + + + + + 149 + 151 + 153 + + + + + + + 158 + 158 + 158 + + + + + + + + true + + + + + + + + + + + Color + + + + + + + + 40 + 16777215 + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 25 + 0 + + + + + 48 + 16777215 + + + + Marker + + + + + + + + 50 + 16777215 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + 0 + 50 + + + + + 16777215 + 423 + + + + 0 + + + + Axis X + + + + + + + + Title : + + + + + + + + + + XMin : + + + + + + + + + + XMax : + + + + + + + + + + Scientific notation + + + + + + + + + Qt::Vertical + + + + 20 + 65 + + + + + + + + + Axis Y + + + + + + + + Title : + + + + + + + + + + YMin : + + + + + + + + + + YMax : + + + + + + + + + + Scientific notation + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + false + + + + + + + + + + + buttonBox + rejected() + PlotSettings + reject() + + + 321 + 577 + + + 286 + 274 + + + + + buttonBox + accepted() + PlotSettings + accept() + + + 253 + 577 + + + 157 + 274 + + + + + showLegendCheckBox + stateChanged(int) + PlotSettings + onShowLegend(int) + + + 56 + 130 + + + 198 + 161 + + + + + colorCurve + clicked() + PlotSettings + onChangeColor() + + + 74 + 232 + + + 343 + 291 + + + + + + onShowLegend(int) + onChangeColor() + + diff --git a/tools/CurvePlot/src/python/ui/PlotWidget.py b/tools/CurvePlot/src/python/ui/PlotWidget.py new file mode 100644 index 000000000..677c5496a --- /dev/null +++ b/tools/CurvePlot/src/python/ui/PlotWidget.py @@ -0,0 +1,16 @@ +from pyqtside import QtGui +from pyqtside.uic import loadUiGen +from utils import completeResPath + +class PlotWidget(QtGui.QMainWindow): + def __init__(self): + QtGui.QMainWindow.__init__(self) + loadUiGen(completeResPath("PlotWidget.ui"), self) + + def clearAll(self): + """ In test context, the PlotWidget is never fully deleted (because the PyQt binding + of QTabWidget doesn't remove completly the references it holds). + So clean up manually. + """ + self.toolBar = None + self.setCentralWidget(None) diff --git a/tools/CurvePlot/src/python/ui/PlotWidget.ui b/tools/CurvePlot/src/python/ui/PlotWidget.ui new file mode 100644 index 000000000..d4bbfe16c --- /dev/null +++ b/tools/CurvePlot/src/python/ui/PlotWidget.ui @@ -0,0 +1,31 @@ + + + PlotWidget + + + + 0 + 0 + 800 + 598 + + + + MainWindow + + + + + toolBar + + + TopToolBarArea + + + false + + + + + + diff --git a/tools/CurvePlot/src/python/views/CMakeLists.txt b/tools/CurvePlot/src/python/views/CMakeLists.txt new file mode 100644 index 000000000..7186c1978 --- /dev/null +++ b/tools/CurvePlot/src/python/views/CMakeLists.txt @@ -0,0 +1,28 @@ +# Copyright (C) 2012-2014 CEA/DEN, EDF R&D +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# + +SET(_all_lib_SCRIPTS + View.py + CurveBrowserView.py + CurveTabsView.py + CurveView.py + XYView.py +) + +SALOME_INSTALL_SCRIPTS("${_all_lib_SCRIPTS}" ${SALOME_CURVEPLOT_INSTALL_PYTHON}) diff --git a/tools/CurvePlot/src/python/views/CurveBrowserView.py b/tools/CurvePlot/src/python/views/CurveBrowserView.py new file mode 100644 index 000000000..ad2facd56 --- /dev/null +++ b/tools/CurvePlot/src/python/views/CurveBrowserView.py @@ -0,0 +1,104 @@ +from pyqtside import QtGui +from pyqtside.QtGui import QMenu +from pyqtside.QtCore import Qt + +from View import View +from CurveTreeDockWidget import CurveTreeDockWidget +from utils import Logger + +class CurveBrowserView( View, CurveTreeDockWidget) : + + def __init__( self, controller) : + """ Constructor """ + View.__init__( self, controller) + CurveTreeDockWidget.__init__(self) + self._noUpdate = False + + treeWidget = self.getTreeWidget() + treeWidget.itemSelectionChanged.connect(self.onItemSelectionChanged) + treeWidget.itemDoubleClicked.connect(self.onItemDoubleCliked) + treeWidget.setContextMenuPolicy(Qt.CustomContextMenu) + treeWidget.customContextMenuRequested.connect(self.openMenu) + + def update(self): + """ Update this view due to model change """ + if self._model == None or self._noUpdate: + return + + # widget clear and repopulation will trigger onItemSelectionChanged(), + # which in turns triggers update() (via setCurrentCurve()). So avoid recursion: + self._noUpdate = True + + treeWidget = self.getTreeWidget() + treeWidget.clear() + + plotSets = self._model._plotSets + + # The second (hidden) column in the tree bares the ID of the object and its nature (plotset or curve) + for p in plotSets.values(): + item = QtGui.QTreeWidgetItem([unicode(p.getTitle()), unicode(p.getID()) + '_set']) + treeWidget.addTopLevelItem(item) + for c in p._curves.values(): + chld = QtGui.QTreeWidgetItem([unicode(c.getTitle()), unicode(c.getID()) + '_crv']) + item.addChild(chld) + + treeWidget.expandAll() + + # Finally select the proper item in the tree: + cps = self._model.getCurrentPlotSet() + if not cps is None: + ccv = cps.getCurrentCurve() + if ccv is None: + key = str(cps.getID()) + '_set' + else: + key = str(ccv.getID()) + '_crv' + listItems = treeWidget.findItems(key, Qt.MatchExactly | Qt.MatchRecursive,1) + if len(listItems) > 0: + treeWidget.setCurrentItem(listItems[0]) + + self._noUpdate = False + + def onItemSelectionChanged(self): + """ + Change the current selected XYplot/curve + """ + if self._noUpdate: + return + + # setCurrentCurve() and setCurrentPlotSet() will trigger update(), + # which in turns triggers onItemSelectionChanged(). So avoid recursion: + self._noUpdate = True + + pm = self._controller._plotManager + treeWidget = self.getTreeWidget() + it = treeWidget.currentItem() + if not it is None: + objStr = str(it.text(1)) # no unicode here! + objID = int(objStr[0:-4]) + objTyp = objStr[-3:] + if objTyp == 'crv': + # Find correct plot set: + cps = pm.getPlotSetContainingCurve(objID) + if not cps is None: + cps.setCurrentCurve(objID) + pm.setCurrentPlotSet(cps.getID()) + elif objTyp == 'set': + pm.clearAllCurrentCurve() + pm.setCurrentPlotSet(objID) + else: + raise Exception("Internal error - should not happen") + else: + ps = pm.getCurrentPlotSet() + if not ps is None: + ps.setCurrentCurve(-1) + pm.setCurrentPlotSet(-1) + + self._noUpdate = False + + def onItemDoubleCliked(self): + Logger.Debug("item doubled clicked") + + def openMenu(self, position): + menu = self._controller._browserContextualMenu + treeWidget = self.getTreeWidget() + menu.exec_(treeWidget.mapToGlobal(position)) diff --git a/tools/CurvePlot/src/python/views/CurveTabsView.py b/tools/CurvePlot/src/python/views/CurveTabsView.py new file mode 100644 index 000000000..3454837c6 --- /dev/null +++ b/tools/CurvePlot/src/python/views/CurveTabsView.py @@ -0,0 +1,76 @@ +from View import View +from XYView import XYView +from utils import Logger + +class CurveTabsView(View): + def __init__(self, controller): + View.__init__(self, controller) + self._XYViews = {} # key: SALOME view ID, value: XYView + + def closeXYView(self, salomeViewID): + Logger.Debug("CurveTabsView::closeXYView: %d" % salomeViewID) + self._controller.setCurvePlotRequestingClose(True) + self._controller._sgPyQt.closeView(salomeViewID) + self._controller.setCurvePlotRequestingClose(False) + # Destroy the view + self._XYViews.pop(salomeViewID) + Logger.Debug("CurveTabsView::closeXYView count %d" % len(self._XYViews)) + + def createXYView(self, model): + v = XYView(self._controller) + self._controller.associate(model, v) + return v + + def onCurrentPlotSetChange(self): + """ Avoid a unnecessary call to update() when just switching current plot set! """ + cps = self._model.getCurrentPlotSet() + if not cps is None: + mp = self.mapModId2ViewId() + salomeViewID = mp[cps.getID()] + self._controller._sgPyQt.activateView(salomeViewID) + + def mapModId2ViewId(self): + """ Gives a map from model ID (the model behind the XYView) to view ID + """ + lst = [(v._model.getID(), view_id) for view_id, v in self._XYViews.items()] + return dict(lst) + + def update(self): + """ + Updates the list of tabs shown in the GUI + """ + if self._model is None: + return + + # Check list of tabs: + set_mod = set(self._model._plotSets.keys()) + set_view = set([ v._model.getID() for v in self._XYViews.values() ]) + mp = self.mapModId2ViewId() + + # Deleted/Added curves: + dels = set_view - set_mod + added = set_mod - set_view + + for d in dels: + salomeViewID = mp[d] + v = self._XYViews[salomeViewID] + v.cleanBeforeClose() + self.closeXYView(salomeViewID) + + newViews = [] + for a in added: + newViews.append(self.createXYView(self._model._plotSets[a])) + + # Now update all tabs individually (this will trigger creation of new ones if not already there): + for v in self._XYViews.values() + newViews: + # The update on newViews will trigger the SALOME view creation: + v.update() + + # And complete internal structure for new views + # This is not done in + for v in newViews: + self._XYViews[v._salomeViewID] = v + + # Finally activate the proper tab: + self.onCurrentPlotSetChange() + \ No newline at end of file diff --git a/tools/CurvePlot/src/python/views/CurveView.py b/tools/CurvePlot/src/python/views/CurveView.py new file mode 100644 index 000000000..39ae897f1 --- /dev/null +++ b/tools/CurvePlot/src/python/views/CurveView.py @@ -0,0 +1,92 @@ +from View import View +from utils import Logger + +class CurveView(View): + _PICKER_PRECISION = 20 #pts + + def __init__(self, controller, parentXYView): + View.__init__(self, controller) + self._mplAxes = None + self._mplLines = None + self._isHighlighted = False + self._initialLineWidth = None + self._parentXYView = parentXYView + + self._marker = None + self._color = None + self._lineStyle = None + + def setMPLAxes(self, axes): + self._mplAxes = axes + + def erase(self): + self._mplAxes.lines.remove(self._mplLines[0]) + self._mplLines = None + + def draw(self): + m = self._model + x_idx, y_idx = m.getXAxisIndex(), m.getYAxisIndex() + d = self._model.getTable().getData() + self._mplLines = self._mplAxes.plot(d[:, x_idx], d[:, y_idx], label=m._title, + picker=self._PICKER_PRECISION) + self._initialLineWidth = self._mplLines[0].get_linewidth() + + def onCurveTitleChange(self): + if self._mplLines is None: + return + self._mplLines[0].set_label(self._model._title) + + def update(self): + Logger.Debug("CurveView::udpate") + if self._mplLines is None: + return + lineStyle, marker, color = self.getLineStyle(), self.getMarker(), self.getColor() + self.erase() + self.draw() + # Reset correctly color, marker and highlight state + self.setLineStyle(lineStyle) + self.setMarker(marker) + self.setColor(color) + self.toggleHighlight(self._isHighlighted, force=True) + + def setLineStyle(self, lin_style): + lin = self._mplLines[0] + lin.set_linestyle(lin_style) + + def getLineStyle(self): + if self._mplLines is None: + return None + return self._mplLines[0].get_linestyle() + + def setMarker(self, marker): + lin = self._mplLines[0] + lin.set_marker(marker) + + def getMarker(self): + if self._mplLines is None: + return None + return self._mplLines[0].get_marker() + + def toggleHighlight(self, highlight, force=False): + lin = self._mplLines[0] + if highlight and (force or not self._isHighlighted): + lin.set_linewidth(2*self._initialLineWidth) + self._isHighlighted = True + elif not highlight and (force or self._isHighlighted): + lin.set_linewidth(self._initialLineWidth) + self._isHighlighted = False + else: + # Nothing to do, already the correct state + return + + def isHighlighted(self): + return self._isHighlighted + + def setColor(self, rgb_color): + lin = self._mplLines[0] + lin.set_color(rgb_color) + + def getColor(self): + if self._mplLines is None: + return None + return self._mplLines[0].get_color() diff --git a/tools/CurvePlot/src/python/views/View.py b/tools/CurvePlot/src/python/views/View.py new file mode 100644 index 000000000..c2afeb77f --- /dev/null +++ b/tools/CurvePlot/src/python/views/View.py @@ -0,0 +1,53 @@ +class View(object) : + + def __init__( self, controller, model=None ) : + """Constructor""" + + self._model = model + self._controller = controller + pass + + def getModel( self ) : + """ + :returns: Model -- The view's model. + """ + return self._model + + def setModel( self, model ) : + """ + Associates a model to the view. + + :param model: Model -- The model to be associated. + + """ + self._model = model + pass + + def getController( self ) : + """ + :returns: Controller -- The controller of the view. + """ + return self._controller + + def setController( self, controller ) : + """ + Associates a controller to the view. + + :param controller: Controller -- The controller to be associated. + + """ + self._controller = controller + pass + + def update( self) : + """ + Updates the view contents. + + .. note:: + + Virtual Method. + + """ + raise NotImplementedError + +pass diff --git a/tools/CurvePlot/src/python/views/XYView.py b/tools/CurvePlot/src/python/views/XYView.py new file mode 100644 index 000000000..e92b7cf52 --- /dev/null +++ b/tools/CurvePlot/src/python/views/XYView.py @@ -0,0 +1,702 @@ +import matplotlib.pyplot as plt +import matplotlib.colors as colors +from View import View +from CurveView import CurveView + +from utils import Logger, trQ +from PlotWidget import PlotWidget +from PlotSettings import PlotSettings +from pyqtside import QtGui, QtCore +from pyqtside.QtCore import QObject +from matplotlib.figure import Figure +from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg, NavigationToolbar2QT + +class EventHandler(QObject): + """ Handle the right-click properly so that it only triggers the contextual menu """ + def __init__(self,parent=None): + QObject.__init__(self, parent) + + def eventFilter(self, obj, event): + if event.type() == QtCore.QEvent.MouseButtonPress: + if event.button() == 2: + # Discarding right button press to only keep context menu display + return True # Event handled (and hence not passed to matplotlib) + return QObject.eventFilter(self, obj, event) + +class XYView(View): + AUTOFIT_MARGIN = 0.03 # 3% + + # See http://matplotlib.org/api/markers_api.html: + CURVE_MARKERS = [ "o" ,# circle + "*", # star + "+", # plus + "x", # x + "s", # square + "p", # pentagon + "h", # hexagon1 + "8", # octagon + "D", # diamond + "^", # triangle_up + "<", # triangle_left + ">", # triangle_right + "1", # tri_down + "2", # tri_up + "3", # tri_left + "4", # tri_right + "v", # triangle_down + "H", # hexagon2 + "d", # thin diamond + "", # NO MARKER + ] + + _DEFAULT_LEGEND_STATE = False # for test purposes mainly - initial status of the legend + + def __init__(self, controller): + View.__init__(self, controller) + self._eventHandler = EventHandler() + + self._curveViews = {} # key: curve (model) ID, value: CurveView + self._salomeViewID = None + self._mplFigure = None + self._mplAxes = None + self._mplCanvas = None + self._plotWidget = None + self._sgPyQt = self._controller._sgPyQt + self._toolbar = None + self._mplNavigationActions = {} + self._toobarMPL = None + self._grid = None + self._currCrv = None # current curve selected in the view + + self._legend = None + self._legendLoc = "right" # "right" or "bottom" + + self._fitArea = False + self._zoomPan = False + self._dragOnDrop = False + self._move = False + + self._patch = None + self._xdata = None + self._ydata = None + self._defaultLineStyle = None + self._last_point = None + self._lastMarkerID = -1 + self._blockLogSignal = False + + self._axisXSciNotation = False + self._axisYSciNotation = False + self._prevTitle = None + + def __repaintOK(self): + """ To be called inside XYView each time a low-level expansive matplotlib methods is to be invoked. + @return False if painting is currently locked, in which case it will also register the current XYView + as needing a refresh when unlocked + """ + ret = self._controller._plotManager.isRepaintLocked() + if ret: + self._controller._plotManager.registerRepaint(self._model) + return (not ret) + + def appendCurve(self, curveID): + newC = CurveView(self._controller, self) + newC.setModel(self._model._curves[curveID]) + newC.setMPLAxes(self._mplAxes) + newC.draw() + newC.setMarker(self.getMarker(go_next=True)) + self._curveViews[curveID] = newC + + def removeCurve(self, curveID): + v = self._curveViews.pop(curveID) + v.erase() + if self._currCrv is not None and self._currCrv.getID() == curveID: + self._currCrv = None + + def cleanBeforeClose(self): + """ Clean some items to avoid accumulating stuff in memory """ + self._mplFigure.clear() + plt.close(self._mplFigure) + self._plotWidget.clearAll() + # For memory debugging only: + import gc + gc.collect() + + def repaint(self): + if self.__repaintOK(): + Logger.Debug("XYView::draw") + self._mplCanvas.draw() + + def onXLabelChange(self): + if self.__repaintOK(): + self._mplAxes.set_xlabel(self._model._xlabel) + self.repaint() + + def onYLabelChange(self): + if self.__repaintOK(): + self._mplAxes.set_ylabel(self._model._ylabel) + self.repaint() + + def onTitleChange(self): + if self.__repaintOK(): + self._mplAxes.set_title(self._model._title) + self.updateViewTitle() + self.repaint() + + def onCurveTitleChange(self): + # Updating the legend should suffice + self.showHideLegend() + + def onClearAll(self): + """ Just does an update with a reset of the marker cycle. """ + if self.__repaintOK(): + self._lastMarkerID = -1 + self.update() + + def onPick(self, event): + """ MPL callback when picking + """ + if event.mouseevent.button == 1: + selected_id = -1 + a = event.artist + for crv_id, cv in self._curveViews.items(): + if cv._mplLines[0] is a: + selected_id = crv_id + # Use the plotmanager so that other plot sets get their current reset: + self._controller._plotManager.setCurrentCurve(selected_id) + + def createAndAddLocalAction(self, icon_file, short_name): + return self._toolbar.addAction(self._sgPyQt.loadIcon("CURVEPLOT", icon_file), short_name) + + def createPlotWidget(self): + self._mplFigure = Figure((8.0,5.0), dpi=100) + self._mplCanvas = FigureCanvasQTAgg(self._mplFigure) + self._mplCanvas.installEventFilter(self._eventHandler) + self._mplCanvas.mpl_connect('pick_event', self.onPick) + self._mplAxes = self._mplFigure.add_subplot(1, 1, 1) + self._plotWidget = PlotWidget() + self._toobarMPL = NavigationToolbar2QT(self._mplCanvas, None) + for act in self._toobarMPL.actions(): + actionName = str(act.text()).strip() + self._mplNavigationActions[actionName] = act + self._plotWidget.setCentralWidget(self._mplCanvas) + self._toolbar = self._plotWidget.toolBar + self.populateToolbar() + + self._popupMenu = QtGui.QMenu() + self._popupMenu.addAction(self._actionLegend) + + # Connect evenement for the graphic scene + self._mplCanvas.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self._mplCanvas.customContextMenuRequested.connect(self.onContextMenu) + self._mplCanvas.mpl_connect('scroll_event', self.onScroll) + self._mplCanvas.mpl_connect('button_press_event', self.onMousePress) + + def populateToolbar(self): + # Action to dump view in a file + a = self.createAndAddLocalAction("dump_view.png", trQ("DUMP_VIEW_TXT")) + a.triggered.connect(self.dumpView) + self._toolbar.addSeparator() + # Actions to manipulate the scene + a = self.createAndAddLocalAction("fit_all.png", trQ("FIT_ALL_TXT")) + a.triggered.connect(self.autoFit) + # Zoom and pan are mutually exclusive but can be both de-activated: + self._zoomAction = self.createAndAddLocalAction("fit_area.png", trQ("FIT_AREA_TXT")) + self._zoomAction.triggered.connect(self.zoomArea) + self._zoomAction.setCheckable(True) + self._panAction = self.createAndAddLocalAction("zoom_pan.png", trQ("ZOOM_PAN_TXT")) + self._panAction.triggered.connect(self.pan) + self._panAction.setCheckable(True) + self._toolbar.addSeparator() + # Actions to change the representation of curves + self._curveActionGroup = QtGui.QActionGroup(self._plotWidget) + self._pointsAction = self.createAndAddLocalAction("draw_points.png", trQ("DRAW_POINTS_TXT")) + self._pointsAction.setCheckable(True) + self._linesAction = self.createAndAddLocalAction("draw_lines.png", trQ("DRAW_LINES_TXT")) + self._linesAction.setCheckable(True) + self._curveActionGroup.addAction(self._pointsAction) + self._curveActionGroup.addAction(self._linesAction) + self._linesAction.setChecked(True) + self._curveActionGroup.triggered.connect(self.changeModeCurve) + self._curveActionGroup.setExclusive(True) + self._toolbar.addSeparator() + # Actions to draw horizontal curves as linear or logarithmic + self._horActionGroup = QtGui.QActionGroup(self._plotWidget) + self._horLinearAction = self.createAndAddLocalAction("hor_linear.png", trQ("HOR_LINEAR_TXT")) + self._horLinearAction.setCheckable(True) + self._horLogarithmicAction = self.createAndAddLocalAction("hor_logarithmic.png", trQ("HOR_LOGARITHMIC_TXT")) + self._horLogarithmicAction.setCheckable(True) + self._horActionGroup.addAction(self._horLinearAction) + self._horActionGroup.addAction(self._horLogarithmicAction) + self._horLinearAction.setChecked(True) + self._horActionGroup.triggered.connect(self.onViewHorizontalMode) + self._toolbar.addSeparator() + # Actions to draw vertical curves as linear or logarithmic + self._verActionGroup = QtGui.QActionGroup(self._plotWidget) + self._verLinearAction = self.createAndAddLocalAction("ver_linear.png", trQ("VER_LINEAR_TXT")) + self._verLinearAction.setCheckable(True) + self._verLogarithmicAction = self.createAndAddLocalAction("ver_logarithmic.png", trQ("VER_LOGARITHMIC_TXT")) + self._verLogarithmicAction.setCheckable(True) + self._verActionGroup.addAction(self._verLinearAction) + self._verActionGroup.addAction(self._verLogarithmicAction) + self._verLinearAction.setChecked(True) + self._verActionGroup.triggered.connect(self.onViewVerticalMode) + self._verActionGroup.setExclusive(True) + self._toolbar.addSeparator() + # Action to show or hide the legend + self._actionLegend = self.createAndAddLocalAction("legend.png", trQ("SHOW_LEGEND_TXT")) + self._actionLegend.setCheckable(True) + self._actionLegend.triggered.connect(self.showHideLegend) + if self._DEFAULT_LEGEND_STATE: + self._actionLegend.setChecked(True) + self._toolbar.addSeparator() + # Action to set the preferences + a = self.createAndAddLocalAction("settings.png", trQ("SETTINGS_TXT")) + a.triggered.connect(self.onSettings) + pass + + def dumpView(self): + # Choice of the view backup file + filters = [] + for form in ["IMAGES_FILES", "PDF_FILES", "POSTSCRIPT_FILES", "ENCAPSULATED_POSTSCRIPT_FILES"]: + filters.append(trQ(form)) + fileName = self._sgPyQt.getFileName(self._sgPyQt.getDesktop(), + "", + filters, + trQ("DUMP_VIEW_FILE"), + False ) + if not fileName.isEmpty(): + name = str(fileName) + self._mplAxes.figure.savefig(name) + pass + + def autoFit(self, check=True, repaint=True): + if self.__repaintOK(): + self._mplAxes.relim() + xm, xM = self._mplAxes.xaxis.get_data_interval() + ym, yM = self._mplAxes.yaxis.get_data_interval() + i = yM-ym + self._mplAxes.axis([xm, xM, ym-i*self.AUTOFIT_MARGIN, yM+i*self.AUTOFIT_MARGIN]) + if repaint: + self.repaint() + + def zoomArea(self): + if self._panAction.isChecked() and self._zoomAction.isChecked(): + self._panAction.setChecked(False) + # Trigger underlying matplotlib action: + self._mplNavigationActions["Zoom"].trigger() + + def pan(self): + if self._panAction.isChecked() and self._zoomAction.isChecked(): + self._zoomAction.setChecked(False) + # Trigger underlying matplotlib action: + self._mplNavigationActions["Pan"].trigger() + + def getMarker(self, go_next=False): + if go_next: + self._lastMarkerID = (self._lastMarkerID+1) % len(self.CURVE_MARKERS) + return self.CURVE_MARKERS[self._lastMarkerID] + + def changeModeCurve(self, repaint=True): + if not self.__repaintOK(): + return + action = self._curveActionGroup.checkedAction() + if action is self._pointsAction : + for crv_view in self._curveViews.values(): + crv_view.setLineStyle("None") + elif action is self._linesAction : + for crv_view in self._curveViews.values(): + crv_view.setLineStyle("-") + else : + raise NotImplementedError + if repaint: + self.repaint() + + def setXLog(self, log, repaint=True): + if not self.__repaintOK(): + return + self._blockLogSignal = True + if log: + self._mplAxes.set_xscale('log') + self._horLogarithmicAction.setChecked(True) + else: + self._mplAxes.set_xscale('linear') + self._horLinearAction.setChecked(True) + if repaint: + self.autoFit() + self.repaint() + self._blockLogSignal = False + + def setYLog(self, log, repaint=True): + if not self.__repaintOK(): + return + self._blockLogSignal = True + if log: + self._mplAxes.set_yscale('log') + self._verLogarithmicAction.setChecked(True) + else: + self._mplAxes.set_yscale('linear') + self._verLinearAction.setChecked(True) + if repaint: + self.autoFit() + self.repaint() + self._blockLogSignal = False + + def setXSciNotation(self, sciNotation, repaint=True): + self._axisXSciNotation = sciNotation + self.changeFormatAxis() + if repaint: + self.repaint() + + def setYSciNotation(self, sciNotation, repaint=True): + self._axisYSciNotation = sciNotation + self.changeFormatAxis() + if repaint: + self.repaint() + + def onViewHorizontalMode(self, checked=True, repaint=True): + if self._blockLogSignal: + return + action = self._horActionGroup.checkedAction() + if action is self._horLinearAction: + self.setXLog(False, repaint) + elif action is self._horLogarithmicAction: + self.setXLog(True, repaint) + else: + raise NotImplementedError + + def onViewVerticalMode(self, checked=True, repaint=True): + if self._blockLogSignal: + return + action = self._verActionGroup.checkedAction() + if action is self._verLinearAction: + self.setYLog(False, repaint) + elif action is self._verLogarithmicAction: + self.setYLog(True, repaint) + else: + raise NotImplementedError + if repaint: + self.repaint() + + def __adjustFigureMargins(self, withLegend): + """ Adjust figure margins to make room for the legend """ + if withLegend: + leg = self._legend + bbox = leg.get_window_extent() + # In axes coordinates: + bbox2 = bbox.transformed(leg.figure.transFigure.inverted()) + if self._legendLoc == "right": + self._mplFigure.subplots_adjust(right=1.0-(bbox2.width+0.02)) + elif self._legendLoc == "bottom": + self._mplFigure.subplots_adjust(bottom=bbox2.height+0.1) + else: + # Reset to default (rc) values + self._mplFigure.subplots_adjust(bottom=0.1, right=0.9) + + def setLegendVisible(self, visible, repaint=True): + if visible and not self._actionLegend.isChecked(): + self._actionLegend.setChecked(True) + self.showHideLegend(repaint=repaint) + if not visible and self._actionLegend.isChecked(): + self._actionLegend.setChecked(False) + self.showHideLegend(repaint=repaint) + + def showHideLegend(self, actionChecked=None, repaint=True): + if not self.__repaintOK(): # Show/hide legend is extremely costly + return + + show = self._actionLegend.isChecked() + nCurves = len(self._curveViews) + if nCurves > 10: fontSize = 'x-small' + else: fontSize = None + + if nCurves == 0: + # Remove legend + leg = self._mplAxes.legend() + if leg is not None: leg.remove() + if show and nCurves > 0: + # Recreate legend from scratch + if self._legend is not None: + self._legend = None + self._mplAxes._legend = None + if self._legendLoc == "bottom": + self._legend = self._mplAxes.legend(loc="upper left", bbox_to_anchor=(0.0, -0.05, 1.0, -0.05), + borderaxespad=0.0, mode="expand", fancybox=True, + shadow=True, ncol=3, prop={'size':fontSize, 'style': 'italic'}) + elif self._legendLoc == "right": + self._legend = self._mplAxes.legend(loc="upper left", bbox_to_anchor=(1.02,1.0), borderaxespad=0.0, + ncol=1, fancybox=True, shadow=True, prop={'size':fontSize, 'style': 'italic'}) + else: + raise Exception("Invalid legend placement! Must be 'bottom' or 'right'") + # Canvas must be drawn so we can adjust the figure placement: + self._mplCanvas.draw() + self.__adjustFigureMargins(withLegend=True) + else: + if self._legend is None: + # Nothing to do + return + else: + self._legend.set_visible(False) + self._legend = None + self._mplAxes._legend = None + self._mplCanvas.draw() + self.__adjustFigureMargins(withLegend=False) + + curr_crv = self._model._currentCurve + if curr_crv is None: curr_title = None + else: curr_title = curr_crv.getTitle() + if self._legend is not None: + for label in self._legend.get_texts() : + text = label.get_text() + if (text == curr_title): + label.set_backgroundcolor('0.85') + else : + label.set_backgroundcolor('white') + + if repaint: + self.repaint() + + def onSettings(self, trigger=False, dlg_test=None): + dlg = dlg_test or PlotSettings() + dlg.titleEdit.setText(self._mplAxes.get_title()) + dlg.axisXTitleEdit.setText(self._mplAxes.get_xlabel()) + dlg.axisYTitleEdit.setText(self._mplAxes.get_ylabel()) + dlg.gridCheckBox.setChecked(self._mplAxes.xaxis._gridOnMajor) # could not find a relevant API to check this + dlg.axisXSciCheckBox.setChecked(self._axisXSciNotation) + dlg.axisYSciCheckBox.setChecked(self._axisYSciNotation) + xmin, xmax = self._mplAxes.get_xlim() + ymin, ymax = self._mplAxes.get_ylim() + xminText = "%g" %xmin + xmaxText = "%g" %xmax + yminText = "%g" %ymin + ymaxText = "%g" %ymax + dlg.axisXMinEdit.setText(xminText) + dlg.axisXMaxEdit.setText(xmaxText) + dlg.axisYMinEdit.setText(yminText) + dlg.axisYMaxEdit.setText(ymaxText) + # List of markers + dlg.markerCurve.clear() + for marker in self.CURVE_MARKERS : + dlg.markerCurve.addItem(marker) + curr_crv = self._model.getCurrentCurve() + if not curr_crv is None: + dlg.colorCurve.setEnabled(True) + dlg.markerCurve.setEnabled(True) + name = curr_crv.getTitle() + dlg.nameCurve.setText(name) + view = self._curveViews[curr_crv.getID()] + marker = view.getMarker() + color = view.getColor() + index = dlg.markerCurve.findText(marker) + dlg.markerCurve.setCurrentIndex(index) + rgb = colors.colorConverter.to_rgb(color) + dlg.setRGB(rgb[0],rgb[1],rgb[2]) + else : + dlg.colorCurve.setEnabled(False) + dlg.markerCurve.setEnabled(False) + dlg.nameCurve.setText("") + view = None + if self._legend is None: + dlg.showLegendCheckBox.setChecked(False) + dlg.legendPositionComboBox.setEnabled(False) + else : + if self._legend.get_visible(): + dlg.showLegendCheckBox.setChecked(True) + dlg.legendPositionComboBox.setEnabled(True) + if self._legendLoc == "bottom": + dlg.legendPositionComboBox.setCurrentIndex(0) + elif self._legendLoc == "right" : + dlg.legendPositionComboBox.setCurrentIndex(1) + else : + dlg.showLegendCheckBox.setChecked(False) + dlg.legendPositionComboBox.setEnabled(False) + + if dlg.exec_(): + # Title + self._model.setTitle(dlg.titleEdit.text()) + # Axis + self._model.setXLabel(dlg.axisXTitleEdit.text()) + self._model.setYLabel(dlg.axisYTitleEdit.text()) + # Grid + if dlg.gridCheckBox.isChecked() : + self._mplAxes.grid(True) + else : + self._mplAxes.grid(False) + # Legend + if dlg.showLegendCheckBox.isChecked(): + self._actionLegend.setChecked(True) + if dlg.legendPositionComboBox.currentIndex() == 0 : + self._legendLoc = "bottom" + elif dlg.legendPositionComboBox.currentIndex() == 1 : + self._legendLoc = "right" + else : + self._actionLegend.setChecked(False) + xminText = dlg.axisXMinEdit.text() + xmaxText = dlg.axisXMaxEdit.text() + yminText = dlg.axisYMinEdit.text() + ymaxText = dlg.axisYMaxEdit.text() + self._mplAxes.axis([float(xminText), float(xmaxText), float(yminText), float(ymaxText)] ) + self._axisXSciNotation = dlg.axisXSciCheckBox.isChecked() + self._axisYSciNotation = dlg.axisYSciCheckBox.isChecked() + self.changeFormatAxis() + # Color and marker of the curve + if view: + view.setColor(dlg.getRGB()) + view.setMarker(self.CURVE_MARKERS[dlg.markerCurve.currentIndex()]) + self.showHideLegend(repaint=True) + self._mplCanvas.draw() + pass + + def updateViewTitle(self): + s = "" + if self._model._title != "": + s = " - %s" % self._model._title + title = "CurvePlot (%d)%s" % (self._model.getID(), s) + self._sgPyQt.setViewTitle(self._salomeViewID, title) + + def onCurrentPlotSetChange(self): + """ Avoid a unnecessary call to update() when just switching current plot set! """ + pass + + def onCurrentCurveChange(self): + curr_crv2 = self._model.getCurrentCurve() + if curr_crv2 != self._currCrv: + if self._currCrv is not None: + view = self._curveViews[self._currCrv.getID()] + view.toggleHighlight(False) + if not curr_crv2 is None: + view = self._curveViews[curr_crv2.getID()] + view.toggleHighlight(True) + self._currCrv = curr_crv2 + self.showHideLegend(repaint=False) # redo legend + self.repaint() + + def changeFormatAxis(self) : + if not self.__repaintOK(): + return + + # don't try to switch to sci notation if we are not using the + # matplotlib.ticker.ScalarFormatter (i.e. if in Log for ex.) + if self._horLinearAction.isChecked(): + if self._axisXSciNotation : + self._mplAxes.ticklabel_format(style='sci',scilimits=(0,0), axis='x') + else : + self._mplAxes.ticklabel_format(style='plain',axis='x') + if self._verLinearAction.isChecked(): + if self._axisYSciNotation : + self._mplAxes.ticklabel_format(style='sci',scilimits=(0,0), axis='y') + else : + self._mplAxes.ticklabel_format(style='plain',axis='y') + + def update(self): + if self._salomeViewID is None: + self.createPlotWidget() + self._salomeViewID = self._sgPyQt.createView("CurvePlot", self._plotWidget) + Logger.Debug("Creating SALOME view ID=%d" % self._salomeViewID) + self._sgPyQt.setViewVisible(self._salomeViewID, True) + + self.updateViewTitle() + + # Check list of curve views: + set_mod = set(self._model._curves.keys()) + set_view = set(self._curveViews.keys()) + + # Deleted/Added curves: + dels = set_view - set_mod + added = set_mod - set_view + + for d in dels: + self.removeCurve(d) + + if not len(self._curveViews): + # Reset color cycle + self._mplAxes.set_color_cycle(None) + + for a in added: + self.appendCurve(a) + + # Axes labels and title + self._mplAxes.set_xlabel(self._model._xlabel) + self._mplAxes.set_ylabel(self._model._ylabel) + self._mplAxes.set_title(self._model._title) + + self.onViewHorizontalMode(repaint=False) + self.onViewVerticalMode(repaint=False) + self.changeModeCurve(repaint=False) + self.showHideLegend(repaint=False) # The canvas is repainted anyway (needed to get legend bounding box) + self.changeFormatAxis() + + # Redo auto-fit + self.autoFit(repaint=False) + self.repaint() + + def onDataChange(self): + # the rest is done in the CurveView: + self.autoFit(repaint=True) + + def onMousePress(self, event): + if event.button == 3 : + if self._panAction.isChecked(): + self._panAction.setChecked(False) + if self._zoomAction.isChecked(): + self._zoomAction.setChecked(False) + + def onContextMenu(self, position): + pos = self._mplCanvas.mapToGlobal(QtCore.QPoint(position.x(),position.y())) + self._popupMenu.exec_(pos) + + def onScroll(self, event): + # Event location (x and y) + xdata = event.xdata + ydata = event.ydata + + cur_xlim = self._mplAxes.get_xlim() + cur_ylim = self._mplAxes.get_ylim() + + base_scale = 2. + if event.button == 'down': + # deal with zoom in + scale_factor = 1 / base_scale + elif event.button == 'up': + # deal with zoom out + scale_factor = base_scale + else: + # deal with something that should never happen + scale_factor = 1 + + new_width = (cur_xlim[1] - cur_xlim[0]) * scale_factor + new_height = (cur_ylim[1] - cur_ylim[0]) * scale_factor + + relx = (cur_xlim[1] - xdata)/(cur_xlim[1] - cur_xlim[0]) + rely = (cur_ylim[1] - ydata)/(cur_ylim[1] - cur_ylim[0]) + + self._mplAxes.set_xlim([xdata - new_width * (1-relx), xdata + new_width * (relx)]) + self._mplAxes.set_ylim([ydata - new_height * (1-rely), ydata + new_height * (rely)]) + + self.repaint() + pass + + def onPressEvent(self, event): + if event.button == 3 : + #self._mplCanvas.emit(QtCore.SIGNAL("button_release_event()")) + canvasSize = event.canvas.geometry() + point = event.canvas.mapToGlobal(QtCore.QPoint(event.x,canvasSize.height()-event.y)) + self._popupMenu.exec_(point) + else : + print "Press event on the other button" + #if event.button == 3 : + # canvasSize = event.canvas.geometry() + # point = event.canvas.mapToGlobal(QtCore.QPoint(event.x,canvasSize.height()-event.y)) + # self._popupMenu.move(point) + # self._popupMenu.show() + + def onMotionEvent(self, event): + print "OnMotionEvent ",event.button + #if event.button == 3 : + # event.button = None + # return True + + def onReleaseEvent(self, event): + print "OnReleaseEvent ",event.button + #if event.button == 3 : + # event.button = None + # return False