# -*- coding: utf-8 -*- from functools import partial from .pyside import QtGui, QtCore, QtWidgets from .colorwheel import ColorDialog from .dialog import get_image_path from .geometry import grow_rect from .path import format_path from .qtutils import icon from .stack import count_panels # don't use style sheet like that, find better design TOGGLER_STYLESHEET = ( 'background: rgb(0, 0, 0, 75); text-align: left; font: bold') X = '✘' V = '✔' class BoolCombo(QtWidgets.QComboBox): valueSet = QtCore.Signal(bool) def __init__(self, state=True, parent=None): super(BoolCombo, self).__init__(parent) self.addItem('True') self.addItem('False') self.setCurrentText(str(state)) self.currentIndexChanged.connect(self.current_index_changed) def state(self): return self.currentText() == 'True' def current_index_changed(self): self.valueSet.emit(self.state()) class BrowseEdit(QtWidgets.QWidget): valueSet = QtCore.Signal(str) def __init__(self, parent=None): super(BrowseEdit, self).__init__(parent) self.text = QtWidgets.QLineEdit() self.text.returnPressed.connect(self.apply) self.text.focusOutEvent = self.text_focus_out_event self.button = QtWidgets.QToolButton(self) self.button.setIcon(icon('mini-open.png')) self.button.setFixedSize(21, 21) self.button.released.connect(self.browse) self.layout = QtWidgets.QHBoxLayout(self) self.layout.setContentsMargins(0, 0, 0, 0) self.layout.setSpacing(0) self.layout.addWidget(self.text) self.layout.addWidget(self.button) self._value = self.value() def text_focus_out_event(self, _): self.apply() def browse(self): filename = get_image_path(self) or '' format_path(filename) if not filename: return self.text.setText(filename) self.apply() def apply(self): text = format_path(self.text.text()) self.text.setText(text) self.valueSet.emit(text) def value(self): value = format(self.text.text()) return value if value != '' else None def set_value(self, value): self.text.setText(value) class WidgetToggler(QtWidgets.QPushButton): def __init__(self, label, widget, parent=None): super(WidgetToggler, self).__init__(parent) self.setStyleSheet(TOGGLER_STYLESHEET) self.setText(' v ' + label) self.widget = widget self.setCheckable(True) self.setChecked(True) self.toggled.connect(self._call_toggled) def _call_toggled(self, state): if state is True: self.widget.show() self.setText(self.text().replace('>', 'v')) else: self.widget.hide() self.setText(self.text().replace('v', '>')) class ColorButton(QtWidgets.QAbstractButton): def __init__(self, parent=None): super(ColorButton, self).__init__(parent) self.color = 'grey' self.setFixedSize(21, 21) self.hover = False def enterEvent(self, event): self.hover = True self.update() def leaveEvent(self, event): self.hover = False self.update() def paintEvent(self, event): painter = QtGui.QPainter(self) painter.setBrush(QtGui.QColor(self.color)) painter.setPen(QtCore.Qt.black) rect = self.rect() rect.setWidth(rect.width() - 1) rect.setHeight(rect.height() - 1) painter.drawRect(rect) if not self.hover: painter.end() return rect = grow_rect(rect, -3).toRect() pixmap = icon('picker.png').pixmap(rect.size()) painter.drawPixmap(rect, pixmap) painter.end() class ColorEdit(QtWidgets.QWidget): valueSet = QtCore.Signal(str) def __init__(self, parent=None): super(ColorEdit, self).__init__(parent) self.color = ColorButton() self.color.released.connect(self.pick_color) self.text = QtWidgets.QLineEdit() self.text.returnPressed.connect(self.apply) self.text.focusInEvent = self.focusInEvent self.text.focusOutEvent = self.focusOutEvent self.layout = QtWidgets.QHBoxLayout(self) self.layout.setContentsMargins(0, 0, 0, 0) self.layout.setSpacing(0) self.layout.addWidget(self.color) self.layout.addWidget(self.text) self.layout.setStretchFactor(self.color, 1) self.layout.setStretchFactor(self.text, 5) self._value = self.value() def focusInEvent(self, event): self._value = self.value() return super(ColorEdit, self).focusInEvent(event) def focusOutEvent(self, event): self.apply() return super(ColorEdit, self).focusOutEvent(event) def pick_color(self): color = self.text.text() or None dialog = ColorDialog(color) if dialog.exec_(): self.text.setText(dialog.colorname()) self.color.color = dialog.colorname() self.color.update() self.apply() def apply(self): self.color.color = self.value() self.color.update() if self._value != self.value(): self.valueSet.emit(self.value()) self._value = self.value() def value(self): value = self.text.text() return value if value != '' else None def set_color(self, color=None): self.color.color = color or 'grey' self.text.setText(color) self.color.update() class LineEdit(QtWidgets.QLineEdit): valueSet = QtCore.Signal(float) VALIDATOR_CLS = QtGui.QDoubleValidator def __init__(self, minimum=None, maximum=None, parent=None): super(LineEdit, self).__init__(parent) self.validator = self.VALIDATOR_CLS() if self.VALIDATOR_CLS else None if minimum is not None: self.validator.setBottom(minimum) if maximum is not None: self.validator.setTop(maximum) self.setValidator(self.validator) self._value = self.value() self.returnPressed.connect(self.apply) def focusInEvent(self, event): self._value = self.value() return super(LineEdit, self).focusInEvent(event) def focusOutEvent(self, event): self.apply() return super(LineEdit, self).focusOutEvent(event) def apply(self): if self._value != self.value(): self.valueSet.emit(self.value()) self._value = self.value() def value(self): if self.text() == '': return None return float(self.text().replace(',', '.')) class TextEdit(LineEdit): VALIDATOR_CLS = None valueSet = QtCore.Signal(str) def value(self): if self.text() == '': return None return self.text() class NumEdit(LineEdit): valueSet = QtCore.Signal(float) VALIDATOR_CLS = QtGui.QDoubleValidator def __init__(self, minimum=None, maximum=None, parent=None): super(NumEdit, self).__init__(parent) self.dragging = False self.init_mouse_pos = QtCore.QPoint() self.last_mouse_pos = QtCore.QPoint() self.minimum = minimum self.maximum = maximum # Minimum horizontal pixels to move before adjusting value self.drag_threshold = 15 self.setMouseTracking(True) def mousePressEvent(self, event): if event.button() != QtCore.Qt.MiddleButton: return super(NumEdit, self).mousePressEvent(event) self.setStyleSheet("background-color: #5285A6;") self.clearFocus() self.dragging = True self.init_mouse_pos = event.globalPos() self.last_mouse_pos = event.globalPos() event.accept() def mouseMoveEvent(self, event): if not self.dragging: return super(NumEdit, self).mouseMoveEvent(event) delta = event.globalPos() - self.last_mouse_pos self.last_mouse_pos = event.globalPos() delta_threshold = abs( self.init_mouse_pos.x() - self.last_mouse_pos.x()) if self.drag_threshold: if delta_threshold < self.drag_threshold: return self.drag_threshold = False self.setStyleSheet("") current_value = float(self.text()) if self.text() else 0.0 is_integer = self.VALIDATOR_CLS == QtGui.QIntValidator step = 1 if is_integer else 0.1 adjustment = delta.x() * step new_value = current_value + adjustment if self.validator: min_val, max_val = self.validator.bottom(), self.validator.top() new_value = max(min_val, min(new_value, max_val)) if is_integer: self.setText(str(int(new_value))) else: self.setText("{0:.2f}".format(new_value)) def mouseReleaseEvent(self, event): self.drag_threshold = 15 self.setStyleSheet("") if event.button() != QtCore.Qt.MiddleButton: return super(NumEdit, self).mouseReleaseEvent(event) self.dragging = False event.accept() self.emit_value() self.clearFocus() def emit_value(self): current_value = self.value() if current_value is not None: self.valueSet.emit(current_value) def value(self): if self.text() == '': return None return float(self.text().replace(',', '.')) def enterEvent(self, event): if not self.hasFocus(): QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.SplitHCursor) super(NumEdit, self).enterEvent(event) def leaveEvent(self, event): QtWidgets.QApplication.restoreOverrideCursor() super(NumEdit, self).leaveEvent(event) def focusInEvent(self, event): QtWidgets.QApplication.restoreOverrideCursor() super(NumEdit, self).focusInEvent(event) def focusOutEvent(self, event): if self.underMouse(): QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.SplitHCursor) super(NumEdit, self).focusOutEvent(event) class FloatEdit(NumEdit): valueSet = QtCore.Signal(float) VALIDATOR_CLS = QtGui.QDoubleValidator def __init__(self, maximum=None, minimum=None, decimals=2, parent=None): super(FloatEdit, self).__init__(maximum, minimum, parent) if minimum is None and maximum is None: self.validator = None else: # using sys.maxsize creates an overflow error, float('inf') is not # sipported by python 2 maximum = 999999999. if maximum is None else maximum self.validator = self.VALIDATOR_CLS( minimum, maximum, decimals, self) self.setValidator(self.validator) class IntEdit(NumEdit): valueSet = QtCore.Signal(int) VALIDATOR_CLS = QtGui.QIntValidator def __init__(self, maximum=None, minimum=None, parent=None): super(IntEdit, self).__init__(maximum, minimum, parent) if minimum is None and maximum is None: self.validator = None else: # using sys.maxsize creates an overflow error. maximum = 999999999 if maximum is None else maximum self.validator = self.VALIDATOR_CLS(minimum, maximum, self) self.setValidator(self.validator) def value(self): if self.text() == '': return None return int(float(self.text())) class Title(QtWidgets.QLabel): def __init__(self, title, parent=None): super(Title, self).__init__(parent) self.setFixedHeight(20) self.setStyleSheet('background: rgb(0, 0, 0, 25)') self.setText('   ' + title) class TouchEdit(QtWidgets.QLineEdit): def keyPressEvent(self, event): self.setText(QtGui.QKeySequence(event.key()).toString().lower()) self.textEdited.emit(self.text()) class CommandButton(QtWidgets.QWidget): released = QtCore.Signal() playReleased = QtCore.Signal() def __init__(self, label, parent=None): super(CommandButton, self).__init__(parent) self.mainbutton = QtWidgets.QPushButton(label) self.mainbutton.released.connect(self.released.emit) self.playbutton = QtWidgets.QPushButton(icon('play.png'), '') self.playbutton.released.connect(self.playReleased.emit) self.playbutton.setFixedSize(22, 22) self.layout = QtWidgets.QHBoxLayout(self) self.layout.setContentsMargins(0, 0, 0, 0) self.layout.setSpacing(2) self.layout.addWidget(self.mainbutton) self.layout.addWidget(self.playbutton) class LayerEdit(QtWidgets.QWidget): valueSet = QtCore.Signal(object) def __init__(self, parent=None): super(LayerEdit, self).__init__(parent) self.layer = QtWidgets.QLineEdit() self.layer.setReadOnly(True) self.reset = QtWidgets.QPushButton('x') self.reset.released.connect(self.do_reset) self.layout = QtWidgets.QHBoxLayout(self) self.layout.setContentsMargins(0, 0, 0, 0) self.layout.setSpacing(0) self.layout.addWidget(self.layer) self.layout.addWidget(self.reset) def set_layer(self, text): self.layer.setText(text or '') def do_reset(self): if not self.layer.text(): return self.layer.setText('') self.valueSet.emit(None) class ZoomsLockedEditor(QtWidgets.QWidget): def __init__(self, document, parent=None): super(ZoomsLockedEditor, self).__init__(parent) self.model = ZoomLockedModel(document) self.table = QtWidgets.QTableView() self.table.setEditTriggers(QtWidgets.QAbstractItemView.AllEditTriggers) self.table.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) mode = QtWidgets.QHeaderView.ResizeToContents self.table.horizontalHeader().setSectionResizeMode(mode) self.table.setFixedHeight(120) self.table.setItemDelegateForColumn(0, CheckDelegate()) self.table.setModel(self.model) layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.table) class ZoomLockedModel(QtCore.QAbstractTableModel): HEADERS = 'Z-lock', 'BG Color', 'Name' def __init__(self, document, parent=None): super(ZoomLockedModel, self).__init__(parent) self.document = document self.document.changed.connect(self.layoutChanged.emit) def columnCount(self, _): return 3 def rowCount(self, _): return count_panels(self.document.data['general']['panels']) def flags(self, _): return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable def headerData(self, section, orientation, role): if role != QtCore.Qt.DisplayRole: return if orientation == QtCore.Qt.Vertical: return str(section + 1) return self.HEADERS[section] def set_zoom_locked(self, row, state): self.layoutAboutToBeChanged.emit() self.document.data['general']['panels.zoom_locked'][row] = state self.document.general_option_changed.emit( 'attribute_editor', 'panels.zoom_locked') self.document.record_undo() self.layoutChanged.emit() def setData(self, index, value, role): if index.column() == 0: return False if index.column() == 1 and role == QtCore.Qt.EditRole: if value and not QtGui.QColor(value).isValid(): if QtGui.QColor('#' + value).isValid(): value = '#' + value else: return False value = value or None self.document.data['general']['panels.colors'][index.row()] = value self.document.general_option_changed.emit( 'attribute_editor', 'panels.colors') self.document.record_undo() return True if index.column() == 2 and role == QtCore.Qt.EditRole: self.document.data['general']['panels.names'][index.row()] = value self.document.general_option_changed.emit( 'attribute_editor', 'panels.names') self.document.record_undo() return True return False def data(self, index, role): if not index.isValid(): return general = self.document.data['general'] if role == QtCore.Qt.DecorationRole: if index.column() == 1: color = general['panels.colors'][index.row()] return get_color_icon(color) if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole): if index.column() == 1: color = general['panels.colors'][index.row()] return str(color) if color else '' if index.column() == 2: return str(general['panels.names'][index.row()]) # @lru_cache() def get_color_icon(color, size=None): px = QtGui.QPixmap(QtCore.QSize(*(size if size else (64, 64)))) px.fill(QtCore.Qt.transparent) rect = QtCore.QRectF(0, 0, px.size().width(), px.size().height()) painter = QtGui.QPainter(px) try: if not color: painter.drawRect(rect) font = QtGui.QFont() font.setPixelSize(50) option = QtGui.QTextOption() option.setAlignment(QtCore.Qt.AlignCenter) painter.setPen(QtGui.QPen(QtGui.QColor('#ddd'))) painter.setFont(font) painter.drawText(grow_rect(rect, 300), X, option) painter.end() return QtGui.QIcon(px) painter.setPen(QtCore.Qt.NoPen) painter.setBrush(QtGui.QColor(color)) painter.drawRect(rect) painter.end() return QtGui.QIcon(px) except BaseException: import traceback print(traceback.format_exc()) painter.end() return QtGui.QIcon() class CheckDelegate(QtWidgets.QItemDelegate): def mouseReleaseEvent(self, event): if event.button() != QtCore.Qt.LeftButton: return self.update() def createEditor(self, parent, _, index): model = index.model() general = model.document.data['general'] state = general['panels.zoom_locked'][index.row()] model.set_zoom_locked(index.row(), not state) checker = CheckWidget(not state, parent) checker.toggled.connect(partial(model.set_zoom_locked, index.row())) return checker def paint(self, painter, option, index): model = index.model() general = model.document.data['general'] state = general['panels.zoom_locked'][index.row()] center = option.rect.center() painter.setBrush(QtCore.Qt.NoBrush) rect = QtCore.QRectF(center.x() - 10, center.y() - 10, 20, 20) if not state: return font = QtGui.QFont() font.setPixelSize(20) option = QtGui.QTextOption() option.setAlignment(QtCore.Qt.AlignCenter) painter.drawText(rect, V, option) class CheckWidget(QtWidgets.QWidget): toggled = QtCore.Signal(bool) def __init__(self, state, parent=None): super(CheckWidget, self).__init__(parent) self.state = state def mouseReleaseEvent(self, event): if event.button() == QtCore.Qt.LeftButton: self.state = not self.state self.toggled.emit(self.state) self.update() def paintEvent(self, _): painter = QtGui.QPainter(self) center = self.rect().center() painter.setBrush(QtCore.Qt.NoBrush) rect = QtCore.QRectF(center.x() - 15, center.y() - 15, 30, 30) painter.drawRect(rect) if not self.state: return font = QtGui.QFont() font.setPixelSize(20) option = QtGui.QTextOption() option.setAlignment(QtCore.Qt.AlignCenter) painter.drawText(rect, V, option) class ChildrenWidget(QtWidgets.QWidget): children_changed = QtCore.Signal(list) def __init__(self, document, display_options, parent=None): super(ChildrenWidget, self).__init__(parent) self.display_options = display_options self.model = ChildrenModel(document) self.list = QtWidgets.QListView() self.list.setModel(self.model) self.list.selectionModel().selectionChanged.connect( self.hightlight_children) mode = QtWidgets.QAbstractItemView.ExtendedSelection self.list.setSelectionMode(mode) self.delete = QtWidgets.QPushButton('Delete') self.delete.released.connect(self.call_delete) layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.list) layout.addWidget(self.delete) def hightlight_children(self, *_): indexes = self.list.selectedIndexes() ids = [self.model.data(i, QtCore.Qt.DisplayRole) for i in indexes] self.display_options.highlighted_children_ids = ids self.display_options.options_changed.emit() def clear(self): self.model.layoutAboutToBeChanged.emit() self.display_options.highlighted_children_ids = [] self.list.selectionModel().clear() self.model.children = [] self.model.layoutChanged.emit() def call_delete(self): indexes = self.list.selectedIndexes() indexes = sorted(indexes, key=lambda i: i.row(), reverse=True) self.model.layoutAboutToBeChanged.emit() for index in indexes: self.model.children.pop(index.row()) self.model.layoutChanged.emit() self.display_options.highlighted_children_ids = [] self.display_options.options_changed.emit() self.children_changed.emit(self.model.children[:]) def set_children(self, children): self.model.layoutAboutToBeChanged.emit() self.model.children = children[:] self.model.layoutChanged.emit() class ChildrenModel(QtCore.QAbstractListModel): def __init__(self, document, parent=None): super(ChildrenModel, self).__init__(parent) self.document = document self.children = [] def rowCount(self, _): return len(self.children) def data(self, index, role): id_ = self.children[index.row()] if role == QtCore.Qt.DisplayRole: return id_ if id_ in self.document.shapes_by_id: return if role == QtCore.Qt.BackgroundRole: brush = QtGui.QBrush(QtGui.QColor('#555555')) brush.setStyle(QtCore.Qt.BDiagPattern) return brush if role == QtCore.Qt.FontRole: font = QtGui.QFont() font.setStrikeOut(True) font.setItalic(True) return font if role == QtCore.Qt.TextColorRole: return QtGui.QColor('#999999')