Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
conda activate sscanss
conda install -c conda-forge libstdcxx-ng
sudo apt-get update
sudo apt-get install xvfb libqt5x11extras5 libgl1-mesa-glx '^libxcb.*-dev'
sudo apt-get install xvfb libqt5x11extras5 libgl1 libglx-mesa0 '^libxcb.*-dev'
xvfb-run --auto-servernum python make.py --build-all
- name: Run unit-tests (Windows)
if: runner.os == 'Windows'
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ NLopt==2.7.1
numpy==1.23.5
gimpact==1.0.1
Pillow==9.2.0
PyInstaller==6.7.0
PyInstaller==6.11.1
PyOpenGL==3.1.6
PyQt6==6.3.1
PyQt6-Qt6==6.5.2
Expand Down
4 changes: 2 additions & 2 deletions sscanss/app/dialogs/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from .misc import (ProgressDialog, ProjectDialog, AlignmentErrorDialog, SimulationDialog, ScriptExportDialog,
PathLengthPlotter, AboutDialog, CalibrationErrorDialog, CurveEditor, InstrumentCoordinatesDialog)
from .misc import (ProgressDialog, ProjectWidget, AlignmentErrorDialog, SimulationDialog, ScriptExportDialog,
PathLengthPlotter, AboutWidget, CalibrationErrorDialog, CurveEditor, InstrumentCoordinatesDialog)
from .preferences import Preferences
from .insert import (InsertPrimitiveDialog, InsertPointDialog, InsertVectorDialog, PickPointDialog, AlignSample,
VolumeLoader)
Expand Down
45 changes: 30 additions & 15 deletions sscanss/app/dialogs/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from sscanss.app.widgets import AlignmentErrorModel, ErrorDetailModel, CenteredBoxProxy


class AboutDialog(QtWidgets.QDialog):
class AboutWidget(QtWidgets.QWidget):
"""Creates a UI that displays information about the software

:param parent: main window instance
Expand All @@ -25,14 +25,14 @@ class AboutDialog(QtWidgets.QDialog):
def __init__(self, parent):
super().__init__(parent)

self.setObjectName('FramelessDialog')
self.main_layout = QtWidgets.QVBoxLayout()
self.setLayout(self.main_layout)
self.setWindowFlags(QtCore.Qt.WindowType.FramelessWindowHint | QtCore.Qt.WindowType.Dialog)
self.setMinimumSize(640, 560)
self.main_layout.setContentsMargins(1, 1, 1, 1)

header = ImageHeader()
header.close_button.clicked.connect(self.reject)
header.close_button.clicked.connect(self.close)
self.main_layout.addWidget(header)

layout = QtWidgets.QVBoxLayout()
Expand Down Expand Up @@ -72,8 +72,16 @@ def __init__(self, parent):
label.setWordWrap(True)
layout.addWidget(label)

def paintEvent(self, event):
opt = QtWidgets.QStyleOption()
opt.initFrom(self)
p = QtGui.QPainter(self)
self.style().drawPrimitive(QtWidgets.QStyle.PrimitiveElement.PE_Widget, opt, p, self)

super().paintEvent(event)


class ProjectDialog(QtWidgets.QDialog):
class ProjectWidget(QtWidgets.QWidget):
"""Creates a UI for creating and loading projects

:param parent: main window instance
Expand All @@ -86,6 +94,7 @@ class ProjectDialog(QtWidgets.QDialog):
def __init__(self, parent):
super().__init__(parent)

self.setObjectName('FramelessDialog')
self._busy = False
self.parent = parent
self.instruments = sorted(parent.presenter.model.instruments.keys())
Expand All @@ -94,13 +103,12 @@ def __init__(self, parent):

self.main_layout = QtWidgets.QVBoxLayout()
self.setLayout(self.main_layout)
self.setWindowFlags(QtCore.Qt.WindowType.FramelessWindowHint | QtCore.Qt.WindowType.Dialog)
self.setMinimumSize(640, 500)
self.main_layout.setContentsMargins(1, 1, 1, 1)

header = ImageHeader()
header.close_button.clicked.connect(self.reject)
self.main_layout.addWidget(header)
self.header = ImageHeader()
self.header.close_button.clicked.connect(self.reject)
self.main_layout.addWidget(self.header)

