-
Andreas Bolin authoredAndreas Bolin authored
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
main_window.py 17.34 KiB
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""B-ASIC Scheduler-gui Module.
Contains the scheduler-gui class for scheduling operations in an SFG.
Start main-window with start_gui().
"""
import os
import sys
from pathlib import Path
from types import ModuleType
from typing import Any, Iterable, List
from pprint import pprint
#from matplotlib.pyplot import bar
#from diagram import *
from importlib.machinery import SourceFileLoader
import inspect
# Qt/qtpy
import qtpy
from qtpy import uic, QtCore, QtGui, QtWidgets
from qtpy.QtCore import QCoreApplication, Qt, Slot, Signal, QSettings, QStandardPaths
from qtpy.QtGui import QCloseEvent
from qtpy.QtWidgets import (
QApplication, QMainWindow, QMessageBox, QFileDialog, QInputDialog, QCheckBox, QAbstractButton,
QTableWidgetItem, QSizePolicy)
# QGraphics and QPainter imports
from qtpy.QtCore import (
QRect, QRectF, QPoint, QSize, QByteArray, QMarginsF, QObject)
from qtpy.QtGui import (
QPaintEvent, QPainter, QPainterPath, QColor, QBrush, QPen, QFont, QPolygon, QIcon, QPixmap,
QLinearGradient)
from qtpy.QtWidgets import (
QGraphicsView, QGraphicsScene, QGraphicsWidget, QGraphicsScale,
QGraphicsLayout, QGraphicsLinearLayout, QGraphicsGridLayout, QGraphicsLayoutItem, QGraphicsAnchorLayout,
QGraphicsItem, QGraphicsItemGroup, QGraphicsRectItem)
# B-ASIC
import logger
from b_asic.schedule import Schedule
from graphics_graph_item import GraphicsGraphItem
from graphics_axis_item import GraphicsAxisItem
from graphics_component_item import GraphicsComponentItem
# if sys.version_info >= (3, 9):
# List = list
# #Dict = dict
log = logger.getLogger()
sys.excepthook = logger.handle_exceptions
# Debug struff
if __debug__:
log.setLevel('DEBUG')
if __debug__:
# Print some system version information
QT_API = os.environ.get('QT_API')
log.debug('Qt version (runtime): {}'.format(QtCore.qVersion()))
log.debug('Qt version (compiletime): {}'.format(QtCore.__version__))
log.debug('QT_API: {}'.format(QT_API))
if QT_API.lower().startswith('pyside'):
import PySide2
log.debug('PySide version: {}'.format(PySide2.__version__))
if QT_API.lower().startswith('pyqt'):
from qtpy.QtCore import PYQT_VERSION_STR
log.debug('PyQt version: {}'.format(PYQT_VERSION_STR))
log.debug('QtPy version: {}'.format(qtpy.__version__))
# Autocompile the .ui form to a python file.
try: # PyQt5, try autocompile
from qtpy.uic import compileUiDir
uic.compileUiDir('.', map=(lambda dir,file: (dir, 'ui_' + file)))
except:
try: # PySide2, try manual compile
import subprocess
os_ = sys.platform
if os_.startswith('linux'):
cmds = ['pyside2-uic -o ui_main_window.py main_window.ui']
for cmd in cmds:
subprocess.call(cmd.split())
else:
#TODO: Implement (startswith) 'win32', 'darwin' (MacOs)
raise SystemExit
except: # Compile failed, look for pre-compiled file
try:
from ui_main_window import Ui_MainWindow
except: # Everything failed, exit
log.exception("Could not import 'Ui_MainWindow'.")
log.exception("Can't autocompile under", QT_API, "eviroment. Try to manual compile 'main_window.ui' to 'ui/main_window_ui.py'")
os._exit(1)
sys.path.insert(0, 'icons/') # Needed for the compiled '*_rc.py' files in 'ui_*.py' files
from ui_main_window import Ui_MainWindow # Only availible when the form (.ui) is compiled
# The folowing QCoreApplication values is used for QSettings among others
QCoreApplication.setOrganizationName('Linöping University')
QCoreApplication.setOrganizationDomain('liu.se')
QCoreApplication.setApplicationName('B-ASIC Scheduler')
#QCoreApplication.setApplicationVersion(__version__) # TODO: read from packet __version__
class MainWindow(QMainWindow, Ui_MainWindow):
"""Schedule of an SFG with scheduled Operations."""
_scene: QGraphicsScene
_graph: GraphicsGraphItem
_scale: float
_debug_rects: QGraphicsItemGroup
def __init__(self):
"""Initialize Schedule-gui."""
super().__init__()
self._graph = None
self._open_file_dialog_opened = False
self._scale = 75
self._debug_rects = None
QIcon.setThemeName('breeze')
log.debug('themeName: \'{}\''.format(QIcon.themeName()))
log.debug('themeSearchPaths: {}'.format(QIcon.themeSearchPaths()))
self._init_ui()
self._init_graphics()
self._read_settings()
def _init_ui(self) -> None:
"""Initialize the ui"""
self.setupUi(self)
# Connect signals to slots
self.menu_load_from_file.triggered .connect(self._load_schedule_from_pyfile)
self.menu_save .triggered .connect(self.save)
self.menu_save_as .triggered .connect(self.save_as)
self.menu_quit .triggered .connect(self.close)
self.menu_node_info .triggered .connect(self.toggle_component_info)
self.menu_exit_dialog .triggered .connect(self.toggle_exit_dialog)
self.actionT .triggered .connect(self.actionTbtn)
self.splitter .splitterMoved .connect(self._splitter_moved)
# Setup event member functions
self.closeEvent = self._close_event
# Setup info table
self.info_table.setHorizontalHeaderLabels(['Property','Value'])
# test = '#b085b2'
# self.info_table.setStyleSheet('alternate-background-color: lightGray;background-color: white;')
self.info_table.setStyleSheet('alternate-background-color: #fadefb;background-color: #ebebeb;')
for i in range(10):
self.info_table.insertRow(i)
item = QTableWidgetItem('this is a very very very very long string that says abolutly nothing')
self.info_table.setItem(i,0, QTableWidgetItem('property {}: '.format(i)))
self.info_table.setItem(i,1,item)
# Init central-widget splitter
self.splitter.setStretchFactor(0, 1)
self.splitter.setStretchFactor(1, 0)
self.splitter.setCollapsible(0, False)
self.splitter.setCollapsible(1, True)
def _init_graphics(self) -> None:
"""Initialize the QGraphics framework"""
self._scene = QGraphicsScene()
self.view.setScene(self._scene)
self.view.scale(self._scale, self._scale)
GraphicsComponentItem._scale = self._scale
GraphicsAxisItem._scale = self._scale
self._scene.changed.connect(self.shrink_scene_to_min_size)
###############
#### Slots ####
###############
@Slot()
def actionTbtn(self) -> None:
self._graph.schedule.plot_schedule()
print(f'filtersChildEvents(): {self._graph.filtersChildEvents()}')
# self.printButtonPressed('callback_pushButton()')
@Slot()
def _load_schedule_from_pyfile(self) -> None:
settings = QSettings()
# open_dir = QStandardPaths.standardLocations(QStandardPaths.HomeLocation)[0] if not self._open_file_dialog_opened else ''
last_file = settings.value('mainwindow/last_opened_file', QStandardPaths.standardLocations(QStandardPaths.HomeLocation)[0], str)
if not os.path.exists(last_file): # if filename does not exist
last_file = os.path.dirname(last_file) + '/'
if not os.path.exists(last_file): # if path does not exist
last_file = QStandardPaths.standardLocations(QStandardPaths.HomeLocation)[0]
abs_path_filename, _ = QFileDialog.getOpenFileName(self,
self.tr("Open python file"),
last_file,
self.tr("Python Files (*.py *.py3)"))
if not abs_path_filename: # return if empty filename (QFileDialog was canceled)
return
log.debug('abs_path_filename = {}.'.format(abs_path_filename))
self._open_file_dialog_opened = True
module_name = inspect.getmodulename(abs_path_filename)
if not module_name: # return if empty module name
log.error('Could not load module from file \'{}\'.'.format(abs_path_filename))
return
try:
module = SourceFileLoader(module_name, abs_path_filename).load_module()
except:
log.exception('Exception occurred. Could not load module from file \'{}\'.'.format(abs_path_filename))
return
schedule_obj_list = dict(inspect.getmembers(module, (lambda x: isinstance(x, Schedule))))
if not schedule_obj_list: # return if no Schedule objects in script
QMessageBox.warning(self,
self.tr('File not found'),
self.tr('Could not find any Schedule object in file \'{}\'.')
.format(os.path.basename(abs_path_filename)))
log.info('Could not find any Schedule object in file \'{}\'.'
.format(os.path.basename(abs_path_filename)))
del module
return
ret_tuple = QInputDialog.getItem(self,
self.tr('Load object'),
self.tr('Found the following Schedule object(s) in file.)\n\n'
'Select an object to proceed:'),
schedule_obj_list.keys(),0,False)
if not ret_tuple[1]: # User canceled the operation
log.debug('Load schedule operation: user canceled')
del module
return
self.open(schedule_obj_list[ret_tuple[0]])
del module
settings.setValue("mainwindow/last_opened_file", abs_path_filename)
#@Slot()
def open(self, schedule: Schedule) -> None:
"""Takes in an Schedule and creates a GraphicsGraphItem object."""
self._graph = GraphicsGraphItem(schedule)
self._scene.addItem(self._graph)
self._graph.installSceneEventFilters()
# graph.prepareGeometryChange()
# graph.setPos(200, 20)
# self._scene.setSceneRect(self._scene.itemsBoundingRect()) # Forces the scene to it's minimum size
# # Debug rectangles
# if __debug__:
# # self._scene.setSceneRect(self._scene.itemsBoundingRect()) # Forces the scene to it's minimum size
# m = QMarginsF(1/self._scale, 1/self._scale, 0, 0)
# m2 = QMarginsF(1, 1, 1, 1)
# pen = QPen(Qt.red)
# pen.setStyle(Qt.DotLine)
# pen.setCosmetic(True)
# for component in graph.items:
# self._scene.addRect(component.mapRectToScene(component.boundingRect() - m), pen)
# pen.setColor(Qt.red)
# for axis in graph.axis.childItems():
# self._scene.addRect(axis.mapRectToScene(axis.boundingRect() - m), pen)
# pen.setColor(Qt.green)
# # self._scene.addRect(self._scene.itemsBoundingRect() - m, pen)
# self._scene.setSceneRect(self._scene.itemsBoundingRect())
self.update_statusbar(self.tr('Schedule loaded successfully'))
@Slot()
def save(self) -> None:
"""This method save an schedule."""
#TODO: all
self.printButtonPressed('save_schedule()')
self.update_statusbar(self.tr('Schedule saved successfully'))
@Slot()
def save_as(self) -> None:
"""This method save as an schedule."""
#TODO: all
self.printButtonPressed('save_schedule()')
self.update_statusbar(self.tr('Schedule saved successfully'))
@Slot(bool)
def toggle_component_info(self, checked: bool) -> None:
"""This method toggles the right hand side info window."""
# Note: splitter handler index 0 is a hidden splitter handle far most left, use index 1
settings = QSettings()
range = self.splitter.getRange(1) # tuple(min, max)
if checked:
self.splitter.restoreState(settings.value("mainwindow/splitter/last_state"))
# self.splitter.restoreState(settings.value("splitterSizes"))
else:
settings.setValue("mainwindow/splitter/last_state", self.splitter.saveState())
self.splitter.moveSplitter(range[1], 1)
@Slot(bool)
def toggle_exit_dialog(self, checked: bool) -> None:
s = QSettings()
s.setValue("mainwindow/hide_exit_dialog", checked)
@Slot(int, int)
def _splitter_moved(self, pos: int, index: int) -> None:
"""Callback method used to check if the right widget (info window)
has collapsed. Update the checkbutton accordingly."""
# TODO: Custom move handler, save state on click-release?
widths: list[int, int] = list(self.splitter.sizes())
if widths[1] == 0:
self.menu_node_info.setChecked(False)
else:
self.menu_node_info.setChecked(True)
@Slot('QList<QRectF>')
def shrink_scene_to_min_size(self, region: List[QRectF]) -> None:
self._scene.setSceneRect(self._scene.itemsBoundingRect())
################
#### Events ####
################
def _close_event(self, event: QCloseEvent) -> None:
"""Replaces QMainWindow default closeEvent(QCloseEvent) event"""
s = QSettings()
hide_dialog = s.value('mainwindow/hide_exit_dialog', False, bool)
ret = QMessageBox.StandardButton.Yes
if not hide_dialog:
box = QMessageBox(self)
box.setWindowTitle(self.tr('Confirm Exit'))
box.setText('<h3>' + self.tr('Confirm Exit') + '</h3><p><br>' +
self.tr('Are you sure you want to exit?') +
' <br></p>')
box.setIcon(QMessageBox.Question)
box.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
buttons: list[QAbstractButton] = box.buttons()
buttons[0].setText(self.tr('&Exit'))
buttons[1].setText(self.tr('&Cancel'))
checkbox = QCheckBox(self.tr('Don\'t ask again'))
box.setCheckBox(checkbox)
ret = box.exec_()
if ret == QMessageBox.StandardButton.Yes:
if not hide_dialog:
s.setValue('mainwindow/hide_exit_dialog', checkbox.isChecked())
self._write_settings()
log.info('Exit: {}'.format(os.path.basename(__file__)))
event.accept()
else:
event.ignore()
#################################
#### Helper member functions ####
#################################
def printButtonPressed(self, func_name: str) -> None:
#TODO: remove
alert = QMessageBox(self)
alert.setText("Called from " + func_name + '!')
alert.exec_()
def update_statusbar(self, msg: str) -> None:
"""Write the given str to the statusbar with temporarily policy."""
self.statusbar.showMessage(msg)
def _write_settings(self) -> None:
"""Write settings from MainWindow to Settings."""
s = QSettings()
s.setValue('mainwindow/maximized', self.isMaximized()) # window: maximized, in X11 - alwas False
s.setValue('mainwindow/pos', self.pos()) # window: pos
s.setValue('mainwindow/size', self.size()) # window: size
s.setValue('mainwindow/state', self.saveState()) # toolbars, dockwidgets: pos, size
s.setValue('mainwindow/menu/node_info', self.menu_node_info.isChecked())
s.setValue('mainwindow/splitter/state', self.splitter.saveState())
if s.isWritable():
log.debug('Settings written to \'{}\'.'.format(s.fileName()))
else:
log.warning('Settings cant be saved to file, read-only.')
def _read_settings(self) -> None:
"""Read settings from Settings to MainWindow."""
s = QSettings()
if s.value('mainwindow/maximized', defaultValue=False, type=bool):
self.showMaximized()
else:
self.move( s.value('mainwindow/pos', self.pos()))
self.resize( s.value('mainwindow/size', self.size()))
self.restoreState( s.value('mainwindow/state', QByteArray()))
self.menu_node_info.setChecked( s.value('mainwindow/menu/node_info', True, bool))
self.splitter.restoreState( s.value('mainwindow/splitter/state', QByteArray()))
self.menu_exit_dialog.setChecked( s.value('mainwindow/hide_exit_dialog', False, bool))
log.debug('Settings read from \'{}\'.'.format(s.fileName()))
def start_gui():
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
if __name__ == "__main__":
start_gui()