class TimeoutProgressBar(QProgressBar): timeout = pyqtSignal() def __init__(self, parent=None, timeout=10000): super().__init__(parent) self.timeout_interval = timeout self.timer = QTimer() self.timer.setSingleShot(False) self.timer.setInterval(100) # update the progress bar tick connect(self.timer.timeout, self._update) self.setMaximum(self.timeout_interval) def _update(self): self.setValue(self.value() + self.timer.interval()) if self.value() >= self.maximum(): self.timer.stop() self.timeout.emit() def start(self): self.setValue(0) self.timer.start() def stop(self): self.setValue(0) self.timer.stop()
class Wpm(QLabel): def __init__(self, *args, **kwargs): super(Wpm, self).__init__("0 wpm", *args, **kwargs) self._correct = 0 self._seconds = 0 self._timer = QTimer() self._timer.timeout.connect(self._update) def _update(self): self._seconds += self._timer.interval() / 1000 wpm = round((self._correct / 5) / (self._seconds / 60)) self.setText(f"{wpm} wpm") def begin(self): self._timer.start(100) # update every tenth of a second def reset(self): self.setText("0 wpm") self._correct = 0 self._seconds = 0 self._timer.stop() def finish(self): self._timer.stop() def addCorrect(self, chars): self._correct += chars
class RandomTimer(QObject): timeout = Signal() intervalChanged = Signal() activeChanged = Signal() def __init__(self, parent=None): super(RandomTimer, self).__init__() self.timer = QTimer() self.timer.timeout.connect(self.timeout) @Slot() def start(self): print("timer start") if not self.timer.isActive(): self.timer.start() self.activeChanged.emit() @Slot() def stop(self): print("timer stop") if self.timer.isActive(): self.timer.stop() self.activeChanged.emit() @Slot(int, int, result=int) def randomInterval(self, min, max): range = max - min msec = min + random.randint(0, range) return msec @pyqtProperty(int, notify=intervalChanged) def interval(self): return self.timer.interval() @interval.setter def interval(self, msec): if self.timer.interval() != msec: self.timer.setInterval(msec) self.intervalChanged.emit() print("interval = {}".format(self.timer.interval())) @pyqtProperty(bool, notify=activeChanged) def active(self): return self.timer.isActive()
class StatusWatcher(Extension): def __init__(self, application: CuraApplication): super().__init__() self._app = application self._path = Path( "~/cura-print-status.txt" ).expanduser() # should be configurable ЪциРђЇРЎђ№ИЈ self._path.write_text("\n") self._timer = QTimer() self._timer.timeout.connect(self.dump_status) self._timer.start(15000) def dump_status(self): try: job_name = "" total_time = -1337 elapsed_time = -1337 remaining_time = -1337 info = self._app.getPrintInformation() if info is not None: job_name = info.jobName total_time = info.currentPrintTime devices = self._app.getMachineManager().printerOutputDevices if devices: printer = devices[0].activePrinter if printer is not None: job = printer.activePrintJob if job is not None: total_time = job.timeTotal elapsed_time = job.timeElapsed remaining_time = job.timeTotal - job.timeElapsed tpl = """ Job Name: {job_name} Total Time: {total_time} Elapsed: {elapsed_time} Remaining: {remaining_time} """ content = tpl.format( job_name=job_name, total_time=duration_text(total_time), elapsed_time=duration_text(elapsed_time), remaining_time=duration_text(remaining_time), ) self._path.write_text(textwrap.dedent(content)) if self._timer.interval() > 1000: self._timer.setInterval(1000) except Exception as e: Logger.log("w", "failed to write status file: {}".format(e))
class Example(QMainWindow, Ui_MainWindow): def __init__(self): super().__init__() self.setupUi(self) # Ui_MainWindow 의 초기화 매소드 self.timer = QTimer() # connect 함수를 사용하여 주기적으로 실행할 check 함수에 연결해줌 self.timer.timeout.connect(self.check) self.timer.setInterval(5 * 60 * 1000) self.rb_5m.setChecked(True) # UI초기 설정 self.show() def setCycle(self): if self.rb_10m.isChecked(): # self.rb_10m.setChecked() self.timer.setInterval(10 * 60 * 1000) elif self.rb_5m.isChecked(): self.timer.setInterval(5 * 60 * 1000) elif self.rb_1m.isChecked(): print('clecked') self.timer.setInterval(1 * 60 * 1000) elif self.rb_30s.isChecked(): self.timer.setInterval(0.5 * 60 * 1000) elif self.rb_5s.isChecked(): print('clecke') self.timer.setInterval(5 * 1000) else: self.timer.setInterval(1000) def startChk(self): self.url.setEnabled(False) self.timer.start() def stopChk(self): self.url.setEnabled(True) self.timer.stop() def check(self): # print('인터벌 :' , self.timer.interval()) self.rsp = req.urlopen(self.url.text()) self.html = bs(self.rsp, 'html.parser') try: self.test = self.html.find(alt="UP").attrs['alt'] except: self.test = "XX" self.t = time.localtime() self.lbl_print.setText("인터벌:{}, {}:{}:{} 채크결과: {}".format( self.timer.interval(), self.t.tm_hour, self.t.tm_min, self.t.tm_sec, self.test))
class Timer(QLabel): def __init__(self, *args, **kwargs): super(Timer, self).__init__("00:00", *args, **kwargs) self._seconds = 0 self._timer = QTimer() self._timer.timeout.connect(self._update) def _update(self): self._seconds += self._timer.interval() / 1000 m = int(self._seconds // 60) s = int(self._seconds % 60) self.setText(f"{m:02}:{s:02}") def begin(self): self._timer.start(500) # update every half second def reset(self): self.setText("00:00") self._seconds = 0 self._timer.stop() def finish(self): self._timer.stop()
def __init__(self, width, height, parent=None): super(DrawerCanvas, self).__init__(parent) self.time = QTime.currentTime() # Create and bind timeout callback for 20ms update rate invoke of Refresh methos timer = QTimer(self) timer.interval = 20 timer.timeout.connect(self.Refresh) timer.start(20) self.setWindowTitle("PDrawer") self.resize(width, height) self.scale = 1 # scaling member for rendered objects self.shapes = [] # object rendering collection self.lockShapes = threading.Lock() # explicit lock object for add/clear/access marshalling self.lastMouseClickValid_L = False # init to no valid click yet self.lastMouseClickValid_R = False # init to no valid click yet # Save actual last event, provides more than just position - future use TODO self.lastMouseClickEvent = QMouseEvent( QMouseEvent.MouseMove, QPointF(), Qt.MouseButton(), Qt.MouseButtons(), Qt.NoModifier ) self.lastMouseClickEvent_L = QMouseEvent( QMouseEvent.MouseMove, QPointF(), Qt.MouseButton(), Qt.MouseButtons(), Qt.NoModifier ) self.lastMouseClickEvent_R = QMouseEvent( QMouseEvent.MouseMove, QPointF(), Qt.MouseButton(), Qt.MouseButtons(), Qt.NoModifier ) self.lastMouseMoveEvent = QMouseEvent( QMouseEvent.MouseMove, QPointF(), Qt.MouseButton(), Qt.MouseButtons(), Qt.NoModifier )
class Window(QWidget): def __init__(self, data, kwargs): super().__init__() self.initUI(data, kwargs) def initUI(self, data, kwargs, intvl=64): self.timer = QTimer(self) self.timer.timeout.connect( self.tick) # QWidget.update() fires a "signal" self.timer.setInterval(intvl) self.dsp = Display(data, kwargs) ## make a Display instance self.stpLbl = QLabel("{:d}".format(self.dsp.stepCt)) self.stpBtn = QPushButton("Step") self.rnpBtn = QPushButton("Run to") self.limTbx = QLineEdit(self) self.limTbx.setFixedWidth(85) self.limTbx.setAlignment(Qt.AlignCenter) self.limTbx.setText("{:d}".format(self.dsp.runLim)) self.fstBtn = QPushButton("Faster") self.slwBtn = QPushButton("Slower") self.outBtn = QPushButton("Zoom out") self.zInBtn = QPushButton("Zoom in") self.dmpBtn = QPushButton("Dump") self.loadBtn = QPushButton("Load") self.clrBtn = QPushButton("Clear Info") self.hideBtn = QPushButton("Hide Info") self.showBtn = QPushButton("Show Info") self.fineGrd = QCheckBox("Fine grid") self.showCoh = QCheckBox("Show COH lns") self.quitBtn = QPushButton("Quit") self.tmrLbl = QLabel("{:d}".format(self.timer.interval())) self.tmrLbl.setAlignment(Qt.AlignCenter) self.scfLbl = QLabel("{:.1f}".format(self.dsp.scaleFact)) self.scfLbl.setAlignment(Qt.AlignCenter) vbox = QVBoxLayout() ## these buttons laid out vertically, vbox.addStretch(1) ## centred by means of a stretch at each end vbox.addWidget(self.stpLbl) vbox.addWidget(self.stpBtn) vbox.addWidget(self.rnpBtn) vbox.addWidget(self.limTbx) vbox.addWidget(self.fstBtn) vbox.addWidget(self.tmrLbl) vbox.addWidget(self.slwBtn) vbox.addWidget(self.outBtn) vbox.addWidget(self.scfLbl) vbox.addWidget(self.zInBtn) vbox.addWidget(self.dmpBtn) vbox.addWidget(self.loadBtn) vbox.addWidget(self.clrBtn) vbox.addWidget(self.hideBtn) vbox.addWidget(self.showBtn) vbox.addWidget(self.fineGrd) vbox.addWidget(self.showCoh) vbox.addWidget(self.quitBtn) vbox.addStretch(1) scrollArea = QScrollArea() ## a scroll pane for the graphical display; scrollArea.setWidget(self.dsp) ## put the display in the scroll pane scrollArea.horizontalScrollBar().setValue(500) scrollArea.verticalScrollBar().setValue(600) hbox = QHBoxLayout() ## Put button panel to left of the scrollArea hbox.addLayout(vbox) hbox.addWidget(scrollArea) self.setLayout(hbox) self.setGeometry(50, 50, 1200, 700) # Initally 1200x700 px with 50 px offsets # Register handlers - methods in Display instance self.rnpBtn.clicked.connect(self.stopStart) #run-to/pause btn self.limTbx.editingFinished.connect( self.updtRunLim) #run limit text box edited self.fstBtn.clicked.connect(self.faster) self.slwBtn.clicked.connect(self.slower) self.stpBtn.clicked.connect(self.step) self.outBtn.clicked.connect(self.zoomOut) self.zInBtn.clicked.connect(self.zoomIn) self.dmpBtn.clicked.connect(self.dumpState) self.loadBtn.clicked.connect(self.loadSwarm) self.clrBtn.clicked.connect(self.clearInfo) self.hideBtn.clicked.connect(self.hideInfo) self.showBtn.clicked.connect(self.showInfo) self.fineGrd.clicked.connect(self.toggle) self.showCoh.clicked.connect(self.toggle) self.quitBtn.clicked.connect(self.quit) # "slot" for editingFinished signal on run-limit text box: # -- update run limit from edited contents of the box def updtRunLim(self): try: self.dsp.runLim = int(self.limTbx.text()) except ValueError: self.limTbx.setText("{:d}".format(self.dsp.runLim)) self.limTbx.setFocus() print("Enter a valid positive integer in the box.") msg = QMessageBox() msg.setText("Enter a valid positive integer in the box.") msg.setStandardButtons(QMessageBox.Retry) x = msg.exec_() # Handle a timer tick by stepping the model and firing a repaint def tick(self): if self.dsp.stepCt < self.dsp.runLim: self.dsp.step() else: self.timer.stop() self.dsp.running = False self.rnpBtn.setText("Run to") self.stpLbl.setText("{:d}".format(self.dsp.stepCt)) def step(self): self.dsp.step() self.stpLbl.setText("{:d}".format(self.dsp.stepCt)) ## Timer control methods def faster(self): i = self.timer.interval() if i > 16: i //= 2 self.timer.setInterval(i) self.tmrLbl.setText("{:d}".format(self.timer.interval())) def slower(self): i = self.timer.interval() if i < 1024: i *= 2 self.timer.setInterval(i) self.tmrLbl.setText("{:d}".format(self.timer.interval())) def stopStart(self): if self.timer.isActive(): self.timer.stop() self.dsp.running = False self.rnpBtn.setText("Run to") else: self.timer.start() self.dsp.running = True self.rnpBtn.setText("Pause") ## Zoom control methods def zoomOut(self): self.dsp.scaleFact *= self.dsp.scaleMultplr # initially x2 if self.dsp.scaleMultplr == 2: # alternately x5, x2 self.dsp.scaleMultplr = 5 else: self.dsp.scaleMultplr = 2 self.dsp.update() self.scfLbl.setText("{:.1f}".format(self.dsp.scaleFact)) def zoomIn(self): if self.dsp.scaleMultplr == 2: # alternate multiplier BEFORE using self.dsp.scaleMultplr = 5 else: self.dsp.scaleMultplr = 2 self.dsp.scaleFact /= self.dsp.scaleMultplr self.dsp.update() self.scfLbl.setText("{:.1f}".format(self.dsp.scaleFact)) # persistence def dumpState(self): path, ok = QInputDialog.getText(self, "Dump", "Path:", QLineEdit.Normal, "") if ok and path != '': mdl.dump_swarm_txt(self.dsp.dta, self.dsp.kwargs, path) def loadSwarm(self): path, ok = QInputDialog.getText(self, "Load", "Path:", QLineEdit.Normal, "") if ok and path != '': self.dsp.dta, self.dsp.kwargs = mdl.load_swarm(path) self.dsp.stepCt = 1 # reset step count self.stpLbl.setText("{:d}".format(self.dsp.stepCt)) self.dsp.update() # repaint # Info pane def clearInfo(self): self.dsp.infoDsp.tArea.setPlainText("") def hideInfo(self): self.dsp.infoDsp.hide() def showInfo(self): self.dsp.infoDsp.show() # Pass grid, cohesion lines options to display window def toggle(self): self.dsp.fineGrdOn = self.fineGrd.isChecked() self.dsp.showCohOn = self.showCoh.isChecked() self.dsp.update() def quit(self): self.dsp.infoDsp.close() self.close()
class RPiHardware(QWidget): sub_bot = pyqtSignal() sub_top = pyqtSignal() target_changed = pyqtSignal() laser_finished = pyqtSignal() laser_time_to_completion = pyqtSignal(int) def __init__(self, laser: CompexLaser, arduino: LaserBrainArduino): super().__init__() # Set up access to the passed laser control object and get current params self.laser = laser self.arduino = arduino # Set up class variables self.homing_sub = False # Status flag that indicates if the substrate is being homed. self.laser_start_delay_msec = 4500 # Setup timer to check when external trigger pulses finish. self.timer_check_laser_finished = QTimer() self.timer_check_laser_finished.timeout.connect( self.check_laser_finished) # Define Pin Dictionaries hold_time = 0.01 self.buttons = { 'sub_bot': NamedButton(22, pull_up=False, hold_time=hold_time, dev_name='sub_bot'), 'sub_top': NamedButton(18, pull_up=False, hold_time=hold_time, dev_name='sub_top'), 'sub_run': NamedButton(21, pull_up=False, hold_time=hold_time, dev_name='sub_run'), 'target_run': NamedButton(20, pull_up=False, hold_time=hold_time, dev_name='target_run'), 'laser_run': NamedButton(19, pull_up=False, hold_time=hold_time, dev_name='laser_run') } self.high_pins = { 'ard_rst': NamedOutputDevice(16, initial_value=True, dev_name='ard_rst'), 'top_hi': NamedOutputDevice(27, initial_value=True, dev_name='top_hi'), 'bot_hi': NamedOutputDevice(23, initial_value=True, dev_name='bot_hi') } self.gpio_handler = GPIOHandler(buttons=self.buttons, high_pins=self.high_pins, sig_print=False, parent=self) self.gpio_handler.sig_gpio_input.connect(self.gpio_act) def start_laser(self, num_pulses=None): self.laser.on() # All cases need the laser in on mode. if num_pulses is None: self.laser_time_to_completion.emit(-9999) else: self.laser_time_to_completion.emit( ceil(self.laser_start_delay_msec / 1000 + num_pulses / self.laser.reprate)) if self.laser.trigger_src == 'INT': if num_pulses is None: pass elif isinstance(num_pulses, int): time = float(num_pulses / self.laser.reprate) warn( "The number of pulses parameter can only be used accurately with an external trigger, internal " "triggering of the laser will continue for {time} seconds then cease." .format(time=time)) QTimer.singleShot( self.laser_start_delay_msec - 500 + time * 1000, self.laser_finished.emit) else: raise TypeError( "Num pulses was not an integer, partial pulses are not possible." ) elif self.laser.trigger_src == 'EXT': # Turn the laser on so that it can be ready for when pulses start # Set the reprate on the arduino first so that we don't have any issues with that. self.arduino.update_laser_param('reprate', self.laser.reprate) if num_pulses is None: self.arduino.update_laser_param('start') elif isinstance(num_pulses, int): # Start the laser after 3 seconds of warmup QTimer.singleShot( self.laser_start_delay_msec, lambda: self.arduino.update_laser_param( 'goal', num_pulses)) QTimer.singleShot(self.laser_start_delay_msec, lambda: print("laser pulsing started")) # Start a timer that will kick off looking for the laser to go dormant self.timer_check_laser_finished.start( self.laser_start_delay_msec + 500) else: raise TypeError( "Num pulses was not an integer, partial pulses are not possible." ) def check_laser_finished(self): if self.buttons[ 'laser_run'].is_active and self.timer_check_laser_finished.interval( ) > 1000: self.timer_check_laser_finished.setInterval(200) elif self.buttons['laser_run'].is_active: pass elif not self.buttons['laser_run'].is_active: print("Laser activity finished, emitting signal") self.laser_finished.emit() self.timer_check_laser_finished.stop() def stop_laser(self): # Stop the laser from generating or accepting trigger pulses. self.laser.off() if self.laser.trigger_src == 'EXT': self.arduino.halt_laser() def set_reprate(self, reprate): self.laser.set_reprate(reprate) self.arduino.update_laser_param('reprate', reprate) # def set_energy(self, ): def substrate_limit(self, channel=None): # Halt the substrate if it is at the limit self.arduino.halt_motor('substrate') def home_sub(self): self.arduino.update_motor_param( 'substrate', 'start', Global.SUB_DOWN) # Start moving the substrate down without a goal self.homing_sub = True def set_sub_home(self): # ToDo: connect this to the sub bot signal so that it runs when the bottom is hit # If the user was running the substrate home routine, set zero position and reset homing_sub flag self.substrate_limit() if self.homing_sub: self.arduino.update_motor_param('substrate', 'position', 0) sleep(Global.OP_DELAY) self.arduino.update_motor_param('substrate', 'goal', 40350) self.homing_sub = False # else warn that the substrate has bottomed out else: # ToDo: Find a way to constrain substrate to only positive values in arduino code? warn( "Substrate has reached its lower limit and will not move further." ) def move_sub_to(self, mm_tts: float): goal_position = (mm_tts - Global.SUB_D0) * Global.SUB_STEPS_PER_MM self.arduino.update_motor_param('substrate', 'goal', int(goal_position)) def set_sub_speed(self, mm_spd: float): sub_spd = mm_spd * Global.SUB_STEPS_PER_MM self.arduino.update_motor_param('substrate', 'max speed', sub_spd) def set_sub_acceleration(self, mmpss: float): sub_acc = mmpss * Global.SUB_STEPS_PER_MM self.arduino.update_motor_param('substrate', 'acceleration', sub_acc) def set_carousel_home(self): home_carousel = HomeTargetCarouselDialog(self) home_carousel.exec_() if home_carousel == QDialog.accepted: self.arduino.update_motor_param('carousel', 'position', 0) elif home_carousel == QDialog.rejected: warn( "User canceled target carousel homing process, previous home value is preserved." ) def set_carousel_rps(self, rps_speed: float): carousel_spd = rps_speed * Global.CAROUSEL_STEPS_PER_REV self.arduino.update_motor_param('carousel', 'max speed', carousel_spd) def set_carousel_acceleration(self, rpss: float): carousel_acc = rpss * Global.CAROUSEL_STEPS_PER_REV self.arduino.update_motor_param('carousel', 'acceleration', carousel_acc) def move_to_target(self, target_num: int): """ Moves to the target indicated by target_num. Target numbers are zero indexed and can be kept track of of in the upper level GUI """ goal = (target_num % 6) * (Global.CAROUSEL_STEPS_PER_REV / 6) self.arduino.update_motor_param('carousel', 'goal', goal) def current_target(self): # This calculates the current position as a fraction of the total rotation, then multiplies by the number of # of steps and rounds to get to an integer target positon, finally takes the modulus by the number of positions # to account for the circle (position 6 = position 0) # print(self.arduino.query_motor_parameters('carousel', 'position')) current_pos = int( self.arduino.query_motor_parameters('carousel', 'position')) return int( np.around(((current_pos % Global.CAROUSEL_STEPS_PER_REV) / Global.CAROUSEL_STEPS_PER_REV) * 6) % 6) def raster_target(self): # ToDo: implement this and test the arduino code attached to it. pass def set_target_carousel_speed(self, dps: float): # ToDo: move these hardcoded values somewhere else so they are easier to change speed = (Global.CAROUSEL_STEPS_PER_REV) * ( dps / 360 ) # (steps per rev) * (degrees per second / degrees per rev) # Naming convention in arduino stepper library uses max speed as target speed and "speed" as instantaneous speed self.arduino.update_motor_param('carousel', 'max speed', speed) def is_sub_running(self): return self.buttons['sub_run'].is_active def is_carousel_running(self): return self.buttons['target_run'].is_active def is_laser_running(self): return self.buttons['laser_run'].is_active def is_idle(self): return not (self.is_carousel_running() or self.is_laser_running() or self.is_sub_running()) @pyqtSlot(str) def gpio_act(self, channel): # {'sub_bot': 22, # 'sub_top': 18, # 'sub_run': 21, # 'target_run': 20, # 'laser_run': 19} if channel == 'GPIO22': self.set_sub_home() elif channel == 'GPIO18': self.substrate_limit() warn( "Substrate has reached upper limit, make sure your entered position is within bounds " "and that the substrate is homed correctly.") print( "GPIO Signal Emitted and acted on from channel {}".format(channel))
class Character(QWidget): def __init__(self, parent): super().__init__() self.parent = parent self.color = self.parent.character_color self.label = QLabel(self.parent) self._angle = math.radians(0) self.radio = self.parent.radio self.anim_angle = 0 self.velocidad = 1 self.update_Pixmap() self._x = 0 self._y = 0 self.isAlive = True self.drawing = True self.fps = self.parent.fps self.sum_intervals = 0 self.seconds_passed = 0 self.cada_tiempo = self.parent.cada_tiempo self.tiempo_corte = self.parent.tiempo_corte self.centro = Point(self.x+self.pixmap.width()/2, \ self.y+self.pixmap.height()/2) self.distancia_hitbox = 8 self.hitbox_poderes = 10 self.has_power = False self.x = random.randint(32*6, self.parent.display_x-32*6) self.y = random.randint(32*6, self.parent.display_y-32*6) position = { "x" : self.x, "y" : self.y } msg_to_send = { "type" : "position_update", \ "user" : self.color, \ "info" : position} self.parent.send_position.emit(msg_to_send) @property def x(self): return self._x @x.setter def x(self, value): self._x = value self.label.move(self.x, self.y) @property def y(self): return self._y @y.setter def y(self, value): self._y = value self.label.move(self.x, self.y) @property def angle(self): return self._angle @angle.setter def angle(self, value): self._angle = value self.update_Pixmap() def update_Pixmap(self, dead=None): if dead is True: self.pixmap = QPixmap(f'sprites/{self.color}_ball_dead.png') else: self.pixmap = QPixmap(f'sprites/{self.color}_ball.png') self.label.setMinimumSize(self.pixmap.width(), self.pixmap.height()) self.label.setAlignment(Qt.AlignCenter) self.label.setFixedSize(self.pixmap.width(), self.pixmap.height()) self.label.setPixmap(self.pixmap) def pos(self): return QPoint(self.x+8, self.y+8) def run(self): self.timer = QTimer(self.parent) self.timer.timeout.connect(self.animation) self.timer.start(1000/self.parent.fps) def cut_track_checker(self): self.time_checker = QTimer(self.parent) self.time_checker.timeout.connect(self.cut_track) self.time_checker.start(1000) def pickup_power_checker(self): self.power_timer = QTimer(self.parent) self.power_timer.timeout.connect(self.pickup_power) self.power_timer.start(1000/self.parent.fps) def pickup_power(self): if self.has_power is False: for power in self.parent.powers_in_screen: distancia = \ math.sqrt( abs(self.centro.x - power.centro.x)**2 + \ abs(self.centro.y - power.centro.y)**2 ) if distancia <= self.hitbox_poderes: power.taken(self) print(self.velocidad) self.has_power = True def cut_track(self): if self.isAlive is True: if self.drawing: if self.seconds_passed == self.cada_tiempo: self.drawing = False self.seconds_passed = 0 else: self.seconds_passed += self.time_checker.interval()/1000 else: if self.seconds_passed == self.tiempo_corte: self.drawing = True self.seconds_passed = 0 else: self.seconds_passed += self.time_checker.interval()/1000 else: self.update_Pixmap(True) def animation(self): if self.isAlive is True: if self.parent.is_paused is False: self.x += self.radio*self.velocidad*\ math.cos(self.angle*self.velocidad) self.y += self.radio*\ self.velocidad*math.sin(self.angle*self.velocidad) self.manipulate_position() self.centro = Point(self.x+self.pixmap.width()/2, \ self.y+self.pixmap.height()/2) painter = QPainter(self.parent.image) painter.setPen(QPen(self.parent.brush_color, \ self.parent.brush_size, \ Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)) if self.drawing is True: point_to_save = \ Point(self.centro.x-17*math.cos(self.angle), \ self.centro.y-17*math.sin(self.angle)) painter.drawLine(self.centro.x-17*math.cos(self.angle), \ self.centro.y-17*math.sin(self.angle), \ self.centro.x-16*math.cos(self.angle), \ self.centro.y-16*math.sin(self.angle) ) self.parent.tracks.add(point_to_save) self.parent.update() if self.x < 0 or self.parent.display_x < self.x+16 or \ self.y < 0 or self.parent.display_y < self.y+16: self.isAlive = False for point in self.parent.tracks: distancia = math.sqrt( abs(self.centro.x - point.x)**2 + \ abs(self.centro.y - point.y)**2 ) if distancia <= self.distancia_hitbox: self.isAlive = False else: self.timer.stop() self.points_to_give = 1 def manipulate_position(self): position = { "x" : self.x, "y" : self.y } msg_to_send = { "type" : "position_update", \ "user" : self.color, \ "info" : position} self.parent.send_position.emit(msg_to_send)
class PomoTimer: def __init__(self, times, label, tray, rpc, subject): self.subject = subject self.tray = tray self.label = label self.main_time = times[0] self.time = QTime(0, self.main_time, 0) self.timer = QTimer() self.timer.setInterval(1000) self.timer.timeout.connect(self.timerEvent) self.rep = 0 self.RPC = rpc self.short_break = self.Interval_timer(times[1], self.label, self.tray, self, self.RPC, self.subject) self.long_break = self.Interval_timer(times[2], self.label, self.tray, self, self.RPC, self.subject) self.round = 1 class Interval_timer: def __init__(self, main_time, label, tray, outer_class, rpc, subject): self.outer_class = outer_class self.tray = tray self.label = label self.main_time = main_time self.time = QTime(0, main_time, 0) self.RPC = rpc self.subject = subject def timerEvent(self): self.time = self.time.addSecs(-1) self.label.setText(self.time.toString("mm:ss")) if self.time.secsTo(QTime(0, 0, 0)) == 0: print("Break timer stopped") self.tray.showMessage("Tomatime", "Break time's up", self.tray.icon(), 4000) self.clearTimer() self.outer_class.timer.timeout.disconnect(self.timerEvent) self.outer_class.timer.timeout.connect( self.outer_class.timerEvent) print("Returning to focus timer") self.outer_class.round += 1 self.outer_class.updateDiscord("Studying") return print(self.time.toString("mm:ss"), " Break timer") def startTimer(self): print("Starting secondary timer") self.outer_class.timer.timeout.connect(self.timerEvent) self.outer_class.updateDiscord("Taking a break") def clearTimer(self): print("Clearing break timer") self.time = QTime(0, self.main_time, 0) self.label.setText(self.time.toString("mm:ss")) def timerEvent(self): self.time = self.time.addSecs(-1) self.label.setText(self.time.toString("mm:ss")) if self.time.secsTo(QTime(0, 0, 0)) == 0: self.rep += 1 self.timer.timeout.disconnect(self.timerEvent) self.clearTimer() print("Focus time's up") self.tray.showMessage("Tomatime", "Focus time's up", self.tray.icon(), 4000) if self.rep == 3: self.rep = 0 self.long_break.startTimer() return print("Starting long break timer") else: self.short_break.startTimer() return print("Starting short break timer") return print(self.time.toString("mm:ss"), (" Focus Timer Ticking")) def startTimer(self): self.timer.start() print(self.timer.interval()) self.updateDiscord("Studying") def clearTimer(self): self.time = QTime(0, self.main_time, 0) def pauseTimer(self): self.timer.stop() try: self.RPC.update(state=f"Studying - Round {self.round}", details="Paused", large_image="fsidfsd") except: print("No Discord app running") def resetTimer(self): self.pauseTimer() self.short_break.clearTimer() self.long_break.clearTimer() self.clearTimer() self.label.setText(str(self.main_time) + ":00") try: self.timer.timeout.disconnect(self.short_break.timerEvent) except: pass try: self.timer.timeout.disconnect(self.long_break.timerEvent) except: pass try: self.timer.timeout.disconnect(self.timerEvent) except: pass self.timer.timeout.connect(self.timerEvent) def epochTime(self, mins, second): orig = datetime.datetime.fromtimestamp(time.time()) new = orig + datetime.timedelta(minutes=mins, seconds=second) return time.mktime(new.timetuple()) def updateDiscord(self, info): try: self.RPC.update( state=f"Studying {self.subject} - Round {self.round}", details=info, large_image="fsidfsd", end=self.epochTime(self.time.minute(), self.time.second())) except: print("No Discord app running")
class E5SideBar(QWidget): """ Class implementing a sidebar with a widget area, that is hidden or shown, if the current tab is clicked again. """ Version = 1 North = 0 East = 1 South = 2 West = 3 def __init__(self, orientation=None, delay=200, parent=None): """ Constructor @param orientation orientation of the sidebar widget (North, East, South, West) @param delay value for the expand/shrink delay in milliseconds (integer) @param parent parent widget (QWidget) """ super(E5SideBar, self).__init__(parent) self.__tabBar = QTabBar() self.__tabBar.setDrawBase(True) self.__tabBar.setShape(QTabBar.RoundedNorth) self.__tabBar.setUsesScrollButtons(True) self.__tabBar.setDrawBase(False) self.__stackedWidget = QStackedWidget(self) self.__stackedWidget.setContentsMargins(0, 0, 0, 0) self.__autoHideButton = QToolButton() self.__autoHideButton.setCheckable(True) self.__autoHideButton.setIcon( UI.PixmapCache.getIcon("autoHideOff.png")) self.__autoHideButton.setChecked(True) self.__autoHideButton.setToolTip( self.tr("Deselect to activate automatic collapsing")) self.barLayout = QBoxLayout(QBoxLayout.LeftToRight) self.barLayout.setContentsMargins(0, 0, 0, 0) self.layout = QBoxLayout(QBoxLayout.TopToBottom) self.layout.setContentsMargins(0, 0, 0, 0) self.layout.setSpacing(0) self.barLayout.addWidget(self.__autoHideButton) self.barLayout.addWidget(self.__tabBar) self.layout.addLayout(self.barLayout) self.layout.addWidget(self.__stackedWidget) self.setLayout(self.layout) # initialize the delay timer self.__actionMethod = None self.__delayTimer = QTimer(self) self.__delayTimer.setSingleShot(True) self.__delayTimer.setInterval(delay) self.__delayTimer.timeout.connect(self.__delayedAction) self.__minimized = False self.__minSize = 0 self.__maxSize = 0 self.__bigSize = QSize() self.splitter = None self.splitterSizes = [] self.__hasFocus = False # flag storing if this widget or any child has the focus self.__autoHide = False self.__tabBar.installEventFilter(self) self.__orientation = E5SideBar.North if orientation is None: orientation = E5SideBar.North self.setOrientation(orientation) self.__tabBar.currentChanged[int].connect( self.__stackedWidget.setCurrentIndex) e5App().focusChanged.connect(self.__appFocusChanged) self.__autoHideButton.toggled[bool].connect(self.__autoHideToggled) def setSplitter(self, splitter): """ Public method to set the splitter managing the sidebar. @param splitter reference to the splitter (QSplitter) """ self.splitter = splitter self.splitter.splitterMoved.connect(self.__splitterMoved) self.splitter.setChildrenCollapsible(False) index = self.splitter.indexOf(self) self.splitter.setCollapsible(index, False) def __splitterMoved(self, pos, index): """ Private slot to react on splitter moves. @param pos new position of the splitter handle (integer) @param index index of the splitter handle (integer) """ if self.splitter: self.splitterSizes = self.splitter.sizes() def __delayedAction(self): """ Private slot to handle the firing of the delay timer. """ if self.__actionMethod is not None: self.__actionMethod() def setDelay(self, delay): """ Public method to set the delay value for the expand/shrink delay in milliseconds. @param delay value for the expand/shrink delay in milliseconds (integer) """ self.__delayTimer.setInterval(delay) def delay(self): """ Public method to get the delay value for the expand/shrink delay in milliseconds. @return value for the expand/shrink delay in milliseconds (integer) """ return self.__delayTimer.interval() def __cancelDelayTimer(self): """ Private method to cancel the current delay timer. """ self.__delayTimer.stop() self.__actionMethod = None def shrink(self): """ Public method to record a shrink request. """ self.__delayTimer.stop() self.__actionMethod = self.__shrinkIt self.__delayTimer.start() def __shrinkIt(self): """ Private method to shrink the sidebar. """ self.__minimized = True self.__bigSize = self.size() if self.__orientation in [E5SideBar.North, E5SideBar.South]: self.__minSize = self.minimumSizeHint().height() self.__maxSize = self.maximumHeight() else: self.__minSize = self.minimumSizeHint().width() self.__maxSize = self.maximumWidth() if self.splitter: self.splitterSizes = self.splitter.sizes() self.__stackedWidget.hide() if self.__orientation in [E5SideBar.North, E5SideBar.South]: self.setFixedHeight(self.__tabBar.minimumSizeHint().height()) else: self.setFixedWidth(self.__tabBar.minimumSizeHint().width()) self.__actionMethod = None def expand(self): """ Public method to record a expand request. """ self.__delayTimer.stop() self.__actionMethod = self.__expandIt self.__delayTimer.start() def __expandIt(self): """ Private method to expand the sidebar. """ self.__minimized = False self.__stackedWidget.show() self.resize(self.__bigSize) if self.__orientation in [E5SideBar.North, E5SideBar.South]: minSize = max(self.__minSize, self.minimumSizeHint().height()) self.setMinimumHeight(minSize) self.setMaximumHeight(self.__maxSize) else: minSize = max(self.__minSize, self.minimumSizeHint().width()) self.setMinimumWidth(minSize) self.setMaximumWidth(self.__maxSize) if self.splitter: self.splitter.setSizes(self.splitterSizes) self.__actionMethod = None def isMinimized(self): """ Public method to check the minimized state. @return flag indicating the minimized state (boolean) """ return self.__minimized def isAutoHiding(self): """ Public method to check, if the auto hide function is active. @return flag indicating the state of auto hiding (boolean) """ return self.__autoHide def eventFilter(self, obj, evt): """ Public method to handle some events for the tabbar. @param obj reference to the object (QObject) @param evt reference to the event object (QEvent) @return flag indicating, if the event was handled (boolean) """ if obj == self.__tabBar: if evt.type() == QEvent.MouseButtonPress: pos = evt.pos() for i in range(self.__tabBar.count()): if self.__tabBar.tabRect(i).contains(pos): break if i == self.__tabBar.currentIndex(): if self.isMinimized(): self.expand() else: self.shrink() return True elif self.isMinimized(): self.expand() elif evt.type() == QEvent.Wheel: if qVersion() >= "5.0.0": delta = evt.angleDelta().y() else: delta = evt.delta() if delta > 0: self.prevTab() else: self.nextTab() return True return QWidget.eventFilter(self, obj, evt) def addTab(self, widget, iconOrLabel, label=None): """ Public method to add a tab to the sidebar. @param widget reference to the widget to add (QWidget) @param iconOrLabel reference to the icon or the label text of the tab (QIcon, string) @param label the labeltext of the tab (string) (only to be used, if the second parameter is a QIcon) """ if label: index = self.__tabBar.addTab(iconOrLabel, label) self.__tabBar.setTabToolTip(index, label) else: index = self.__tabBar.addTab(iconOrLabel) self.__tabBar.setTabToolTip(index, iconOrLabel) self.__stackedWidget.addWidget(widget) if self.__orientation in [E5SideBar.North, E5SideBar.South]: self.__minSize = self.minimumSizeHint().height() else: self.__minSize = self.minimumSizeHint().width() def insertTab(self, index, widget, iconOrLabel, label=None): """ Public method to insert a tab into the sidebar. @param index the index to insert the tab at (integer) @param widget reference to the widget to insert (QWidget) @param iconOrLabel reference to the icon or the labeltext of the tab (QIcon, string) @param label the labeltext of the tab (string) (only to be used, if the second parameter is a QIcon) """ if label: index = self.__tabBar.insertTab(index, iconOrLabel, label) self.__tabBar.setTabToolTip(index, label) else: index = self.__tabBar.insertTab(index, iconOrLabel) self.__tabBar.setTabToolTip(index, iconOrLabel) self.__stackedWidget.insertWidget(index, widget) if self.__orientation in [E5SideBar.North, E5SideBar.South]: self.__minSize = self.minimumSizeHint().height() else: self.__minSize = self.minimumSizeHint().width() def removeTab(self, index): """ Public method to remove a tab. @param index the index of the tab to remove (integer) """ self.__stackedWidget.removeWidget(self.__stackedWidget.widget(index)) self.__tabBar.removeTab(index) if self.__orientation in [E5SideBar.North, E5SideBar.South]: self.__minSize = self.minimumSizeHint().height() else: self.__minSize = self.minimumSizeHint().width() def clear(self): """ Public method to remove all tabs. """ while self.count() > 0: self.removeTab(0) def prevTab(self): """ Public slot used to show the previous tab. """ ind = self.currentIndex() - 1 if ind == -1: ind = self.count() - 1 self.setCurrentIndex(ind) self.currentWidget().setFocus() def nextTab(self): """ Public slot used to show the next tab. """ ind = self.currentIndex() + 1 if ind == self.count(): ind = 0 self.setCurrentIndex(ind) self.currentWidget().setFocus() def count(self): """ Public method to get the number of tabs. @return number of tabs in the sidebar (integer) """ return self.__tabBar.count() def currentIndex(self): """ Public method to get the index of the current tab. @return index of the current tab (integer) """ return self.__stackedWidget.currentIndex() def setCurrentIndex(self, index): """ Public slot to set the current index. @param index the index to set as the current index (integer) """ self.__tabBar.setCurrentIndex(index) self.__stackedWidget.setCurrentIndex(index) if self.isMinimized(): self.expand() def currentWidget(self): """ Public method to get a reference to the current widget. @return reference to the current widget (QWidget) """ return self.__stackedWidget.currentWidget() def setCurrentWidget(self, widget): """ Public slot to set the current widget. @param widget reference to the widget to become the current widget (QWidget) """ self.__stackedWidget.setCurrentWidget(widget) self.__tabBar.setCurrentIndex(self.__stackedWidget.currentIndex()) if self.isMinimized(): self.expand() def indexOf(self, widget): """ Public method to get the index of the given widget. @param widget reference to the widget to get the index of (QWidget) @return index of the given widget (integer) """ return self.__stackedWidget.indexOf(widget) def isTabEnabled(self, index): """ Public method to check, if a tab is enabled. @param index index of the tab to check (integer) @return flag indicating the enabled state (boolean) """ return self.__tabBar.isTabEnabled(index) def setTabEnabled(self, index, enabled): """ Public method to set the enabled state of a tab. @param index index of the tab to set (integer) @param enabled enabled state to set (boolean) """ self.__tabBar.setTabEnabled(index, enabled) def orientation(self): """ Public method to get the orientation of the sidebar. @return orientation of the sidebar (North, East, South, West) """ return self.__orientation def setOrientation(self, orient): """ Public method to set the orientation of the sidebar. @param orient orientation of the sidebar (North, East, South, West) """ if orient == E5SideBar.North: self.__tabBar.setShape(QTabBar.RoundedNorth) self.__tabBar.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) self.barLayout.setDirection(QBoxLayout.LeftToRight) self.layout.setDirection(QBoxLayout.TopToBottom) self.layout.setAlignment(self.barLayout, Qt.AlignLeft) elif orient == E5SideBar.East: self.__tabBar.setShape(QTabBar.RoundedEast) self.__tabBar.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) self.barLayout.setDirection(QBoxLayout.TopToBottom) self.layout.setDirection(QBoxLayout.RightToLeft) self.layout.setAlignment(self.barLayout, Qt.AlignTop) elif orient == E5SideBar.South: self.__tabBar.setShape(QTabBar.RoundedSouth) self.__tabBar.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) self.barLayout.setDirection(QBoxLayout.LeftToRight) self.layout.setDirection(QBoxLayout.BottomToTop) self.layout.setAlignment(self.barLayout, Qt.AlignLeft) elif orient == E5SideBar.West: self.__tabBar.setShape(QTabBar.RoundedWest) self.__tabBar.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) self.barLayout.setDirection(QBoxLayout.TopToBottom) self.layout.setDirection(QBoxLayout.LeftToRight) self.layout.setAlignment(self.barLayout, Qt.AlignTop) self.__orientation = orient def tabIcon(self, index): """ Public method to get the icon of a tab. @param index index of the tab (integer) @return icon of the tab (QIcon) """ return self.__tabBar.tabIcon(index) def setTabIcon(self, index, icon): """ Public method to set the icon of a tab. @param index index of the tab (integer) @param icon icon to be set (QIcon) """ self.__tabBar.setTabIcon(index, icon) def tabText(self, index): """ Public method to get the text of a tab. @param index index of the tab (integer) @return text of the tab (string) """ return self.__tabBar.tabText(index) def setTabText(self, index, text): """ Public method to set the text of a tab. @param index index of the tab (integer) @param text text to set (string) """ self.__tabBar.setTabText(index, text) def tabToolTip(self, index): """ Public method to get the tooltip text of a tab. @param index index of the tab (integer) @return tooltip text of the tab (string) """ return self.__tabBar.tabToolTip(index) def setTabToolTip(self, index, tip): """ Public method to set the tooltip text of a tab. @param index index of the tab (integer) @param tip tooltip text to set (string) """ self.__tabBar.setTabToolTip(index, tip) def tabWhatsThis(self, index): """ Public method to get the WhatsThis text of a tab. @param index index of the tab (integer) @return WhatsThis text of the tab (string) """ return self.__tabBar.tabWhatsThis(index) def setTabWhatsThis(self, index, text): """ Public method to set the WhatsThis text of a tab. @param index index of the tab (integer) @param text WhatsThis text to set (string) """ self.__tabBar.setTabWhatsThis(index, text) def widget(self, index): """ Public method to get a reference to the widget associated with a tab. @param index index of the tab (integer) @return reference to the widget (QWidget) """ return self.__stackedWidget.widget(index) def saveState(self): """ Public method to save the state of the sidebar. @return saved state as a byte array (QByteArray) """ if len(self.splitterSizes) == 0: if self.splitter: self.splitterSizes = self.splitter.sizes() self.__bigSize = self.size() if self.__orientation in [E5SideBar.North, E5SideBar.South]: self.__minSize = self.minimumSizeHint().height() self.__maxSize = self.maximumHeight() else: self.__minSize = self.minimumSizeHint().width() self.__maxSize = self.maximumWidth() data = QByteArray() stream = QDataStream(data, QIODevice.WriteOnly) stream.setVersion(QDataStream.Qt_4_6) stream.writeUInt16(self.Version) stream.writeBool(self.__minimized) stream << self.__bigSize stream.writeUInt16(self.__minSize) stream.writeUInt16(self.__maxSize) stream.writeUInt16(len(self.splitterSizes)) for size in self.splitterSizes: stream.writeUInt16(size) stream.writeBool(self.__autoHide) return data def restoreState(self, state): """ Public method to restore the state of the sidebar. @param state byte array containing the saved state (QByteArray) @return flag indicating success (boolean) """ if state.isEmpty(): return False if self.__orientation in [E5SideBar.North, E5SideBar.South]: minSize = self.layout.minimumSize().height() maxSize = self.maximumHeight() else: minSize = self.layout.minimumSize().width() maxSize = self.maximumWidth() data = QByteArray(state) stream = QDataStream(data, QIODevice.ReadOnly) stream.setVersion(QDataStream.Qt_4_6) stream.readUInt16() # version minimized = stream.readBool() if minimized and not self.__minimized: self.shrink() stream >> self.__bigSize self.__minSize = max(stream.readUInt16(), minSize) self.__maxSize = max(stream.readUInt16(), maxSize) count = stream.readUInt16() self.splitterSizes = [] for i in range(count): self.splitterSizes.append(stream.readUInt16()) self.__autoHide = stream.readBool() self.__autoHideButton.setChecked(not self.__autoHide) if not minimized: self.expand() return True ####################################################################### ## methods below implement the autohide functionality ####################################################################### def __autoHideToggled(self, checked): """ Private slot to handle the toggling of the autohide button. @param checked flag indicating the checked state of the button (boolean) """ self.__autoHide = not checked if self.__autoHide: self.__autoHideButton.setIcon( UI.PixmapCache.getIcon("autoHideOn.png")) else: self.__autoHideButton.setIcon( UI.PixmapCache.getIcon("autoHideOff.png")) def __appFocusChanged(self, old, now): """ Private slot to handle a change of the focus. @param old reference to the widget, that lost focus (QWidget or None) @param now reference to the widget having the focus (QWidget or None) """ self.__hasFocus = self.isAncestorOf(now) if self.__autoHide and not self.__hasFocus and not self.isMinimized(): self.shrink() elif self.__autoHide and self.__hasFocus and self.isMinimized(): self.expand() def enterEvent(self, event): """ Protected method to handle the mouse entering this widget. @param event reference to the event (QEvent) """ if self.__autoHide and self.isMinimized(): self.expand() else: self.__cancelDelayTimer() def leaveEvent(self, event): """ Protected method to handle the mouse leaving this widget. @param event reference to the event (QEvent) """ if self.__autoHide and not self.__hasFocus and not self.isMinimized(): self.shrink() else: self.__cancelDelayTimer() def shutdown(self): """ Public method to shut down the object. This method does some preparations so the object can be deleted properly. It disconnects from the focusChanged signal in order to avoid trouble later on. """ e5App().focusChanged.disconnect(self.__appFocusChanged)
class PyScientificSpinBox(QDoubleSpinBox): # This is emited like valueChanged, but not when using setValue_quiet and not within valueChange_gui_time valueChanged_gui = pyqtSignal(float) # For PyQt < 5.11: only a class with attributes using integer can be made an enum # (so simple object subclass) # from 5.11: Enum class can also be used. (also Q_ENUMS, Q_FLAGS are depracated for Q_ENUM and Q_FLAG) # Version can be obtain as: # import PyQt5.QtCore # PyQt5.QtCore.PYQT_VERSION_STR #class Sci_Mode(Enum): class Sci_Mode(object): ENG_UNIT, ENG, SCI = range(3) Q_ENUMS(Sci_Mode) class Enabled_Controls(object): none = 0x00 minimum = 0x01 maximum = 0x02 min_increment = 0x04 precision = 0x08 display_mode = 0x10 log_increment_mode = 0x20 fixed_exponent = 0x40 all = 0x7f minmax = 0x03 no_minmax = all & ~minmax Q_FLAGS(Enabled_Controls) def __init__(self, *args, **kwargs): """ display_mode can be any of the Sci_Mode enum: ENG_UNIT (default), ENG or SCI To have a unit, set the suffix. keyboardTracking is disabled by default. accelerated is enabled by default decimals is overriden and redirected to precision. A new signal, valueChanged_gui, is emitted when the value is updated but at a restricted rate, valueChange_gui_time, and also when pressing enter. It is not emitted when using setValue_quiet option disable_context_menu can be used to disable the context menu. It is False by default. enabled_controls property can be used to disable some entries in the context_menu """ self._initializing = True self._in_setValue_quiet = False key_track = kwargs.pop('keyboardTracking', False) kwargs['keyboardTracking'] = key_track # need to handle min/max before decimal self._disable_context_menu = kwargs.pop('disable_context_menu', False) maximum = kwargs.pop('maximum', np.inf) minimum = kwargs.pop('minimum', -np.inf) accelerated = kwargs.pop('accelerated', True) decimals = kwargs.pop('decimals', 5) precision = kwargs.pop('precision', decimals) self.text_formatter = Format_Float() self.text_formatter.precision = precision self.validator = FloatValidator() self._min_increment = 1e-15 self._log_increment_mode = False self._log_increment = 0.01 self._display_mode = self.Sci_Mode.ENG_UNIT self._enabled_controls = self.Enabled_Controls.all self._last_enabled_controls = None self._valueChange_timer = QTimer() self._valueChange_timer.setInterval(100) # ms # We override decimals. This should set the parent value. kwargs[ 'decimals'] = 1000 # this get adjusted to max double precision (323) kwargs['accelerated'] = accelerated kwargs['minimum'] = minimum kwargs['maximum'] = maximum # Note that this will call all properties that are left in kwargs super(PyScientificSpinBox, self).__init__(*args, **kwargs) # decimals is used when changing min/max, on setValue and on converting value to Text (internally, it is overriden here) #super(PyScientificSpinBox, self).setDecimals(1000) # this get adjusted to max double precision (323) # make sure parent class decimals is set properly if super(PyScientificSpinBox, self).decimals() < 323: raise RuntimeError('Unexpected parent decimals value.') # The suffix/prefix change in the above QDoubleSpinBox.__init__ did not call our override # lets update them now self.setSuffix(self.suffix()) self.setPrefix(self.prefix()) # pyqt5 5.6 on windows (at least) QAction requires the parent value. self.copyAction = QAction('&Copy', None, shortcut=QKeySequence(QKeySequence.Copy), triggered=self.handle_copy) self.pasteAction = QAction('&Paste', None, shortcut=QKeySequence(QKeySequence.Paste), triggered=self.handle_paste) self.addActions((self.copyAction, self.pasteAction)) self._create_menu_init() self.clipboard = QApplication.instance().clipboard() self._last_valueChanged_gui = self.value() self._current_valueChanged_gui = self._last_valueChanged_gui self.valueChanged.connect(self._valueChanged_helper) self._valueChange_timer.timeout.connect(self._do_valueChanged_gui_emit) self._last_focus_reason_mouse = False self.lineEdit().installEventFilter(self) self.text_is_being_changed_state = False self.lineEdit().textEdited.connect(self.text_is_being_changed) self._last_key_pressed_is_enter = False self.editingFinished.connect(self.editFinished_slot) self._in_config_menu = False self._initializing = False def _create_menu_init(self): if self._disable_context_menu: return # pyqt5 5.6 on windows (at least) QAction requires the parent value. self.accel_action = QAction('&Acceleration', None, checkable=True, checked=self.isAccelerated(), triggered=self.setAccelerated) self.log_increment_mode_action = QAction( '&Log increment', None, checkable=True, checked=self.log_increment_mode, triggered=self.log_increment_mode_change) self.help_action = QAction('&Help', None, triggered=self.help_dialog) self.stepUpAction = QAction('Step &up', None) self.stepUpAction.triggered.connect(self.stepUp) self.stepDownAction = QAction('Step &down', None) self.stepDownAction.triggered.connect(self.stepDown) self.display_mode_grp = QActionGroup(None) self.display_mode_eng_unit = QAction('Engineering with &units', None, checkable=True) self.display_mode_eng = QAction('&Engineering', None, checkable=True) self.display_mode_sci = QAction('&Scientific', None, checkable=True) self.display_mode_option = { self.Sci_Mode.ENG_UNIT: self.display_mode_eng_unit, self.Sci_Mode.ENG: self.display_mode_eng, self.Sci_Mode.SCI: self.display_mode_sci } self.display_mode_grp.addAction(self.display_mode_eng_unit) self.display_mode_grp.addAction(self.display_mode_eng) self.display_mode_grp.addAction(self.display_mode_sci) self.display_mode_grp.triggered.connect(self.handle_display_mode) # fixed exponent self.fixed_exponent_menu = submenu = QMenu('&Fixed exponent') self.fixed_exponent_group = subgroup = QActionGroup(None) self.fixed_exponent_actions = actions = {} for i in range(-15, 9 + 1, 3)[::-1]: if i == 0: act = QAction('%i: %s' % (i, ''), None, checkable=True) else: act = QAction('%i: %s' % (i, units_inverse[i]), None, checkable=True) submenu.addAction(act) subgroup.addAction(act) actions[i] = act act = QAction('&Custom', None, checkable=True) submenu.addAction(act) subgroup.addAction(act) actions['custom'] = act self.fixed_exponent_custom_action = QWidgetAction(None) self.fixed_exponent_custom_widget = QSpinBox(value=0, minimum=-MAX_FIXED_EXP, maximum=MAX_FIXED_EXP) self.fixed_exponent_custom_action.setDefaultWidget( self.fixed_exponent_custom_widget) submenu.addAction(self.fixed_exponent_custom_action) submenu.addSeparator() act = QAction('&Disabled', None, checkable=True) submenu.addAction(act) subgroup.addAction(act) actions['disabled'] = act subgroup.triggered.connect(self.handle_fixed_exponent) self.fixed_exponent_custom_widget.valueChanged.connect( self.handle_fixed_exponent) # precision self.precision_menu = submenu = QMenu('&Precision') self.precision_group = subgroup = QActionGroup(None) self.precision_actions = actions = {} for i in range(0, 10): act = QAction('&%i' % i, None, checkable=True) submenu.addAction(act) subgroup.addAction(act) actions[i] = act act = QAction('&Custom', None, checkable=True) submenu.addAction(act) subgroup.addAction(act) actions['custom'] = act self.precision_custom_action = QWidgetAction(None) self.precision_custom_widget = QSpinBox(value=0, minimum=0, maximum=MAX_PRECISION) self.precision_custom_action.setDefaultWidget( self.precision_custom_widget) submenu.addAction(self.precision_custom_action) subgroup.triggered.connect(self.handle_precision) self.precision_custom_widget.valueChanged.connect( self.handle_precision) # min, max, min_increment self.min_custom_action = QWidgetAction(None) self.min_custom_widget = PyScientificSpinBox(prefix='min: ', disable_context_menu=True) self.min_custom_action.setDefaultWidget(self.min_custom_widget) self.min_custom_widget.valueChanged.connect( self.handle_min_custom_widget) self.max_custom_action = QWidgetAction(None) self.max_custom_widget = PyScientificSpinBox(prefix='max: ', disable_context_menu=True) self.max_custom_action.setDefaultWidget(self.max_custom_widget) self.max_custom_widget.valueChanged.connect( self.handle_max_custom_widget) self.min_incr_custom_action = QWidgetAction(None) self.min_incr_custom_widget = PyScientificSpinBox( prefix='min incr: ', disable_context_menu=True) self.min_incr_custom_action.setDefaultWidget( self.min_incr_custom_widget) self.min_incr_custom_widget.valueChanged.connect( self.handle_min_incr_custom_widget) self.display_mode_sep = QAction('Display mode', None) self.display_mode_sep.setSeparator(True) def text_is_being_changed(self): self.text_is_being_changed_state = True def _create_menu(self): ec = self.enabled_controls EC = self.Enabled_Controls if ec == self._last_enabled_controls: return self.modified_context_menu self._last_enabled_controls = ec #self.modified_context_menu = menu = self.lineEdit().createStandardContextMenu() self.modified_context_menu = menu = QMenu('Main Menu') menu.addActions((self.copyAction, self.pasteAction)) menu.addSeparator() menu.addActions((self.stepUpAction, self.stepDownAction)) menu.addSeparator() if ec & EC.display_mode: menu.addActions((self.display_mode_sep, self.display_mode_eng_unit, self.display_mode_eng, self.display_mode_sci)) menu.addSeparator() menu.addAction(self.accel_action) if ec & EC.log_increment_mode: menu.addAction(self.log_increment_mode_action) if ec & EC.fixed_exponent: menu.addMenu(self.fixed_exponent_menu) if ec & EC.precision: menu.addMenu(self.precision_menu) menu.addActions((self.min_custom_action, self.max_custom_action, self.min_incr_custom_action)) self.min_custom_action.setEnabled(ec & EC.minimum) self.max_custom_action.setEnabled(ec & EC.maximum) self.min_incr_custom_action.setEnabled(ec & EC.min_increment) menu.addSeparator() menu.addAction(self.help_action) return menu #def __del__(self): # print('Deleting up PyScientificSpinBox') def pyqtConfigure(self, **kwargs): # this is necessary to override pyqtConfigure decimals # otherwise it calls the parent one directly. decimals = kwargs.pop('decimals', None) if decimals is not None: self.setDecimals(decimals) # same thing for suffix/prefix prefix = kwargs.pop('prefix', None) if prefix is not None: self.setPrefix(prefix) suffix = kwargs.pop('suffix', None) if suffix is not None: self.setSuffix(suffix) if len(kwargs): super(PyScientificSpinBox, self).pyqtConfigure(**kwargs) def get_config(self): """ The return value can by used in pyqtConfigure. """ return dict(display_mode=self.display_mode, precision=self.precision, min_increment=self.min_increment, minimum=self.minimum(), maximum=self.maximum(), log_increment_mode=self.log_increment_mode, log_increment=self.log_increment, fixed_exponent=self.fixed_exponent, suffix=self.suffix(), prefix=self.prefix(), singleStep=self.singleStep(), accelerated=self.isAccelerated(), wrapping=self.wrapping(), keyboardTracking=self.keyboardTracking()) def force_update(self): val = self.value() self.setValue(val) self.selectAll() def decimals(self): return self.precision def setDecimals(self, val): self.precision = val @pyqtProperty(Enabled_Controls) def enabled_controls(self): return self._enabled_controls @enabled_controls.setter def enabled_controls(self, val): self._enabled_controls = val @pyqtProperty(Sci_Mode) #@pyqtProperty(int) def display_mode(self): return self._display_mode @display_mode.setter def display_mode(self, val): self._display_mode = val self.force_update() self.updateGeometry() @pyqtProperty(int) def min_increment(self): return self._min_increment @min_increment.setter def min_increment(self, val): self._min_increment = val if self.singleStep() < val: self.setSingleStep(val) @pyqtProperty(int) def precision(self): return self.text_formatter.precision @precision.setter def precision(self, val): val = min(max(val, 0), MAX_PRECISION) self.text_formatter.precision = val self.force_update() self.updateGeometry() @pyqtProperty(bool) def log_increment_mode(self): return self._log_increment_mode @log_increment_mode.setter def log_increment_mode(self, val): self._log_increment_mode = val @pyqtProperty(float) def log_increment(self): return self._log_increment @log_increment.setter def log_increment(self, val): self._log_increment = val @pyqtProperty(int) def fixed_exponent(self): """ 9999 disables fixed exponent mode """ val = self.text_formatter.fixed_exponent if val is None: return 9999 return val @fixed_exponent.setter def fixed_exponent(self, val): if val == 9999: val = None else: # keep within -308 to 308: MAX_FIXED_EXP = 308 val = max(val, -MAX_FIXED_EXP) val = min(val, MAX_FIXED_EXP) self.text_formatter.fixed_exponent = val self.force_update() self.updateGeometry() @pyqtProperty(float) def valueChange_gui_time(self): return self._valueChange_timer.interval() / 1e3 @valueChange_gui_time.setter def valueChange_gui_time(self, val): val = int(val * 1e3) if val <= 0: self._valueChange_timer.stop() self._valueChange_timer.setInterval(val) def keyPressEvent(self, event): key = event.key() self._last_key_pressed_is_enter = False if event.matches(QKeySequence.Copy): #print('doing copy') self.copyAction.trigger() elif event.matches(QKeySequence.Paste): #print('doing paste') self.pasteAction.trigger() elif key == QtCore.Qt.Key_Escape: if self.text_is_being_changed_state: #print('doing escape') self.setValue(self.value()) else: #print('letting escape propagate') event.ignore() elif key in [QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return]: self._last_key_pressed_is_enter = True text_was_being_changed_state = self.text_is_being_changed_state super(PyScientificSpinBox, self).keyPressEvent(event) # default handling does event.ignore() so event, after doing something here # propagate. only do that if test is not being changed. if text_was_being_changed_state: event.accept() else: super(PyScientificSpinBox, self).keyPressEvent(event) # Can't seem to do the same thing (stop short timer) for wheelEvent def keyReleaseEvent(self, event): if not event.isAutoRepeat() and event.key() in [ QtCore.Qt.Key_Up, QtCore.Qt.Key_Down, QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown ]: self._valueChange_timer.stop() self._do_valueChanged_gui_emit() super(PyScientificSpinBox, self).keyReleaseEvent(event) def mouseReleaseEvent(self, event): if event.button() == QtCore.Qt.LeftButton: self._valueChange_timer.stop() self._do_valueChanged_gui_emit() super(PyScientificSpinBox, self).mouseReleaseEvent(event) def mousePressEvent(self, event): self._last_focus_reason_mouse = False super(PyScientificSpinBox, self).mousePressEvent(event) def focusInEvent(self, event): if event.reason() == QtCore.Qt.MouseFocusReason: self._last_focus_reason_mouse = True l = self.lineEdit() super(PyScientificSpinBox, self).focusInEvent(event) # Using tab to activate widget already does this. # It is necessary so that pressing the up/down buttons directly from another widget # does not increment before the unit (because position is 0, selection is False) self.selectAll() def editFinished_slot(self): self.text_is_being_changed_state = False if self._last_key_pressed_is_enter: self._last_key_pressed_is_enter = False self._do_valueChanged_gui_emit(force=True) def eventFilter(self, watched_obj, event): if event.type( ) == QEvent.MouseButtonPress and self._last_focus_reason_mouse: self._last_focus_reason_mouse = False return True return False def handle_copy(self): #print('handling copy') self.clipboard.setText('%r' % self.value()) def handle_paste(self): #print('handling paste') text = self.clipboard.text() # let user decide if it is ok by pressing ok (not esc) self.lineEdit().setText(text) self.text_is_being_changed_state = True # update value immediately #try: # self.setValue(float(text)) #except ValueError: # maybe text is not pure number (contains units) # self.lineEdit().setText(text) # self.interpretText() def handle_display_mode(self, action): if self._in_config_menu: return mode = [k for k, v in self.display_mode_option.items() if v == action][0] self.display_mode = mode def handle_fixed_exponent(self, action_or_val): if self._in_config_menu: return if isinstance(action_or_val, QAction): enable = False action = [ k for k, v in self.fixed_exponent_actions.items() if v == action_or_val ][0] if action == 'disabled': self.fixed_exponent = 9999 elif action == 'custom': enable = True self.fixed_exponent = self.fixed_exponent_custom_widget.value() else: self.fixed_exponent = action self.fixed_exponent_custom_action.setEnabled(enable) else: self.fixed_exponent = action_or_val def handle_precision(self, action_or_val): if self._in_config_menu: return if isinstance(action_or_val, QAction): enable = False action = [ k for k, v in self.precision_actions.items() if v == action_or_val ][0] if action == 'custom': enable = True self.precision = self.precision_custom_widget.value() else: self.precision = action self.precision_custom_action.setEnabled(enable) else: self.precision = action_or_val def handle_min_custom_widget(self, val): if self._in_config_menu: return self.setMinimum(val) def handle_max_custom_widget(self, val): if self._in_config_menu: return self.setMaximum(val) def handle_min_incr_custom_widget(self, val): if self._in_config_menu: return self.min_increment = val def setSuffix(self, string): self.validator.suffix = string super(PyScientificSpinBox, self).setSuffix(string) self.updateGeometry() def setPrefix(self, string): self.validator.prefix = string super(PyScientificSpinBox, self).setPrefix(string) self.updateGeometry() def contextMenuEvent(self, event): if self._disable_context_menu: return self._in_config_menu = True menu = self._create_menu() se = self.stepEnabled() self.stepUpAction.setEnabled(se & QAbstractSpinBox.StepUpEnabled) self.stepDownAction.setEnabled(se & QAbstractSpinBox.StepDownEnabled) mode = self.display_mode self.display_mode_option[mode].setChecked(True) self.log_increment_mode_action.setChecked(self.log_increment_mode) self.accel_action.setChecked(self.isAccelerated()) # fixed_exponent fe = self.fixed_exponent if fe == 9999: fe = 'disabled' action = self.fixed_exponent_actions.get(fe, None) fe_action_custom = self.fixed_exponent_actions['custom'] if fe_action_custom.isChecked() or action is None: action = fe_action_custom self.fixed_exponent_custom_action.setEnabled(True) self.fixed_exponent_custom_widget.setValue(fe) else: self.fixed_exponent_custom_action.setEnabled(False) action.setChecked(True) # precision prec = self.precision action = self.precision_actions.get(prec, None) prec_action_custom = self.precision_actions['custom'] if prec_action_custom.isChecked() or action is None: action = prec_action_custom self.precision_custom_action.setEnabled(True) self.precision_custom_widget.setValue(prec) else: self.precision_custom_action.setEnabled(False) action.setChecked(True) #min conf = self.get_config() del conf['prefix'], conf['maximum'], conf['minimum'], conf[ 'min_increment'] conf['keyboardTracking'] = False self.min_custom_widget.pyqtConfigure(value=self.minimum(), **conf) self.max_custom_widget.pyqtConfigure(value=self.maximum(), **conf) self.min_incr_custom_widget.pyqtConfigure(value=self.min_increment, minimum=0., **conf) self._in_config_menu = False menu.exec_(event.globalPos()) def validate(self, text, position): if self._initializing: return QValidator.Invalid, text, position return self.validator.validate(text, position) def fixup(self, text): return self.validator.fixup(text) def valueFromText(self, text): val, unit, scale = self.validator.valueFromText(text) if unit: if val == 0: self.text_formatter.decode_eng(scale) # update last_exp return val def textFromValue(self, value, tmp=False): #mode = self.display_mode_grp.checkedAction() mode = self.display_mode fmt = self.text_formatter fmt_func_d = { self.Sci_Mode.ENG_UNIT: fmt.to_eng_unit, self.Sci_Mode.ENG: fmt.to_eng, self.Sci_Mode.SCI: fmt.to_float } suffix = self.suffix() ret = fmt_func_d[mode](value, suffix, tmp) return ret # this is needed because setAccelerated is not a slot, which makes # using it connect block deletes. wrapping it in python makes it work properly. def setAccelerated(self, val): if self._in_config_menu: return super(PyScientificSpinBox, self).setAccelerated(val) def log_increment_mode_change(self, val): if self._in_config_menu: return self.log_increment_mode = val def setSingleStep_bounded(self, incr): incr = max(incr, self.min_increment) self.setSingleStep(incr) @pyqtSlot(int) def stepBy(self, steps): pos = self.lineEdit().cursorPosition() if not self.lineEdit().hasSelectedText(): text = self.lineEdit().text() special = self.specialValueText() if not (special and special == text): incr, log_incr = self.validator.find_increment(text, pos) self.setSingleStep_bounded(incr) if log_incr is not None: self.log_increment = log_incr if self.log_increment_mode: #frac = self._adapative_step_frac #frac = .01 frac = self.log_increment frac_exp = np.floor(np.log10(frac)) cval = self.value() step_dir = np.sign(steps) val_dir = np.sign(cval) min_increment = self.min_increment for i in range(np.abs(steps)): cval = self.value() val_dir = np.sign(cval) abs_cval = np.abs(cval) abs_increase = val_dir == step_dir if cval == 0: incr = min_increment elif abs_cval < min_increment: if abs_increase: incr = min_increment - cval else: #incr = abs_cval self.setValue(0) continue else: incr = abs_cval * frac cval_exp = np.log10(abs_cval) incr_exp = np.floor(cval_exp + frac_exp) incr = 10.**incr_exp if not abs_increase: incr_1 = 10.**(incr_exp - 1) if np.log10(np.abs(cval) - incr_1) < np.floor(cval_exp): incr = incr_1 self.setSingleStep_bounded(incr) super(PyScientificSpinBox, self).stepBy(step_dir) else: super(PyScientificSpinBox, self).stepBy(steps) def _do_valueChanged_gui_emit(self, val=None, force=False): if val is None: val = self._current_valueChanged_gui if val != self._last_valueChanged_gui or force: self._last_valueChanged_gui = val self.valueChanged_gui.emit(val) else: self._valueChange_timer.stop() @pyqtSlot(float) def _valueChanged_helper(self, val): if not self._in_setValue_quiet: self._current_valueChanged_gui = val if not self._valueChange_timer.isActive(): self._do_valueChanged_gui_emit(val) if self.valueChange_gui_time > 0: self._valueChange_timer.start() def setValue_quiet(self, val): self._in_setValue_quiet = True self.setValue(val) self._in_setValue_quiet = False def _sizeHint_helper(self, minimum=False): self.ensurePolished() # code from widgets/qabstractspinbox.cpp fm = self.fontMetrics() # migth need a copy try: fmw = fm.horizontalAdvance except AttributeError: fmw = fm.width h = self.lineEdit().sizeHint().height() w = 0 if minimum: fixedContent = self.prefix() + " " else: fixedContent = self.prefix() + self.suffix() + " " val = -988.888e-99 if self.fixed_exponent != 9999: if self.maximum() != np.inf: val = self.maximum() else: val = -999888. * 10.**self.fixed_exponent s = self.textFromValue(val, tmp=True) s += fixedContent w = fmw(s) special = self.specialValueText() if special: w = max(w, fmw(special)) w += 2 # cursor blinking space hint = QSize(w, h) opt = QStyleOptionSpinBox() self.initStyleOption(opt) style = self.style() hint = style.sizeFromContents(style.CT_SpinBox, opt, QSize(w, h), self) qapp = QApplication.instance() hint = hint.expandedTo(qapp.globalStrut()) return hint def minimumSizeHint(self): return self._sizeHint_helper(minimum=True) def sizeHint(self): return self._sizeHint_helper() # Observations from QDoubleSpinBox code: # minimumSizeHint is the same as SizeHint less the space for suffix # sizeHint # # internal commands is space of prefix+suffix+max(min, max, special)+graphical elements # # note that text is limited to 18 char. # sizeHint is recalculated and advertised when calling setSuffix (doubleSpinBox.setPrefix seems to only update text) # aslo setMinimum, setMaximum, setRange all update the geometry # setSpecialValueText, setValue, setSingleStep and setSuffix all update the text help_text = u""" For entry you can use scientific entry (like 1e-3) or units. You can add spaces. The available units are (they are case sensitive): Y(1e24), Z, E, P, T, G, M, k(1e3), h(100), da(10), d(0.1), c, m, µ, n, p, f, a, z, y(1e-24) you can use u instead of µ, and b for no unit (1) The arrows, or the mouse wheel steps the value. Page up/down steps the value 10 times faster. Using the CTRL key also goes 10 times faster but only for wheel events. If you change the cursor position (left/right keys or mouse click) before stepping, the number to the left of the cursor is incremented by 1. Adapatative option will increase in a logarithmic way. Acceleration is a speed up of the steps when pressing the GUI button. """ def help_dialog(self): dialog = QMessageBox.information(self, 'Entry Help', self.help_text)
class Widget(QMainWindow): loaded_signal = pyqtSignal() progress_bar = pyqtSignal(int) exception_signal = pyqtSignal(Exception) def __init__(self): super().__init__() self.pbar = QProgressBar(self) self.pbar.setGeometry(30, 40, 200, 25) self.pbar.setValue(0) self.pbar.hide() self.isOpened = False self.is_loading = False self.img = QLabel() self.form_widget = CentralWidget(self) self.setCentralWidget(self.form_widget) self.progress_bar.connect(self.pbar.setValue) self.exception_signal.connect(self.show_error) self.loaded_signal.connect(self.set_gif_settings) self.gifinfo = None self.gif_id = 0 self.create_menubar() self.scrTimer = QTimer(self) self.scrTimer.setInterval(50) self.scrTimer.timeout.connect(self.timerEvent) self.pal = self.form_widget.isPaused.palette() self.pal.setColor(QPalette.WindowText, QColor("red")) self.form_widget.isPaused.setPalette(self.pal) self.frames = [QPixmap()] def show_error(self, e): self.test = QMessageBox(self) self.test.setWindowModality(Qt.ApplicationModal) self.test.setWindowTitle('Error') self.test.setIcon(QMessageBox.Critical) self.test.setText(str(e)) self.test.setStandardButtons(QMessageBox.Ok) self.test.show() self.pbar.hide() self.is_loading = False def create_menubar(self): main_menu = self.menuBar() file_menu = main_menu.addMenu('File') open_button = QAction('Open', self) open_button.setShortcut('Ctrl+O') open_button.triggered.connect(self.open_gif) close_button = QAction('Close', self) close_button.setShortcut('Ctrl+C') close_button.triggered.connect(self.close_gif) file_menu.addAction(open_button) file_menu.addAction(close_button) frame_menu = main_menu.addMenu('Frame') prev_button = QAction('Prev', self) prev_button.setShortcut('Left') prev_button.triggered.connect(self.prev_frame) pause_button = QAction('Play/Pause', self) pause_button.setShortcut('Space') pause_button.triggered.connect(self.pause_gif) next_button = QAction('Next', self) next_button.setShortcut('Right') next_button.triggered.connect(self.next_frame) frame_menu.addAction(prev_button) frame_menu.addAction(pause_button) frame_menu.addAction(next_button) play_menu = main_menu.addMenu('Play') dspeed_button = QAction('Speed -', self) dspeed_button.setShortcut('-') dspeed_button.triggered.connect(self.dspeed_gif) uspeed_button = QAction('Speed +', self) uspeed_button.setShortcut('+') uspeed_button.triggered.connect(self.uspeed_gif) play_menu.addAction(dspeed_button) play_menu.addAction(uspeed_button) return main_menu def open_gif(self): if self.is_loading: return self.is_loading = True self.fname = QFileDialog.getOpenFileName( self, 'Open file', options=QFileDialog.DontUseNativeDialog)[0] if not self.fname: self.is_loading = False return self.close_gif() self.pbar.show() self.update() self.t = SomeThread(self.fname, self) self.t.start() def set_gif_settings(self): self.form_widget.isPaused.setText('Play') self.pal.setColor(QPalette.WindowText, QColor("green")) self.form_widget.isPaused.setPalette(self.pal) fps = round(1000 / self.scrTimer.interval()) self.form_widget.fps.setText('FPS: {}'.format(fps)) frames_length = len(self.gifinfo.frames) self.form_widget.frames.setText('Frame: 1/{}'.format(frames_length)) self.isOpened = True width = int(self.gifinfo.width, 16) height = int(self.gifinfo.height, 16) width = width + 18 if width + 18 > 300 else 300 height = height + 74 if height + 74 > 300 else 300 self.setMinimumSize(width, height) self.resize(width, height) self.setWindowTitle(self.fname) self.pbar.hide() self.pbar.setValue(0) self.scrTimer.start() self.is_loading = False def close_gif(self): self.scrTimer.stop() self.gif_id = 0 self.isOpened = False self.frames = [QPixmap(QImage().fill(QColor('#ffffff')))] self.form_widget.fps.setText('FPS: 0') self.form_widget.frames.setText('Frames: 0/0') self.form_widget.isPaused.setText('Pause') self.pal.setColor(QPalette.WindowText, QColor("red")) self.form_widget.isPaused.setPalette(self.pal) self.setMinimumSize(300, 300) self.resize(300, 300) self.setWindowTitle('GIF') self.update() def pause_gif(self): if not self.isOpened: return if self.scrTimer.isActive(): self.scrTimer.stop() else: self.scrTimer.start() self.change_pause_text() def change_pause_text(self): if self.scrTimer.isActive(): self.form_widget.isPaused.setText('Play') self.form_widget.isPaused.adjustSize() self.pal.setColor(QPalette.WindowText, QColor("green")) self.form_widget.isPaused.setPalette(self.pal) else: self.form_widget.isPaused.setText('Pause') self.form_widget.isPaused.adjustSize() self.pal.setColor(QPalette.WindowText, QColor("red")) self.form_widget.isPaused.setPalette(self.pal) def prev_frame(self): if not self.isOpened: return self.scrTimer.stop() if self.gif_id: self.gif_id -= 1 else: self.gif_id = len(self.gifinfo.images) - 1 self.set_frames_text() self.change_pause_text() self.update() def next_frame(self): if not self.isOpened: return self.scrTimer.stop() self.change_pause_text() if self.gif_id != len(self.gifinfo.frames) - 1: self.gif_id += 1 else: self.gif_id = 0 self.set_frames_text() self.update() def set_frames_text(self): frames_text = 'Frame: {}/{}'.format(self.gif_id + 1, len(self.gifinfo.frames)) self.form_widget.frames.setText(frames_text) def set_fps_text(self): fps_text = 'FPS: {}'.format(round(1000 / self.scrTimer.interval())) self.form_widget.fps.setText(fps_text) def uspeed_gif(self): if not self.isOpened: return current_interval = self.scrTimer.interval() possible_fps = 1000 / current_interval + 1 if possible_fps > 20: self.scrTimer.setInterval(50) else: self.scrTimer.setInterval(round(1000 / possible_fps)) self.set_fps_text() def dspeed_gif(self): if not self.isOpened: return current_interval = self.scrTimer.interval() possible_fps = 1000 / current_interval - 1 if possible_fps < 1: self.scrTimer.setInterval(1000) else: self.scrTimer.setInterval(round(1000 / possible_fps)) self.set_fps_text() def timerEvent(self): self.gif_id = (self.gif_id + 1) % len(self.gifinfo.images) self.set_frames_text() self.update() def paintEvent(self, event): super().paintEvent(event) self.img.setPixmap(self.frames[self.gif_id]) def get_all_pixmaps(self): result = [] count = len(self.gifinfo.frames) all_percents = 70 z = 0 all = int(self.gifinfo.height, 16) * count for t in self.gifinfo.frames: frame = QImage(int(self.gifinfo.width, 16), int(self.gifinfo.height, 16), QImage.Format_RGB32) y = 0 for i in t: x = 0 for j in i: frame.setPixelColor(x, y, QColor('#' + j)) x += 1 y += 1 z += 1 self.progress_bar.emit(all_percents * z / all + 30) result.append(QPixmap(frame)) self.progress_bar.emit(all_percents * z / all + 30) return result
class MarkedHistogram(QWidget): """Histogram with color-indication markers MarkedHistogram shows a histogram of a data set and an optional label for the numeric range of the data set. Color markers can be placed on the histogram by the user and moved interactively, either with the mouse or by typing in a particular data value. A color button is used to control the color of the "current" marker (the one most recently selected with the mouse). Markers can either be vertical bars or squares. Vertical bars can only be moved left/right whereas squares can also be moved up/down. Squares are also connected from left to right by line segments. A row of associated widgets (such as the marker color button) is placed below the histogram. User-specified widgets can also be placed in this row with the add_custom_widget() method. Individual markers are grouped into HistogramMarkers instances, and several HistogramMarkers instances can be associated with a single histogram, though only one instance is active/shown at a time. MarkedHistogram has the following constructor options: [Options noted as init options can only be specified at widget creation. Others can be changed later via the corresponding property name.] color_button -- controls whether a color button is offered in the user interface for changing marker colors. default: True data_source -- either a string or a 3-tuple. If a string, then no histogram is displayed and instead the string is displayed in the histogram area as a text message. The first 2 components of a 3-tuple should be the minimum and maximum values of the histogram, The third component should either be an array of numbers (i.e. the histogram) or a callback function that takes as its single argument the number of bins to histogram into and that returns a histogram array. default: 'no data' layout -- [init option] how to organize the megawidget layout. Choices are 'single', 'top', and 'below'. 'single' should be used when you are using a single histogram in your GUI, or histograms that aren't arrayed vertically. 'top' and 'below' should be used for histograms that are laid out in a vertical array ('top' for the top-most one, and 'below' for all others). Certain repeated elements will be omitted in 'below' histograms (e.g. some widget labels). default: single max_label/min_label [init options] show the max/min histogram values as labels below the histogram on the right/left. If neither is True, then the range will be shown in a single label widget below the histogram. default: False redraw_delay -- amount of time (in seconds) to delay between needing to redraw and actually starting to redraw. Used to avoid multiple (possibly slow) redraws during a resize. default: 0.25 scaling -- how to scale the vertical height of the histogram. Either 'logarithmic' or 'linear'. default: logarithmic select_callback -- [init option] function to call when the "current" marker changes. The function receives 4 argments: previous active marker set/marker, new active marker set/marker. The marker set can be None to indicate no active marker set and the marker can be None if no marker was/is current. show_marker_help -- [init option] whether to show the help text over the histogram describing how to add/delete markers. default: True status_line -- function to use to output messages (such as warning when trying to add more markers than allowed). The function should take a single string argument. default: None value_label -- [init option] label to use next to the entry widget describing the current marker value. default: 'Value' value_width -- width of the current marker value entry widget. default: 7 Constructor options that begin with 'Markers_' specify default constructor options for HistogramMarkers objects created in the add_markers method (e.g. Markers_connect_color='red' will supply connect_color='red' to the HistogramMarkers constructor). Options for specific instances can still be provided to the add_ markers() method as keyword arguments (without the 'Markers_' prefix). """ def __init__(self, *args, color_button=True, data_source='no data', layout='single', max_label=False, min_label=False, redraw_delay=0.25, scaling='logarithmic', select_callback=None, show_marker_help=True, status_line=None, value_label='Value', value_width=7, **kw): # Get HistogramMarkers options and initialise base class self._histogram_markers_kw = markers_kw = {} for opt_name in list(kw.keys()): if opt_name.startswith('Markers_'): markers_kw[opt_name[8:]] = kw.pop(opt_name) super().__init__(*args, **kw) # initialize variables self._layout = layout self.status_line = status_line self._show_marker_help = show_marker_help self._active_markers = None self._markers = [] self._markable = False self._drag_marker = None self._scaling = scaling self._select_callback = select_callback if select_callback: self._prev_markers = None self._prev_marker = None overall_layout = QVBoxLayout() self.setLayout(overall_layout) # Create the add/delete marker help if show_marker_help and layout != 'below': self._marker_help = QLabel( "Ctrl-click on histogram to add or delete thresholds") self._marker_help.setAlignment(Qt.AlignCenter) overall_layout.addWidget(self._marker_help) else: self._marker_help = None # Create the data area class HistFrame(QFrame): def sizeHint(self): return QSize(300, 64) data_frame = QFrame() data_frame.setLineWidth(1) data_frame.setMidLineWidth(2) data_frame.setFrameStyle(data_frame.Panel | data_frame.Sunken) data_frame.setContentsMargins(0, 0, 0, 0) data_frame_layout = QHBoxLayout() data_frame.setLayout(data_frame_layout) self._data_widgets = QStackedWidget() data_frame_layout.addWidget(self._data_widgets) # Crate the histogram widget self._hist_scene = QGraphicsScene() self._hist_bars = self._hist_scene.createItemGroup([]) self._hist_view = QGraphicsView(self._hist_scene) self._hist_view.resizeEvent = self._redraw self._hist_scene.mousePressEvent = lambda event: self._add_or_delete_marker_cb(event) \ if event.modifiers() & mod_key_info("control")[0] else self._select_marker_cb(event) self._hist_scene.mouseMoveEvent = lambda event: self._move_marker_cb(event) \ if self._drag_marker else super().mouseMoveEvent(event) self._hist_scene.mouseReleaseEvent = self._button_up_cb self._redraw_timer = QTimer() self._redraw_timer.timeout.connect(self._redraw_cb) self._redraw_timer.start(1000 * redraw_delay) self._redraw_timer.stop() self._data_widgets.addWidget(self._hist_view) # Create the histogram replacement label self._no_histogram_label = QLabel() self._no_histogram_label.setAlignment(Qt.AlignCenter) self._data_widgets.addWidget(self._no_histogram_label) overall_layout.addWidget(self._data_widgets, stretch=1) # Create range label(s) self._widget_area = QWidget() self._widget_layout = QHBoxLayout() self._min_label = self._max_label = None if min_label or max_label: min_max_layout = QHBoxLayout() if min_label: self._min_label = QLabel() min_max_layout.addWidget(self._min_label, alignment=Qt.AlignLeft & Qt.AlignTop) if max_label: self._max_label = QLabel() min_max_layout.addWidget(self._max_label, alignment=Qt.AlignRight & Qt.AlignTop) overall_layout.addLayout(min_max_layout) else: self._range_label = QLabel() if layout == 'below': self._widget_layout.addWidget(self._range_label) else: lab = QLabel("Range") if layout == 'single': self._widget_layout.addWidget(lab, alignment=Qt.AlignRight) self._widget_layout.addWidget(self._range_label, alignment=Qt.AlignLeft) else: # layout == 'top' range_layout = QVBoxLayout() range_layout.addWidget(lab, alignment=Qt.AlignBottom) range_layout.addWidget(self._range_label, alignment=Qt.AlignTop) self._widget_layout.addLayout(range_layout) self._widget_area.setLayout(self._widget_layout) overall_layout.addWidget(self._widget_area) # Create value widget self._value_entry = QLineEdit() self._value_entry.setEnabled(False) self._value_entry.returnPressed.connect(self._set_value_cb) self.value_width = value_width if layout == 'below': self._widget_layout.addWidget(self._value_entry) else: lab = QLabel(value_label) if layout == 'single': self._widget_layout.addWidget(lab, alignment=Qt.AlignRight) self._widget_layout.addWidget(self._value_entry, alignment=Qt.AlignLeft) else: value_layout = QVBoxLayout() value_layout.addWidget(lab, alignment=Qt.AlignBottom) value_layout.addWidget(self._value_entry, alignment=Qt.AlignTop) self._widget_layout.addLayout(value_layout) # Create color button widget from .color_button import ColorButton self._color_button = cb = ColorButton() cb.color_changed.connect( lambda rgba8: self._color_button_cb([c / 255.0 for c in rgba8])) cb.setEnabled(False) self._color_button_label = cbl = QLabel("Color") if layout == 'below': self._widget_layout.addWidget(self._color_button) else: if layout == 'single': self._widget_layout.addWidget(cbl, alignment=Qt.AlignRight) self._widget_layout.addWidget(cb, alignment=Qt.AlignLeft) else: color_layout = QVBoxLayout() color_layout.addWidget(cbl, alignment=Qt.AlignBottom) color_layout.addWidget(cb, alignment=Qt.AlignTop) self._widget_layout.addLayout(color_layout) self._color_button_shown = True # Show the histogram or the no-data label self.data_source = data_source def activate(self, markers): """Make the given set of markers the currently active set Any previously-active set will be hidden. """ if markers is not None and markers not in self._markers: raise ValueError("activate() called with bad value") if markers == self._active_markers: return if self._active_markers is not None: self._active_markers._hide() elif self.layout != 'below' and self._show_marker_help: self._marker_help.setHidden(False) self._active_markers = markers if self._active_markers is not None: self._active_markers.shown = True self._set_sel_marker(self._active_markers._sel_marker) else: if self.layout != 'below' and self._show_marker_help: self._marker_help.setHidden(True) if self._select_callback: if self._prev_marker is not None: self._select_callback(self._prev_markers, self._prev_marker, None, None) self._prev_markers = None self._prev_marker = None def add_custom_widget(self, widget, left_side=True): self._widget_layout.addWidget(0 if left_side else -1, widget) def add_markers(self, activate=True, **kw): """Create and return a new set of markers. If 'activate' is true, the new set will also become the active set. Other keyword arguments will be passed to the HistogramMarkers constructor. """ final_kw = {k: v for k, v in self._histogram_markers_kw.items()} final_kw.update(kw) final_kw['histogram'] = self markers = HistogramMarkers(**final_kw) self._markers.append(markers) if activate: self.activate(markers) return markers @property def color_button(self): return self._color_button_shown @color_button.setter def color_button(self, show): if show == self._color_button_shown: return if self.layout != 'below': self._color_button_label.setHidden(not show) self._color_button.setHidden(not show) self._color_button_shown = show def current_marker_info(self): """Identify the marker currently selected by the user. Returns a HistogramMarkers instance and a marker. The instance will be None if no marker set has been activated. The marker will be None if no marker has been selected by the user. """ if self._active_markers is None: return None, None return self._active_markers, self._active_markers._sel_marker @property def data_source(self): return self._data_source @data_source.setter def data_source(self, data_source): self._data_source = data_source self._new_data() def delete_markers(self, markers): """Delete the given set of markers. If the markers were active, there will be no active set of markers afterward. """ if markers not in self._markers: raise ValueError("Bad value for delete()") if markers == self._active_markers: self.activate(None) self._markers.remove(markers) @property def layout(self): return self._layout @property def redraw_delay(self): return self._redraw_timer.interval() / 1000.0 @redraw_delay.setter def redraw_delay(self, secs): self._redraw_timer.setInterval(secs * 1000) @property def scaling(self): return self._scaling @scaling.setter def scaling(self, scaling): if self._scaling != scaling: self._scaling = scaling self._redraw_cb() def snapshot_data(self): info = { 'version': 1, 'draw_min': self._draw_min, 'draw_max': self._draw_max, 'markers': [markers.snapshot_data() for markers in self._markers], } if self._active_markers is None: info['active markers'] = None else: info['active markers'] = self._markers.index(self._active_markers) if self['color_button']: info['color well'] = self._color_button.color else: info['color well'] = None return info def snapshot_restore(self, data): self._draw_min = data['draw_min'] self._draw_max = data['draw_max'] if data['color well'] is not None: self._color_button.color = data['color well'] if len(data['markers']) != len(self._markers): # don't know how to deal with this situation return for markers, markers_data in zip(self._markers, data['markers']): markers.snapshot_restore(markers_data) if data['active markers'] is not None: self.activate(self._markers[data['active markers']]) self._set_sel_marker(self._active_markers._sel_marker) @property def value_width(self): return self._value_width @value_width.setter def value_width(self, vw): self._value_width = vw ve = self._value_entry fm = ve.fontMetrics() tm = ve.textMargins() cm = ve.contentsMargins() w = vw * fm.width('w') + tm.left() + tm.right() + cm.left() + cm.right( ) + 8 ve.setMaximumWidth(w) def _abs2rel(self, abs_xy): x, y = abs_xy rel_x = (x - self._min_val) / float(self._max_val - self._min_val) rel_y = y / float(self._ymax) return rel_x, rel_y def _abs_xy(self, scene_xy): scene_x, scene_y = scene_xy dy = min(max(self._bottom - scene_y, 0), self._hist_height - 1) if self.scaling == 'logarithmic': exp = dy / float(self._hist_height - 1) abs_y = (self._max_height + 1)**exp - 1 else: abs_y = self._max_height * dy / float(self._hist_height - 1) cx = scene_x - self._border num_bins = len(self._bins) if num_bins == self._hist_width: fract = cx / (num_bins - 1) abs_x = self._min_val + fract * (self._max_val - self._min_val) elif num_bins == 2: abs_x = self._min_val + (self._max_val - self._min_val) * ( 2 * cx / self._hist_width - 0.5) else: extra = self._hist_width / (2.0 * (num_bins - 1)) abs_x = self._min_val + (self._max_val - self._min_val) * ( cx - extra) / (self._hist_width - 2.0 * extra) abs_x = max(self._min_val, abs_x) abs_x = min(self._max_val, abs_x) return abs_x, abs_y def _add_or_delete_marker_cb(self, event=None): if self._active_markers is None: return marker = self._active_markers._pick_marker(event.scenePos()) if marker is None: max_marks = self._active_markers.max_marks if max_marks is not None and len( self._active_markers) >= max_marks: if self.status_line: self.status_line("Maximum of %d markers\n" % max_marks) return xy = self._abs_xy((event.scenePos().x(), event.scenePos().y())) if self._active_markers.coord_type == 'relative': xy = self._abs2rel(xy) sel_marker = self._active_markers._sel_marker if sel_marker: color = sel_marker.rgba else: color = self._active_markers.new_color marker = self._active_markers.append((xy, color)) self._set_sel_marker(marker, drag_start=event) else: min_marks = self._active_markers.min_marks if min_marks is not None and len( self._active_markers) <= min_marks: if self.status_line: self.status_line("Minimum of %d markers\n" % min_marks) return self._active_markers.remove(marker) self._set_sel_marker(None) def _button_up_cb(self, event=None): if self._drag_marker: self._drag_marker = None if self._active_markers.move_callback: self._active_markers.move_callback('end') def _scene_xy(self, abs_xy): # minimum is in the _center_ of the first bin, # likewise, maximum is in the center of the last bin abs_x, abs_y = abs_xy abs_y = max(0, abs_y) abs_y = min(self._max_height, abs_y) if self.scaling == 'logarithmic': import math abs_y = math.log(abs_y + 1) scene_y = self._bottom - (self._hist_height - 1) * (abs_y / self._max_height) abs_x = max(self._min_val, abs_x) abs_x = min(self._max_val, abs_x) num_bins = len(self._bins) if num_bins == self._hist_width: bin_width = (self._max_val - self._min_val) / float(num_bins - 1) left_edge = self._min_val - 0.5 * bin_width scene_x = int((abs_x - left_edge) / bin_width) else: # histogram is effectively one bin wider (two half-empty bins on each end) if num_bins == 1: scene_x = 0.5 * (self._hist_width - 1) else: extra = (self._max_val - self._min_val) / (2.0 * (num_bins - 1)) eff_min_val = self._min_val - extra eff_max_val = self._max_val + extra eff_range = float(eff_max_val - eff_min_val) scene_x = (self._hist_width - 1) * (abs_x - eff_min_val) / eff_range return self._border + scene_x, scene_y def _color_button_cb(self, rgba): m = self._active_markers._sel_marker if not m: if self.status_line: self.status_line("No marker selected") return m.rgba = rgba def _marker2abs(self, marker): if self._active_markers.coord_type == 'absolute': return marker.xy else: return self._rel2abs(marker.xy) def _move_cur_marker(self, x, yy=None): # # Don't allow dragging out of the scene box. # m = self._active_markers._sel_marker if x < self._min_val: x = self._min_val elif x > self._max_val: x = self._max_val if yy is None: y = m.xy[1] else: y = yy if y < 0: y = 0 elif y > self._ymax: y = self._ymax if self._active_markers.coord_type == 'absolute': m.xy = (x, y) else: m.xy = self._abs2rel((x, y)) if yy is None: m.xy = (m.xy[0], y) self._set_value_entry(x) self._active_markers._update_plot() if self._active_markers.move_callback: self._active_markers.move_callback(m) def _move_marker_cb(self, event): mouse_xy = self._abs_xy((event.scenePos().x(), event.scenePos().y())) dx = mouse_xy[0] - self._last_mouse_xy[0] dy = mouse_xy[1] - self._last_mouse_xy[1] self._last_mouse_xy = mouse_xy if event.modifiers() & mod_key_info("shift")[0]: dx *= .1 dy *= .1 m = self._drag_marker mxy = self._marker2abs(m) x, y = mxy[0] + dx, mxy[1] + dy self._move_cur_marker(x, y) def _new_data(self): ds = self.data_source if isinstance(ds, str): self._no_histogram_label.setText(ds) self._data_widgets.setCurrentWidget(self._no_histogram_label) if self._min_label: self._min_label.setText("") if self._max_label: self._max_label.setText("") if self.layout != 'below' and self._show_marker_help: self._marker_help.setHidden(True) self._widget_area.setHidden(True) else: self._data_widgets.setCurrentWidget(self._hist_view) if self.layout != 'below' and self._show_marker_help: self._marker_help.setHidden(False) self._widget_area.setHidden(False) self._draw_min = self._draw_max = None self._redraw_cb() def _redraw(self, event=None): self._markable = False self._redraw_timer.start() def _redraw_cb(self): self._redraw_timer.stop() ds = self.data_source if isinstance(ds, str): # displaying a text label right now return view = self._hist_view scene = self._hist_scene hist_size = view.viewport().size() self._hist_width, self._hist_height = hist_width, hist_height = hist_size.width( ), hist_size.height() self._min_val, self._max_val, self._bins = ds filled_range = self._max_val - self._min_val empty_ranges = [0, 0] if self._draw_min != None: empty_ranges[0] = self._min_val - self._draw_min self._min_val = self._draw_min if self._draw_max != None: empty_ranges[1] = self._draw_max - self._max_val self._max_val = self._draw_max if callable(self._bins): if empty_ranges[0] or empty_ranges[1]: full_range = filled_range + empty_ranges[0] + empty_ranges[1] filled_bins = self._bins( int(hist_width * filled_range / full_range)) left = [0] * int(hist_width * empty_ranges[0] / full_range) right = [0] * (hist_width - len(filled_bins) - len(left)) self._bins = left + filled_bins + right else: self._bins = self._bins(hist_width) elif empty_ranges[0] or empty_ranges[1]: full_range = filled_range + empty_ranges[0] + empty_ranges[1] left = [0] * int(len(self._bins) * empty_ranges[0] / full_range) right = [0] * int(len(self._bins) * empty_ranges[1] / full_range) self._bins = left + self._bins + right if self._min_label: self._min_label.setText(self._str_val(self._min_val)) if self._max_label: self._max_label.setText(self._str_val(self._max_val)) if not self._min_label and not self._max_label: self._range_label.setText( "%s - %s" % (self._str_val(self._min_val), self._str_val(self._max_val))) bars = self._hist_bars.childItems() for bar in bars: self._hist_bars.removeFromGroup(bar) self._hist_scene.removeItem(bar) self._ymax = max(self._bins) if self.scaling == 'logarithmic': from numpy import array, log, float32 self._bins = array(self._bins, float32) self._bins += 1.0 log(self._bins, self._bins) max_height = max(self._bins) self._max_height = max_height h_scale = float(hist_height - 1) / max_height self._border = border = 0 bottom = hist_height + border - 1 self._bottom = bottom num_bins = len(self._bins) if num_bins == hist_width: for b, n in enumerate(self._bins): x = border + b h = int(h_scale * n) line = self._hist_scene.addLine(x, bottom, x, bottom - h) self._hist_bars.addToGroup(line) line.setZValue(-1) # keep bars below markers else: x_scale = (hist_width - 1) / float(num_bins) for b, n in enumerate(self._bins): x1 = border + b * x_scale x2 = border + (b + 1) * x_scale h = int(h_scale * n) rect = self._hist_scene.addRect(x1, bottom - h, x2 - x1, h) self._hist_bars.addToGroup(rect) rect.setZValue(-1) # keep bars below markers self._markable = True if self._active_markers is not None: self._active_markers._update_plot() marker = self._active_markers._sel_marker if marker: self._set_value_entry(self._marker2abs(marker)[0]) self._hist_scene.setSceneRect(self._hist_scene.itemsBoundingRect()) def _rel2abs(self, rel_xy): x, y = rel_xy abs_x = self._min_val * (1 - x) + x * self._max_val abs_y = y * self._ymax return abs_x, abs_y def _select_marker_cb(self, event=None): if self._active_markers is not None: marker = self._active_markers._pick_marker(event.scenePos()) self._set_sel_marker(marker, drag_start=event) if marker is not None: return # show value where histogram clicked self._set_value_entry(self._abs_xy((event.scenePos().x(), 0))[0]) def _set_sel_marker(self, marker, drag_start=None): self._active_markers._sel_marker = marker if not marker: self._color_button.color = "gray" self._color_button.setEnabled(False) self._set_value_entry("") self._value_entry.setEnabled(False) else: self._color_button.setEnabled(True) self._color_button.color = marker.rgba self._value_entry.setEnabled(True) self._set_value_entry(self._marker2abs(marker)[0]) if self._select_callback: if marker is not None or self._prev_marker is not None: self._select_callback(self._prev_markers, self._prev_marker, self._active_markers, marker) self._prev_markers = self._active_markers self._prev_marker = marker if not drag_start: return self._drag_marker = marker if not marker: return self._last_mouse_xy = self._abs_xy( (drag_start.scenePos().x(), drag_start.scenePos().y())) if self._active_markers.move_callback: self._active_markers.move_callback('start') def _set_value_cb(self): try: v = eval(self._value_entry.text()) except Exception: raise ValueError("Invalid histogram value") if type(self._min_val) != type(v): v = type(self._min_val)(v) if v < self._min_val: self._draw_min = v self._redraw_cb() elif v > self._max_val: self._draw_max = v self._redraw_cb() self._move_cur_marker(v) def _set_value_entry(self, val): if isinstance(val, str): self._value_entry.setText(val) return if isinstance(self._min_val, int): val = int(val + 0.5) self._value_entry.setText("%g" % val) def _str_val(self, val): if isinstance(val, (int, bool)): return str(val) return "%g" % val
class Player(QWidget): # signal that there is a new frame for the selected universe new_pix = pyqtSignal(QPixmap) new_image = pyqtSignal(QImage) new_load = pyqtSignal() new_frame = pyqtSignal() new_frame_index = pyqtSignal(int) clear = pyqtSignal() __players__ = [] def __init__(self, name, filepath): super(QWidget, self).__init__() self.name = name self._loop = 'repeat' self.frames = 0 self.loop_points = None # set openCV capture self.cap = cv2.VideoCapture() self.autostart = True self.current_frame = None self.timer = QTimer() self.fps = 25 self._play = False self.timer.timeout.connect(self.render_frame) #self.filepath = filepath if filepath: self.load(filepath) else: self.filepath = None self.__players__.append(self) def __repr__(self): return self.name @property def fps(self): return 1000. / self.timer.interval() @fps.setter def fps(self, fps): self.timer.setInterval(1000. / fps) def render_frame(self): ret, frame = self.cap.read() #frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) if ret: img = QImage(frame, frame.shape[1], frame.shape[0], QImage.Format_RGB888) img = img.rgbSwapped() self.image = img self.new_image.emit(img) pix = QPixmap.fromImage(img) current_frame = self.cap.get(1) #print('render frame : ' + str(current_frame)) # emit the new frame signal self.new_pix.emit(pix) self.new_frame.emit() self.new_frame_index.emit(current_frame) if current_frame == self.frames: print(self.loop) if self.loop == 'repeat': self.seek(0) elif self.loop == 'one-shot': self.end_action() else: self.end_action() else: print('nothing to play') def end_action(self, action='eject'): if action == 'loop': self.seek(0) elif action == 'freeze': self.play = False elif action == 'eject': self.eject() def load(self, filepath): # release first the previous playhead if existing if self.cap: self.cap.release() # store filepath self.filepath = filepath try: # open the capture self.cap.open(self.filepath) ret, frame = self.cap.read() if ret: try: # get properties of the movie self.width = self.cap.get(3) self.height = self.cap.get(4) self.frames = self.cap.get(7) self.fps = (self.cap.get(5)) self.loop_points = [0, self.frames] print( self.filepath.split('/')[-1], self.fps, self.width, self.height) self.render_frame() if self.autostart: self.play = True self.new_load.emit() except: print('cannot access to the movie') except: print('cannot open the file') @property def loop(self): return self._loop @loop.setter def loop(self, mode): self._loop = mode @property def loop_points(self): return self._loop_points @loop_points.setter def loop_points(self, points): print('loop_point setter : ', points) self._loop_points = points @property def autostart(self): """ if True, player will start when load a movie if False, player will load the movie and """ return self._autostart @autostart.setter def autostart(self, state): self._autostart = state def seek(self, frame): # check that the frame exists if frame <= self.frames: # set playhead to the desired frame self.cap.set(1, frame) # render the frame self.render_frame() else: print('frame ' + str(frame) + ' does not exist') def pause(self): """ calling this method will pause the media same effect than self.play = 0 """ self.play = False def resume(self): """ calling this methid will play the media same effect than self.play = 1 """ self.play = True @property def play(self): """ The play property if set to True, media is playing if set to False, media is pausing """ return self._play @play.setter def play(self, state): self._play = state if state: self.timer.start() else: self.timer.stop() def eject(self): """ unload the media from the player """ # release the player self.cap.release() self.pause() self.clear.emit()
class ImageSlider(QWidget): def __init__(self): super().__init__() # setting title self.setWindowTitle("Python ") self.setGeometry(1000, 1000, 2000, 100) stylesheett = """ QPushbutton{ height: 50px; width: 50px; background-color: rgb(54,57,63); background-image: url("./Images/left.png"); } QPushbutton#pic:hover{ background-color: rgb(54,57,63); background-image: url("./Images/left_hover.png"); } QPushbutton#pic2{ height: 50px; width: 50px; background-color: rgb(54,57,63); background-image: url("./Images/right.png"); } QPushbutton#pic2:hover{ background-color: rgb(54,57,63); background-image: url("./Images/right_hover.png"); } """ self.setStyleSheet(stylesheett) self.hbox = QHBoxLayout() self.list_widget = QListWidget() self.items = [] # setting flow self.list_widget.setFlow(QListView.LeftToRight) self.list_widget.setIconSize(QtCore.QSize(190, 190)) self.list_widget.hasAutoScroll() self.list_widget.setAutoFillBackground(False) self.pic = QPushButton() self.pic.setObjectName("pic") self.pic.clicked.connect(lambda: self.goleftSmooth()) #use full ABSOLUTE path to the image, not relative self.hbox.addWidget(self.pic) self.hbox.addWidget(self.list_widget) self.pic2 = QPushButton() self.pic2.setObjectName("pic2") self.pic2.clicked.connect(lambda: self.gorightSmooth()) self.hbox.addWidget(self.pic2) self.list_widget.horizontalScrollBar().setDisabled(True) self.list_widget.horizontalScrollBar().hide() self.list_widget.verticalScrollBar().setDisabled(True) self.list_widget.verticalScrollBar().hide() self.list_widget.setHorizontalScrollMode( QAbstractItemView.ScrollPerPixel) self.setLayout(self.hbox) self.atCurrentRight = 16 self.atCurrentLeft = 0 self.timerBaseInterval = 25 self.floorInterval = 5 self.timer = QTimer(self) self.timer.setInterval(self.timerBaseInterval) self.timer.timeout.connect(self.goRight) self.timer2 = QTimer(self) self.timer2.setInterval(self.timerBaseInterval) self.timer2.timeout.connect(self.goLeft) self.rightCounter = 0 self.leftCounter = 0 self.incrementalStep = 1 self.counterSize = 410 self.lingertime = 1 self.lingertimeCounter = 0 self.show() #Takes list of images and adds them to the image container of this class def setImages(self, images, urls, movieIDs, titles): self.list_widget.clear() self.items = [] self.list_widget.setViewMode(QListWidget.IconMode) # self.list_widget.setResizeMode(QListWidget.Adjust); # self.list_widget.setIconSize(QSize(150,150)); # self.list_widget.setAcceptDrops(True); # self.list_widget.setDragEnabled(False); for i in range(len(images)): self.items.append(ClickableThumbnail(urls[i], movieIDs[i])) self.items[i].setText(titles[i]) self.list_widget.addItem(self.items[i]) pm = QPixmap() pm.loadFromData(base64.b64decode(images[i])) ic = QIcon() pm = pm.scaled(QSize(220, 150), Qt.IgnoreAspectRatio) ic.addPixmap(pm) if (ic.isNull() == False): self.items[i].setIcon(ic) #movies images to the right to slide left by initiating a timer that moves the images smoothly by pixels def goleftSmooth(self): self.timer2.start() self.pic.setDisabled(True) self.pic2.setDisabled(True) #movies images to the left to slide right by initiating a timer that moves the images smoothly by pixels def gorightSmooth(self): self.timer.start() self.pic.setDisabled(True) self.pic2.setDisabled(True) #Function that timer2 uses to movie images right #It works by modifying the timer interval (time needed until this function is called again) #starts by high time interval to low then to high (slow fast slow) def goLeft(self): if (self.leftCounter != self.counterSize): if (self.leftCounter < math.ceil(self.counterSize * 0.4)): if (self.lingertime > self.lingertimeCounter): self.lingertimeCounter += 1 else: if (self.timer2.interval() > self.floorInterval): self.timer2.setInterval(self.timer2.interval() - 1) self.lingertime = self.timerBaseInterval - self.timer2.interval( ) self.lingertimeCounter = 5 elif (self.leftCounter > self.counterSize - math.ceil(self.counterSize * 0.4)): if (self.lingertime > self.lingertimeCounter): self.lingertimeCounter += 1 else: if (self.timer2.interval() < self.timerBaseInterval): self.timer2.setInterval(self.timer2.interval() + 1) self.lingertime = self.timerBaseInterval - self.timer2.interval( ) self.lingertimeCounter = 5 self.list_widget.horizontalScrollBar().setValue( self.list_widget.horizontalScrollBar().value() - self.incrementalStep) self.leftCounter += 1 self.repaint() if (self.leftCounter == math.ceil(self.counterSize / 2)): self.lingertimeCounter = 5 self.lingertime = self.timerBaseInterval else: self.timer2.setInterval(self.timerBaseInterval) self.leftCounter = 0 self.timer2.stop() self.pic.setEnabled(True) self.pic2.setEnabled(True) if (self.list_widget.horizontalScrollBar().value() == 0): self.timer2.setInterval(self.timerBaseInterval) self.leftCounter = 0 self.timer2.stop() self.pic.setEnabled(True) self.pic2.setEnabled(True) #functions similarly to goLeft but adds scrolls to the right by adding pixels to the scrollbar value rather than subtracting #TODO: integrate goleft and goright into 1 function, no need for 2 def goRight(self): # print(self.timer.interval()) if (self.rightCounter != self.counterSize): if (self.rightCounter < math.ceil(self.counterSize * 0.4)): if (self.lingertime > self.lingertimeCounter): self.lingertimeCounter += 1 else: if (self.timer.interval() > self.floorInterval): self.timer.setInterval(self.timer.interval() - 1) self.lingertime = self.timerBaseInterval - self.timer.interval( ) self.lingertimeCounter = 5 elif (self.rightCounter > self.counterSize - math.ceil(self.counterSize * 0.4)): if (self.lingertime > self.lingertimeCounter): self.lingertimeCounter += 1 else: if (self.timer.interval() < self.timerBaseInterval): self.timer.setInterval(self.timer.interval() + 1) self.lingertime = self.timerBaseInterval - self.timer.interval( ) self.lingertimeCounter = 5 self.list_widget.horizontalScrollBar().setValue( self.list_widget.horizontalScrollBar().value() + self.incrementalStep) self.rightCounter += 1 self.repaint() if (self.rightCounter == math.ceil(self.counterSize / 2)): self.lingertimeCounter = 5 self.lingertime = self.timerBaseInterval else: self.timer.setInterval(self.timerBaseInterval) self.rightCounter = 0 self.timer.stop() self.pic.setEnabled(True) self.pic2.setEnabled(True) if (self.list_widget.horizontalScrollBar().value() == self.list_widget.horizontalScrollBar().maximum()): self.timer.setInterval(self.timerBaseInterval) self.rightCounter = 0 self.timer.stop() self.pic.setEnabled(True) self.pic2.setEnabled(True)
class Main(QMainWindow): current_x = 0. current_y = 0. current_z = 0. changes = dict() def __init__(self, *args): super(Main, self).__init__(*args) loadUi('mainwindow.ui', self) self.flavor = config.get('flavor', default='smoothie') self['connection.host'] = config.get('server_host', 'localhost') self['connection.port'] = str(config.get('server_port', '11011')) #self.prompt.setText("M114") self.prompt.addItem("M114") self.prompt.addItem("MULTILINE") self.conn = QHcClient(self) self.conn.readyRead.connect(self.readSocket) self.conn.error.connect(self.socketError) self.conn.connected.connect(self.socketConnect) self.conn.disconnected.connect(self.socketDisconnect) self.connected = False self.actionSave_log.triggered.connect(self.save_log_dialog) self.actionSave_probe_data.triggered.connect(self.save_probe_data_dialog) self.actionLoad_probe_data.triggered.connect(self.load_probe_data_dialog) self.actionLoad_G_code.triggered.connect(self.load_gcode_dialog) self.actionSave_G_code.triggered.connect(self.save_gcode_dialog) self.actionSave_probe_G_code.triggered.connect(self.save_probe_gcode_dialog) self.prompt.setFocus() self.prompt.lineEdit().returnPressed.connect(self.on_send_clicked) # paramtree handlers self.ptree.params.sigTreeStateChanged.connect(self.pchange) self.ptree.params.param('Connection', 'Connect').sigActivated.connect(self.do_connect) self.ptree.params.param('Probe', 'Run probe').sigActivated.connect(self.run_probe) self.ptree.params.param('Probe', 'Process').sigActivated.connect(self.process) self.ptree.params.param('Probe', 'Save processed G-code').sigActivated.connect(self.save_gcode_dialog) self.ptree.params.param('GCode', 'Load G-code').sigActivated.connect(self.load_gcode_dialog) # alias self.p = self.ptree.get_param self.ptree self.do_connect() self.update_probe() self.update_grid() #self.commodel = QStandardItemModel(self.comlist) #self.comlist.setModel(self.commodel) self.comtree.setColumnCount(3) self.comtree.setColumnWidth(0, 200) self.comtree.setColumnWidth(1, 100) self.comtree.setHeaderLabels(['Time', 'Command', 'Response']) #self.comtree.header().setSectionResizeMode(2, QHeaderView.Stretch) self.comtree.header().setSectionResizeMode(2, QHeaderView.ResizeToContents) self.comtree.header().setStretchLastSection(False) self.reconnect_timer = QTimer() self.reconnect_timer.timeout.connect(self.do_connect) joystick = False if joystick: self.joy = Joystick() self.joy.button_down.connect(self.button_down) self.joy.axis_moving.connect(self.axis_moving) self.init_timer = QTimer() self.init_timer.timeout.connect(self.late_init) self.init_timer.setSingleShot(True) self.init_timer.start(333) def late_init(self): self.ptree.changing(self.changing) self.tool_ipython() def tool_ipython(self): self.ipy.start() self.ipy.push({"self": self}) #self.ipy.execute('%pylab inline') def __getitem__(self, attr): """ Access HCParameTree values via self['some.path'] Returns cached value from self.changes if parameter is changing """ param = self.ptree.get_param(attr) path = self.ptree.params.childPath(param) if path is not None: pl = '.'.join(path).lower() if pl in self.changes: return self.changes[pl] return param.value() def __setitem__(self, attr, val): """ Set HCParameTree values via self['some.path'] """ param = self.ptree.get_param(attr) param.setValue(val) def changing(self, param, value): ''' Keep track of changing parameters and store these in self.changes dict ''' path = self.ptree.params.childPath(param) if path is not None: pl = '.'.join(path).lower() # cache changes, eval `value` to param.type try: # will fail for complex types like colormap so just ignore self.changes[pl] = eval('{}({})'.format(param.type(), value)) except: pass self.handle_updates(pl) def pchange(self, param, changes): """ HCParamTreechange handler """ for param, change, data in changes: path = self.ptree.params.childPath(param) if path is not None: pl = '.'.join(path).lower() if pl in self.changes: del self.changes[pl] self.handle_updates(pl) def handle_updates(self, path=None): if path: if path.startswith('probe'): self.update_probe() if path.startswith('grid'): self.update_grid() if path.startswith('cross'): self.update_cross() if path.startswith('gcode'): self.update_gcode() if path.startswith('probe result'): self.update_result() self.gl.paintGL() self.gl.repaint() proc_events() def load_gcode_dialog(self): fname, mask = QFileDialog.getOpenFileName(None, "Load G-Code", "", "GCode (*.ngc *.gcode);;All files (*.*)") if not fname: return try: self.gl.gcode.load_gcode(fname) # prefill probe width / height xmin, xmax = self.gl.gcode.limits['X'] xlen = xmax - xmin ymin, ymax = self.gl.gcode.limits['Y'] ylen = ymax - ymin zmin, zmax = self.gl.gcode.limits['Z'] zlen = zmax - zmin self['gcode.limits.x min'] = xmin self['gcode.limits.x max'] = xmax self['gcode.limits.x len'] = xlen self['gcode.limits.y min'] = ymin self['gcode.limits.y max'] = ymax self['gcode.limits.y len'] = ylen self['gcode.limits.z min'] = zmin self['gcode.limits.z max'] = zmax self['gcode.limits.z len'] = zlen self['probe.width'] = xlen self['probe.height'] = ylen self['grid.x origin'] = xmin self['grid.y origin'] = ymin self['grid.width'] = xlen self['grid.height'] = ylen o = 3 self.gl.ruler.x = xmin self.gl.ruler.size = xmax - xmin self.gl.ruler.reset() self.gl.ruler.translate(xmin, ymin - o, zmin) self.gl.ruler.redraw() self.gl.yruler.y = ymin self.gl.yruler.size = ymax - ymin self.gl.yruler.reset() self.gl.yruler.translate(xmin - o, ymin, zmin) self.gl.yruler.redraw() self.gl.zruler.z = zmin self.gl.zruler.size = zmax - zmin self.gl.zruler.reset() self.gl.zruler.translate(xmin - o, ymin - o, zmin) self.gl.zruler.redraw() self.gcode_path = fname self.update_probe() self.update_grid() self.info('Loaded {}'.format(fname)) except IOError as e: self.info('Unable to load {}'.format(fname)) self.info(str(e)) def save_gcode_dialog(self): fname, mask = QFileDialog.getSaveFileName(None, "Save G-Code", "", "GCode (*.ngc *.gcode);;All files (*.*)") if not fname: return try: if self.gl.postgcode.orig: self.gl.postgcode.save_gcode(name) self.info('Saved post-processed g-code to {}'.format(name)) elif self.gl.gcode.orig: self.gl.gcode.save_gcode(name) self.info('Saved original g-code to {}'.format(name)) else: self.info('Nothing to save') except IOError as e: self.info('Unable to save to {}'.format(name)) self.info(str(e)) def append(self, text): self.text.append(text) c = self.text.textCursor() c.movePosition(QTextCursor.End, QTextCursor.MoveAnchor) self.text.setTextCursor(c) def handle_response(self, idx, txt): root = self.comtree.invisibleRootItem() item = root.child(idx) item.setText(0, time.strftime("%Y.%m.%d. %H:%M:%S", time.localtime())) if not txt: txt = 'ok' item.setText(2, txt) item.setCheckState(0, Qt.Checked) self.comtree.scrollToItem(item) cmd = item.text(1) proc_events() if 'G0' in cmd or 'G1' in cmd: x, y, z = parse.xyz(cmd[2:]) # should probably emit signals self.current_x = x self.current_y = y self.current_z = z if 'G38.2' in cmd: try: # e.g. Probe not tripped from LinuxCNC # (we get error message from backend) if 'error' in txt: z = -999.0 else: z = parse.probe(txt) except: self.err('Unable to parse probe: {}'.format(txt)) self.err('Is your flavor ({}) correct?'.format(self.flavor)) z = -999.0 self.gl.result.data.append((self.current_x, self.current_y, z)) self.update_result() self['probe result.lowest'] = min(self['probe result.lowest'], z) self['probe result.highest'] = max(self['probe result.highest'], z) self['probe result.last'] = z def save_probe_data_dialog(self): if not self.gl.result.data: # err not much to save return fname, sel = QFileDialog.getSaveFileName( self, 'Save Log',) #'/path/to/default/directory', FIXME: lastused #selectedFilter='*.txt') if fname: self.save_probe_data(fname) def save_probe_data(self, fname): with open(fname, 'w') as f: for x, y, z in self.gl.result.data: f.write("{:04.2f} {:04.2f} {:04.2f}\n".format(x, y, z)) def load_probe_data_dialog(self): fname, mask = QFileDialog.getOpenFileName(None, "Load probe data", "", "Log data (*.txt *.log);;All files (*.*)") if fname: self.load_probe_data(fname) def load_probe_data(self, fname): with open(fname, 'r') as f: d = (map(lambda x: map(float, x.split()), f.readlines())) self.gl.result.data = d self.update_result() @pyqtSlot() def on_save_clicked(self): root = self.comtree.invisibleRootItem() {'name': 'Visible', 'type': 'bool', 'value': 1}, count = root.childCount() parts = [] for i in range(count): item = root.child(i) time = item.text(0) cmd = item.text(1) resp = item.text(2) parts.append((time, cmd, resp)) fname, sel = QFileDialog.getSaveFileName( self, 'Save Log',) #'/path/to/default/directory', FIXME: lastused #selectedFilter='*.txt') if fname: with open(fname, 'w') as f: for time, cmd, resp in parts: f.write('{}\t{}\t{}\n'.format(time, cmd, resp)) def readSocket(self): def handle(r): if not r: return #print('buffered', r) (idx, txt) = dec_msg(r) if idx is not None: self.handle_response(idx, txt) buffer = '' while True: r = str(self.conn.readLine()) if not r: handle(buffer) if self.conn.canReadLine(): buffer = '' continue break if r[0] == '/': _, msg = dec_msg(r) self.append('{}'.format(msg)) continue if r[0] == '[': handle(buffer) buffer = r continue buffer += r def info(self, errtext): self.text.setTextColor(QColor(20, 20, 20)) self.append(errtext) def err(self, errtext): self.text.setTextColor(QColor(100, 0, 0)) self.append(errtext) def socketDisconnect(self): self.err("Disconnected") self.connected = False self.info("Reconnecting") self.reconnect_timer.start(1000) def socketConnect(self): self.connected = True self.reconnect_timer.stop() self.info("Connected to {}:{}".format(self['connection.host'], self['connection.port'])) self.ptree.collapse_group('connection') def socketError(self, socketError): # backoff self.reconnect_timer.setInterval(self.reconnect_timer.interval() * 2) if socketError == QAbstractSocket.RemoteHostClosedError: pass elif socketError == QAbstractSocket.HostNotFoundError: self.err("The host was not found. Please check the host name and " "port settings.") elif socketError == QAbstractSocket.ConnectionRefusedError: self.err("The connection was refused by the peer. Make sure the " "server is running, and check that the host name " "and port settings are correct.") else: self.err("The following error occurred: {0}" .format(self.conn.errorString())) def save_log_dialog(self): fname, sel = QFileDialog.getSaveFileName( self, 'Save Log',) #'/path/to/default/directory', FIXME: lastused #selectedFilter='*.txt') if fname: with open(fname, 'w') as f: f.write(self.text.toPlainText()) def save_probe_gcode_dialog(self): fname, sel = QFileDialog.getSaveFileName( self, 'Save probe G-code',) #'/path/to/default/directory', FIXME: lastused #selectedFilter='*.txt') if fname: with open(fname, 'w') as f: for code in self.gen_probe_gcode(self.get_probe_points()): f.write(code + '\n') def do_connect(self): self.conn.abort() self.info("Connecting to {}:{}".format(self['connection.host'], self['connection.port'])) self.conn.connectToHost(self['connection.host'], int(self['connection.port'])) @pyqtSlot() def on_prompt_activated(self): pass def run_cmd(self, cmd): item = QTreeWidgetItem(self.comtree) item.setText(0, time.strftime("%Y.%m.%d. %H:%M:%S", time.localtime())) item.setText(1, cmd) for i in range(3): item.setTextAlignment(i, Qt.AlignTop) item.setForeground(1, QtGui.QBrush(green)) item.setForeground(2, QtGui.QBrush(red)) self.comtree.scrollToItem(item) self.conn.cmd(cmd) proc_events() @pyqtSlot() def on_send_clicked(self): if not self.connected: self.err("Not connected") return out = self.prompt.currentText() self.run_cmd(out) # FIXME: should go to hc lib def gen_probe_grid(self, rows, cols, w, h, x_margin, y_margin, x_trans, y_trans, start_z): w = w - x_margin * 2. h = h - y_margin * 2. if rows <= 0 or cols <= 0: return [] if cols == 1 or rows == 1: return [] xstep = w / (cols - 1) ystep = h / (rows - 1) cx = x_margin cy = y_margin out = [] for i in range(rows): for j in range(cols): out.append((cx + x_trans, cy + y_trans, start_z)) cx += xstep cx = x_margin cy += ystep return out def get_probe_points(self): mx = self['probe.x margin'] my = self['probe.y margin'] probe_points = self.gen_probe_grid( self['probe.rows'], self['probe.cols'], self['probe.width'], self['probe.height'], mx, my, self['gcode.limits.x min'], self['gcode.limits.y min'], self['probe.start z']) return probe_points def gen_probe_gcode(self, points): feed = self['probe.z feedrate'] travel_feed = self['probe.travel feedrate'] depth = self['probe.max depth'] sz = self['probe.start z'] yield 'G90' yield 'G1 Z{}'.format(sz) yield 'G4 P0' for point in points: yield 'G1 {} F{}'.format(xyzfmt(*point), travel_feed) yield probecmd.format(depth, feed) yield 'G1 Z{}'.format(sz) yield 'G4 P0' def run_probe(self): # clean probe result self.gl.result.data = [] for code in self.gen_probe_gcode(self.get_probe_points()): self.run_cmd(code) def update_gcode(self): self.gl.gcode.setVisible(self['gcode.visible']) def update_probe(self): probe_points = self.get_probe_points() self.gl.probelist.probe_points = probe_points self.gl.probelist.redraw() def update_result(self): self.gl.result.setVisible(self['probe result.visible']) self.gl.result.offset = self['probe result.offset'] self.gl.result.multiply = self['probe result.multiply'] self.gl.result.snapz = self['probe result.snap z'] self.gl.result.colormap = self['probe result.gradient'] self.gl.result.opts['drawEdges'] = self['probe result.draw edges'] self.gl.result.update_data() def update_grid(self): w = self['grid.width'] h = self['grid.height'] tx = self['grid.x origin'] ty = self['grid.y origin'] self.gl.grid.setSize(w, h, 1) self.gl.grid.resetTransform() self.gl.grid.translate(w / 2. + tx, h / 2. + ty, -0.05) self.gl.grid.setVisible(self['grid.visible']) self.gl.grid.redraw() def update_cross(self): s = self['cross.size'] pos = [] for i in ['x', 'y', 'z']: pos.append(self['cross.pos.{}'.format(i)]) self.gl.cross.size = s self.gl.cross.redraw() self.gl.cross.resetTransform() self.gl.cross.translate(*pos) self.gl.cross.setVisible(self['cross.visible']) def process(self): self.info('Processing') self.precision = 0.1 res = self.gl.result.data[:] if not res: self.err('No probe results') return gcpath = os.path.abspath(self.gcode_path) gcdir = os.path.dirname(gcpath) datapath = os.path.join(gcdir, 'probedata.txt') outpath = os.path.join(gcdir, 'lvl_{}'.format(os.path.basename(gcpath))) with open(datapath, 'w') as f: for x, y, z in res: f.write("{:04.2f} {:04.2f} {:04.2f}\n".format(x, y, z)) script = config.get('leveling_tool', 'scale_gcode.py') args = '{} 1-999999 --zlevel {} {:.2f}'.format(gcpath, datapath, self.precision) args = args.split() self.info('Calling "{}"'.format(" ".join([script] + args))) proc_events() import subprocess proc = subprocess.Popen([script] + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True) (stdout, stderr) = proc.communicate() if proc.returncode == 0: self.info('Done. Saving to {}'.format(outpath)) with open(outpath, 'w') as f: f.write(stdout) self.gl.postgcode.load_gcode(outpath) self.info('Loaded post-processed G-code') else: self.info('Processing failed with return code {}'.format(proc.returncode)) self.info("Error was:") self.info(stderr) # joystick handlers def axis_moving(self, axis, value): #self.run_cmd('G91') m = ['x', 'y', 'z', 'a'] if abs(value) < 0.1: return value = value / 10 #self.run_cmd('G0 {}{}'.format(m[axis], value)) self.run_cmd('/jog {} {:04.2f}'.format(m[axis], value)) def button_down(self, button): pass
class ScrollLabel(QLabel): def __init__(self, parent=None, flags=Qt.WindowFlags()): super().__init__(parent=parent, flags=flags) self.timer = QTimer(self) self.timer.timeout.connect(self.move) def move(self): if self.countdown > 0: self.countdown -= self.timer.interval() elif self.reverse: if self.x >= 0: self.refresh(cooldown=True) else: self.x += 1 else: if self.textWidth + self.x <= self.width(): self.refresh(reverse=True, cooldown=True) else: self.x -= 1 self.update() def refresh(self, reverse=False, cooldown=False): self.x = self.width() - self.textWidth if reverse else 0 self.countdown = 2000 if cooldown else 0 self.reverse = reverse self.update() def makeShort(self): self.short = self.long metrics = QFontMetrics(self.font()) width = metrics.width(self.short) if width >= self.width(): while width >= self.width(): self.short = self.short[:-1] width = metrics.width(self.short + '...') self.short = self.short + '...' def setText(self, a0): self.long = a0 self.makeShort() self.txt = self.short self.refresh() def enterEvent(self, event): self.txt = self.long self.refresh() self.timer.start(50) def leaveEvent(self, event): self.timer.stop() self.txt = self.short self.refresh() def paintEvent(self, a0): self.textWidth = QFontMetrics(self.font()).width(self.txt) painter = QPainter(self) painter.setFont(self.font()) painter.setPen(self.palette().windowText().color()) painter.setBrush(self.palette().windowText()) if self.textWidth > self.width(): painter.drawText( QRect(self.x, 0, self.textWidth, self.height()), Qt.AlignHCenter | (self.alignment() & Qt.AlignVCenter), self.txt) else: painter.drawText(QRect(0, 0, self.width(), self.height()), self.alignment(), self.txt) self.timer.stop()
class E5SideBar(QWidget): """ Class implementing a sidebar with a widget area, that is hidden or shown, if the current tab is clicked again. """ Version = 1 North = 0 East = 1 South = 2 West = 3 def __init__(self, orientation=None, delay=200, parent=None): """ Constructor @param orientation orientation of the sidebar widget (North, East, South, West) @param delay value for the expand/shrink delay in milliseconds (integer) @param parent parent widget (QWidget) """ super(E5SideBar, self).__init__(parent) self.__tabBar = QTabBar() self.__tabBar.setDrawBase(True) self.__tabBar.setShape(QTabBar.RoundedNorth) self.__tabBar.setUsesScrollButtons(True) self.__tabBar.setDrawBase(False) self.__stackedWidget = QStackedWidget(self) self.__stackedWidget.setContentsMargins(0, 0, 0, 0) self.__autoHideButton = QToolButton() self.__autoHideButton.setCheckable(True) self.__autoHideButton.setIcon( UI.PixmapCache.getIcon("autoHideOff.png")) self.__autoHideButton.setChecked(True) self.__autoHideButton.setToolTip( self.tr("Deselect to activate automatic collapsing")) self.barLayout = QBoxLayout(QBoxLayout.LeftToRight) self.barLayout.setContentsMargins(0, 0, 0, 0) self.layout = QBoxLayout(QBoxLayout.TopToBottom) self.layout.setContentsMargins(0, 0, 0, 0) self.layout.setSpacing(0) self.barLayout.addWidget(self.__autoHideButton) self.barLayout.addWidget(self.__tabBar) self.layout.addLayout(self.barLayout) self.layout.addWidget(self.__stackedWidget) self.setLayout(self.layout) # initialize the delay timer self.__actionMethod = None self.__delayTimer = QTimer(self) self.__delayTimer.setSingleShot(True) self.__delayTimer.setInterval(delay) self.__delayTimer.timeout.connect(self.__delayedAction) self.__minimized = False self.__minSize = 0 self.__maxSize = 0 self.__bigSize = QSize() self.splitter = None self.splitterSizes = [] self.__hasFocus = False # flag storing if this widget or any child has the focus self.__autoHide = False self.__tabBar.installEventFilter(self) self.__orientation = E5SideBar.North if orientation is None: orientation = E5SideBar.North self.setOrientation(orientation) self.__tabBar.currentChanged[int].connect( self.__stackedWidget.setCurrentIndex) e5App().focusChanged[QWidget, QWidget].connect(self.__appFocusChanged) self.__autoHideButton.toggled[bool].connect(self.__autoHideToggled) def setSplitter(self, splitter): """ Public method to set the splitter managing the sidebar. @param splitter reference to the splitter (QSplitter) """ self.splitter = splitter self.splitter.splitterMoved.connect(self.__splitterMoved) self.splitter.setChildrenCollapsible(False) index = self.splitter.indexOf(self) self.splitter.setCollapsible(index, False) def __splitterMoved(self, pos, index): """ Private slot to react on splitter moves. @param pos new position of the splitter handle (integer) @param index index of the splitter handle (integer) """ if self.splitter: self.splitterSizes = self.splitter.sizes() def __delayedAction(self): """ Private slot to handle the firing of the delay timer. """ if self.__actionMethod is not None: self.__actionMethod() def setDelay(self, delay): """ Public method to set the delay value for the expand/shrink delay in milliseconds. @param delay value for the expand/shrink delay in milliseconds (integer) """ self.__delayTimer.setInterval(delay) def delay(self): """ Public method to get the delay value for the expand/shrink delay in milliseconds. @return value for the expand/shrink delay in milliseconds (integer) """ return self.__delayTimer.interval() def __cancelDelayTimer(self): """ Private method to cancel the current delay timer. """ self.__delayTimer.stop() self.__actionMethod = None def shrink(self): """ Public method to record a shrink request. """ self.__delayTimer.stop() self.__actionMethod = self.__shrinkIt self.__delayTimer.start() def __shrinkIt(self): """ Private method to shrink the sidebar. """ self.__minimized = True self.__bigSize = self.size() if self.__orientation in [E5SideBar.North, E5SideBar.South]: self.__minSize = self.minimumSizeHint().height() self.__maxSize = self.maximumHeight() else: self.__minSize = self.minimumSizeHint().width() self.__maxSize = self.maximumWidth() if self.splitter: self.splitterSizes = self.splitter.sizes() self.__stackedWidget.hide() if self.__orientation in [E5SideBar.North, E5SideBar.South]: self.setFixedHeight(self.__tabBar.minimumSizeHint().height()) else: self.setFixedWidth(self.__tabBar.minimumSizeHint().width()) self.__actionMethod = None def expand(self): """ Public method to record a expand request. """ self.__delayTimer.stop() self.__actionMethod = self.__expandIt self.__delayTimer.start() def __expandIt(self): """ Private method to expand the sidebar. """ self.__minimized = False self.__stackedWidget.show() self.resize(self.__bigSize) if self.__orientation in [E5SideBar.North, E5SideBar.South]: minSize = max(self.__minSize, self.minimumSizeHint().height()) self.setMinimumHeight(minSize) self.setMaximumHeight(self.__maxSize) else: minSize = max(self.__minSize, self.minimumSizeHint().width()) self.setMinimumWidth(minSize) self.setMaximumWidth(self.__maxSize) if self.splitter: self.splitter.setSizes(self.splitterSizes) self.__actionMethod = None def isMinimized(self): """ Public method to check the minimized state. @return flag indicating the minimized state (boolean) """ return self.__minimized def isAutoHiding(self): """ Public method to check, if the auto hide function is active. @return flag indicating the state of auto hiding (boolean) """ return self.__autoHide def eventFilter(self, obj, evt): """ Public method to handle some events for the tabbar. @param obj reference to the object (QObject) @param evt reference to the event object (QEvent) @return flag indicating, if the event was handled (boolean) """ if obj == self.__tabBar: if evt.type() == QEvent.MouseButtonPress: pos = evt.pos() for i in range(self.__tabBar.count()): if self.__tabBar.tabRect(i).contains(pos): break if i == self.__tabBar.currentIndex(): if self.isMinimized(): self.expand() else: self.shrink() return True elif self.isMinimized(): self.expand() elif evt.type() == QEvent.Wheel: if qVersion() >= "5.0.0": delta = evt.angleDelta().y() else: delta = evt.delta() if delta > 0: self.prevTab() else: self.nextTab() return True return QWidget.eventFilter(self, obj, evt) def addTab(self, widget, iconOrLabel, label=None): """ Public method to add a tab to the sidebar. @param widget reference to the widget to add (QWidget) @param iconOrLabel reference to the icon or the label text of the tab (QIcon, string) @param label the labeltext of the tab (string) (only to be used, if the second parameter is a QIcon) """ if label: index = self.__tabBar.addTab(iconOrLabel, label) self.__tabBar.setTabToolTip(index, label) else: index = self.__tabBar.addTab(iconOrLabel) self.__tabBar.setTabToolTip(index, iconOrLabel) self.__stackedWidget.addWidget(widget) if self.__orientation in [E5SideBar.North, E5SideBar.South]: self.__minSize = self.minimumSizeHint().height() else: self.__minSize = self.minimumSizeHint().width() def insertTab(self, index, widget, iconOrLabel, label=None): """ Public method to insert a tab into the sidebar. @param index the index to insert the tab at (integer) @param widget reference to the widget to insert (QWidget) @param iconOrLabel reference to the icon or the labeltext of the tab (QIcon, string) @param label the labeltext of the tab (string) (only to be used, if the second parameter is a QIcon) """ if label: index = self.__tabBar.insertTab(index, iconOrLabel, label) self.__tabBar.setTabToolTip(index, label) else: index = self.__tabBar.insertTab(index, iconOrLabel) self.__tabBar.setTabToolTip(index, iconOrLabel) self.__stackedWidget.insertWidget(index, widget) if self.__orientation in [E5SideBar.North, E5SideBar.South]: self.__minSize = self.minimumSizeHint().height() else: self.__minSize = self.minimumSizeHint().width() def removeTab(self, index): """ Public method to remove a tab. @param index the index of the tab to remove (integer) """ self.__stackedWidget.removeWidget(self.__stackedWidget.widget(index)) self.__tabBar.removeTab(index) if self.__orientation in [E5SideBar.North, E5SideBar.South]: self.__minSize = self.minimumSizeHint().height() else: self.__minSize = self.minimumSizeHint().width() def clear(self): """ Public method to remove all tabs. """ while self.count() > 0: self.removeTab(0) def prevTab(self): """ Public slot used to show the previous tab. """ ind = self.currentIndex() - 1 if ind == -1: ind = self.count() - 1 self.setCurrentIndex(ind) self.currentWidget().setFocus() def nextTab(self): """ Public slot used to show the next tab. """ ind = self.currentIndex() + 1 if ind == self.count(): ind = 0 self.setCurrentIndex(ind) self.currentWidget().setFocus() def count(self): """ Public method to get the number of tabs. @return number of tabs in the sidebar (integer) """ return self.__tabBar.count() def currentIndex(self): """ Public method to get the index of the current tab. @return index of the current tab (integer) """ return self.__stackedWidget.currentIndex() def setCurrentIndex(self, index): """ Public slot to set the current index. @param index the index to set as the current index (integer) """ self.__tabBar.setCurrentIndex(index) self.__stackedWidget.setCurrentIndex(index) if self.isMinimized(): self.expand() def currentWidget(self): """ Public method to get a reference to the current widget. @return reference to the current widget (QWidget) """ return self.__stackedWidget.currentWidget() def setCurrentWidget(self, widget): """ Public slot to set the current widget. @param widget reference to the widget to become the current widget (QWidget) """ self.__stackedWidget.setCurrentWidget(widget) self.__tabBar.setCurrentIndex(self.__stackedWidget.currentIndex()) if self.isMinimized(): self.expand() def indexOf(self, widget): """ Public method to get the index of the given widget. @param widget reference to the widget to get the index of (QWidget) @return index of the given widget (integer) """ return self.__stackedWidget.indexOf(widget) def isTabEnabled(self, index): """ Public method to check, if a tab is enabled. @param index index of the tab to check (integer) @return flag indicating the enabled state (boolean) """ return self.__tabBar.isTabEnabled(index) def setTabEnabled(self, index, enabled): """ Public method to set the enabled state of a tab. @param index index of the tab to set (integer) @param enabled enabled state to set (boolean) """ self.__tabBar.setTabEnabled(index, enabled) def orientation(self): """ Public method to get the orientation of the sidebar. @return orientation of the sidebar (North, East, South, West) """ return self.__orientation def setOrientation(self, orient): """ Public method to set the orientation of the sidebar. @param orient orientation of the sidebar (North, East, South, West) """ if orient == E5SideBar.North: self.__tabBar.setShape(QTabBar.RoundedNorth) self.__tabBar.setSizePolicy( QSizePolicy.Expanding, QSizePolicy.Preferred) self.barLayout.setDirection(QBoxLayout.LeftToRight) self.layout.setDirection(QBoxLayout.TopToBottom) self.layout.setAlignment(self.barLayout, Qt.AlignLeft) elif orient == E5SideBar.East: self.__tabBar.setShape(QTabBar.RoundedEast) self.__tabBar.setSizePolicy( QSizePolicy.Preferred, QSizePolicy.Expanding) self.barLayout.setDirection(QBoxLayout.TopToBottom) self.layout.setDirection(QBoxLayout.RightToLeft) self.layout.setAlignment(self.barLayout, Qt.AlignTop) elif orient == E5SideBar.South: self.__tabBar.setShape(QTabBar.RoundedSouth) self.__tabBar.setSizePolicy( QSizePolicy.Expanding, QSizePolicy.Preferred) self.barLayout.setDirection(QBoxLayout.LeftToRight) self.layout.setDirection(QBoxLayout.BottomToTop) self.layout.setAlignment(self.barLayout, Qt.AlignLeft) elif orient == E5SideBar.West: self.__tabBar.setShape(QTabBar.RoundedWest) self.__tabBar.setSizePolicy( QSizePolicy.Preferred, QSizePolicy.Expanding) self.barLayout.setDirection(QBoxLayout.TopToBottom) self.layout.setDirection(QBoxLayout.LeftToRight) self.layout.setAlignment(self.barLayout, Qt.AlignTop) self.__orientation = orient def tabIcon(self, index): """ Public method to get the icon of a tab. @param index index of the tab (integer) @return icon of the tab (QIcon) """ return self.__tabBar.tabIcon(index) def setTabIcon(self, index, icon): """ Public method to set the icon of a tab. @param index index of the tab (integer) @param icon icon to be set (QIcon) """ self.__tabBar.setTabIcon(index, icon) def tabText(self, index): """ Public method to get the text of a tab. @param index index of the tab (integer) @return text of the tab (string) """ return self.__tabBar.tabText(index) def setTabText(self, index, text): """ Public method to set the text of a tab. @param index index of the tab (integer) @param text text to set (string) """ self.__tabBar.setTabText(index, text) def tabToolTip(self, index): """ Public method to get the tooltip text of a tab. @param index index of the tab (integer) @return tooltip text of the tab (string) """ return self.__tabBar.tabToolTip(index) def setTabToolTip(self, index, tip): """ Public method to set the tooltip text of a tab. @param index index of the tab (integer) @param tip tooltip text to set (string) """ self.__tabBar.setTabToolTip(index, tip) def tabWhatsThis(self, index): """ Public method to get the WhatsThis text of a tab. @param index index of the tab (integer) @return WhatsThis text of the tab (string) """ return self.__tabBar.tabWhatsThis(index) def setTabWhatsThis(self, index, text): """ Public method to set the WhatsThis text of a tab. @param index index of the tab (integer) @param text WhatsThis text to set (string) """ self.__tabBar.setTabWhatsThis(index, text) def widget(self, index): """ Public method to get a reference to the widget associated with a tab. @param index index of the tab (integer) @return reference to the widget (QWidget) """ return self.__stackedWidget.widget(index) def saveState(self): """ Public method to save the state of the sidebar. @return saved state as a byte array (QByteArray) """ if len(self.splitterSizes) == 0: if self.splitter: self.splitterSizes = self.splitter.sizes() self.__bigSize = self.size() if self.__orientation in [E5SideBar.North, E5SideBar.South]: self.__minSize = self.minimumSizeHint().height() self.__maxSize = self.maximumHeight() else: self.__minSize = self.minimumSizeHint().width() self.__maxSize = self.maximumWidth() data = QByteArray() stream = QDataStream(data, QIODevice.WriteOnly) stream.setVersion(QDataStream.Qt_4_6) stream.writeUInt16(self.Version) stream.writeBool(self.__minimized) stream << self.__bigSize stream.writeUInt16(self.__minSize) stream.writeUInt16(self.__maxSize) stream.writeUInt16(len(self.splitterSizes)) for size in self.splitterSizes: stream.writeUInt16(size) stream.writeBool(self.__autoHide) return data def restoreState(self, state): """ Public method to restore the state of the sidebar. @param state byte array containing the saved state (QByteArray) @return flag indicating success (boolean) """ if state.isEmpty(): return False if self.__orientation in [E5SideBar.North, E5SideBar.South]: minSize = self.layout.minimumSize().height() maxSize = self.maximumHeight() else: minSize = self.layout.minimumSize().width() maxSize = self.maximumWidth() data = QByteArray(state) stream = QDataStream(data, QIODevice.ReadOnly) stream.setVersion(QDataStream.Qt_4_6) stream.readUInt16() # version minimized = stream.readBool() if minimized and not self.__minimized: self.shrink() stream >> self.__bigSize self.__minSize = max(stream.readUInt16(), minSize) self.__maxSize = max(stream.readUInt16(), maxSize) count = stream.readUInt16() self.splitterSizes = [] for i in range(count): self.splitterSizes.append(stream.readUInt16()) self.__autoHide = stream.readBool() self.__autoHideButton.setChecked(not self.__autoHide) if not minimized: self.expand() return True ####################################################################### ## methods below implement the autohide functionality ####################################################################### def __autoHideToggled(self, checked): """ Private slot to handle the toggling of the autohide button. @param checked flag indicating the checked state of the button (boolean) """ self.__autoHide = not checked if self.__autoHide: self.__autoHideButton.setIcon( UI.PixmapCache.getIcon("autoHideOn.png")) else: self.__autoHideButton.setIcon( UI.PixmapCache.getIcon("autoHideOff.png")) def __appFocusChanged(self, old, now): """ Private slot to handle a change of the focus. @param old reference to the widget, that lost focus (QWidget or None) @param now reference to the widget having the focus (QWidget or None) """ self.__hasFocus = self.isAncestorOf(now) if self.__autoHide and not self.__hasFocus and not self.isMinimized(): self.shrink() elif self.__autoHide and self.__hasFocus and self.isMinimized(): self.expand() def enterEvent(self, event): """ Protected method to handle the mouse entering this widget. @param event reference to the event (QEvent) """ if self.__autoHide and self.isMinimized(): self.expand() else: self.__cancelDelayTimer() def leaveEvent(self, event): """ Protected method to handle the mouse leaving this widget. @param event reference to the event (QEvent) """ if self.__autoHide and not self.__hasFocus and not self.isMinimized(): self.shrink() else: self.__cancelDelayTimer() def shutdown(self): """ Public method to shut down the object. This method does some preparations so the object can be deleted properly. It disconnects from the focusChanged signal in order to avoid trouble later on. """ e5App().focusChanged[QWidget, QWidget].disconnect( self.__appFocusChanged)
class AbstractPLCConnector(QObject, object): """Connector with buffered PLC access. The access is not done directly via PLC addresses but instead with Tags. Data is exchanged with the PLC when the C{poll()} function is called or C{autopoll} is enabled. :type tags: dict :ivar tags: holds the Tag objects, the key is the tag name :type poll_interval: int :ivar poll_interval: interval for auto-polling in ms """ polled = pyqtSignal() connectionError = pyqtSignal(str) def __init__(self) -> None: super(AbstractPLCConnector, self).__init__() self.tags: Dict[str, Tag] = dict() self.autopoll_timer = QTimer(self) self.autopoll_timer.timeout.connect(self.poll) def add_tag(self, tag: Tag) -> Tag: """Add a Tag to the list.""" self.tags[tag.name] = tag return tag def add_tags(self, tags: Iterable) -> None: """Add multiple tags to the internal list.""" list(map(self.add_tag, tags)) @property def cycletime(self) -> int: """Return current cycletime.""" return self.autopoll_timer.interval() def remove_tag(self, tag_name: str) -> None: """Remove a Tag from the list.""" self.tags.pop(tag_name) def poll(self) -> None: """Exchange data with PLC. If a Tag value has been modified in the GUI the value is first written to the PLC and then read again. The pyqtSignal C{polled()} is emitted when finished. """ for tag in self.tags.values(): try: if tag.dirty: self.write_to_plc(tag.address, tag.raw_value, tag.plc_datatype) tag.dirty = False tag.raw_value = self.read_from_plc(tag.address, tag.plc_datatype) except ConnectionError as e: self.connectionError.emit(str(e)) self.polled.emit() @abstractmethod def write_to_plc(self, *args: Any, **kwargs: Any) -> None: """Write data to the plc. B{Abstract method. Overwrite when inherited.} """ pass @abstractmethod def read_from_plc(self, *args: Any, **kwargs: Any) -> None: """Read data from the plc. B{Abstract method. Overwrite when inherited.} """ pass def start_autopoll(self, poll_interval: int) -> None: """Enable auto-polling data. :param poll_interval: interval for autopolling in ms """ self.autopoll_timer.start(poll_interval) def stop_autopoll(self) -> None: """Disable auto-polling data.""" self.autopoll_timer.stop()
class Main(view.Game): def __init__(self): super().__init__() self._board_model = model.Board() self._vi_o_model = model.VI('3T-VI O') self._vi_x_model = model.VI('3T-VI X') self._start = random.choice([-1, +1]) self._turn = self._start self._thinking = True self._checking = True self._step_timer = QTimer() self._step_timer.timeout.connect(self._step) self._step_timer.setInterval(int(self.delay() * 1000)) def refresh(self, board: 'model.Board'): super().refresh(board) if self._step_timer.interval() != int(self.delay() * 1000): self._step_timer.setInterval(int(self.delay() * 1000)) def _toggle(self): super()._toggle() if self.is_running(): self._step_timer.start() else: self._step_timer.stop() def _check(self, result): if result < 0: self._x_wins += 1 self.state_o = 'lost' self.state_x = 'won' elif result > 0: self._o_wins += 1 self.state_o = 'won' self.state_x = 'lost' else: self._ties += 1 self.state_o = 'tied' self.state_x = 'tied' self._vi_o_model.train(result) self._vi_x_model.train(-result) self._vi_o_model.save() self._vi_x_model.save() self._checking = False def _move(self): if self._turn == +1: move = self._vi_o_model.move(self._board_model) self._board_model.set(*move, +1) elif self._turn == -1: move = self._vi_x_model.move(self._board_model.invert()) self._board_model.set(*move, -1) self._turn *= -1 self._thinking = True def _reset(self): self._board_model.clear() self._start *= -1 self._turn = self._start self._checking = True def _step(self): result = self._board_model.result() if result is None: if self._thinking: self._think() else: self._move() else: if self._checking: self._check(result) else: self._reset() def _think(self): if self.delay() != 0: if self._turn == +1: self.state_o = 'thinking' self.state_x = 'waiting' else: self.state_o = 'waiting' self.state_x = 'thinking' self._thinking = False
class OctoPrintOutputDevice(NetworkedPrinterOutputDevice): def __init__(self, instance_id: str, address: str, port: int, properties: dict, parent=None) -> None: super().__init__(device_id=instance_id, address=address, properties=properties, parent=parent) self._address = address self._port = port self._path = properties.get(b"path", b"/").decode("utf-8") if self._path[-1:] != "/": self._path += "/" self._id = instance_id self._properties = properties # Properties dict as provided by zero conf self._gcode_stream = StringIO() self._auto_print = True self._forced_queue = False # We start with a single extruder, but update this when we get data from octoprint self._number_of_extruders_set = False self._number_of_extruders = 1 # Try to get version information from plugin.json plugin_file_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), "plugin.json") try: with open(plugin_file_path) as plugin_file: plugin_info = json.load(plugin_file) plugin_version = plugin_info["version"] except: # The actual version info is not critical to have so we can continue plugin_version = "Unknown" Logger.logException( "w", "Could not get version information for the plugin") self._user_agent_header = "User-Agent".encode() self._user_agent = ( "%s/%s %s/%s" % (CuraApplication.getInstance().getApplicationName(), CuraApplication.getInstance().getVersion(), "OctoPrintPlugin", plugin_version) ) # NetworkedPrinterOutputDevice defines this as string, so we encode this later self._api_prefix = "api/" self._api_header = "X-Api-Key".encode() self._api_key = b"" self._protocol = "https" if properties.get( b'useHttps') == b"true" else "http" self._base_url = "%s://%s:%d%s" % (self._protocol, self._address, self._port, self._path) self._api_url = self._base_url + self._api_prefix self._basic_auth_header = "Authorization".encode() self._basic_auth_data = None basic_auth_username = properties.get(b"userName", b"").decode("utf-8") basic_auth_password = properties.get(b"password", b"").decode("utf-8") if basic_auth_username and basic_auth_password: data = base64.b64encode( ("%s:%s" % (basic_auth_username, basic_auth_password)).encode()).decode("utf-8") self._basic_auth_data = ("basic %s" % data).encode() self._monitor_view_qml_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), "MonitorItem.qml") name = self._id matches = re.search(r"^\"(.*)\"\._octoprint\._tcp.local$", name) if matches: name = matches.group(1) self.setPriority( 2 ) # Make sure the output device gets selected above local file output self.setName(name) self.setShortDescription( i18n_catalog.i18nc("@action:button", "Print with OctoPrint")) self.setDescription( i18n_catalog.i18nc("@properties:tooltip", "Print with OctoPrint")) self.setIconName("print") self.setConnectionText( i18n_catalog.i18nc("@info:status", "Connected to OctoPrint on {0}").format( self._id)) self._post_reply = None self._progress_message = None # type: Union[None, Message] self._error_message = None # type: Union[None, Message] self._connection_message = None # type: Union[None, Message] self._queued_gcode_commands = [] # type: List[str] self._queued_gcode_timer = QTimer() self._queued_gcode_timer.setInterval(0) self._queued_gcode_timer.setSingleShot(True) self._queued_gcode_timer.timeout.connect(self._sendQueuedGcode) # TODO; Add preference for update intervals self._update_fast_interval = 2000 self._update_slow_interval = 10000 self._update_timer = QTimer() self._update_timer.setInterval(self._update_fast_interval) self._update_timer.setSingleShot(False) self._update_timer.timeout.connect(self._update) self._show_camera = False self._camera_mirror = False self._camera_rotation = 0 self._camera_url = "" self._camera_shares_proxy = False self._sd_supported = False self._plugin_data = {} #type: Dict[str, Any] self._output_controller = GenericOutputController(self) def getProperties(self) -> Dict[bytes, bytes]: return self._properties @pyqtSlot(str, result=str) def getProperty(self, key: str) -> str: key_b = key.encode("utf-8") if key_b in self._properties: return self._properties.get(key_b, b"").decode("utf-8") else: return "" ## Get the unique key of this machine # \return key String containing the key of the machine. @pyqtSlot(result=str) def getId(self) -> str: return self._id ## Set the API key of this OctoPrint instance def setApiKey(self, api_key: str) -> None: self._api_key = api_key.encode() ## Name of the instance (as returned from the zeroConf properties) @pyqtProperty(str, constant=True) def name(self) -> str: return self._name ## Version (as returned from the zeroConf properties) @pyqtProperty(str, constant=True) def octoprintVersion(self) -> str: return self._properties.get(b"version", b"").decode("utf-8") ## IPadress of this instance @pyqtProperty(str, constant=True) def ipAddress(self) -> str: return self._address ## IPadress of this instance # Overridden from NetworkedPrinterOutputDevice because OctoPrint does not # send the ip address with zeroconf @pyqtProperty(str, constant=True) def address(self) -> str: return self._address ## port of this instance @pyqtProperty(int, constant=True) def port(self) -> int: return self._port ## path of this instance @pyqtProperty(str, constant=True) def path(self) -> str: return self._path ## absolute url of this instance @pyqtProperty(str, constant=True) def baseURL(self) -> str: return self._base_url cameraOrientationChanged = pyqtSignal() @pyqtProperty("QVariantMap", notify=cameraOrientationChanged) def cameraOrientation(self) -> Dict[str, Any]: return { "mirror": self._camera_mirror, "rotation": self._camera_rotation, } cameraUrlChanged = pyqtSignal() @pyqtProperty("QUrl", notify=cameraUrlChanged) def cameraUrl(self) -> QUrl: return QUrl(self._camera_url) def setShowCamera(self, show_camera: bool) -> None: if show_camera != self._show_camera: self._show_camera = show_camera self.showCameraChanged.emit() showCameraChanged = pyqtSignal() @pyqtProperty(bool, notify=showCameraChanged) def showCamera(self) -> bool: return self._show_camera def _update(self) -> None: ## Request 'general' printer data self.get("printer", self._onRequestFinished) ## Request print_job data self.get("job", self._onRequestFinished) def _createEmptyRequest(self, target: str, content_type: Optional[str] = "application/json" ) -> QNetworkRequest: request = QNetworkRequest(QUrl(self._api_url + target)) request.setRawHeader(self._user_agent_header, self._user_agent.encode()) request.setRawHeader(self._api_header, self._api_key) if content_type is not None: request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") if self._basic_auth_data: request.setRawHeader(self._basic_auth_header, self._basic_auth_data) return request def close(self) -> None: self.setConnectionState(ConnectionState.closed) if self._progress_message: self._progress_message.hide() if self._error_message: self._error_message.hide() self._update_timer.stop() ## Start requesting data from the instance def connect(self) -> None: self._createNetworkManager() self.setConnectionState(ConnectionState.connecting) self._update( ) # Manually trigger the first update, as we don't want to wait a few secs before it starts. Logger.log("d", "Connection with instance %s with url %s started", self._id, self._base_url) self._update_timer.start() self._last_response_time = None self._setAcceptsCommands(False) self.setConnectionText( i18n_catalog.i18nc("@info:status", "Connecting to OctoPrint on {0}").format( self._id)) ## Request 'settings' dump self.get("settings", self._onRequestFinished) ## Stop requesting data from the instance def disconnect(self) -> None: Logger.log("d", "Connection with instance %s with url %s stopped", self._id, self._base_url) self.close() def pausePrint(self) -> None: self._sendJobCommand("pause") def resumePrint(self) -> None: if not self._printers[0].activePrintJob: return if self._printers[0].activePrintJob.state == "paused": self._sendJobCommand("pause") else: self._sendJobCommand("start") def cancelPrint(self) -> None: self._sendJobCommand("cancel") def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional["FileHandler"] = None, **kwargs: str) -> None: global_container_stack = CuraApplication.getInstance( ).getGlobalContainerStack() if not global_container_stack: return # Make sure post-processing plugin are run on the gcode self.writeStarted.emit(self) # Get the g-code through the GCodeWriter plugin # This produces the same output as "Save to File", adding the print settings to the bottom of the file gcode_writer = cast( MeshWriter, PluginRegistry.getInstance().getPluginObject("GCodeWriter")) self._gcode_stream = StringIO() if not gcode_writer.write(self._gcode_stream, None): Logger.log("e", "GCodeWrite failed: %s" % gcode_writer.getInformation()) return if self._error_message: self._error_message.hide() self._error_message = None if self._progress_message: self._progress_message.hide() self._progress_message = None self._auto_print = parseBool( global_container_stack.getMetaDataEntry("octoprint_auto_print", True)) self._forced_queue = False if self.activePrinter.state not in ["idle", ""]: Logger.log( "d", "Tried starting a print, but current state is %s" % self.activePrinter.state) if not self._auto_print: # Allow queueing the job even if OctoPrint is currently busy if autoprinting is disabled self._error_message = None elif self.activePrinter.state == "offline": self._error_message = Message( i18n_catalog.i18nc( "@info:status", "The printer is offline. Unable to start a new job.")) else: self._error_message = Message( i18n_catalog.i18nc( "@info:status", "OctoPrint is busy. Unable to start a new job.")) if self._error_message: self._error_message.addAction( "Queue", i18n_catalog.i18nc("@action:button", "Queue job"), "", i18n_catalog.i18nc( "@action:tooltip", "Queue this print job so it can be printed later")) self._error_message.actionTriggered.connect(self._queuePrint) self._error_message.show() return self._startPrint() def _queuePrint(self, message_id: Optional[str] = None, action_id: Optional[str] = None) -> None: if self._error_message: self._error_message.hide() self._forced_queue = True self._startPrint() def _startPrint(self) -> None: global_container_stack = CuraApplication.getInstance( ).getGlobalContainerStack() if not global_container_stack: return if self._auto_print and not self._forced_queue: CuraApplication.getInstance().getController().setActiveStage( "MonitorStage") # cancel any ongoing preheat timer before starting a print try: self._printers[0].stopPreheatTimers() except AttributeError: # stopPreheatTimers was added after Cura 3.3 beta pass self._progress_message = Message( i18n_catalog.i18nc("@info:status", "Sending data to OctoPrint"), 0, False, -1) self._progress_message.addAction( "Cancel", i18n_catalog.i18nc("@action:button", "Cancel"), "", "") self._progress_message.actionTriggered.connect(self._cancelSendGcode) self._progress_message.show() job_name = CuraApplication.getInstance().getPrintInformation( ).jobName.strip() if job_name is "": job_name = "untitled_print" file_name = "%s.gcode" % job_name ## Create multi_part request post_parts = [] # type: List[QHttpPart] ## Create parts (to be placed inside multipart) post_part = QHttpPart() post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"select\"") post_part.setBody(b"true") post_parts.append(post_part) if self._auto_print and not self._forced_queue: post_part = QHttpPart() post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"print\"") post_part.setBody(b"true") post_parts.append(post_part) post_part = QHttpPart() post_part.setHeader( QNetworkRequest.ContentDispositionHeader, "form-data; name=\"file\"; filename=\"%s\"" % file_name) post_part.setBody(self._gcode_stream.getvalue().encode()) post_parts.append(post_part) destination = "local" if self._sd_supported and parseBool( global_container_stack.getMetaDataEntry( "octoprint_store_sd", False)): destination = "sdcard" try: ## Post request + data post_request = self._createEmptyRequest("files/" + destination) self._post_reply = self.postFormWithParts( "files/" + destination, post_parts, on_finished=self._onRequestFinished, on_progress=self._onUploadProgress) except IOError: self._progress_message.hide() self._error_message = Message( i18n_catalog.i18nc("@info:status", "Unable to send data to OctoPrint.")) self._error_message.show() except Exception as e: self._progress_message.hide() Logger.log( "e", "An exception occurred in network connection: %s" % str(e)) self._gcode_stream = StringIO() def _cancelSendGcode(self, message_id: Optional[str] = None, action_id: Optional[str] = None) -> None: if self._post_reply: Logger.log("d", "Stopping upload because the user pressed cancel.") try: self._post_reply.uploadProgress.disconnect( self._onUploadProgress) except TypeError: pass # The disconnection can fail on mac in some cases. Ignore that. self._post_reply.abort() self._post_reply = None if self._progress_message: self._progress_message.hide() def sendCommand(self, command: str) -> None: self._queued_gcode_commands.append(command) self._queued_gcode_timer.start() # Send gcode commands that are queued in quick succession as a single batch def _sendQueuedGcode(self) -> None: if self._queued_gcode_commands: self._sendCommandToApi("printer/command", self._queued_gcode_commands) Logger.log("d", "Sent gcode command to OctoPrint instance: %s", self._queued_gcode_commands) self._queued_gcode_commands = [] # type: List[str] def _sendJobCommand(self, command: str) -> None: self._sendCommandToApi("job", command) Logger.log("d", "Sent job command to OctoPrint instance: %s", command) def _sendCommandToApi(self, end_point: str, commands: Union[str, List[str]]) -> None: if isinstance(commands, list): data = json.dumps({"commands": commands}) else: data = json.dumps({"command": commands}) self.post(end_point, data, self._onRequestFinished) ## Overloaded from NetworkedPrinterOutputDevice.post() to backport https://github.com/Ultimaker/Cura/pull/4678 def post(self, target: str, data: str, on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Callable = None) -> None: self._validateManager() request = self._createEmptyRequest(target) self._last_request_time = time() if self._manager is not None: reply = self._manager.post(request, data.encode()) if on_progress is not None: reply.uploadProgress.connect(on_progress) self._registerOnFinishedCallback(reply, on_finished) else: Logger.log("e", "Could not find manager.") ## Handler for all requests that have finished. def _onRequestFinished(self, reply: QNetworkReply) -> None: if reply.error() == QNetworkReply.TimeoutError: Logger.log("w", "Received a timeout on a request to the instance") self._connection_state_before_timeout = self._connection_state self.setConnectionState(ConnectionState.error) return if self._connection_state_before_timeout and reply.error( ) == QNetworkReply.NoError: # There was a timeout, but we got a correct answer again. if self._last_response_time: Logger.log( "d", "We got a response from the instance after %s of silence", time() - self._last_response_time) self.setConnectionState(self._connection_state_before_timeout) self._connection_state_before_timeout = None if reply.error() == QNetworkReply.NoError: self._last_response_time = time() http_status_code = reply.attribute( QNetworkRequest.HttpStatusCodeAttribute) if not http_status_code: # Received no or empty reply return error_handled = False if reply.operation() == QNetworkAccessManager.GetOperation: if self._api_prefix + "printer" in reply.url().toString( ): # Status update from /printer. if not self._printers: self._createPrinterList() # An OctoPrint instance has a single printer. printer = self._printers[0] update_pace = self._update_slow_interval if http_status_code == 200: update_pace = self._update_fast_interval if not self.acceptsCommands: self._setAcceptsCommands(True) self.setConnectionText( i18n_catalog.i18nc( "@info:status", "Connected to OctoPrint on {0}").format( self._id)) if self._connection_state == ConnectionState.connecting: self.setConnectionState(ConnectionState.connected) try: json_data = json.loads( bytes(reply.readAll()).decode("utf-8")) except json.decoder.JSONDecodeError: Logger.log( "w", "Received invalid JSON from octoprint instance.") json_data = {} if "temperature" in json_data: if not self._number_of_extruders_set: self._number_of_extruders = 0 while "tool%d" % self._number_of_extruders in json_data[ "temperature"]: self._number_of_extruders += 1 if self._number_of_extruders > 1: # Recreate list of printers to match the new _number_of_extruders self._createPrinterList() printer = self._printers[0] if self._number_of_extruders > 0: self._number_of_extruders_set = True # Check for hotend temperatures for index in range(0, self._number_of_extruders): extruder = printer.extruders[index] if ("tool%d" % index) in json_data["temperature"]: hotend_temperatures = json_data["temperature"][ "tool%d" % index] extruder.updateTargetHotendTemperature( hotend_temperatures["target"]) extruder.updateHotendTemperature( hotend_temperatures["actual"]) else: extruder.updateTargetHotendTemperature(0) extruder.updateHotendTemperature(0) if "bed" in json_data["temperature"]: bed_temperatures = json_data["temperature"]["bed"] actual_temperature = bed_temperatures[ "actual"] if bed_temperatures[ "actual"] is not None else -1 printer.updateBedTemperature(actual_temperature) target_temperature = bed_temperatures[ "target"] if bed_temperatures[ "target"] is not None else -1 printer.updateTargetBedTemperature( target_temperature) else: printer.updateBedTemperature(-1) printer.updateTargetBedTemperature(0) printer_state = "offline" if "state" in json_data: flags = json_data["state"]["flags"] if flags["error"] or flags["closedOrError"]: printer_state = "error" elif flags["paused"] or flags["pausing"]: printer_state = "paused" elif flags["printing"]: printer_state = "printing" elif flags["cancelling"]: printer_state = "aborted" elif flags["ready"] or flags["operational"]: printer_state = "idle" printer.updateState(printer_state) elif http_status_code == 401: printer.updateState("offline") if printer.activePrintJob: printer.activePrintJob.updateState("offline") self.setConnectionText( i18n_catalog.i18nc( "@info:status", "OctoPrint on {0} does not allow access to print"). format(self._id)) error_handled = True elif http_status_code == 409: if self._connection_state == ConnectionState.connecting: self.setConnectionState(ConnectionState.connected) printer.updateState("offline") if printer.activePrintJob: printer.activePrintJob.updateState("offline") self.setConnectionText( i18n_catalog.i18nc( "@info:status", "The printer connected to OctoPrint on {0} is not operational" ).format(self._id)) error_handled = True elif http_status_code == 502 or http_status_code == 503: printer.updateState("offline") if printer.activePrintJob: printer.activePrintJob.updateState("offline") self.setConnectionText( i18n_catalog.i18nc( "@info:status", "OctoPrint on {0} is not running").format( self._id)) error_handled = True else: printer.updateState("offline") if printer.activePrintJob: printer.activePrintJob.updateState("offline") Logger.log("w", "Received an unexpected returncode: %d", http_status_code) if update_pace != self._update_timer.interval(): self._update_timer.setInterval(update_pace) elif self._api_prefix + "job" in reply.url().toString( ): # Status update from /job: if not self._printers: return # Ignore the data for now, we don't have info about a printer yet. printer = self._printers[0] if http_status_code == 200: try: json_data = json.loads( bytes(reply.readAll()).decode("utf-8")) except json.decoder.JSONDecodeError: Logger.log( "w", "Received invalid JSON from octoprint instance.") json_data = {} if printer.activePrintJob is None: print_job = PrintJobOutputModel( output_controller=self._output_controller) printer.updateActivePrintJob(print_job) else: print_job = printer.activePrintJob print_job_state = "offline" if "state" in json_data: if json_data["state"] == "Error": print_job_state = "error" elif json_data["state"] == "Pausing": print_job_state = "pausing" elif json_data["state"] == "Paused": print_job_state = "paused" elif json_data["state"] == "Printing": print_job_state = "printing" elif json_data["state"] == "Cancelling": print_job_state = "abort" elif json_data["state"] == "Operational": print_job_state = "ready" printer.updateState("idle") print_job.updateState(print_job_state) print_time = json_data["progress"]["printTime"] if print_time: print_job.updateTimeElapsed(print_time) if json_data["progress"][ "completion"]: # not 0 or None or "" print_job.updateTimeTotal( print_time / (json_data["progress"]["completion"] / 100)) else: print_job.updateTimeTotal(0) else: print_job.updateTimeElapsed(0) print_job.updateTimeTotal(0) print_job.updateName(json_data["job"]["file"]["name"]) else: pass # See generic error handler below elif self._api_prefix + "settings" in reply.url().toString( ): # OctoPrint settings dump from /settings: if http_status_code == 200: try: json_data = json.loads( bytes(reply.readAll()).decode("utf-8")) except json.decoder.JSONDecodeError: Logger.log( "w", "Received invalid JSON from octoprint instance.") json_data = {} if "feature" in json_data and "sdSupport" in json_data[ "feature"]: self._sd_supported = json_data["feature"]["sdSupport"] if "webcam" in json_data and "streamUrl" in json_data[ "webcam"]: self._camera_shares_proxy = False stream_url = json_data["webcam"]["streamUrl"] if not stream_url: #empty string or None self._camera_url = "" elif stream_url[:4].lower() == "http": # absolute uri self._camera_url = stream_url elif stream_url[:2] == "//": # protocol-relative self._camera_url = "%s:%s" % (self._protocol, stream_url) elif stream_url[: 1] == ":": # domain-relative (on another port) self._camera_url = "%s://%s%s" % ( self._protocol, self._address, stream_url) elif stream_url[: 1] == "/": # domain-relative (on same port) self._camera_url = "%s://%s:%d%s" % ( self._protocol, self._address, self._port, stream_url) self._camera_shares_proxy = True else: Logger.log("w", "Unusable stream url received: %s", stream_url) self._camera_url = "" Logger.log("d", "Set OctoPrint camera url to %s", self._camera_url) self.cameraUrlChanged.emit() if "rotate90" in json_data["webcam"]: self._camera_rotation = -90 if json_data["webcam"][ "rotate90"] else 0 if json_data["webcam"]["flipH"] and json_data[ "webcam"]["flipV"]: self._camera_mirror = False self._camera_rotation += 180 elif json_data["webcam"]["flipH"]: self._camera_mirror = True self._camera_rotation += 180 elif json_data["webcam"]["flipV"]: self._camera_mirror = True else: self._camera_mirror = False self.cameraOrientationChanged.emit() if "plugins" in json_data: self._plugin_data = json_data["plugins"] elif reply.operation() == QNetworkAccessManager.PostOperation: if self._api_prefix + "files" in reply.url().toString( ): # Result from /files command: if http_status_code == 201: Logger.log( "d", "Resource created on OctoPrint instance: %s", reply.header( QNetworkRequest.LocationHeader).toString()) else: pass # See generic error handler below reply.uploadProgress.disconnect(self._onUploadProgress) if self._progress_message: self._progress_message.hide() if self._forced_queue or not self._auto_print: location = reply.header(QNetworkRequest.LocationHeader) if location: file_name = QUrl( reply.header(QNetworkRequest.LocationHeader). toString()).fileName() message = Message( i18n_catalog.i18nc( "@info:status", "Saved to OctoPrint as {0}").format(file_name)) else: message = Message( i18n_catalog.i18nc("@info:status", "Saved to OctoPrint")) message.addAction( "open_browser", i18n_catalog.i18nc("@action:button", "OctoPrint..."), "globe", i18n_catalog.i18nc("@info:tooltip", "Open the OctoPrint web interface")) message.actionTriggered.connect(self._openOctoPrint) message.show() elif self._api_prefix + "job" in reply.url().toString( ): # Result from /job command (eg start/pause): if http_status_code == 204: Logger.log("d", "Octoprint job command accepted") else: pass # See generic error handler below elif self._api_prefix + "printer/command" in reply.url().toString( ): # Result from /printer/command (gcode statements): if http_status_code == 204: Logger.log("d", "Octoprint gcode command(s) accepted") else: pass # See generic error handler below else: Logger.log("d", "OctoPrintOutputDevice got an unhandled operation %s", reply.operation()) if not error_handled and http_status_code >= 400: # Received an error reply error_string = reply.attribute( QNetworkRequest.HttpReasonPhraseAttribute) if self._error_message: self._error_message.hide() self._error_message = Message( i18n_catalog.i18nc( "@info:status", "OctoPrint returned an error: {0}.").format(error_string)) self._error_message.show() return def _onUploadProgress(self, bytes_sent: int, bytes_total: int) -> None: if not self._progress_message: return if bytes_total > 0: # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get # timeout responses if this happens. self._last_response_time = time() progress = bytes_sent / bytes_total * 100 previous_progress = self._progress_message.getProgress() if progress < 100: if previous_progress is not None and progress > previous_progress: self._progress_message.setProgress(progress) else: self._progress_message.hide() self._progress_message = Message( i18n_catalog.i18nc("@info:status", "Storing data on OctoPrint"), 0, False, -1) self._progress_message.show() else: self._progress_message.setProgress(0) def _createPrinterList(self) -> None: printer = PrinterOutputModel( output_controller=self._output_controller, number_of_extruders=self._number_of_extruders) printer.updateName(self.name) self._printers = [printer] self.printersChanged.emit() def _openOctoPrint(self, message_id: Optional[str] = None, action_id: Optional[str] = None) -> None: QDesktopServices.openUrl(QUrl(self._base_url))
class MainForm(QWidget): def __init__(self): super(MainForm, self).__init__() self.ui = Ui_mainForm() self.ui.setupUi(self) self.setWindowTitle("Растительное сообщество") self.map = None self.timer = QTimer(self) # Соединение сигнала timeout() со слотом updateTime() self.timer.timeout.connect(self.__nextDay) self.timer.setInterval(1000) # Интервалы с которыми может срабатывать таймер. # Первый - самая низкая скорость self._timeIntervals = [2000, 1500, 1000, 750, 500] self._time_NORM_INTERVAL = 1000 # Поле self.grid = self.ui.gridLayout_Map self.initDialog = InitDialog(self) self.initDialog.setWindowTitle("Новая симуляция") # Кнопка "начать новую симуляцию" self.ui.initButton.clicked.connect(self._showInitDialog) # Кнопка "Пауза". Соединение сигнала и слота self.ui.pushButton_Pause.clicked.connect(self._timePause) # Кнопка "Быстрее". Соединение сигнала и слота self.ui.pushButton_Faster.clicked.connect(self._timeFaster) # Кнопка "Медленнее". Соединение сигнала и слота self.ui.pushButton_Slower.clicked.connect(self._timeSlower) def __nextDay(self): self.map.oneStep() self._redrawMap() self._changeInformation() if len(self.map.plantsList) == 0 : self.timer.stop() # Реакция на нажатие кнопки Пауза def _timePause(self) : if self.timer.isActive() : self.timer.stop() self.ui.pushButton_Pause.setText("|>") self.ui.pushButton_Faster.setText(">") self.ui.pushButton_Slower.setText("<") else: self.timer.setInterval(self._time_NORM_INTERVAL) self.timer.start() self.ui.pushButton_Pause.setText("||") # Реакция на нажатие кнопки Быстрее def _timeFaster(self) : # Если таймер идет if self.timer.isActive() : i = self._timeIntervals.index(self.timer.interval()) # Если скорость можно увеличить, то индекс интервала # будет меньше максимального индекса if i < len(self._timeIntervals) - 1 : self.timer.setInterval(self._timeIntervals[i + 1]) # Изменить подписи кнопок перемотки str = self.ui.pushButton_Faster.text() str += '>' self.ui.pushButton_Faster.setText(str) self.ui.pushButton_Slower.setText('<') else : #Если Таймер на паузе, эмитируем нажатие кнопки "Пауза" self._timePause() # Реакция на нажатие кнопки def _timeSlower(self) : # Если таймер идет if self.timer.isActive() : i = self._timeIntervals.index(self.timer.interval()) # Если скорость можно уменьшить, то индекс интервала # будет больше 0 if i > 0 : self.timer.setInterval(self._timeIntervals[i - 1]) # Изменить подписи кнопок перемотки str = self.ui.pushButton_Slower.text() str += '<' self.ui.pushButton_Slower.setText(str) self.ui.pushButton_Faster.setText('>') else : #Если Таймер на паузе, эмитируем нажатие кнопки "Пауза" self._timePause() def _showInitDialog(self) : self.timer.stop() # Если пользователь подтвердил, что хочет создать карту if self.initDialog.exec() == 1 : # Включить кнопки управления скоростью симуляции self.ui.pushButton_Slower.setEnabled(True) self.ui.pushButton_Pause.setEnabled(True) self.ui.pushButton_Faster.setEnabled(True) # Считать данные из формы nums = self.initDialog.getSpinBoxInf() self.xy = self.initDialog.getXY() self.partition = self.initDialog.getPartition() self.numOfClim = int(self.initDialog.getNumOfClim()) # Создать интеллектуальную модель карты self.map = Map(nums, self.xy, self.partition, self.numOfClim) # Отрисовать карту self._drawMap() self._changeInformation() if self.map is not None : self.timer.start() def _redrawMap(self) : try : for i in range(len(self.map.cells)) : for j in range(len(self.map.cells[0])) : w = self.grid.itemAtPosition(i, j).widget() h = self.map.cells[i][j].getHeighOfHighestPlant() w.checkColor(h) msg = self.map.cells[i][j].getNameOfHighestPlant() if h is not None : msg += '\n' + 'Высота: '+ str(h) age = self.map.cells[i][j].getAgeOfHighestPlant() if age is not None : msg += '\n' + 'Процент жизни: '+ str(age) w.setToolTip(msg) except Exception as e : print(e, i, j) def _drawMap(self) : #Сначала очистить карту, если она уже была создана ранее if self.grid.isEmpty() is False : for i in range(self.grid.rowCount()) : for j in range(self.grid.columnCount()) : w = self.grid.itemAtPosition(i, j).widget() self.grid.removeWidget(w) w.setParent(None) self.grid.setSpacing(1) self.grid.setSizeConstraint(QLayout.SetFixedSize) xSize, ySize = self.xy xSize *= self.partition ySize *= self.partition for i in range(ySize) : for j in range(xSize) : h = self.map.cells[i][j].getHeighOfHighestPlant() w = CellWidget(j, i, self.partition, h) self.grid.addWidget(w, i, j) def _changeInformation(self) : msg = str(len(self.map.plantsList)) self.ui.label_CountOfPlants.setText(msg)
class ImageSlider(QWidget): def __init__(self): super().__init__() # setting title self.setWindowTitle("Python ") self.setGeometry(600, 600, 900, 300) self.hbox = QHBoxLayout() self.list_widget = QListWidget() # self.list_widget.setGeometry(50, 70, 300, 150) # item1.setIcon(QIcon("key_icon.ico")) self.items = [] # for i in range(20): # self.items.append(QListWidgetItem(str(i))) # self.list_widget.addItem(self.items[i]) # if(i%2 == 0): # self.items[i].setIcon(QIcon("thumbnail.jpg")) # else: # self.items[i].setIcon(QIcon("1.jpg")) # setting flow self.list_widget.setFlow(QListView.LeftToRight) self.list_widget.setIconSize(QtCore.QSize(190, 190)) self.list_widget.hasAutoScroll() self.list_widget.setAutoFillBackground(False) self.pic = QPushButton() self.pic.clicked.connect(lambda: self.goleftSmooth()) self.pic.setGeometry(10, 10, 50, 50) #use full ABSOLUTE path to the image, not relative self.pic.setIcon(QIcon("left.png")) self.hbox.addWidget(self.pic) self.hbox.addWidget(self.list_widget) self.pic2 = QPushButton() self.pic2.clicked.connect(lambda: self.gorightSmooth()) self.pic2.setGeometry(10, 10, 50, 50) #use full ABSOLUTE path to the image, not relative self.pic2.setIcon(QIcon("right.png")) self.hbox.addWidget(self.pic2) self.list_widget.horizontalScrollBar().setDisabled(True) self.list_widget.horizontalScrollBar().hide() self.list_widget.verticalScrollBar().setDisabled(True) self.list_widget.verticalScrollBar().hide() self.list_widget.setHorizontalScrollMode( QAbstractItemView.ScrollPerPixel) self.setLayout(self.hbox) self.atCurrentRight = 16 self.atCurrentLeft = 0 self.timerBaseInterval = 25 self.floorInterval = 5 self.timer = QTimer(self) self.timer.setInterval(self.timerBaseInterval) self.timer.timeout.connect(self.goRight) self.timer2 = QTimer(self) self.timer2.setInterval(self.timerBaseInterval) self.timer2.timeout.connect(self.goLeft) self.rightCounter = 0 self.leftCounter = 0 self.incrementalStep = 2 self.counterSize = 410 self.lingertime = 1 self.lingertimeCounter = 0 # self.list_widget.itemPressed.connect(lambda: self.test()) self.show() # def test(self): # print(self.list_widget.selectedItems()[0].url) #Takes list of images and adds them to the image container of this class def setImages(self, images, urls, movieIDs, titles): self.list_widget.clear() self.items = [] for i in range(len(images)): self.items.append(ClickableThumbnail(urls[i], movieIDs[i])) self.items[i].setText(titles[i]) self.list_widget.addItem(self.items[i]) pm = QPixmap() pm.loadFromData(base64.b64decode(images[i])) ic = QIcon() ic.addPixmap(pm) if (ic.isNull() == False): self.items[i].setIcon(ic) #movies images to the right to slide left by initiating a timer that moves the images smoothly by pixels def goleftSmooth(self): self.timer2.start() self.pic.setDisabled(True) self.pic2.setDisabled(True) #movies images to the left to slide right by initiating a timer that moves the images smoothly by pixels def gorightSmooth(self): self.timer.start() self.pic.setDisabled(True) self.pic2.setDisabled(True) #Function that timer2 uses to movie images right #It works by modifying the timer interval (time needed until this function is called again) #starts by high time interval to low then to high (slow fast slow) def goLeft(self): if (self.leftCounter != self.counterSize): if (self.leftCounter < math.ceil(self.counterSize * 0.4)): if (self.lingertime > self.lingertimeCounter): self.lingertimeCounter += 1 else: if (self.timer2.interval() > self.floorInterval): self.timer2.setInterval(self.timer2.interval() - 1) self.lingertime = self.timerBaseInterval - self.timer2.interval( ) self.lingertimeCounter = 5 elif (self.leftCounter > self.counterSize - math.ceil(self.counterSize * 0.4)): if (self.lingertime > self.lingertimeCounter): self.lingertimeCounter += 1 else: if (self.timer2.interval() < self.timerBaseInterval): self.timer2.setInterval(self.timer2.interval() + 1) self.lingertime = self.timerBaseInterval - self.timer2.interval( ) self.lingertimeCounter = 5 self.list_widget.horizontalScrollBar().setValue( self.list_widget.horizontalScrollBar().value() - self.incrementalStep) self.leftCounter += 1 self.repaint() if (self.leftCounter == math.ceil(self.counterSize / 2)): self.lingertimeCounter = 5 self.lingertime = self.timerBaseInterval else: self.timer2.setInterval(self.timerBaseInterval) self.leftCounter = 0 self.timer2.stop() self.pic.setEnabled(True) self.pic2.setEnabled(True) if (self.list_widget.horizontalScrollBar().value() == 0): self.timer2.setInterval(self.timerBaseInterval) self.leftCounter = 0 self.timer2.stop() self.pic.setEnabled(True) self.pic2.setEnabled(True) #functions similarly to goLeft but adds scrolls to the right by adding pixels to the scrollbar value rather than subtracting #TODO: integrate goleft and goright into 1 function, no need for 2 def goRight(self): # print(self.timer.interval()) if (self.rightCounter != self.counterSize): if (self.rightCounter < math.ceil(self.counterSize * 0.4)): if (self.lingertime > self.lingertimeCounter): self.lingertimeCounter += 1 else: if (self.timer.interval() > self.floorInterval): self.timer.setInterval(self.timer.interval() - 1) self.lingertime = self.timerBaseInterval - self.timer.interval( ) self.lingertimeCounter = 5 elif (self.rightCounter > self.counterSize - math.ceil(self.counterSize * 0.4)): if (self.lingertime > self.lingertimeCounter): self.lingertimeCounter += 1 else: if (self.timer.interval() < self.timerBaseInterval): self.timer.setInterval(self.timer.interval() + 1) self.lingertime = self.timerBaseInterval - self.timer.interval( ) self.lingertimeCounter = 5 self.list_widget.horizontalScrollBar().setValue( self.list_widget.horizontalScrollBar().value() + self.incrementalStep) self.rightCounter += 1 self.repaint() if (self.rightCounter == math.ceil(self.counterSize / 2)): self.lingertimeCounter = 5 self.lingertime = self.timerBaseInterval else: self.timer.setInterval(self.timerBaseInterval) self.rightCounter = 0 self.timer.stop() self.pic.setEnabled(True) self.pic2.setEnabled(True) if (self.list_widget.horizontalScrollBar().value() == self.list_widget.horizontalScrollBar().maximum()): self.timer.setInterval(self.timerBaseInterval) self.rightCounter = 0 self.timer.stop() self.pic.setEnabled(True) self.pic2.setEnabled(True)
class GameScene(Scene): def __init__(self, parentWindow): super().__init__(parentWindow) #初始化场景 #传入从0开始,1为递增量的难度系数 def init(self, difficulty): #获取宽高方向地图线的数目 self.spaceNum = 20 #获取宽和高方向的间隔 self.widthSpace = int(self.width() / self.spaceNum) self.heightSpace = int(self.height() / self.spaceNum) #设定FPS值 #平常的按阶梯递增的难度 if difficulty == 3: self.fps = 1000 #光速难度 else: self.fps = difficulty * 5 + 5 #初始化蛇 self.snake = Snake([5, 5], 2, 3) #初始化食物 self.food = Food() self.food.bodyGrowth = difficulty + 1 self.food.updatePos(-1, self.spaceNum, -1, self.spaceNum, self.snake.pos) #初始化当前分数 self.score = 0 #设置分数增长 self.scoreGrowth = difficulty + 1 #设置背景颜色 self.setAutoFillBackground(True) self.setPalette(QPalette(QColor(255, 255, 255))) #绘制画面定时器 self.timer = QTimer(self) self.timer.timeout.connect(self.gameCycle) self.timer.setInterval(1000 / self.fps) self.timer.start() #重写按键事件 def keyPressEvent(self, event): if not event.isAutoRepeat(): if event.key() == Qt.Key_Up and abs( self.snake.pos[0][1] - self.snake.pos[1][1]) != 1: self.snake.dir = 0 elif event.key() == Qt.Key_Down and abs( self.snake.pos[0][1] - self.snake.pos[1][1]) != 1: self.snake.dir = 1 elif event.key() == Qt.Key_Left and abs( self.snake.pos[0][0] - self.snake.pos[1][0]) != 1: self.snake.dir = 2 elif event.key() == Qt.Key_Right and abs( self.snake.pos[0][0] - self.snake.pos[1][0]) != 1: self.snake.dir = 3 elif event.key() == Qt.Key_Space: self.timer.setInterval(self.timer.interval() / 3) return super().keyPressEvent(event) #按键释放的时候的事件 def keyReleaseEvent(self, event): if not event.isAutoRepeat(): if event.key() == Qt.Key_Space: self.timer.setInterval(self.timer.interval() * 3) return super().keyReleaseEvent(event) #游戏循环 def gameCycle(self): #蛇的移动 self.snake.move() #如果吃到了食物 if self.snake.eatFood(self.food): #刷新分数 self.score += self.scoreGrowth #刷新食物坐标 self.food.updatePos(-1, self.spaceNum, -1, self.spaceNum, self.snake.pos) #判断游戏是否结束 if self.snake.isDead(0, self.spaceNum - 1, 0, self.spaceNum - 1): self.gameEnd() #更新界面,触发paintEvent事件重绘界面 self.update() #绘制事件 def paintEvent(self, event): painter = QPainter(self) #绘制背景线 for i in range(self.spaceNum): painter.drawLine(i * self.widthSpace, 0, i * self.widthSpace, self.height()) for i in range(self.spaceNum): painter.drawLine(0, i * self.heightSpace, self.width(), i * self.heightSpace) #绘制蛇身 painter.setBrush(QBrush(QColor(0, 0, 255))) for i, j in self.snake.pos: painter.drawRect(i * self.widthSpace, j * self.heightSpace, self.widthSpace, self.heightSpace) #绘制食物 painter.setBrush(QBrush(QColor(255, 0, 0))) painter.drawRect(self.food.x * self.widthSpace, self.food.y * self.heightSpace, self.widthSpace, self.heightSpace) #绘制分数 painter.setPen(Qt.red) painter.setFont(QFont("微软雅黑", 13, 5)) painter.drawText(0, 0, self.width(), 25, Qt.AlignCenter, "分数:%d" % self.score) painter.end() return super().paintEvent(event) #游戏结束 def gameEnd(self): #定时器停止 self.timer.stop() #判断是否是最高分 #如果是的话,则保存最高分并且录入保持者名字 if self.score > Config().highestScore: Config().highestScore = self.score highestScorePlayer = "" #必须有纪录保持者 while highestScorePlayer == "": highestScorePlayer = QInputDialog.getText( self, self.tr("最高分!"), self.tr("恭喜你的分数 %d 成为最高分,请留下你的尊姓大名。") % self.score)[0] #录入最高分纪录保持者 Config().highestScorePlayer = highestScorePlayer #判断是否要再来一局 #如果不要的话回到开始场景 if QMessageBox.information( self, self.tr("游戏结束"), self.tr("您的得分是:%d,是否要以当前难度再来一局?") % self.score, QMessageBox.Yes, QMessageBox.No) == QMessageBox.No: startScene = StartScene(Director().window) Director().nowScene = startScene startScene.init() startScene.show() self.deleteLater() #否则将游戏的各项数据设置复原 else: #重置蛇和食物 self.snake = Snake([5, 5], 2, 3) self.food.updatePos(-1, self.spaceNum, -1, self.spaceNum, self.snake.pos) #重置分数 self.score = 0 #重置绘制定时器 self.timer.setInterval(1000 / self.fps) self.timer.start()
class Place(QWidget): def __init__(self): super().__init__(None, Qt.WindowCloseButtonHint | Qt.WindowMaximizeButtonHint) loadUi("gui/Place.ui", self) self.services = dict() self.queue = dict() self.client_count = 0 self.ticket = Ticket(self.groupBox, self) self.ticket.hide() self.ticket_anim = QPropertyAnimation(self.ticket, b"pos") self.ticket_anim.setEasingCurve(QEasingCurve.OutExpo) self.ticket_anim.setDuration(1000) self.ticket_time = QTimer(self) self.ticket_time.setInterval(1500) self.ticket_time.timeout.connect(self.ticket.hide) self.emulation = QTimer(self) self.emulation.timeout.connect(self.emulate) self.emulation_events = None self.client_event = RandomEvent(100 - ClientProb, ClientProb) self.switch_emul.clicked.connect(self.switch_emulation) self.display.mousePressEvent = lambda: None self.top_left.setAlignment(Qt.AlignLeft) self.delay.setValidator(QIntValidator(0, 99999)) self.delay.returnPressed.connect(lambda: self.emulation.setInterval(int(self.delay.text()))) def init(self, services: dict, emulation): try: if bool(emulation): self.init_emulation(len(services), emulation) win_index = 0 for index, service in enumerate(services): option = TButton(self, service, index) option.setEnabled(services[service] > 0) option.clicked.connect(self.select_service) self.terminal.addWidget(option, index // 2, index % 2) for i in range(services[service]): if not self.services.get(Alpha[index], False): self.services[Alpha[index]] = list() window = Window(self, win_index + 1, index) self.services[Alpha[index]].append(window) self.windows_place.addWidget(window, win_index // 3, win_index % 3) win_index += 1 self.show() self.splitter.setSizes([self.height()//2, self.height()//2]) self.splitter2.setSizes([self.bottom.height()//2, self.bottom.height()//2]) except Exception as e: print("init", e) def init_emulation(self, servicesCount: int, delay): global Emulation Emulation = True self.bottom_area.setEnabled(False) self.ticket.setEnabled(False) self.switch_emul.setChecked(True) self.delay.setText(str(delay)) self.emulation.setInterval(delay) self.emulation.start() events = [100 // servicesCount for _ in range(servicesCount)] if not (100 / servicesCount).is_integer(): events.append(100 - sum(events)) self.emulation_events = RandomEvent(*events, shuffle=True) def emulate(self): if self.client_event.event() and Emulation: index = self.emulation_events.event() if index == len(self.services): return self.emulate() self.select_service(index) def switch_emulation(self, turn_on): try: if self.emulation.interval() == 0: self.init_emulation(len(self.services), 1500) global Emulation Emulation = turn_on self.bottom_area.setEnabled(not turn_on) self.ticket.setEnabled(not turn_on) if turn_on: self.emulation.start() self.switch_emul.setText(" Pause emulation ") else: self.switch_emul.setText(" Continue emulation ") self.emulation.stop() except Exception as e: print("switch_emulation", e) def select_service(self, index): try: if isinstance(self.sender(), QPushButton): service = Alpha[self.sender().index] else: service = Alpha[index] self.client_count += 1 client = service + str(self.client_count) if not self.queue.get(service, False): self.queue[service] = Queue() self.queue[service].enqueue(client) self.print_ticket(client) self.check_windows(client) except Exception as e: print("select_service", e) def print_ticket(self, client): try: if self.groupBox.height() > 0 and self.groupBox.width() > 0: y = self.groupBox.height() // 2 - self.ticket.height() // 2 + 10 x = self.groupBox.width() // 2 - self.ticket.width() // 2 self.ticket_anim.setStartValue(QPoint(-self.ticket.width(), y)) self.ticket_anim.setEndValue(QPoint(x, y)) self.ticket.text.setText("Your number\n" + client) self.ticket.show() self.ticket_anim.start() self.ticket_time.start() except Exception as e: print("print_ticket", e) def check_windows(self, client): try: for window in self.services[client[0]]: if window.client is None: window.makeOrder(client) self.queue[client[0]].dequeue() return except Exception as e: print("check_windows", e) def keyPressEvent(self, event): if event.key() == Qt.Key_Escape: self.close()
class MonitorVerb(DisplayVerb): """ Base class for Monitor verbs (verbs 11 through 17 inclusive) """ def __init__(self, name, verb_number, noun): """ Class constructor :param name: name (description) of verb :type name: string :param verb_number: the verb number :type verb_number: str :return: None """ super().__init__(name, verb_number, noun) self.timer = QTimer() self.timer.timeout.connect(self._update_display) self.is_tooltips_set = False def _send_output(self): """ Sends the requested output to the DSKY """ # check if the display update interval needs to be changed if self.timer.interval() != config.DISPLAY_UPDATE_INTERVAL: # stop and start the timer to change the update interval self.timer.stop() self.timer.start(config.DISPLAY_UPDATE_INTERVAL) if self.noun is None: self.noun = Verb.computer.keyboard_state["requested_noun"] if self.noun in self.illegal_nouns: raise NounNotAcceptableError noun_function = Verb.computer.nouns[self.noun]() try: data = noun_function.return_data() except nouns.NounNotImplementedError: self.computer.operator_error("Noun {} not implemented yet. Sorry about that...".format(self.noun)) self.terminate() return except KSPNotConnected: utils.log("KSP not connected, terminating V{}".format(self.number), log_level="ERROR") Verb.computer.program_alarm(110) self.terminate() raise except TelemetryNotAvailable: utils.log("Telemetry not available, terminating V{}".format(self.number), log_level="ERROR") Verb.computer.program_alarm(111) self.terminate() raise if not data: # if the noun returns False, the noun *should* have already raised a program alarm, so we just need to # terminate and return self.terminate() return output = self._format_output_data(data) # set tooltips if not self.is_tooltips_set: Verb.computer.dsky.set_tooltip("data_1", data["tooltips"][0]) Verb.computer.dsky.set_tooltip("data_2", data["tooltips"][1]) Verb.computer.dsky.set_tooltip("data_3", data["tooltips"][2]) self.is_tooltips_set = True # display data on DSKY registers Verb.computer.dsky.set_register(output[0], "data_1") Verb.computer.dsky.set_register(output[1], "data_2") Verb.computer.dsky.set_register(output[2], "data_3") Verb.computer.dsky.flash_comp_acty() def start_monitor(self): """ Starts the timer to monitor the verb """ # if Verb.computer.keyboard_state["backgrounded_update"] is not None: # Verb.computer.keyboard_state["backgrounded_update"].terminate() Verb.computer.keyboard_state["display_lock"] = self try: self._send_output() except KSPNotConnected: return except TelemetryNotAvailable: return if self.noun is None: #JRI if monitoring was terminated noun=None and we don't want to start the timer. return self.timer.start(config.DISPLAY_UPDATE_INTERVAL) def _update_display(self): """ a simple wrapper to call the display update method """ # if not self.activity_timer.active(): # self.activity_timer.Start(1000) self._send_output() def terminate(self): """ Terminates the verb :return: None """ utils.log("Terminating V{}".format(self.number)) Verb.computer.dsky.stop_annunciator_blink("key_rel") Verb.computer.keyboard_state["display_lock"] = None Verb.computer.keyboard_state["backgrounded_update"] = None self.timer.stop() self.noun = None # self.activity_timer.Stop() # reset tooltips to "" Verb.computer.dsky.set_tooltip("data_1", "") Verb.computer.dsky.set_tooltip("data_2", "") Verb.computer.dsky.set_tooltip("data_3", "") def background(self): """ Backgrounds verb display updates :return: None """ Verb.computer.keyboard_state["backgrounded_update"] = self Verb.computer.keyboard_state["display_lock"] = None self.timer.stop() Verb.computer.dsky.start_annunciator_blink("key_rel") def resume(self): """ Resumes verb display updates :return: None """ Verb.computer.keyboard_state["display_lock"] = self Verb.computer.keyboard_state["backgrounded_update"] = None Verb.computer.dsky.set_register(self.number, "verb") Verb.computer.dsky.set_register(self.noun, "noun") self.start_monitor()
class AbcPlayable(ABC): def __init__(self): super().__init__() # self.signals = Signals() self.timer = QTimer() self.timer.setInterval(50) self.signals = self.Signals() self._flag_playing = False self._flag_cur_index = -1 self.scheduled = self.Schedule(None, None, None) self.timer.timeout.connect(self.flush) @property @abstractmethod def indices(self): pass @property def fps(self): return 1000 / self.timer.interval() @fps.setter def fps(self, fps): logger.debug(fps) self.timer.setInterval(1000 / fps) def is_playing(self): return self._flag_playing def start(self, clear_schedule=False): logger.debug('') self._flag_playing = True if clear_schedule: self.scheduled.clear() if not self.timer.isActive(): self.timer.start() def pause(self): logger.debug('') self._flag_playing = False # self.timer.stop() @abstractmethod def set_viewer(self, view): pass @abstractmethod def flush(self): pass @abstractmethod def to_head(self): pass @abstractmethod def to_tail(self): pass def schedule(self, jump_to, bias, stop_at, emitter): logger.debug(f'{jump_to}, {bias}, {stop_at}, {emitter}, {self._flag_cur_index}') last_frame_index = self.indices[-1] jump_to = self._flag_cur_index + bias if jump_to == -1 else max(0, min(jump_to, last_frame_index)) stop_at = None if stop_at == -1 else max(0, min(stop_at, last_frame_index)) self.scheduled.set(emitter, jump_to, stop_at) class Signals(QObject): flushed = pyqtSignal(int) class Schedule: def __init__(self, emitter, jump_to, stop_at): self.emitter = emitter self.jump_to = jump_to self.stop_at = stop_at def set(self, emitter, jump_to, stop_at): self.emitter = emitter self.jump_to = jump_to self.stop_at = stop_at logger.debug(f'scheduled: {self}') def clear(self): self.emitter = None self.jump_to = None self.stop_at = None def __str__(self): return f'emitter:{self.emitter} jumpTo:{self.jump_to} stopAt:{self.stop_at}' def __bool__(self): return self.emitter is not None and self.jump_to is not None
class Main(QMainWindow): current_x = 0. current_y = 0. current_z = 0. def __init__(self, *args): super(Main, self).__init__(*args) loadUi('mainwindow.ui', self) self.flavor = config.get('flavor', default='smoothie') self['connection.host'] = config.get('server_host', 'localhost') self['connection.port'] = str(config.get('server_port', '11011')) #self.prompt.setText("M114") self.prompt.addItem("M114") self.prompt.addItem("MULTILINE") #self.conn = QTcpSocket(self) self.conn = QHcClient(self) self.conn.readyRead.connect(self.readSocket) self.conn.error.connect(self.socketError) self.conn.connected.connect(self.socketConnect) self.conn.disconnected.connect(self.socketDisconnect) self.connected = False self.actionSave_log.triggered.connect(self.save_log_dialog) self.actionSave_probe_data.triggered.connect( self.save_probe_data_dialog) self.actionLoad_probe_data.triggered.connect( self.load_probe_data_dialog) self.actionLoad_G_code.triggered.connect(self.load_gcode_dialog) self.actionSave_G_code.triggered.connect(self.save_gcode_dialog) self.actionSave_probe_G_code.triggered.connect( self.save_probe_gcode_dialog) self.prompt.setFocus() self.prompt.lineEdit().returnPressed.connect(self.on_send_clicked) # paramtree handlers self.ptree.params.sigTreeStateChanged.connect(self.pchange) self.ptree.changing(self.changing) self.ptree.params.param('Connection', 'Connect').sigActivated.connect( self.do_connect) self.ptree.params.param('Probe', 'Run probe').sigActivated.connect( self.run_probe) self.ptree.params.param('Probe', 'Process').sigActivated.connect(self.process) self.ptree.params.param('Probe', 'Save processed G-code').sigActivated.connect( self.save_gcode_dialog) self.ptree.params.param('GCode', 'Load G-code').sigActivated.connect( self.load_gcode_dialog) # alias self.p = self.ptree.get_param self.ptree self.do_connect() self.update_probe() self.update_grid() #self.commodel = QStandardItemModel(self.comlist) #self.comlist.setModel(self.commodel) self.comtree.setColumnCount(3) self.comtree.setColumnWidth(0, 200) self.comtree.setColumnWidth(1, 350) self.comtree.setHeaderLabels(['Time', 'Command', 'Response']) #self.comtree.header().setSectionResizeMode(2, QHeaderView.Stretch) self.comtree.header().setSectionResizeMode( 2, QHeaderView.ResizeToContents) self.comtree.header().setStretchLastSection(False) self.reconnect_timer = QTimer() self.reconnect_timer.timeout.connect(self.do_connect) #self.otimer = QTimer() #self.otimer.timeout.connect(self.gl.autoorbit) #self.otimer.start(50) def __getitem__(self, attr): """ Access HCParameTree values via self['some.path'] """ param = self.ptree.get_param(attr) return param.value() def __setitem__(self, attr, val): """ Set HCParameTree values via self['some.path'] """ param = self.ptree.get_param(attr) param.setValue(val) def changing(self, param, value): print('Changing {} {}'.format(param, value)) path = self.ptree.params.childPath(param) if path is not None: pl = '.'.join(path).lower() self.handle_updates(pl) def pchange(self, param, changes): """ HCParamTreechange handler """ for param, change, data in changes: path = self.ptree.params.childPath(param) if path is not None: pl = '.'.join(path).lower() self.handle_updates(pl) childName = '.'.join(path) else: childName = param.name() print(' parameter: %s' % childName) print(' change: %s' % change) print(' data: %s' % str(data)) print(' ----------') def handle_updates(self, path=None): if path: if path.startswith('probe'): self.update_probe() if path.startswith('grid'): self.update_grid() if path.startswith('cross'): self.update_cross() if path.startswith('gcode'): self.update_gcode() if path.startswith('probe result'): self.update_proberesult() def load_gcode_dialog(self): d = QFileDialog(self) d.setNameFilter("GCode (*.ngc *.gcode);;All files (*.*)") d.exec_() name = d.selectedFiles()[0] try: self.gl.gcode.load_gcode(name) # prefill probe width / height xmin, xmax = self.gl.gcode.limits['X'] ymin, ymax = self.gl.gcode.limits['Y'] zmin, zmax = self.gl.gcode.limits['Z'] self['probe.width'] = xmax self['probe.height'] = ymax self['gcode.width'] = xmax self['gcode.height'] = ymax self['gcode.min z'] = zmin self['gcode.max z'] = zmax self.gcode_path = name print('Loaded {}'.format(name)) except IOError as e: print('Unable to load {}'.format(name)) print(e) def save_gcode_dialog(self): d = QFileDialog(self) d.setNameFilter("GCode (*.ngc *.gcode);;All files (*.*)") d.exec_() name = d.selectedFiles()[0] try: if self.gl.postgcode.orig: self.gl.postgcode.save_gcode(name) print('Saved post-processed g-code to {}'.format(name)) elif self.gl.gcode.orig: self.gl.gcode.save_gcode(name) print('Saved original g-code to {}'.format(name)) else: print('Nothing to save') except IOError as e: print('Unable to save to {}'.format(name)) print(e) def append(self, text): self.text.append(text) #if self.autoscroll.isChecked(): # c = self.text.textCursor() # c.movePosition(QTextCursor.End, QTextCursor.MoveAnchor) # self.text.setTextCursor(c) def handle_response(self, idx, txt): root = self.comtree.invisibleRootItem() item = root.child(idx) item.setText(0, time.strftime("%Y.%m.%d. %H:%M:%S", time.localtime())) if not txt: txt = 'ok' item.setText(2, txt) #item.setBackground(0, QtGui.QBrush(green)) item.setCheckState(0, Qt.Checked) self.comtree.scrollToItem(item) cmd = item.text(1) if 'G0' in cmd: x, y, z = parse.xyz(cmd[2:]) # should probably emit signals self.current_x = x self.current_y = y self.current_z = z if 'G38.2' in cmd: try: z = parse.probe(txt) except: log.err('Unable to parse probe: {}'.format(txt)) log.err('Is your flavor ({}) correct?'.format(self.flavor)) z = 0.0, 0.0 return self.gl.proberes.probe_results.append( (self.current_x, self.current_y, z)) self.gl.proberes.update_results() self['probe result.lowest'] = min(self['probe result.lowest'], z) self['probe result.highest'] = max(self['probe result.highest'], z) self['probe result.last'] = z def save_probe_data_dialog(self): if not self.gl.proberes.probe_results: # err not much to save return fname, sel = QFileDialog.getSaveFileName( self, 'Save Log', ) #'/path/to/default/directory', FIXME: lastused #selectedFilter='*.txt') if fname: self.save_probe_data(fname) def save_probe_data(self, fname): with open(fname, 'w') as f: for x, y, z in self.gl.proberes.probe_results: f.write("{:04.2f} {:04.2f} {:04.2f}\n".format(x, y, z)) def load_probe_data_dialog(self): d = QFileDialog(self) d.setNameFilter("Log data (*.txt *.log);;All files (*.*)") d.exec_() fname = d.selectedFiles()[0] if fname: self.load_probe_data(fname) def load_probe_data(self, fname): with open(fname, 'r') as f: d = (map(lambda x: map(float, x.split()), f.readlines())) self.gl.proberes.probe_results = d self.gl.proberes.update_results() @pyqtSlot() def on_save_clicked(self): root = self.comtree.invisibleRootItem() {'name': 'Visible', 'type': 'bool', 'value': 1}, count = root.childCount() parts = [] for i in range(count): item = root.child(i) time = item.text(0) cmd = item.text(1) resp = item.text(2) parts.append((time, cmd, resp)) fname, sel = QFileDialog.getSaveFileName( self, 'Save Log', ) #'/path/to/default/directory', FIXME: lastused #selectedFilter='*.txt') if fname: with open(fname, 'w') as f: for time, cmd, resp in parts: f.write('{}\t{}\t{}\n'.format(time, cmd, resp)) def readSocket(self): def handle(r): if not r: return #print('buffered', r) (idx, txt) = dec_msg(r) if idx is not None: self.handle_response(idx, txt) buffer = '' while True: r = str(self.conn.readLine()) if not r: handle(buffer) if self.conn.canReadLine(): buffer = '' continue break if r[0] == '/': self.append('{}'.format(r.strip())) continue if r[0] == '[': handle(buffer) buffer = r continue buffer += r def info(self, errtext): self.text.setTextColor(QColor(20, 20, 20)) self.append(errtext) def err(self, errtext): self.text.setTextColor(QColor(100, 0, 0)) self.append(errtext) def socketDisconnect(self): self.err("Disconnected") self.connected = False self.info("Reconnecting") self.reconnect_timer.start(1000) def socketConnect(self): self.connected = True self.reconnect_timer.stop() self.info("Connected to {}:{}".format(self['connection.host'], self['connection.port'])) self.ptree.collapse_group('connection') def socketError(self, socketError): # backoff self.reconnect_timer.setInterval(self.reconnect_timer.interval() * 2) if socketError == QAbstractSocket.RemoteHostClosedError: pass elif socketError == QAbstractSocket.HostNotFoundError: self.err("The host was not found. Please check the host name and " "port settings.") elif socketError == QAbstractSocket.ConnectionRefusedError: self.err("The connection was refused by the peer. Make sure the " "server is running, and check that the host name " "and port settings are correct.") else: self.err("The following error occurred: {0}".format( self.conn.errorString())) def save_log_dialog(self): fname, sel = QFileDialog.getSaveFileName( self, 'Save Log', ) #'/path/to/default/directory', FIXME: lastused #selectedFilter='*.txt') if fname: with open(fname, 'w') as f: f.write(self.text.toPlainText()) def save_probe_gcode_dialog(self): fname, sel = QFileDialog.getSaveFileName( self, 'Save probe G-code', ) #'/path/to/default/directory', FIXME: lastused #selectedFilter='*.txt') if fname: with open(fname, 'w') as f: for code in self.gen_probe_gcode(self.get_probe_points()): f.write(code + '\n') def do_connect(self): self.conn.abort() self.info("Connecting to {}:{}".format(self['connection.host'], self['connection.port'])) self.conn.connectToHost(self['connection.host'], int(self['connection.port'])) @pyqtSlot() def on_prompt_activated(self): print("LAA") def run_cmd(self, cmd): item = QTreeWidgetItem(self.comtree) item.setText(0, time.strftime("%Y.%m.%d. %H:%M:%S", time.localtime())) item.setText(1, cmd) for i in range(3): item.setTextAlignment(i, Qt.AlignTop) item.setForeground(1, QtGui.QBrush(blue)) item.setForeground(1, QtGui.QBrush(green)) item.setForeground(2, QtGui.QBrush(red)) self.comtree.scrollToItem(item) self.conn.cmd(cmd) proc_events() @pyqtSlot() def on_send_clicked(self): if not self.connected: self.err("Not connected") return out = self.prompt.currentText() self.run_cmd(out) # FIXME: should go to hc lib def gen_probe_grid(self, rows, cols, w, h, x_margin, y_margin, start_z): w = w - x_margin * 2. h = h - y_margin * 2. if rows <= 0 or cols <= 0: return [] if cols == 1 or rows == 1: return [] xstep = w / (cols - 1) ystep = h / (rows - 1) cx = x_margin cy = y_margin out = [] for i in range(rows): for j in range(cols): out.append((cx, cy, start_z)) cx += xstep cx = x_margin cy += ystep return out def get_probe_points(self): m = self['probe.margin'] probe_points = self.gen_probe_grid(self['probe.rows'], self['probe.cols'], self['probe.width'], self['probe.height'], m, m, self['probe.start z']) return probe_points def gen_probe_gcode(self, points): feed = self['probe.feedrate'] depth = self['probe.max depth'] sz = self['probe.start z'] yield 'G90' for point in points: yield 'G0 {}'.format(xyzfmt(*point)) yield probecmd.format(depth, feed) # go straight up startz/2. yield 'G0 Z{}'.format(sz / 2.) def run_probe(self): # clean probe result self.gl.proberes.probe_results = [] for code in self.gen_probe_gcode(self.get_probe_points()): self.run_cmd(code) def update_gcode(self): self.gl.gcode.setVisible(self['gcode.visible']) def update_probe(self): probe_points = self.get_probe_points() self.gl.probelist.probe_points = probe_points self.gl.probelist.update() def update_proberesult(self): self.gl.proberes.setVisible(self['probe result.visible']) def update_grid(self): w = self['grid.width'] h = self['grid.height'] self.gl.grid.setSize(w, h, 1) self.gl.grid.resetTransform() self.gl.grid.translate(w / 2., h / 2., -0.05) self.gl.grid.setVisible(self['grid.visible']) def update_cross(self): s = self['cross.size'] self.gl.cross.setVisible(self['cross.visible']) #self.gl.grid.setSize(w, h, 1) #self.gl.grid.translate(w/2., h/2., -0.05) #@pyqtSlot() #def on_normalize_clicked(self): # res = self.gl.proberes.probe_results[:] # print(res) # sres = sorted(res, key=lambda x: x[2]) # print(sres) # minz = sres[0][2] # maxz = sres[-1][2] # print("Normalizing, minz {} maxz {}".format(minz, maxz)) # nres = [] # for x, y, z in res: # nz = z - maxz # nz = abs(nz) # nres.append((x, y, nz)) # print(nres) # self.gl.proberes.probe_results = nres # self.gl.proberes.update_results() def process(self): self.info('Processing') self.precision = 0.1 res = self.gl.proberes.probe_results[:] if not res: self.err('No probe results') return datapath = '/tmp/hc_probedata' print("RES") print(res) with open(datapath, 'w') as f: for x, y, z in res: f.write("{:04.2f} {:04.2f} {:04.2f}\n".format(x, y, z)) script = config.get('leveling_tool', 'scale_gcode.py') gcpath = os.path.abspath(self.gcode_path) args = '{} 1-999999 --zlevel {} {:.2f}'.format(gcpath, datapath, self.precision) args = args.split() self.info('Calling "{}"'.format(" ".join([script] + args))) proc_events() import subprocess proc = subprocess.Popen([script] + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True) (stdout, stderr) = proc.communicate() print("stderr:") print(stderr) print("stdout:") print(stdout) self.info('Done. Saving to /tmp/hc_postgcode') fpath = '/tmp/hc_postgcode' with open(fpath, 'w') as f: f.write(stdout) self.gl.postgcode.load_gcode(fpath) self.info('Loaded post-processed G-code')