self.createTabWidgets()
self.createNewProjectWidgets()
Expand All @@ -117,7 +125,10 @@ def is_busy(self):
@is_busy.setter
def is_busy(self, value):
self._busy = value
self.setModal(value)
self.parent.updateMenus(not value and self.parent.presenter.model.project_data is not None)
self.parent.docks.upper_dock.setDisabled(value)
self.parent.docks.bottom_dock.setDisabled(value)
self.header.close_button.setVisible(not value)
self.loading_bar.setMaximum(0 if value else 1)

def createTabWidgets(self):
Expand Down Expand Up @@ -244,20 +255,24 @@ def onFailure(self, exception, args):
self.parent.presenter.projectCreationError(exception, args)
self.is_busy = False

def keyPressEvent(self, event):
"""This ensures the user cannot close the dialog box with the Esc key"""
if not self.is_busy:
super().keyPressEvent(event)
def paintEvent(self, event):
opt = QtWidgets.QStyleOption()
opt.initFrom(self)
p = QtGui.QPainter(self)
self.style().drawPrimitive(QtWidgets.QStyle.PrimitiveElement.PE_Widget, opt, p, self)

super().paintEvent(event)

def accept(self):
self.project_name_textbox.clear()
self.is_busy = False
super().accept()
self.close()

def reject(self):
if self.parent.presenter.model.project_data is None:
self.parent.instrument_label.setText("Most menu options are disabled until you create/open a project.")
super().reject()
self.is_busy = False
self.close()


class ProgressDialog(QtWidgets.QDialog):
Expand Down
2 changes: 1 addition & 1 deletion sscanss/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def ui_execute():
msg = f'{filename} could not be opened because it has an unknown file type'
QtCore.QTimer.singleShot(wait_time, lambda: window.showMessage(msg))
else:
QtCore.QTimer.singleShot(wait_time, window.showNewProjectDialog)
QtCore.QTimer.singleShot(wait_time, window.showNewProjectWidget)

window.show()
window.updater.check(True)
Expand Down
4 changes: 2 additions & 2 deletions sscanss/app/window/presenter.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def updateView(self):

self.view.docks.closeAll()
self.view.closeNonModalDialog()
self.view.updateMenus()
self.view.updateMenus(True)

def projectCreationError(self, exception, args):
"""Handles errors from project creation or instrument change
Expand All @@ -109,7 +109,7 @@ def projectCreationError(self, exception, args):
self.view.docks.closeAll()
if self.model.project_data is None or self.model.instrument is None:
self.model.project_data = None
self.view.updateMenus()
self.view.updateMenus(False)
self.view.clearUndoStack()
else:
toggle_action_in_group(self.model.instrument.name, self.view.change_instrument_action_group)
Expand Down
103 changes: 85 additions & 18 deletions sscanss/app/window/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
from sscanss.__version import __version__, Version
from sscanss.config import settings, DOCS_URL, UPDATE_URL, RELEASES_URL
from sscanss.themes import ThemeManager, path_for, IconEngine
from sscanss.app.dialogs import (ProgressDialog, ProjectDialog, Preferences, AlignmentErrorDialog, ScriptExportDialog,
PathLengthPlotter, AboutDialog, CalibrationErrorDialog, InstrumentCoordinatesDialog,
from sscanss.app.dialogs import (ProgressDialog, ProjectWidget, Preferences, AlignmentErrorDialog, ScriptExportDialog,
PathLengthPlotter, AboutWidget, CalibrationErrorDialog, InstrumentCoordinatesDialog,
CurveEditor, VolumeLoader)
from sscanss.core.scene import Node, OpenGLRenderer, SceneManager
from sscanss.core.util import (Primitives, Directions, TransformType, PointType, MessageType, Attributes,
Expand All @@ -37,6 +37,69 @@ def __call__(self):
return self.view.presenter.openProject(self.filename)


class OverlayContainer(QtWidgets.QWidget):
"""A container that allow a widget to be overlaid over a background widget

