class MClickBrowserFolderToolButton(MToolButton): """A Clickable tool button to browser folders""" sig_folder_changed = Signal(str) sig_folders_changed = Signal(list) slot_browser_folder = _slot_browser_folder def __init__(self, multiple=False, parent=None): super(MClickBrowserFolderToolButton, self).__init__(parent=parent) self.set_dayu_svg('folder_line.svg') self.icon_only() self.clicked.connect(self.slot_browser_folder) self.setToolTip(self.tr('Click to browser folder')) self._path = None self._multiple = multiple def get_dayu_path(self): """ Get last browser file path :return: str """ return self._path def set_dayu_path(self, value): """ Set browser file start path :param value: str :return: None """ self._path = value def get_dayu_multiple(self): """ Get browser can select multiple file or not :return: bool """ return self._multiple def set_dayu_multiple(self, value): """ Set browser can select multiple file or not :param value: bool :return: None """ self._multiple = value dayu_multiple = Property(bool, get_dayu_multiple, set_dayu_multiple) dayu_path = Property(six.string_types[0], get_dayu_path, set_dayu_path)
class MClickBrowserFolderPushButton(MPushButton): """A Clickable push button to browser folders""" sig_folder_changed = Signal(str) sig_folders_changed = Signal(list) slot_browser_folder = _slot_browser_folder def __init__(self, text='', multiple=False, parent=None): super(MClickBrowserFolderPushButton, self).__init__(text=text, parent=parent) self.setProperty('multiple', multiple) self.clicked.connect(self.slot_browser_folder) self.setToolTip(self.tr('Click to browser folder')) self._path = None self._multiple = multiple def get_dayu_path(self): """ Get last browser file path :return: str """ return self._path def set_dayu_path(self, value): """ Set browser file start path :param value: str :return: None """ self._path = value def get_dayu_multiple(self): """ Get browser can select multiple file or not :return: bool """ return self._multiple def set_dayu_multiple(self, value): """ Set browser can select multiple file or not :param value: bool :return: None """ self._multiple = value dayu_multiple = Property(bool, get_dayu_multiple, set_dayu_multiple) dayu_path = Property(basestring, get_dayu_path, set_dayu_path)
class MTreeView(QTreeView): set_header_list = set_header_list enable_context_menu = enable_context_menu slot_context_menu = slot_context_menu sig_context_menu = Signal(object) def __init__(self, parent=None): super(MTreeView, self).__init__(parent) self._no_data_image = None self._no_data_text = self.tr('No Data') self.header_list = [] self.header_view = MHeaderView(Qt.Horizontal) self.setHeader(self.header_view) self.setSortingEnabled(True) self.setAlternatingRowColors(True) def paintEvent(self, event): """Override paintEvent when there is no data to show, draw the preset picture and text.""" model = utils.real_model(self.model()) if model is None: draw_empty_content(self.viewport(), self._no_data_text, self._no_date_image) elif isinstance(model, MTableModel): if not model.get_data_list(): draw_empty_content(self.viewport(), self._no_data_text, self._no_date_image) return super(MTreeView, self).paintEvent(event) def set_no_data_text(self, text): self._no_data_text = text
class MRadioButtonGroup(MButtonGroupBase): """ Property: dayu_checked """ sig_checked_changed = Signal(int) def __init__(self, orientation=Qt.Horizontal, parent=None): super(MRadioButtonGroup, self).__init__(orientation=orientation, parent=parent) self.set_spacing(15) self._button_group.setExclusive(True) self._button_group.buttonClicked[int].connect(self.sig_checked_changed) def create_button(self, data_dict): return MRadioButton() def set_dayu_checked(self, value): if value == self.get_dayu_checked(): return button = self._button_group.button(value) if button: button.setChecked(True) self.sig_checked_changed.emit(value) else: print('error') def get_dayu_checked(self): return self._button_group.checkedId() dayu_checked = Property(int, get_dayu_checked, set_dayu_checked, notify=sig_checked_changed)
class MClickSaveFileToolButton(MToolButton): """A Clickable tool button to browser files""" sig_file_changed = Signal(str) slot_browser_file = _slot_save_file def __init__(self, multiple=False, parent=None): super(MClickSaveFileToolButton, self).__init__(parent=parent) self.set_dayu_svg('save_line.svg') self.icon_only() self.clicked.connect(self.slot_browser_file) self.setToolTip(self.tr('Click to save file')) self._path = None self._multiple = multiple self._filters = [] def get_dayu_filters(self): """ Get browser's format filters :return: list """ return self._filters def set_dayu_filters(self, value): """ Set browser file format filters :param value: :return: None """ self._filters = value def get_dayu_path(self): """ Get last browser file path :return: str """ return self._path def set_dayu_path(self, value): """ Set browser file start path :param value: str :return: None """ self._path = value dayu_path = Property(six.string_types[0], get_dayu_path, set_dayu_path) dayu_filters = Property(list, get_dayu_filters, set_dayu_filters)
class MToolButtonGroup(MButtonGroupBase): sig_checked_changed = Signal(int) def __init__(self, size=None, type=None, exclusive=False, orientation=Qt.Horizontal, parent=None): super(MToolButtonGroup, self).__init__(orientation=orientation, parent=parent) self.set_spacing(1) self._button_group.setExclusive(exclusive) self._size = size self._type = type self._button_group.buttonClicked[int].connect(self.sig_checked_changed) def create_button(self, data_dict): button = MToolButton() if data_dict.get('svg'): button.svg(data_dict.get('svg')) if data_dict.get('text'): if data_dict.get('svg') or data_dict.get('icon'): button.text_beside_icon() else: button.text_only() else: button.icon_only() return button def set_dayu_checked(self, value): if value == self.get_dayu_checked(): return button = self._button_group.button(value) if button: button.setChecked(True) self.sig_checked_changed.emit(value) else: print 'error' def get_dayu_checked(self): return self._button_group.checkedId() dayu_checked = Property(int, get_dayu_checked, set_dayu_checked, notify=sig_checked_changed)
class MBigView(QListView): set_header_list = set_header_list enable_context_menu = enable_context_menu slot_context_menu = slot_context_menu sig_context_menu = Signal(object) def __init__(self, parent=None): super(MBigView, self).__init__(parent) self._no_date_image = None self._no_data_text = self.tr('No Data') self.header_list = [] self.header_view = None self.setViewMode(QListView.IconMode) self.setResizeMode(QListView.Adjust) self.setMovement(QListView.Static) self.setSpacing(10) self.setIconSize(QSize(128, 128)) def wheelEvent(self, event): """Override wheelEvent while user press ctrl, zoom the list view icon size.""" if event.modifiers() == Qt.ControlModifier: num_degrees = event.delta() / 8.0 num_steps = num_degrees / 15.0 factor = pow(1.125, num_steps) new_size = self.iconSize() * factor if new_size.width() > 200: new_size = QSize(200, 200) elif new_size.width() < 24: new_size = QSize(24, 24) self.setIconSize(new_size) else: super(MBigView, self).wheelEvent(event) def paintEvent(self, event): """Override paintEvent when there is no data to show, draw the preset picture and text.""" model = utils.real_model(self.model()) if model is None: draw_empty_content(self.viewport(), self._no_data_text, self._no_date_image) elif isinstance(model, MTableModel): if not model.get_data_list(): draw_empty_content(self.viewport(), self._no_data_text, self._no_date_image) return super(MBigView, self).paintEvent(event) def set_no_data_text(self, text): self._no_data_text = text
class MListView(QListView): set_header_list = set_header_list enable_context_menu = enable_context_menu slot_context_menu = slot_context_menu sig_context_menu = Signal(object) def __init__(self, size=None, parent=None): super(MListView, self).__init__(parent) self._no_date_image = None self._no_data_text = self.tr('No Data') self.setProperty('dayu_size', size or dayu_theme.default_size) self.header_list = [] self.header_view = None self.setModelColumn(0) self.setAlternatingRowColors(True) def set_show_column(self, attr): for index, attr_dict in enumerate(self.header_list): if attr_dict.get('key') == attr: self.setModelColumn(index) break else: self.setModelColumn(0) def paintEvent(self, event): """Override paintEvent when there is no data to show, draw the preset picture and text.""" model = utils.real_model(self.model()) if model is None: draw_empty_content(self.viewport(), self._no_data_text, self._no_date_image) elif isinstance(model, MTableModel): if not model.get_data_list(): draw_empty_content(self.viewport(), self._no_data_text, self._no_date_image) return super(MListView, self).paintEvent(event) def set_no_data_text(self, text): self._no_data_text = text def minimumSizeHint(self, *args, **kwargs): return QSize(200, 50)
class MNewTag(QWidget): sig_add_tag = Signal(str) def __init__(self, text='New Tag', parent=None): super(MNewTag, self).__init__(parent) self.setAttribute(Qt.WA_StyledBackground) self._add_button = MToolButton().tiny().svg( 'add_line.svg').text_beside_icon() self._add_button.setText(text) self._add_button.clicked.connect(self._slot_show_edit) self._line_edit = MLineEdit().tiny() self._line_edit.returnPressed.connect(self._slot_return_pressed) self._line_edit.setVisible(False) self._main_lay = QGridLayout() self._main_lay.setContentsMargins(3, 3, 3, 3) self._main_lay.addWidget(self._add_button, 0, 0) self._main_lay.addWidget(self._line_edit, 0, 0) self.setLayout(self._main_lay) def set_completer(self, completer): self._line_edit.setCompleter(completer) def _slot_show_edit(self): self._line_edit.setVisible(True) self._add_button.setVisible(False) self._line_edit.setFocus(Qt.MouseFocusReason) def _slot_return_pressed(self): self._line_edit.setVisible(False) self._add_button.setVisible(True) if self._line_edit.text(): self.sig_add_tag.emit(self._line_edit.text()) self._line_edit.clear() def focusOutEvent(self, *args, **kwargs): self._line_edit.setVisible(False) self._add_button.setVisible(True) return super(MNewTag, self).focusOutEvent(*args, **kwargs)
class MBlockButtonGroup(MButtonGroupBase): """MBlockButtonGroup""" sig_checked_changed = Signal(int) def __init__(self, parent=None): super(MBlockButtonGroup, self).__init__(parent=parent) self.set_spacing(1) self._button_group.setExclusive(True) self._button_group.buttonClicked[int].connect(self.sig_checked_changed) def create_button(self, data_dict): button = MBlockButton() if data_dict.get('svg'): button.svg(data_dict.get('svg')) if data_dict.get('text'): if data_dict.get('svg') or data_dict.get('icon'): button.text_beside_icon() else: button.text_only() else: button.icon_only() button.set_dayu_size(dayu_theme.large) return button def set_dayu_checked(self, value): """Set current checked button's id""" button = self._button_group.button(value) button.setChecked(True) self.sig_checked_changed.emit(value) def get_dayu_checked(self): """Get current checked button's id""" return self._button_group.checkedId() dayu_checked = Property(int, get_dayu_checked, set_dayu_checked, notify=sig_checked_changed)
class MPage(QWidget, MFieldMixin): """ MPage A long list can be divided into several pages by MPage, and only one page will be loaded at a time. """ sig_page_changed = Signal(int, int) def __init__(self, parent=None): super(MPage, self).__init__(parent) self.register_field('page_size_selected', 25) self.register_field('page_size_list', [{'label': '25 - Fastest', 'value': 25}, {'label': '50 - Fast', 'value': 50}, {'label': '75 - Medium', 'value': 75}, {'label': '100 - Slow', 'value': 100}]) self.register_field('total', 0) self.register_field('current_page', 0) self.register_field('total_page', lambda: utils.get_total_page(self.field('total'), self.field('page_size_selected'))) self.register_field('total_page_text', lambda: str(self.field('total_page'))) self.register_field('display_text', lambda: utils.get_page_display_string(self.field('current_page'), self.field('page_size_selected'), self.field('total'))) self.register_field('can_pre', lambda: self.field('current_page') > 1) self.register_field('can_next', lambda: self.field('current_page') < self.field('total_page')) page_setting_menu = MMenu(parent=self) self._display_label = MLabel() self._display_label.setAlignment(Qt.AlignCenter) self._change_page_size_button = MComboBox().small() self._change_page_size_button.setFixedWidth(110) self._change_page_size_button.set_menu(page_setting_menu) self._change_page_size_button.set_formatter(lambda x: u'{} per page'.format(x)) self._change_page_size_button.sig_value_changed.connect(self._emit_page_changed) self._pre_button = MToolButton().icon_only().svg('left_fill.svg').small() self._pre_button.clicked.connect(functools.partial(self._slot_change_current_page, -1)) self._next_button = MToolButton().small().icon_only().svg('right_fill.svg') self._next_button.clicked.connect(functools.partial(self._slot_change_current_page, 1)) self._current_page_spin_box = MSpinBox() self._current_page_spin_box.setMinimum(1) self._current_page_spin_box.set_dayu_size(dayu_theme.small) self._current_page_spin_box.valueChanged.connect(self._emit_page_changed) self._total_page_label = MLabel() self.bind('page_size_list', page_setting_menu, 'data') self.bind('page_size_selected', page_setting_menu, 'value', signal='sig_value_changed') self.bind('page_size_selected', self._change_page_size_button, 'value', signal='sig_value_changed') self.bind('current_page', self._current_page_spin_box, 'value', signal='valueChanged') self.bind('total_page', self._current_page_spin_box, 'maximum') self.bind('total_page_text', self._total_page_label, 'dayu_text') self.bind('display_text', self._display_label, 'dayu_text') self.bind('can_pre', self._pre_button, 'enabled') self.bind('can_next', self._next_button, 'enabled') main_lay = QHBoxLayout() main_lay.setContentsMargins(0, 0, 0, 0) main_lay.setSpacing(2) main_lay.addStretch() main_lay.addWidget(self._display_label) main_lay.addStretch() main_lay.addWidget(MLabel('|').secondary()) main_lay.addWidget(self._change_page_size_button) main_lay.addWidget(MLabel('|').secondary()) main_lay.addWidget(self._pre_button) main_lay.addWidget(MLabel('Page')) main_lay.addWidget(self._current_page_spin_box) main_lay.addWidget(MLabel('/')) main_lay.addWidget(self._total_page_label) main_lay.addWidget(self._next_button) self.setLayout(main_lay) def set_total(self, value): """Set page component total count.""" self.set_field('total', value) self.set_field('current_page', 1) def _slot_change_current_page(self, offset): self.set_field('current_page', self.field('current_page') + offset) self._emit_page_changed() def set_page_config(self, data_list): """Set page component per page settings.""" self.set_field('page_size_list', [{'label': str(data), 'value': data} if isinstance(data, int) else data for data in data_list]) def _emit_page_changed(self): self.sig_page_changed.emit(self.field('page_size_selected'), self.field('current_page'))
class MItemViewSet(QWidget): sig_double_clicked = Signal(QModelIndex) sig_left_clicked = Signal(QModelIndex) TableViewType = MTableView BigViewType = MBigView TreeViewType = MTreeView ListViewType = MListView def __init__(self, view_type=None, parent=None): super(MItemViewSet, self).__init__(parent) self._main_lay = QVBoxLayout() self._main_lay.setSpacing(5) self._main_lay.setContentsMargins(0, 0, 0, 0) self.sort_filter_model = MSortFilterModel() self.source_model = MTableModel() self.sort_filter_model.setSourceModel(self.source_model) view_class = view_type or MItemViewSet.TableViewType self.item_view = view_class() self.item_view.doubleClicked.connect(self.sig_double_clicked) self.item_view.pressed.connect(self.slot_left_clicked) self.item_view.setModel(self.sort_filter_model) self._search_line_edit = MLineEdit().search().small() self._search_attr_button = MToolButton().icon_only().svg( 'down_fill.svg').small() self._search_line_edit.set_prefix_widget(self._search_attr_button) self._search_line_edit.textChanged.connect( self.sort_filter_model.set_search_pattern) self._search_line_edit.setVisible(False) _search_lay = QHBoxLayout() _search_lay.setContentsMargins(0, 0, 0, 0) _search_lay.addStretch() _search_lay.addWidget(self._search_line_edit) self._main_lay.addLayout(_search_lay) self._main_lay.addWidget(self.item_view) self.setLayout(self._main_lay) @Slot(QModelIndex) def slot_left_clicked(self, start_index): button = QApplication.mouseButtons() if button == Qt.LeftButton: real_index = self.sort_filter_model.mapToSource(start_index) self.sig_left_clicked.emit(real_index) def set_header_list(self, header_list): self.source_model.set_header_list(header_list) self.sort_filter_model.set_header_list(header_list) self.sort_filter_model.setSourceModel(self.source_model) self.item_view.set_header_list(header_list) @Slot() def setup_data(self, data_list): self.source_model.clear() if data_list: self.source_model.set_data_list(data_list) def get_data(self): return self.source_model.get_data_list() def searchable(self): """Enable search line edit visible.""" self._search_line_edit.setVisible(True) return self
class MTableView(QTableView): sig_context_menu = Signal(object) set_header_list = set_header_list def __init__(self, size=None, show_row_count=False, parent=None): super(MTableView, self).__init__(parent) self._no_data_image = None self._no_data_text = self.tr('No Data') size = size or dayu_theme.default_size ver_header_view = MHeaderView(Qt.Vertical, parent=self) ver_header_view.setDefaultSectionSize(size) self.setVerticalHeader(ver_header_view) self.header_list = [] self.header_view = MHeaderView(Qt.Horizontal, parent=self) self.header_view.setFixedHeight(size) if not show_row_count: ver_header_view.hide() self.setHorizontalHeader(self.header_view) self.setSortingEnabled(True) self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setAlternatingRowColors(True) self.setShowGrid(False) def set_no_data_text(self, text): self._no_data_text = text def set_no_data_image(self, image): self._no_data_image = image def setShowGrid(self, flag): self.header_view.setProperty('grid', flag) self.verticalHeader().setProperty('grid', flag) self.header_view.style().polish(self.header_view) return super(MTableView, self).setShowGrid(flag) # setting = { # 'key': attr, # 必填,用来读取 model后台数据结构的属性 # 'label': attr.title(), # 选填,显示在界面的该列的名字 # 'width': 100, # 选填,单元格默认的宽度 # 'default_filter': False, # 选填,如果有组合的filter组件,该属性默认是否显示,默认False # 'searchable': False, # 选填,如果有搜索组件,该属性是否可以被搜索,默认False # 'editable': False, # 选填,该列是否可以双击编辑,默认False # 'selectable': False, # 选填,该列是否可以双击编辑,且使用下拉列表选择。该下拉框的选项们,是通过 data 拿数据的 # 'checkable': False, # 选填,该单元格是否要加checkbox,默认False # 'exclusive': True, # 配合selectable,如果是可以多选的则为 False,如果是单选,则为True # 'order': None, # 选填,初始化时,该列的排序方式, 0 升序,1 降序 # # 下面的是每个单元格的设置,主要用来根据本单元格数据,动态设置样式 # 'color': None, # QColor选填,该单元格文字的颜色,例如根据百分比数据大小,大于100%显示红色,小于100%显示绿色 # 'bg_color': None, # 选填,该单元格的背景色,例如根据bool数据,True显示绿色,False显示红色 # 'display': None, # 选填,该单元显示的内容,例如数据是以分钟为单位,可以在这里给转换成按小时为单位 # 'align': None, # 选填,该单元格文字的对齐方式 # 'font': None, # 选填,该单元格文字的格式,例如加下划线、加粗等等 # 'icon': None, # 选填,该单格元的图标,注意,当 QListView 使用图标模式时,每个item的图片也是在这里设置 # 'tooltip': None, # 选填,鼠标指向该单元格时,显示的提示信息 # 'size': None, # 选填,该列的 hint size,设置 # 'data': None, # 'edit': None # } def paintEvent(self, event): """Override paintEvent when there is no data to show, draw the preset picture and text.""" model = utils.real_model(self.model()) if model is None: draw_empty_content(self.viewport(), self._no_data_text, self._no_data_image) elif isinstance(model, MTableModel): if not model.get_data_list(): draw_empty_content(self.viewport(), self._no_data_text, self._no_data_image) return super(MTableView, self).paintEvent(event) def save_state(self, name): settings = QSettings(QSettings.IniFormat, QSettings.UserScope, 'DAYU', 'dayu_widgets') settings.setValue('{}/headerState'.format( name, self.header_view.saveState())) def load_state(self, name): settings = QSettings(QSettings.IniFormat, QSettings.UserScope, 'DAYU', 'dayu_widgets') if settings.value('{}/headerState'.format(name)): self.header_view.restoreState( settings.value('{}/headerState'.format(name)))
class MClickBrowserFilePushButton(MPushButton): """A Clickable push button to browser files""" sig_file_changed = Signal(str) sig_files_changed = Signal(list) slot_browser_file = _slot_browser_file def __init__(self, text='Browser', multiple=False, parent=None): super(MClickBrowserFilePushButton, self).__init__(text=text, parent=parent) self.setProperty('multiple', multiple) self.clicked.connect(self.slot_browser_file) self.setToolTip(self.tr('Click to browser file')) self._path = None self._multiple = multiple self._filters = [] def get_dayu_filters(self): """ Get browser's format filters :return: list """ return self._filters def set_dayu_filters(self, value): """ Set browser file format filters :param value: :return: None """ self._filters = value def get_dayu_path(self): """ Get last browser file path :return: str """ return self._path def set_dayu_path(self, value): """ Set browser file start path :param value: str :return: None """ self._path = value def get_dayu_multiple(self): """ Get browser can select multiple file or not :return: bool """ return self._multiple def set_dayu_multiple(self, value): """ Set browser can select multiple file or not :param value: bool :return: None """ self._multiple = value dayu_multiple = Property(bool, get_dayu_multiple, set_dayu_multiple) dayu_path = Property(six.string_types[0], get_dayu_path, set_dayu_path) dayu_filters = Property(list, get_dayu_filters, set_dayu_filters)
class MDragFolderButton(MToolButton): """A Clickable and draggable tool button to browser folders""" sig_folder_changed = Signal(str) sig_folders_changed = Signal(list) slot_browser_folder = _slot_browser_folder def __init__(self, multiple=False, parent=None): super(MDragFolderButton, self).__init__(parent=parent) self.setAcceptDrops(True) self.setMouseTracking(True) self.text_under_icon() self.set_dayu_svg('folder_line.svg') self.set_dayu_size(60) self.setIconSize(QSize(60, 60)) self.setText(self.tr('Click or drag folder here')) self.clicked.connect(self.slot_browser_folder) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.setToolTip(self.tr('Click to browser folder or drag folder here')) self._path = None self._multiple = multiple def get_dayu_path(self): """ Get last browser file path :return: str """ return self._path def set_dayu_path(self, value): """ Set browser file start path :param value: str :return: None """ self._path = value def get_dayu_multiple(self): """ Get browser can select multiple file or not :return: bool """ return self._multiple def set_dayu_multiple(self, value): """ Set browser can select multiple file or not :param value: bool :return: None """ self._multiple = value dayu_multiple = Property(bool, get_dayu_multiple, set_dayu_multiple) dayu_path = Property(bool, get_dayu_path, set_dayu_path) def dragEnterEvent(self, event): """Override dragEnterEvent. Validate dragged folders""" if event.mimeData().hasFormat("text/uri-list"): folder_list = [ url.toLocalFile() for url in event.mimeData().urls() if os.path.isdir(url.toLocalFile()) ] count = len(folder_list) if count == 1 or (count > 1 and self.get_dayu_multiple()): event.acceptProposedAction() return def dropEvent(self, event): """Override dropEvent to accept the dropped folders""" folder_list = [ url.toLocalFile() for url in event.mimeData().urls() if os.path.isdir(url.toLocalFile()) ] if self.get_dayu_multiple(): self.sig_folders_changed.emit(folder_list) else: self.sig_folder_changed.emit(folder_list[0]) self.set_dayu_path(folder_list[0])
class MNewTag(QWidget): """New Tag input component.""" sig_add_tag = Signal(str) def __init__(self, text='New Tag', parent=None): super(MNewTag, self).__init__(parent) self.setAttribute(Qt.WA_StyledBackground) self._add_button = MToolButton().text_beside_icon().tiny().svg( 'add_line.svg') self._add_button.setText(text) self._add_button.clicked.connect(self._slot_show_edit) self._line_edit = MLineEdit().tiny() self._line_edit.returnPressed.connect(self._slot_return_pressed) self._line_edit.setVisible(False) self._line_edit.installEventFilter(self) self._main_lay = QGridLayout() self._main_lay.setContentsMargins(3, 3, 3, 3) self._main_lay.addWidget(self._add_button, 0, 0) self._main_lay.addWidget(self._line_edit, 0, 0) self.setLayout(self._main_lay) style = QssTemplate(''' MNewTag{ border: 1px dashed @border_color; } MNewTag MToolButton:hover{ border:none; } ''') self.setStyleSheet(style.substitute(vars(dayu_theme))) def set_completer(self, completer): """Set the input completer""" self._line_edit.setCompleter(completer) def _slot_show_edit(self): self._line_edit.setVisible(True) self._add_button.setVisible(False) self._line_edit.setFocus(Qt.MouseFocusReason) def _slot_return_pressed(self): self._line_edit.setVisible(False) self._add_button.setVisible(True) if self._line_edit.text(): self.sig_add_tag.emit(self._line_edit.text()) self._line_edit.clear() def focusOutEvent(self, *args, **kwargs): """Override focusOutEvent to change the edit mode to button mode.""" self._line_edit.setVisible(False) self._add_button.setVisible(True) return super(MNewTag, self).focusOutEvent(*args, **kwargs) def eventFilter(self, widget, event): if widget is self._line_edit: if event.type() == QEvent.Type.KeyPress and event.key( ) == Qt.Key_Escape: self._line_edit.setVisible(False) self._add_button.setVisible(True) return super(MNewTag, self).eventFilter(widget, event)
class MDragFileButton(MToolButton): """A Clickable and draggable tool button to upload files""" sig_file_changed = Signal(str) sig_files_changed = Signal(list) slot_browser_file = _slot_browser_file def __init__(self, text='', multiple=False, parent=None): super(MDragFileButton, self).__init__(parent=parent) self.setAcceptDrops(True) self.setMouseTracking(True) self.text_under_icon() self.setText(text) self.set_dayu_size(60) self.set_dayu_svg('cloud_line.svg') self.setIconSize(QSize(60, 60)) self.clicked.connect(self.slot_browser_file) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.setToolTip(self.tr('Click to browser file')) self._path = None self._multiple = multiple self._filters = [] def get_dayu_filters(self): """ Get browser's format filters :return: list """ return self._filters def set_dayu_filters(self, value): """ Set browser file format filters :param value: :return: None """ self._filters = value def get_dayu_path(self): """ Get last browser file path :return: str """ return self._path def set_dayu_path(self, value): """ Set browser file start path :param value: str :return: None """ self._path = value def get_dayu_multiple(self): """ Get browser can select multiple file or not :return: bool """ return self._multiple def set_dayu_multiple(self, value): """ Set browser can select multiple file or not :param value: bool :return: None """ self._multiple = value dayu_multiple = Property(bool, get_dayu_multiple, set_dayu_multiple) dayu_path = Property(six.string_types[0], get_dayu_path, set_dayu_path) dayu_filters = Property(list, get_dayu_filters, set_dayu_filters) def dragEnterEvent(self, event): """Override dragEnterEvent. Validate dragged files""" if event.mimeData().hasFormat("text/uri-list"): file_list = self._get_valid_file_list(event.mimeData().urls()) count = len(file_list) if count == 1 or (count > 1 and self.get_dayu_multiple()): event.acceptProposedAction() return def dropEvent(self, event): """Override dropEvent to accept the dropped files""" file_list = self._get_valid_file_list(event.mimeData().urls()) if self.get_dayu_multiple(): self.sig_files_changed.emit(file_list) self.set_dayu_path(file_list) else: self.sig_file_changed.emit(file_list[0]) self.set_dayu_path(file_list[0]) def _get_valid_file_list(self, url_list): import subprocess import sys file_list = [] for url in url_list: file_name = url.toLocalFile() if sys.platform == 'darwin': sub_process = subprocess.Popen( 'osascript -e \'get posix path of posix file \"file://{}\" -- kthxbai\'' .format(file_name), stdout=subprocess.PIPE, shell=True) # print sub_process.communicate()[0].strip() file_name = sub_process.communicate()[0].strip() sub_process.wait() if os.path.isfile(file_name): if self.get_dayu_filters(): if os.path.splitext( file_name)[-1] in self.get_dayu_filters(): file_list.append(file_name) else: file_list.append(file_name) return file_list
class MTag(QLabel): """ Tag for categorizing or markup. """ sig_closed = Signal() sig_clicked = Signal() def __init__(self, text='', parent=None): super(MTag, self).__init__(text=text, parent=parent) self._is_pressed = False self._close_button = MToolButton().tiny().svg('close_line.svg').icon_only() self._close_button.clicked.connect(self.sig_closed) self._close_button.clicked.connect(self.close) self._close_button.setVisible(False) self._main_lay = QHBoxLayout() self._main_lay.setContentsMargins(0, 0, 0, 0) self._main_lay.addStretch() self._main_lay.addWidget(self._close_button) self.setLayout(self._main_lay) self._clickable = False self._border = True self._border_style = QssTemplate(''' MTag{ font-size: 12px; padding: 3px; color: @text_color; border-radius: @border_radius; border: 1px solid @border_color; background-color: @background_color; } MTag:hover{ color: @hover_color; } ''') self._no_border_style = QssTemplate(''' MTag{ font-size: 12px; padding: 4px; border-radius: @border_radius; color: @text_color; border: 0 solid @border_color; background-color: @background_color; } MTag:hover{ background-color:@hover_color; } ''') self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) self._color = None self.set_dayu_color(dayu_theme.secondary_text_color) def minimumSizeHint(self, *args, **kwargs): """Override minimumSizeHint for expand width when the close button is visible.""" orig = super(MTag, self).minimumSizeHint(*args, **kwargs) orig.setWidth(orig.width() + (dayu_theme.tiny if self._close_button.isVisible() else 0)) return orig def get_dayu_color(self): """Get tag's color""" return self._color def set_dayu_color(self, value): """Set Tag primary color.""" self._color = value self._update_style() def _update_style(self): if self._border: self.setStyleSheet( self._border_style.substitute(background_color=utils.fade_color(self._color, '15%'), border_radius=dayu_theme.border_radius_base, border_color=utils.fade_color(self._color, '35%'), hover_color=utils.generate_color(self._color, 5), text_color=self._color)) else: self.setStyleSheet(self._no_border_style.substitute( background_color=utils.generate_color(self._color, 6), border_radius=dayu_theme.border_radius_base, border_color=utils.generate_color(self._color, 6), hover_color=utils.generate_color(self._color, 5), text_color=dayu_theme.text_color_inverse)) dayu_color = Property(str, get_dayu_color, set_dayu_color) def mousePressEvent(self, event): """Override mousePressEvent to flag _is_pressed.""" if event.button() == Qt.LeftButton: self._is_pressed = True return super(MTag, self).mousePressEvent(event) def leaveEvent(self, event): """Override leaveEvent to reset _is_pressed flag.""" self._is_pressed = False return super(MTag, self).leaveEvent(event) def mouseReleaseEvent(self, event): """Override mouseReleaseEvent to emit sig_clicked signal.""" if event.button() == Qt.LeftButton and self._is_pressed: if self._clickable: self.sig_clicked.emit() self._is_pressed = False return super(MTag, self).mouseReleaseEvent(event) def closeable(self): """Set Tag can be closed and show the close icon button.""" self._close_button.setVisible(True) return self def clickable(self): """Set Tag can be clicked and change the cursor to pointing-hand shape when enter.""" self.setCursor(Qt.PointingHandCursor) self._clickable = True return self def no_border(self): """Set Tag style is border or fill.""" self._border = False self._update_style() return self def coloring(self, color): """Same as set_dayu_color. Support chain.""" self.set_dayu_color(color) return self
class MToast(QWidget): """ MToast A Phone style message. """ InfoType = 'info' SuccessType = 'success' WarningType = 'warning' ErrorType = 'error' LoadingType = 'loading' default_config = { 'duration': 2, } sig_closed = Signal() def __init__(self, text, duration=None, dayu_type=None, parent=None): super(MToast, self).__init__(parent) self.setWindowFlags(Qt.FramelessWindowHint | Qt.Dialog | Qt.WA_TranslucentBackground | Qt.WA_DeleteOnClose) self.setAttribute(Qt.WA_StyledBackground) _icon_lay = QHBoxLayout() _icon_lay.addStretch() if dayu_type == MToast.LoadingType: _icon_lay.addWidget( MLoading(size=dayu_theme.huge, color=dayu_theme.text_color_inverse)) else: _icon_label = MAvatar() _icon_label.set_dayu_size(60) _icon_label.set_dayu_image( MPixmap('{}_line.svg'.format(dayu_type or MToast.InfoType), dayu_theme.text_color_inverse)) _icon_lay.addWidget(_icon_label) _icon_lay.addStretch() _content_label = MLabel() _content_label.setText(text) _content_label.setAlignment(Qt.AlignCenter) _main_lay = QVBoxLayout() _main_lay.setContentsMargins(0, 0, 0, 0) _main_lay.addStretch() _main_lay.addLayout(_icon_lay) _main_lay.addSpacing(10) _main_lay.addWidget(_content_label) _main_lay.addStretch() self.setLayout(_main_lay) self.setFixedSize(QSize(120, 120)) _close_timer = QTimer(self) _close_timer.setSingleShot(True) _close_timer.timeout.connect(self.close) _close_timer.timeout.connect(self.sig_closed) _close_timer.setInterval( (duration or self.default_config.get('duration')) * 1000) _ani_timer = QTimer(self) _ani_timer.timeout.connect(self._fade_out) _ani_timer.setInterval( (duration or self.default_config.get('duration')) * 1000 - 300) _close_timer.start() _ani_timer.start() self._opacity_ani = QPropertyAnimation() self._opacity_ani.setTargetObject(self) self._opacity_ani.setDuration(300) self._opacity_ani.setEasingCurve(QEasingCurve.OutCubic) self._opacity_ani.setPropertyName(b'windowOpacity') self._opacity_ani.setStartValue(0.0) self._opacity_ani.setEndValue(0.9) self._get_center_position(parent) self._fade_int() def _fade_out(self): self._opacity_ani.setDirection(QAbstractAnimation.Backward) self._opacity_ani.start() def _fade_int(self): self._opacity_ani.start() def _get_center_position(self, parent): parent_geo = parent.geometry() pos = parent_geo.topLeft() \ if parent.parent() is None else parent.mapToGlobal(parent_geo.topLeft()) offset = 0 for child in parent.children(): if isinstance(child, MToast) and child.isVisible(): offset = max(offset, child.y()) target_x = pos.x() + parent_geo.width() / 2 - self.width() / 2 target_y = pos.y() + parent_geo.height() / 2 - self.height() / 2 self.setProperty('pos', QPoint(target_x, target_y)) @classmethod def info(cls, text, parent, duration=None): """Show a normal toast message""" inst = cls(text, duration=duration, dayu_type=MToast.InfoType, parent=parent) inst.show() return inst @classmethod def success(cls, text, parent, duration=None): """Show a success toast message""" inst = cls(text, duration=duration, dayu_type=MToast.SuccessType, parent=parent) inst.show() return inst @classmethod def warning(cls, text, parent, duration=None): """Show a warning toast message""" inst = cls(text, duration=duration, dayu_type=MToast.WarningType, parent=parent) inst.show() return inst @classmethod def error(cls, text, parent, duration=None): """Show an error toast message""" inst = cls(text, duration=duration, dayu_type=MToast.ErrorType, parent=parent) inst.show() return inst @classmethod def loading(cls, text, parent): """Show a toast message with loading animation""" inst = cls(text, dayu_type=MToast.LoadingType, parent=parent) inst.show() return inst @classmethod def config(cls, duration): """ Config the global MToast duration setting. :param duration: int (unit is second) :return: None """ if duration is not None: cls.default_config['duration'] = duration
class MComboBox(QComboBox): Separator = '/' sig_value_changed = Signal(list) def __init__(self, parent=None): super(MComboBox, self).__init__(parent) self._root_menu = None self._display_formatter = utils.display_formatter self.setEditable(True) line_edit = self.lineEdit() line_edit.setReadOnly(True) line_edit.setTextMargins(4, 0, 4, 0) line_edit.setStyleSheet('background-color:transparent') # line_edit.setCursor(Qt.PointingHandCursor) line_edit.installEventFilter(self) self._has_custom_view = False self.set_value('') self.set_placeholder(self.tr('Please Select')) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) self._dayu_size = dayu_theme.default_size def get_dayu_size(self): """ Get the push button height :return: integer """ return self._dayu_size def set_dayu_size(self, value): """ Set the avatar size. :param value: integer :return: None """ self._dayu_size = value self.lineEdit().setProperty('dayu_size', value) self.style().polish(self) dayu_size = Property(int, get_dayu_size, set_dayu_size) def set_formatter(self, func): self._display_formatter = func def set_placeholder(self, text): """Display the text when no item selected.""" self.lineEdit().setPlaceholderText(text) def set_value(self, value): self.setProperty('value', value) def _set_value(self, value): self.lineEdit().setProperty('text', self._display_formatter(value)) if self._root_menu: self._root_menu.set_value(value) def set_menu(self, menu): self._root_menu = menu self._root_menu.sig_value_changed.connect(self.sig_value_changed) self._root_menu.sig_value_changed.connect(self.set_value) def setView(self, *args, **kwargs): """Override setView to flag _has_custom_view variable.""" self._has_custom_view = True super(MComboBox, self).setView(*args, **kwargs) def showPopup(self): """Override default showPopup. When set custom menu, show the menu instead.""" if self._has_custom_view or self._root_menu is None: super(MComboBox, self).showPopup() else: QComboBox.hidePopup(self) self._root_menu.popup(self.mapToGlobal(QPoint(0, self.height()))) def setCurrentIndex(self, index): raise NotImplementedError def eventFilter(self, widget, event): if widget is self.lineEdit(): if event.type() == QEvent.MouseButtonPress: self.showPopup() return super(MComboBox, self).eventFilter(widget, event) def huge(self): """Set MComboBox to huge size""" self.set_dayu_size(dayu_theme.huge) return self def large(self): """Set MComboBox to large size""" self.set_dayu_size(dayu_theme.large) return self def medium(self): """Set MComboBox to medium""" self.set_dayu_size(dayu_theme.medium) return self def small(self): """Set MComboBox to small size""" self.set_dayu_size(dayu_theme.small) return self def tiny(self): """Set MComboBox to tiny size""" self.set_dayu_size(dayu_theme.tiny) return self
class MCheckBoxGroup(MButtonGroupBase): sig_checked_changed = Signal(list) def __init__(self, orientation=Qt.Horizontal, parent=None): super(MCheckBoxGroup, self).__init__(orientation=orientation, parent=parent) self.set_spacing(15) self._button_group.setExclusive(False) self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self._slot_context_menu) self._button_group.buttonClicked[int].connect(self._slot_map_signal) self._dayu_checked = [] def create_button(self, data_dict): return MCheckBox() @Slot(QPoint) def _slot_context_menu(self, point): context_menu = MMenu(parent=self) action_select_all = context_menu.addAction('Select All') action_select_none = context_menu.addAction('Select None') action_select_invert = context_menu.addAction('Select Invert') action_select_all.triggered.connect(functools.partial(self._slot_set_select, True)) action_select_none.triggered.connect(functools.partial(self._slot_set_select, False)) action_select_invert.triggered.connect(functools.partial(self._slot_set_select, None)) context_menu.exec_(QCursor.pos() + QPoint(10, 10)) @Slot(bool) def _slot_set_select(self, state): for check_box in self._button_group.buttons(): if state is None: old_state = check_box.isChecked() check_box.setChecked(not old_state) else: check_box.setChecked(state) self._slot_map_signal() @Slot(int) def _slot_map_signal(self, state=None): self.sig_checked_changed.emit( [check_box.text() for check_box in self._button_group.buttons() if check_box.isChecked()]) def set_dayu_checked(self, value): if not isinstance(value, list): value = [value] if value == self.get_dayu_checked(): return self._dayu_checked = value for check_box in self._button_group.buttons(): flag = Qt.Checked if check_box.text() in value else Qt.Unchecked if flag != check_box.checkState(): check_box.setCheckState(flag) self.sig_checked_changed.emit(value) def get_dayu_checked(self): return [check_box.text() for check_box in self._button_group.buttons() if check_box.isChecked()] # TODO: pyside 的 Property 不直接支持 list,需要寻求解决办法 dayu_checked = Property('QVariantList', get_dayu_checked, set_dayu_checked, notify=sig_checked_changed)
class MMessage(QWidget): """ Display global messages as feedback in response to user operations. """ InfoType = 'info' SuccessType = 'success' WarningType = 'warning' ErrorType = 'error' LoadingType = 'loading' default_config = {'duration': 2, 'top': 24} sig_closed = Signal() def __init__(self, text, duration=None, dayu_type=None, closable=False, parent=None): super(MMessage, self).__init__(parent) self.setObjectName('message') self.setWindowFlags(Qt.FramelessWindowHint | Qt.Dialog | Qt.WA_TranslucentBackground | Qt.WA_DeleteOnClose) self.setAttribute(Qt.WA_StyledBackground) if dayu_type == MMessage.LoadingType: _icon_label = MLoading.tiny() else: _icon_label = MAvatar.tiny() current_type = dayu_type or MMessage.InfoType _icon_label.set_dayu_image( MPixmap('{}_fill.svg'.format(current_type), vars(dayu_theme).get(current_type + '_color'))) self._content_label = MLabel(parent=self) # self._content_label.set_elide_mode(Qt.ElideMiddle) self._content_label.setText(text) self._close_button = MToolButton( parent=self).icon_only().svg('close_line.svg').tiny() self._close_button.clicked.connect(self.close) self._close_button.setVisible(closable or False) self._main_lay = QHBoxLayout() self._main_lay.addWidget(_icon_label) self._main_lay.addWidget(self._content_label) self._main_lay.addStretch() self._main_lay.addWidget(self._close_button) self.setLayout(self._main_lay) _close_timer = QTimer(self) _close_timer.setSingleShot(True) _close_timer.timeout.connect(self.close) _close_timer.timeout.connect(self.sig_closed) _close_timer.setInterval( (duration or self.default_config.get('duration')) * 1000) _ani_timer = QTimer(self) _ani_timer.timeout.connect(self._fade_out) _ani_timer.setInterval( (duration or self.default_config.get('duration')) * 1000 - 300) _close_timer.start() _ani_timer.start() self._pos_ani = QPropertyAnimation(self) self._pos_ani.setTargetObject(self) self._pos_ani.setEasingCurve(QEasingCurve.OutCubic) self._pos_ani.setDuration(300) self._pos_ani.setPropertyName(b'pos') self._opacity_ani = QPropertyAnimation() self._opacity_ani.setTargetObject(self) self._opacity_ani.setDuration(300) self._opacity_ani.setEasingCurve(QEasingCurve.OutCubic) self._opacity_ani.setPropertyName(b'windowOpacity') self._opacity_ani.setStartValue(0.0) self._opacity_ani.setEndValue(1.0) self._set_proper_position(parent) self._fade_int() def _fade_out(self): self._pos_ani.setDirection(QAbstractAnimation.Backward) self._pos_ani.start() self._opacity_ani.setDirection(QAbstractAnimation.Backward) self._opacity_ani.start() def _fade_int(self): self._pos_ani.start() self._opacity_ani.start() def _set_proper_position(self, parent): parent_geo = parent.geometry() pos = parent_geo.topLeft( ) if parent.parent() is None else parent.mapToGlobal( parent_geo.topLeft()) offset = 0 for child in parent.children(): if isinstance(child, MMessage) and child.isVisible(): offset = max(offset, child.y()) base = pos.y() + MMessage.default_config.get('top') target_x = pos.x() + parent_geo.width() / 2 - 100 target_y = (offset + 50) if offset else base self._pos_ani.setStartValue(QPoint(target_x, target_y - 40)) self._pos_ani.setEndValue(QPoint(target_x, target_y)) @classmethod def info(cls, text, parent, duration=None, closable=None): """Show a normal message""" inst = cls(text, dayu_type=MMessage.InfoType, duration=duration, closable=closable, parent=parent) inst.show() return inst @classmethod def success(cls, text, parent, duration=None, closable=None): """Show a success message""" inst = cls(text, dayu_type=MMessage.SuccessType, duration=duration, closable=closable, parent=parent) inst.show() return inst @classmethod def warning(cls, text, parent, duration=None, closable=None): """Show a warning message""" inst = cls(text, dayu_type=MMessage.WarningType, duration=duration, closable=closable, parent=parent) inst.show() return inst @classmethod def error(cls, text, parent, duration=None, closable=None): """Show an error message""" inst = cls(text, dayu_type=MMessage.ErrorType, duration=duration, closable=closable, parent=parent) inst.show() return inst @classmethod def loading(cls, text, parent): """Show a message with loading animation""" inst = cls(text, dayu_type=MMessage.LoadingType, parent=parent) inst.show() return inst @classmethod def config(cls, duration=None, top=None): """ Config the global MMessage duration and top setting. :param duration: int (unit is second) :param top: int (unit is px) :return: None """ if duration is not None: cls.default_config['duration'] = duration if top is not None: cls.default_config['top'] = top
class MMenu(QMenu): sig_value_changed = Signal(list) def __init__(self, exclusive=True, cascader=False, title='', parent=None): super(MMenu, self).__init__(title=title, parent=parent) self.setProperty('cascader', cascader) self.setCursor(Qt.PointingHandCursor) self._action_group = QActionGroup(self) self._action_group.setExclusive(exclusive) self._action_group.triggered.connect(self.slot_on_action_triggered) self._load_data_func = None self.set_value('') self.set_data([]) self.set_separator('/') def set_separator(self, chr): self.setProperty('separator', chr) def set_load_callback(self, func): assert callable(func) self._load_data_func = func self.aboutToShow.connect(self.slot_fetch_data) def slot_fetch_data(self): data_list = self._load_data_func() self.set_data(data_list) def set_value(self, data): assert isinstance(data, (list, basestring, int, float)) # if isinstance(data, int): # action = self._action_group.actions()[data] # data = action.property('value') if self.property('cascader') and isinstance(data, basestring): data = data.split(self.property('separator')) self.setProperty('value', data) def _set_value(self, value): data_list = value if isinstance(value, list) else [value] flag = False for act in self._action_group.actions(): checked = act.property('value') in data_list if act.isChecked() != checked: # 更新来自代码 act.setChecked(checked) flag = True if flag: self.sig_value_changed.emit(value) def _add_menu(self, parent_menu, data_dict): if 'children' in data_dict: menu = MMenu(title=data_dict.get('label'), parent=self) menu.setProperty('value', data_dict.get('value')) parent_menu.addMenu(menu) if not (parent_menu is self): # 用来将来获取父层级数据 menu.setProperty('parent_menu', parent_menu) for i in data_dict.get('children'): self._add_menu(menu, i) else: action = self._action_group.addAction( utils.display_formatter(data_dict.get('label'))) if data_dict.get('icon'): action.setIcon(data_dict.get('icon')) action.setProperty('value', data_dict.get('value')) action.setCheckable(True) # 用来将来获取父层级数据 action.setProperty('parent_menu', parent_menu) parent_menu.addAction(action) def set_data(self, option_list): assert isinstance(option_list, list) if option_list: if all(isinstance(i, basestring) for i in option_list): option_list = utils.from_list_to_nested_dict( option_list, sep=self.property('separator')) if all(isinstance(i, (int, float)) for i in option_list): option_list = [{ 'value': i, 'label': str(i) } for i in option_list] # 全部转换成 dict 类型的 list self.setProperty('data', option_list) def _set_data(self, option_list): self.clear() for act in self._action_group.actions(): self._action_group.removeAction(act) for data_dict in option_list: self._add_menu(self, data_dict) def _get_parent(self, result, obj): if obj.property('parent_menu'): parent_menu = obj.property('parent_menu') result.insert(0, parent_menu.title()) self._get_parent(result, parent_menu) @Slot(QAction) def slot_on_action_triggered(self, action): current_data = action.property('value') if self.property('cascader'): selected_data = [current_data] self._get_parent(selected_data, action) else: if self._action_group.isExclusive(): selected_data = current_data else: selected_data = [ act.property('value') for act in self._action_group.actions() if act.isChecked() ] self.set_value(selected_data) self.sig_value_changed.emit(selected_data) def set_loader(self, func): self._load_data_func = func
class MDrawer(QWidget): """ A panel which slides in from the edge of the screen. """ LeftPos = 'left' RightPos = 'right' TopPos = 'top' BottomPos = 'bottom' sig_closed = Signal() def __init__(self, title, position='right', closable=True, parent=None): super(MDrawer, self).__init__(parent) self.setObjectName('message') # self.setWindowFlags(Qt.Popup ) # self.setWindowFlags( # Qt.FramelessWindowHint | Qt.Popup | Qt.WA_TranslucentBackground) self.setAttribute(Qt.WA_StyledBackground) self._title_label = MLabel(parent=self).h4() # self._title_label.set_elide_mode(Qt.ElideRight) self._title_label.setText(title) self._close_button = MToolButton( parent=self).icon_only().svg('close_line.svg').small() self._close_button.clicked.connect(self.close) self._close_button.setVisible(closable or False) _title_lay = QHBoxLayout() _title_lay.addWidget(self._title_label) _title_lay.addStretch() _title_lay.addWidget(self._close_button) self._button_lay = QHBoxLayout() self._button_lay.addStretch() self._scroll_area = QScrollArea() self._main_lay = QVBoxLayout() self._main_lay.addLayout(_title_lay) self._main_lay.addWidget(MDivider()) self._main_lay.addWidget(self._scroll_area) self._main_lay.addWidget(MDivider()) self._main_lay.addLayout(self._button_lay) self.setLayout(self._main_lay) self._position = position self._close_timer = QTimer(self) self._close_timer.setSingleShot(True) self._close_timer.timeout.connect(self.close) self._close_timer.timeout.connect(self.sig_closed) self._close_timer.setInterval(300) self._is_first_close = True self._pos_ani = QPropertyAnimation(self) self._pos_ani.setTargetObject(self) self._pos_ani.setEasingCurve(QEasingCurve.OutCubic) self._pos_ani.setDuration(300) self._pos_ani.setPropertyName('pos') self._opacity_ani = QPropertyAnimation() self._opacity_ani.setTargetObject(self) self._opacity_ani.setDuration(300) self._opacity_ani.setEasingCurve(QEasingCurve.OutCubic) self._opacity_ani.setPropertyName('windowOpacity') self._opacity_ani.setStartValue(0.0) self._opacity_ani.setEndValue(1.0) # self._shadow_effect = QGraphicsDropShadowEffect(self) # color = dayu_theme.red # self._shadow_effect.setColor(color) # self._shadow_effect.setOffset(0, 0) # self._shadow_effect.setBlurRadius(5) # self._shadow_effect.setEnabled(False) # self.setGraphicsEffect(self._shadow_effect) self.app = QApplication.instance() self.app.installEventFilter(self) self.protect_time = time.time() def retrieveChildren(self, parent, receiver): if parent is receiver: return True if not hasattr(parent, "children"): return for child in parent.children(): ret = self.retrieveChildren(child, receiver) if ret: return ret def eventFilter(self, receiver, event): # Note QEvent.Type.MouseButtonPress 为 2 if event.type() == 2: if self.retrieveChildren(self, receiver): self.protect_time = time.time() # NOTE 如果点击多次触发,通过时间进行保护 if (time.time() - self.protect_time) > .1: self.close() elif event.type() == QEvent.Type.Resize and receiver is self.window(): self.close() return False def set_widget(self, widget): self._scroll_area.setWidget(widget) def add_button(self, button): self._button_lay.addWidget(button) def _fade_out(self): self._pos_ani.setDirection(QAbstractAnimation.Backward) self._pos_ani.start() self._opacity_ani.setDirection(QAbstractAnimation.Backward) self._opacity_ani.start() def _fade_int(self): self._pos_ani.start() self._opacity_ani.start() def _set_proper_position(self): parent = self.parent() parent_geo = parent.geometry() if self._position == MDrawer.LeftPos: pos = parent_geo.topLeft( ) if parent.parent() is None else parent.mapToGlobal( parent_geo.topLeft()) pos -= self.window().geometry().topLeft() target_x = pos.x() target_y = pos.y() self.setFixedHeight(parent_geo.height()) self._pos_ani.setStartValue( QPoint(target_x - self.width(), target_y)) self._pos_ani.setEndValue(QPoint(target_x, target_y)) if self._position == MDrawer.RightPos: pos = parent_geo.topRight( ) if parent.parent() is None else parent.mapToGlobal( parent_geo.topRight()) pos -= self.window().geometry().topLeft() self.setFixedHeight(parent_geo.height()) target_x = pos.x() - self.width() target_y = pos.y() self._pos_ani.setStartValue( QPoint(target_x + self.width(), target_y)) self._pos_ani.setEndValue(QPoint(target_x, target_y)) if self._position == MDrawer.TopPos: pos = parent_geo.topLeft( ) if parent.parent() is None else parent.mapToGlobal( parent_geo.topLeft()) pos -= self.window().geometry().topLeft() self.setFixedWidth(parent_geo.width()) target_x = pos.x() target_y = pos.y() self._pos_ani.setStartValue( QPoint(target_x, target_y - self.height())) self._pos_ani.setEndValue(QPoint(target_x, target_y)) if self._position == MDrawer.BottomPos: pos = parent_geo.bottomLeft( ) if parent.parent() is None else parent.mapToGlobal( parent_geo.bottomLeft()) pos -= self.window().geometry().topLeft() self.setFixedWidth(parent_geo.width()) target_x = pos.x() target_y = pos.y() - self.height() self._pos_ani.setStartValue( QPoint(target_x, target_y + self.height())) self._pos_ani.setEndValue(QPoint(target_x, target_y)) def set_dayu_position(self, value): """ Set the placement of the MDrawer. top/right/bottom/left, default is right :param value: str :return: None """ self._position = value if value in [MDrawer.BottomPos, MDrawer.TopPos]: self.setFixedHeight(200) else: self.setFixedWidth(200) def get_dayu_position(self): """ Get the placement of the MDrawer :return: str """ return self._position dayu_position = Property(str, get_dayu_position, set_dayu_position) def left(self): """Set drawer's placement to left""" self.set_dayu_position(MDrawer.LeftPos) return self def right(self): """Set drawer's placement to right""" self.set_dayu_position(MDrawer.RightPos) return self def top(self): """Set drawer's placement to top""" self.set_dayu_position(MDrawer.TopPos) return self def bottom(self): """Set drawer's placement to bottom""" self.set_dayu_position(MDrawer.BottomPos) return self def show(self): self._set_proper_position() self._fade_int() return super(MDrawer, self).show() def closeEvent(self, event): self.app.removeEventFilter(self) if self._is_first_close: self._is_first_close = False self._close_timer.start() self._fade_out() event.ignore() else: event.accept()
class MLineEdit(QLineEdit): """MLineEdit""" sig_delay_text_changed = Signal(basestring) def __init__(self, text='', parent=None): super(MLineEdit, self).__init__(text, parent) self._main_layout = QHBoxLayout() self._main_layout.setContentsMargins(0, 0, 0, 0) self._main_layout.addStretch() self._prefix_widget = None self._suffix_widget = None self.setLayout(self._main_layout) self.setProperty('history', self.property('text')) self.setTextMargins(2, 0, 2, 0) self._delay_timer = QTimer() self._delay_timer.setInterval(500) self._delay_timer.setSingleShot(True) self._delay_timer.timeout.connect(self._slot_delay_text_changed) self._dayu_size = dayu_theme.default_size def get_dayu_size(self): """ Get the push button height :return: integer """ return self._dayu_size def set_dayu_size(self, value): """ Set the avatar size. :param value: integer :return: None """ self._dayu_size = value if hasattr(self._prefix_widget, 'set_dayu_size'): self._prefix_widget.set_dayu_size(self._dayu_size) if hasattr(self._suffix_widget, 'set_dayu_size'): self._suffix_widget.set_dayu_size(self._dayu_size) self.style().polish(self) dayu_size = Property(int, get_dayu_size, set_dayu_size) def set_delay_duration(self, millisecond): """Set delay timer's timeout duration.""" self._delay_timer.setInterval(millisecond) @Slot() def _slot_delay_text_changed(self): self.sig_delay_text_changed.emit(self.text()) def get_prefix_widget(self): """Get the prefix widget for user to edit""" return self._prefix_widget def set_prefix_widget(self, widget): """Set the line edit left start widget""" if self._prefix_widget: index = self._main_layout.indexOf(self._prefix_widget) self._main_layout.takeAt(index) self._prefix_widget.setVisible(False) # if isinstance(widget, MPushButton): widget.setProperty('combine', 'horizontal') widget.setProperty('position', 'left') if hasattr(widget, 'set_dayu_size'): widget.set_dayu_size(self._dayu_size) margin = self.textMargins() margin.setLeft(margin.left() + widget.width()) self.setTextMargins(margin) self._main_layout.insertWidget(0, widget) self._prefix_widget = widget return widget def get_suffix_widget(self): """Get the suffix widget for user to edit""" return self._suffix_widget def set_suffix_widget(self, widget): """Set the line edit right end widget""" if self._suffix_widget: index = self._main_layout.indexOf(self._suffix_widget) self._main_layout.takeAt(index) self._suffix_widget.setVisible(False) # if isinstance(widget, MPushButton): widget.setProperty('combine', 'horizontal') widget.setProperty('position', 'right') if hasattr(widget, 'set_dayu_size'): widget.set_dayu_size(self._dayu_size) margin = self.textMargins() margin.setRight(margin.right() + widget.width()) self.setTextMargins(margin) self._main_layout.addWidget(widget) self._suffix_widget = widget return widget def setText(self, text): """Override setText save text to history""" self.setProperty('history', u'{}\n{}'.format(self.property('history'), text)) return super(MLineEdit, self).setText(text) def clear(self): """Override clear to clear history""" self.setProperty('history', '') return super(MLineEdit, self).clear() def keyPressEvent(self, event): """Override keyPressEvent to start delay timer""" if event.key() not in [Qt.Key_Enter, Qt.Key_Tab]: if self._delay_timer.isActive(): self._delay_timer.stop() self._delay_timer.start() super(MLineEdit, self).keyPressEvent(event) def search(self): """Add a search icon button for MLineEdit.""" suffix_button = MToolButton().icon_only().svg('close_line.svg') suffix_button.clicked.connect(self.clear) self.set_suffix_widget(suffix_button) self.setPlaceholderText(self.tr('Enter key word to search...')) return self def error(self): """A a toolset to MLineEdit to store error info with red style""" @Slot() def _slot_show_detail(self): dialog = QTextEdit(self) dialog.setReadOnly(True) geo = QApplication.desktop().screenGeometry() dialog.setGeometry(geo.width() / 2, geo.height() / 2, geo.width() / 4, geo.height() / 4) dialog.setWindowTitle(self.tr('Error Detail Information')) dialog.setText(self.property('history')) dialog.setWindowFlags(Qt.Dialog) dialog.show() self.setProperty('dayu_type', 'error') self.setReadOnly(True) _suffix_button = MToolButton().icon_only().svg('detail_line.svg') _suffix_button.clicked.connect( functools.partial(_slot_show_detail, self)) self.set_suffix_widget(_suffix_button) self.setPlaceholderText(self.tr('Error information will be here...')) return self def search_engine(self, text='Search'): """Add a MPushButton to suffix for MLineEdit""" _suffix_button = MPushButton(text=text).primary() _suffix_button.clicked.connect(self.returnPressed) _suffix_button.setFixedWidth(100) self.set_suffix_widget(_suffix_button) self.setPlaceholderText(self.tr('Enter key word to search...')) return self def file(self, filters=None): """Add a MClickBrowserFileToolButton for MLineEdit to select file""" _suffix_button = MClickBrowserFileToolButton() _suffix_button.sig_file_changed.connect(self.setText) _suffix_button.set_dayu_filters(filters or []) self.textChanged.connect(_suffix_button.set_dayu_path) self.set_suffix_widget(_suffix_button) self.setPlaceholderText(self.tr('Click button to browser files')) return self def folder(self): """Add a MClickBrowserFolderToolButton for MLineEdit to select folder""" _suffix_button = MClickBrowserFolderToolButton() _suffix_button.sig_folder_changed.connect(self.setText) self.textChanged.connect(_suffix_button.set_dayu_path) self.set_suffix_widget(_suffix_button) self.setPlaceholderText(self.tr('Click button to browser folder')) return self def huge(self): """Set MLineEdit to huge size""" self.set_dayu_size(dayu_theme.huge) return self def large(self): """Set MLineEdit to large size""" self.set_dayu_size(dayu_theme.large) return self def medium(self): """Set MLineEdit to medium""" self.set_dayu_size(dayu_theme.medium) return self def small(self): """Set MLineEdit to small size""" self.set_dayu_size(dayu_theme.small) return self def tiny(self): """Set MLineEdit to tiny size""" self.set_dayu_size(dayu_theme.tiny) return self def password(self): """Set MLineEdit to password echo mode""" self.setEchoMode(QLineEdit.Password) return self
class MItemViewFullSet(QWidget): sig_double_clicked = Signal(QModelIndex) sig_left_clicked = Signal(QModelIndex) sig_current_changed = Signal(QModelIndex, QModelIndex) sig_current_row_changed = Signal(QModelIndex, QModelIndex) sig_current_column_changed = Signal(QModelIndex, QModelIndex) sig_selection_changed = Signal(QItemSelection, QItemSelection) sig_context_menu = Signal(object) def __init__(self, table_view=True, big_view=False, parent=None): super(MItemViewFullSet, self).__init__(parent) self.sort_filter_model = MSortFilterModel() self.source_model = MTableModel() self.sort_filter_model.setSourceModel(self.source_model) self.stack_widget = QStackedWidget() self.view_button_grp = MToolButtonGroup(exclusive=True) data_group = [] if table_view: self.table_view = MTableView(show_row_count=True) self.table_view.doubleClicked.connect(self.sig_double_clicked) self.table_view.pressed.connect(self.slot_left_clicked) self.table_view.setModel(self.sort_filter_model) self.stack_widget.addWidget(self.table_view) data_group.append({ 'svg': 'table_view.svg', 'checkable': True, 'tooltip': u'Table View' }) if big_view: self.big_view = MBigView() self.big_view.doubleClicked.connect(self.sig_double_clicked) self.big_view.pressed.connect(self.slot_left_clicked) self.big_view.setModel(self.sort_filter_model) self.stack_widget.addWidget(self.big_view) data_group.append({ 'svg': 'big_view.svg', 'checkable': True, 'tooltip': u'Big View' }) # 设置多个view 共享 MItemSelectionModel leader_view = self.stack_widget.widget(0) self.selection_model = leader_view.selectionModel() for index in range(self.stack_widget.count()): if index == 0: continue other_view = self.stack_widget.widget(index) other_view.setSelectionModel(self.selection_model) self.selection_model.currentChanged.connect(self.sig_current_changed) self.selection_model.currentRowChanged.connect( self.sig_current_row_changed) self.selection_model.currentColumnChanged.connect( self.sig_current_column_changed) self.selection_model.selectionChanged.connect( self.sig_selection_changed) self.tool_bar = QWidget() self.top_lay = QHBoxLayout() self.top_lay.setContentsMargins(0, 0, 0, 0) if data_group and len(data_group) > 1: self.view_button_grp.sig_checked_changed.connect( self.stack_widget.setCurrentIndex) self.view_button_grp.set_button_list(data_group) self.view_button_grp.set_dayu_checked(0) self.top_lay.addWidget(self.view_button_grp) self.search_line_edit = MLineEdit().search().small() self.search_attr_button = MToolButton().icon_only().svg( 'down_fill.svg').small() self.search_line_edit.set_prefix_widget(self.search_attr_button) self.search_line_edit.textChanged.connect( self.sort_filter_model.set_search_pattern) self.search_line_edit.setVisible(False) self.top_lay.addStretch() self.top_lay.addWidget(self.search_line_edit) self.tool_bar.setLayout(self.top_lay) self.page_set = MPage() self.main_lay = QVBoxLayout() self.main_lay.setSpacing(5) self.main_lay.setContentsMargins(0, 0, 0, 0) self.main_lay.addWidget(self.tool_bar) self.main_lay.addWidget(self.stack_widget) self.main_lay.addWidget(self.page_set) self.setLayout(self.main_lay) def enable_context_menu(self): for index in range(self.stack_widget.count()): view = self.stack_widget.widget(index) view.enable_context_menu(True) view.sig_context_menu.connect(self.sig_context_menu) def set_no_data_text(self, text): for index in range(self.stack_widget.count()): view = self.stack_widget.widget(index) view.set_no_data_text(text) def set_selection_mode(self, mode): for index in range(self.stack_widget.count()): view = self.stack_widget.widget(index) view.setSelectionMode(mode) def tool_bar_visible(self, flag): self.tool_bar.setVisible(flag) @Slot(QModelIndex) def slot_left_clicked(self, start_index): button = QApplication.mouseButtons() if button == Qt.LeftButton: real_index = self.sort_filter_model.mapToSource(start_index) self.sig_left_clicked.emit(real_index) def set_header_list(self, header_list): self.source_model.set_header_list(header_list) self.sort_filter_model.set_header_list(header_list) self.sort_filter_model.setSourceModel(self.source_model) for index in range(self.stack_widget.count()): view = self.stack_widget.widget(index) view.set_header_list(header_list) def tool_bar_append_widget(self, widget): self.top_lay.addWidget(widget) def tool_bar_insert_widget(self, widget): self.top_lay.insertWidget(0, widget) @Slot() def setup_data(self, data_list): self.source_model.clear() if data_list: self.source_model.set_data_list(data_list) self.set_record_count(len(data_list)) @Slot(int) def set_record_count(self, total): self.page_set.set_total(total) def get_data(self): return self.source_model.get_data_list() def searchable(self): """Enable search line edit visible.""" self.search_line_edit.setVisible(True) return self