def _model_data(self, index, role): """Replacement method for model.data(). Create pixmaps as they're requested by the data() method, to reduce loading time. """ if role == Qt.DisplayRole: return None if role != Qt.DecorationRole: return QStandardItemModel.data(self.model, index, role) display_icon = index.data(Qt.UserRole) pixmap = self.create_object_pixmap(display_icon) return QIcon(pixmap)
def _model_data(self, index, role): """Creates pixmaps as they're requested by the data() method, to reduce loading time. Args: index (QModelIndex): index to the model role (int): data role Returns: Any: role-dependent model data """ if role == Qt.DisplayRole: return None if role != Qt.DecorationRole: return QStandardItemModel.data(self.model, index, role) display_icon = index.data(Qt.UserRole) return object_icon(display_icon)
class MainWindow(QMainWindow): def __init__(self, parent=None, flags=Qt.WindowFlags()): super(MainWindow, self).__init__(parent, flags) self.dayWidth = 70 self.ui = Ui_MainWindow() self.ui.setupUi(self) self.initModel() self.initActions() self.initItemDelegate() self.initGrid() leftView = self.ui.ganttView.leftView() leftView.setColumnHidden(1, True) leftView.setColumnHidden(2, True) leftView.setColumnHidden(3, True) leftView.setColumnHidden(4, True) leftView.setColumnHidden(5, True) leftView.header().setStretchLastSection(True) self.ui.ganttView.leftView().customContextMenuRequested.connect( self.showContextMenu) self.ui.ganttView.selectionModel().selectionChanged.connect( self.enableActions) self.ui.ganttView.graphicsView().clicked.connect(self.slotClicked) self.ui.ganttView.graphicsView().qrealClicked.connect( self.slotDoubleClicked) def initModel(self): self.model = QStandardItemModel(0, 6, self) self.model.setHeaderData(0, Qt.Horizontal, "Task") self.ui.ganttView.setModel(self.model) self.l = QWidget(self) self.l.setWindowTitle("Legend") self.l.show() ##self.l.setModel(self.model) self.constraintModel = ConstraintModel(self) self.ui.ganttView.setConstraintModel(self.constraintModel) def initActions(self): self.newEntryAction = QAction("New entry", self) self.newEntryAction.setShortcut(QKeySequence.New) self.newEntryAction.triggered.connect(self.addNewEntry) self.removeEntryAction = QAction("Remove entry", self) self.removeEntryAction.setShortcut(QKeySequence.Delete) self.removeEntryAction.triggered.connect(self.removeEntry) self.demoAction = QAction("Demo entry", self) self.demoAction.triggered.connect(self.addDemoEntry) self.printAction = QAction("Print Preview...", self) self.printAction.triggered.connect(self.printPreview) self.zoomInAction = QAction("Zoom In", self) self.zoomInAction.setShortcut(QKeySequence.ZoomIn) self.zoomInAction.triggered.connect(self.zoomIn) self.zoomOutAction = QAction("Zoom Out", self) self.zoomOutAction.setShortcut(QKeySequence.ZoomOut) self.zoomOutAction.triggered.connect(self.zoomOut) self.ui.ganttView.leftView().setContextMenuPolicy(Qt.CustomContextMenu) self.ui.ganttView.leftView().addAction(self.newEntryAction) self.ui.ganttView.leftView().addAction(self.removeEntryAction) menuBar = QMenuBar(self) self.setMenuBar(menuBar) entryMenu = menuBar.addMenu("Entry") entryMenu.addAction(self.newEntryAction) entryMenu.addAction(self.removeEntryAction) entryMenu.addSeparator() entryMenu.addAction(self.demoAction) entryMenu.addSeparator() entryMenu.addAction(self.printAction) zoomMenu = menuBar.addMenu("Zoom") zoomMenu.addAction(self.zoomInAction) zoomMenu.addAction(self.zoomOutAction) ##self.enableActions(QItemSelection()) def initItemDelegate(self): delegate = EntryDelegate(self.constraintModel, self) self.ui.ganttView.leftView().setItemDelegate(delegate) def initGrid(self): self.grid = DateTimeGrid() self.grid.setDayWidth(self.dayWidth) self.ui.ganttView.setGrid(self.grid) def showContextMenu(self, pos): if not self.ui.ganttView.leftView().indexAt(pos).isValid(): self.ui.ganttView.selectionModel().clearSelection() menu = QMenu(self.ui.ganttView.leftView()) menu.addAction(self.newEntryAction) menu.addAction(self.removeEntryAction) menu.exec_(self.ui.ganttView.leftView().viewport().mapToGlobal(pos)) def enableActions(self, selected): if len(selected.indexes()) == 0: self.newEntryAction.setEnabled(True) self.removeEntryAction.setEnabled(False) return selectedIndex = selected.indexes()[0] dataType = self.model.data(self.model.index(selectedIndex.row(), 1)) if dataType in [KDGantt.TypeEvent, KDGantt.TypeTask]: self.newEntryAction.setEnabled(False) self.removeEntryAction.setEnabled(True) return self.newEntryAction.setEnabled(True) self.removeEntryAction.setEnabled(True) def addNewEntry(self): dialog = EntryDialog(self.model) dialog.setWindowTitle("New Entry") if dialog.exec_() == QDialog.Rejected: dialog = None return selectedIndexes = self.ui.ganttView.selectionModel().selectedIndexes() if len(selectedIndexes) > 0: parent = selectedIndexes[0] else: parent = QModelIndex() if not self.model.insertRow(self.model.rowCount(parent), parent): return row = self.model.rowCount(parent) - 1 if row == 0 and parent.isValid(): self.model.insertColumns(self.model.columnCount(paren), 5, parent) self.model.setData(self.model.index(row, 0, parent), dialog.name()) self.model.setData(self.model.index(row, 1, parent), dialog.type()) if dialog.type() != KDGantt.TypeSummary: self.model.setData(self.model.index(row, 2, parent), dialog.startDate(), KDGantt.StartTimeRole) self.model.setData(self.model.index(row, 3, parent), dialog.endDate(), KDGantt.EndTimeRole) self.model.setData(self.model.index(row, 4, parent), dialog.completion()) self.model.setData(self.model.index(row, 5, parent), dialog.legend()) self.addConstraint(dialog.depends(), self.model.index(row, 0, parent)) self.setReadOnly(self.model.index(row, 0, parent), dialog.readOnly()) dialog = None def setReadOnly(self, index, readOnly): row = index.row() parent = index.parent() for column in range(0, 5): item = self.model.itemFromIndex( self.model.index(row, column, parent)) flags = None if readOnly: flags = item.flags() & ~Qt.ItemIsEditable else: flags = item.flags() | Qt.ItemIsEditable item.setFlags(flags) def addConstraint(self, index1, index2): if not index1.isValid() or not index2.isValid(): return c = Constraint(index1, index2) self.ui.ganttView.constraintModel().addConstraint(c) def addConstraintFromItem(self, item1, item2): self.addConstraint(self.model.indexFromItem(item1), self.model.indexFromItem(item2)) def removeEntry(self): selectedIndexes = self.ui.ganttView.selectionModel().selectedIndexes() if len(selectedIndexes) > 0: index = selectedIndexes[0] else: index = QModelIndex() if not index.isValid(): return self.model.removeRow(index.row(), index.parent()) def addDemoEntry(self): softwareRelease = MyStandardItem("Software Release") codeFreeze = MyStandardItem("Code Freeze") codeFreeze.setData(KDGantt.TextPositionRole, StyleOptionGanttItem.Right) packaging = MyStandardItem("Packaging") upload = MyStandardItem("Upload") testing = MyStandardItem("Testing") updateDocumentation = MyStandardItem("Update Documentation") now = QDateTime.currentDateTime() softwareRelease.appendRow([ codeFreeze, MyStandardItem(KDGantt.TypeEvent), MyStandardItem(now, KDGantt.StartTimeRole) ]) softwareRelease.appendRow([ packaging, MyStandardItem(KDGantt.TypeTask), MyStandardItem(now.addDays(5), KDGantt.StartTimeRole), MyStandardItem(now.addDays(10), KDGantt.EndTimeRole) ]) softwareRelease.appendRow([ upload, MyStandardItem(KDGantt.TypeTask), MyStandardItem( now.addDays(10).addSecs(2 * 60 * 60), KDGantt.StartTimeRole), MyStandardItem(now.addDays(11), KDGantt.EndTimeRole) ]) softwareRelease.appendRow([ testing, MyStandardItem(KDGantt.TypeTask), MyStandardItem(now.addSecs(3 * 60 * 60), KDGantt.StartTimeRole), MyStandardItem(now.addDays(5), KDGantt.EndTimeRole) ]) softwareRelease.appendRow([ updateDocumentation, MyStandardItem(KDGantt.TypeTask), MyStandardItem(now.addSecs(3 * 60 * 60), KDGantt.StartTimeRole), MyStandardItem(now.addDays(3), KDGantt.EndTimeRole) ]) self.model.appendRow( [softwareRelease, MyStandardItem(KDGantt.TypeSummary)]) self.addConstraintFromItem(codeFreeze, packaging) self.addConstraintFromItem(codeFreeze, testing) self.addConstraintFromItem(codeFreeze, updateDocumentation) self.addConstraintFromItem(packaging, upload) self.addConstraintFromItem(testing, packaging) self.addConstraintFromItem(updateDocumentation, packaging) def zoomIn(self): self.dayWidth += 10 if self.dayWidth > 400: self.grid.setScale(DateTimeGrid.ScaleHour) self.grid.setDayWidth(self.dayWidth) def zoomOut(self): self.dayWidth -= 10 if self.dayWidth < 10: self.dayWidth = 10 if self.dayWidth <= 400: self.grid.setScale(DateTimeGrid.ScaleDay) self.grid.setDayWidth(self.dayWidth) def printPreview(self): preview = QLabel(self, Qt.Window) preview.setAttribute(Qt.WA_DeleteOnClose) preview.setScaledContents(True) preview.setWindowTitle("Print Preview") pix = QPixmap(1000, 300) pix.fill(Qt.white) p = QPainter(pix) p.setRenderHints(QPainter.Antialiasing) self.ui.ganttView.print_(p, pix.rect()) preview.setPixmap(pix) preview.show() def slotClicked(self, index): self.statusBar().showMessage( "(%d,%d,_,%s) clicked" % (index.row(), index.column(), index.model())) def slotDoubleClicked(self, index): self.statusBar().showMessage( "(%d,%d,_,%s) qreal clicked" % (index.row(), index.column(), index.model()))
class MainWindowUi(QMainWindow): """sets up ui properties of MainWindowUi class""" def __init__(self) -> None: """inits MainWindow class configuring parameters of MainWindow class and inherits from QtWidget.QMainWindow loads .ui file sets up file and directory path vars, inits click events(menuebar, coboboxes, btns) and shows gui the first time Returns: None""" super(MainWindowUi, self).__init__() self.setWindowTitle("It_Hilfe") self.resize(820, 450) self.setWindowIcon(QIcon("./data/favicon2.png")) self.setMinimumSize(700, 250) self.file_path = None self.dir = None self.last_open_file_path = None self.last_open_file_dir = None self.initial_theme = None self.registered_devices = {} self.user_config_file = "./data/user_config.json" # setup stackedwidget self.stacked_widget = QStackedWidget() self.setCentralWidget(self.stacked_widget) self.setup_menubar() self.setup_p_view() self.setup_p_register() self.setup_p_create() self.setup_p_preferences() self.setup_signals() self.font = QFont() self.font.setPointSize(9) self.validate(self.set_user_preferences, file_path=self.user_config_file, schema=validate_json.ItHilfeUserPreferencesSchema, forbidden=[""]) # setup statusbar self.statusbar = self.statusBar() self.stacked_widget.setCurrentWidget(self.p_view) def setup_menubar(self) -> None: """inits menubar Returns: None""" self.menu_Bar = self.menuBar() menu_file = self.menu_Bar.addMenu("file") self.action_open = QAction("open") self.action_save = QAction("save") self.action_new = QAction("new") self.action_print = QAction("print") self.action_preferences = QAction("preferences") self.action_hide_menu_bar = QAction("hide menubar") self.action_print.setShortcut(QKeySequence("Ctrl+p")) self.action_open.setShortcut(QKeySequence("Ctrl+o")) self.action_save.setShortcut(QKeySequence("Ctrl+s")) self.action_hide_menu_bar.setShortcut(QKeySequence("Ctrl+h")) self.action_hide_menu_bar.setIcon(QIcon("./data/show_hide.ico")) self.action_print.setIcon(QIcon("./data/print2.ico")) self.action_open.setIcon(QIcon("./data/open.ico")) self.action_save.setIcon(QIcon("./data/save.ico")) self.action_new.setIcon(QIcon("./data/newfile.ico")) self.action_preferences.setIcon(QIcon("./data/preferences.ico")) menu_file.addAction(self.action_open) menu_file.addAction(self.action_save) menu_file.addAction(self.action_new) menu_file.addAction(self.action_print) menu_file.addAction(self.action_preferences) menu_edit = self.menu_Bar.addMenu("edit") self.action_register = QAction("register") self.action_register.setShortcut(QKeySequence("Ctrl+n")) self.action_register.setIcon(QIcon("./data/register.ico")) menu_edit.addAction(self.action_register) menu_view = self.menu_Bar.addMenu("view") menu_view.addAction(self.action_hide_menu_bar) def setup_p_view(self) -> None: """inits stacked widget page widget Returns: None""" self.p_view = QtWidgets.QWidget() self.stacked_widget.addWidget(self.p_view) self.model = QStandardItemModel(self.p_view) self.model.setHorizontalHeaderLabels(labels) self.filters = [] source_model = self.model for filter_num in range(7): filter = QSortFilterProxyModel() filter.setSourceModel(source_model) filter.setFilterKeyColumn(filter_num) source_model = filter self.filters.append(filter) delegate = ComboDelegate() self.table = QtWidgets.QTableView(self.p_view) self.table.setModel(self.filters[-1]) self.table.setItemDelegateForColumn(2, delegate) self.table.horizontalHeader().setStretchLastSection(True) self.table.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.header = FilterHeader(self.table) self.header.set_filter_boxes() self.header.setMaximumHeight(50) self.table.setHorizontalHeader(self.header) self.bt_burger = QPushButton(self.p_view) self.bt_burger.setIcon(QIcon("./data/menu2.svg")) self.bt_burger.setIconSize(QSize(30, 30)) self.bt_burger.setToolTip('slide out description') l_burger = QLabel("menu", self.p_view) self.bt_register_new = QPushButton(self.p_view) self.bt_register_new.setIcon(QIcon("./data/add.ico")) self.bt_register_new.setIconSize(QSize(30, 30)) self.bt_register_new.setToolTip("register new") l_register_new = QLabel("register new", self.p_view) self.bt_delete_column = QPushButton(self.p_view) self.bt_delete_column.setIcon(QIcon("./data/remove.ico")) self.bt_delete_column.setIconSize(QSize(30, 30)) self.bt_delete_column.setToolTip( "delete columns with min 1 cell selected") l_delete = QLabel("delete column", self.p_view) self.bt_hide_show_filter = QPushButton(self.p_view) self.bt_hide_show_filter.setIcon(QIcon("./data/show_hide.ico")) self.bt_hide_show_filter.setIconSize(QSize(30, 30)) self.bt_hide_show_filter.setToolTip("hide/show filter input") l_hide_show = QLabel("hide/show", self.p_view) self.left_btn_frame = QFrame(self.p_view) self.left_btn_frame.setMaximumWidth(40) self.left_btn_frame.setContentsMargins(0, 0, 0, 0) self.left_menu_frame = QFrame(self.p_view) self.left_menu_frame.setMaximumWidth(0) self.left_menu_frame.setContentsMargins(0, 0, 0, 0) p_view_layout2 = QtWidgets.QVBoxLayout(self.left_btn_frame) p_view_layout2.addWidget(self.bt_burger) p_view_layout2.addWidget(self.bt_register_new) p_view_layout2.addWidget(self.bt_delete_column) p_view_layout2.addWidget(self.bt_hide_show_filter) p_view_layout2.setAlignment(Qt.AlignTop) p_view_layout2.setContentsMargins(0, 0, 0, 0) self.p_view_layout3 = QtWidgets.QVBoxLayout(self.left_menu_frame) self.p_view_layout3.addWidget(l_burger) self.p_view_layout3.addWidget(l_register_new) self.p_view_layout3.addWidget(l_delete) self.p_view_layout3.addWidget(l_hide_show) self.p_view_layout3.setAlignment(Qt.AlignTop | Qt.AlignCenter) self.p_view_layout3.setContentsMargins(0, 0, 0, 0) self.p_view_layout3.setSpacing(25) p_view_layout = QHBoxLayout(self.p_view) p_view_layout.setContentsMargins(0, 0, 0, 0) p_view_layout.addWidget(self.left_btn_frame) p_view_layout.addWidget(self.left_menu_frame) p_view_layout.addWidget(self.table) self.p_view.setLayout(p_view_layout) self.p_view.addAction(self.action_open) self.p_view.addAction(self.action_save) self.p_view.addAction(self.action_new) self.p_view.addAction(self.action_print) self.p_view.addAction(self.action_register) self.p_view.addAction(self.action_hide_menu_bar) def setup_p_register(self) -> None: """inits stacked widget page widgets Returns: None""" self.p_register = QtWidgets.QWidget() self.stacked_widget.addWidget(self.p_register) l_user = QtWidgets.QLabel("Username", self.p_register) self.in_username = QtWidgets.QLineEdit(self.p_register) l_devicename = QtWidgets.QLabel("Devicename", self.p_register) self.in_devicename = QtWidgets.QLineEdit(self.p_register) l_devicetype = QtWidgets.QLabel("DeviceType", self.p_register) self.in_combobox_devicetype = QtWidgets.QComboBox(self.p_register) l_os = QtWidgets.QLabel("OS", self.p_register) self.in_combobox_os = QtWidgets.QComboBox(self.p_register) l_comment = QtWidgets.QLabel("Comment", self.p_register) self.in_comment = QtWidgets.QTextEdit(self.p_register) self.bt_enter_register = QPushButton("register", self.p_register) self.bt_cancel_register = QPushButton("cancel", self.p_register) p_register_layout = QtWidgets.QVBoxLayout(self.p_register) p_register_layout.addWidget(l_user) p_register_layout.addWidget(self.in_username) p_register_layout.addWidget(l_devicename) p_register_layout.addWidget(self.in_devicename) p_register_layout.addWidget(l_devicetype) p_register_layout.addWidget(self.in_combobox_devicetype) p_register_layout.addWidget(l_os) p_register_layout.addWidget(self.in_combobox_os) p_register_layout.addWidget(l_comment) p_register_layout.addWidget(self.in_comment) p_register_layout.addWidget(self.bt_enter_register) p_register_layout.addWidget(self.bt_cancel_register) def setup_p_create(self) -> None: """inits stacked widget page widget Returns: None""" self.p_create = QtWidgets.QWidget() self.stacked_widget.addWidget(self.p_create) l_new_filepath = QtWidgets.QLabel("new filepath", self.p_create) self.bt_mod_new_path = QPushButton("mod filepath", self.p_create) self.in_new_filepath = QtWidgets.QLineEdit(self.p_create) l_new_filename = QtWidgets.QLabel("new filename", self.p_create) self.in_new_filename = QtWidgets.QLineEdit(self.p_create) self.bt_create = QPushButton("create", self.p_create) self.bt_cancel_create = QPushButton("cancel", self.p_create) p_create_layout = QtWidgets.QVBoxLayout(self.p_create) p_create_layout.addWidget(l_new_filepath) p_create_layout.addWidget(self.in_new_filepath) p_create_layout.addWidget(l_new_filename) p_create_layout.addWidget(self.in_new_filename) p_create_layout.addStretch(100) p_create_layout.addWidget(self.bt_mod_new_path) p_create_layout.addWidget(self.bt_create) p_create_layout.addWidget(self.bt_cancel_create) def setup_p_preferences(self) -> None: """inits setup_p_preferences stacked widget page widget Returns: None""" self.p_preferences = QWidget() self.p_preferences.resize(500, 250) self.p_preferences.setWindowTitle("preferences") self.list_Widget = QListWidget(self.p_preferences) self.list_Widget.addItems(["appearance", "about"]) self.list_Widget.setMaximumWidth(100) self.stacked_widget_preferences = QStackedWidget(self.p_preferences) # setup appearance self.apperence_widget = QWidget() self.stacked_widget_preferences.addWidget(self.apperence_widget) self.in_combo_themes = QComboBox(self.apperence_widget) self.in_combo_themes.addItems(["dark_theme", "light_theme"]) self.in_combo_theme_initial = QComboBox(self.apperence_widget) self.in_combo_theme_initial.addItems(["dark_theme", "light_theme"]) self.text_size_slider = QSlider(QtCore.Qt.Orientation.Horizontal, self.apperence_widget) self.text_size_slider.setTickPosition(QSlider.TickPosition.TicksAbove) self.text_size_slider.setMaximum(15) self.text_size_slider.setMinimum(8) stacked_widget_preferences_layout = QGridLayout(self.apperence_widget) stacked_widget_preferences_layout.setAlignment(QtCore.Qt.AlignTop) stacked_widget_preferences_layout.addWidget(QLabel("theme"), 0, 0) stacked_widget_preferences_layout.addWidget(self.in_combo_themes, 0, 1) stacked_widget_preferences_layout.addWidget(QLabel("initial theme"), 1, 0) stacked_widget_preferences_layout.addWidget( self.in_combo_theme_initial, 1, 1) stacked_widget_preferences_layout.addWidget(QLabel("Fontsize"), 2, 0) stacked_widget_preferences_layout.addWidget(self.text_size_slider, 2, 1) self.about_widget = QWidget() self.stacked_widget_preferences.addWidget(self.about_widget) about_text_edit = QTextEdit(self.about_widget) about_text_edit.setText( "developed by Maurice Jarck\nwith kind support from Shuai Lou\n07.2020-04.2021" ) about_text_edit.setEnabled(False) stacked_widget_about_layout = QGridLayout(self.about_widget) stacked_widget_about_layout.addWidget(about_text_edit) p_apperance_layout = QHBoxLayout(self.p_preferences) p_apperance_layout.addWidget(self.list_Widget) p_apperance_layout.addWidget(self.stacked_widget_preferences) def setup_signals(self) -> None: """connects signals Returns: None""" # header for filter, editor in zip(self.filters, self.header.editors): editor.textChanged.connect(filter.setFilterRegExp) # line edit self.in_new_filename.returnPressed.connect(lambda: self.validate( self.new, line_edit_list=[self.in_new_filepath, self.in_new_filename], data=False)) # comboboxes self.in_combobox_devicetype.addItems( ["choose here"] + [x.__name__ for x in valid_devices]) self.in_combobox_devicetype.currentIndexChanged.connect( lambda: self.update_combobox( self.in_combobox_os, valid_devices[ self.in_combobox_devicetype.currentIndex() - 1].expected_OS )) self.in_combo_themes.currentIndexChanged.connect( lambda: self.change_theme(self.in_combo_themes.currentText())) self.in_combo_theme_initial.currentTextChanged.connect(lambda: setattr( self, "initial_theme", self.in_combo_theme_initial.currentText())) # btns self.bt_delete_column.clicked.connect(self.delete) # self.bt_hide_show_filter.clicked.connect(lambda: self.toggle_hide_show_ani(37, 47, "height", self.header, b"maximumHeight")) self.bt_hide_show_filter.clicked.connect(self.header.hide_show) # self.bt_hide_show_filter.clicked.connect(lambda: self.toggle_hide_show_ani(30, 44, "height", self.header, b"maximumHeight")) self.bt_register_new.clicked.connect( lambda: self.stacked_widget.setCurrentWidget(self.p_register)) self.bt_enter_register.clicked.connect(lambda: self.validate( self.register, line_edit_list=[self.in_username, self.in_devicename], combo_box_list=[self.in_combobox_devicetype, self.in_combobox_os], forbidden=list(self.registered_devices.keys()), checkfname=True)) self.bt_create.clicked.connect(lambda: self.validate( self.new, line_edit_list=[self.in_new_filepath, self.in_new_filename], data=False)) self.bt_mod_new_path.clicked.connect(lambda: self.new(True)) self.bt_burger.clicked.connect(lambda: self.toggle_hide_show_ani( 0, 66, "width", self.left_menu_frame, b"maximumWidth", )) # menu bar self.action_register.triggered.connect( lambda: self.stacked_widget.setCurrentWidget(self.p_register)) self.action_open.triggered.connect(self.get_open_file_path) self.action_save.triggered.connect(self.save) self.action_new.triggered.connect(lambda: self.new(True)) self.action_print.triggered.connect( lambda: self.validate(self.print, data=False, checkfname=True)) self.action_hide_menu_bar.triggered.connect( lambda: self.toggle_hide_show(self.menu_Bar)) self.action_preferences.triggered.connect(self.p_preferences.show) # cancel self.bt_cancel_register.clicked.connect(lambda: self.cancel([ self.in_username, self.in_devicename, self.in_combobox_os, self. in_comment ])) # list widget self.list_Widget.currentRowChanged.connect( lambda: self.stacked_widget_preferences.setCurrentIndex( self.list_Widget.currentIndex().row())) # slider self.text_size_slider.sliderMoved.connect( lambda: self.change_font_size(self.text_size_slider.value())) # self.text_size_slider.sliderMoved.connect(lambda: print(self.text_size_slider.value())) def change_theme(self, theme) -> None: """changes theme according to combobox selection Returns: None""" with open(f"./data/{theme}.css", "r") as file: stylesheed = " ".join(file.readlines()) self.setStyleSheet(stylesheed) self.p_preferences.setStyleSheet(stylesheed) if self.in_combo_themes.currentText() == "dark_theme": self.left_btn_frame.setStyleSheet( u"background: #455364; border: 0px solid;") self.p_view_layout3.setSpacing(30) else: self.left_btn_frame.setStyleSheet( u"background: #ADADAD; border: 0px solid;") self.p_view_layout3.setSpacing(25) return self.in_combo_themes.currentText() def toggle_hide_show_ani(self, collapsed_val: int, expanded_val: int, actual: str, to_animate, property: bytes): """interpolates over a defined range of vales and sets it to a given property of a given widget""" if getattr(to_animate, actual)() == expanded_val: destination = collapsed_val else: destination = expanded_val print(getattr(to_animate, actual)(), destination) self.ani = QPropertyAnimation(to_animate, property) self.ani.setDuration(300) self.ani.setStartValue(getattr(to_animate, actual)()) self.ani.setEndValue(destination) self.ani.setEasingCurve(QEasingCurve.Linear) self.ani.start() def toggle_hide_show(self, widget: QWidget) -> None: """toggles visibiliy of a given widget Arg: widget: widget which is aimed to be hidden or shown Returs: None""" if widget.isVisible(): widget.hide() else: widget.show() def reopen_last_file(self) -> None: """asks for reopening of the last opened file""" if self.last_open_file_path != "" or self.last_open_file_path is not None: reopen_dialog = QMessageBox.question( self.p_view, "reopen last file?", "Do you want to reopen the last edited file?", QMessageBox.Yes | QMessageBox.No) if reopen_dialog == QMessageBox.Yes: self.file_path = self.last_open_file_path self.load() def change_font_size(self, size: int) -> None: """changes all font sizes""" self.font.setPointSize(size) self.menu_Bar.setFont(self.font) self.header.setFont(self.font) self.table.setFont(self.font) self.p_preferences.setFont(self.font) def set_user_preferences(self) -> None: """Reads user_config file and sets its propertys""" with open(self.user_config_file, "r") as config_file: data = dict(json.load(config_file)) self.last_open_file_path = data["last_open_file_path"] self.initial_theme = data['initial_theme'] self.change_font_size(data['font_size']) self.text_size_slider.setValue(data['font_size']) self.in_combo_theme_initial.setCurrentText(self.initial_theme) self.in_combo_themes.setCurrentText(self.initial_theme) with open(f"./data/{self.initial_theme}.css") as file: style_sheed = " ".join(file.readlines()) self.setStyleSheet(style_sheed) self.p_preferences.setStyleSheet(style_sheed) self.bt_burger.setStyleSheet( "border: 0px solid; background: transparent;") self.bt_register_new.setStyleSheet( "border: 0px solid; background: transparent;") self.bt_delete_column.setStyleSheet( "border: 0px solid; background: transparent;") self.bt_hide_show_filter.setStyleSheet( "border: 0px solid; background: transparent;") self.left_menu_frame.setStyleSheet(u" border: 0px solid;") if self.initial_theme == "dark_theme": self.left_btn_frame.setStyleSheet( u"background: #455364; border: 0px solid;") else: self.left_btn_frame.setStyleSheet( u"background: #ADADAD; border: 0px solid;") def cancel(self, widgets: list) -> None: """click event for all cancel buttons shows fist page in stacked widget and clears all widgets in widgets Args: widgets: defines list containing widgets to clear, only widgets with method .clear() are possible Returns: None""" for widget in widgets: widget.clear() self.stacked_widget.setCurrentWidget(self.p_view) def update_combobox(self, box, data: list) -> None: """ clears combo box updates combobox so that old content not needed any more isnt displayed and adds 'choose here' dummy to ensure an index change will be made (updating next box depends on index change) Args: box: instance of pyqt5.QtWidgets.qComboBox data: data supposed to be inserted into combobox Returns: None""" box.clear() box.addItems(["choose here"] + data) def validate(self, command, file_path: str = None, schema=None, line_edit_list: list = None, combo_box_list: list = None, data=None, forbidden: list = None, checkfname: bool = None) -> None: """validates user input Args: command: function to be called after vailidation process if finished line_edit_list: contents pyqt5.QtWidgets.QlineEdit instances to be checked if empty or current text in forbidden or not in allowed combo_box_list: contents pyqt5.QtWidgets.qComboBox instances to be checked if nothing selected data: data to be passed into command function if needed forbidden: houses keys which are not allowed to be entered checkfname: check weather an file path exists or not Returns: None""" fails = 0 if line_edit_list is not None: for x in line_edit_list: if x.text() == "": x.setText("fill all fields") fails += 1 if forbidden is not None and x.text() in forbidden: x.setText("in forbidden!!") fails += 1 if combo_box_list is not None: for combobox in combo_box_list: if combobox.currentText() == "": self.statusbar.showMessage("all comboboxes must be filled") fails += 1 if checkfname is True and self.file_path is None: self.statusbar.showMessage( "no file path specified, visit Ctrl+o or menuebar/edit/open to fix" ) fails += 1 if file_path is not None: if forbidden is not None and file_path in forbidden: fails += 1 self.statusbar.showMessage("select a file to continue") else: try: validate_json.validate(file_path, schema) except ValidationError as e: self.msg_box = QtWidgets.QMessageBox.critical( self, "validation failed", f"Invalid Json file, problem in: {e.messages}") fails += 1 if fails == 0: if data is None: command() else: command(data) else: message = f"problem\ncommand: {command.__name__}\nfails: {fails}" print(message) return message def register(self) -> None: """registers a new device and saves Returns: None""" logic.register(devname=self.in_devicename.text(), devtype=[ device for device in valid_devices if device.__name__ == self.in_combobox_devicetype.currentText() ].pop(), username=self.in_username.text(), os=self.in_combobox_os.currentText(), comment=self.in_comment.toPlainText(), datetime=str(datetime.datetime.now()), registered_devices=self.registered_devices) new_values = [ self.in_devicename.text(), self.in_username.text(), self.in_combobox_os.currentText(), [ device.__name__ for device in valid_devices if device.__name__ == self.in_combobox_devicetype.currentText() ].pop(), self.in_comment.toPlainText(), str(datetime.datetime.now()) ] row = [QStandardItem(str(item)) for item in new_values] self.model.appendRow(row) self.stacked_widget.setCurrentWidget(self.p_view) self.in_devicename.clear() self.in_username.clear() self.in_combobox_os.clear() self.in_comment.clear() self.save() def delete(self) -> None: """deletes all rows associated with min 1 slected cell Returns: None""" rows = sorted(set(index.row() for index in self.table.selectedIndexes()), reverse=True) qb = QMessageBox() answ = qb.question(self, 'delete rows', f"Are you sure to delete {rows} rows?", qb.Yes | qb.No) if answ == qb.Yes: for row in rows: self.registered_devices.pop( str(self.model.index(row, 0).data())) self.model.removeRow(row) qb.information(self, 'notification', f"deleted {rows} row") else: qb.information(self, 'notification', "Nothing Changed") self.save() def get_open_file_path(self) -> None: """gets file-path and set it to self.file_path, extra step for json validation Returns: None""" self.file_path = \ QFileDialog.getOpenFileName(self, "open file", f"{self.last_open_file_dir or 'c://'}", "json files (*json)")[0] self.validate(command=self.load, file_path=self.file_path, schema=validate_json.ItHilfeDataSchema, forbidden=[""]) def load(self) -> None: """opens json file and loads its content into registered devices Returns: None""" self.model.clear() self.registered_devices.clear() with open(self.file_path, "r") as file: data = dict(json.load(file)) devices = data["devices"].values() self.last_open_file_dir = data["last_open_file_dir"] for value in devices: row = [] for i, item in enumerate(value): cell = QStandardItem(str(item)) row.append(cell) if i == 0 or i == 3 or i == 5: cell.setEditable(False) self.model.appendRow(row) new = [x for x in valid_devices if x.__name__ == value[3]].pop(0)(value[0], value[1], value[4], value[5]) new.OS = value[2] self.registered_devices[value[0]] = new self.model.setHorizontalHeaderLabels(labels) self.statusbar.showMessage("") # auto complete for a in range(len(self.header.editors)): completer = QCompleter([ self.model.data(self.model.index(x, a)) for x in range(self.model.rowCount()) ]) completer.setCompletionMode(QCompleter.InlineCompletion) self.header.editors[a].setCompleter(completer) def save(self) -> None: """saves content fo self.registered_devices into specified json file Returns: None""" if not self.file_path: self.statusbar.showMessage( "no file path set all changes get lost if closed") else: with open( self.file_path, 'w', ) as file: devices = { k: [ v.name, v.user, v.OS, v.__class__.__name__, v.comment, v.datetime ] for (k, v) in enumerate(self.registered_devices.values()) } last_open_file_dir = "/".join(self.file_path.split("/")[:-1]) resulting_dict = { "devices": devices, "last_open_file_dir": last_open_file_dir } json.dump(resulting_dict, file) self.statusbar.showMessage("saved file") with open(self.user_config_file, "w") as user_preferences_file: json.dump( { "last_open_file_path": self.last_open_file_path, "initial_theme": self.initial_theme, "font_size": self.text_size_slider.value() }, user_preferences_file) def new(self, stage: bool, test: bool = False) -> None: """creates new csv file to save into stage is True: set filepath stage is False: set new name, save Args: stage: determines a which stage to execute this function Returns: None""" if stage is True: if not test: self.dir = QFileDialog.getExistingDirectory( self, "select a folder", "c://") self.stacked_widget.setCurrentWidget(self.p_create) self.in_new_filepath.setText(self.dir) self.registered_devices.clear() else: self.file_path = self.dir + f"/{self.in_new_filename.text()}.json" self.save() self.stacked_widget.setCurrentWidget(self.p_view) def print(self, test: bool) -> None: """setup and preview pViewTable for paper printing Returns: None""" with open(self.file_path) as f: self.data = json.dumps(dict(json.load(f)), sort_keys=True, indent=6, separators=(".", "=")) self.document = QtWidgets.QTextEdit() self.document.setText(self.data) if not test: printer = QPrinter() previewDialog = QPrintPreviewDialog(printer, self) previewDialog.paintRequested.connect( lambda: self.document.print_(printer)) previewDialog.exec_()
class FitablesModel(BaseModel): def __init__(self, parent=None): super().__init__(parent) self._log = logger.getLogger(self.__class__.__module__) # limits self._left_limit_for_zero_value = -1 self._right_limit_for_zero_value = 1 self._limit_percentage_deviation = 0.2 # minor properties self._first_role = Qt.UserRole + 1 self._edit_role_increment = 100 self._edit_role_name_suffix = 'Edit' # major properties self._model = QStandardItemModel() # set role names self._role_names_list = [ 'path', 'label', 'value', 'error', 'min', 'max', 'refine', 'unit', 'labelList' ] # 257 - 265 self._roles_list = [] self._roles_dict = {} self._setRolesListAndDict() self._model.setItemRoleNames(self._roles_dict) # connect signals self._model.dataChanged.connect(self.onModelChanged) def _setRolesListAndDict(self): """ Create the display and edit role list and dict from role names. """ for i, role_name in enumerate(self._role_names_list): display_role = self._first_role + i edit_role = display_role + self._edit_role_increment self._roles_dict[display_role] = role_name.encode() self._roles_dict[edit_role] = '{}{}'.format( role_name, self._edit_role_name_suffix).encode() self._roles_list.append(display_role) self._roles_list.append(edit_role) self._log.debug(f"roles: {self._roles_dict}") def _setModelsFromProjectDict(self): """ Create the initial data list with structure for GUI fitables table. """ self._log.info("Starting to set Model from Project Dict") # block model signals self._model.blockSignals(True) # reset model self._model.setColumnCount( 0) # faster than clear(); clear() crashes app! why? self._model.setRowCount(0) # sort background for experiment in self._project_dict['experiments'].keys(): self._project_dict['experiments'][experiment]['background'].sort() # set column column = [] for path in find_in_obj(self._project_dict.asDict(), 'refine'): keys_list = path[:-1] hide = self._project_dict.getItemByPath(keys_list + ['hide']) if hide: continue item = QStandardItem() for role, role_name_bytes in self._roles_dict.items(): role_name = role_name_bytes.decode() if role_name.endswith(self._edit_role_name_suffix): continue if role_name == 'path': value = keys_list elif role_name == 'labelList': value = keys_list[0:-1] # Insert elements according to CIF if len(value) > 0 and value[-1] == "offset": value.insert(-1, "setup") if len(value) > 0 and value[-1] == "wavelength": value.insert(-1, "setup") # Renaming according to CIF if len(value) > 0 and value[-1] == "offset": value[-1] = "offset_2theta" if len(value) > 2 and value[2] == "atoms": value[2] = "atom_site" if len(value) > 2 and value[2] == "resolution": value[2] = "pd_instr_resolution" if len(value) > 2 and value[2] == "polarization": value[2] = "diffrn_radiation" if len(value) > 2 and value[2] == "background": value.insert(3, "2theta") if len(value) > 5 and value[4] == "MSP": value[5] = "susceptibility_" + value[5] del value[4] elif role_name == 'label': value = ' '.join(keys_list[:-1]) elif role_name == 'value': value = self._project_dict.getItemByPath( keys_list[:-1]).value elif role_name == 'min': value = self._project_dict.getItemByPath(keys_list).min elif role_name == 'max': value = self._project_dict.getItemByPath(keys_list).max elif role_name == 'unit': # conversion to str is needed if role = unit ! value = str( nested_get(self._project_dict, keys_list + [role_name])) else: value = nested_get(self._project_dict, keys_list + [role_name]) item.setData(value, role) column.append(item) # set model self._model.appendColumn(column) # dataChanged is not emited. why? # unblock signals and emit model layout changed self._model.blockSignals(False) self._model.layoutChanged.emit() self._log.info("Finished setting Model from Project Dict") # Emit signal which is caught by the QStandardItemModel-based # QML GUI elements in order to update their views def _updateProjectByIndexAndRole(self, index, edit_role): """ Update project element, which is changed in the model, depends on its index and role. """ self._log.info("Starting updating Project Dict from Model") display_role = edit_role - self._edit_role_increment display_role_name = self._roles_dict[display_role].decode() path_role = self._role_names_list.index('path') + self._first_role keys_list = self._model.data(index, path_role) + [display_role_name] edit_value = self._model.data(index, edit_role) display_value = self._model.data(index, display_role) self._log.debug(f"edit_role: {edit_role}") self._log.debug(f"display_role: {display_role}") self._log.debug(f"display_role_name: {display_role_name}") self._log.debug(f"path_role: {path_role}") self._log.debug(f"keys_list: {keys_list}") self._log.debug(f"edit_value: {edit_value}") self._log.debug(f"display_value: {display_value}") fitable_name = '.'.join(keys_list[:-2]) fitable_value = edit_value data_block_name = keys_list[0] self._log.debug(f"fitable_name: {fitable_name}") self._log.debug(f"fitable_value: {fitable_value}") self._log.debug(f"data_block_name: {data_block_name}") if display_role_name == 'refine': undo_redo_text = f"Changing '{fitable_name}' refine state to '{fitable_value}'" self._log.debug(f"undo_redo_text: {undo_redo_text}") self._calculator_interface.project_dict.startBulkUpdate( undo_redo_text) self._calculator_interface.canUndoOrRedoChanged.emit() if data_block_name == 'phases': try: self._calculator_interface.setPhaseRefine( keys_list[1], keys_list[2:-2], edit_value) except AttributeError: # In this case the calculator/dict are out of phase :-/ So fallback to manual. self._calculator_interface.project_dict.setItemByPath( keys_list, edit_value) # self._calculator_interface.updatePhases() elif data_block_name == 'experiments': try: self._calculator_interface.setExperimentRefine( keys_list[1], keys_list[2:-2], edit_value) except AttributeError: self._calculator_interface.project_dict.setItemByPath( keys_list, edit_value) # self._calculator_interface.updateExperiments() else: self._calculator_interface.setDictByPath(keys_list, edit_value) self._calculator_interface.projectDictChanged.emit() elif display_role_name == 'value': undo_redo_text = f"Changing '{fitable_name}' to '{fitable_value:.4f}'" self._log.debug(f"undo_redo_text: {undo_redo_text}") self._calculator_interface.project_dict.startBulkUpdate( undo_redo_text) self._calculator_interface.canUndoOrRedoChanged.emit() if data_block_name == 'phases': try: self._calculator_interface.setPhaseValue( keys_list[1], keys_list[2:-2], edit_value) except AttributeError: self._calculator_interface.project_dict.setItemByPath( keys_list, edit_value) self._calculator_interface.updatePhases() self._calculator_interface.updateCalculations( ) # phases also updated ? elif data_block_name == 'experiments': try: self._calculator_interface.setExperimentValue( keys_list[1], keys_list[2:-2], edit_value) except AttributeError: self._calculator_interface.project_dict.setItemByPath( keys_list, edit_value) self._calculator_interface.updateExperiments() self._calculator_interface.updateCalculations( ) # experiments also updated ? else: self._calculator_interface.setDictByPath(keys_list, edit_value) # Update min and max if value is outside [min, max] range value = self._calculator_interface.project_dict.getItemByPath( keys_list) min_value = self._calculator_interface.project_dict.getItemByPath( [*keys_list[:-1], 'min']) max_value = self._calculator_interface.project_dict.getItemByPath( [*keys_list[:-1], 'max']) self._log.debug(f"initial min: {min}, max: {max}") # TODO: the code below duplicates the code from BaseClasses.py - class Base - def updateMinMax # stacked changes (for GUI triggered changes) if np.isclose([value], [0]): min_value = self._left_limit_for_zero_value max_value = self._right_limit_for_zero_value if value < min_value: if value > 0: min_value = value * (1 - self._limit_percentage_deviation) else: min_value = value * (1 + self._limit_percentage_deviation) if value > max_value: if value > 0: max_value = value * (1 + self._limit_percentage_deviation) else: max_value = value * (1 - self._limit_percentage_deviation) # Update min and max in project dict self._log.debug(f"re-calculated min: {min}, max: {max}") self._calculator_interface.project_dict.setItemByPath( [*keys_list[:-1], 'min'], min_value) self._calculator_interface.project_dict.setItemByPath( [*keys_list[:-1], 'max'], max_value) self._calculator_interface.projectDictChanged.emit() elif display_role_name == 'min' or display_role_name == 'max': undo_redo_text = f"Changing '{fitable_name}' {display_role_name} to '{fitable_value}'" self._log.debug(f"undo_redo_text: {undo_redo_text}") self._calculator_interface.project_dict.startBulkUpdate( undo_redo_text) self._calculator_interface.canUndoOrRedoChanged.emit() # TODO: try to use setDictByPath below # self._calculator_interface.setDictByPath(keys_list, edit_value) # Temporary (?) solution until above is fixed self._calculator_interface.project_dict.setItemByPath( keys_list, edit_value) self._calculator_interface.projectDictChanged.emit() else: self._log.warning(f"unsupported role: {display_role_name}") return self._calculator_interface.project_dict.endBulkUpdate() self._calculator_interface.canUndoOrRedoChanged.emit() self._log.info("Finished updating Project Dict from Model") def onModelChanged(self, top_left_index, bottom_right_index, roles): """ Define what to do if model is changed, e.g. from GUI. """ role = roles[0] role_name = self._roles_dict[role].decode() self._log.debug(f"roles: {roles}") self._log.debug(f"role: {role}") self._log.debug(f"role_name: {role_name}") if role_name.endswith(self._edit_role_name_suffix): self._updateProjectByIndexAndRole(top_left_index, role)