:param background: background widget
:type background: QWidget
:param parent: instance of main window
:type parent: MainWindow
"""
def __init__(self, background, parent):
super().__init__(parent)

self.widget = None
self.main_layout = QtWidgets.QStackedLayout()
self.main_layout.setStackingMode(QtWidgets.QStackedLayout.StackingMode.StackAll)
self.setLayout(self.main_layout)

self.main_layout.addWidget(background)
overlay_widget = QtWidgets.QWidget()
overlay_widget.setObjectName('overlay')
overlay_widget.setStyleSheet('#overlay {background: transparent;}')

layout = QtWidgets.QHBoxLayout()
self.overlay_layout = QtWidgets.QVBoxLayout()
layout.addStretch(1)
layout.addLayout(self.overlay_layout)
layout.addStretch(1)
overlay_widget.setLayout(layout)
self.main_layout.addWidget(overlay_widget)

def setOverlayWidget(self, widget):
"""Sets and shows the overlay widget

:param widget: widget to overlay over background
:type widget: QWidget
"""
self.closeOverlayWidget()
self.widget = widget
self.overlay_layout.addStretch(1)
self.overlay_layout.addWidget(widget)
self.overlay_layout.addStretch(1)
self.main_layout.setCurrentIndex(1)

widget.installEventFilter(self)

def eventFilter(self, _obj, event):
"""Catch close event for overlay widget"""
if event.type() == QtCore.QEvent.Type.Close:
self.closeOverlayWidget()
return True
return False

def closeOverlayWidget(self):
"""Closes the overlay widget and show background"""
if self.widget is None:
return

self.main_layout.setCurrentIndex(0)
for i in reversed(range(self.overlay_layout.count())):
self.overlay_layout.takeAt(i)
self.widget.hide()
self.widget = None


class MainWindow(QtWidgets.QMainWindow):
"""Creates the main view for the sscanss app"""
def __init__(self):
Expand All @@ -54,9 +117,11 @@ def __init__(self):
self.themes = ThemeManager(self)
self.themes.theme_changed.connect(self.setStyleSheet)
self.themes.theme_changed.connect(self.updateImages)

self.gl_widget = OpenGLRenderer(self)
self.gl_widget.custom_error_handler = self.sceneSizeErrorHandler
self.setCentralWidget(self.gl_widget)
self.overlay = OverlayContainer(self.gl_widget, self)
self.setCentralWidget(self.overlay)

self.docks = DockManager(self)
self.scenes = SceneManager(self.presenter.model, self.gl_widget)
Expand All @@ -76,21 +141,21 @@ def __init__(self):
self.setAttribute(QtCore.Qt.WidgetAttribute.WA_DeleteOnClose)

self.readSettings()
self.updateMenus()
self.updateMenus(False)

def createActions(self):
"""Creates the menu and toolbar actions """
self.new_project_action = QtGui.QAction('&New Project', self)
self.new_project_action.setStatusTip('Create a new project')
self.new_project_action.setIcon(QtGui.QIcon(IconEngine('file.png')))
self.new_project_action.setShortcut(QtGui.QKeySequence.StandardKey.New)
self.new_project_action.triggered.connect(lambda: self.presenter.confirmSave(self.showNewProjectDialog))
self.new_project_action.triggered.connect(lambda: self.presenter.confirmSave(self.showNewProjectWidget))

self.open_project_action = QtGui.QAction('&Open Project', self)
self.open_project_action.setStatusTip('Open an existing project')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see an error originating from line 158 if a certain sequence is followed. Can you verify if it happens on Windows too.

  1. Open SScanSS-2 app.

  2. Click X on the ProjectWidget.
    image

  3. Click the open project button.
    image

  4. Do not select anything. Just press cancel.

  5. I see this Traceback on Mac
    image

I updated openProject as follows to get rid of the error on mac but there might be a better way to do it. Maybe the Open Project icon can be disabled till either a new project is created or an existing project is selected from the ProjectWidget.
image

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, the bug was caused by this line

self.open_project_action.triggered.connect(lambda: self.presenter.confirmSave(self.presenter.openProject()))

which should be

self.open_project_action.triggered.connect(lambda: self.presenter.confirmSave(self.presenter.openProject))

Should be fixed now

self.open_project_action.setIcon(QtGui.QIcon(IconEngine('folder-open.png')))
self.open_project_action.setShortcut(QtGui.QKeySequence.StandardKey.Open)
self.open_project_action.triggered.connect(lambda: self.presenter.confirmSave(self.presenter.openProject()))
self.open_project_action.triggered.connect(lambda: self.presenter.confirmSave(self.presenter.openProject))

self.save_project_action = QtGui.QAction('&Save Project', self)
self.save_project_action.setStatusTip('Save project')
Expand Down Expand Up @@ -378,7 +443,7 @@ def createActions(self):

self.show_about_action = QtGui.QAction(f'&About {MAIN_WINDOW_TITLE}', self)
self.show_about_action.setStatusTip(f'About {MAIN_WINDOW_TITLE}')
self.show_about_action.triggered.connect(self.showAboutDialog)
self.show_about_action.triggered.connect(self.showAboutWidget)

# ToolBar Actions
self.rotate_sample_action = QtGui.QAction('Rotate Sample', self)
Expand Down Expand Up @@ -422,10 +487,11 @@ def createActions(self):
self.show_curve_editor_action.setIcon(QtGui.QIcon(IconEngine('curve.png')))
self.show_curve_editor_action.triggered.connect(self.showCurveEditor)

def showAboutDialog(self):
def showAboutWidget(self):
"""Display the About Dialog"""
self.createNonModalDialog(AboutDialog)
self.non_modal_dialog.show()
self.closeNonModalDialog()
about_dialog = AboutWidget(self)
self.overlay.setOverlayWidget(about_dialog)

def updateImages(self):
"""Updates the images of the actions on the menu"""
Expand Down Expand Up @@ -579,10 +645,8 @@ def createMenus(self):
help_menu.addAction(self.check_update_action)
help_menu.addAction(self.show_about_action)

def updateMenus(self):
def updateMenus(self, enable):
"""Disables the menus when a project is not created and enables menus when a project is created"""
enable = self.presenter.model.project_data is not None

self.save_project_action.setEnabled(enable)
self.save_as_action.setEnabled(enable)
for action in self.export_menu.actions():
Expand Down Expand Up @@ -706,6 +770,7 @@ def createStatusBar(self):
sb.addPermanentWidget(self.size_label)

def createNonModalDialog(self, dialog_type):
self.overlay.closeOverlayWidget()
if not isinstance(self.non_modal_dialog, dialog_type):
self.closeNonModalDialog()
dialog = dialog_type(self)
Expand All @@ -716,6 +781,7 @@ def createNonModalDialog(self, dialog_type):
def closeNonModalDialog(self):
if self.non_modal_dialog is not None:
self.non_modal_dialog.close()
self.non_modal_dialog = None

def clearUndoStack(self):
"""Clears the undo stack and ensures stack is cleaned even when stack is empty"""
Expand Down Expand Up @@ -859,11 +925,12 @@ def __createChangeCollimatorAction(self, name, active, detector):

return change_collimator_action

def showNewProjectDialog(self):
"""Opens the new project dialog"""
self.createNonModalDialog(ProjectDialog)
self.non_modal_dialog.updateRecentProjects(self.recent_projects)
self.non_modal_dialog.show()
def showNewProjectWidget(self):
"""Opens the new project widget"""
self.closeNonModalDialog()
project_dialog = ProjectWidget(self)
project_dialog.updateRecentProjects(self.recent_projects)
self.overlay.setOverlayWidget(project_dialog)

def showPreferences(self, group=None):
"""Opens the preferences dialog"""
Expand Down
7 changes: 6 additions & 1 deletion sscanss/static/dark_theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ QDialog
background-color: rgb(52, 49, 49);
}

#FramelessDialog{
border: 1px solid rgb(70, 70, 70);
color: rgb(255, 255, 255);
background-color: rgb(52, 49, 49);
}

QListWidget
{
Expand Down Expand Up @@ -601,7 +606,7 @@ QProgressBar::chunk
width: 3px;
margin: 1px;
}
ProjectDialog QProgressBar
ProjectWidget QProgressBar
{
border: transparent;
border-radius: 0px;
Expand Down
8 changes: 6 additions & 2 deletions sscanss/static/mac_style.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@
QDialog{
border: 1px solid #ddd;
}


#FramelessDialog{
border: 1px solid #ddd;
background-color: #eee;
}
QToolBar {
spacing: 10px; /* spacing between items in the tool bar */
padding: 5px;
Expand Down Expand Up @@ -258,7 +262,7 @@ QComboBox {
image: url(@Path/splitter.png);
}

ProjectDialog QProgressBar {
ProjectWidget QProgressBar {
border: transparent;
border-radius: 0px;
background-color: transparent;
Expand Down
Loading