class RegexPatternValidator(gui.Validator): error_occured = core.Signal(str) pattern_updated = core.Signal(object) def __repr__(self): return f"{type(self).__name__}()" def __eq__(self, other: object): return isinstance(other, type(self)) def validate( # type: ignore self, text: str, pos: int = 0) -> tuple[QtGui.QValidator.State, str, int]: # if text == "": # self.compiled = None # return (self.Intermediate, text, pos) try: compiled = re.compile(text) except sre_constants.error as e: self.error_occured.emit(str(e)) self.pattern_updated.emit(None) return self.State.Intermediate, text, pos except re._regex_core.error as e: self.error_occured.emit(str(e)) self.pattern_updated.emit(None) return self.State.Intermediate, text, pos else: self.error_occured.emit("") self.pattern_updated.emit(compiled) return self.State.Acceptable, text, pos
class ScrollBar(QtWidgets.QScrollBar): value_changed = core.Signal(int) def __init__(self, orientation="horizontal", parent=None): super().__init__(ORIENTATIONS[orientation], parent) self.valueChanged.connect(self.on_value_change)
class TimeEdit(QtWidgets.QTimeEdit): value_changed = core.Signal(datetime.datetime) def __getstate__(self): return dict(calendar_popup=self.calendarPopup(), time=self.get_time(), display_format=self.displayFormat(), range=(self.min_time(), self.max_time()), tooltip=self.toolTip(), statustip=self.statusTip(), enabled=self.isEnabled()) def __setstate__(self, state): self.__init__(state["time"]) self.setEnabled(state.get("enabled", True)) self.setDisplayFormat(state["display_format"]) self.set_range(*state["range"]) self.setToolTip(state.get("tooltip", "")) self.setStatusTip(state.get("statustip", "")) def set_range(self, lower: datetime.time, upper: datetime.time): self.setToolTip(f"{lower} <= x <= {upper}") self.setTimeRange(lower, upper) def get_value(self) -> datetime.time: return self.get_time() def set_value(self, value: datetime.time): return self.setTime(value)
class DetachedTab(widgets.MainWindow): """window containing a detached tab When a tab is detached, the contents are placed into this QMainWindow. The tab can be re-attached by closing the dialog Attributes: on_close: signal, emitted when window is closed (widget, title, icon) """ on_close = core.Signal(QtWidgets.QWidget, str, QtGui.QIcon) def __init__(self, name, widget): super().__init__(None) self.id = name self.title = name self.widget = widget self.setCentralWidget(self.widget) self.widget.show() # If the window is closed, emit the on_close and give the # content widget back to the DetachableTabWidget def closeEvent(self, event): self.on_close.emit(self.widget, self.id, self.windowIcon())
class RadioButton(QtWidgets.QRadioButton): value_changed = core.Signal(bool) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.toggled.connect(self.value_changed)
class SingleApplication(widgets.Application): messageReceived = core.Signal(str) def __init__(self, app_id: str): super().__init__(sys.argv) self.app_id = app_id self._activate_on_message = True # Is there another instance running? out_socket = network.LocalSocket() out_socket.connect_to_server(self.app_id) self._is_running = out_socket.waitForConnected() self._in_socket = None self._in_stream = None if not self._is_running: self._out_socket = None self._out_stream = None self._server = network.LocalServer() self._server.listen(self.app_id) self._server.newConnection.connect(self._on_new_connection) else: self._out_socket = out_socket self._out_stream = core.TextStream(self._out_socket) self._out_stream.set_codec("UTF-8") def is_running(self) -> bool: return self._is_running def activate_window(self): window = self.get_mainwindow() if window is None: return window.raise_to_top() def send_message(self, msg: str) -> bool: if self._out_stream is None or self._out_socket is None: return False self._out_stream << msg << "\n" self._out_stream.flush() return self._out_socket.waitForBytesWritten() def _on_new_connection(self): if self._in_socket: self._in_socket.readyRead.disconnect(self._on_ready_read) self._in_socket = self._server.nextPendingConnection() # type: ignore if self._in_socket is None: return self._in_stream = core.TextStream(self._in_socket) self._in_stream.set_codec("UTF-8") self._in_socket.readyRead.connect(self._on_ready_read) if self._activate_on_message: self.activate_window() def _on_ready_read(self): if self._in_stream is None: raise RuntimeError() for msg in self._in_stream.read_lines(): self.messageReceived.emit(msg)
class DateEdit(QtWidgets.QDateEdit): value_changed = core.Signal(datetime.datetime) def __setstate__(self, state): super().__setstate__(state) self.setDate(state["date"]) self.set_range(*state["range"]) def __reduce__(self): return type(self), (), self.__getstate__() def serialize_fields(self): return dict( date=self.get_date(), range=(self.min_date(), self.max_date()), ) def set_value(self, value: types.DateType): if isinstance(value, str): value = QtCore.QDate.fromString(value) self.setDate(value) # type: ignore def set_range(self, lower: types.DateType, upper: types.DateType): if isinstance(lower, str): lower = QtCore.QDate.fromString(lower) if isinstance(upper, str): upper = QtCore.QDate.fromString(upper) self.setToolTip(f"{lower} <= x <= {upper}") self.setDateRange(lower, upper) # type: ignore def get_value(self) -> datetime.date: return self.get_date()
class FontChooserButton(widgets.Widget): value_changed = core.Signal(gui.Font) def __init__( self, font: QtGui.QFont | None = None, parent: QtWidgets.QWidget | None = None, ): super().__init__(parent) self._current_font = font layout = widgets.BoxLayout("horizontal", self) layout.set_margin(0) self.lineedit = widgets.LineEdit() self.lineedit.set_read_only() layout.add(self.lineedit) action = widgets.Action() action.triggered.connect(self.choose_font) self.button = widgets.ToolButton() self.button.setDefaultAction(action) layout.add(self.button) def __repr__(self): return f"{type(self).__name__}({self._current_font})" def serialize_fields(self): return dict(current_font=self._current_font) def __setstate__(self, state): super().__setstate__(state) if state["current_font"]: self.set_value(state["current_font"]) self.set_enabled(state.get("enabled", True)) def __reduce__(self): return type(self), (), self.__getstate__() @core.Slot() def choose_font(self): dlg = widgets.FontDialog() if self._current_font: dlg.setCurrentFont(self._current_font) if dlg.main_loop(): self.set_current_font(dlg.current_font()) self.value_changed.emit(dlg.current_font()) def set_current_font(self, font: str | QtGui.QFont): if isinstance(font, str): self._current_font = gui.Font(font) else: self._current_font = font self.lineedit.setText(self._current_font.family()) def set_value(self, value: str | QtGui.QFont): self.set_current_font(value) def get_value(self): return self._current_font
class ColorChooserButton(widgets.Widget): value_changed = core.Signal(gui.Color) def __init__(self, color=None, parent=None): super().__init__(parent) layout = widgets.BoxLayout("horizontal", self) layout.set_margin(0) self.lineedit = widgets.LineEdit() self.lineedit.set_regex_validator(r"^#(?:[0-9a-fA-F]{6})$") layout += self.lineedit action = widgets.Action(icon="mdi.format-color-fill") action.triggered.connect(self.choose_color) self.button = widgets.ToolButton() self.button.setDefaultAction(action) layout += self.button if color is not None: self.set_color(color) def __repr__(self): return f"ColorChooserButton({self.current_color})" def __getstate__(self): return dict(color=self.current_color, enabled=self.isEnabled()) def __setstate__(self, state): self.__init__() if state["color"]: self.set_color(state["color"]) self.setEnabled(state.get("enabled", True)) @core.Slot() def choose_color(self): dlg = widgets.ColorDialog() if self.current_color: dlg.setCurrentColor(self.current_color) if dlg.exec_(): self.set_color(dlg.current_color()) self.value_changed.emit(dlg.current_color()) def set_color(self, color): if isinstance(color, str): self.current_color = gui.Color(color) else: self.current_color = color self.lineedit.set_text(self.current_color.name().upper()) icon = gui.Icon.for_color(self.current_color) self.button.set_icon(icon) def is_valid(self) -> bool: return self.lineedit.is_valid() def get_value(self): return self.current_color def set_value(self, value): self.set_color(value)
class LineSignalLogger(logging.Handler, core.Object): log_line = core.Signal(str) def __init__(self): super().__init__() core.Object.__init__(self) def emit(self, record): msg = self.format(record) self.log_line.emit(msg)
class SelectionWidget(widgets.GroupBox): option_changed = core.Signal(str) def __init__(self, parent=None): super().__init__(parent) self.layout = widgets.BoxLayout("horizontal") self.rb_other = widgets.RadioButton("Other") self.buttons = dict() # self.rb_comma.setChecked(True) self.setLayout(self.layout) def add_items(self, dct): for k, v in dct.items(): self.add_item(k, v) def select_radio_by_data(self, value): for rb, data in self.buttons.items(): if data == value: rb.setChecked(True) def add_item(self, title, data=None): rb = widgets.RadioButton(title) rb.toggled.connect(self.update_choice) self.buttons[rb] = data if len(self.buttons) == 1: rb.setChecked(True) self.layout.addWidget(rb) def add_custom(self, regex): self.lineedit_custom_sep = widgets.LineEdit(self) # TODO: Enable this or add BAR radio and option. self.lineedit_custom_sep.setEnabled(False) self.rb_other.toggled.connect(self.lineedit_custom_sep.setEnabled) self.lineedit_custom_sep.textChanged.connect(lambda: self.update_choice(True)) self.lineedit_custom_sep.set_regex_validator(regex) self.layout.addWidget(self.rb_other) self.layout.addWidget(self.lineedit_custom_sep) def current_choice(self): for k, v in self.buttons.items(): if k.isChecked(): return v if self.rb_other.isChecked(): return self.lineedit_custom_sep.text() return @core.Slot(bool) def update_choice(self, checked): if not checked: return None choice = self.current_choice() if len(choice) > 0: self.option_changed.emit(choice)
class FontComboBox(QtWidgets.QFontComboBox): value_changed = core.Signal(object) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.currentIndexChanged.connect(self.index_changed) def serialize_fields(self): return dict( current_font=self.get_current_font(), font_filters=self.get_font_filters(), ) def __setstate__(self, state): self.set_font_filters(*state.get("font_filters", [])) self.setCurrentFont(state["current_font"]) def __reduce__(self): return type(self), (), self.__getstate__() def set_font_filters(self, *filters: FontFilterStr): """Set font filters. Args: filters: font filters to use Raises: InvalidParamError: invalid font filters """ if not filters: filters = ("all",) for item in filters: if item not in FONT_FILTERS: raise InvalidParamError(item, FONT_FILTERS) flags = helpers.merge_flags(filters, FONT_FILTERS) self.setFontFilters(flags) def get_font_filters(self) -> list[FontFilterStr]: """Return list of font filters. Returns: font filter list """ return [k for k, v in FONT_FILTERS.items() if v & self.fontFilters()] def set_value(self, value: QtGui.QFont): self.setCurrentFont(value) def get_value(self) -> gui.Font: return self.get_current_font() def get_current_font(self) -> gui.Font: return gui.Font(self.currentFont())
class Slider(QtWidgets.QSlider): value_changed = core.Signal(int) def __init__( self, orientation: (constants.OrientationStr | QtCore.Qt.Orientation) = "horizontal", parent: QtWidgets.QWidget | None = None, ): if isinstance(orientation, QtCore.Qt.Orientation): ori = orientation else: ori = constants.ORIENTATION[orientation] super().__init__(ori, parent) self.valueChanged.connect(self.on_value_change) def serialize_fields(self): return dict( tick_position=self.get_tick_position(), tick_interval=self.tickInterval(), ) def __setstate__(self, state): super().__setstate__(state) self.set_tick_position(state["tick_position"]) self.setTickInterval(state["tick_interval"]) def __reduce__(self): return type(self), (), self.__getstate__() def set_tick_position(self, position: TickPositionAllStr): """Set the tick position for the slider. For vertical orientation, "above" equals to "left" and "below" to "right". Args: position: position for the ticks """ if position == "left": position = "above" elif position == "right": position = "below" elif position not in TICK_POSITION: raise InvalidParamError(position, TICK_POSITION) self.setTickPosition(TICK_POSITION[position]) def get_tick_position(self) -> TickPositionStr: """Return tick position. Returns: tick position """ return TICK_POSITION.inverse[self.tickPosition()]
class FlagSelectionWidget(widgets.GroupBox): value_changed = core.Signal(int) def __init__( self, label: str = "", layout: Literal["horizontal", "vertical"] = "vertical", parent: QtWidgets.QWidget | None = None, ): super().__init__(title=label, parent=parent) self.box = widgets.BoxLayout(layout) self.buttons: dict[widgets.CheckBox, int] = {} self.set_layout(self.box) def __iter__(self) -> Iterator[tuple[widgets.CheckBox, int]]: return iter(self.buttons.items()) def add_items(self, items: Iterable | Mapping): if isinstance(items, Mapping): for k, v in items.items(): self.add(v, k) else: for i in items: if isinstance(i, Iterable): self.add(*i) else: raise TypeError("Invalid item type") def add(self, title: str, flag: int): checkbox = widgets.CheckBox(title) checkbox.toggled.connect(self.update_choice) self.buttons[checkbox] = flag self.box.add(checkbox) def current_choice(self) -> int: ret_val = 0 for btn, flag in self.buttons.items(): if btn.isChecked(): ret_val |= flag return int(ret_val) @core.Slot(bool) def update_choice(self, checked: bool): choice = self.current_choice() self.value_changed.emit(choice) def set_value(self, value: int): value = int(value) for btn, flag in self.buttons.items(): btn.setChecked(bool(value & flag)) def get_value(self) -> int: return self.current_choice()
class PlaceManager(core.Object): on_finished = core.Signal(location.PlaceSearchReply) def __init__(self, item: QtLocation.QPlaceManager): super().__init__() self.item = item self.finished.connect(self._on_finished) def __getattr__(self, val): return getattr(self.item, val) def _on_finished(self, reply: QtLocation.QPlaceSearchReply): reply = location.PlaceSearchReply.clone_from(reply) self.on_finished.emit(reply) def get_category(self, cat_id: str) -> location.PlaceCategory: return location.PlaceCategory(self.item.category(cat_id)) def get_child_categories(self, cat_id: str) -> list[location.PlaceCategory]: return [ location.PlaceCategory(i) for i in self.item.childCategories(cat_id) ] def get_locales(self) -> list[core.Locale]: return [core.Locale(i) for i in self.locales()] def search_place( self, search_term: str, coord: tuple[float, float] | QtPositioning.QGeoCoordinate, radius: float | None = None, limit: int | None = None, relevance: location.placesearchrequest.RelevanceHintStr | None = None, categories: list[str] | None = None, ): request = location.PlaceSearchRequest() request.setSearchTerm(search_term) if radius is None: radius = -1 if isinstance(coord, tuple): coord = positioning.GeoCoordinate(*coord) circle = positioning.GeoCircle(coord, radius) request.setSearchArea(circle) if limit is not None: request.setLimit(limit) if relevance is not None: request.set_relevance_hint(relevance) if categories is not None: self.setCategories(categories) return self.search(request)
class FileChooserButton(widgets.Widget): value_changed = core.Signal(pathlib.Path) def __init__(self, extensions=None, mode="save", parent=None): super().__init__(parent) self.path = None self.extensions = extensions self.mode = mode layout = widgets.BoxLayout("horizontal", self) layout.set_margin(0) self.lineedit = widgets.LineEdit() self.lineedit.set_read_only() layout += self.lineedit action = widgets.Action() action.set_icon("mdi.file-outline") action.triggered.connect(self.open_file) self.button = widgets.ToolButton() self.button.setDefaultAction(action) layout += self.button def __getstate__(self): return dict(path=self.path, extensions=self.extensions, enabled=self.isEnabled()) def __setstate__(self, state): self.__init__(state["extensions"]) self.set_path(state["path"]) self.setEnabled(state.get("enabled", True)) @core.Slot() def open_file(self): dialog = widgets.FileDialog(parent=self, path_id="file_path") dialog.set_accept_mode(self.mode) if self.extensions: dialog.setNameFilter(self.extensions) if not dialog.open_file(): return None self.set_path(dialog.selected_file()) self.value_changed.emit(self.path) def set_path(self, path): self.path = path self.lineedit.setText(str(path)) def get_value(self) -> Optional[pathlib.Path]: return self.path def set_value(self, value): self.set_path(value)
class ElidedLabel(widgets.Frame): elision_changed = core.Signal(bool) def __init__( self, text: str = "", parent: QtWidgets.QWidget | None = None, ): super().__init__(parent=parent) self.elided = False self.content = text self.set_size_policy("expanding", "preferred") def __repr__(self): return f"{type(self).__name__}({self.text()!r})" def set_text(self, text: str): self.content = text self.update() def paintEvent(self, event): super().paintEvent(event) painter = gui.Painter(self) metrics = painter.get_font_metrics() did_elide = False line_spacing = metrics.lineSpacing() y = 0 layout = gui.TextLayout(self.content, painter.font()) with layout.process_layout(): while True: line = layout.createLine() if not line.isValid(): break line.setLineWidth(self.width()) next_line_y = y + line_spacing if self.height() >= next_line_y + line_spacing: line.draw(painter, core.Point(0, y)) y = next_line_y else: last_line = self.content[line.textStart():] elided_line = metrics.elided_text(last_line, "right", self.width()) painter.drawText(0, y + metrics.ascent(), elided_line) line = layout.createLine() did_elide = line.isValid() break if did_elide != self.elided: self.elided = did_elide self.elision_changed.emit(did_elide)
class SpinBox(QtWidgets.QSpinBox): value_changed = core.Signal(int) def __init__( self, parent: QtWidgets.QWidget | None = None, min_value: int | None = None, max_value: int | None = None, default_value: int | None = None, ): super().__init__(parent) self.valueChanged.connect(self.value_changed) self.set_range(min_value, max_value) if default_value is not None: self.set_value(default_value) def serialize_fields(self): return dict( range=(self.minimum(), self.maximum()), value=self.value(), prefix=self.prefix(), suffix=self.suffix(), step_type=self.get_step_type(), single_step=self.singleStep(), int_base=self.displayIntegerBase(), ) def __setstate__(self, state): super().__setstate__(state) self.set_range(*state["range"]) self.setValue(state["value"]) self.setSingleStep(state["single_step"]) self.setPrefix(state["prefix"]) self.setSuffix(state["suffix"]) self.setDisplayIntegerBase(state["int_base"]) self.set_step_type(state["step_type"]) def __reduce__(self): return type(self), (), self.__getstate__() def set_range(self, start: int | None, end: int | None): if start is None: start = -2147483647 if end is None: end = 2147483647 self.setRange(start, end) def set_step_size(self, step_size): self.setSingleStep(step_size)
def __new__(cls, name, bases, attrs): for key in list(attrs.keys()): attr = attrs[key] if not isinstance(attr, SyncedProperty): continue initial_value = attr.initial_value type_ = type(initial_value) notifier = core.Signal(type_) attrs[key] = PropertyImpl(initial_value, name=key, type_=type_, notify=notifier) attrs[signal_attribute_name(key)] = notifier return super().__new__(cls, name, bases, attrs)
class DoubleSpinBox(QtWidgets.QDoubleSpinBox): value_changed = core.Signal(float) def __init__(self, parent=None, min_value=None, max_value=None, default_value=None): super().__init__(parent) self.valueChanged.connect(self.value_changed) self.set_range(min_value, max_value) if default_value is not None: self.set_value(default_value) def __getstate__(self): return dict(range=(self.minimum(), self.maximum()), value=super().value(), enabled=self.isEnabled(), tooltip=self.toolTip(), statustip=self.statusTip(), step_type=self.get_step_type(), prefix=self.prefix(), correction_mode=self.get_correction_mode(), button_symbols=self.get_button_symbols(), decimals=self.decimals(), suffix=self.suffix(), single_step=self.singleStep()) def __setstate__(self, state): self.__init__() self.set_range(*state["range"]) self.setValue(state["value"]) self.setEnabled(state.get("enabled", True)) self.setToolTip(state.get("tooltip", "")) self.setStatusTip(state.get("statustip", "")) self.setPrefix(state["prefix"]) self.setSuffix(state["suffix"]) self.setDecimals(state["decimals"]) self.setSingleStep(state["single_step"]) self.set_step_type(state["step_type"]) self.set_correction_mode(state["correction_mode"]) self.set_button_symbols(state["button_symbols"]) def set_range(self, start, end): if start is None: start = -float("inf") if end is None: end = float("inf") self.setRange(start, end)
class StringOrNumberWidget(widgets.GroupBox): value_changed = core.Signal(object) def __init__(self, title: str = "", parent: QtWidgets.QWidget | None = None): super().__init__(checkable=False, title=title) self.set_layout("vertical") self.rb_lineedit = widgets.RadioButton("String") self.lineedit = widgets.LineEdit() self.rb_spinbox = widgets.RadioButton("Number") self.spinbox = widgets.DoubleSpinBox() layout_lineedit = widgets.BoxLayout("horizontal") layout_lineedit.add(self.rb_lineedit) layout_lineedit.add(self.lineedit) layout_spinbox = widgets.BoxLayout("horizontal") layout_spinbox.add(self.rb_spinbox) layout_spinbox.add(self.spinbox) self.box.add(layout_lineedit) self.box.add(layout_spinbox) self.rb_spinbox.toggled.connect(self.spinbox.setEnabled) self.rb_spinbox.toggled.connect(self.lineedit.setDisabled) self.rb_lineedit.toggled.connect(self.lineedit.setEnabled) self.rb_lineedit.toggled.connect(self.spinbox.setDisabled) self.spinbox.value_changed.connect(self.on_value_change) self.lineedit.value_changed.connect(self.on_value_change) self.rb_lineedit.setChecked(True) def on_value_change(self): value = self.get_value() self.value_changed.emit(value) def get_value(self) -> float | str: if self.rb_spinbox.isChecked(): val = self.spinbox.get_value() return int(val) if val.is_integer() else val else: return self.lineedit.get_value() def set_value(self, value: float | str): if isinstance(value, str): self.rb_lineedit.setChecked(True) self.lineedit.set_value(value) elif isinstance(value, (int, float)): self.rb_spinbox.setChecked(True) self.spinbox.set_value(value) else: raise TypeError(f"Invalid Type for set_value: {type(value)}")
class InputAndSlider(widgets.Widget): value_changed = core.Signal(int) def __init__( self, bounds: tuple[int, int] | None = None, parent: QtWidgets.QWidget | None = None, ): super().__init__(parent) self.path = None layout = widgets.BoxLayout("horizontal", self) layout.set_margin(0) self.spinbox = widgets.SpinBox() layout.add(self.spinbox) self.slider = widgets.Slider() layout.add(self.slider) if bounds: self.set_range(*bounds) self.spinbox.valueChanged.connect(self.slider.set_value) self.slider.valueChanged.connect(self.spinbox.set_value) self.spinbox.valueChanged.connect(self.value_changed) def serialize_fields(self): return dict(path=self.path) # def __setstate__(self, state): # self.__init__(state["extensions"]) # self.set_path(state["path"]) # self.set_enabled(state.get("enabled", True)) def set_range(self, min_val: int, max_val: int): self.spinbox.set_range(min_val, max_val) self.slider.set_range(min_val, max_val) def get_value(self) -> int: return self.spinbox.get_value() def set_value(self, value: int): self.spinbox.set_value(value) self.slider.set_value(value) def is_valid(self) -> bool: return self.spinbox.is_valid() def set_step_size(self, step_size: int): self.spinbox.set_step_size(step_size) self.slider.set_step_size(step_size) self.slider.setTickInterval(step_size)
class ScrollBar(QtWidgets.QScrollBar): value_changed = core.Signal(int) def __init__( self, orientation: (QtCore.Qt.Orientation | constants.OrientationStr) = "horizontal", parent: QtWidgets.QWidget | None = None, ): if isinstance(orientation, QtCore.Qt.Orientation): ori = orientation else: ori = constants.ORIENTATION[orientation] super().__init__(ori, parent) self.valueChanged.connect(self.on_value_change)
class PushButton(QtWidgets.QPushButton): value_changed = core.Signal(bool) def __init__( self, label: str | None = None, parent: QtWidgets.QWidget | None = None, callback: Callable | None = None, ): if label is None: label = "" super().__init__(label, parent) if callback: self.clicked.connect(callback) self.toggled.connect(self.value_changed)
class DoubleSpinBox(QtWidgets.QDoubleSpinBox): value_changed = core.Signal(float) def __init__( self, parent: QtWidgets.QWidget | None = None, min_value: float | None = None, max_value: float | None = None, default_value: float | None = None, ): super().__init__(parent) self.valueChanged.connect(self.value_changed) self.set_range(min_value, max_value) if default_value is not None: self.set_value(default_value) def __setstate__(self, state): super().__setstate__(state) self.set_range(*state["range"]) self.setValue(state["value"]) self.setPrefix(state["prefix"]) self.setSuffix(state["suffix"]) self.setDecimals(state["decimals"]) self.setSingleStep(state["single_step"]) self.set_step_type(state["step_type"]) def __reduce__(self): return type(self), (), self.__getstate__() def serialize_fields(self): return dict( range=(self.minimum(), self.maximum()), prefix=self.prefix(), suffix=self.suffix(), step_type=self.get_step_type(), single_step=self.singleStep(), value=self.value(), decimals=self.decimals(), ) def set_range(self, start: float | None, end: float | None): if start is None: start = -float("inf") if end is None: end = float("inf") self.setRange(start, end)
class FontChooserButton(widgets.Widget): value_changed = core.Signal(gui.Font) def __init__(self, font=None, parent=None): super().__init__(parent) self.current_font = font layout = widgets.BoxLayout("horizontal", self) layout.set_margin(0) self.lineedit = widgets.LineEdit() self.lineedit.set_read_only() layout += self.lineedit action = widgets.Action() action.triggered.connect(self.choose_font) self.button = widgets.ToolButton() self.button.setDefaultAction(action) layout += self.button def __repr__(self): return f"FontChooserButton({self.current_font})" def __getstate__(self): return dict(font=self.current_font, enabled=self.isEnabled()) def __setstate__(self, state): self.__init__() if state["font"]: self.set_font(state["font"]) self.setEnabled(state.get("enabled", True)) @core.Slot() def choose_font(self): dlg = widgets.FontDialog() if self.current_font: dlg.setCurrentFont(self.current_font) if dlg.exec_(): self.set_font(dlg.current_font()) self.value_changed.emit(dlg.current_font()) def set_font(self, font): if isinstance(font, str): self.current_font = gui.Font(font) else: self.current_font = font self.lineedit.setText(self.current_font.family())
class StarEditor(widgets.Widget): """The custom editor for editing StarRatings.""" # A signal to tell the delegate when we've finished editing. editing_finished = core.Signal() def __init__(self, parent: QtWidgets.QWidget | None = None): """Initialize the editor object, making sure we can watch mouse events.""" super().__init__(parent) self.setMouseTracking(True) self.setAutoFillBackground(True) self.star_rating = StarRating() def sizeHint(self): """Tell the caller how big we are.""" return self.star_rating.sizeHint() def paintEvent(self, event): """Paint the editor, offloading the work to the StarRating class.""" painter = gui.Painter(self) self.star_rating.paint(painter, self.rect(), self.palette(), is_editable=True) def mouseMoveEvent(self, event): """Update stars on mouse move.""" star = self.star_at_position(event.x()) if star != -1: self.star_rating.star_count = star self.update() def mouseReleaseEvent(self, event): """Once star rating was clicked, tell the delegate we're done editing.""" self.editing_finished.emit() def star_at_position(self, x: int) -> int: """Calculate which star the user's mouse cursor is currently hovering over.""" val = x // (self.star_rating.sizeHint().width() // self.star_rating.max_stars) + 1 if not 0 < val <= self.star_rating.max_stars: return -1 return val def set_star_rating(self, rating: int): self.star_rating.star_count = rating
class KeySequenceEdit(QtWidgets.QKeySequenceEdit): value_changed = core.Signal(QtGui.QKeySequence) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.keySequenceChanged.connect(self.value_changed) def __repr__(self): return f"KeySequenceEdit({self.get_value()})" def set_value(self, value: str): seq = gui.KeySequence.fromString(value) self.setKeySequence(seq) def get_value(self) -> str: return self.keySequence().toString() def is_valid(self) -> bool: return True
class BoolDictToolButton(widgets.ToolButton): value_changed = core.Signal(dict) def __init__( self, title: str, icon: types.IconType = None, dct: dict[str, str] = None, parent: QtWidgets.QWidget | None = None, ): super().__init__(parent=parent) self.set_text(title) self.set_icon(icon) self.button_menu = widgets.Menu() self.setMenu(self.button_menu) self.set_popup_mode("instant") if dct: self.set_dict(dct) def __getitem__(self, key: str) -> bool: # type: ignore return self.button_menu[key].isChecked() def __setitem__(self, key: str, value: bool): self.button_menu[key].setChecked(value) self.value_changed.emit(self.as_dict()) def set_dict(self, dct: dict[str, str]): self.button_menu.clear() for k, v in dct.items(): action = widgets.Action() action.set_text(v) action.setCheckable(True) action.set_id(k) action.triggered.connect( lambda: self.value_changed.emit(self.as_dict())) self.button_menu.add(action) self.value_changed.emit(self.as_dict()) def as_dict(self) -> dict[str, bool]: return {act.get_id(): act.isChecked() for act in self.button_menu}
class TextEdit(QtWidgets.QTextEdit): value_changed = core.Signal(str) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.textChanged.connect(self.on_value_change) def on_value_change(self): self.value_changed.emit(self.text()) def __getstate__(self): return dict(text=self.text(), enabled=self.isEnabled(), font=gui.Font(self.font())) def __setstate__(self, state): self.__init__() self.set_text(state["text"]) self.setEnabled(state.get("enabled", True)) self.setFont(state["font"]) def __add__(self, other): if isinstance(other, str): self.append_text(other) return self def set_text(self, text: str): self.setPlainText(text) def append_text(self, text: str): self.append(text) def text(self) -> str: return self.toPlainText() def set_read_only(self, value: bool = True): self.setReadOnly(value)