-
Andreas Bolin authoredAndreas Bolin authored
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
main_window.py 14.77 KiB
# This Python file uses the following encoding: 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
from pprint import pprint
#from matplotlib.pyplot import bar
#from diagram import *
from importlib.machinery import SourceFileLoader
import inspect
from numpy import uint
# Qt/qtpy
import qtpy
from qtpy import uic, QtCore, QtGui, QtWidgets
from qtpy.QtCore import QCoreApplication, Qt, Slot, QSettings, QStandardPaths
#QtCore QSize, QPoint
from qtpy.QtGui import QCloseEvent
from qtpy.QtWidgets import (
QApplication, QMainWindow, QMessageBox, QFileDialog, QInputDialog, QCheckBox, QAbstractButton)
# QGraphics and QPainter imports
from qtpy.QtWidgets import (
QGraphicsView, QGraphicsScene, QGraphicsWidget,
QGraphicsLayout, QGraphicsLinearLayout, QGraphicsGridLayout, QGraphicsLayoutItem, QGraphicsAnchorLayout,
QGraphicsItem, QGraphicsItemGroup)
from qtpy.QtGui import (
QPaintEvent, QPainter, QPainterPath, QColor, QBrush, QPen, QFont, QPolygon, QIcon, QPixmap,
QLinearGradient)
from qtpy.QtCore import (
QRect, QPoint, QSize, QByteArray)
# B-ASIC
import logger
from b_asic.schedule import Schedule
log = logger.getLogger()
# Debug struff
if __debug__:
log.setLevel('DEBUG')
if __debug__:
# Print some system version information
QT_API = os.environ.get('QT_API')
print('Qt version (runtime): ', str(QtCore.qVersion()))
print('Qt version (compiletime):', QtCore.__version__)
print('QT_API: ', QT_API)
if QT_API.lower().startswith('pyside'):
import PySide2
print('PySide version: ', PySide2.__version__)
if QT_API.lower().startswith('pyqt'):
from qtpy.QtCore import PYQT_VERSION_STR
print('PyQt version: ', PYQT_VERSION_STR)
print('QtPy version: ', qtpy.__version__)
# Autocompile the .ui form to a python file.
try: # PyQt5, try autocompile
from qtpy.uic import compileUiDir
# def _map_func(dir: str, file: str) -> tuple[str, str]:
# file_base,_ = os.path.splitext(file)
# return (dir+'/ui/', file_base+'_ui.py')
# uic.compileUiDir('.', map=_map_func)
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 = ["mkdir -p ui",
"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)
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."""
_schedules: dict
_schedule_id: int
def __init__(self):
"""Initialize Schedule-gui."""
super(MainWindow, self).__init__()
self._schedules = {}
self._schedule_id = 0
QIcon.setThemeName('breeze')
log.debug('themeName: \'{}\''.format(QIcon.themeName()))
log.debug('themeSearchPaths: {}'.format(QIcon.themeSearchPaths()))
self._init_ui()
self._init_graphics_view()
self._read_settings()
def _init_ui(self) -> None:
"""Initialize the ui"""
self.setupUi(self)
# Connect signals to slots
self.pushbutton_add .clicked .connect(self.callback_pushButton)
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.splitter_center .splitterMoved .connect(self._splitter_center_moved)
# Setup event member functions
self.closeEvent = self._close_event
# Init info sidebar
for i in range(10):
self.listWidget.addItem('this is a very very very very long string that says abolutly nothing' + str(i))
self.listWidget.addItem('this is a short string' + str(i))
# Init central-widget splitter
self.splitter_center.setStretchFactor(0, 1)
self.splitter_center.setStretchFactor(1, 0)
self.splitter_center.setCollapsible(0, False)
self.splitter_center.setCollapsible(1, True)
def _init_graphics_view(self) -> None:
"""Initialize the QGraphics framework"""
self.graphic_scene = QGraphicsScene(self)
self.graphic_view.setScene(self.graphic_scene)
self.graphic_view.setRenderHint(QPainter.Antialiasing)
self.graphic_view.setGeometry(20, 20, self.width(), self.height())
self.graphic_view.setDragMode(QGraphicsView.RubberBandDrag)
###############
#### Slots ####
###############
@Slot()
def callback_pushButton(self) -> None:
self.printButtonPressed('callback_pushButton()')
@Slot()
def _load_schedule_from_pyfile(self) -> None:
abs_path_filename = QFileDialog.getOpenFileName(self, self.tr("Open python file"),
QStandardPaths.standardLocations(QStandardPaths.HomeLocation)[0],
self.tr("Python Files (*.py *.py3)"))
abs_path_filename = abs_path_filename[0]
if not abs_path_filename: # return if empty filename (QFileDialog was canceled)
return
log.debug('abs_path_filename = {}.'.format(abs_path_filename))
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: type(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)))
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')
return
self.open(schedule_obj_list[ret_tuple[0]])
#@Slot()
def open(self, schedule: Schedule) -> None:
"""Takes in an Schedule and place it in the schedule list."""
#TODO: all
#TODO: Unique hash keys
#TODO: self.open(schedule_obj_list[ret_tuple[0])
self._schedules[self._schedule_id] = schedule
self._schedule_id += 1
print(self._schedule_id)
pprint(self._schedules)
self.printButtonPressed('load_sfg()')
self.update_statusbar(self.tr('SFG 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_center.getRange(1) # tuple(min, max)
if checked:
self.splitter_center.restoreState(settings.value("mainwindow/splitter_center/last_state"))
# self.splitter_center.restoreState(settings.value("splitterSizes"))
else:
settings.setValue("mainwindow/splitter_center/last_state", self.splitter_center.saveState())
self.splitter_center.moveSplitter(range[1], 1)
@Slot(bool)
def toggle_exit_dialog(self, checked: bool):
s = QSettings()
s.setValue("mainwindow/hide_exit_dialog", checked)
@Slot(int, int)
def _splitter_center_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_center.sizes())
if widths[1] == 0:
self.menu_node_info.setChecked(False)
else:
self.menu_node_info.setChecked(True)
################
#### 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.setIconPixmap(QIcon.fromTheme('dialog-question').pixmap(64))
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
self.label.setText("hello")
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_center.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_center.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()