diff --git a/.flake8 b/.flake8 index 03532a6d20ef60a78e1923ba931b7ac66aee7029..75306b2f44c1f07c3df7b2eb17e846bf6d4f8772 100644 --- a/.flake8 +++ b/.flake8 @@ -14,7 +14,9 @@ exclude = installers, instrument, MantidPlot, - qt, + qt/paraview_ext, + qt/scientific_interfaces, + qt/widgets, scripts/test, Testing/PerformanceTests, Testing/SystemTests/lib, diff --git a/.gitignore b/.gitignore index 21d37fac751f97074d2ecaac926b5610ab2d2c7c..3871a5934c967678ff5421fcc32cc6e190a37dfc 100644 --- a/.gitignore +++ b/.gitignore @@ -97,7 +97,8 @@ build/ *.tmp *.vspscc .builds - +qt/python/mantidqt/utils/qt/plugins.py +*.egg-info/ # Visual C++ cache files ipch/ @@ -181,4 +182,3 @@ Third_Party/ win64/ dbg/ rel/ - diff --git a/CMakeLists.txt b/CMakeLists.txt index 5f6ee8b137822b08888eb7b615d4b123c11cf328..b010c53ffa42a9c0634dba0916607d3b954bd1d3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -119,8 +119,11 @@ if (ENABLE_MANTIDPLOT) separate_arguments ( PYQT4_SIP_FLAGS ) endif() -if (ENABLE_WORKBENCH) +if ( ENABLE_WORKBENCH ) find_package ( Qt5 COMPONENTS Core Gui Widgets REQUIRED ) + if ( Qt5_FOUND ) + message ( STATUS "Found Qt ${Qt5_VERSION}: ${Qt5_DIR}" ) + endif() find_package ( PyQt5 REQUIRED ) find_package ( SIP REQUIRED ) separate_arguments ( PYQT5_SIP_FLAGS ) diff --git a/buildconfig/CMake/FindPyUnitTest.cmake b/buildconfig/CMake/FindPyUnitTest.cmake index 9bfc279dca311adcce015b4edb2496a8d1a7179a..93d79e29df9430b7579e4aab2ef0294674aa4f3f 100644 --- a/buildconfig/CMake/FindPyUnitTest.cmake +++ b/buildconfig/CMake/FindPyUnitTest.cmake @@ -1,6 +1,7 @@ # # PYUNITTEST_ADD_TEST (public macro to add unit tests) # Adds a set of python tests based upon the unittest module +# # Parameters: # _test_src_dir_base :: A base directory when added to the relative test paths gives # an absolute path to that test. This directory is added to the diff --git a/buildconfig/CMake/QtTargetFunctions.cmake b/buildconfig/CMake/QtTargetFunctions.cmake index ac724e8de03e8ad7cb3c893770fac4a158a23fb0..504a0fd8e81d007b2dfb99a64ebb7db3f99b0766 100644 --- a/buildconfig/CMake/QtTargetFunctions.cmake +++ b/buildconfig/CMake/QtTargetFunctions.cmake @@ -56,7 +56,7 @@ endfunction() # keyword: UI Qt designer ui files that are to be parsed by the UI compiler # keyword: NOMOC Additional headers that are not to be passed to moc # keyword: RES Any resource .qrc files -# keyword: DEFS Compiler definitions to set to the target +# keyword: DEFS Compiler definitions to set for all targets. Also QTX_DEFS can be used to set per-version targets # keyword: OUTPUT_DIR_BASE Base directory the build output. The final product goes into a subdirectory based on the Qt version # keyword: OUTPUT_SUBDIR Additional directory to added to the final output path # keyword: INCLUDE_DIRS A list of include directories to add to the target @@ -79,8 +79,8 @@ function (mtd_add_qt_target) set (oneValueArgs TARGET_NAME QT_VERSION OUTPUT_DIR_BASE OUTPUT_SUBDIR INSTALL_DIR INSTALL_DIR_BASE PRECOMPILED) - set (multiValueArgs SRC QT4_SRC QT5_SRC UI MOC - NOMOC RES DEFS INCLUDE_DIRS SYSTEM_INCLUDE_DIRS LINK_LIBS + set (multiValueArgs SRC UI MOC + NOMOC RES DEFS QT4_DEFS QT5_DEFS INCLUDE_DIRS SYSTEM_INCLUDE_DIRS LINK_LIBS QT4_LINK_LIBS QT5_LINK_LIBS MTD_QT_LINK_LIBS OSX_INSTALL_RPATH) cmake_parse_arguments (PARSED "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) @@ -98,15 +98,16 @@ function (mtd_add_qt_target) _append_qt_suffix (AS_DIR VERSION ${PARSED_QT_VERSION} OUTPUT_VARIABLE _ui_dir ${CMAKE_CURRENT_BINARY_DIR}) set (CMAKE_CURRENT_BINARY_DIR ${_ui_dir}) + set ( _all_defines ${PARSED_DEFS};${PARSED_QT${PARSED_QT_VERSION}_DEFS} ) if (PARSED_QT_VERSION EQUAL 4) qt4_wrap_ui (UI_HEADERS ${PARSED_UI}) - _internal_qt_wrap_cpp ( 4 MOC_GENERATED ${PARSED_MOC}) + _internal_qt_wrap_cpp ( 4 MOC_GENERATED DEFS ${_all_defines} INFILES ${PARSED_MOC}) set (ALL_SRC ${PARSED_SRC} ${PARSED_QT4_SRC} ${MOC_GENERATED}) qt4_add_resources (RES_FILES ${PARSED_RES}) set (_qt_link_libraries Qt4::QtGui ${PARSED_QT4_LINK_LIBS}) elseif (PARSED_QT_VERSION EQUAL 5) qt5_wrap_ui (UI_HEADERS ${PARSED_UI}) - _internal_qt_wrap_cpp ( 5 MOC_GENERATED ${PARSED_MOC} ) + _internal_qt_wrap_cpp ( 5 MOC_GENERATED DEFS ${_all_defines} INFILES ${PARSED_MOC} ) set (ALL_SRC ${PARSED_SRC} ${PARSED_QT5_SRC} ${MOC_GENERATED}) qt5_add_resources (RES_FILES ${PARSED_RES}) set (_qt_link_libraries Qt5::Widgets ${PARSED_QT5_LINK_LIBS}) @@ -162,8 +163,8 @@ function (mtd_add_qt_target) endif() target_link_libraries (${_target} PRIVATE ${_qt_link_libraries} ${PARSED_LINK_LIBS} ${_mtd_qt_libs}) - if(PARSED_DEFS) - set_target_properties ( ${_target} PROPERTIES COMPILE_DEFINITIONS "${PARSED_DEFS}" ) + if(_all_defines) + set_target_properties ( ${_target} PROPERTIES COMPILE_DEFINITIONS "${_all_defines}" ) endif() if (OSX_VERSION VERSION_GREATER 10.8) @@ -336,19 +337,29 @@ endfunction () # allowed limit on Windows (260 chars). It is assumed that the input paths # can be made absolute by prefixing them with ${CMAKE_CURRENT_LIST_DIR} # It assumes that the unnamed arguments are the input files to run through moc -function(_internal_qt_wrap_cpp qtversion moc_generated) - foreach (_infile ${ARGN}) +function (_internal_qt_wrap_cpp qtversion moc_generated ) + set (options) + set (oneValueArgs) + set (multiValueArgs DEFS INFILES) + cmake_parse_arguments (PARSED "${options}" "${oneValueArgs}" + "${multiValueArgs}" ${ARGN}) + foreach (_def ${PARSED_DEFS}) + list ( APPEND _moc_defs "-D${_def}") + endforeach () + foreach (_infile ${PARSED_INFILES}) if(qtversion EQUAL 4) - qt4_wrap_cpp (moc_generated ${_infile} OPTIONS -i -f${CMAKE_CURRENT_LIST_DIR}/${_infile} ) + qt4_wrap_cpp (moc_generated ${_infile} + OPTIONS -i -f${CMAKE_CURRENT_LIST_DIR}/${_infile} ${_moc_defs} ) elseif (qtversion EQUAL 5) - qt5_wrap_cpp (moc_generated ${_infile} OPTIONS -i -f${CMAKE_CURRENT_LIST_DIR}/${_infile} ) + qt5_wrap_cpp (moc_generated ${_infile} + OPTIONS -i -f${CMAKE_CURRENT_LIST_DIR}/${_infile} ${_moc_defs} ) else() message (FATAL_ERROR "Unknown Qt version='${qtversion}'.") endif() endforeach() # Pass the output variable out set (${moc_generated} ${${moc_generated}} PARENT_SCOPE) -endfunction() +endfunction () # Disables suggest override for versions of Qt < 5.6.2 as # Q_OBJECT produces them and the cannot be avoided. diff --git a/buildconfig/windows/buildenv.bat.in b/buildconfig/windows/buildenv.bat.in index a32094c47e03935399c3fadb7ef6f09a64da9301..1792ac68a50f37abeefff0df5f9e11903ebc6920 100644 --- a/buildconfig/windows/buildenv.bat.in +++ b/buildconfig/windows/buildenv.bat.in @@ -13,6 +13,9 @@ set THIRD_PARTY_DIR=%CM_THIRD_PARTY_DIR:/=\% :: Qt4 - exes are in bin & dlls are in lib set QT4_BIN=%THIRD_PARTY_DIR%\lib\qt4\bin;%THIRD_PARTY_DIR%\lib\qt4\lib +:: Qt5 - exes are in bin & dlls are in lib +set QT5_BIN=%THIRD_PARTY_DIR%\lib\qt5\bin;%THIRD_PARTY_DIR%\lib\qt5\lib + :: Python - set PYTHONHOME=%THIRD_PARTY_DIR%\lib\python@PYTHON_MAJOR_VERSION@.@PYTHON_MINOR_VERSION@ @@ -20,4 +23,4 @@ set PYTHONHOME=%THIRD_PARTY_DIR%\lib\python@PYTHON_MAJOR_VERSION@.@PYTHON_MINOR_ set MISC_BIN=%THIRD_PARTY_DIR%\bin;%THIRD_PARTY_DIR%\bin\mingw :: Update PATH -set PATH=%MISC_BIN%;%PYTHONHOME%;%QT4_BIN%;%PATH% +set PATH=%MISC_BIN%;%PYTHONHOME%;%QT5_BIN%;%QT4_BIN%;%PATH% diff --git a/qt/CMakeLists.txt b/qt/CMakeLists.txt index f32ea2d43b601759de7fee94d1e408599e404bc2..82b42ecd9bc51ed36905e6e998ee0f6a6d408e27 100644 --- a/qt/CMakeLists.txt +++ b/qt/CMakeLists.txt @@ -1,14 +1,47 @@ -########################################################################### -# Now add the packages one-by-one, building up the dependencies as we go -########################################################################### find_package ( QScintillaQt4 REQUIRED ) # Utilities for defining targets include ( QtTargetFunctions ) +# Function to create links to python packages in the source tree +# If the EXECUTABLE option is provided then it additional build rules are +# defined to ensure startup scripts are regenerated appropriately +function ( add_python_package pkg_name ) + # Create a setup.py file + set ( _setup_py ${CMAKE_CURRENT_SOURCE_DIR}/setup.py ) + set ( _egg_link_dir ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/${CMAKE_CFG_INTDIR} ) + set ( _egg_link ${_egg_link_dir}/${pkg_name}.egg-link ) + if ( ARGV0 EQUAL "EXECUTABLE" ) + if ( WIN32 ) + set ( _startup_script ${_egg_link_dir}/${pkg_name}-script.pyw ) + set ( _startup_exe ${_egg_link_dir}/${pkg_name}.exe ) + else () + set ( _startup_script ) + set ( _startup_exe ${_egg_link_dir}/${pkg_name} ) + endif () + endif () + set ( _outputs "${_egg_link} ${_startup_script} ${_startup_exe}" ) + add_custom_command ( OUTPUT ${_outputs} + COMMAND ${CMAKE_COMMAND} -E env PYTHONPATH=${_egg_link_dir} + ${PYTHON_EXECUTABLE} ${_setup_py} develop + --install-dir ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/${CMAKE_CFG_INTDIR} + --script-dir ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/${CMAKE_CFG_INTDIR} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + DEPENDS ${_setup_py} + ) + add_custom_target ( ${pkg_name} ALL + DEPENDS ${_outputs} + ) +endfunction () + +########################################################################### +# Qt-based targets +########################################################################### add_subdirectory ( widgets ) add_subdirectory ( python ) add_subdirectory ( scientific_interfaces ) if ( MAKE_VATES ) add_subdirectory ( paraview_ext ) endif ( MAKE_VATES ) + +add_subdirectory ( applications ) diff --git a/qt/applications/CMakeLists.txt b/qt/applications/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..cb53ce0a4a3fecbf5c5bfa2bed416d0b4822cb93 --- /dev/null +++ b/qt/applications/CMakeLists.txt @@ -0,0 +1,3 @@ +if ( ENABLE_WORKBENCH ) + add_subdirectory ( workbench ) +endif () diff --git a/qt/applications/workbench/CMakeLists.txt b/qt/applications/workbench/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..c6414f123903ba2e20521dac1ed7af6887e9ddc0 --- /dev/null +++ b/qt/applications/workbench/CMakeLists.txt @@ -0,0 +1,15 @@ +# Creates egg link to binary output directory +add_python_package ( workbench EXECUTABLE ) +add_dependencies ( workbench mantidqt ) + +# ctest targets +set ( TEST_FILES + workbench/config/test/user_test.py + workbench/test/import_test.py +) + +pyunittest_add_test ( ${CMAKE_CURRENT_SOURCE_DIR} + workbench ${TEST_FILES} +) + +# Not installed yet... diff --git a/qt/applications/workbench/scripts/workbench.py b/qt/applications/workbench/scripts/workbench.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/qt/applications/workbench/setup.py b/qt/applications/workbench/setup.py new file mode 100644 index 0000000000000000000000000000000000000000..6e3db4ad665309e688d04d56de958f5b79b2dfc1 --- /dev/null +++ b/qt/applications/workbench/setup.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# This file is part of the mantid workbench. +# +# Copyright (C) 2017 mantidproject +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from setuptools import setup + +# The most basic setup possible to be able to use setup.py develop +setup( + name="workbench", + install_requires=['mantidqt'], + entry_points={ + 'gui_scripts': [ + 'workbench = workbench.app.mainwindow:main' + ] + }, +) diff --git a/qt/applications/workbench/workbench/__init__.py b/qt/applications/workbench/workbench/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..27614544cb414ae77fa4a9d3637b3d201e6197ac --- /dev/null +++ b/qt/applications/workbench/workbench/__init__.py @@ -0,0 +1,26 @@ +# This file is part of the mantid workbench. +# +# Copyright (C) 2017 mantidproject +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +""" +workbench +--------- + +This package contains the mantidproject's Qt-based workbench. It is the primary +GUI for interacting with the mantid reduction and analysis package. +""" + +# This file should be free from Qt imports to keep it lightweight to import +# and check metadata. diff --git a/qt/applications/workbench/workbench/app/__init__.py b/qt/applications/workbench/workbench/app/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..fde0b4389afda4c454662daf6773bc9e22dfae86 --- /dev/null +++ b/qt/applications/workbench/workbench/app/__init__.py @@ -0,0 +1,22 @@ +# This file is part of the mantid workbench. +# +# Copyright (C) 2017 mantidproject +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +""" +workbench.app +------------- + +Holds code related to application startup. +""" diff --git a/qt/applications/workbench/workbench/app/mainwindow.py b/qt/applications/workbench/workbench/app/mainwindow.py new file mode 100644 index 0000000000000000000000000000000000000000..7fd5915e1a5858025e595c9074960329529a436d --- /dev/null +++ b/qt/applications/workbench/workbench/app/mainwindow.py @@ -0,0 +1,140 @@ +# This file is part of the mantid workbench. +# +# Copyright (C) 2017 mantidproject +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +""" +Defines the QMainWindow of the application and the main() entry point. +""" +from __future__ import (absolute_import, division, + print_function, unicode_literals) + +import sys + +# ----------------------------------------------------------------------------- +# Constants +# ----------------------------------------------------------------------------- +ORIGINAL_SYS_EXIT = sys.exit +STDERR = sys.stderr + +# ----------------------------------------------------------------------------- +# Requirements +# ----------------------------------------------------------------------------- +from workbench import requirements # noqa +requirements.check_qt() + +# ----------------------------------------------------------------------------- +# Qt +# ----------------------------------------------------------------------------- +from qtpy.QtCore import QCoreApplication, Qt # noqa +from qtpy.QtWidgets import QApplication, QMainWindow # noqa +from mantidqt.utils.qt import load_ui, plugins # noqa + +# Pre-application startup +plugins.setup_library_paths() +if hasattr(Qt, 'AA_EnableHighDpiScaling'): + QCoreApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) + + +# ----------------------------------------------------------------------------- +# Create the application instance early, set the application name for window +# titles and hold on to a reference to it. Required to be performed early so +# that the splash screen can be displayed +# ----------------------------------------------------------------------------- + +def qapplication(): + """Either return a reference to an existing application instance + or create a new one + :return: A reference to the QApplication object + """ + app = QApplication.instance() + if app is None: + app = QApplication(['Mantid Workbench']) + return app + +# Create the application object early +MAIN_APP = qapplication() + + +# ----------------------------------------------------------------------------- +# MainWindow +# ----------------------------------------------------------------------------- +class MainWindow(QMainWindow): + + def __init__(self): + QMainWindow.__init__(self) + self._ui = load_ui(__file__, 'mainwindow.ui', baseinstance=self) + MAIN_APP.setAttribute(Qt.AA_UseHighDpiPixmaps) + + +def initialize(): + """Perform an initialization of the application instance. Most notably + this patches sys.exit so that it does nothing. + + :return: A reference to the existing application instance + """ + app = qapplication() + + # Monkey patching sys.exit so users can't kill + # the application this way + def fake_sys_exit(arg=[]): + pass + sys.exit = fake_sys_exit + + return app + + +def start_workbench(app): + """Given an application instance create the MainWindow, + show it and start the main event loop + """ + main_window = MainWindow() + # do some pre-show setup... + main_window.show() + # do some pre-show setup... + + # lift-off! + app.exec_() + + return main_window + + +def main(): + """Main entry point for the application""" + + # parse command arguments early + + # general initialization + app = initialize() + + main_window = None + try: + main_window = start_workbench(app) + except BaseException: + # We count this as a crash + import traceback + # This is type of thing we want to capture and have reports + # about. Prints to stderr as we can't really count on anything + # else + traceback.print_exc(file=STDERR) + + if main_window is None: + # An exception occurred don't exit here + return + + ORIGINAL_SYS_EXIT() + + +if __name__ == '__main__': + main() diff --git a/qt/applications/workbench/workbench/app/mainwindow.ui b/qt/applications/workbench/workbench/app/mainwindow.ui new file mode 100644 index 0000000000000000000000000000000000000000..a6f65a22a74fc103e55779b6ab61ba47b2f41970 --- /dev/null +++ b/qt/applications/workbench/workbench/app/mainwindow.ui @@ -0,0 +1,63 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>MainWindow</class> + <widget class="QMainWindow" name="MainWindow"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>800</width> + <height>600</height> + </rect> + </property> + <property name="windowTitle"> + <string>Mantid Workbench</string> + </property> + <widget class="QWidget" name="centralwidget"/> + <widget class="QMenuBar" name="menubar"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>800</width> + <height>26</height> + </rect> + </property> + <widget class="QMenu" name="menuFile"> + <property name="title"> + <string>File</string> + </property> + <addaction name="actionQuit"/> + </widget> + <addaction name="menuFile"/> + </widget> + <widget class="QStatusBar" name="statusbar"/> + <action name="actionQuit"> + <property name="text"> + <string>Quit</string> + </property> + <property name="shortcut"> + <string>Ctrl+Q</string> + </property> + </action> + </widget> + <resources/> + <connections> + <connection> + <sender>actionQuit</sender> + <signal>triggered()</signal> + <receiver>MainWindow</receiver> + <slot>close()</slot> + <hints> + <hint type="sourcelabel"> + <x>-1</x> + <y>-1</y> + </hint> + <hint type="destinationlabel"> + <x>399</x> + <y>299</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/qt/applications/workbench/workbench/config/__init__.py b/qt/applications/workbench/workbench/config/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..38b6452e68f2d819b646bccff5c213e92581ea5a --- /dev/null +++ b/qt/applications/workbench/workbench/config/__init__.py @@ -0,0 +1,16 @@ +# This file is part of the mantid workbench. +# +# Copyright (C) 2017 mantidproject +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. diff --git a/qt/applications/workbench/workbench/config/main.py b/qt/applications/workbench/workbench/config/main.py new file mode 100644 index 0000000000000000000000000000000000000000..dbaa95639b656c75d4c1ce99b65726cdf1a419a7 --- /dev/null +++ b/qt/applications/workbench/workbench/config/main.py @@ -0,0 +1,47 @@ +# This file is part of the mantid workbench. +# +# Copyright (C) 2017 mantidproject +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +""" Main configuration module. + +A singleton instance called CONF is defined. Modules wishing to access the settings +should import the CONF object as + + from workbench.config.main import CONF + +and use it to access the settings +""" +from __future__ import (absolute_import, unicode_literals) + +from workbench.config.user import UserConfig + +# ----------------------------------------------------------------------------- +# Constants +# ----------------------------------------------------------------------------- +ORGANIZATION = 'mantidproject' +APPNAME = 'workbench' + +# Iterable containing defaults for each configurable section of the code +# General application settings are in the main section +DEFAULTS = { + 'main': { + 'high_dpi_scaling': True, + } +} + +# ----------------------------------------------------------------------------- +# 'Singleton' instance +# ----------------------------------------------------------------------------- +CONF = UserConfig(ORGANIZATION, APPNAME, defaults=DEFAULTS) diff --git a/qt/applications/workbench/workbench/config/test/user_test.py b/qt/applications/workbench/workbench/config/test/user_test.py new file mode 100644 index 0000000000000000000000000000000000000000..ed66573c01c7063c802e3e07425b98b75e1bd404 --- /dev/null +++ b/qt/applications/workbench/workbench/config/test/user_test.py @@ -0,0 +1,71 @@ +# This file is part of the mantid workbench. +# +# Copyright (C) 2017 mantidproject +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +from __future__ import absolute_import + +import os +from unittest import TestCase + +from workbench.config.user import UserConfig + + +class ConfigUserTest(TestCase): + + @classmethod + def setUpClass(cls): + if not hasattr(cls, 'cfg'): + defaults = { + 'main': {'a-default-key': 100} + } + cls.cfg = UserConfig(cls.__name__, cls.__name__, defaults) + + @classmethod + def tearDownClass(cls): + try: + os.remove(cls.cfg.filename) + del cls.cfg + except OSError: + pass + + # ---------------------------------------------- + # Success tests + # ---------------------------------------------- + + def test_stored_value_not_in_defaults_is_retrieved(self): + self.cfg.set('main', 'key1', 1) + self.assertEqual(1, self.cfg.get('main', 'key1')) + + def test_value_not_in_settings_retrieves_default_if_it_exists(self): + self.assertEqual(100, self.cfg.get('main', 'a-default-key')) + + # ---------------------------------------------- + # Failure tests + # ---------------------------------------------- + + def test_get_raises_error_with_invalid_section_type(self): + self.assertRaises(RuntimeError, self.cfg.get, 1, 'key1') + + def test_get_raises_error_with_invalid_option_type(self): + self.assertRaises(RuntimeError, self.cfg.get, 'section', 1) + + def test_get_raises_keyerror_with_no_saved_setting_or_default(self): + self.assertRaises(KeyError, self.cfg.get, 'main', 'missing-key') + + def test_set_raises_error_with_invalid_section_type(self): + self.assertRaises(RuntimeError, self.cfg.set, 1, 'key1', 1) + + def test_set_raises_error_with_invalid_option_type(self): + self.assertRaises(RuntimeError, self.cfg.set, 'section', 1, 1) diff --git a/qt/applications/workbench/workbench/config/user.py b/qt/applications/workbench/workbench/config/user.py new file mode 100644 index 0000000000000000000000000000000000000000..cc0a914dc21dd63930e8cbba409e955a17b05ba5 --- /dev/null +++ b/qt/applications/workbench/workbench/config/user.py @@ -0,0 +1,119 @@ +# This file is part of the mantid workbench. +# +# Copyright (C) 2017 mantidproject +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +from mantidqt.py3compat import is_text_string + +from qtpy.QtCore import QSettings + + +class UserConfig(object): + """Holds user configuration option. Options are assigned a section + and a key must only be unique within a section. + + Uses QSettings for the heavy lifting. All platforms use the Ini format + at UserScope + """ + + # The raw QSettings instance + settings = None + defaults = None + + def __init__(self, organization, application, defaults=None): + """ + + :param organization: A string name for the organization + :param application: A string name for the application name + :param defaults: Default configuration values for this instance in the + form of nested dict instances + """ + # Loads the saved settings if found + self.settings = QSettings(QSettings.IniFormat, QSettings.UserScope, + organization, application) + self.defaults = defaults + + def all_keys(self): + return self.settings.allKeys() + + @property + def filename(self): + return self.settings.fileName() + + def get(self, section, option): + """ + Return a value for an option in a given section. If not + specified in the saved settings then the defaults are + consulted. If no option is found then a KeyError is raised + :param section: A string section name + :param option: A string option name + :return: The value of the option + """ + value = self.settings.value(self._settings_path(section, option)) + if not value: + value = self._get_default_or_raise(section, option) + + return value + + def set(self, section, option, value): + """ + Set a value for an option in a given section. + :param section: A string section name + :param option: A string option name + :param value: The value of the setting + """ + self.settings.setValue(self._settings_path(section, option), value) + + # ------------------------------------------------------------------------- + # "Private" methods + # ------------------------------------------------------------------------- + + def _check_section_option_is_valid(self, section, option): + """ + Sanity check the section and option are strings + """ + if not is_text_string(section): + raise RuntimeError("section is not a text string") + if not is_text_string(option): + raise RuntimeError("option is not a text string") + + def _get_default_or_raise(self, section, option): + """ + Returns the value listed in the defaults if it exists + :param section: A string denoting the section (not checked) + :param option: A string denoting the option name + :return: The value of the default + :raises KeyError: if the item does not exist + """ + value = None + if self.defaults and section in self.defaults: + value = self.defaults[section].get(option, None) + if not value: + raise KeyError("Unknown config item requested: " + + self._settings_path(section, option)) + return value + + def _settings_path(self, section, option): + """ + Private method to construct a path to the given option with the + section + :param section: The name of the section + :param option: The name of the option + :return: A path to the location within the QSettings instance + """ + self._check_section_option_is_valid(section, option) + return section + "/" + option diff --git a/qt/applications/workbench/workbench/requirements.py b/qt/applications/workbench/workbench/requirements.py new file mode 100644 index 0000000000000000000000000000000000000000..f591b866497f93c1010fed6e75ab0d27a4022fb5 --- /dev/null +++ b/qt/applications/workbench/workbench/requirements.py @@ -0,0 +1,39 @@ +"""Defines methods to check requirements for the application +""" +from __future__ import absolute_import + +from distutils.version import LooseVersion + + +def show_warning(message): + """Show warning using Tkinter if available""" + try: + # If Tkinter is installed (highly probable), showing an error pop-up + import Tkinter + import tkMessageBox + root = Tkinter.Tk() + root.withdraw() + tkMessageBox.showerror("Mantid Workbench", message) + except ImportError: + pass + raise RuntimeError(message) + + +def check_qt(): + """Check the qt requirements are as expected + """ + package_name, required_ver = ("PyQt5", "5.2") + try: + import qtpy + actual_ver = qtpy.PYQT_VERSION + if LooseVersion(actual_ver) < LooseVersion(required_ver): + show_warning( + "Please check installation requirements:\n" + "{} {}+ is required (found v{}).".format( + package_name, + required_ver, + actual_ver) + ) + except ImportError as exc: + show_warning("Failed to import qtpy: '{}'\n".format(str(exc)) + + "Please check the installation.") diff --git a/qt/applications/workbench/workbench/test/import_test.py b/qt/applications/workbench/workbench/test/import_test.py new file mode 100644 index 0000000000000000000000000000000000000000..03fa79e0b7f4ce9ea843ed5e91877af912e0bf69 --- /dev/null +++ b/qt/applications/workbench/workbench/test/import_test.py @@ -0,0 +1,29 @@ +# This file is part of the mantid workbench. +# +# Copyright (C) 2017 mantidproject +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import unittest + + +class ImportTest(unittest.TestCase): + + def test_import_workbench(self): + import workbench # noqa + +if __name__ == "__main__": + unittest.main() diff --git a/qt/python/CMakeLists.txt b/qt/python/CMakeLists.txt index d8ae39214d00fa852f081fc9e32e00a3e8bd99cf..3a2c792eed2e23e24307cd583f374a92ec457620 100644 --- a/qt/python/CMakeLists.txt +++ b/qt/python/CMakeLists.txt @@ -5,14 +5,34 @@ # Legacy wrappers for MantidPlot add_subdirectory ( mantidqtpython ) -# mantidqt is run from the source directory so just add tests -set ( PYTHON_TEST_FILES - mantidqt/test/import_test.py -) +########################################################################### +# mantidqt +########################################################################### -# Tests -pyunittest_add_test ( ${CMAKE_CURRENT_SOURCE_DIR} - mantidqt ${PYTHON_TEST_FILES} -) +if ( ENABLE_WORKBENCH ) -# No package installation yet... + # Configure utils.qt.plugins file for build. It is placed in the source + # directory and added to the .gitignore for simplicity. + if ( WIN32 ) + set ( QT_PLUGINS_PATH "${THIRD_PARTY_DIR}/lib/qt%V/plugins" ) + endif () + configure_file ( mantidqt/utils/qt/plugins.py.in + ${CMAKE_CURRENT_SOURCE_DIR}/mantidqt/utils/qt/plugins.py + ) + + # Create egg link to binary output directory for mantidqt + add_python_package ( mantidqt ) + + # ctest targets + set ( PYTHON_TEST_FILES + mantidqt/test/import_test.py + ) + + # Tests + pyunittest_add_test ( ${CMAKE_CURRENT_SOURCE_DIR} + mantidqt ${PYTHON_TEST_FILES} + ) + + # No package installation yet... + # Configure utils.qt.plugins file for install +endif () diff --git a/qt/python/mantidqt/py3compat.py b/qt/python/mantidqt/py3compat.py new file mode 100644 index 0000000000000000000000000000000000000000..43d094229f8d18689d0aa33abab74da94147c26a --- /dev/null +++ b/qt/python/mantidqt/py3compat.py @@ -0,0 +1,67 @@ +# This file is part of the mantid workbench. +# +# Copyright (C) 2017 mantidproject +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +""" +mantidqt.py3compat +------------------ + +Transitional module providing compatibility functions intended to help +migrating from Python 2 to Python 3. Mostly just wraps six but allowing +for additional functionality of our own. + +This module should be fully compatible with: + * Python >=v2.7 + * Python 3 +""" + +from __future__ import absolute_import, print_function + +import six +from six import * # noqa + +# ----------------------------------------------------------------------------- +# Globals and constants +# ----------------------------------------------------------------------------- +__all__ = dir(six) + + +# ----------------------------------------------------------------------------- +# Strings +# ----------------------------------------------------------------------------- +def is_text_string(obj): + """Return True if `obj` is a text string, False if it is anything else, + like binary data (Python 3) or QString (Python 2, PyQt API #1)""" + return isinstance(obj, string_types) + + +def to_text_string(obj, encoding=None): + """Convert `obj` to (unicode) text string""" + + if PY2: + # Python 2 + if encoding is None: + return unicode(obj) + else: + return unicode(obj, encoding) + else: + # Python 3 + if encoding is None: + return str(obj) + elif isinstance(obj, str): + # In case this function is not used properly, this could happen + return obj + else: + return str(obj, encoding) diff --git a/qt/python/mantidqt/test/import_test.py b/qt/python/mantidqt/test/import_test.py index 672afdaaf1ff83ca644c112f0badebeb1557854e..2ac0a192e5a0f7d57839f3c02e607d937f373542 100644 --- a/qt/python/mantidqt/test/import_test.py +++ b/qt/python/mantidqt/test/import_test.py @@ -3,6 +3,7 @@ from __future__ import (absolute_import, division, print_function, import unittest + class ImportTest(unittest.TestCase): def test_import(self): diff --git a/qt/python/mantidqt/utils/__init__.py b/qt/python/mantidqt/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/qt/python/mantidqt/utils/qt/__init__.py b/qt/python/mantidqt/utils/qt/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..821a7937edbd768ed79c34f3af240da8b64857db --- /dev/null +++ b/qt/python/mantidqt/utils/qt/__init__.py @@ -0,0 +1,41 @@ +# This file is part of the mantid workbench. +# +# Copyright (C) 2017 mantidproject +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +"""A selection of utility functions related to Qt functionality +""" +from __future__ import absolute_import + +# stdlib modules +import os + +# 3rd-party modules +from qtpy.uic import loadUi + + +def load_ui(caller_filename, ui_relfilename, baseinstance=None): + """Load a ui file assuming it is relative to a given callers filepath + + :param caller_filename: An absolute path to a file whose basename when + joined with ui_relfilename gives the full path to the .ui file. Generally + this called with __file__ + :param ui_relfilename: A relative filepath that when joined with the + basename of caller_filename gives the absolute path to the .ui file. + :param baseinstance: An instance of a widget to pass to uic.loadUi + that becomes the base class rather than a new widget being created. + :return: A new instance of the form class + """ + filepath = os.path.join(os.path.dirname(caller_filename), ui_relfilename) + return loadUi(filepath, baseinstance=baseinstance) diff --git a/qt/python/mantidqt/utils/qt/plugins.py.in b/qt/python/mantidqt/utils/qt/plugins.py.in new file mode 100644 index 0000000000000000000000000000000000000000..666ca568da5ee293fc168f04e0d1e5e24d41c447 --- /dev/null +++ b/qt/python/mantidqt/utils/qt/plugins.py.in @@ -0,0 +1,57 @@ +# This file is part of the mantid workbench. +# +# Copyright (C) 2017 mantidproject +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +"""Defines functions to setup paths to Qt plugins at runtime. This is +a generated file. Any changes made here will be lost. See +qt/python/mantidqt/utils/qt/plugins.py.in +""" +from __future__ import absolute_import + +# stdlib imports +import os + +# 3rd-party imports +from qtpy import QT_VERSION +from qtpy.QtCore import QCoreApplication + + +# Qt needs to be able to find its plugins and by default it looks in the +# directories where they were built. On Linux the libraries are in fixed +# locations. +# +# On Windows the package can be installed anywhere so the locations must +# be determined at runtime. The two options are: +# 1. using a qt.conf file: requires it to be next to the application +# executable but this would be python.exe and then would not allow +# switching between Qt4/Qt5 +# 2. add extra paths to the LibraryPath list here +# As we can dynamically determine the Qt version here we choose option 2. + +def setup_library_paths_win(): + """Adds the build-time configured directory to the Qt library path. + The buildsystem generates the path at build time. + A %V marker can be used that will be replaced by the major version of Qt + at runtime. + """ + QCoreApplication.addLibraryPath( + '@QT_PLUGINS_PATH@'.replace('%V', QT_VERSION[0]) + ) + +# Set the implementation appropriate for the platform +if os.name == 'nt': + setup_library_paths = setup_library_paths_win +else: + setup_library_paths = lambda: None diff --git a/qt/python/setup.py b/qt/python/setup.py new file mode 100644 index 0000000000000000000000000000000000000000..bdfe7f9bd16f6f1e16d2ca037f09879741805526 --- /dev/null +++ b/qt/python/setup.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# This file is part of the mantid workbench. +# +# Copyright (C) 2017 mantidproject +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from setuptools import setup + +# The most basic setup possible to be able to use setup.py develop +setup( + name="mantidqt", +) diff --git a/qt/widgets/common/CMakeLists.txt b/qt/widgets/common/CMakeLists.txt index 0f6696bbb21bb383a59e7ccd050b2a8083a427d4..32ec0b86f5128e87c6652f7feef8fc1ac53691d8 100644 --- a/qt/widgets/common/CMakeLists.txt +++ b/qt/widgets/common/CMakeLists.txt @@ -421,9 +421,25 @@ endif() ########################################################################### # Target ########################################################################### -if (ENABLE_WORKBENCH) +if ( ENABLE_WORKBENCH ) + # Additional components for this module find_package ( Qt5 COMPONENTS Concurrent Help Network PrintSupport - WebKitWidgets REQUIRED ) + REQUIRED ) + # Prefer WebEngineWidgets over WebkitWidgets + unset ( Qt5_FOUND ) + find_package ( Qt5 COMPONENTS WebEngineWidgets QUIET ) + if ( Qt5_FOUND ) + set ( _webwidgets_tgt Qt5::WebEngineWidgets ) + else () + find_package ( Qt5 COMPONENTS WebKitWidgets QUIET ) + if ( Qt5_FOUND ) + set ( _webwidgets_tgt Qt5::WebKitWidgets ) + set ( _webengine_def USE_QTWEBKIT ) + else () + message ( FATAL_ERROR "Unable to find suitable module for web widgets. Tried: WebEnginewidgets, WebKitWidgets" ) + endif () + endif () + find_package ( QScintillaQt5 REQUIRED ) endif() @@ -433,7 +449,12 @@ mtd_add_qt_library (TARGET_NAME MantidQtWidgetsCommon NOMOC ${INC_FILES} UI ${UI_FILES} DEFS - IN_MANTIDQT_COMMON QSCINTILLA_DLL + IN_MANTIDQT_COMMON + QSCINTILLA_DLL + QT4_DEFS + USE_QTWEBKIT + QT5_DEFS + ${_webengine_def} INCLUDE_DIRS inc ${PYTHON_INCLUDE_PATH} @@ -450,7 +471,7 @@ mtd_add_qt_library (TARGET_NAME MantidQtWidgetsCommon Qt5::Help Qt5::Network Qt5::PrintSupport - Qt5::WebKitWidgets + ${_webwidgets_tgt} Qt5::Qscintilla OSX_INSTALL_RPATH @loader_path/../MacOS diff --git a/qt/widgets/common/inc/MantidQtWidgets/Common/pqHelpWindow.h b/qt/widgets/common/inc/MantidQtWidgets/Common/pqHelpWindow.h index 94236a6470a372124a90ebdaccf361ca94e1b712..43914261b212705e2591fe7b70867d737f02edec 100644 --- a/qt/widgets/common/inc/MantidQtWidgets/Common/pqHelpWindow.h +++ b/qt/widgets/common/inc/MantidQtWidgets/Common/pqHelpWindow.h @@ -35,11 +35,38 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include <QMainWindow> #include "DllOption.h" -class HelpBrowser; class QHelpEngine; class QToolButton; class QUrl; +#if defined(USE_QTWEBKIT) class QWebView; +using QWebEngineView = QWebView; +#else +#include <QWebEnginePage> +class QWebEngineView; + +/// Mimic the WebKit class to emit linkClicked signal from the page +class DelegatingWebPage : public QWebEnginePage { + Q_OBJECT + +public: + DelegatingWebPage(QObject *parent = nullptr) : QWebEnginePage(parent) {} + + bool acceptNavigationRequest(const QUrl &url, + QWebEnginePage::NavigationType type, + bool) override { + if (type == QWebEnginePage::NavigationTypeLinkClicked) { + + emit linkClicked(url); + } + return true; + } + +signals: + void linkClicked(const QUrl &); +}; + +#endif /// pqHelpWindow provides a assistant-like window for showing help provided by /// a QHelpEngine. @@ -50,7 +77,6 @@ class EXPORT_OPT_MANTIDQT_COMMON pqHelpWindow : public QMainWindow { public: pqHelpWindow(QHelpEngine *engine, QWidget *parent = 0, Qt::WindowFlags flags = 0); - ~pqHelpWindow() override; public slots: /// Requests showing of a particular page. The url must begin with "qthelp:" @@ -72,13 +98,13 @@ signals: protected slots: void search(); - void linkHovered(const QString &link, const QString &title, - const QString &textContent); + void linkHovered(const QString &link, const QString &title = "", + const QString &textContent = ""); void updateNavButtons(); protected: QHelpEngine *m_helpEngine; - QWebView *m_browser; + QWebEngineView *m_browser; QToolButton *m_forward; QToolButton *m_backward; diff --git a/qt/widgets/common/src/QtPropertyBrowser/qttreepropertybrowser.cpp b/qt/widgets/common/src/QtPropertyBrowser/qttreepropertybrowser.cpp index 77691026f4c28b9810aebffd3b7dc49b84450cec..80229ea10fb1b1f3574b2017140535db88980235 100644 --- a/qt/widgets/common/src/QtPropertyBrowser/qttreepropertybrowser.cpp +++ b/qt/widgets/common/src/QtPropertyBrowser/qttreepropertybrowser.cpp @@ -131,7 +131,12 @@ QtPropertyEditorView::QtPropertyEditorView(QWidget *parent, bool darkTopLevel) void QtPropertyEditorView::drawRow(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { +#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0) QStyleOptionViewItemV3 opt = option; +#else + QStyleOptionViewItem opt = option; +#endif + bool hasValue = true; if (m_editorPrivate) { QtProperty *property = m_editorPrivate->indexToProperty(index); @@ -308,7 +313,12 @@ void QtPropertyEditorDelegate::paint(QPainter *painter, if (property) hasValue = property->hasValue(); } + +#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0) QStyleOptionViewItemV3 opt = option; +#else + QStyleOptionViewItem opt = option; +#endif if ((m_editorPrivate && index.column() == 0) || !hasValue) { QtProperty *property = m_editorPrivate->indexToProperty(index); if (property && property->isModified()) { diff --git a/qt/widgets/common/src/ScriptRepositoryView.cpp b/qt/widgets/common/src/ScriptRepositoryView.cpp index 248dc374fd81498b38c612f036e2bd7ecda27a72..1fdd3e636266c931dd9c704d3744bff625affcb2 100644 --- a/qt/widgets/common/src/ScriptRepositoryView.cpp +++ b/qt/widgets/common/src/ScriptRepositoryView.cpp @@ -452,7 +452,11 @@ void ScriptRepositoryView::CheckBoxDelegate::paint( if (painter->device() == nullptr) return; +#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0) QStyleOptionViewItemV4 modifiedOption(option); +#else + QStyleOptionViewItem modifiedOption(option); +#endif QPoint p = modifiedOption.rect.center(); QSize curr = modifiedOption.rect.size(); diff --git a/qt/widgets/common/src/pqHelpWindow.cxx b/qt/widgets/common/src/pqHelpWindow.cxx index a19abcca8aa9610a01d5bdb0cd048d3c26eb7f2d..09f29aeda238b2778b1a5de781fc7244dccfc3ee 100644 --- a/qt/widgets/common/src/pqHelpWindow.cxx +++ b/qt/widgets/common/src/pqHelpWindow.cxx @@ -51,27 +51,24 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include <QToolBar> #include <QToolButton> #include <QUrl> -#include <QWebHistory> -#include <QWebView> - -using MantidQt::API::MantidDesktopServices; // **************************************************************************** // CLASS pqHelpWindowNetworkReply // **************************************************************************** -/// Internal class used to add support to QWebView to load files from +/// Internal class used to add support to QWeb(Engine)View to load files from /// QHelpEngine. -class pqHelpWindowNetworkReply : public QNetworkReply -{ +class pqHelpWindowNetworkReply : public QNetworkReply { typedef QNetworkReply Superclass; + public: - pqHelpWindowNetworkReply(const QUrl& url, QHelpEngineCore* helpEngine); + pqHelpWindowNetworkReply(const QUrl &url, QHelpEngineCore *helpEngine, + QObject *parent = nullptr); void abort() override {} qint64 bytesAvailable() const override { return (this->RawData.size() - this->Offset) + - this->Superclass::bytesAvailable(); + this->Superclass::bytesAvailable(); } bool isSequential() const override { return true; } @@ -80,14 +77,16 @@ protected: QByteArray RawData; qint64 Offset; + private: Q_DISABLE_COPY(pqHelpWindowNetworkReply) }; //----------------------------------------------------------------------------- -pqHelpWindowNetworkReply::pqHelpWindowNetworkReply( - const QUrl& my_url, QHelpEngineCore* engine) : Superclass(engine), Offset(0) -{ +pqHelpWindowNetworkReply::pqHelpWindowNetworkReply(const QUrl &my_url, + QHelpEngineCore *engine, + QObject *parent) + : Superclass(parent), Offset(0) { Q_ASSERT(engine); this->RawData = engine->fileData(my_url); @@ -95,37 +94,34 @@ pqHelpWindowNetworkReply::pqHelpWindowNetworkReply( QString content_type = "text/plain"; QString extension = QFileInfo(my_url.path()).suffix().toLower(); QMap<QString, QString> extension_type_map; - extension_type_map["jpg"] = "image/jpeg"; - extension_type_map["jpeg"] = "image/jpeg"; - extension_type_map["png"] = "image/png"; - extension_type_map["gif"] = "image/gif"; - extension_type_map["tiff"] = "image/tiff"; - extension_type_map["htm"] = "text/html"; - extension_type_map["html"] = "text/html"; - extension_type_map["css"] = "text/css"; - extension_type_map["xml"] = "text/xml"; - - if (extension_type_map.contains(extension)) - { + extension_type_map["jpg"] = "image/jpeg"; + extension_type_map["jpeg"] = "image/jpeg"; + extension_type_map["png"] = "image/png"; + extension_type_map["gif"] = "image/gif"; + extension_type_map["tiff"] = "image/tiff"; + extension_type_map["htm"] = "text/html"; + extension_type_map["html"] = "text/html"; + extension_type_map["css"] = "text/css"; + extension_type_map["xml"] = "text/xml"; + + if (extension_type_map.contains(extension)) { content_type = extension_type_map[extension]; } this->setHeader(QNetworkRequest::ContentLengthHeader, QVariant(this->RawData.size())); this->setHeader(QNetworkRequest::ContentTypeHeader, content_type); - this->open(QIODevice::ReadOnly|QIODevice::Unbuffered); + this->open(QIODevice::ReadOnly | QIODevice::Unbuffered); this->setUrl(my_url); QTimer::singleShot(0, this, SIGNAL(readyRead())); QTimer::singleShot(0, this, SLOT(finished())); } //----------------------------------------------------------------------------- -qint64 pqHelpWindowNetworkReply::readData(char *data, qint64 maxSize) -{ - if (this->Offset <= this->RawData.size()) - { - qint64 end = qMin(this->Offset + maxSize, - static_cast<qint64>(this->RawData.size())); +qint64 pqHelpWindowNetworkReply::readData(char *data, qint64 maxSize) { + if (this->Offset <= this->RawData.size()) { + qint64 end = + qMin(this->Offset + maxSize, static_cast<qint64>(this->RawData.size())); qint64 delta = end - this->Offset; memcpy(data, this->RawData.constData() + this->Offset, delta); this->Offset += delta; @@ -134,22 +130,22 @@ qint64 pqHelpWindowNetworkReply::readData(char *data, qint64 maxSize) return -1; } +#if defined(USE_QTWEBKIT) +#include <QWebHistory> +#include <QWebView> // **************************************************************************** // CLASS pqHelpWindow::pqNetworkAccessManager // **************************************************************************** //----------------------------------------------------------------------------- -class pqHelpWindow::pqNetworkAccessManager : public QNetworkAccessManager -{ +class pqHelpWindow::pqNetworkAccessManager : public QNetworkAccessManager { typedef QNetworkAccessManager Superclass; QPointer<QHelpEngineCore> Engine; + public: - pqNetworkAccessManager( - QHelpEngineCore* helpEngine, QNetworkAccessManager *manager, - QObject *parentObject) : - Superclass(parentObject), - Engine(helpEngine) - { + pqNetworkAccessManager(QHelpEngineCore *helpEngine, + QNetworkAccessManager *manager, QObject *parentObject) + : Superclass(parentObject), Engine(helpEngine) { Q_ASSERT(manager != NULL && helpEngine != NULL); this->setCache(manager->cache()); @@ -162,12 +158,9 @@ protected: QNetworkReply *createRequest(Operation operation, const QNetworkRequest &request, QIODevice *device) override { - if (request.url().scheme() == "qthelp" && operation == GetOperation) - { - return new pqHelpWindowNetworkReply(request.url(), this->Engine); - } - else - { + if (request.url().scheme() == "qthelp" && operation == GetOperation) { + return new pqHelpWindowNetworkReply(request.url(), this->Engine, this); + } else { return this->Superclass::createRequest(operation, request, device); } } @@ -176,23 +169,50 @@ private: Q_DISABLE_COPY(pqNetworkAccessManager) }; +#else + +#include <QWebEngineHistory> +#include <QWebEnginePage> +#include <QWebEngineProfile> +#include <QWebEngineUrlRequestJob> +#include <QWebEngineUrlSchemeHandler> +#include <QWebEngineView> + +/// Adds support for qthelp scheme links that load content from them QHelpEngine +class QtHelpUrlHandler : public QWebEngineUrlSchemeHandler { +public: + QtHelpUrlHandler(QHelpEngineCore *helpEngine, QObject *parent = nullptr) + : QWebEngineUrlSchemeHandler(parent), Engine(helpEngine) {} + +protected: + void requestStarted(QWebEngineUrlRequestJob *request) override { + request->reply("text/html", + new pqHelpWindowNetworkReply(request->requestUrl(), + this->Engine, request)); + } + +private: + QHelpEngineCore *Engine; +}; + +#endif + // **************************************************************************** // CLASS pqHelpWindow // **************************************************************************** //----------------------------------------------------------------------------- -pqHelpWindow::pqHelpWindow( - QHelpEngine* engine, QWidget* parentObject, Qt::WindowFlags parentFlags) - : Superclass(parentObject, parentFlags), m_helpEngine(engine) -{ +pqHelpWindow::pqHelpWindow(QHelpEngine *engine, QWidget *parentObject, + Qt::WindowFlags parentFlags) + : Superclass(parentObject, parentFlags), m_helpEngine(engine) { Q_ASSERT(engine != NULL); Ui::pqHelpWindow ui; ui.setupUi(this); // all warnings from the help engine get logged - QObject::connect(this->m_helpEngine, SIGNAL(warning(const QString&)), - this, SIGNAL(helpWarnings(const QString&))); + QObject::connect(this->m_helpEngine, SIGNAL(warning(const QString &)), this, + SIGNAL(helpWarnings(const QString &))); // add a navigation toolbar QToolBar *navigation = new QToolBar("Navigation"); @@ -229,13 +249,13 @@ pqHelpWindow::pqHelpWindow( ui.contentsDock->raise(); // setup the search tab - QWidget* searchPane = new QWidget(this); - QVBoxLayout* vbox = new QVBoxLayout(); + QWidget *searchPane = new QWidget(this); + QVBoxLayout *vbox = new QVBoxLayout(); searchPane->setLayout(vbox); vbox->addWidget(this->m_helpEngine->searchEngine()->queryWidget()); vbox->addWidget(this->m_helpEngine->searchEngine()->resultWidget()); - connect(this->m_helpEngine->searchEngine()->resultWidget(), SIGNAL(requestShowLink(QUrl)), - this, SLOT(showPage(QUrl))); + connect(this->m_helpEngine->searchEngine()->resultWidget(), + SIGNAL(requestShowLink(QUrl)), this, SLOT(showPage(QUrl))); // set the search connection ui.searchDock->setWidget(searchPane); @@ -243,50 +263,56 @@ pqHelpWindow::pqHelpWindow( this, SLOT(search())); // connect the index page to the content pane - connect(m_helpEngine->contentWidget(), SIGNAL(linkActivated(QUrl)), - this, SLOT(showPage(QUrl))); - connect(this->m_helpEngine->indexWidget(), SIGNAL(linkActivated(QUrl,QString)), - this, SLOT(showPage(QUrl))); - - // setup the content pane - this->m_browser = new QWebView(this); - m_browser->page()->setLinkDelegationPolicy(QWebPage::DelegateAllLinks); - this->setCentralWidget(this->m_browser); + connect(m_helpEngine->contentWidget(), SIGNAL(linkActivated(QUrl)), this, + SLOT(showPage(QUrl))); + connect(this->m_helpEngine->indexWidget(), + SIGNAL(linkActivated(QUrl, QString)), this, SLOT(showPage(QUrl))); +// setup the content pane +#if defined(USE_QTWEBKIT) QNetworkAccessManager *oldManager = m_browser->page()->networkAccessManager(); - pqNetworkAccessManager* newManager = new pqNetworkAccessManager( - m_helpEngine, oldManager, this); + pqNetworkAccessManager *newManager = + new pqNetworkAccessManager(m_helpEngine, oldManager, this); + m_browser = new QWebView(this); + m_browser->page()->setLinkDelegationPolicy(QWebPage::DelegateAllLinks); m_browser->page()->setNetworkAccessManager(newManager); m_browser->page()->setForwardUnsupportedContent(false); - connect(this->m_browser, SIGNAL(linkClicked(QUrl)), this, SLOT(showPage(QUrl))); + connect(m_browser, SIGNAL(linkClicked(QUrl)), this, SLOT(showPage(QUrl))); + // set up the status bar + connect(m_browser->page(), SIGNAL(linkHovered(QString, QString, QString)), + this, SLOT(linkHovered(QString, QString, QString))); +#else + QWebEngineProfile::defaultProfile()->installUrlSchemeHandler( + "qthelp", new QtHelpUrlHandler(engine)); + m_browser = new QWebEngineView(this); + m_browser->setPage(new DelegatingWebPage(m_browser)); + connect(m_browser->page(), SIGNAL(linkClicked(QUrl)), this, + SLOT(showPage(QUrl))); + // set up the status bar + connect(m_browser->page(), SIGNAL(linkHovered(QString)), this, + SLOT(linkHovered(QString))); +#endif + this->setCentralWidget(this->m_browser); // connect the navigation buttons connect(home, SIGNAL(clicked()), this, SLOT(showHomePage())); connect(print, SIGNAL(clicked()), this, SLOT(printPage())); - connect(m_forward, SIGNAL(clicked()), m_browser, SLOT(forward())); + connect(m_forward, SIGNAL(clicked()), m_browser, SLOT(forward())); connect(m_backward, SIGNAL(clicked()), m_browser, SLOT(back())); - connect(m_forward, SIGNAL(clicked()), this, SLOT(updateNavButtons())); + connect(m_forward, SIGNAL(clicked()), this, SLOT(updateNavButtons())); connect(m_backward, SIGNAL(clicked()), this, SLOT(updateNavButtons())); - // set up the status bar - connect(m_browser->page(), SIGNAL(linkHovered(QString, QString, QString)), this, - SLOT(linkHovered(QString, QString, QString))); - // setup the search engine to do its job m_helpEngine->searchEngine()->reindexDocumentation(); } //----------------------------------------------------------------------------- -pqHelpWindow::~pqHelpWindow() -{ -} /** * Set the contents of the browser to show an error message. * @param url The url that could not be found. */ -void pqHelpWindow::errorMissingPage(const QUrl& url) -{ +void pqHelpWindow::errorMissingPage(const QUrl &url) { QString htmlDoc = QString(QLatin1String( "<html><head><title>Invalid Url - %1</title></head><body>")) @@ -302,24 +328,20 @@ void pqHelpWindow::errorMissingPage(const QUrl& url) } //----------------------------------------------------------------------------- -void pqHelpWindow::showPage(const QString& url) -{ +void pqHelpWindow::showPage(const QString &url) { this->showPage(QUrl::fromUserInput(url)); } //----------------------------------------------------------------------------- -void pqHelpWindow::showPage(const QUrl& url) -{ - if(url.scheme() == "qthelp") - { +void pqHelpWindow::showPage(const QUrl &url) { + if (url.scheme() == "qthelp") { if (this->m_helpEngine->findFile(url).isValid()) this->m_browser->setUrl(url); else errorMissingPage(url); this->updateNavButtons(); - } - else - { + } else { + using MantidQt::API::MantidDesktopServices; MantidDesktopServices::openUrl(url); } } @@ -331,48 +353,45 @@ void pqHelpWindow::printPage() { dialog.setWindowTitle(tr("Print Document")); if (dialog.exec() != QDialog::Accepted) return; +#if defined(USE_QTWEBKIT) m_browser->print(&printer); +#else + m_browser->page()->print(&printer, [](bool) {}); +#endif } //----------------------------------------------------------------------------- -void pqHelpWindow::updateNavButtons() -{ +void pqHelpWindow::updateNavButtons() { m_forward->setEnabled(m_browser->history()->canGoForward()); m_backward->setEnabled(m_browser->history()->canGoBack()); } //----------------------------------------------------------------------------- -void pqHelpWindow::search() -{ +void pqHelpWindow::search() { QList<QHelpSearchQuery> query = this->m_helpEngine->searchEngine()->queryWidget()->query(); this->m_helpEngine->searchEngine()->search(query); } //----------------------------------------------------------------------------- -void pqHelpWindow::linkHovered(const QString & link, const QString & title, - const QString & textContent) -{ +void pqHelpWindow::linkHovered(const QString &link, const QString &title, + const QString &textContent) { (void)title; (void)textContent; this->statusBar()->showMessage(link); } -void pqHelpWindow::showHomePage() -{ +void pqHelpWindow::showHomePage() { showPage(QString("qthelp://org.mantidproject/doc/index.html")); } //----------------------------------------------------------------------------- -void pqHelpWindow::showHomePage(const QString& namespace_name) -{ - QList<QUrl> html_pages = this->m_helpEngine->files(namespace_name, - QStringList(), "html"); +void pqHelpWindow::showHomePage(const QString &namespace_name) { + QList<QUrl> html_pages = + this->m_helpEngine->files(namespace_name, QStringList(), "html"); // now try to locate a file named index.html in this collection. - foreach (QUrl url, html_pages) - { - if (url.path().endsWith("index.html")) - { + foreach (QUrl url, html_pages) { + if (url.path().endsWith("index.html")) { this->showPage(url.toString()); return; }