def set_intervals(self, intervals): self.timers.clear() self.intervals = intervals for val in intervals: timer = QTimer(self) timer.setSingleShot(True) timer.setInterval(val * 1000) timer.setTimerType(Qt.PreciseTimer) timer.timeout.connect(self.target) if (val == intervals[-1]): timer.timeout.connect(self.callback) self.timers.append(timer)
class IdleDetection(QObject): def __init__(self, parent): super(IdleDetection, self).__init__(parent) self._parent: QWidget = parent # Report user inactivity self._idle_timer = QTimer() self._idle_timer.setSingleShot(True) self._idle_timer.setTimerType(Qt.VeryCoarseTimer) self._idle_timer.setInterval(10000) # Detect inactivity for automatic session save self._idle_timer.timeout.connect(self.set_inactive) self.idle = False self.parent.installEventFilter(self) def is_active(self): return self.idle def set_active(self): self.idle = False self._idle_timer.stop() def set_inactive(self): self.idle = True def eventFilter(self, obj, eve): if eve is None or obj is None: return False if eve.type() == QEvent.KeyPress or \ eve.type() == QEvent.MouseMove or \ eve.type() == QEvent.MouseButtonPress: self.set_active() return False if not self._idle_timer.isActive(): self._idle_timer.start() return False
class ViewerApp(QApplication): idle_event = Signal() def __init__(self, version: str): super(ViewerApp, self).__init__(sys.argv) self.setApplicationName(f'{APP_NAME}') self.setApplicationVersion(version) self.setApplicationDisplayName(f'{APP_NAME} v{version}') load_style(self) self.idle_timer = QTimer() self.idle_timer.setSingleShot(True) self.idle_timer.setTimerType(Qt.VeryCoarseTimer) self.idle_timer.setInterval(3 * 60 * 1000) # 3 min until idle self.idle_timer.timeout.connect(self.set_idle) self.installEventFilter(self) self.window = ViewerWindow(self) self.window.show() def eventFilter(self, obj, eve): if eve is None or obj is None: return False if eve.type() == QEvent.KeyPress or \ eve.type() == QEvent.MouseMove or \ eve.type() == QEvent.MouseButtonPress: self.set_active() return False return False def set_active(self): self.idle_timer.start() def set_idle(self): LOGGER.debug('Application is idle.') self.idle_event.emit()
class MainWindow(QMainWindow): def __init__(self, app, state: State, screenFPS, fixedFPS, parent=None): """ The MainWindow is created by the application, here we handle ui/scenes and send out updates. :param state: The current state we want to start with. :param parent: The widget this held under, None by default because this is top widget """ QMainWindow.__init__(self, parent) self.app = app # the application created self.gameScreen = None # where all graphics are drawn self.uiManager = UiManager(self, customWidgets=[ GameScreen ]) # Loads widgets into window from a file self.stateManager = StateManager( self, state) # Manages state updates and transitions self.updateTimer = QTimer(self) # Used to create update calls self.updateTimer.setTimerType(Qt.PreciseTimer) self.updateTimer.timeout.connect(self.__updateHandler) self.updateFPSTimer = QTimer(self) # Used to manage frame timings self.updateFPSTimer.timeout.connect(self.__calculateFPS) self.lastScreen = 0 # Last frame time self.lastFixed = 0 self.fixedFps = 0 # Calculated FPS self.screenFps = 0 self.fixedTime = 0 # Accumulated Time self.screenTime = 0 self.fixedFrames = 1 # Accumulated Frames self.screenFrames = 1 self.targetfixedFPS = fixedFPS self.targetscreenFPS = screenFPS self.fixedTiming = 1 / fixedFPS # seconds / frames per second self.screenTiming = 1 / screenFPS self.avgscreenTime = 0 # Average amount of time it takes the screen to update includes state update def start(self): """ Called to start the window :return: None """ self.stateManager.start() # start the state self.updateTimer.start(0) # start game loops self.updateFPSTimer.start(150) # start frame timing management def __updateHandler(self): """ In here fixed and scene updates are made based on timing :return: None """ # TODO pref counter start = time.clock() # start time of the update call if time.clock( ) - self.lastFixed > self.fixedTiming: # Has enough time passed for next call self.__fixedUpdate() self.fixedFrames += 1 now = time.clock() # Time after physics has been calculated if (now - start) < (self.fixedTiming - self.avgscreenTime ): # Is there enough time left to call update if (now - self.lastScreen ) >= self.screenTiming: # Has enough time passed for next call if self.gameScreen is not None: self.gameScreen.update() self.__stateUpdate() self.screenFrames += 1 self.avgscreenTime = ( self.avgscreenTime + (time.clock() - now)) / 2 # How long the state update took def __stateUpdate(self): self.screenTime += 1 / (time.clock() - self.lastScreen) self.lastScreen = time.clock() self.stateManager.update() def __fixedUpdate(self): self.fixedTime += 1 / (time.clock() - self.lastFixed) self.lastFixed = time.clock() self.stateManager.fixedUpdate() def closeEvent(self, event: PySide2.QtGui.QCloseEvent): """ Called when the window its closed :return: None """ print("Window closed.") self.updateTimer.stop() self.updateFPSTimer.stop() self.stateManager.exit() def __calculateFPS(self): """ Averages FPS and adjust frame timings :return: None """ self.fixedFps = self.fixedTime / self.fixedFrames self.fixedTime = 0 self.fixedFrames = 1 self.screenFps = self.screenTime / self.screenFrames self.screenTime = 0 self.screenFrames = 1 if self.fixedFps < self.targetfixedFPS: self.fixedTiming *= 0.99 elif self.fixedFps > self.targetfixedFPS: self.fixedTiming *= 1.01 if self.screenFps < self.targetscreenFPS: self.screenTiming *= 0.99 elif self.screenFps > self.targetscreenFPS: self.screenTiming *= 1.01 def resizeEvent(self, event: PySide2.QtGui.QResizeEvent): self.gameScreen: GameScreen if self.gameScreen is not None: self.gameScreen.setGeometry(0, 0, self.width(), self.height())
class DataView(QWidget): def __init__(self, main_window, experiment_data): super(DataView, self).__init__() self.file_name = None self.main_window = main_window self.experiment_data = experiment_data self.condition_dataframe = None self.maximum_time_sec = None self.maximum_time_msec = None self.timer = QTimer(self) self.behavioralView = BehavioralView() self.eyetrackingView = EyeTrackingView(main_window) self.physioRespiration = PsychoPhysiologicalView('Respiration') self.physioHeartRate = PsychoPhysiologicalView('HeartRate') self.physioPupilDilation = PsychoPhysiologicalView('PupilDilation') self.fMRIRoiView = fMRIRoiView() self.fMRIFullView = fMRIFullView() self.create_layout() # initialize view self.stimuli_changed(0) def create_layout(self): left_layout = self.create_layout_left() right_layout = self.create_layout_right() bottom_layout = self.create_layout_bottom() center_layout = QHBoxLayout() center_layout.addLayout(left_layout) center_layout.addLayout(right_layout) main_layout = QVBoxLayout(self) main_layout.addLayout(center_layout) main_layout.addLayout(bottom_layout) def create_layout_left(self): left_layout = QVBoxLayout() self.participant_label = QLabel("Participant: " + self.experiment_data['participant']) self.participant_label.setFont(QtGui.QFont("Times", 16, QtGui.QFont.Bold)) self.participant_label.setTextInteractionFlags(Qt.TextBrowserInteraction) left_layout.addWidget(self.participant_label) left_stimuli_layout = QHBoxLayout() stimuli_label = QLabel("Stimuli: ") stimuli_label.setFont(QtGui.QFont("Times", 16, QtGui.QFont.Bold)) left_stimuli_layout.addWidget(stimuli_label) self.stimuli_selection_box = QComboBox() self.stimuli_selection_box.setFont(QtGui.QFont("Times", 14, QtGui.QFont.Normal)) for condition in self.experiment_data['conditions']: self.stimuli_selection_box.addItem(condition) self.stimuli_selection_box.currentIndexChanged[int].connect(self.stimuli_changed) left_stimuli_layout.addWidget(self.stimuli_selection_box) left_stimuli_layout.addStretch() left_layout.addLayout(left_stimuli_layout) left_layout.addStretch() if config.PLUGIN_EYETRACKING_ACTIVE: self.eyetrackingView.create_view(left_layout) left_layout.addStretch() if config.PLUGIN_BEHAVORIAL_ACTIVE: self.behavioralView.create_view(left_layout) return left_layout def create_layout_bottom(self): bottomLayout = QHBoxLayout() self.time = 0 self.timeLabel = QLabel("0:00.00 / 0:00.00") self.timeLabel.setFont(QtGui.QFont("Times", 14, QtGui.QFont.Normal)) self.slider = QtWidgets.QSlider(QtCore.Qt.Horizontal) self.slider.setRange(0, 99) self.slider.setValue(0) self.slider.setTickInterval(100) self.slider.setTickPosition(QtWidgets.QSlider.TicksBelow) self.slider.setToolTip("Time") self.slider.sliderMoved.connect(self.set_time) self.playButton = QPushButton("Play") self.playButton.clicked.connect(self.play_data) self.isPlaying = False bottomLayout.addStretch() bottomLayout.addWidget(self.playButton) playerLayout = QVBoxLayout() timeLayout = QHBoxLayout() timeLayout.addStretch() timeLayout.addWidget(self.timeLabel) timeLayout.addStretch() playerLayout.addLayout(timeLayout) playerLayout.addWidget(self.slider) bottomLayout.addLayout(playerLayout) bottomLayout.addStretch() return bottomLayout def create_layout_right(self): right_layout = QVBoxLayout() if config.PLUGIN_PHYSIO_ACTIVE: physio_label = QLabel("Psycho-Physiological Data") physio_label.setFont(QtGui.QFont("Times", 16, QtGui.QFont.Bold)) right_layout.addWidget(physio_label) physio_label_layout = QHBoxLayout() physio_plot_layout = QHBoxLayout() self.physioHeartRate.create_view(physio_label_layout, physio_plot_layout, self.experiment_data['participant']) self.physioRespiration.create_view(physio_label_layout, physio_plot_layout, self.experiment_data['participant']) self.physioPupilDilation.create_view(physio_label_layout, physio_plot_layout, self.experiment_data['participant']) right_layout.addLayout(physio_label_layout) right_layout.addLayout(physio_plot_layout) if config.PLUGIN_FMRI_ACTIVE: self.fMRIRoiView.create_view(right_layout, self.experiment_data) self.fMRIFullView.create_view(right_layout, self.experiment_data) right_layout.addStretch(1) return right_layout def set_time(self, time): self.time = time self.update_data(force_update=True) def stimuli_changed(self, index): selected_condition = self.experiment_data['conditions'][index] self.condition_dataframe = self.experiment_data['dataframe'][self.experiment_data['dataframe']['Condition'] == selected_condition] self.condition_start_pos = self.condition_dataframe['Time'].iloc[0] self.condition_end_pos = self.condition_dataframe['Time'].iloc[-1] self.condition_dataframe.reset_index(inplace=True, drop=True) self.maximum_time_sec = math.floor(len(self.condition_dataframe) / 100) self.maximum_time_msec = len(self.condition_dataframe) % 100 self.time = 0 self.shifted_scan = None self.slider.setMaximum(self.maximum_time_sec * 100 + self.maximum_time_msec) self.slider.setValue(0) self.timeLabel.setText("0:00.00 / 0:" + str(self.maximum_time_sec).zfill(2) + "." + str(self.maximum_time_msec).zfill(2)) # update plug-in views self.behavioralView.update_view(self.experiment_data, selected_condition, self.condition_start_pos) self.eyetrackingView.setConditionDataframe(selected_condition, self.condition_dataframe) self.update_data() def play_data(self): if self.isPlaying: self.isPlaying = False self.timer.stop() self.playButton.setText('Play') else: self.timer.setTimerType(QtCore.Qt.TimerType.PreciseTimer) self.timer.timeout.connect(self.update_data) self.timer.start(10) self.isPlaying = True self.playButton.setText('Stop') def update_data(self, force_update=False): # check if timer should stop if self.time >= (self.maximum_time_sec * 100 + self.maximum_time_msec): self.timer.stop() self.isPlaying = False self.playButton.setText('Play') else: self.slider.setValue(self.time) self.timeLabel.setText("0:" + str(math.floor(self.time / 100)).zfill(2) + "." + str(self.time % 100).zfill(2) + " / 0:" + str(self.maximum_time_sec).zfill(2) + "." + str(self.maximum_time_msec).zfill(2)) # psycho-physiological data if force_update or self.time % 10 == 0: self.physioRespiration.update_text(self.condition_dataframe['Respiration'][self.time]) self.physioHeartRate.update_text(self.condition_dataframe['HeartRate'][self.time]) self.physioPupilDilation.update_text(self.condition_dataframe['PupilDilation'][self.time]) # only update plots every second instead of every millisecond (for performance) # use preprocessed plots and just switch image for faster speeds if force_update or self.time % 100 == 0: self.physioRespiration.update_plot(self.time) self.physioHeartRate.update_plot(self.time) self.physioPupilDilation.update_plot(self.time) # eye-tracking data self.eyetrackingView.setTime(self.time) # fMRI data current_scan = math.floor(((self.condition_start_pos + self.time) / 100) * config.FMRI_RESOLUTION) shifted_scan = current_scan + SHIFT_FMRI_SCAN if self.shifted_scan != shifted_scan: self.fMRIRoiView.update_data(self.experiment_data, current_scan) self.fMRIFullView.update_data(self.experiment_data, shifted_scan) self.shifted_scan = shifted_scan # todo: it should only add 1ms per tick, for now artificially jump a few ms to fix slow replay time self.time += 4 # my Macbook is really slow # self.time += 2 # windows PC is faster def dialog(self): msg_box = QMessageBox() msg_box.setText("Not supported yet.") msg_box.setStandardButtons(QMessageBox.Ok) msg_box.exec_()
class KnechtSession(QObject): session_zip = CreateZip.settings_dir / 'Session_data.zip' files_list_name = 'session_files.json' class FileNameStorage: def __init__(self): self.store = { 'example_filename.xml': {'file': Path(), 'order': 0, 'clean_state': False} } self.store = dict() self.dupli_count = 0 def file_names(self) -> List[str]: return [file_entry['file'].name for file_entry in self.store.values()] def add_file(self, file: Path, tab_order: int, clean: bool) -> Path: # Rename duplicate file names if file.name in self.store: self.dupli_count += 1 file = file.parent / Path(file.stem + f'_{self.dupli_count:01d}' + file.suffix) # Add entry self.store[file.name] = { 'file': file.as_posix(), 'order': tab_order, 'clean_state': clean } return file def restore_file_order(self, load_dir: Path) -> List[Tuple[Path, bool]]: """ Restore the order in which the files have been saved """ file_ls = list() files_names_to_restore = [f.name for f in load_dir.glob('*.xml')] for file_entry in self.sort_storage_dict_entries(self.store): file = Path(file_entry.get('file') or '') if file.name in files_names_to_restore: file_ls.append(file) return file_ls[::-1] @staticmethod def sort_storage_dict_entries(d) -> List[dict]: entry_list, order_list = list(), list() for k, entry in d.items(): if not isinstance(entry, dict): continue order = entry.get('order') or 0 order_list.append(order) insert_idx = bisect(sorted(order_list), order) - 1 entry_list.insert(insert_idx, d[k]) return entry_list def __init__(self, ui, idle_save: bool=False): """ Save and restore user session of opened documents :param modules.gui.main_ui.KnechtWindow ui: The main UI window :param bool idle_save: auto save session when UI is idle """ super(KnechtSession, self).__init__(ui) self.restore_files_storage = self.FileNameStorage() self.load_save_mgr = SaveLoadController(self, create_recent_entries=False) # Load models into views and associate original save file path self.load_save_mgr.model_loaded.connect(self.model_loaded) # We will silently ignore load errors on session restore self.load_save_mgr.load_aborted.connect(self._load_next) self.idle_timer = QTimer() self.save_timer = QTimer() self.idle = False if idle_save: self.idle_timer.setSingleShot(True) self.save_timer.setSingleShot(True) self.idle_timer.setTimerType(Qt.VeryCoarseTimer) self.save_timer.setTimerType(Qt.VeryCoarseTimer) self.idle_timer.setInterval(10000) self.save_timer.setInterval(10000) # Detect inactivity for automatic session save self.idle_timer.timeout.connect(self.set_inactive) self.save_timer.timeout.connect(self.auto_save) ui.app.installEventFilter(self) self.ui = ui self.load_dir = None self.load_queue = list() def set_active(self): self.idle = False self.idle_timer.start() def set_inactive(self): self.idle = True self.save_timer.start() LOGGER.debug('Idling.') def eventFilter(self, obj, eve): if eve is None or obj is None: return False if eve.type() == QEvent.KeyPress or \ eve.type() == QEvent.MouseMove or \ eve.type() == QEvent.MouseButtonPress: self.set_active() return False return False def _load_next(self): if not self.load_queue: self.restore_finished() return file = self.load_queue.pop() file = self.load_dir / file.name self.load_save_mgr.open(file) @Slot(KnechtModel, Path) def model_loaded(self, model: KnechtModel, file: Path): LOGGER.debug('Restoring: %s', file.name) # Update progress view = self.ui.view_mgr.current_view() view.progress_msg.hide_progress() clean_state = True # Restore original save path if file.name in self.restore_files_storage.store: if isinstance(self.restore_files_storage.store[file.name], dict): file = Path(self.restore_files_storage.store[file.name].get('file') or '') clean_state = self.restore_files_storage.store[file.name].get('clean_state') else: file = Path(self.restore_files_storage.store[file.name]) if file.name == 'Variants_Tree.xml': # Update Variants Tree update_model = UpdateModel(self.ui.variantTree) update_model.update(model) new_view = self.ui.variantTree else: # Create a new view inside a new tab or load into current view if view model is empty new_view = self.ui.view_mgr.create_view(model, file) # Refresh model data new_view.model().sourceModel().initial_item_id_connection() new_view.model().sourceModel().refreshData() # Mark document non-clean if isinstance(clean_state, bool): if not clean_state: new_view.undo_stack.resetClean() self._load_next() def auto_save(self): if self.load_queue or not self.idle: return result = self.save() if result: self.ui.statusBar().showMessage(_('Sitzung während Leerlauf erfolgreich gespeichert')) def save(self) -> bool: tmp_dir = CreateZip.create_tmp_dir() storage = self.FileNameStorage() result = True documents_list = list() documents_list.append( (self.ui.variantTree, Path('Variants_Tree.xml')) ) for widget, file in zip(self.ui.view_mgr.file_mgr.widgets, self.ui.view_mgr.file_mgr.files): if hasattr(widget, 'user_view'): documents_list.append( (widget.user_view, file) ) for view, file in documents_list: if not view.model().rowCount() or not file: continue tab_order_index = self.get_tab_order(file) file = storage.add_file(file, tab_order_index, view.undo_stack.isClean()) # Save document tmp_file = tmp_dir / file.name r, _ = self.load_save_mgr.save(tmp_file, view) if not r: result = False del storage.store[file.name] else: LOGGER.debug('Saved session document: %s', tmp_file.name) # Save original file paths stored in Files class Settings.save(storage, tmp_dir / self.files_list_name) if not CreateZip.save_dir_to_zip(tmp_dir, self.session_zip): result = False CreateZip.remove_dir(tmp_dir) return result def get_tab_order(self, file: Path): """ Find the current file in the TabWidgets TabBar """ current_tab_text = '' for tab_idx in range(0, self.ui.view_mgr.tab.count()): tab_file = self.ui.view_mgr.file_mgr.get_file_from_widget(self.ui.view_mgr.tab.widget(tab_idx)) if tab_file == file: current_tab_text = self.ui.view_mgr.tab.tabText(tab_idx) for tab_bar_idx in range(0, self.ui.view_mgr.tab.tabBar().count()): if self.ui.view_mgr.tab.tabBar().tabText(tab_bar_idx) == current_tab_text: break else: tab_bar_idx = 0 return tab_bar_idx def restore(self) -> bool: """ Restore a user session asynchronous """ if not path_exists(self.session_zip): return False self.load_dir = CreateZip.create_tmp_dir() try: with ZipFile(self.session_zip, 'r') as zip_file: zip_file.extractall(self.load_dir) except Exception as e: LOGGER.error(e) return False # Restore original file save paths Settings.load(self.restore_files_storage, self.load_dir / self.files_list_name) # Restore in saved order for file in self.restore_files_storage.restore_file_order(self.load_dir): LOGGER.debug('Starting restore of document: %s @ %s', file.name, self.restore_files_storage.store.get(file.name)) self.load_queue.append(file) self._load_next() return True def restore_finished(self): CreateZip.remove_dir(self.load_dir) LOGGER.debug('Session restored.') self.ui.statusBar().showMessage(_('Sitzungswiederherstellung abgeschlossen'), 8000) self.ui.view_mgr.tab.setCurrentIndex(0)
class PlumbingBridge(QObject): engineLoaded = Signal(top.PlumbingEngine) dataUpdated = Signal(dict, np.ndarray) pausedChanged = Signal() def __init__(self): QObject.__init__(self) self.engine = None self.step_size = 0.05e6 # TODO(jacob): Add a UI field for this. self.timer = QTimer() self.timer.setTimerType(Qt.PreciseTimer) self.timer.timeout.connect(self.step_time) self._paused = True def load_engine(self, new_engine): self.engine = new_engine self.engineLoaded.emit(new_engine) self.timeStop() def load_from_files(self, filepaths): parser = top.pdl.Parser(filepaths) new_engine = parser.make_engine() self.load_engine(new_engine) def step_time(self): pressures = self.engine.step(self.step_size) self.dataUpdated.emit(pressures, np.array([self.engine.time])) def set_paused(self, should_pause): if should_pause: self.timer.stop() self._paused = should_pause self.pausedChanged.emit() def get_paused(self): return self._paused # `paused` is both a Python property and a Qt property, which means # we can directly treat it as a variable. Writing `paused = True` # will call set_paused(True). paused = Property(bool, get_paused, set_paused, notify=pausedChanged) @Slot() def loadFromDialog(self): # TODO(jacob): Make this menu remember the last file opened filepaths, _ = QFileDialog.getOpenFileNames(self.parent(), 'Load PDL files') if len(filepaths) > 0: self.load_from_files(filepaths) # Time controls @Slot() def timePlayBackwards(self): pass @Slot() def timeStepBackwards(self): pass @Slot() def timePlay(self): self.paused = False self.timer.start(50) @Slot() def timePause(self): self.paused = True @Slot() def timeStop(self): self.paused = True self.engine.reset() self.dataUpdated.emit(self.engine.current_pressures(), np.array([self.engine.time])) @Slot() def timeStepForward(self): self.paused = True self.step_time() @Slot() def timeAdvance(self): self.paused = True initial_time = self.engine.time states = self.engine.solve(return_resolution=self.step_size) # TODO(jacob/wendi): Change the data format returned by # PlumbingEngine.solve so we don't need to rearrange the data on # this side. pressures = {node: [] for node in states[0]} for state in states: for node, pressure in state.items(): pressures[node].append(pressure) pressures_np = { node: np.array(pvals) for node, pvals in pressures.items() } times = np.arange( len(states)) * self.step_size + initial_time + self.step_size self.dataUpdated.emit(pressures_np, times)
class OverlayWidget(QWidget): def __init__(self): super().__init__() self.setWindowTitle('pointout canvas') self.setWindowFlags(self.windowFlags() | Qt.Window | Qt.WindowTransparentForInput | Qt.WindowDoesNotAcceptFocus | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) self.setAttribute(Qt.WA_TransparentForMouseEvents) self.setAttribute(Qt.WA_NoSystemBackground) self.setAttribute(Qt.WA_TranslucentBackground) self.setAttribute(Qt.WA_TabletTracking) self.scribbles = [] self.current_wet = Overlay() self.undo_stack = [] cursor_bitmap = QBitmap.fromData( QSize(5, 5), bytes(( 0b00000, 0b00000, 0b00100, 0b00000, 0b00000, ))) mask_bitmap = QBitmap.fromData( QSize(5, 5), bytes(( 0b00100, 0b00000, 0b10101, 0b00000, 0b00100, ))) self.setCursor(QCursor(cursor_bitmap, mask_bitmap)) self.anim_timer = QTimer() self.anim_timer.timeout.connect(self.anim_update) self.anim_timer.start(1000 // 30) self.anim_timer.setTimerType(Qt.CoarseTimer) self.wet_end = time.monotonic() self.tools = { 'marker': Marker(), 'highlighter': Highlighter(), 'eraser': Eraser(), 'red': ColorMarker(1, 0, 0), 'green': ColorMarker(0, 1, 0), 'blue': ColorMarker(0, 0, 1), 'yellow': ColorMarker(1, 1, 0), 'purple': ColorMarker(1, 0, 1), 'cyan': ColorMarker(0, 1, 1), } self.tool = self.tools['marker'] def _handle_timeout(self): self.anim_update() if self.scribble_parts: del self.scribble_parts[0] self.scribble_parts.append(Overlay()) if self.current_part: for part in self.scribble_parts: part.add(self.current_part) if not self.scribbles: self.scribbles.append(Overlay()) self.scribbles[-1].add(self.current_part) self.current_part = None self.anim_update() def anim_update(self): if self.current_wet and self.current_wet.rect is not None: with self.current_wet.painter_context() as painter: painter.setBrush(QColor(0, 0, 0, 255)) painter.setPen(QPen(0)) painter.setOpacity(0.1) painter.setCompositionMode( QPainter.CompositionMode_DestinationOut) painter.drawRect(self.current_wet.rect) self.update(self.current_wet.rect) if self.wet_end < time.monotonic(): self.current_wet = Overlay() def paintEvent(self, e): painter = QPainter(self) painter.setOpacity(0.5) canvas = None for scribble in self.scribbles: if scribble.rect: if scribble.blend_with_next: if canvas is None: canvas = Overlay() canvas.add(scribble) else: if canvas: canvas.add(scribble) canvas.paint(painter) canvas = None else: scribble.paint(painter) if canvas: canvas.paint(painter) painter.setOpacity(1) if self.current_wet: self.current_wet.paint(painter) painter.end() def tabletEvent(self, e): #print(e.posF(), e.device(), hex(e.buttons()), e.pointerType(), e.pressure(), e.rotation(), e.xTilt(), e.yTilt()) e.accept() if e.type() == QEvent.TabletPress: self.last_point = e.posF() if self.current_wet.rect and self.scribbles: self.scribbles[-1].blend_with_next = True self.scribbles.append(Overlay()) if e.type() in (QEvent.TabletMove, QEvent.TabletRelease): tool = self.tool if self.tool is None: return if e.pointerType() == QTabletEvent.Eraser: tool = self.tools['eraser'] if not self.scribbles: self.scribbles.append(Overlay()) if self.last_point: tool.set_size(e.pressure()) update_rect = QRect(self.last_point.toPoint(), e.pos()) update_rect = update_rect.normalized().adjusted( -tool.size - 1, -tool.size - 1, tool.size + 1, tool.size + 1, ) tool.draw(self.last_point, e.posF(), update_rect, self.scribbles, self.current_wet) self.update(update_rect) self.last_point = e.posF() self.update_wet() if e.type() == QEvent.TabletRelease: pass e.accept() def paint(self, painter, e): if not self.last_point: return last_pos, last_pressure = self.last_point painter.drawLine(last_pos, e.posF()) self.update( QRect(last_pos.toPoint(), e.pos()).normalized().adjusted(-MAX_RADIUS, -MAX_RADIUS, MAX_RADIUS, MAX_RADIUS)) def set_tool(self, tool_name): self.tool = self.tools[tool_name] def unset_tool(self): self.tool = None def clear(self): while self.scribbles: self.undo() def undo(self): if self.scribbles: undone = self.scribbles.pop() if not undone.rect: self.undo() else: self.undo_stack.append(undone) self.update(undone.rect) def redo(self): if self.undo_stack: redone = self.undo_stack.pop() self.scribbles.append(redone) self.update(redone.rect) def update_wet(self, seconds=1): self.wet_end = time.monotonic() + seconds
class KnechtUpdate(QObject): update_available = Signal(str) remote_version = '0.00' remote_version_age = datetime.datetime.now() installer_file = Path('Dummy.exe') first_run = True def __init__(self, ui): """ Run an updater thread to check for new versions on remote path. :param modules.gui.main_ui.KnechtWindow ui: """ super(KnechtUpdate, self).__init__(ui) self.ui = ui # Update check thread self.ut: _KnechtUpdateThread = None self.timeout = QTimer() self.timeout.setSingleShot(True) self.timeout.setInterval(3000) self.schedule_timer = QTimer() self.schedule_timer.setInterval(8000000) self.schedule_timer.setTimerType(Qt.VeryCoarseTimer) self.schedule_timer.timeout.connect(self.schedule_update) self.schedule_timer.start() self.last_update_check = datetime.datetime.now() def schedule_update(self): """ Daily update check """ delta = datetime.datetime.now() - self.last_update_check if delta > datetime.timedelta(days=1): LOGGER.info('Running scheduled update check after: %s', delta) self.run_update() def _init_thread(self): # Update check thread self.ut = _KnechtUpdateThread(self) # Update thread signals self.ut.already_up_to_date.connect(self._already_latest_version) self.ut.update_failed.connect(self._update_error) self.ut.update_ready.connect(self._set_update_available) self.ut.new_version.connect(self._set_remote_version) def run_update(self): """ If an updated installer is available: ask user to execute it. Otherwise run update thread to check for newer versions """ # Make sure this is not called within timeout if self.timeout.isActive(): return self.timeout.start() self.first_run = False self.last_update_check = datetime.datetime.now() # Exit on running thread if self.is_running(): LOGGER.info( 'Can not check for updates while update thread is running!') return # Run update if already available if self._is_update_ready(): if not self._ask_to_run(): return self._execute_update() return # Check if already up to date if self.is_up_to_date(): self._already_latest_version() return self._init_thread() self.ut.start() self.ui.msg(_('Suche nach Anwendungs Updates gestartet.')) def is_up_to_date(self) -> bool: """ Return True if the remote version equals current version """ delta = datetime.datetime.now() - self.remote_version_age if delta > datetime.timedelta(hours=1): # Remote version info is older than 1 hour return False if self.remote_version == KnechtSettings.app['version']: return True return False def is_running(self): """ Determine wherever a Update thread is running. """ if self.ut is not None: if self.ut.is_alive(): return True return False def _is_update_ready(self) -> bool: if path_exists(self.installer_file): if self.remote_version > KnechtSettings.app['version']: return True return False def _execute_update(self): args = [ self.installer_file.as_posix(), '/SILENT', '/CLOSEAPPLICATIONS', '/RESTARTAPPLICATIONS' ] try: Popen(args) LOGGER.info('Running Update Installer: %s', args) except Exception as e: LOGGER.error('Could not run update installer. %s', e) return # Close application self.ui.close() @Slot(Path) def _set_update_available(self, installer_file: Path): self.installer_file = installer_file if self._is_update_ready(): self.update_available.emit(self.remote_version) self._show_notification() @Slot(str) def _set_remote_version(self, version: str): self.remote_version = version self.remote_version_age = datetime.datetime.now() @Slot() def _already_latest_version(self): self.ui.msg(_('Die Anwendung ist auf dem neusten Stand.'), 5000) @Slot() def _update_error(self): self.ui.msg(_('Aktualisierung konnte nicht durchgeführt werden.'), 4000) def _ask_to_run(self) -> bool: msg_box = AskToContinue(self.ui) self.ui.play_hint_sound() if not msg_box.ask( title=_('App Update'), txt=_( 'Eine aktualisierte Version {} ist verfügbar.<br><br>' 'Möchten Sie die Aktualisierung jetzt durchführen?<br><br>' 'Die Anwendung muss für das Update beendet werden.'). format(self.remote_version), ok_btn_txt=_('Jetzt aktualisieren'), abort_btn_txt=_('Vielleicht später...'), ): # User wants to update later return False return True def _show_notification(self): def msg_callback(): if self._ask_to_run(): self._execute_update() self.ui.show_tray_notification( title=_('Aktualisierung'), message=_('Version {} ist bereit zur Installation.').format( self.remote_version), clicked_callback=msg_callback)
rows = Counter() numberClasses.valueChanged[int].connect(edit_number_rows) file_list = load_from_file("classes.csv", rows) numberClasses.setValue(rows) submitButton = QPushButton(text="Save") submitButton.clicked.connect(save_form) layout.addWidget(submitButton) timer = QTimer() timer.setInterval(5 * 1000) timer.setTimerType(Qt.CoarseTimer) timer.timeout.connect(open_class_wrapper) runButton = QPushButton(text="Run") runButton.clicked.connect(run_app) layout.addWidget(runButton) trayIcon = QSystemTrayIcon(QIcon(app_icon)) trayIcon.activated.connect(foreground) minButton = QPushButton(text="Minimize") minButton.clicked.connect(minimize) layout.addWidget(minButton) editPage = QWidget() editPage.setLayout(layout)