193 lines
6.3 KiB
Python
193 lines
6.3 KiB
Python
|
import logging
|
||
|
import os.path
|
||
|
|
||
|
import bpy
|
||
|
import bgl
|
||
|
|
||
|
import pillarsdk
|
||
|
from . import nodes
|
||
|
|
||
|
if bpy.app.version < (2, 80):
|
||
|
from . import draw_27 as draw
|
||
|
else:
|
||
|
from . import draw
|
||
|
|
||
|
|
||
|
library_icons_path = os.path.join(os.path.dirname(__file__), "icons")
|
||
|
|
||
|
ICON_WIDTH = 128
|
||
|
ICON_HEIGHT = 128
|
||
|
|
||
|
|
||
|
class MenuItem:
|
||
|
"""GUI menu item for the 3D View GUI."""
|
||
|
|
||
|
icon_margin_x = 4
|
||
|
icon_margin_y = 4
|
||
|
text_margin_x = 6
|
||
|
|
||
|
text_size = 12
|
||
|
text_size_small = 10
|
||
|
|
||
|
DEFAULT_ICONS = {
|
||
|
'FOLDER': os.path.join(library_icons_path, 'folder.png'),
|
||
|
'SPINNER': os.path.join(library_icons_path, 'spinner.png'),
|
||
|
'ERROR': os.path.join(library_icons_path, 'error.png'),
|
||
|
}
|
||
|
|
||
|
FOLDER_NODE_TYPES = {'group_texture', 'group_hdri',
|
||
|
nodes.UpNode.NODE_TYPE, nodes.ProjectNode.NODE_TYPE}
|
||
|
SUPPORTED_NODE_TYPES = {'texture', 'hdri'}.union(FOLDER_NODE_TYPES)
|
||
|
|
||
|
def __init__(self, node, file_desc, thumb_path: str, label_text):
|
||
|
self.log = logging.getLogger('%s.MenuItem' % __name__)
|
||
|
if node['node_type'] not in self.SUPPORTED_NODE_TYPES:
|
||
|
self.log.info('Invalid node type in node: %s', node)
|
||
|
raise TypeError('Node of type %r not supported; supported are %r.' % (
|
||
|
node['node_type'], self.SUPPORTED_NODE_TYPES))
|
||
|
|
||
|
assert isinstance(node, pillarsdk.Node), 'wrong type for node: %r' % type(node)
|
||
|
assert isinstance(node['_id'], str), 'wrong type for node["_id"]: %r' % type(node['_id'])
|
||
|
self.node = node # pillarsdk.Node, contains 'node_type' key to indicate type
|
||
|
self.file_desc = file_desc # pillarsdk.File object, or None if a 'folder' node.
|
||
|
self.label_text = label_text
|
||
|
self.small_text = self._small_text_from_node()
|
||
|
self._thumb_path = ''
|
||
|
self.icon = None
|
||
|
self._is_folder = node['node_type'] in self.FOLDER_NODE_TYPES
|
||
|
self._is_spinning = False
|
||
|
|
||
|
# Determine sorting order.
|
||
|
# by default, sort all the way at the end and folders first.
|
||
|
self._order = 0 if self._is_folder else 10000
|
||
|
if node and node.properties and node.properties.order is not None:
|
||
|
self._order = node.properties.order
|
||
|
|
||
|
self.thumb_path = thumb_path
|
||
|
|
||
|
# Updated when drawing the image
|
||
|
self.x = 0
|
||
|
self.y = 0
|
||
|
self.width = 0
|
||
|
self.height = 0
|
||
|
|
||
|
def _small_text_from_node(self) -> str:
|
||
|
"""Return the components of the texture (i.e. which map types are available)."""
|
||
|
|
||
|
if not self.node:
|
||
|
return ''
|
||
|
|
||
|
try:
|
||
|
node_files = self.node.properties.files
|
||
|
except AttributeError:
|
||
|
# Happens for nodes that don't have .properties.files.
|
||
|
return ''
|
||
|
if not node_files:
|
||
|
return ''
|
||
|
|
||
|
map_types = {f.map_type for f in node_files if f.map_type}
|
||
|
map_types.discard('color') # all textures have colour
|
||
|
if not map_types:
|
||
|
return ''
|
||
|
return ', '.join(sorted(map_types))
|
||
|
|
||
|
def sort_key(self):
|
||
|
"""Key for sorting lists of MenuItems."""
|
||
|
return self._order, self.label_text
|
||
|
|
||
|
@property
|
||
|
def thumb_path(self) -> str:
|
||
|
return self._thumb_path
|
||
|
|
||
|
@thumb_path.setter
|
||
|
def thumb_path(self, new_thumb_path: str):
|
||
|
self._is_spinning = new_thumb_path == 'SPINNER'
|
||
|
|
||
|
self._thumb_path = self.DEFAULT_ICONS.get(new_thumb_path, new_thumb_path)
|
||
|
if self._thumb_path:
|
||
|
self.icon = bpy.data.images.load(filepath=self._thumb_path)
|
||
|
else:
|
||
|
self.icon = None
|
||
|
|
||
|
@property
|
||
|
def node_uuid(self) -> str:
|
||
|
return self.node['_id']
|
||
|
|
||
|
def represents(self, node) -> bool:
|
||
|
"""Returns True iff this MenuItem represents the given node."""
|
||
|
|
||
|
node_uuid = node['_id']
|
||
|
return self.node_uuid == node_uuid
|
||
|
|
||
|
def update(self, node, file_desc, thumb_path: str, label_text=None):
|
||
|
# We can get updated information about our Node, but a MenuItem should
|
||
|
# always represent one node, and it shouldn't be shared between nodes.
|
||
|
if self.node_uuid != node['_id']:
|
||
|
raise ValueError("Don't change the node ID this MenuItem reflects, "
|
||
|
"just create a new one.")
|
||
|
self.node = node
|
||
|
self.file_desc = file_desc # pillarsdk.File object, or None if a 'folder' node.
|
||
|
self.thumb_path = thumb_path
|
||
|
|
||
|
if label_text is not None:
|
||
|
self.label_text = label_text
|
||
|
|
||
|
if thumb_path == 'ERROR':
|
||
|
self.small_text = 'This open is broken'
|
||
|
else:
|
||
|
self.small_text = self._small_text_from_node()
|
||
|
|
||
|
@property
|
||
|
def is_folder(self) -> bool:
|
||
|
return self._is_folder
|
||
|
|
||
|
@property
|
||
|
def is_spinning(self) -> bool:
|
||
|
return self._is_spinning
|
||
|
|
||
|
def update_placement(self, x, y, width, height):
|
||
|
"""Use OpenGL to draw this one menu item."""
|
||
|
|
||
|
self.x = x
|
||
|
self.y = y
|
||
|
self.width = width
|
||
|
self.height = height
|
||
|
|
||
|
def draw(self, highlighted: bool):
|
||
|
bgl.glEnable(bgl.GL_BLEND)
|
||
|
if highlighted:
|
||
|
color = (0.555, 0.555, 0.555, 0.8)
|
||
|
else:
|
||
|
color = (0.447, 0.447, 0.447, 0.8)
|
||
|
|
||
|
draw.aabox((self.x, self.y), (self.x + self.width, self.y + self.height), color)
|
||
|
|
||
|
texture = self.icon
|
||
|
if texture:
|
||
|
err = texture.gl_load(filter=bgl.GL_NEAREST, mag=bgl.GL_NEAREST)
|
||
|
assert not err, 'OpenGL error: %i' % err
|
||
|
|
||
|
# ------ TEXTURE ---------#
|
||
|
if texture:
|
||
|
draw.bind_texture(texture)
|
||
|
bgl.glBlendFunc(bgl.GL_SRC_ALPHA, bgl.GL_ONE_MINUS_SRC_ALPHA)
|
||
|
|
||
|
draw.aabox_with_texture(
|
||
|
(self.x + self.icon_margin_x, self.y),
|
||
|
(self.x + self.icon_margin_x + ICON_WIDTH, self.y + ICON_HEIGHT),
|
||
|
)
|
||
|
bgl.glDisable(bgl.GL_BLEND)
|
||
|
|
||
|
if texture:
|
||
|
texture.gl_free()
|
||
|
|
||
|
# draw some text
|
||
|
text_x = self.x + self.icon_margin_x + ICON_WIDTH + self.text_margin_x
|
||
|
text_y = self.y + ICON_HEIGHT * 0.5 - 0.25 * self.text_size
|
||
|
draw.text((text_x, text_y), self.label_text, fsize=self.text_size)
|
||
|
draw.text((text_x, self.y + 0.5 * self.text_size_small), self.small_text,
|
||
|
fsize=self.text_size_small, rgba=(1.0, 1.0, 1.0, 0.5))
|
||
|
|
||
|
def hits(self, mouse_x: int, mouse_y: int) -> bool:
|
||
|
return self.x < mouse_x < self.x + self.width and self.y < mouse_y < self.y + self.height
|