def create_flat_button( icon=None, icon_size=None, name='', text=200, background_color=[54, 51, 51], ui_color=68, border_color=180, push_col=120, checkable=True, w_max=None, w_min=None, h_max=None, h_min=None, policy=None, tip=None, flat=True, hover=True, destroy_flag=False, context=None, ): btn = QPushButton() btn.setText(name) btn.setCheckable(checkable) if icon: if isinstance(icon, QIcon): btn.setIcon(icon) else: btn.setIcon(QIcon(icon)) btn.setFlat(flat) if flat: change_button_color(button=btn, text_color=text, bg_color=ui_color, hi_color=background_color, mode='button', hover=hover, destroy=destroy_flag, ds_color=border_color) btn.toggled.connect( lambda: change_button_color(button=btn, text_color=text, bg_color=ui_color, hi_color=background_color, mode='button', toggle=True, hover=hover, destroy=destroy_flag, ds_color=border_color)) else: change_button_color(button=btn, text_color=text, bg_color=background_color, hi_color=push_col, mode='button', hover=hover, destroy=destroy_flag, ds_color=border_color) if w_max: btn.setMaximumWidth(w_max) if w_min: btn.setMinimumWidth(w_min) if h_max: btn.setMaximumHeight(h_max) if h_min: btn.setMinimumHeight(h_min) if icon_size: btn.setIconSize(QSize(*icon_size)) if policy: btn.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding) if tip: btn.setToolTip(tip) if context: btn.setContextMenuPolicy(Qt.CustomContextMenu) btn.customContextMenuRequested.connect(context) return btn
class WindowStatusBar(statusbar.StatusWidget, object): def __init__(self, parent=None): super(WindowStatusBar, self).__init__(parent) self._info_url = None self._tool = None self.setFixedHeight(25) self._info_btn = QPushButton() self._info_btn.setIconSize(QSize(25, 25)) self._info_btn.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)) self._info_btn.setIcon(resources.icon('info1')) self._info_btn.setStyleSheet('QWidget {background-color: rgba(255, 255, 255, 0); border:0px;}') self._bug_btn = QPushButton() self._bug_btn.setIconSize(QSize(25, 25)) self._bug_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) self._bug_btn.setIcon(resources.icon('bug')) self._bug_btn.setStyleSheet('QWidget {background-color: rgba(255, 255, 255, 0); border:0px;}') self.main_layout.insertWidget(0, self._info_btn) self.main_layout.insertWidget(1, self._bug_btn) self._info_btn.clicked.connect(self._on_open_url) self._bug_btn.clicked.connect(self._on_send_bug) def set_info_url(self, url): """ Sets the URL used to open tool info documentation web :param url: str """ self._info_url = url def set_tool(self, tool): """ :param tool: :return: """ self._tool = tool def has_url(self): """ Returns whether the URL documentation web is set or not :return: bool """ if self._info_url: return True return False def has_tool(self): """ Returns whether window has a tool attached or not :return: bool """ if self._tool: return True return False def show_info(self): """ Shows the info button of the status bar """ self._info_btn.setVisible(True) def hide_info(self): """ Hides the info button of the status bar """ self._info_btn.setVisible(False) def show_bug(self): self._bug_btn.setVisible(True) def hide_bug(self): self._bug_btn.setVisible(False) def open_info_url(self): """ Opens tool documentation URL in user web browser """ if not self._project: return False if self._info_url: webbrowser.open_new_tab(self._info_url) def _on_send_bug(self): if not self._project: return False # tools.ToolsManager().run_tool(self._project, 'bugtracker', extra_args={'tool': self._tool}) def _on_open_url(self): """ Internal callback function that is called when the user presses the info icon button :return: """ self.open_info_url()
class Badge(base.BaseWidget, object): """ Widget that can be located near notification or user avatars to display unread messages count We support 3 types of styles: 1. dof: show a dot 2. count: show a number 3. text: show a string """ def __init__(self, widget=None, parent=None): self._dot = None self._text = None self._count = None self._widget = widget self._overflow_count = 99 super(Badge, self).__init__(parent=parent) # ================================================================================================================= # PROPERTIES # ================================================================================================================= @property def overflow(self): """ Returns current overflow number :return: int """ return self._overflow_count @overflow.setter def overflow(self, value): """ Sets overflow number :param value: int """ self._overflow_count = value self._update_number() @property def count(self): """ Returns current badge count number :return: int """ return self._count @count.setter def count(self, value): """ Sets current badge count number :param value: int """ self._count = value self._update_number() @property def text(self): """ Returns current badge text :return: str """ return self._text @text.setter def text(self, value): """ Sets current badge text :param value: str """ self._text = value self._badge_btn.setText(self._text) self._badge_btn.setVisible(bool(self._text)) self._dot = None self.style().polish(self) def _get_dot(self): """ Returns whether or not current badge style is dot :return: bool """ return self._dot def _set_dot(self, flag): """ Sets whether or not current badge style is dot :param flag: bool """ self._dot = flag self._badge_btn.setText('') self._badge_btn.setVisible(flag) self.style().polish(self) dot = Property(bool, _get_dot, _set_dot) # ================================================================================================================= # OVERRIDES # ================================================================================================================= def get_main_layout(self): main_layout = layouts.GridLayout(margins=(0, 0, 0, 0)) return main_layout def ui(self): super(Badge, self).ui() self._badge_btn = QPushButton() self._badge_btn.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) if self._widget is not None: self.main_layout.addWidget(self._widget, 0, 0) self.main_layout.addWidget(self._badge_btn, 0, 0, Qt.AlignTop | Qt.AlignRight) # ================================================================================================================= # BASE # ================================================================================================================= @classmethod def create_dot(cls, show=False, widget=None, parent=None): """ Creates a new badge with dot style :param show: bool :param widget: QWidget :param parent: QWidget :return: Badge """ inst = cls(widget=widget, parent=parent) inst.dot = show return inst @classmethod def create_count(cls, count=0, widget=None, parent=None): """ Creates a new badge with count style :param count: int :param widget: QWidget :param parent: QWidget :return: Badge """ inst = cls(widget=widget, parent=parent) inst.count = count return inst @classmethod def create_text(cls, text='', widget=None, parent=None): """ Creates a new badge with dot style :param text: str :param widget: QWidget :param parent: QWidget :return: Badge """ inst = cls(widget=widget, parent=parent) inst.text = text return inst # ================================================================================================================= # INTERNAL # ================================================================================================================= def _update_number(self): """ Internal function that updates overflow number """ self._badge_btn.setText( formatters.overflow_format(self._count, self._overflow_count)) self._badge_btn.setVisible(self._count > 0) self._dot = None self.style().polish(self)
class Project(base.BaseWidget): projectOpened = Signal(object) projectRemoved = Signal(str) projectImageChanged = Signal(str) def __init__(self, project_data, parent=None): self._project_data = project_data super(Project, self).__init__(parent) # ============================================================================================================ # PROPERTIES # ============================================================================================================ @property def name(self): return self._project_data.name @property def path(self): return self._project_data.path @property def full_path(self): return self._project_data.full_path @property def settings(self): return self._project_data.settings @property def project_data(self): return self._project_data # ============================================================================================================ # CLASS FUNCTIONS # ============================================================================================================ @classmethod def create_project_from_data(cls, project_data_path): """ Creates a new project using a project data JSON file :param project_data_path: str, path where project JSON data file is located :return: Project """ if project_data_path is None or not path.is_file(project_data_path): LOGGER.warning('Project Data Path {} is not valid!'.format(project_data_path)) return None project_data = settings.JSONSettings() project_options = settings.JSONSettings() project_dir = path.get_dirname(project_data_path) project_name = path.get_basename(project_data_path) project_data.set_directory(project_dir, project_name) project_options.set_directory(project_dir, 'options.json') project_name = project_data.get('name') project_path = path.get_dirname(path.get_dirname(project_data_path)) project_image = project_data.get('image') LOGGER.debug('New Project found [{}]: {}'.format(project_name, project_path)) project_data = core_project.ProjectData( name=project_name, project_path=project_path, settings=project_data, options=project_options) new_project = cls(project_data=project_data) if project_image: new_project.set_image(project_image) return new_project # ============================================================================================================ # OVERRIDES # ============================================================================================================ def ui(self): super(Project, self).ui() self.setMaximumWidth(qtutils.dpi_scale(160)) self.setMaximumHeight(qtutils.dpi_scale(200)) widget_layout = layouts.VerticalLayout(spacing=0, margins=(0, 0, 0, 0)) main_frame = QFrame() main_frame.setFrameStyle(QFrame.StyledPanel | QFrame.Raised) main_frame.setLineWidth(1) main_frame.setLayout(widget_layout) self.main_layout.addWidget(main_frame) self.project_btn = QPushButton('', self) self.project_btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.project_btn.setIconSize(QSize(120, 120)) project_lbl = label.BaseLabel(self.name, parent=self) project_lbl.setObjectName('projectLabel') project_lbl.setAlignment(Qt.AlignCenter) widget_layout.addWidget(self.project_btn) widget_layout.addWidget(project_lbl) def setup_signals(self): self.project_btn.clicked.connect(self._on_open_project) def contextMenuEvent(self, event): menu = QMenu(self) remove_icon = resources.icon(name='delete') remove_action = QAction(remove_icon, 'Remove', menu) remove_tooltip = 'Delete selected project' remove_action.setStatusTip(remove_tooltip) remove_action.setToolTip(remove_tooltip) remove_action.triggered.connect(self._on_remove_project) folder_icon = resources.icon(name='open_folder', extension='png') folder_action = QAction(folder_icon, 'Open in Browser', menu) open_project_in_explorer_tooltip = 'Open project folder in explorer' folder_action.setStatusTip(open_project_in_explorer_tooltip) folder_action.setToolTip(open_project_in_explorer_tooltip) folder_action.triggered.connect(self._on_open_in_browser) image_icon = resources.icon(name='picture', extension='png') set_image_action = QAction(image_icon, 'Set Project Image', menu) set_project_image_tooltip = 'Set the image used by the project' set_image_action.setToolTip(set_project_image_tooltip) set_image_action.setStatusTip(set_project_image_tooltip) set_image_action.triggered.connect(self._on_set_project_image) for action in [remove_action, None, folder_action, None, set_image_action]: if action is None: menu.addSeparator() else: menu.addAction(action) menu.exec_(self.mapToGlobal(event.pos())) # ============================================================================================================ # BASE # ============================================================================================================ def open(self): """ Opens project """ self._on_open_project() def has_option(self, name, group=None): """ Returns whether the current object has given option or not :param name: str, name of the option :param group: variant, str || None, group of the option (optional) :return: bool """ if not self._project_data: return False return self._project_data.has_option(name=name, group=group) def add_option(self, name, value, group=None, option_type=None): """ Adds a new option to the options file :param name: str, name of the option :param value: variant, value of the option :param group: variant, str || None, group of the option (optional) :param option_type: variant, str || None, option type (optional) """ if not self._project_data: return self._project_data.add_option(name, value, group=group, option_type=option_type) def get_option(self, name, group=None, default=None): """ Returns option by name and group :param name: str, name of the option we want to retrieve :param group: variant, str || None, group of the option (optional) :return: variant """ if not self._project_data: return return self._project_data.get_option(name, group=group, default=default) def reload_options(self): """ Reload settings """ if not self._project_data: return self._project_data.reload_options() def clear_options(self): """ Clears all the options """ if not self._project_data: return self._project_data.clear_options() def set_image(self, encoded_image): from tpDcc.libs.qt.core import image if not encoded_image: return encoded_image = encoded_image.encode('utf-8') project_icon = QIcon(QPixmap.fromImage(image.base64_to_image(encoded_image))) if project_icon.isNull(): project_icon = resources.icon('tpDcc') self.project_btn.setIcon(project_icon) def remove(self, force=False): if not path.is_dir(self.full_path): LOGGER.warning('Impossible to remove Project Path: {}'.format(self.full_path)) return False project_name = self.project_data.name project_path = self.project_data.path if not force: result = qtutils.get_permission( message='Are you sure you want to delete project: "{}"'.format(self.name), title='Deleting Project', cancel=False, parent=self) if not result: return valid_delete = folder.delete_folder(folder_name=project_name, directory=project_path) if valid_delete is None: return False return True def load_project_data(self): """ Return dictionary data contained in the project :return: dict """ if not self.settings: return return self.settings.data() def get_project_nodes(self): """ Returns path where nodes should be stored :return: str """ return [os.path.join(self.full_path, 'nodes'), os.path.join(self.full_path, 'components')] def get_options(self): """ Returns all options contained in the project :return: str """ return self._project_data.get_options() def get_project_image(self): """ Returns the image used by the project :return: QPixmap """ return self._project_data.get_project_image() # ============================================================================================================ # CALLBACKS # ============================================================================================================ def _on_open_project(self): """ Internal callback function that is called when a project is opened """ LOGGER.info('Loading project "{}" ...'.format(self.full_path)) self.projectOpened.emit(self) def _on_remove_project(self): """ Internal callback function that is called when a project is removed """ valid_remove = self.remove() if valid_remove: self.projectRemoved.emit(self.name) def _on_open_in_browser(self): """ Internal callback function that is called when a project is browsed """ fileio.open_browser(self.full_path) def _on_set_project_image(self): """ Internal callback function that is called when project image is set """ image_file = dcc.select_file_dialog( title='Select Project Image File', pattern="PNG Files (*.png)") if image_file is None or not path.is_file(image_file): LOGGER.warning('Selected Image "{}" is not valid!'.format(image_file)) return valid_change = self._project_data.set_project_image(image_file) if valid_change: project_image = self._project_data.settings.get('image') if project_image: self.set_image(project_image) self.projectImageChanged.emit(image_file)
class SaveWidget(BaseSaveWidget, object): def __init__(self, item, settings, temp_path=None, parent=None): self._script_job = None self._sequence_path = None self._icon_path = '' super(SaveWidget, self).__init__(item=item, settings=settings, temp_path=temp_path, parent=parent) self.create_sequence_widget() self.update_thumbnail_size() try: self._on_selection_changed() # self.set_script_job_enabled(True) except NameError as e: LOGGER.error('{} | {}'.format(e, traceback.format_exc())) def ui(self): super(SaveWidget, self).ui() model_panel_layout = layouts.HorizontalLayout() model_panel_layout.setContentsMargins(0, 0, 0, 0) model_panel_layout.setSpacing(0) thumbnail_layout = layouts.VerticalLayout() thumbnail_layout.setContentsMargins(0, 0, 0, 0) thumbnail_layout.setSpacing(0) self._thumbnail_frame = QFrame() self._thumbnail_frame.setMinimumSize(QSize(50, 50)) self._thumbnail_frame.setMaximumSize(QSize(150, 150)) self._thumbnail_frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self._thumbnail_frame.setFrameShape(QFrame.NoFrame) self._thumbnail_frame.setFrameShadow(QFrame.Plain) self._thumbnail_frame.setLineWidth(0) self._thumbnail_frame.setLayout(thumbnail_layout) model_panel_layout.addWidget(self._thumbnail_frame) self._thumbnail_btn = QPushButton() self._thumbnail_btn.setMinimumSize(QSize(0, 0)) self._thumbnail_btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self._thumbnail_btn.setMaximumSize(QSize(150, 150)) self._thumbnail_btn.setToolTip('Take snapshot') self._thumbnail_btn.setStyleSheet( 'color: rgb(40, 40, 40);border: 0px solid rgb(0, 0, 0, 150);background-color: rgb(254, 255, 230, 200);' ) self._thumbnail_btn.setIcon(resources.icon('thumbnail')) self._thumbnail_btn.setToolTip(""" Click to capture a thumbnail from the current viewport.\n CTRL + Click to show the capture window for better framing """) thumbnail_layout.addWidget(self._thumbnail_btn) self._extra_layout.addLayout(model_panel_layout) def setup_signals(self): super(SaveWidget, self).setup_signals() self._thumbnail_btn.clicked.connect(self._on_thumbnail_capture) def resizeEvent(self, event): """ Overrides base QWidget resizeEvent function :param event: QResizeEvent """ self.update_thumbnail_size() def icon_path(self): """ Returns the icon path to be used for the thumbnail :return: str """ return self._icon_path def set_icon(self, icon): """ Sets the icon for the create widget thumbnail :param icon: QIcon """ self._thumbnail_btn.setIcon(icon) self._thumbnail_btn.setIconSize(QSize(200, 200)) self._thumbnail_btn.setText('') def sequence_path(self): """ Returns the playblast path :return: str """ return self._sequence_path def set_sequence_path(self, path): """ Sets the disk location for the image sequence to be saved :param path: str """ self._sequence_path = path self._thumbnail_btn.set_dirname(os.path.dirname(path)) def create_sequence_widget(self): """ Creates a sequence widget to replace the static thumbnail widget """ sequence_widget = widgets.LibraryImageSequenceWidget(self) sequence_widget.setObjectName('thumbnailButton') sequence_widget.setStyleSheet(self._thumbnail_btn.styleSheet()) sequence_widget.setToolTip(self._thumbnail_btn.toolTip()) camera_icon = resources.get('icons', 'camera.svg') expand_icon = resources.get('icons', 'expand.svg') folder_icon = resources.get('icons', 'folder.svg') sequence_widget.addAction(camera_icon, 'Capture new image', 'Capture new image', self._on_thumbnail_capture) sequence_widget.addAction(expand_icon, 'Show Capture window', 'Show Capture window', self._on_show_capture_window) sequence_widget.addAction(folder_icon, 'Load image from disk', 'Load image from disk', self._on_show_browse_image_dialog) sequence_widget.setIcon(resources.icon('thumbnail2')) self._thumbnail_frame.layout().insertWidget(0, sequence_widget) self._thumbnail_btn.hide() self._thumbnail_btn = sequence_widget self._thumbnail_btn.clicked.connect(self._on_thumbnail_capture) def set_sequence(self, source): """ Sets the sequenced path for the thumbnail widget :param source: str """ self.set_thumbnail(source, sequence=True) def set_thumbnail(self, source, sequence=False): """ Sets the thumbnail :param source: str :param sequence: bool """ source = os.path.normpath(source) # TODO: Find a way to remove temp folder afteer saving the file # filename, extension = os.path.splitext(source) # with path_utils.temp_dir() as dir_path: # dir_path = path_utils.temp_dir() # target = os.path.join(dir_path, 'thumbnail{}'.format(extension)) # shutil.copyfile(source, target) # tpQtLib.logger.debug('Source Thumbnail: {}'.format(source)) # tpQtLib.logger.debug('Target Thumbnail: {}'.format(target)) # self._icon_path = target # self._thumbnail_btn.set_path(target) self._icon_path = source self._thumbnail_btn.set_path(source) if sequence: self.set_sequence_path(source) def update_thumbnail_size(self): """ Updates the thumbnail button to teh size of the widget """ width = self.width() - 10 if width > 250: width = 250 size = QSize(width, width) self._thumbnail_btn.setIconSize(size) self._thumbnail_btn.setMaximumSize(size) self._thumbnail_frame.setMaximumSize(size) def show_by_frame_dialog(self): """ Show the by frame dialog """ help_text = """ To help speed up the playblast you can set the "by frame" to another greather than 1. For example if the "by frame" is set to 2 it will playblast every second frame """ options = self._options_widget.values() by_frame = options.get('byFrame', 1) start_frame, end_frame = options.get('frameRange', [None, None]) duration = 1 if start_frame is not None and end_frame is not None: duration = end_frame - start_frame if duration > 100 and by_frame == 1: buttons = QDialogButtonBox.Ok | QDialogButtonBox.Cancel result = messagebox.MessageBox.question( self.library_window(), title='Tip', text=help_text, buttons=buttons, enable_dont_show_checkbox=True) if result != QDialogButtonBox.Ok: raise Exception('Cancelled by user') def show_thumbnail_capture_dialog(self): """ Asks the user if they would like to capture a thumbnail :return: int """ buttons = QDialogButtonBox.Yes | QDialogButtonBox.Ignore | QDialogButtonBox.Cancel parent = self.item().library_window() btn = messagebox.MessageBox.question( None, 'Create a thumbnail', 'Would you like to capture a thumbnail?', buttons=buttons) if btn == QDialogButtonBox.Yes: self.thumbnail_capture() return btn def thumbnail_capture(self, show=False): """ Captures a playblast and saves it to the temporal thumbnail path :param show: bool """ options = self._options_widget.values() start_frame, end_frame = options.get('frameRange', [None, None]) step = options.get('byFrame', 1) if not qtutils.is_control_modifier(): self.show_by_frame_dialog() if not self._temp_path or not os.path.isdir(self._temp_path): self._temp_path = tempfile.mkdtemp() self._temp_path = os.path.join(self._temp_path, 'thumbnail.jpg') try: snapshot.SnapshotWindow(path=self._temp_path, on_save=self._on_thumbnail_captured) # thumbnail.ThumbnailCaptureDialog.thumbnail_capture( # path=self._temp_path, # show=show, # start_frame=start_frame, # end_frame=end_frame, # step=step, # clear_cache=False, # captured=self._on_thumbnail_captured # ) except Exception as e: messagebox.MessageBox.critical(self.library_window(), 'Error while capturing thumbnail', str(e)) LOGGER.error(traceback.format_exc()) def save(self, path, icon_path, objects=None): """ Saves the item with the given objects to the given disk location path :param path: list(str) :param icon_path: str :param objects: str """ item = self.item() options = self._options_widget.values() sequence_path = self.sequence_path() if sequence_path: sequence_path = os.path.dirname(sequence_path) item.save(path=path, objects=objects, icon_path=icon_path, sequence_path=sequence_path, **options) self.close() def _on_selection_changed(self): """ Internal callback functino that is called when DCC selection changes """ if self._options_widget: self._options_widget.validate() def _on_thumbnail_capture(self): self.thumbnail_capture(show=False) def _on_thumbnail_captured(self, captured_path): """ Internal callback function that is called when thumbnail is captured :param captured_path: str """ self.set_sequence(captured_path) def _on_show_capture_window(self): """ Internal callback function that shows the capture window for framing """ self.thumbnail_capture(show=True) def _on_show_browse_image_dialog(self): """ Internal callback function that shows a file dialog for choosing an image from disk """ file_dialog = QFileDialog(self, caption='Open Image', filter='Image Files (*.png *.jpg)') file_dialog.fileSelected.connect(self.set_thumbnail) file_dialog.exec_()