Source code for shotgun_fields.file_link_widget
# Copyright (c) 2016 Shotgun Software Inc.
#
# CONFIDENTIAL AND PROPRIETARY
#
# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit
# Source Code License included in this distribution package. See LICENSE.
# By accessing, using, copying or modifying this work you indicate your
# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights
# not expressly granted therein are reserved by Shotgun Software Inc.
import os
import sgtk
from sgtk.platform.qt import QtCore, QtGui
from tank_vendor import six
from .label_base_widget import ElidedLabelBaseWidget
from .shotgun_field_meta import ShotgunFieldMeta
# ensures access to `link_menu.png`
from .ui import resources_rc
[docs]@six.add_metaclass(ShotgunFieldMeta)
class FileLinkWidget(ElidedLabelBaseWidget):
"""
Display a ``url`` field value as returned by the Shotgun API.
The ``FileLinkWidget`` represents both the ``DISPLAY`` and ``EDITOR`` widget type.
"""
_DISPLAY_TYPE = "url"
_EDITOR_TYPE = "url"
[docs] def enable_editing(self, enable):
"""
Enable or disable editing of the widget.
This is provided as required for widgets that are used as both editor
and display.
:param bool enable: ``True`` to enable, ``False`` to disable
"""
self._editable = enable
self._update_btn_position()
def eventFilter(self, obj, event):
"""
Filters out mouse enter/leave events in order to show/hide the edit
menu when the widget is editable.
:param obj: The watched object.
:type obj: :class:`~PySide.QtGui.QObject`
:param event: The filtered event.
:type event: :class:`~PySide.QtGui.QEvent`
:return: True if the event was processed, False otherwise
"""
if obj == self and self._editable:
if event.type() == QtCore.QEvent.Enter:
self._update_btn_position()
self._popup_btn.show()
elif event.type() == QtCore.QEvent.Leave:
self._popup_btn.hide()
return False
[docs] def setup_widget(self):
"""
Prepare the widget for display.
Called by the metaclass during initialization.
"""
self._editable = False
self._popup_btn = QtGui.QPushButton(self)
self._popup_btn.setIcon(QtGui.QIcon(":/qtwidgets-shotgun-fields/link_menu.png"))
self._popup_btn.setFixedSize(QtCore.QSize(18, 12))
self._popup_btn.hide()
if not self._delegate:
# not sure why, but when the widget is being used in a delegate,
# this causes editor to close immediately when clicked.
self._popup_btn.setFocusPolicy(QtCore.Qt.NoFocus)
# make sure there's never a bg color or border
self._popup_btn.setStyleSheet("background-color: none; border: none;")
# ---- actions
# each of the different types use the same callback, but they display
# different text depending on the current value
# upload
self._upload_file_action = QtGui.QAction("Upload File", self)
self._upload_file_action.triggered.connect(self._upload_file)
self._edit_upload_file_action = QtGui.QAction("Upload New File", self)
self._edit_upload_file_action.triggered.connect(self._upload_file)
self._replace_with_upload_file_action = QtGui.QAction(
"Replace with Uploaded File", self
)
self._replace_with_upload_file_action.triggered.connect(self._upload_file)
# web
self._web_page_link_action = QtGui.QAction("Link to Web Page", self)
self._web_page_link_action.triggered.connect(self._edit_link)
self._edit_web_page_link_action = QtGui.QAction("Edit Web Page Link", self)
self._edit_web_page_link_action.triggered.connect(self._edit_link)
self._replace_with_web_page_link_action = QtGui.QAction(
"Replace with Web Page Link", self
)
self._replace_with_web_page_link_action.triggered.connect(self._edit_link)
# local
self._local_path_action = QtGui.QAction("Link to Local File or Directory", self)
self._local_path_action.triggered.connect(self._browse_local)
self._edit_local_path_action = QtGui.QAction(
"Edit Local File or Directory", self
)
self._edit_local_path_action.triggered.connect(self._browse_local)
self._replace_with_local_path_action = QtGui.QAction(
"Replace with Local File or Directory", self
)
self._replace_with_local_path_action.triggered.connect(self._browse_local)
# remove
self._remove_link_action = QtGui.QAction("Remove File/Link", self)
self._remove_link_action.triggered.connect(self._remove_link)
self.installEventFilter(self)
self._display_default()
self._update_btn_position()
# ---- connect signals
self._popup_btn.clicked.connect(self._on_popup_btn_click)
self.linkActivated.connect(self._on_link_activated)
def _browse_local(self):
"""
Opens a file browser for choosing a local file for the field.
If a file is selected, this method emits the ``value_changed`` signal
and upates the stored value.
"""
# prompt the user for a file. this always returns a tuple, the second
# value being the path (or None)
file_path = QtGui.QFileDialog.getOpenFileName(
self,
caption="Link to Local File or Directory",
options=QtGui.QFileDialog.DontResolveSymlinks,
)[0]
# make sure we have a valid path
if not file_path or not os.path.exists(file_path):
return
self.clear()
file_path = str(file_path)
# emit the value changed signal. build a fake value for now that
# will presumably be updated via calling code
self._value = {
"name": os.path.split(file_path)[-1],
"link_type": "local",
"url": None,
"local_path": file_path,
}
self._display_value(self._value)
self.value_changed.emit()
def _display_default(self):
"""
Display the default value of the widget.
"""
self.clear()
def _edit_link(self):
"""
Opens a custom dialog for the user to input a url and display name.
"""
url = None
display = None
# get the current url and display if the current link is a web link
if self._value and self._value["link_type"] == "web":
url = self._value.get("url", None)
display = self._value.get("name", None)
# prompt the user
edit_link_dialog = _EditWebLinkDialog(self, url, display)
result = edit_link_dialog.exec_()
if result == QtGui.QDialog.Rejected:
return
self.clear()
# emit the value changed signal. build a fake value for now that
# will presumably be updated via calling code
self._value = {
"name": edit_link_dialog.display,
"link_type": "web",
"url": edit_link_dialog.url,
}
self._display_value(self._value)
self.value_changed.emit()
def _on_link_activated(self, url):
"""
Open the displayed link in an appropriate way.
Called when a user clicks the link.
:param url: The url for the clicked link.
"""
if self._value:
link_type = self._value["link_type"]
else:
link_type = None
if self._value.get("link_type") == "upload":
# the uploaded urls have timeouts built in. query the current
# value before continuing
sg = self._bundle.shotgun
result = sg.find_one(
self._entity["type"],
[["id", "is", self._entity["id"]]],
[self._field_name],
)
if not result:
return
url = result[self._field_name]["url"]
# PTR returns an encoded url already. make sure it doesn't get
# a second encoding pass (the default translation from python str
# to QUrl seems to assume the str is not encoded).
url = QtCore.QUrl.fromEncoded(url)
# display the url appropriately
if url:
QtGui.QDesktopServices.openUrl(url)
def _on_popup_btn_click(self):
"""
Display a context menu based on the current field value.
"""
popup_menu = QtGui.QMenu()
if self._value:
link_type = self._value["link_type"]
else:
link_type = None
if not link_type:
# no link type, display options for each type
popup_menu.addAction(self._upload_file_action)
popup_menu.addAction(self._web_page_link_action)
popup_menu.addAction(self._local_path_action)
elif link_type == "upload":
popup_menu.addAction(self._edit_upload_file_action)
popup_menu.addAction(self._replace_with_web_page_link_action)
popup_menu.addAction(self._replace_with_local_path_action)
popup_menu.addAction(self._remove_link_action)
elif link_type == "web":
popup_menu.addAction(self._replace_with_upload_file_action)
popup_menu.addAction(self._edit_web_page_link_action)
popup_menu.addAction(self._replace_with_local_path_action)
popup_menu.addAction(self._remove_link_action)
elif link_type == "local":
popup_menu.addAction(self._replace_with_upload_file_action)
popup_menu.addAction(self._replace_with_web_page_link_action)
popup_menu.addAction(self._edit_local_path_action)
popup_menu.addAction(self._remove_link_action)
# show the menu below the button
popup_menu.exec_(
self._popup_btn.mapToGlobal(QtCore.QPoint(0, self._popup_btn.height()))
)
def _remove_link(self):
"""
Called when user selects the menu option to clear the value.
"""
# clear the value of the field, emit the ``value_changed`` signal.
self.clear()
self._value = {"name": None, "link_type": None, "url": None}
self._display_value(self._value)
self.value_changed.emit()
def _string_value(self, value):
"""
Convert the Shotgun value for this field into a string
:param value: The value to convert into a string
:type value: A dictionary as returned by the Shotgun API for a url field
"""
utils = self._bundle.import_module("utils")
if value["link_type"] in ["web", "upload"]:
url = value["url"]
img_src = ":/qtwidgets-shotgun-fields/link_%s.png" % (value["link_type"],)
hyperlink = utils.get_hyperlink_html(url, value.get("name", url))
str_val = "<span><img src='%s'> %s</span>" % (img_src, hyperlink)
elif value["link_type"] == "local":
local_path = value["local_path"]
# for file on OS that differs from the current OS, this will
# result in the display of the full path rather than just the
# file basename.ext (the PTR behavior).
file_name = os.path.split(local_path)[-1]
img_src = ":/qtwidgets-shotgun-fields/link_%s.png" % (value["link_type"],)
hyperlink = utils.get_hyperlink_html(local_path, file_name)
str_val = "<span><img src='%s'> %s</span>" % (img_src, hyperlink)
else:
str_val = ""
return str_val
def _update_btn_position(self):
"""
Ensures the menu button is displayed properly in relation to the label text.
"""
# `line_width` is the elided width of the line in pixels. use this as a
# starting point for where to place the menu button
x = self.line_width + 4
# if the label is too wide, move the button inside the visible rectangle
visible_width = self.visibleRegion().boundingRect().width()
if (x + self._popup_btn.width()) > visible_width:
x = self.visibleRegion().boundingRect().width() - self._popup_btn.width()
self._popup_btn.move(x, -2)
def _upload_file(self):
"""
Opens a file browser for uploading a file for the field.
If a file is selected, this method emits the ``value_changed`` signal
and upates the stored value.
"""
# prompt the user for a file. this always returns a tuple, the second
# value being the path (or None)
file_path = QtGui.QFileDialog.getOpenFileName(
self,
caption="Choose a File to Upload",
options=QtGui.QFileDialog.DontResolveSymlinks,
)[0]
if not file_path or not os.path.exists(file_path):
return
self.clear()
file_path = str(file_path)
# emit the value changed signal. build a fake value for now that
# will presumably be updated via calling code
self._value = {
"name": os.path.split(file_path)[-1],
"link_type": "upload",
"url": file_path,
}
self._display_value(self._value)
self.value_changed.emit()
class _EditWebLinkDialog(QtGui.QDialog):
"""
Class for prompting the user for link url and aname
"""
def __init__(self, parent=None, url=None, display=None):
"""
Initialize the dialog.
:param parent: Optional parent widget
:param url: Optional url to insert it the input
:param display: Optional display name to
:return:
"""
super(_EditWebLinkDialog, self).__init__(parent)
self.setMinimumWidth(350)
url = url
display = display
self.setWindowTitle("Link to Web Page")
# url input
url_lbl = QtGui.QLabel("<h3>Web page address</h3>")
self._url_input = QtGui.QLineEdit()
if url:
self._url_input.setText(url)
self._url_input.selectAll()
# display input
display_lbl = QtGui.QLabel("Optional display name")
self._display_input = QtGui.QLineEdit()
if display:
self._display_input.setText(display)
self._display_input.selectAll()
# get the highlight color
btn_color = sgtk.platform.current_bundle().style_constants["SG_HIGHLIGHT_COLOR"]
btn_palette = self.palette()
btn_palette.setColor(QtGui.QPalette.Button, btn_color)
# color the button to match the PTR version.
self._add_link_btn = QtGui.QPushButton("Add Link")
self._add_link_btn.setEnabled(False)
self._add_link_btn.setPalette(btn_palette)
cancel_btn = QtGui.QPushButton("Cancel")
# put the buttons together. Didn't user QDialogButtonBox since can't
# control the order
btn_box = QtGui.QHBoxLayout()
btn_box.addStretch()
btn_box.addWidget(cancel_btn)
btn_box.addWidget(self._add_link_btn)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(url_lbl)
layout.addWidget(self._url_input)
layout.addWidget(display_lbl)
layout.addWidget(self._display_input)
layout.addLayout(btn_box)
# signals
self._url_input.textChanged.connect(self._check_url)
self._add_link_btn.clicked.connect(self.accept)
cancel_btn.clicked.connect(self.reject)
def _check_url(self, text):
"""
Enable add link button if the url is valid, disable otherwise
:param text: The typed text
"""
url = QtCore.QUrl(text)
self._add_link_btn.setEnabled(url.isValid())
@property
def url(self):
""":obj:`str` url entered by the user."""
return self._url_input.text()
@property
def display(self):
""":obj:`str` display name entered by the user."""
return self._display_input.text()