diff options
Diffstat (limited to 'mvc/widgets')
54 files changed, 0 insertions, 16240 deletions
diff --git a/mvc/widgets/__init__.py b/mvc/widgets/__init__.py deleted file mode 100644 index 23a6edc..0000000 --- a/mvc/widgets/__init__.py +++ /dev/null @@ -1,30 +0,0 @@ -import logging -import os -import sys - -if sys.platform == 'darwin': - import osx as plat - from .osx import widgetset -else: - import gtk as plat - from .gtk import widgetset - -attach_menubar = plat.attach_menubar -mainloop_start = plat.mainloop_start -mainloop_stop = plat.mainloop_stop -idle_add = plat.idle_add -idle_remove = plat.idle_remove -reveal_file = plat.reveal_file -get_conversion_directory = plat.get_conversion_directory - -def get_conversion_directory(): - return os.path.join(plat.get_conversion_directory(), 'Libre Video Converter') - """directorio donde se guardan los videos convertidos""" - -def initialize(app): - try: - os.makedirs(get_conversion_directory()) - except EnvironmentError, e: - logging.info('os.makedirs: %s', str(e)) - if app: - plat.initialize(app) diff --git a/mvc/widgets/app.py b/mvc/widgets/app.py deleted file mode 100644 index 531b745..0000000 --- a/mvc/widgets/app.py +++ /dev/null @@ -1,4 +0,0 @@ -# app.py - -widgetapp = None - diff --git a/mvc/widgets/cellpack.py b/mvc/widgets/cellpack.py deleted file mode 100644 index 1347f56..0000000 --- a/mvc/widgets/cellpack.py +++ /dev/null @@ -1,843 +0,0 @@ -"""``miro.frontends.widgets.cellpack`` -- Code to layout -CustomTableCells. - -We use the hbox/vbox model to lay things out with a couple changes. -The main difference here is that layouts are one-shot. We don't keep -state around inside the cell renderers, so we just set up the objects -at the start, then use them to calculate info. -""" - -class Margin(object): - """Helper object used to calculate margins. - """ - def __init__(self , margin): - if margin is None: - margin = (0, 0, 0, 0) - self.margin_left = margin[3] - self.margin_top = margin[0] - self.margin_width = margin[1] + margin[3] - self.margin_height = margin[0] + margin[2] - - def inner_rect(self, x, y, width, height): - """Returns the x, y, width, height of the inner - box. - """ - return (x + self.margin_left, - y + self.margin_top, - width - self.margin_width, - height - self.margin_height) - - def outer_size(self, inner_size): - """Returns the width, height of the outer box. - """ - return (inner_size[0] + self.margin_width, - inner_size[1] + self.margin_height) - - def point_in_margin(self, x, y, width, height): - """Returns whether a given point is inside of the - margins. - """ - return ((0 <= x - self.margin_left < width - self.margin_width) and - (0 <= y - self.margin_top < height - self.margin_height)) - -class Packing(object): - """Helper object used to layout Boxes. - """ - def __init__(self, child, expand): - self.child = child - self.expand = expand - - def calc_size(self, translate_func): - return translate_func(*self.child.get_size()) - - def draw(self, context, x, y, width, height): - self.child.draw(context, x, y, width, height) - -class WhitespacePacking(object): - """Helper object used to layout Boxes. - """ - def __init__(self, size, expand): - self.size = size - self.expand = expand - - def calc_size(self, translate_func): - return self.size, 0 - - def draw(self, context, x, y, width, height): - pass - -class Packer(object): - """Base class packing objects. Packer objects work similarly to widgets, - but they only used in custom cell renderers so there's a couple - differences. The main difference is that cell renderers don't keep state - around. Therefore Packers just get set up, used, then discarded. - Also Packers can't receive events directly, so they have a different - system to figure out where mouse clicks happened (the Hotspot class). - """ - - def render_layout(self, context): - """position the child elements then call draw() on them.""" - self._layout(context, 0, 0, context.width, context.height) - - def draw(self, context, x, y, width, height): - """Included so that Packer objects have a draw() method that matches - ImageSurfaces, TextBoxes, etc. - """ - self._layout(context, x, y, width, height) - - def _find_child_at(self, x, y, width, height): - raise NotImplementedError() - - def get_size(self): - """Get the minimum size required to hold the Packer. """ - try: - return self._size - except AttributeError: - self._size = self._calc_size() - return self._size - - def get_current_size(self): - """Get the minimum size required to hold the Packer at this point - - Call this method if you are going to change the packer after the call, - for example if you have more children to pack into a box. get_size() - saves caches it's result which is can mess things up. - """ - return self._calc_size() - - def find_hotspot(self, x, y, width, height): - """Find the hotspot at (x, y). width and height are the size of the - cell this Packer is rendering. - - If a hotspot is found, return the tuple (name, x, y, width, height) - where name is the name of the hotspot, x, y is the position relative - to the top-left of the hotspot area and width, height are the - dimensions of the hotspot. - - If no Hotspot is found return None. - """ - child_pos = self._find_child_at(x, y, width, height) - if child_pos: - child, child_x, child_y, child_width, child_height = child_pos - try: - return child.find_hotspot(x - child_x, y - child_y, - child_width, child_height) - except AttributeError: - pass # child is a TextBox, Button or something like that - return None - - def _layout(self, context, x, y, width, height): - """Layout our children and call ``draw()`` on them. - """ - raise NotImplementedError() - - def _calc_size(self): - """Calculate the size needed to hold the box. The return value gets - cached and return in ``get_size()``. - """ - raise NotImplementedError() - -class Box(Packer): - """Box is the base class for VBox and HBox. Box objects lay out children - linearly either left to right or top to bottom. - """ - - def __init__(self, spacing=0): - """Create a new Box. spacing is the amount of space to place - in-between children. - """ - self.spacing = spacing - self.children = [] - self.children_end = [] - self.expand_count = 0 - - def pack(self, child, expand=False): - """Add a new child to the box. The child will be placed after all the - children packed before with pack_start. - - :param child: child to pack. It can be anything with a - ``get_size()`` method, including TextBoxes, - ImageSurfarces, Buttons, Boxes and Backgrounds. - :param expand: If True, then the child will enlarge if space - available is more than the space required. - """ - if not (hasattr(child, 'draw') and hasattr(child, 'get_size')): - raise TypeError("%s can't be drawn" % child) - self.children.append(Packing(child, expand)) - if expand: - self.expand_count += 1 - - def pack_end(self, child, expand=False): - """Add a new child to the end box. The child will be placed before - all the children packed before with pack_end. - - :param child: child to pack. It can be anything with a - ``get_size()`` method, including TextBoxes, - ImageSurfarces, Buttons, Boxes and Backgrounds. - :param expand: If True, then the child will enlarge if space - available is more than the space required. - """ - if not (hasattr(child, 'draw') and hasattr(child, 'get_size')): - raise TypeError("%s can't be drawn" % child) - self.children_end.append(Packing(child, expand)) - if expand: - self.expand_count += 1 - - def pack_space(self, size, expand=False): - """Pack whitespace into the box. - """ - self.children.append(WhitespacePacking(size, expand)) - if expand: - self.expand_count += 1 - - def pack_space_end(self, size, expand=False): - """Pack whitespace into the end of box. - """ - self.children_end.append(WhitespacePacking(size, expand)) - if expand: - self.expand_count += 1 - - def _calc_size(self): - length = 0 - breadth = 0 - for packing in self.children + self.children_end: - child_length, child_breadth = packing.calc_size(self._translate) - length += child_length - breadth = max(breadth, child_breadth) - total_children = len(self.children) + len(self.children_end) - length += self.spacing * (total_children - 1) - return self._translate(length, breadth) - - def _extra_space_iter(self, total_extra_space): - """Generate the amount of extra space for children with expand set.""" - if total_extra_space <= 0: - while True: - yield 0 - average_extra_space, leftover = \ - divmod(total_extra_space, self.expand_count) - while leftover > 1: - # expand_count doesn't divide equally into total_extra_space, - # yield average_extra_space+1 for each extra pixel - yield average_extra_space + 1 - leftover -= 1 - # if there's a fraction of a pixel leftover, add that in - yield average_extra_space + leftover - while True: - # no more leftover space - yield average_extra_space - - def _position_children(self, total_length): - my_length, my_breadth = self._translate(*self.get_size()) - extra_space_iter = self._extra_space_iter(total_length - my_length) - - pos = 0 - for packing in self.children: - child_length, child_breadth = packing.calc_size(self._translate) - if packing.expand: - child_length += extra_space_iter.next() - yield packing, pos, child_length - pos += child_length + self.spacing - - pos = total_length - for packing in self.children_end: - child_length, child_breadth = packing.calc_size(self._translate) - if packing.expand: - child_length += extra_space_iter.next() - pos -= child_length - yield packing, pos, child_length - pos -= self.spacing - - def _layout(self, context, x, y, width, height): - total_length, total_breadth = self._translate(width, height) - pos, offset = self._translate(x, y) - position_iter = self._position_children(total_length) - for packing, child_pos, child_length in position_iter: - x, y = self._translate(pos + child_pos, offset) - width, height = self._translate(child_length, total_breadth) - packing.draw(context, x, y, width, height) - - def _find_child_at(self, x, y, width, height): - total_length, total_breadth = self._translate(width, height) - pos, offset = self._translate(x, y) - position_iter = self._position_children(total_length) - for packing, child_pos, child_length in position_iter: - if child_pos <= pos < child_pos + child_length: - x, y = self._translate(child_pos, 0) - width, height = self._translate(child_length, total_breadth) - if isinstance(packing, WhitespacePacking): - return None - return packing.child, x, y, width, height - elif child_pos > pos: - break - return None - - def _translate(self, x, y): - """Translate (x, y) coordinates into (length, breadth) and - vice-versa. - """ - raise NotImplementedError() - -class HBox(Box): - def _translate(self, x, y): - return x, y - -class VBox(Box): - def _translate(self, x, y): - return y, x - -class Table(Packer): - def __init__(self, row_length=1, col_length=1, - row_spacing=0, col_spacing=0): - """Create a new Table. - - :param row_length: how many rows long this should be - :param col_length: how many rows wide this should be - :param row_spacing: amount of spacing (in pixels) between rows - :param col_spacing: amount of spacing (in pixels) between columns - """ - assert min(row_length, col_length) > 0 - assert isinstance(row_length, int) and isinstance(col_length, int) - self.row_length = row_length - self.col_length = col_length - self.row_spacing = row_spacing - self.col_spacing = col_spacing - self.table_multiarray = self._generate_table_multiarray() - - def _generate_table_multiarray(self): - table_multiarray = [] - table_multiarray = [ - [None for col in range(self.col_length)] - for row in range(self.row_length)] - return table_multiarray - - def pack(self, child, row, column, expand=False): - # TODO: flesh out "expand" ability, maybe? - # - # possibly throw a special exception if outside the range. - # For now, just allowing an IndexError to be thrown. - self.table_multiarray[row][column] = Packing(child, expand) - - def _get_grid_sizes(self): - """Get the width and eights for both rows and columns - """ - row_sizes = {} - col_sizes = {} - for row_count, row in enumerate(self.table_multiarray): - row_sizes.setdefault(row_count, 0) - for col_count, col_packing in enumerate(row): - col_sizes.setdefault(col_count, 0) - if col_packing: - x, y = col_packing.calc_size(self._translate) - if y > row_sizes[row_count]: - row_sizes[row_count] = y - if x > col_sizes[col_count]: - col_sizes[col_count] = x - return col_sizes, row_sizes - - def _find_child_at(self, x, y, width, height): - col_sizes, row_sizes = self._get_grid_sizes() - row_distance = 0 - for row_count, row in enumerate(self.table_multiarray): - col_distance = 0 - for col_count, packing in enumerate(row): - child_width, child_height = packing.calc_size(self._translate) - if packing.child: - if (col_distance <= x < col_distance + child_width - and row_distance <= y < row_distance + child_height): - return (packing.child, - col_distance, row_distance, - child_width, child_height) - col_distance += col_sizes[col_count] + self.col_spacing - row_distance += row_sizes[row_count] + self.row_spacing - - def _calc_size(self): - col_sizes, row_sizes = self._get_grid_sizes() - x = sum(col_sizes.values()) + ( - (self.col_length - 1) * self.col_spacing) - y = sum(row_sizes.values()) + ( - (self.row_length - 1) * self.row_spacing) - return x, y - - def _layout(self, context, x, y, width, height): - col_sizes, row_sizes = self._get_grid_sizes() - - row_distance = 0 - for row_count, row in enumerate(self.table_multiarray): - col_distance = 0 - for col_count, packing in enumerate(row): - if packing: - child_width, child_height = packing.calc_size( - self._translate) - packing.child.draw(context, - x + col_distance, y + row_distance, - child_width, child_height) - col_distance += col_sizes[col_count] + self.col_spacing - row_distance += row_sizes[row_count] + self.row_spacing - - def _translate(self, x, y): - return x, y - - -class Alignment(Packer): - """Positions a child inside a larger space. - """ - def __init__(self, child, xscale=1.0, yscale=1.0, xalign=0.0, yalign=0.0, - min_width=0, min_height=0): - self.child = child - self.xscale = xscale - self.yscale = yscale - self.xalign = xalign - self.yalign = yalign - self.min_width = min_width - self.min_height = min_height - - def _calc_size(self): - width, height = self.child.get_size() - return max(self.min_width, width), max(self.min_height, height) - - def _calc_child_position(self, width, height): - req_width, req_height = self.child.get_size() - child_width = req_width + self.xscale * (width-req_width) - child_height = req_height + self.yscale * (height-req_height) - child_x = round(self.xalign * (width - child_width)) - child_y = round(self.yalign * (height - child_height)) - return child_x, child_y, child_width, child_height - - def _layout(self, context, x, y, width, height): - child_x, child_y, child_width, child_height = \ - self._calc_child_position(width, height) - self.child.draw(context, x + child_x, y + child_y, child_width, - child_height) - - def _find_child_at(self, x, y, width, height): - child_x, child_y, child_width, child_height = \ - self._calc_child_position(width, height) - if ((child_x <= x < child_x + child_width) and - (child_y <= y < child_y + child_height)): - return self.child, child_x, child_y, child_width, child_height - else: - return None # (x, y) is in the empty space around child - -class DrawingArea(Packer): - """Area that uses custom drawing code. - """ - def __init__(self, width, height, callback, *args): - self.width = width - self.height = height - self.callback_info = (callback, args) - - def _calc_size(self): - return self.width, self.height - - def _layout(self, context, x, y, width, height): - callback, args = self.callback_info - callback(context, x, y, width, height, *args) - - def _find_child_at(self, x, y, width, height): - return None - -class Background(Packer): - """Draws a background behind a child element. - """ - def __init__(self, child, min_width=0, min_height=0, margin=None): - self.child = child - self.min_width = min_width - self.min_height = min_height - self.margin = Margin(margin) - self.callback_info = None - - def set_callback(self, callback, *args): - self.callback_info = (callback, args) - - def _calc_size(self): - width, height = self.child.get_size() - width = max(self.min_width, width) - height = max(self.min_height, height) - return self.margin.outer_size((width, height)) - - def _layout(self, context, x, y, width, height): - if self.callback_info: - callback, args = self.callback_info - callback(context, x, y, width, height, *args) - self.child.draw(context, *self.margin.inner_rect(x, y, width, height)) - - def _find_child_at(self, x, y, width, height): - if not self.margin.point_in_margin(x, y, width, height): - return None - return (self.child,) + self.margin.inner_rect(0, 0, width, height) - -class Padding(Packer): - """Adds padding to the edges of a packer. - """ - def __init__(self, child, top=0, right=0, bottom=0, left=0): - self.child = child - self.margin = Margin((top, right, bottom, left)) - - def _calc_size(self): - return self.margin.outer_size(self.child.get_size()) - - def _layout(self, context, x, y, width, height): - self.child.draw(context, *self.margin.inner_rect(x, y, width, height)) - - def _find_child_at(self, x, y, width, height): - if not self.margin.point_in_margin(x, y, width, height): - return None - return (self.child,) + self.margin.inner_rect(0, 0, width, height) - -class TextBoxPacker(Packer): - """Base class for ClippedTextLine and ClippedTextBox. - """ - def _layout(self, context, x, y, width, height): - self.textbox.draw(context, x, y, width, height) - - def _find_child_at(self, x, y, width, height): - # We could return the TextBox here, but we know it doesn't have a - # find_hotspot() method - return None - -class ClippedTextBox(TextBoxPacker): - """A TextBox that gets clipped if it's larger than it's allocated - width. - """ - def __init__(self, textbox, min_width=0, min_height=0): - self.textbox = textbox - self.min_width = min_width - self.min_height = min_height - - def _calc_size(self): - height = max(self.min_height, self.textbox.font.line_height()) - return self.min_width, height - -class ClippedTextLine(TextBoxPacker): - """A single line of text that gets clipped if it's larger than the - space allocated to it. By default the clipping will happen at character - boundaries. - """ - def __init__(self, textbox, min_width=0): - self.textbox = textbox - self.textbox.set_wrap_style('char') - self.min_width = min_width - - def _calc_size(self): - return self.min_width, self.textbox.font.line_height() - -class TruncatedTextLine(ClippedTextLine): - def __init__(self, textbox, min_width=0): - ClippedTextLine.__init__(self, textbox, min_width) - self.textbox.set_wrap_style('truncated-char') - -class Hotspot(Packer): - """A Hotspot handles mouse click tracking. It's only purpose is - to store a name to return from ``find_hotspot()``. In terms of - layout, it simply renders it's child in it's allocated space. - """ - def __init__(self, name, child): - self.name = name - self.child = child - - def _calc_size(self): - return self.child.get_size() - - def _layout(self, context, x, y, width, height): - self.child.draw(context, x, y, width, height) - - def find_hotspot(self, x, y, width, height): - return self.name, x, y, width, height - -class Stack(Packer): - """Packer that stacks other packers on top of each other. - """ - def __init__(self): - self.children = [] - - def pack(self, packer): - self.children.append(packer) - - def pack_below(self, packer): - self.children.insert(0, packer) - - def _layout(self, context, x, y, width, height): - for packer in self.children: - packer._layout(context, x, y, width, height) - - def _calc_size(self): - """Calculate the size needed to hold the box. The return value gets - cached and return in get_size(). - """ - width = height = 0 - for packer in self.children: - child_width, child_height = packer.get_size() - width = max(width, child_width) - height = max(height, child_height) - return width, height - - def _find_child_at(self, x, y, width, height): - # Return the topmost packer - try: - top = self.children[-1] - except IndexError: - return None - else: - return top._find_child_at(x, y, width, height) - -def align_left(packer): - """Align a packer to the left side of it's allocated space.""" - return Alignment(packer, xalign=0.0, xscale=0.0) - -def align_right(packer): - """Align a packer to the right side of it's allocated space.""" - return Alignment(packer, xalign=1.0, xscale=0.0) - -def align_top(packer): - """Align a packer to the top side of it's allocated space.""" - return Alignment(packer, yalign=0.0, yscale=0.0) - -def align_bottom(packer): - """Align a packer to the bottom side of it's allocated space.""" - return Alignment(packer, yalign=1.0, yscale=0.0) - -def align_middle(packer): - """Align a packer to the middle of it's allocated space.""" - return Alignment(packer, yalign=0.5, yscale=0.0) - -def align_center(packer): - """Align a packer to the center of it's allocated space.""" - return Alignment(packer, xalign=0.5, xscale=0.0) - -def pad(packer, top=0, left=0, bottom=0, right=0): - """Add padding to a packer.""" - return Padding(packer, top, right, bottom, left) - -class LayoutRect(object): - """Lightweight object use to track rectangles inside a layout - - :attribute x: top coordinate, read-write - :attribute y: left coordinate, read-write - :attribute width: width of the rect, read-write - :attribute height: height of the rect, read-write - """ - - def __init__(self, x, y, width, height): - self.x = x - self.y = y - self.width = width - self.height = height - - def __str__(self): - return "LayoutRect(%s, %s, %s, %s)" % (self.x, self.y, self.width, - self.height) - - def __eq__(self, other): - my_values = (self.x, self.y, self.width, self.height) - try: - other_values = (other.x, other.y, other.width, other.height) - except AttributeError: - return NotImplemented - return my_values == other_values - - def subsection(self, left, right, top, bottom): - """Create a new LayoutRect from inside this one.""" - return LayoutRect(self.x + left, self.y + top, - self.width - left - right, self.height - top - bottom) - - def right_side(self, width): - """Create a new LayoutRect from the right side of this one.""" - return LayoutRect(self.right - width, self.y, width, self.height) - - def left_side(self, width): - """Create a new LayoutRect from the left side of this one.""" - return LayoutRect(self.x, self.y, width, self.height) - - def top_side(self, height): - """Create a new LayoutRect from the top side of this one.""" - return LayoutRect(self.x, self.y, self.width, height) - - def bottom_side(self, height): - """Create a new LayoutRect from the bottom side of this one.""" - return LayoutRect(self.x, self.bottom - height, self.width, height) - - def past_right(self, width): - """Create a LayoutRect width pixels to the right of this one>""" - return LayoutRect(self.right, self.y, width, self.height) - - def past_left(self, width): - """Create a LayoutRect width pixels to the right of this one>""" - return LayoutRect(self.x-width, self.y, width, self.height) - - def past_top(self, height): - """Create a LayoutRect height pixels above this one>""" - return LayoutRect(self.x, self.y-height, self.width, height) - - def past_bottom(self, height): - """Create a LayoutRect height pixels below this one>""" - return LayoutRect(self.x, self.bottom, self.width, height) - - def is_point_inside(self, x, y): - return (self.x <= x < self.x + self.width - and self.y <= y < self.y + self.height) - - def get_right(self): - return self.x + self.width - def set_right(self, right): - self.width = right - self.x - right = property(get_right, set_right) - - def get_bottom(self): - return self.y + self.height - def set_bottom(self, bottom): - self.height = bottom - self.y - bottom = property(get_bottom, set_bottom) - -class Layout(object): - """Store the layout for a cell - - Layouts are lightweight objects that keep track of where stuff is inside a - cell. They can be used for both rendering and hotspot tracking. - - :attribute last_rect: the LayoutRect most recently added to the layout - """ - - def __init__(self): - self._rects = [] - self.last_rect = None - - def rect_count(self): - """Get the number of rects in this layout.""" - return len(self._rects) - - def add(self, x, y, width, height, drawing_function=None, - hotspot=None): - """Add a new element to this Layout - - :param x: x coordinate - :param y: y coordinate - :param width: width - :param height: height - :param drawing_function: if set, call this function to render the - element on a DrawingContext - :param hotspot: if set, the hotspot for this element - - :returns: LayoutRect of the added element - """ - return self.add_rect(LayoutRect(x, y, width, height), - drawing_function, hotspot) - - def add_rect(self, layout_rect, drawing_function=None, hotspot=None): - """Add a new element to this Layout using a LayoutRect - - :param layout_rect: LayoutRect object for positioning - :param drawing_function: if set, call this function to render the - element on a DrawingContext - :param hotspot: if set, the hotspot for this element - :returns: LayoutRect of the added element - """ - self.last_rect = layout_rect - value = (layout_rect, drawing_function, hotspot) - self._rects.append(value) - return layout_rect - - def add_text_line(self, textbox, x, y, width, hotspot=None): - """Add one line of text from a text box to the layout - - This is convenience method that's equivelent to: - self.add(x, y, width, textbox.font.line_height(), textbox.draw, - hotspot) - """ - return self.add(x, y, width, textbox.font.line_height(), textbox.draw, - hotspot) - - def add_image(self, image, x, y, hotspot=None): - """Add an ImageSurface to the layout - - This is convenience method that's equivelent to: - self.add(x, y, image.width, image.height, image.draw, hotspot) - """ - width, height = image.get_size() - return self.add(x, y, width, height, image.draw, hotspot) - - def merge(self, layout): - """Add another layout's elements with this one - """ - self._rects.extend(layout._rects) - self.last_rect = layout.last_rect - - def translate(self, delta_x, delta_y): - """Move each element inside this layout """ - for rect, _, _ in self._rects: - rect.x += delta_x - rect.y += delta_y - - def max_width(self): - """Get the max width of the elements in current group.""" - return max(rect.width for (rect, _, _) in self._rects) - - def max_height(self): - """Get the max height of the elements in current group.""" - return max(rect.height for (rect, _, _) in self._rects) - - def center_x(self, left=None, right=None): - """Center each rect inside this layout horizontally. - - The left and right arguments control the area to center the rects to. - If one is missing, it will be calculated using largest width of the - layout. If both are missing, a ValueError will be thrown. - - :param left: left-side of the area to center to - :param right: right-side of the area to center to - """ - if left is None: - if right is None: - raise ValueError("both left and right are None") - left = right - self.max_width() - elif right is None: - right = left + self.max_width() - area_width = right - left - for rect, _, _ in self._rects: - rect.x = left + (area_width - rect.width) // 2 - - def center_y(self, top=None, bottom=None): - """Center each rect inside this layout vertically. - - The top and bottom arguments control the area to center the rects to. - If one is missing, it will be calculated using largest height in the - layout. If both are missing, a ValueError will be thrown. - - :param top: top of the area to center to - :param bottom: bottom of the area to center to - """ - if top is None: - if bottom is None: - raise ValueError("both top and bottom are None") - top = bottom - self.max_height() - elif bottom is None: - bottom = top + self.max_height() - area_height = bottom - top - for rect, _, _ in self._rects: - rect.y = top + (area_height - rect.height) // 2 - - def find_hotspot(self, x, y): - """Find a hotspot inside our rects. - - If (x, y) is inside any of the rects for this layout and that rect has - a hotspot set, a 3-tuple containing the hotspot name, and the x, y - coordinates relative to the hotspot rect. If no rect is found, we - return None. - - :param x: x coordinate to check - :param y: y coordinate to check - """ - for rect, drawing_function, hotspot in self._rects: - if hotspot is not None and rect.is_point_inside(x, y): - return hotspot, x - rect.x, y - rect.y - return None - - def draw(self, context): - """Render each layout rect onto context - - :param context: a DrawingContext to draw on - """ - - for rect, drawing_function, hotspot in self._rects: - if drawing_function is not None: - drawing_function(context, rect.x, rect.y, rect.width, - rect.height) diff --git a/mvc/widgets/dialogs.py b/mvc/widgets/dialogs.py deleted file mode 100644 index 3ccdcd7..0000000 --- a/mvc/widgets/dialogs.py +++ /dev/null @@ -1,276 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""``miro.frontends.widgets.dialogs`` -- Dialog boxes for the Widget -frontend. - -The difference between this module and rundialog.py is that rundialog -handles dialog boxes that are coming from the backend code. This -model handles dialogs that we create from the frontend - -One big difference is that we don't have to be as general about -dialogs, so they can present a somewhat nicer API. One important -difference is that all of the dialogs run modally. -""" - -from mvc.widgets import widgetset -from mvc.widgets import widgetutil - -class DialogButton(object): - def __init__(self, text): - self._text = text - def __eq__(self, other): - return isinstance(other, DialogButton) and self.text == other.text - def __str__(self): - return "DialogButton(%r)" % self.text - @property - def text(self): - return unicode(self._text) - -BUTTON_OK = DialogButton("OK") -BUTTON_APPLY = DialogButton("Apply") -BUTTON_CLOSE = DialogButton("Close") -BUTTON_CANCEL = DialogButton("Cancel") -BUTTON_DONE = DialogButton("Done") -BUTTON_YES = DialogButton("Yes") -BUTTON_NO = DialogButton("No") -BUTTON_QUIT = DialogButton("Quit") -BUTTON_CONTINUE = DialogButton("Continue") -BUTTON_IGNORE = DialogButton("Ignore") -BUTTON_IMPORT_FILES = DialogButton("Import Files") -BUTTON_SUBMIT_REPORT = DialogButton("Submit Crash Report") -BUTTON_MIGRATE = DialogButton("Migrate") -BUTTON_DONT_MIGRATE = DialogButton("Don't Migrate") -BUTTON_DOWNLOAD = DialogButton("Download") -BUTTON_REMOVE_ENTRY = DialogButton("Remove Entry") -BUTTON_DELETE_FILE = DialogButton("Delete File") -BUTTON_DELETE_FILES = DialogButton("Delete Files") -BUTTON_KEEP_VIDEOS = DialogButton("Keep Videos") -BUTTON_DELETE_VIDEOS = DialogButton("Delete Videos") -BUTTON_CREATE = DialogButton("Create") -BUTTON_CREATE_FEED = DialogButton("Create Podcast") -BUTTON_CREATE_FOLDER = DialogButton("Create Folder") -BUTTON_CHOOSE_NEW_FOLDER = DialogButton("Choose New Folder") -BUTTON_ADD_FOLDER = DialogButton("Add Folder") -BUTTON_ADD = DialogButton("Add") -BUTTON_ADD_INTO_NEW_FOLDER = DialogButton("Add Into New Folder") -BUTTON_KEEP = DialogButton("Keep") -BUTTON_DELETE = DialogButton("Delete") -BUTTON_REMOVE = DialogButton("Remove") -BUTTON_NOT_NOW = DialogButton("Not Now") -BUTTON_CLOSE_TO_TRAY = DialogButton("Close to Tray") -BUTTON_LAUNCH_MIRO = DialogButton("Launch Miro") -BUTTON_DOWNLOAD_ANYWAY = DialogButton("Download Anyway") -BUTTON_OPEN_IN_EXTERNAL_BROWSER = DialogButton( - "Open in External Browser") -BUTTON_DONT_INSTALL = DialogButton("Don't Install") -BUTTON_SUBSCRIBE = DialogButton("Subscribe") -BUTTON_STOP_WATCHING = DialogButton("Stop Watching") -BUTTON_RETRY = DialogButton("Retry") -BUTTON_START_FRESH = DialogButton("Start Fresh") -BUTTON_INCLUDE_DATABASE = DialogButton("Include Database") -BUTTON_DONT_INCLUDE_DATABASE = DialogButton( - "Don't Include Database") - -WARNING_MESSAGE = 0 -INFO_MESSAGE = 1 -CRITICAL_MESSAGE = 2 - - -class ProgressDialog(widgetset.Dialog): - def __init__(self, title): - widgetset.Dialog.__init__(self, title, description='') - self.progress_bar = widgetset.ProgressBar() - self.label = widgetset.Label() - self.label.set_size(1.2) - self.vbox = widgetset.VBox(spacing=6) - self.vbox.pack_end(widgetutil.align_center(self.label)) - self.vbox.pack_end(self.progress_bar) - self.set_extra_widget(self.vbox) - - def update(self, description, progress): - self.label.set_text(description) - if progress >= 0: - self.progress_bar.set_progress(progress) - self.progress_bar.stop_pulsing() - else: - self.progress_bar.start_pulsing() - -class DBUpgradeProgressDialog(widgetset.Dialog): - def __init__(self, title, text): - widgetset.Dialog.__init__(self, title) - self.progress_bar = widgetset.ProgressBar() - self.top_label = widgetset.Label() - self.top_label.set_text(text) - self.top_label.set_wrap(True) - self.top_label.set_size_request(350, -1) - self.label = widgetset.Label() - self.vbox = widgetset.VBox(spacing=6) - self.vbox.pack_end(widgetutil.align_center(self.label)) - self.vbox.pack_end(self.progress_bar) - self.vbox.pack_end(widgetutil.pad(self.top_label, bottom=6)) - self.set_extra_widget(self.vbox) - - def update(self, stage, stage_progress, progress): - self.label.set_text(stage) - self.progress_bar.set_progress(progress) - -def show_about(): - window = widgetset.AboutDialog() - set_transient_for_main(window) - try: - window.run() - finally: - window.destroy() - -def show_message(title, description, alert_type=INFO_MESSAGE, - transient_for=None): - """Display a message to the user and wait for them to click OK""" - window = widgetset.AlertDialog(title, description, alert_type) - _set_transient_for(window, transient_for) - try: - window.add_button(BUTTON_OK.text) - window.run() - finally: - window.destroy() - -def show_choice_dialog(title, description, choices, transient_for=None): - """Display a message to the user and wait for them to choose an option. - Returns the button object chosen.""" - window = widgetset.Dialog(title, description) - try: - for mem in choices: - window.add_button(mem.text) - response = window.run() - return choices[response] - finally: - window.destroy() - -def ask_for_string(title, description, initial_text=None, transient_for=None): - """Ask the user to enter a string in a TextEntry box. - - description - textual description with newlines - initial_text - None, string or callable to pre-populate the entry box - - Returns the value entered, or None if the user clicked cancel - """ - window = widgetset.Dialog(title, description) - try: - window.add_button(BUTTON_OK.text) - window.add_button(BUTTON_CANCEL.text) - entry = widgetset.TextEntry() - entry.set_activates_default(True) - if initial_text: - if callable(initial_text): - initial_text = initial_text() - entry.set_text(initial_text) - window.set_extra_widget(entry) - response = window.run() - if response == 0: - return entry.get_text() - else: - return None - finally: - window.destroy() - -def ask_for_choice(title, description, choices): - """Ask the user to enter a string in a TextEntry box. - - :param title: title for the window - :param description: textual description with newlines - :param choices: list of labels for choices - Returns the index of the value chosen, or None if the user clicked cancel - """ - window = widgetset.Dialog(title, description) - try: - window.add_button(BUTTON_OK.text) - window.add_button(BUTTON_CANCEL.text) - menu = widgetset.OptionMenu(choices) - window.set_extra_widget(menu) - response = window.run() - if response == 0: - return menu.get_selected() - else: - return None - finally: - window.destroy() - -def ask_for_open_pathname(title, initial_filename=None, filters=[], - transient_for=None, select_multiple=False): - """Returns the file pathname or None. - """ - window = widgetset.FileOpenDialog(title) - _set_transient_for(window, transient_for) - try: - if initial_filename: - window.set_filename(initial_filename) - - if filters: - window.add_filters(filters) - - if select_multiple: - window.set_select_multiple(select_multiple) - - response = window.run() - if response == 0: - if select_multiple: - return window.get_filenames() - else: - return window.get_filename() - finally: - window.destroy() - -def ask_for_save_pathname(title, initial_filename=None, transient_for=None): - """Returns the file pathname or None. - """ - window = widgetset.FileSaveDialog(title) - _set_transient_for(window, transient_for) - try: - if initial_filename: - window.set_filename(initial_filename) - response = window.run() - if response == 0: - return window.get_filename() - finally: - window.destroy() - -def ask_for_directory(title, initial_directory=None, transient_for=None): - """Returns the directory pathname or None. - """ - window = widgetset.DirectorySelectDialog(title) - _set_transient_for(window, transient_for) - try: - if initial_directory: - window.set_directory(initial_directory) - - response = window.run() - if response == 0: - return window.get_directory() - finally: - window.destroy() diff --git a/mvc/widgets/gtk/__init__.py b/mvc/widgets/gtk/__init__.py deleted file mode 100644 index 8e58700..0000000 --- a/mvc/widgets/gtk/__init__.py +++ /dev/null @@ -1,65 +0,0 @@ -import os -import sys -import gtk -import gobject - -def initialize(app): - from gtkmenus import MainWindowMenuBar - app.menubar = MainWindowMenuBar() - app.startup() - app.run() - -def attach_menubar(): - from mvc.widgets import app - app.widgetapp.vbox.pack_start(app.widgetapp.menubar) - -def mainloop_start(): - gobject.threads_init() - gtk.main() - -def mainloop_stop(): - gtk.main_quit() - -def idle_add(callback, periodic=None): - if periodic is not None and periodic < 0: - raise ValueError('periodic cannot be negative') - def wrapper(): - callback() - return periodic is not None - delay = periodic - if delay is not None: - delay *= 1000 # milliseconds - else: - delay = 0 - return gobject.timeout_add(delay, wrapper) - -def idle_remove(id_): - gobject.source_remove(id_) - -def check_kde(): - return os.environ.get("KDE_FULL_SESSION", None) != None - -def open_file_linux(filename): - if check_kde(): - os.spawnlp(os.P_NOWAIT, "kfmclient", "kfmclient", # kfmclient is part of konqueror - "exec", "file://" + filename) - else: - os.spawnlp(os.P_NOWAIT, "gnome-open", "gnome-open", filename) - -def reveal_file(filename): - if hasattr(os, 'startfile'): # Windows - os.startfile(os.path.dirname(filename)) - else: - open_file_linux(filename) - -def get_conversion_directory_windows(): - from mvc.windows import specialfolders - return specialfolders.base_movies_directory - -def get_conversion_directory_linux(): - return os.path.expanduser('~') - -if sys.platform == 'win32': - get_conversion_directory = get_conversion_directory_windows -else: - get_conversion_directory = get_conversion_directory_linux diff --git a/mvc/widgets/gtk/base.py b/mvc/widgets/gtk/base.py deleted file mode 100644 index e02db3f..0000000 --- a/mvc/widgets/gtk/base.py +++ /dev/null @@ -1,300 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -""".base -- Base classes for GTK Widgets.""" - -import gtk - -from mvc import signals -import wrappermap -from .weakconnect import weak_connect -import keymap - -def make_gdk_color(miro_color): - def convert_value(value): - return int(round(value * 65535)) - - values = tuple(convert_value(c) for c in miro_color) - return gtk.gdk.Color(*values) - -class Widget(signals.SignalEmitter): - """Base class for GTK widgets. - - The actual GTK Widget is stored in '_widget'. - - signals: - - 'size-allocated' (widget, width, height): The widget had it's size - allocated. - """ - def __init__(self, *signal_names): - signals.SignalEmitter.__init__(self, *signal_names) - self.create_signal('size-allocated') - self.create_signal('key-press') - self.create_signal('focus-out') - self.style_mods = {} - self.use_custom_style = False - self._disabled = False - - def wrapped_widget_connect(self, signal, method, *user_args): - """Connect to a signal of the widget we're wrapping. - - We use a weak reference to ensures that we don't have circular - references between the wrapped widget and the wrapper widget. - """ - return weak_connect(self._widget, signal, method, *user_args) - - def set_widget(self, widget): - self._widget = widget - wrappermap.add(self._widget, self) - if self.should_connect_to_hierarchy_changed(): - self.wrapped_widget_connect('hierarchy_changed', - self.on_hierarchy_changed) - self.wrapped_widget_connect('size-allocate', self.on_size_allocate) - self.wrapped_widget_connect('key-press-event', self.on_key_press) - self.wrapped_widget_connect('focus-out-event', self.on_focus_out) - self.use_custom_style_callback = None - - def should_connect_to_hierarchy_changed(self): - # GTK creates windows to handle submenus, which messes with our - # on_hierarchy_changed callback. We don't care about custom styles - # for menus anyways, so just ignore the signal. - return not isinstance(self._widget, gtk.MenuItem) - - def set_can_focus(self, allow): - """Set if we allow the widget to hold keyboard focus. - """ - if allow: - self._widget.set_flags(gtk.CAN_FOCUS) - else: - self._widget.unset_flags(gtk.CAN_FOCUS) - - def on_hierarchy_changed(self, widget, previous_toplevel): - toplevel = widget.get_toplevel() - if not (toplevel.flags() & gtk.TOPLEVEL): - toplevel = None - if previous_toplevel != toplevel: - if self.use_custom_style_callback: - old_window = wrappermap.wrapper(previous_toplevel) - old_window.disconnect(self.use_custom_style_callback) - if toplevel is not None: - window = wrappermap.wrapper(toplevel) - callback_id = window.connect('use-custom-style-changed', - self.on_use_custom_style_changed) - self.use_custom_style_callback = callback_id - else: - self.use_custom_style_callback = None - if previous_toplevel is None: - # Setup our initial state - self.on_use_custom_style_changed(window) - - def on_size_allocate(self, widget, allocation): - self.emit('size-allocated', allocation.width, allocation.height) - - def on_key_press(self, widget, event): - key_modifiers = keymap.translate_gtk_event(event) - if key_modifiers: - key, modifiers = key_modifiers - return self.emit('key-press', key, modifiers) - - def on_focus_out(self, widget, event): - self.emit('focus-out') - - def on_use_custom_style_changed(self, window): - self.use_custom_style = window.use_custom_style - if not self.style_mods: - return # no need to do any work here - if self.use_custom_style: - for (what, state), color in self.style_mods.items(): - self.do_modify_style(what, state, color) - else: - # This should reset the style changes we've made - self._widget.modify_style(gtk.RcStyle()) - self.handle_custom_style_change() - - def handle_custom_style_change(self): - """Called when the user changes a from a theme where we don't want to - use our custom style to one where we do, or vice-versa. The Widget - class handles changes that used modify_style(), but subclasses might - want to do additional work. - """ - pass - - def modify_style(self, what, state, color): - """Change the style of our widget. This method checks to see if we - think the user's theme is compatible with our stylings, and doesn't - change things if not. what is either 'base', 'text', 'bg' or 'fg' - depending on which color is to be changed. - """ - if self.use_custom_style: - self.do_modify_style(what, state, color) - self.style_mods[(what, state)] = color - - def unmodify_style(self, what, state): - if (what, state) in self.style_mods: - del self.style_mods[(what, state)] - default_color = getattr(self.style, what)[state] - self.do_modify_style(what, state, default_color) - - def do_modify_style(self, what, state, color): - if what == 'base': - self._widget.modify_base(state, color) - elif what == 'text': - self._widget.modify_text(state, color) - elif what == 'bg': - self._widget.modify_bg(state, color) - elif what == 'fg': - self._widget.modify_fg(state, color) - else: - raise ValueError("Unknown what in do_modify_style: %s" % what) - - def get_window(self): - gtk_window = self._widget.get_toplevel() - return wrappermap.wrapper(gtk_window) - - def clear_size_request_cache(self): - # This is just an OS X hack - pass - - def get_size_request(self): - return self._widget.size_request() - - def invalidate_size_request(self): - self._widget.queue_resize() - - def set_size_request(self, width, height): - if not width >= -1 and height >= -1: - raise ValueError("invalid dimensions in set_size_request: %s" % - repr((width, height))) - self._widget.set_size_request(width, height) - - def relative_position(self, other_widget): - return other_widget._widget.translate_coordinates(self._widget, 0, 0) - - def convert_gtk_color(self, color): - return (color.red / 65535.0, color.green / 65535.0, - color.blue / 65535.0) - - def get_width(self): - try: - return self._widget.allocation.width - except AttributeError: - return -1 - width = property(get_width) - - def get_height(self): - try: - return self._widget.allocation.height - except AttributeError: - return -1 - height = property(get_height) - - def queue_redraw(self): - if self._widget: - self._widget.queue_draw() - - def redraw_now(self): - if self._widget: - self._widget.queue_draw() - self._widget.window.process_updates(True) - - def forward_signal(self, signal_name, forwarded_signal_name=None): - """Add a callback so that when the GTK widget emits a signal, we emit - signal from the wrapper widget. - """ - if forwarded_signal_name is None: - forwarded_signal_name = signal_name - self.wrapped_widget_connect(signal_name, self.do_forward_signal, - forwarded_signal_name) - - def do_forward_signal(self, widget, *args): - forwarded_signal_name = args[-1] - args = args[:-1] - self.emit(forwarded_signal_name, *args) - - def make_color(self, miro_color): - color = make_gdk_color(miro_color) - self._widget.get_colormap().alloc_color(color) - return color - - def enable(self): - self._disabled = False - self._widget.set_sensitive(True) - - def disable(self): - self._disabled = True - self._widget.set_sensitive(False) - - def set_disabled(self, disabled): - if disabled: - self.disable() - else: - self.enable() - - def get_disabled(self): - return self._disabled - -class Bin(Widget): - def __init__(self): - Widget.__init__(self) - self.child = None - - def add(self, child): - if self.child is not None: - raise ValueError("Already have a child: %s" % self.child) - if child._widget.parent is not None: - raise ValueError("%s already has a parent" % child) - self.child = child - self.add_child_to_widget() - child._widget.show() - - def add_child_to_widget(self): - self._widget.add(self.child._widget) - - def remove_child_from_widget(self): - if self._widget.get_child() is not None: - # otherwise gtkmozembed gets confused - self._widget.get_child().hide() - self._widget.remove(self._widget.get_child()) - - - def remove(self): - if self.child is not None: - self.child = None - self.remove_child_from_widget() - - def set_child(self, new_child): - self.remove() - self.add(new_child) - - def enable(self): - self.child.enable() - - def disable(self): - self.child.disable() diff --git a/mvc/widgets/gtk/const.py b/mvc/widgets/gtk/const.py deleted file mode 100644 index 5e9ec05..0000000 --- a/mvc/widgets/gtk/const.py +++ /dev/null @@ -1,44 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -""".const -- Constants.""" - -import gtk - -DRAG_ACTION_NONE = 0 -DRAG_ACTION_COPY = gtk.gdk.ACTION_COPY -DRAG_ACTION_MOVE = gtk.gdk.ACTION_MOVE -DRAG_ACTION_LINK = gtk.gdk.ACTION_LINK -DRAG_ACTION_ALL = DRAG_ACTION_COPY | DRAG_ACTION_MOVE | DRAG_ACTION_LINK - -ITEM_TITLE_FONT = "Helvetica" -ITEM_DESC_FONT = "Helvetica" -ITEM_INFO_FONT = "Lucida Grande" - -TOOLBAR_GRAY = (0.2, 0.2, 0.2) diff --git a/mvc/widgets/gtk/contextmenu.py b/mvc/widgets/gtk/contextmenu.py deleted file mode 100644 index cd5b6ba..0000000 --- a/mvc/widgets/gtk/contextmenu.py +++ /dev/null @@ -1,31 +0,0 @@ -import gtk - -from .base import Widget - -class ContextMenu(Widget): - - def __init__(self, options): - super(ContextMenu, self).__init__() - self.set_widget(gtk.Menu()) - for i, item_info in enumerate(options): - if item_info is None: - # separator - item = gtk.SeparatorMenuItem() - else: - label, callback = item_info - item = gtk.MenuItem(label) - if isinstance(callback, list): - submenu = ContextMenu(callback) - item.set_submenu(submenu._widget) - elif callback is not None: - item.connect('activate', self.on_activate, callback, i) - else: - item.set_sensitive(False) - self._widget.append(item) - item.show() - - def popup(self): - self._widget.popup(None, None, None, 0, 0) - - def on_activate(self, widget, callback, i): - callback(self, i) diff --git a/mvc/widgets/gtk/controls.py b/mvc/widgets/gtk/controls.py deleted file mode 100644 index 4367c1f..0000000 --- a/mvc/widgets/gtk/controls.py +++ /dev/null @@ -1,337 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -""".controls -- Control Widgets.""" - -import gtk -import pango - -from mvc.widgets import widgetconst -import layout -from .base import Widget -from .simple import Label - -class BinBaselineCalculator(object): - """Mixin class that defines the baseline method for gtk.Bin subclasses, - where the child is the label that we are trying to get the baseline for. - """ - - def baseline(self): - my_size = self._widget.size_request() - child_size = self._widget.child.size_request() - ypad = (my_size[1] - child_size[1]) / 2 - - pango_context = self._widget.get_pango_context() - metrics = pango_context.get_metrics(self._widget.style.font_desc) - return pango.PIXELS(metrics.get_descent()) + ypad - -class TextEntry(Widget): - entry_class = gtk.Entry - def __init__(self, initial_text=None): - Widget.__init__(self) - self.create_signal('activate') - self.create_signal('changed') - self.create_signal('validate') - self.set_widget(self.entry_class()) - self.forward_signal('activate') - self.forward_signal('changed') - if initial_text is not None: - self.set_text(initial_text) - - def focus(self): - self._widget.grab_focus() - - def start_editing(self, text): - self.set_text(text) - self.focus() - self._widget.emit('move-cursor', gtk.MOVEMENT_BUFFER_ENDS, 1, False) - - def set_text(self, text): - self._widget.set_text(text) - - def get_text(self): - return self._widget.get_text().decode('utf-8') - - def set_max_length(self, chars): - self._widget.set_max_length(chars) - - def set_width(self, chars): - self._widget.set_width_chars(chars) - - def set_invisible(self, setting): - self._widget.props.visibility = not setting - - def set_activates_default(self, setting): - self._widget.set_activates_default(setting) - - def baseline(self): - layout_height = pango.PIXELS(self._widget.get_layout().get_size()[1]) - ypad = (self._widget.size_request()[1] - layout_height) / 2 - pango_context = self._widget.get_pango_context() - metrics = pango_context.get_metrics(self._widget.style.font_desc) - return pango.PIXELS(metrics.get_descent()) + ypad - - -class NumberEntry(TextEntry): - def __init__(self, initial_text=None): - TextEntry.__init__(self, initial_text) - self._widget.connect('changed', self.validate) - self.previous_text = initial_text or "" - - def validate(self, entry): - text = self.get_text() - if text.isdigit() or not text: - self.previous_text = text - else: - self._widget.set_text(self.previous_text) - -class SecureTextEntry(TextEntry): - def __init__(self, initial_text=None): - TextEntry.__init__(self, initial_text) - self.set_invisible(True) - -class MultilineTextEntry(Widget): - entry_class = gtk.TextView - def __init__(self, initial_text=None, border=False): - Widget.__init__(self) - self.set_widget(self.entry_class()) - if initial_text is not None: - self.set_text(initial_text) - self._widget.set_wrap_mode(gtk.WRAP_WORD) - self._widget.set_accepts_tab(False) - self.border = border - - def focus(self): - self._widget.grab_focus() - - def set_text(self, text): - self._widget.get_buffer().set_text(text) - - def get_text(self): - buffer_ = self._widget.get_buffer() - return buffer_.get_text(*(buffer_.get_bounds())).decode('utf-8') - - def baseline(self): - # FIXME - layout_height = pango.PIXELS(self._widget.get_layout().get_size()[1]) - ypad = (self._widget.size_request()[1] - layout_height) / 2 - pango_context = self._widget.get_pango_context() - metrics = pango_context.get_metrics(self._widget.style.font_desc) - return pango.PIXELS(metrics.get_descent()) + ypad - - def set_editable(self, editable): - self._widget.set_editable(editable) - -class Checkbox(Widget, BinBaselineCalculator): - """Widget that the user can toggle on or off.""" - - def __init__(self, text=None, bold=False, color=None): - Widget.__init__(self) - BinBaselineCalculator.__init__(self) - if text is None: - text = '' - self.set_widget(gtk.CheckButton()) - self.label = Label(text, color=color) - self._widget.add(self.label._widget) - self.label._widget.show() - self.create_signal('toggled') - self.forward_signal('toggled') - if bold: - self.label.set_bold(True) - - def get_checked(self): - return self._widget.get_active() - - def set_checked(self, value): - self._widget.set_active(value) - - def set_size(self, scale_factor): - self.label.set_size(scale_factor) - - def get_text_padding(self): - """ - Returns the amount of space the checkbox takes up before the label. - """ - indicator_size = self._widget.style_get_property('indicator-size') - indicator_spacing = self._widget.style_get_property( - 'indicator-spacing') - focus_width = self._widget.style_get_property('focus-line-width') - focus_padding = self._widget.style_get_property('focus-padding') - return (indicator_size + 3 * indicator_spacing + 2 * (focus_width + - focus_padding)) - -class RadioButtonGroup(Widget, BinBaselineCalculator): - """RadioButtonGroup. - - Create the group, then create a bunch of RadioButtons passing in the group. - - NB: GTK has built-in radio button grouping functionality, and we should - be using that but we need this widget for portable code. We create - a dummy GTK radio button and make this the "root" button which gets - inherited by all buttons in this radio button group. - """ - def __init__(self): - Widget.__init__(self) - BinBaselineCalculator.__init__(self) - self.set_widget(gtk.RadioButton(label="")) - self._widget.set_active(False) - self._buttons = [] - - def add_button(self, button): - self._buttons.append(button) - - def get_buttons(self): - return self._buttons - - def get_selected(self): - for mem in self._buttons: - if mem.get_selected(): - return mem - - def set_selected(self, button): - for mem in self._buttons: - if mem is button: - mem._widget.set_active(True) - else: - mem._widget.set_active(False) - -class RadioButton(Widget, BinBaselineCalculator): - """RadioButton.""" - def __init__(self, label, group=None, color=None): - Widget.__init__(self) - BinBaselineCalculator.__init__(self) - if group: - self.group = group - else: - self.group = RadioButtonGroup() - self.set_widget(gtk.RadioButton(group=self.group._widget)) - self.label = Label(label, color=color) - self._widget.add(self.label._widget) - self.label._widget.show() - self.create_signal('clicked') - self.forward_signal('clicked') - - group.add_button(self) - - def set_size(self, size): - self.label.set_size(size) - - def get_group(self): - return self.group - - def get_selected(self): - return self._widget.get_active() - - def set_selected(self): - self.group.set_selected(self) - -class Button(Widget, BinBaselineCalculator): - def __init__(self, text, style='normal', width=None): - Widget.__init__(self) - BinBaselineCalculator.__init__(self) - # We just ignore style here, GTK users expect their own buttons. - self.set_widget(gtk.Button()) - self.create_signal('clicked') - self.forward_signal('clicked') - self.label = Label(text) - # only honor width if its bigger than the width we need to display the - # label (#18994) - if width and width > self.label.get_width(): - alignment = layout.Alignment(0.5, 0.5, 0, 0) - alignment.set_size_request(width, -1) - alignment.add(self.label) - self._widget.add(alignment._widget) - else: - self._widget.add(self.label._widget) - self.label._widget.show() - - def set_text(self, title): - self.label.set_text(title) - - def set_bold(self, bold): - self.label.set_bold(bold) - - def set_size(self, scale_factor): - self.label.set_size(scale_factor) - - def set_color(self, color): - self.label.set_color(color) - -class OptionMenu(Widget): - def __init__(self, options): - Widget.__init__(self) - self.create_signal('changed') - - self.set_widget(gtk.ComboBox(gtk.ListStore(str, str))) - self.cell = gtk.CellRendererText() - self._widget.pack_start(self.cell, True) - self._widget.add_attribute(self.cell, 'text', 0) - if options: - for option, value in options: - self._widget.get_model().append((option, value)) - self._widget.set_active(0) - self.options = options - self.wrapped_widget_connect('changed', self.on_changed) - - def baseline(self): - my_size = self._widget.size_request() - child_size = self._widget.child.size_request() - ypad = self.cell.props.ypad + (my_size[1] - child_size[1]) / 2 - - pango_context = self._widget.get_pango_context() - metrics = pango_context.get_metrics(self._widget.style.font_desc) - return pango.PIXELS(metrics.get_descent()) + ypad - - def set_bold(self, bold): - if bold: - self.cell.props.weight = pango.WEIGHT_BOLD - else: - self.cell.props.weight = pango.WEIGHT_NORMAL - - def set_size(self, size): - if size == widgetconst.SIZE_NORMAL: - self.cell.props.scale = 1 - else: - self.cell.props.scale = 0.75 - - def set_color(self, color): - self.cell.props.foreground_gdk = self.make_color(color) - - def set_selected(self, index): - self._widget.set_active(index) - - def get_selected(self): - return self._widget.get_active() - - def on_changed(self, widget): - index = widget.get_active() - self.emit('changed', index) - - def set_width(self, width): - self._widget.set_property('width-request', width) diff --git a/mvc/widgets/gtk/customcontrols.py b/mvc/widgets/gtk/customcontrols.py deleted file mode 100644 index 070cebd..0000000 --- a/mvc/widgets/gtk/customcontrols.py +++ /dev/null @@ -1,517 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -""".controls -- Contains the ControlBox and -CustomControl classes. These handle the custom buttons/sliders used during -playback. -""" - -from __future__ import division -import math - -import gtk -import gobject - -import wrappermap -from .base import Widget -from .simple import Label, Image -from .drawing import (CustomDrawingMixin, Drawable, - ImageSurface) -from mvc.widgets import widgetconst - -class CustomControlMixin(CustomDrawingMixin): - def do_expose_event(self, event): - CustomDrawingMixin.do_expose_event(self, event) - if self.is_focus(): - style = self.get_style() - style.paint_focus(self.window, self.state, - event.area, self, None, self.allocation.x, - self.allocation.y, self.allocation.width, - self.allocation.height) - -class CustomButtonWidget(CustomControlMixin, gtk.Button): - def draw(self, wrapper, context): - if self.is_active(): - wrapper.state = 'pressed' - elif self.state == gtk.STATE_PRELIGHT: - wrapper.state = 'hover' - else: - wrapper.state = 'normal' - wrapper.draw(context, wrapper.layout_manager) - self.set_focus_on_click(False) - - def is_active(self): - return self.state == gtk.STATE_ACTIVE - -class ContinuousCustomButtonWidget(CustomButtonWidget): - def is_active(self): - return (self.state == gtk.STATE_ACTIVE or - wrappermap.wrapper(self).button_down) - -class DragableCustomButtonWidget(CustomButtonWidget): - def __init__(self): - CustomButtonWidget.__init__(self) - self.button_press_x = None - self.set_events(self.get_events() | gtk.gdk.POINTER_MOTION_MASK) - - def do_button_press_event(self, event): - self.button_press_x = event.x - self.last_drag_event = None - gtk.Button.do_button_press_event(self, event) - - def do_button_release_event(self, event): - self.button_press_x = None - gtk.Button.do_button_release_event(self, event) - - def do_motion_notify_event(self, event): - DRAG_THRESHOLD = 15 - if self.button_press_x is None: - # button not down - return - if (self.last_drag_event != 'right' and - event.x > self.button_press_x + DRAG_THRESHOLD): - wrappermap.wrapper(self).emit('dragged-right') - self.last_drag_event = 'right' - elif (self.last_drag_event != 'left' and - event.x < self.button_press_x - DRAG_THRESHOLD): - wrappermap.wrapper(self).emit('dragged-left') - self.last_drag_event = 'left' - - def do_clicked(self): - # only emit clicked if we didn't emit dragged-left or dragged-right - if self.last_drag_event is None: - wrappermap.wrapper(self).emit('clicked') - -class _DragInfo(object): - """Info about the start of a drag. - - Attributes: - - - button: button that started the drag - - start_pos: position of the slider - - click_pos: position of the click - - Note that start_pos and click_pos will be different if the user clicks - inside the slider. - """ - - def __init__(self, button, start_pos, click_pos): - self.button = button - self.start_pos = start_pos - self.click_pos = click_pos - -class CustomScaleMixin(CustomControlMixin): - def __init__(self): - CustomControlMixin.__init__(self) - self.drag_info = None - self.min = self.max = 0.0 - - def get_range(self): - return self.min, self.max - - def set_range(self, min, max): - self.min = float(min) - self.max = float(max) - gtk.Range.set_range(self, min, max) - - def is_continuous(self): - return wrappermap.wrapper(self).is_continuous() - - def is_horizontal(self): - # this comes from a mixin - pass - - def gtk_scale_class(self): - if self.is_horizontal(): - return gtk.HScale - else: - return gtk.VScale - - def get_slider_pos(self, value=None): - if value is None: - value = self.get_value() - if self.is_horizontal(): - size = self.allocation.width - else: - size = self.allocation.height - ratio = (float(value) - self.min) / (self.max - self.min) - start_pos = self.slider_size() / 2.0 - return start_pos + ratio * (size - self.slider_size()) - - def slider_size(self): - return wrappermap.wrapper(self).slider_size() - - def _event_pos(self, event): - """Get the position of an event. - - If we are horizontal, this will be the x coordinate. If we are - vertical, the y. - """ - if self.is_horizontal(): - return event.x - else: - return event.y - - def do_button_press_event(self, event): - if self.drag_info is not None: - return - current_pos = self.get_slider_pos() - event_pos = self._event_pos(event) - pos_difference = abs(current_pos - event_pos) - # only move the slider if the click was outside its boundaries - # (#18840) - if pos_difference > self.slider_size() / 2.0: - self.move_slider(event_pos) - current_pos = event_pos - self.drag_info = _DragInfo(event.button, current_pos, event_pos) - self.grab_focus() - wrappermap.wrapper(self).emit('pressed') - - def do_motion_notify_event(self, event): - if self.drag_info is not None: - event_pos = self._event_pos(event) - delta = event_pos - self.drag_info.click_pos - self.move_slider(self.drag_info.start_pos + delta) - - def move_slider(self, new_pos): - """Move the slider so that it's centered on new_pos.""" - if self.is_horizontal(): - size = self.allocation.width - else: - size = self.allocation.height - - slider_size = self.slider_size() - new_pos -= slider_size / 2 - size -= slider_size - ratio = max(0, min(1, float(new_pos) / size)) - self.set_value(ratio * (self.max - self.min)) - - wrappermap.wrapper(self).emit('moved', self.get_value()) - if self.is_continuous(): - wrappermap.wrapper(self).emit('changed', self.get_value()) - - def handle_drag_out_of_bounds(self): - if not self.is_continuous(): - self.set_value(self.start_value) - - def do_button_release_event(self, event): - if self.drag_info is None or event.button != self.drag_info.button: - return - self.drag_info = None - if (self.is_continuous and - (0 <= event.x < self.allocation.width) and - (0 <= event.y < self.allocation.height)): - wrappermap.wrapper(self).emit('changed', self.get_value()) - wrappermap.wrapper(self).emit('released') - - def do_scroll_event(self, event): - wrapper = wrappermap.wrapper(self) - if self.is_horizontal(): - if event.direction == gtk.gdk.SCROLL_UP: - event.direction = gtk.gdk.SCROLL_DOWN - elif event.direction == gtk.gdk.SCROLL_DOWN: - event.direction = gtk.gdk.SCROLL_UP - if (wrapper._scroll_step is not None and - event.direction in (gtk.gdk.SCROLL_UP, gtk.gdk.SCROLL_DOWN)): - # handle the scroll ourself - if event.direction == gtk.gdk.SCROLL_DOWN: - delta = wrapper._scroll_step - else: - delta = -wrapper._scroll_step - self.set_value(self.get_value() + delta) - else: - # let GTK handle the scroll - self.gtk_scale_class().do_scroll_event(self, event) - # Treat mouse scrolls as if the user clicked on the new position - wrapper.emit('pressed') - wrapper.emit('changed', self.get_value()) - wrapper.emit('released') - - def do_move_slider(self, scroll): - if self.is_horizontal(): - if scroll == gtk.SCROLL_STEP_UP: - scroll = gtk.SCROLL_STEP_DOWN - elif scroll == gtk.SCROLL_STEP_DOWN: - scroll = gtk.SCROLL_STEP_UP - elif scroll == gtk.SCROLL_PAGE_UP: - scroll = gtk.SCROLL_PAGE_DOWN - elif scroll == gtk.SCROLL_PAGE_DOWN: - scroll = gtk.SCROLL_PAGE_UP - elif scroll == gtk.SCROLL_START: - scroll = gtk.SCROLL_END - elif scroll == gtk.SCROLL_END: - scroll = gtk.SCROLL_START - return self.gtk_scale_class().do_move_slider(self, scroll) - -class CustomHScaleWidget(CustomScaleMixin, gtk.HScale): - def __init__(self): - CustomScaleMixin.__init__(self) - gtk.HScale.__init__(self) - - def is_horizontal(self): - return True - -class CustomVScaleWidget(CustomScaleMixin, gtk.VScale): - def __init__(self): - CustomScaleMixin.__init__(self) - gtk.VScale.__init__(self) - - def is_horizontal(self): - return False - -gobject.type_register(CustomButtonWidget) -gobject.type_register(ContinuousCustomButtonWidget) -gobject.type_register(DragableCustomButtonWidget) -gobject.type_register(CustomHScaleWidget) -gobject.type_register(CustomVScaleWidget) - -class CustomControlBase(Drawable, Widget): - def __init__(self): - Widget.__init__(self) - Drawable.__init__(self) - self._gtk_cursor = None - self._entry_handlers = None - - def _connect_enter_notify_handlers(self): - if self._entry_handlers is None: - self._entry_handlers = [ - self.wrapped_widget_connect('enter-notify-event', - self.on_enter_notify), - self.wrapped_widget_connect('leave-notify-event', - self.on_leave_notify), - self.wrapped_widget_connect('button-release-event', - self.on_click) - ] - - def _disconnect_enter_notify_handlers(self): - if self._entry_handlers is not None: - for handle in self._entry_handlers: - self._widget.disconnect(handle) - self._entry_handlers = None - - def set_cursor(self, cursor): - if cursor == widgetconst.CURSOR_NORMAL: - self._gtk_cursor = None - self._disconnect_enter_notify_handlers() - elif cursor == widgetconst.CURSOR_POINTING_HAND: - self._gtk_cursor = gtk.gdk.Cursor(gtk.gdk.HAND2) - self._connect_enter_notify_handlers() - else: - raise ValueError("Unknown cursor: %s" % cursor) - - def on_enter_notify(self, widget, event): - self._widget.window.set_cursor(self._gtk_cursor) - - def on_leave_notify(self, widget, event): - if self._widget.window: - self._widget.window.set_cursor(None) - - def on_click(self, widget, event): - self.emit('clicked') - return True - -class CustomButton(CustomControlBase): - def __init__(self): - """Create a new CustomButton. active_image will be displayed while - the button is pressed. The image must have the same size. - """ - CustomControlBase.__init__(self) - self.set_widget(CustomButtonWidget()) - self.create_signal('clicked') - self.forward_signal('clicked') - -class DragableCustomButton(CustomControlBase): - def __init__(self): - CustomControlBase.__init__(self) - self.set_widget(DragableCustomButtonWidget()) - self.create_signal('clicked') - self.create_signal('dragged-left') - self.create_signal('dragged-right') - -class CustomSlider(CustomControlBase): - def __init__(self): - CustomControlBase.__init__(self) - self.create_signal('pressed') - self.create_signal('released') - self.create_signal('changed') - self.create_signal('moved') - self._scroll_step = None - if self.is_horizontal(): - self.set_widget(CustomHScaleWidget()) - else: - self.set_widget(CustomVScaleWidget()) - self.wrapped_widget_connect('move-slider', self.on_slider_move) - - def on_slider_move(self, widget, scrolltype): - self.emit('changed', widget.get_value()) - self.emit('moved', widget.get_value()) - - def get_value(self): - return self._widget.get_value() - - def set_value(self, value): - self._widget.set_value(value) - - def get_range(self): - return self._widget.get_range() - - def get_slider_pos(self, value=None): - """Get the position for the slider for our current value. - - This will return position that the slider should be centered on to - display the value. It will be the x coordinate if is_horizontal() is - True and the y coordinate otherwise. - - This method takes into acount the size of the slider when calculating - the position. The slider position will start at (slider_size / 2) and - will end (slider_size / 2) px before the end of the widget. - - :param value: value to get the position for. Defaults to the current - value - """ - return self._widget.get_slider_pos(value) - - def set_range(self, min_value, max_value): - self._widget.set_range(min_value, max_value) - # set_digits controls the precision of the scale by limiting changes - # to a certain number of digits. If the range is [0, 1], this code - # will give us 4 digits of precision, which seems reasonable. - range = max_value - min_value - self._widget.set_digits(int(round(math.log10(10000.0 / range)))) - - def set_increments(self, small_step, big_step, scroll_step=None): - """Set the increments to scroll. - - :param small_step: scroll amount for up/down - :param big_step: scroll amount for page up/page down. - :param scroll_step: scroll amount for mouse wheel, or None to make - this 2 times the small step - """ - self._widget.set_increments(small_step, big_step) - self._scroll_step = scroll_step - -def to_miro_volume(value): - """Convert from 0 to 1.0 to 0.0 to MAX_VOLUME. - """ - if value == 0: - return 0.0 - return value * widgetconst.MAX_VOLUME - -def to_gtk_volume(value): - """Convert from 0.0 to MAX_VOLUME to 0 to 1.0. - """ - if value > 0.0: - value = (value / widgetconst.MAX_VOLUME) - return value - -if hasattr(gtk.VolumeButton, "get_popup"): - # FIXME - Miro on Windows has an old version of gtk (2.16) and - # doesn't have the get_popup method. Once we upgrade and - # fix that, we can take out the hasattr check. - - class VolumeMuter(Label): - """Empty space that has a clicked signal so it can be dropped - in place of the VolumeMuter. - """ - def __init__(self): - Label.__init__(self) - self.create_signal("clicked") - - class VolumeSlider(Widget): - """VolumeSlider that uses the gtk.VolumeButton(). - """ - def __init__(self): - Widget.__init__(self) - self.set_widget(gtk.VolumeButton()) - self.wrapped_widget_connect('value-changed', self.on_value_changed) - self._widget.get_popup().connect("hide", self.on_hide) - self.create_signal('changed') - self.create_signal('released') - - def on_value_changed(self, *args): - value = self.get_value() - self.emit('changed', value) - - def on_hide(self, *args): - self.emit('released') - - def get_value(self): - value = self._widget.get_property('value') - return to_miro_volume(value) - - def set_value(self, value): - value = to_gtk_volume(value) - self._widget.set_property('value', value) - -class ClickableImageButton(CustomButton): - """Image that can send clicked events. If max_width and/or max_height are - specified, resizes the image proportionally such that all constraints are - met. - """ - def __init__(self, image_path, max_width=None, max_height=None): - CustomButton.__init__(self) - self.max_width = max_width - self.max_height = max_height - self.image = None - self._width, self._height = None, None - if image_path: - self.set_path(image_path) - self.set_cursor(widgetconst.CURSOR_POINTING_HAND) - - def set_path(self, path): - image = Image(path) - if self.max_width: - image = image.resize_for_space(self.max_width, self.max_height) - self.image = ImageSurface(image) - self._width, self._height = image.width, image.height - - def size_request(self, layout): - w = self._width - h = self._height - if not w: - w = self.max_width - if not h: - h = self.max_height - return w, h - - def draw(self, context, layout): - if self.image: - self.image.draw(context, 0, 0, self._width, self._height) - w = self._width - h = self._height - if not w: - w = self.max_width - if not h: - h = self.max_height - w = min(context.width, w) - h = min(context.height, h) - context.rectangle(0, 0, w, h) - context.set_color((0, 0, 0)) # black - context.set_line_width(1) - context.stroke() diff --git a/mvc/widgets/gtk/drawing.py b/mvc/widgets/gtk/drawing.py deleted file mode 100644 index 5888851..0000000 --- a/mvc/widgets/gtk/drawing.py +++ /dev/null @@ -1,268 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -""".drawing -- Contains classes used to draw on -widgets. -""" - -import cairo -import gobject -import gtk - -import wrappermap -from .base import Widget, Bin -from .layoutmanager import LayoutManager - -def css_to_color(css_string): - parts = (css_string[1:3], css_string[3:5], css_string[5:7]) - return tuple((int(value, 16) / 255.0) for value in parts) - -class ImageSurface: - def __init__(self, image): - format = cairo.FORMAT_RGB24 - if image.pixbuf.get_has_alpha(): - format = cairo.FORMAT_ARGB32 - self.image = cairo.ImageSurface( - format, int(image.width), int(image.height)) - context = cairo.Context(self.image) - gdkcontext = gtk.gdk.CairoContext(context) - gdkcontext.set_source_pixbuf(image.pixbuf, 0, 0) - gdkcontext.paint() - self.pattern = cairo.SurfacePattern(self.image) - self.pattern.set_extend(cairo.EXTEND_REPEAT) - self.width = image.width - self.height = image.height - - def get_size(self): - return self.width, self.height - - def _align_pattern(self, x, y): - """Line up our image pattern so that it's top-left corner is x, y.""" - m = cairo.Matrix() - m.translate(-x, -y) - self.pattern.set_matrix(m) - - def draw(self, context, x, y, width, height, fraction=1.0): - self._align_pattern(x, y) - cairo_context = context.context - cairo_context.save() - cairo_context.set_source(self.pattern) - cairo_context.new_path() - cairo_context.rectangle(x, y, width, height) - if fraction >= 1.0: - cairo_context.fill() - else: - cairo_context.clip() - cairo_context.paint_with_alpha(fraction) - cairo_context.restore() - - def draw_rect(self, context, dest_x, dest_y, source_x, source_y, - width, height, fraction=1.0): - - self._align_pattern(dest_x-source_x, dest_y-source_y) - cairo_context = context.context - cairo_context.save() - cairo_context.set_source(self.pattern) - cairo_context.new_path() - cairo_context.rectangle(dest_x, dest_y, width, height) - if fraction >= 1.0: - cairo_context.fill() - else: - cairo_context.clip() - cairo_context.paint_with_alpha(fraction) - cairo_context.restore() - -class DrawingStyle(object): - def __init__(self, widget, use_base_color=False, state=None): - if state is None: - state = widget._widget.state - self.use_custom_style = widget.use_custom_style - self.style = widget._widget.style - self.text_color = widget.convert_gtk_color(self.style.text[state]) - if use_base_color: - self.bg_color = widget.convert_gtk_color(self.style.base[state]) - else: - self.bg_color = widget.convert_gtk_color(self.style.bg[state]) - -class DrawingContext(object): - """DrawingContext. This basically just wraps a Cairo context and adds a - couple convenience methods. - """ - - def __init__(self, window, drawing_area, expose_area): - self.window = window - self.context = window.cairo_create() - self.context.rectangle(expose_area.x, expose_area.y, - expose_area.width, expose_area.height) - self.context.clip() - self.width = drawing_area.width - self.height = drawing_area.height - self.context.translate(drawing_area.x, drawing_area.y) - - def __getattr__(self, name): - return getattr(self.context, name) - - def set_color(self, (red, green, blue), alpha=1.0): - self.context.set_source_rgba(red, green, blue, alpha) - - def set_shadow(self, color, opacity, offset, blur_radius): - pass - - def gradient_fill(self, gradient): - old_source = self.context.get_source() - self.context.set_source(gradient.pattern) - self.context.fill() - self.context.set_source(old_source) - - def gradient_fill_preserve(self, gradient): - old_source = self.context.get_source() - self.context.set_source(gradient.pattern) - self.context.fill_preserve() - self.context.set_source(old_source) - -class Gradient(object): - def __init__(self, x1, y1, x2, y2): - self.pattern = cairo.LinearGradient(x1, y1, x2, y2) - - def set_start_color(self, (red, green, blue)): - self.pattern.add_color_stop_rgb(0, red, green, blue) - - def set_end_color(self, (red, green, blue)): - self.pattern.add_color_stop_rgb(1, red, green, blue) - -class CustomDrawingMixin(object): - def do_expose_event(self, event): - wrapper = wrappermap.wrapper(self) - if self.flags() & gtk.NO_WINDOW: - drawing_area = self.allocation - else: - drawing_area = gtk.gdk.Rectangle(0, 0, - self.allocation.width, self.allocation.height) - context = DrawingContext(event.window, drawing_area, event.area) - context.style = DrawingStyle(wrapper) - if self.flags() & gtk.CAN_FOCUS: - focus_space = (self.style_get_property('focus-padding') + - self.style_get_property('focus-line-width')) - if not wrapper.squish_width: - context.width -= focus_space * 2 - translate_x = focus_space - else: - translate_x = 0 - if not wrapper.squish_height: - context.height -= focus_space * 2 - translate_y = focus_space - else: - translate_y = 0 - context.translate(translate_x, translate_y) - wrapper.layout_manager.update_cairo_context(context.context) - self.draw(wrapper, context) - - def draw(self, wrapper, context): - wrapper.layout_manager.reset() - wrapper.draw(context, wrapper.layout_manager) - - def do_size_request(self, requesition): - wrapper = wrappermap.wrapper(self) - width, height = wrapper.size_request(wrapper.layout_manager) - requesition.width = width - requesition.height = height - if self.flags() & gtk.CAN_FOCUS: - focus_space = (self.style_get_property('focus-padding') + - self.style_get_property('focus-line-width')) - if not wrapper.squish_width: - requesition.width += focus_space * 2 - if not wrapper.squish_height: - requesition.height += focus_space * 2 - -class MiroDrawingArea(CustomDrawingMixin, gtk.Widget): - def __init__(self): - gtk.Widget.__init__(self) - CustomDrawingMixin.__init__(self) - self.set_flags(gtk.NO_WINDOW) - -class BackgroundWidget(CustomDrawingMixin, gtk.Bin): - def do_size_request(self, requesition): - CustomDrawingMixin.do_size_request(self, requesition) - if self.get_child(): - child_width, child_height = self.get_child().size_request() - requesition.width = max(child_width, requesition.width) - requesition.height = max(child_height, requesition.height) - - def do_expose_event(self, event): - CustomDrawingMixin.do_expose_event(self, event) - if self.get_child(): - self.propagate_expose(self.get_child(), event) - - def do_size_allocate(self, allocation): - gtk.Bin.do_size_allocate(self, allocation) - if self.get_child(): - self.get_child().size_allocate(allocation) - -gobject.type_register(MiroDrawingArea) -gobject.type_register(BackgroundWidget) - -class Drawable: - def __init__(self): - self.squish_width = self.squish_height = False - - def set_squish_width(self, setting): - self.squish_width = setting - - def set_squish_height(self, setting): - self.squish_height = setting - - def set_widget(self, drawing_widget): - if self.is_opaque() and 0: - box = gtk.EventBox() - box.add(drawing_widget) - Widget.set_widget(self, box) - else: - Widget.set_widget(self, drawing_widget) - self.layout_manager = LayoutManager(self._widget) - - def size_request(self, layout_manager): - return 0, 0 - - def draw(self, context, layout_manager): - pass - - def is_opaque(self): - return False - -class DrawingArea(Drawable, Widget): - def __init__(self): - Widget.__init__(self) - Drawable.__init__(self) - self.set_widget(MiroDrawingArea()) - -class Background(Drawable, Bin): - def __init__(self): - Bin.__init__(self) - Drawable.__init__(self) - self.set_widget(BackgroundWidget()) diff --git a/mvc/widgets/gtk/gtkmenus.py b/mvc/widgets/gtk/gtkmenus.py deleted file mode 100644 index 926ba15..0000000 --- a/mvc/widgets/gtk/gtkmenus.py +++ /dev/null @@ -1,404 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""gtkmenus.py -- Manage menu layout.""" - -import gtk - -from mvc.widgets import app - -import base -import keymap -import wrappermap - -def _setup_accel(widget, name, shortcut=None): - """Setup accelerators for a menu item. - - This method sets an accel path for the widget and optionally connects a - shortcut to that accel path. - """ - # The GTK docs say that we should set the path using this form: - # <Window-Name>/Menu/Submenu/MenuItem - # ...but this is hard to do because we don't yet know what window/menu - # this menu item is going to be added to. gtk.Action and gtk.ActionGroup - # don't follow the above suggestion, so we don't need to either. - path = "<MiroActions>/MenuBar/%s" % name - widget.set_accel_path(path) - if shortcut is not None: - accel_string = keymap.get_accel_string(shortcut) - key, mods = gtk.accelerator_parse(accel_string) - if gtk.accel_map_lookup_entry(path) is None: - gtk.accel_map_add_entry(path, key, mods) - else: - gtk.accel_map_change_entry(path, key, mods, True) - -# map menu names to GTK stock ids. -_STOCK_IDS = { - "SaveItem": gtk.STOCK_SAVE, - "CopyItemURL": gtk.STOCK_COPY, - "RemoveItems": gtk.STOCK_REMOVE, - "StopItem": gtk.STOCK_MEDIA_STOP, - "NextItem": gtk.STOCK_MEDIA_NEXT, - "PreviousItem": gtk.STOCK_MEDIA_PREVIOUS, - "PlayPauseItem": gtk.STOCK_MEDIA_PLAY, - "Open": gtk.STOCK_OPEN, - "EditPreferences": gtk.STOCK_PREFERENCES, - "Quit": gtk.STOCK_QUIT, - "Help": gtk.STOCK_HELP, - "About": gtk.STOCK_ABOUT, - "Translate": gtk.STOCK_EDIT -} -try: - _STOCK_IDS['Fullscreen'] = gtk.STOCK_FULLSCREEN -except AttributeError: - # fullscreen not available on all GTK versions - pass - -class MenuItemBase(base.Widget): - """Base class for MenuItem and Separator.""" - - def show(self): - """Show this menu item.""" - self._widget.show() - - def hide(self): - """Hide and disable this menu item.""" - self._widget.hide() - - def remove_from_parent(self): - """Remove this menu item from it's parent Menu.""" - parent_menu = self._widget.get_parent() - if parent_menu is None: - return - parent_menu_item = parent_menu.get_attach_widget() - if parent_menu_item is None: - return - parent_menu_item.remove(self._widget) - - def _set_accel_group(self, accel_group): - # menu items don't care about the accel group, their parent Menu - # handles it for them - pass - -class MenuItem(MenuItemBase): - """Single item in the menu that can be clicked - - :param label: The label it has (must be internationalized) - :param name: String identifier for this item - :param shortcut: Shortcut object to use - - Signals: - - activate: menu item was clicked - - Example: - - >>> MenuItem(_("Preferences"), "EditPreferences") - >>> MenuItem(_("Cu_t"), "ClipboardCut", Shortcut("x", MOD)) - >>> MenuItem(_("_Update Podcasts and Library"), "UpdatePodcasts", - ... (Shortcut("r", MOD), Shortcut(F5))) - >>> MenuItem(_("_Play"), "PlayPauseItem", - ... play=_("_Play"), pause=_("_Pause")) - """ - - def __init__(self, label, name, shortcut=None): - MenuItemBase.__init__(self) - self.name = name - self.set_widget(self.make_widget(label)) - self.activate_id = self.wrapped_widget_connect('activate', - self._on_activate) - self._widget.show() - self.create_signal('activate') - _setup_accel(self._widget, self.name, shortcut) - - def _on_activate(self, menu_item): - self.emit('activate') - gtk_menubar = self._find_menubar() - if gtk_menubar is not None: - try: - menubar = wrappermap.wrapper(gtk_menubar) - except KeyError: - logging.exception('menubar activate: ' - 'no wrapper for gtbbk.MenuBar') - else: - menubar.emit('activate', self.name) - - def _find_menubar(self): - """Find the MenuBar that this menu item is attached to.""" - menu_item = self._widget - while True: - parent_menu = menu_item.get_parent() - if isinstance(parent_menu, gtk.MenuBar): - return parent_menu - elif parent_menu is None: - return None - menu_item = parent_menu.get_attach_widget() - if menu_item is None: - return None - - def make_widget(self, label): - """Create the menu item to use for this widget. - - Subclasses will probably want to override this. - """ - if self.name in _STOCK_IDS: - mi = gtk.ImageMenuItem(stock_id=_STOCK_IDS[self.name]) - mi.set_label(label) - return mi - else: - return gtk.MenuItem(label) - - def set_label(self, new_label): - self._widget.set_label(new_label) - - def get_label(self): - self._widget.get_label() - -class CheckMenuItem(MenuItem): - """MenuItem that toggles on/off""" - - def make_widget(self, label): - return gtk.CheckMenuItem(label) - - def set_state(self, active): - # prevent the activate signal from fireing in response to us manually - # changing a value - self._widget.handler_block(self.activate_id) - if active is not None: - self._widget.set_inconsistent(False) - self._widget.set_active(active) - else: - self._widget.set_inconsistent(True) - self._widget.set_active(False) - self._widget.handler_unblock(self.activate_id) - - def get_state(self): - return self._widget.get_active() - -class RadioMenuItem(CheckMenuItem): - """MenuItem that toggles on/off and is grouped with other RadioMenuItems. - """ - - def make_widget(self, label): - widget = gtk.RadioMenuItem() - widget.set_label(label) - return widget - - def set_group(self, group_item): - self._widget.set_group(group_item._widget) - - def remove_from_group(self): - """Remove this RadioMenuItem from its current group.""" - self._widget.set_group(None) - - def _on_activate(self, menu_item): - # GTK sends the activate signal for both the radio button that's - # toggled on and the one that gets turned off. Just emit our signal - # for the active radio button. - if self.get_state(): - MenuItem._on_activate(self, menu_item) - -class Separator(MenuItemBase): - """Separator item for menus""" - - def __init__(self): - MenuItemBase.__init__(self) - self.set_widget(gtk.SeparatorMenuItem()) - self._widget.show() - # Set name to be None just so that it has a similar API to other menu - # items. - self.name = None - -class MenuShell(base.Widget): - """Common code shared between Menu and MenuBar. - - Subclasses must define a _menu attribute that's a gtk.MenuShell subclass. - """ - - def __init__(self): - base.Widget.__init__(self) - self._accel_group = None - self.children = [] - - def append(self, menu_item): - """Add a menu item to the end of this menu.""" - self.children.append(menu_item) - menu_item._set_accel_group(self._accel_group) - self._menu.append(menu_item._widget) - - def insert(self, index, menu_item): - """Insert a menu item in the middle of this menu.""" - self.children.insert(index, menu_item) - menu_item._set_accel_group(self._accel_group) - self._menu.insert(menu_item._widget, index) - - def remove(self, menu_item): - """Remove a child menu item. - - :raises ValueError: menu_item is not a child of this menu - """ - self.children.remove(menu_item) - self._menu.remove(menu_item._widget) - menu_item._set_accel_group(None) - - def index(self, name): - """Get the position of a menu item in this list. - - :param name: name of the menu - :returns: index of the menu item, or -1 if not found. - """ - for i, menu_item in enumerate(self.children): - if menu_item.name == name: - return i - return -1 - - def get_children(self): - """Get the child menu items in order.""" - return list(self.children) - - def find(self, name): - """Search for a menu or menu item - - This method recursively searches the entire menu structure for a Menu - or MenuItem object with a given name. - - :raises KeyError: name not found - """ - found = self._find(name) - if found is None: - raise KeyError(name) - else: - return found - - def _find(self, name): - """Low-level helper-method for find(). - - :returns: found menu item or None. - """ - for menu_item in self.get_children(): - if menu_item.name == name: - return menu_item - if isinstance(menu_item, MenuShell): - submenu_find = menu_item._find(name) - if submenu_find is not None: - return submenu_find - return None - -class Menu(MenuShell): - """A Menu holds a list of MenuItems and Menus. - - Example: - >>> Menu(_("P_layback"), "Playback", [ - ... MenuItem(_("_Foo"), "Foo"), - ... MenuItem(_("_Bar"), "Bar") - ... ]) - >>> Menu("", "toplevel", [ - ... Menu(_("_File"), "File", [ ... ]) - ... ]) - """ - - def __init__(self, label, name, child_items): - MenuShell.__init__(self) - self.set_widget(gtk.MenuItem(label)) - self._widget.show() - self.name = name - # set up _menu for the MenuShell code - self._menu = gtk.Menu() - _setup_accel(self._menu, self.name) - self._widget.set_submenu(self._menu) - for item in child_items: - self.append(item) - - def show(self): - """Show this menu.""" - self._widget.show() - - def hide(self): - """Hide this menu.""" - self._widget.hide() - - def _set_accel_group(self, accel_group): - """Set the accel group for this widget. - - Accel groups get created by the MenuBar. Whenever a menu or menu item - is added to that menu bar, the parent calls _set_accel_group() to give - the accel group to the child. - """ - if accel_group == self._accel_group: - return - self._menu.set_accel_group(accel_group) - self._accel_group = accel_group - for child in self.children: - child._set_accel_group(accel_group) - -class MenuBar(MenuShell): - """Displays a list of Menu items. - - Signals: - - - activate(menu_bar, name): a menu item was activated - """ - - def __init__(self): - """Create a new MenuBar - - :param name: string id to use for our action group - """ - MenuShell.__init__(self) - self.create_signal('activate') - self.set_widget(gtk.MenuBar()) - self._widget.show() - self._accel_group = gtk.AccelGroup() - # set up _menu for the MenuShell code - self._menu = self._widget - - def get_accel_group(self): - return self._accel_group - -class MainWindowMenuBar(MenuBar): - """MenuBar for the main window. - - This gets installed into app.widgetapp.menubar on GTK. - """ - def add_initial_menus(self, menus): - """Add the initial set of menus. - - We modify the menu structure slightly for GTK. - """ - for menu in menus: - self.append(menu) - self._modify_initial_menus() - - def _modify_initial_menus(self): - """Update the portable root menu with GTK-specific stuff.""" - # on linux, we don't have a CheckVersion option because - # we update with the package system. - #this_platform = app.config.get(prefs.APP_PLATFORM) - #if this_platform == 'linux': - # self.find("CheckVersion").remove_from_parent() - #app.video_renderer.setup_subtitle_encoding_menu() diff --git a/mvc/widgets/gtk/keymap.py b/mvc/widgets/gtk/keymap.py deleted file mode 100644 index cf341ff..0000000 --- a/mvc/widgets/gtk/keymap.py +++ /dev/null @@ -1,94 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""keymap.py -- Map portable key values to GTK ones. -""" - -import gtk - -from mvc.widgets import keyboard - -menubar_mod_map = { - keyboard.MOD: '<Ctrl>', - keyboard.CTRL: '<Ctrl>', - keyboard.ALT: '<Alt>', - keyboard.SHIFT: '<Shift>', -} - -menubar_key_map = { - keyboard.RIGHT_ARROW: 'Right', - keyboard.LEFT_ARROW: 'Left', - keyboard.UP_ARROW: 'Up', - keyboard.DOWN_ARROW: 'Down', - keyboard.SPACE: 'space', - keyboard.ENTER: 'Return', - keyboard.DELETE: 'Delete', - keyboard.BKSPACE: 'BackSpace', - keyboard.ESCAPE: 'Escape', - '>': 'greater', - '<': 'less' -} -for i in range(1, 13): - name = 'F%d' % i - menubar_key_map[getattr(keyboard, name)] = name - -# These are reversed versions of menubar_key_map and menubar_mod_map -gtk_key_map = dict((i[1], i[0]) for i in menubar_key_map.items()) - -def get_accel_string(shortcut): - mod_str = ''.join(menubar_mod_map[mod] for mod in shortcut.modifiers) - key_str = menubar_key_map.get(shortcut.shortcut, shortcut.shortcut) - return mod_str + key_str - -def translate_gtk_modifiers(event): - """Convert a keypress event to a set of modifiers from the shortcut - module. - """ - modifiers = set() - if event.state & gtk.gdk.CONTROL_MASK: - modifiers.add(keyboard.CTRL) - if event.state & gtk.gdk.MOD1_MASK: - modifiers.add(keyboard.ALT) - if event.state & gtk.gdk.SHIFT_MASK: - modifiers.add(keyboard.SHIFT) - return modifiers - -def translate_gtk_event(event): - """Convert a GTK key event into the tuple (key, modifiers) where - key and modifiers are from the shortcut module. - """ - gtk_keyval = gtk.gdk.keyval_name(event.keyval) - if gtk_keyval == None: - return None - if len(gtk_keyval) == 1: - key = gtk_keyval - else: - key = gtk_key_map.get(gtk_keyval) - modifiers = translate_gtk_modifiers(event) - return key, modifiers diff --git a/mvc/widgets/gtk/layout.py b/mvc/widgets/gtk/layout.py deleted file mode 100644 index d887fcb..0000000 --- a/mvc/widgets/gtk/layout.py +++ /dev/null @@ -1,227 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -""".layout -- Layout widgets. """ - -import gtk - -from mvc.utils import Matrix -from .base import Widget, Bin - -class Box(Widget): - def __init__(self, spacing=0): - Widget.__init__(self) - self.children = set() - self.set_widget(self.WIDGET_CLASS(spacing=spacing)) - - def pack_start(self, widget, expand=False, padding=0): - self._widget.pack_start(widget._widget, expand, fill=True, - padding=padding) - widget._widget.show() - self.children.add(widget) - - def pack_end(self, widget, expand=False, padding=0): - self._widget.pack_end(widget._widget, expand, fill=True, - padding=padding) - widget._widget.show() - self.children.add(widget) - - def remove(self, widget): - widget._widget.hide() # otherwise gtkmozembed gets confused - self._widget.remove(widget._widget) - self.children.remove(widget) - - def enable(self): - for mem in self.children: - mem.enable() - - def disable(self): - for mem in self.children: - mem.disable() - -class HBox(Box): - WIDGET_CLASS = gtk.HBox - -class VBox(Box): - WIDGET_CLASS = gtk.VBox - -class Alignment(Bin): - def __init__(self, xalign=0, yalign=0, xscale=0, yscale=0, - top_pad=0, bottom_pad=0, left_pad=0, right_pad=0): - Bin.__init__(self) - self.set_widget(gtk.Alignment(xalign, yalign, xscale, yscale)) - self.set_padding(top_pad, bottom_pad, left_pad, right_pad) - - def set(self, xalign=0, yalign=0, xscale=0, yscale=0): - self._widget.set(xalign, yalign, xscale, yscale) - - def set_padding(self, top_pad=0, bottom_pad=0, left_pad=0, right_pad=0): - self._widget.set_padding(top_pad, bottom_pad, left_pad, right_pad) - -class DetachedWindowHolder(Alignment): - def __init__(self): - Alignment.__init__(self, xscale=1, yscale=1) - -class Splitter(Widget): - def __init__(self): - """Create a new splitter.""" - Widget.__init__(self) - self.set_widget(gtk.HPaned()) - - def set_left(self, widget): - """Set the left child widget.""" - self.left = widget - self._widget.pack1(widget._widget, resize=False, shrink=False) - widget._widget.show() - - def set_right(self, widget): - """Set the right child widget. """ - self.right = widget - self._widget.pack2(widget._widget, resize=True, shrink=False) - widget._widget.show() - - def remove_left(self): - """Remove the left child widget.""" - if self.left is not None: - self.left._widget.hide() # otherwise gtkmozembed gets confused - self._widget.remove(self.left._widget) - self.left = None - - def remove_right(self): - """Remove the right child widget.""" - if self.right is not None: - self.right._widget.hide() # otherwise gtkmozembed gets confused - self._widget.remove(self.right._widget) - self.right = None - - def set_left_width(self, width): - self._widget.set_position(width) - - def get_left_width(self): - return self._widget.get_position() - - def set_right_width(self, width): - self._widget.set_position(self.width - width) - # We should take into account the width of the bar, but this seems - # good enough. - -class Table(Widget): - """Lays out widgets in a table. It works very similar to the GTK Table - widget, or an HTML table. - """ - def __init__(self, columns, rows): - Widget.__init__(self) - self.set_widget(gtk.Table(rows, columns, homogeneous=False)) - self.children = Matrix(columns, rows) - - def pack(self, widget, column, row, column_span=1, row_span=1): - """Add a widget to the table. - """ - self.children[column, row] = widget - self._widget.attach(widget._widget, column, column + column_span, - row, row + row_span) - widget._widget.show() - - def remove(self, widget): - widget._widget.hide() # otherwise gtkmozembed gets confused - self.children.remove(widget) - self._widget.remove(widget._widget) - - def set_column_spacing(self, spacing): - self._widget.set_col_spacings(spacing) - - def set_row_spacing(self, spacing): - self._widget.set_row_spacings(spacing) - - def enable(self, row=None, column=None): - if row != None and column != None: - if self.children[column, row]: - self.children[column, row].enable() - elif row != None: - for mem in self.children.row(row): - if mem: mem.enable() - elif column != None: - for mem in self.children.column(column): - if mem: mem.enable() - else: - for mem in self.children: - if mem: mem.enable() - - def disable(self, row=None, column=None): - if row != None and column != None: - if self.children[column, row]: - self.children[column, row].disable() - elif row != None: - for mem in self.children.row(row): - if mem: mem.disable() - elif column != None: - for mem in self.children.column(column): - if mem: mem.disable() - else: - for mem in self.children: - if mem: mem.disable() - -class TabContainer(Widget): - def __init__(self, xalign=0, yalign=0, xscale=0, yscale=0, - top_pad=0, bottom_pad=0, left_pad=0, right_pad=0): - Widget.__init__(self) - self.set_widget(gtk.Notebook()) - self._widget.set_tab_pos(gtk.POS_TOP) - self.children = [] - self._page_to_select = None - self.wrapped_widget_connect('realize', self._on_realize) - - def _on_realize(self, widget): - if self._page_to_select is not None: - self._widget.set_current_page(self._page_to_select) - self._page_to_select = None - - def append_tab(self, child_widget, text, image=None): - if image is not None: - label_widget = gtk.VBox(spacing=2) - image_widget = gtk.Image() - image_widget.set_from_pixbuf(image.pixbuf) - label_widget.pack_start(image_widget) - label_widget.pack_start(gtk.Label(text)) - label_widget.show_all() - else: - label_widget = gtk.Label(text) - - # switch from a center align to a top align - child_widget.set(0, 0, 1, 0) - child_widget.set_padding(10, 10, 10, 10) - - self._widget.append_page(child_widget._widget, label_widget) - self.children.append(child_widget) - - def select_tab(self, index): - if self._widget.flags() & gtk.REALIZED: - self._widget.set_current_page(index) - else: - self._page_to_select = index diff --git a/mvc/widgets/gtk/layoutmanager.py b/mvc/widgets/gtk/layoutmanager.py deleted file mode 100644 index fb60049..0000000 --- a/mvc/widgets/gtk/layoutmanager.py +++ /dev/null @@ -1,550 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""drawing.py -- Contains the LayoutManager class. LayoutManager is -handles laying out complex objects for the custom drawing code like -text blocks and buttons. -""" - -import math - -import cairo -import gtk -import pango - -from mvc import utils - -use_native_buttons = False # not implemented in MVC - -class FontCache(utils.Cache): - def get(self, context, description, scale_factor, bold, italic): - key = (context, description, scale_factor, bold, italic) - return utils.Cache.get(self, key) - - def create_new_value(self, key, invalidator=None): - (context, description, scale_factor, bold, italic) = key - return Font(context, description, scale_factor, bold, italic) - -_font_cache = FontCache(512) - -class LayoutManager(object): - def __init__(self, widget): - self.pango_context = widget.get_pango_context() - self.update_style(widget.style) - self.update_direction(widget.get_direction()) - widget.connect('style-set', self.on_style_set) - widget.connect('direction-changed', self.on_direction_changed) - self.widget = widget - self.reset() - - def reset(self): - self.current_font = self.font(1.0) - self.text_color = (0, 0, 0) - self.text_shadow = None - - def on_style_set(self, widget, previous_style): - old_font_desc = self.style_font_desc - self.update_style(widget.style) - if self.style_font_desc != old_font_desc: - # bug #17423 font changed, so the widget's width might have changed - widget.queue_resize() - - def on_direction_changed(self, widget, previous_direction): - self.update_direction(widget.get_direction()) - - def update_style(self, style): - self.style_font_desc = style.font_desc - self.style = style - - def update_direction(self, direction): - if direction == gtk.TEXT_DIR_RTL: - self.pango_context.set_base_dir(pango.DIRECTION_RTL) - else: - self.pango_context.set_base_dir(pango.DIRECTION_LTR) - - def font(self, scale_factor, bold=False, italic=False, family=None): - return _font_cache.get(self.pango_context, self.style_font_desc, - scale_factor, bold, italic) - - def set_font(self, scale_factor, bold=False, italic=False, family=None): - self.current_font = self.font(scale_factor, bold, italic) - - def set_text_color(self, color): - self.text_color = color - - def set_text_shadow(self, shadow): - self.text_shadow = shadow - - def textbox(self, text, underline=False): - textbox = TextBox(self.pango_context, self.current_font, - self.text_color, self.text_shadow) - textbox.set_text(text, underline=underline) - return textbox - - def button(self, text, pressed=False, disabled=False, style='normal'): - if style == 'webby': - return StyledButton(text, self.pango_context, self.current_font, - pressed, disabled) - elif use_native_buttons: - return NativeButton(text, self.pango_context, self.current_font, - pressed, self.style, self.widget) - else: - return StyledButton(text, self.pango_context, self.current_font, - pressed) - - def update_cairo_context(self, cairo_context): - cairo_context.update_context(self.pango_context) - -class Font(object): - def __init__(self, context, style_font_desc, scale, bold, italic): - self.context = context - self.description = style_font_desc.copy() - self.description.set_size(int(scale * style_font_desc.get_size())) - if bold: - self.description.set_weight(pango.WEIGHT_BOLD) - if italic: - self.description.set_style(pango.STYLE_ITALIC) - self.font_metrics = None - - def get_font_metrics(self): - if self.font_metrics is None: - self.font_metrics = self.context.get_metrics(self.description) - return self.font_metrics - - def ascent(self): - return pango.PIXELS(self.get_font_metrics().get_ascent()) - - def descent(self): - return pango.PIXELS(self.get_font_metrics().get_descent()) - - def line_height(self): - metrics = self.get_font_metrics() - # the +1: some glyphs can be slightly taller than ascent+descent - # (#17329) - return (pango.PIXELS(metrics.get_ascent()) + - pango.PIXELS(metrics.get_descent()) + 1) - -class TextBox(object): - def __init__(self, context, font, color, shadow): - self.layout = pango.Layout(context) - self.layout.set_wrap(pango.WRAP_WORD_CHAR) - self.font = font - self.color = color - self.layout.set_font_description(font.description.copy()) - self.width = self.height = None - self.shadow = shadow - - def set_text(self, text, font=None, color=None, underline=False): - self.text_chunks = [] - self.attributes = [] - self.text_length = 0 - self.underlines = [] - self.append_text(text, font, color, underline) - - def append_text(self, text, font=None, color=None, underline=False): - if text == None: - text = u"" - startpos = self.text_length - self.text_chunks.append(text) - endpos = self.text_length = self.text_length + len(text) - if font is not None: - attr = pango.AttrFontDesc(font.description, startpos, endpos) - self.attributes.append(attr) - if underline: - self.underlines.append((startpos, endpos)) - if color: - def convert(value): - return int(round(value * 65535)) - attr = pango.AttrForeground(convert(color[0]), convert(color[1]), - convert(color[2]), startpos, endpos) - self.attributes.append(attr) - self.text_set = False - - def set_width(self, width): - if width is not None: - self.layout.set_width(int(width * pango.SCALE)) - else: - self.layout.set_width(-1) - self.width = width - - def set_height(self, height): - # if height is not None: - # # not sure why set_height isn't in the python bindings, but it - # # isn't - # pygtkhacks.set_pango_layout_height(self.layout, - # int(height * pango.SCALE)) - self.height = height - - def set_wrap_style(self, wrap): - if wrap == 'word': - self.layout.set_wrap(pango.WRAP_WORD_CHAR) - elif wrap == 'char' or wrap == 'truncated-char': - self.layout.set_wrap(pango.WRAP_CHAR) - else: - raise ValueError("Unknown wrap value: %s" % wrap) - if wrap == 'truncated-char': - self.layout.set_ellipsize(pango.ELLIPSIZE_END) - else: - self.layout.set_ellipsize(pango.ELLIPSIZE_NONE) - - def set_alignment(self, align): - if align == 'left': - self.layout.set_alignment(pango.ALIGN_LEFT) - elif align == 'right': - self.layout.set_alignment(pango.ALIGN_RIGHT) - elif align == 'center': - self.layout.set_alignment(pango.ALIGN_CENTER) - else: - raise ValueError("Unknown align value: %s" % align) - - def ensure_layout(self): - if not self.text_set: - text = ''.join(self.text_chunks) - if len(text) > 100: - text = text[:self._calc_text_cutoff()] - self.layout.set_text(text) - attr_list = pango.AttrList() - for attr in self.attributes: - attr_list.insert(attr) - self.layout.set_attributes(attr_list) - self.text_set = True - - def _calc_text_cutoff(self): - """This method is a bit of a hack... GTK slows down if we pass too - much text to the layout. Even text that falls below our height has a - performance penalty. Try not to have too much more than is necessary. - """ - if None in (self.width, self.height): - return -1 - - chars_per_line = (self.width * pango.SCALE // - self.font.get_font_metrics().get_approximate_char_width()) - lines_available = self.height // self.font.line_height() - # overestimate these because it's better to have too many characters - # than too little. - return int(chars_per_line * lines_available * 1.2) - - def line_count(self): - self.ensure_layout() - return self.layout.get_line_count() - - def get_size(self): - self.ensure_layout() - return self.layout.get_pixel_size() - - def char_at(self, x, y): - self.ensure_layout() - x *= pango.SCALE - y *= pango.SCALE - width, height = self.layout.get_size() - if 0 <= x < width and 0 <= y < height: - index, leading = self.layout.xy_to_index(x, y) - # xy_to_index returns the nearest character, but that - # doesn't mean the user actually clicked on it. Double - # check that (x, y) is actually inside that char's - # bounding box - char_x, char_y, char_w, char_h = self.layout.index_to_pos(index) - if char_w > 0: # the glyph is LTR - left = char_x - right = char_x + char_w - else: # the glyph is RTL - left = char_x + char_w - right = char_x - if left <= x < right: - return index - return None - - - def draw(self, context, x, y, width, height): - self.set_width(width) - self.set_height(height) - self.ensure_layout() - cairo_context = context.context - cairo_context.save() - underline_drawer = UnderlineDrawer(self.underlines) - if self.shadow: - # draw shadow first so that it's underneath the regular text - # FIXME: we don't use the blur_radius setting - cairo_context.set_source_rgba(self.shadow.color[0], - self.shadow.color[1], self.shadow.color[2], - self.shadow.opacity) - self._draw_layout(context, x + self.shadow.offset[0], - y + self.shadow.offset[1], width, height, - underline_drawer) - cairo_context.set_source_rgb(*self.color) - self._draw_layout(context, x, y, width, height, underline_drawer) - cairo_context.restore() - cairo_context.new_path() - - def _draw_layout(self, context, x, y, width, height, underline_drawer): - line_height = 0 - alignment = self.layout.get_alignment() - for i in xrange(self.layout.get_line_count()): - line = self.layout.get_line_readonly(i) - extents = line.get_pixel_extents()[1] - next_line_height = line_height + extents[3] - if next_line_height > height: - break - if alignment == pango.ALIGN_CENTER: - line_x = max(x, x + (width - extents[2]) / 2.0) - elif alignment == pango.ALIGN_RIGHT: - line_x = max(x, x + width - extents[2]) - else: - line_x = x - baseline = y + line_height + pango.ASCENT(extents) - context.move_to(line_x, baseline) - context.context.show_layout_line(line) - underline_drawer.draw(context, line_x, baseline, line) - line_height = next_line_height - -class UnderlineDrawer(object): - """Class to draw our own underlines because cairo's don't look - that great at small fonts. We make sure that the underline is - always drawn at a pixel boundary and that there always is space - between the text and the baseline. - - This class makes a couple assumptions that might not be that - great. It assumes that the correct underline size is 1 pixel and - that the text color doesn't change in the middle of an underline. - """ - def __init__(self, underlines): - self.underline_iter = iter(underlines) - self.finished = False - self.next_underline() - - def next_underline(self): - try: - self.startpos, self.endpos = self.underline_iter.next() - except StopIteration: - self.finished = True - else: - # endpos is the char to stop underlining at - self.endpos -= 1 - - def draw(self, context, x, baseline, line): - baseline = round(baseline) + 0.5 - context.set_line_width(1) - while not self.finished and line.start_index <= self.startpos: - startpos = max(line.start_index, self.startpos) - endpos = min(self.endpos, line.start_index + line.length) - x1 = x + pango.PIXELS(line.index_to_x(startpos, 0)) - x2 = x + pango.PIXELS(line.index_to_x(endpos, 1)) - context.move_to(x1, baseline + 1) - context.line_to(x2, baseline + 1) - context.stroke() - if endpos < self.endpos: - break - else: - self.next_underline() - -class NativeButton(object): - ICON_PAD = 4 - - def __init__(self, text, context, font, pressed, style, widget): - self.layout = pango.Layout(context) - self.font = font - self.pressed = pressed - self.layout.set_font_description(font.description.copy()) - self.layout.set_text(text) - self.pad_x = style.xthickness + 11 - self.pad_y = style.ythickness + 1 - self.style = style - self.widget = widget - # The above code assumes an "inner-border" style property of - # 1. PyGTK doesn't seem to support Border objects very well, - # so can't get it from the widget style. - self.min_width = 0 - self.icon = None - - def set_min_width(self, width): - self.min_width = width - - def set_icon(self, icon): - self.icon = icon - - def get_size(self): - width, height = self.layout.get_pixel_size() - if self.icon: - width += self.icon.width + self.ICON_PAD - height = max(height, self.icon.height) - width += self.pad_x * 2 - height += self.pad_y * 2 - return max(self.min_width, width), height - - def draw(self, context, x, y, width, height): - text_width, text_height = self.layout.get_pixel_size() - if self.icon: - inner_width = text_width + self.icon.width + self.ICON_PAD - # calculate the icon position x and y are still in cairo - # coordinates - icon_x = x + (width - inner_width) / 2.0 - icon_y = y + (height - self.icon.height) / 2.0 - text_x = icon_x + self.icon.width + self.ICON_PAD - else: - text_x = x + (width - text_width) / 2.0 - text_y = y + (height - text_height) / 2.0 - - x, y = context.context.user_to_device(x, y) - text_x, text_y = context.context.user_to_device(text_x, text_y) - # Hmm, maybe we should somehow support floating point numbers - # here, but I don't know how to. - x, y, width, height = (int(f) for f in (x, y, width, height)) - context.context.get_target().flush() - self.draw_box(context.window, x, y, width, height) - self.draw_text(context.window, text_x, text_y) - if self.icon: - self.icon.draw(context, icon_x, icon_y, self.icon.width, - self.icon.height) - - def draw_box(self, window, x, y, width, height): - if self.pressed: - shadow = gtk.SHADOW_IN - state = gtk.STATE_ACTIVE - else: - shadow = gtk.SHADOW_OUT - state = gtk.STATE_NORMAL - if 'QtCurveStyle' in str(self.style): - # This is a horrible hack for the libqtcurve library. See - # http://bugzilla.pculture.org/show_bug.cgi?id=10380 for - # details - widget = window.get_user_data() - else: - widget = self.widget - - self.style.paint_box(window, state, shadow, None, widget, "button", - int(x), int(y), int(width), int(height)) - - def draw_text(self, window, x, y): - if self.pressed: - state = gtk.STATE_ACTIVE - else: - state = gtk.STATE_NORMAL - self.style.paint_layout(window, state, True, None, None, None, - int(x), int(y), self.layout) - -class StyledButton(object): - PAD_HORIZONTAL = 4 - PAD_VERTICAL = 3 - TOP_COLOR = (1, 1, 1) - BOTTOM_COLOR = (0.86, 0.86, 0.86) - LINE_COLOR_TOP = (0.71, 0.71, 0.71) - LINE_COLOR_BOTTOM = (0.45, 0.45, 0.45) - TEXT_COLOR = (0.184, 0.184, 0.184) - DISABLED_COLOR = (0.86, 0.86, 0.86) - DISABLED_TEXT_COLOR = (0.5, 0.5, 0.5) - ICON_PAD = 8 - - def __init__(self, text, context, font, pressed, disabled=False): - self.layout = pango.Layout(context) - self.font = font - self.layout.set_font_description(font.description.copy()) - self.layout.set_text(text) - self.min_width = 0 - self.pressed = pressed - self.disabled = disabled - self.icon = None - - def set_icon(self, icon): - self.icon = icon - - def set_min_width(self, width): - self.min_width = width - - def get_size(self): - width, height = self.layout.get_pixel_size() - if self.icon: - width += self.icon.width + self.ICON_PAD - height = max(height, self.icon.height) - height += self.PAD_VERTICAL * 2 - if height % 2 == 1: - # make height even so that the radius of our circle is - # whole - height += 1 - width += self.PAD_HORIZONTAL * 2 + height - return max(self.min_width, width), height - - def draw_path(self, context, x, y, width, height, radius): - inner_width = width - radius * 2 - context.move_to(x + radius, y) - context.rel_line_to(inner_width, 0) - context.arc(x + width - radius, y+radius, radius, -math.pi/2, - math.pi/2) - context.rel_line_to(-inner_width, 0) - context.arc(x + radius, y+radius, radius, math.pi/2, -math.pi/2) - - def draw_button(self, context, x, y, width, height, radius): - context.context.save() - self.draw_path(context, x, y, width, height, radius) - if self.disabled: - end_color = self.DISABLED_COLOR - start_color = self.DISABLED_COLOR - elif self.pressed: - end_color = self.TOP_COLOR - start_color = self.BOTTOM_COLOR - else: - context.set_line_width(1) - start_color = self.TOP_COLOR - end_color = self.BOTTOM_COLOR - gradient = cairo.LinearGradient(x, y, x, y + height) - gradient.add_color_stop_rgb(0, *start_color) - gradient.add_color_stop_rgb(1, *end_color) - context.context.set_source(gradient) - context.fill() - context.set_line_width(1) - self.draw_path(context, x+0.5, y+0.5, width, height, radius) - gradient = cairo.LinearGradient(x, y, x, y + height) - gradient.add_color_stop_rgb(0, *self.LINE_COLOR_TOP) - gradient.add_color_stop_rgb(1, *self.LINE_COLOR_BOTTOM) - context.context.set_source(gradient) - context.stroke() - context.context.restore() - - def draw(self, context, x, y, width, height): - radius = height / 2 - self.draw_button(context, x, y, width, height, radius) - - text_width, text_height = self.layout.get_pixel_size() - # draw the text in the center of the button - text_x = x + (width - text_width) / 2 - text_y = y + (height - text_height) / 2 - if self.icon: - icon_x = text_x - (self.icon.width + self.ICON_PAD) / 2 - text_x += (self.icon.width + self.ICON_PAD) / 2 - icon_y = y + (height - self.icon.height) / 2 - self.icon.draw(context, icon_x, icon_y, self.icon.width, - self.icon.height) - self.draw_text(context, text_x, text_y, width, height, radius) - - def draw_text(self, context, x, y, width, height, radius): - if self.disabled: - context.set_color(self.DISABLED_TEXT_COLOR) - else: - context.set_color(self.TEXT_COLOR) - context.move_to(x, y) - context.context.show_layout(self.layout) diff --git a/mvc/widgets/gtk/simple.py b/mvc/widgets/gtk/simple.py deleted file mode 100644 index f0921e0..0000000 --- a/mvc/widgets/gtk/simple.py +++ /dev/null @@ -1,313 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""simple.py -- Collection of simple widgets.""" - -import gtk -import gobject -import pango - -from mvc.widgets import widgetconst -from .base import Widget, Bin - -class Image(object): - def __init__(self, path): - try: - self._set_pixbuf(gtk.gdk.pixbuf_new_from_file(path)) - except gobject.GError, ge: - raise ValueError("%s" % ge) - self.width = self.pixbuf.get_width() - self.height = self.pixbuf.get_height() - - def _set_pixbuf(self, pixbuf): - self.pixbuf = pixbuf - self.width = self.pixbuf.get_width() - self.height = self.pixbuf.get_height() - - def resize(self, width, height): - width = int(round(width)) - height = int(round(height)) - resized_pixbuf = self.pixbuf.scale_simple(width, height, - gtk.gdk.INTERP_BILINEAR) - return TransformedImage(resized_pixbuf) - - def resize_for_space(self, width, height): - """Returns an image scaled to fit into the specified space at the - correct height/width ratio. - """ - ratio = min(1.0 * width / self.width, 1.0 * height / self.height) - return self.resize(ratio * self.width, ratio * self.height) - - def crop_and_scale(self, src_x, src_y, src_width, src_height, dest_width, - dest_height): - """Crop an image then scale it. - - The image will be cropped to the rectangle (src_x, src_y, src_width, - src_height), that rectangle will be scaled to a new Image with tisez - (dest_width, dest_height) - """ - dest = gtk.gdk.Pixbuf(self.pixbuf.get_colorspace(), - self.pixbuf.get_has_alpha(), - self.pixbuf.get_bits_per_sample(), dest_width, dest_height) - - scale_x = dest_width / float(src_width) - scale_y = dest_height / float(src_height) - - self.pixbuf.scale(dest, 0, 0, dest_width, dest_height, - -src_x * scale_x, -src_y * scale_y, scale_x, scale_y, - gtk.gdk.INTERP_BILINEAR) - return TransformedImage(dest) - -class TransformedImage(Image): - def __init__(self, pixbuf): - # XXX intentionally not calling direct super's __init__; we should do - # this differently - self._set_pixbuf(pixbuf) - -class ImageDisplay(Widget): - def __init__(self, image=None): - Widget.__init__(self) - self.set_widget(gtk.Image()) - self.set_image(image) - - def set_image(self, image): - self.image = image - if image is not None: - self._widget.set_from_pixbuf(image.pixbuf) - else: - self._widget.clear() - -class AnimatedImageDisplay(Widget): - def __init__(self, path): - Widget.__init__(self) - self.set_widget(gtk.Image()) - self._animation = gtk.gdk.PixbufAnimation(path) - # Set to animate before we are shown and stop animating after - # we disappear. - self._widget.connect('map', lambda w: self._set_animate(True)) - self._widget.connect('unmap-event', - lambda w, a: self._set_animate(False)) - - def _set_animate(self, enabled): - if enabled: - self._widget.set_from_animation(self._animation) - else: - self._widget.clear() - -class Label(Widget): - """Widget that displays simple text.""" - def __init__(self, text="", color=None): - Widget.__init__(self) - self.set_widget(gtk.Label()) - if text: - self.set_text(text) - self.attr_list = pango.AttrList() - self.font_description = self._widget.style.font_desc.copy() - self.scale_factor = 1.0 - if color is not None: - self.set_color(color) - self.wrapped_widget_connect('style-set', self.on_style_set) - - def set_bold(self, bold): - if bold: - weight = pango.WEIGHT_BOLD - else: - weight = pango.WEIGHT_NORMAL - self.font_description.set_weight(weight) - self.set_attr(pango.AttrFontDesc(self.font_description)) - - def set_size(self, size): - if size == widgetconst.SIZE_NORMAL: - self.scale_factor = 1 - elif size == widgetconst.SIZE_SMALL: - self.scale_factor = 0.75 - else: - self.scale_factor = size - baseline = self._widget.style.font_desc.get_size() - self.font_description.set_size(int(baseline * self.scale_factor)) - self.set_attr(pango.AttrFontDesc(self.font_description)) - - def get_preferred_width(self): - return self._widget.size_request()[0] - - def on_style_set(self, widget, old_style): - self.set_size(self.scale_factor) - - def set_wrap(self, wrap): - self._widget.set_line_wrap(wrap) - - def set_alignment(self, alignment): - # default to left. - gtkalignment = gtk.JUSTIFY_LEFT - if alignment == widgetconst.TEXT_JUSTIFY_LEFT: - gtkalignment = gtk.JUSTIFY_LEFT - elif alignment == widgetconst.TEXT_JUSTIFY_RIGHT: - gtkalignment = gtk.JUSTIFY_RIGHT - elif alignment == widgetconst.TEXT_JUSTIFY_CENTER: - gtkalignment = gtk.JUSTIFY_CENTER - self._widget.set_justify(gtkalignment) - - def get_alignment(self): - return self._widget.get_justify() - - def get_width(self): - return self._widget.get_layout().get_pixel_size()[0] - - def set_text(self, text): - self._widget.set_text(text) - - def get_text(self): - return self._widget.get_text().decode('utf-8') - - def set_selectable(self, val): - self._widget.set_selectable(val) - - def set_attr(self, attr): - attr.end_index = 65535 - self.attr_list.change(attr) - self._widget.set_attributes(self.attr_list) - - def set_color(self, color): - color_as_int = (int(65535 * c) for c in color) - self.set_attr(pango.AttrForeground(*color_as_int)) - - def baseline(self): - pango_context = self._widget.get_pango_context() - metrics = pango_context.get_metrics(self.font_description) - return pango.PIXELS(metrics.get_descent()) - - def hide(self): - self._widget.hide() - - def show(self): - self._widget.show() - -class Scroller(Bin): - def __init__(self, horizontal, vertical): - Bin.__init__(self) - self.set_widget(gtk.ScrolledWindow()) - if horizontal: - h_policy = gtk.POLICY_AUTOMATIC - else: - h_policy = gtk.POLICY_NEVER - if vertical: - v_policy = gtk.POLICY_AUTOMATIC - else: - v_policy = gtk.POLICY_NEVER - self._widget.set_policy(h_policy, v_policy) - - def set_has_borders(self, has_border): - pass - - def set_background_color(self, color): - pass - - def add_child_to_widget(self): - if (isinstance(self.child._widget, gtk.TreeView) or - isinstance(self.child._widget, gtk.TextView)): - # child has native scroller - self._widget.add(self.child._widget) - else: - self._widget.add_with_viewport(self.child._widget) - self._widget.get_child().set_shadow_type(gtk.SHADOW_NONE) - if isinstance(self.child._widget, gtk.TextView): - self._widget.set_shadow_type(gtk.SHADOW_IN) - else: - self._widget.set_shadow_type(gtk.SHADOW_NONE) - - def prepare_for_dark_content(self): - # this is just a hack for cocoa - pass - - -class SolidBackground(Bin): - def __init__(self, color=None): - Bin.__init__(self) - self.set_widget(gtk.EventBox()) - if color is not None: - self.set_background_color(color) - - def set_background_color(self, color): - self.modify_style('base', gtk.STATE_NORMAL, self.make_color(color)) - self.modify_style('bg', gtk.STATE_NORMAL, self.make_color(color)) - -class Expander(Bin): - def __init__(self, child=None): - Bin.__init__(self) - self.set_widget(gtk.Expander()) - if child is not None: - self.add(child) - self.label = None - # This is a complete hack. GTK expanders have a transparent - # background most of the time, except when they are prelighted. So we - # just set the background to white there because that's what should - # happen in the item list. - self.modify_style('bg', gtk.STATE_PRELIGHT, - gtk.gdk.color_parse('white')) - - def set_spacing(self, spacing): - self._widget.set_spacing(spacing) - - def set_label(self, widget): - self.label = widget - self._widget.set_label_widget(widget._widget) - widget._widget.show() - - def set_expanded(self, expanded): - self._widget.set_expanded(expanded) - -class ProgressBar(Widget): - def __init__(self): - Widget.__init__(self) - self.set_widget(gtk.ProgressBar()) - self._timer = None - - def set_progress(self, fraction): - self._widget.set_fraction(fraction) - - def start_pulsing(self): - if self._timer is None: - self._timer = gobject.timeout_add(100, self._do_pulse) - - def stop_pulsing(self): - if self._timer: - gobject.source_remove(self._timer) - self._timer = None - - def _do_pulse(self): - self._widget.pulse() - return True - -class HLine(Widget): - """A horizontal separator. Not to be confused with HSeparator, which is is - a DrawingArea, not a Widget. - """ - def __init__(self): - Widget.__init__(self) - self.set_widget(gtk.HSeparator()) diff --git a/mvc/widgets/gtk/tableview.py b/mvc/widgets/gtk/tableview.py deleted file mode 100644 index 930270c..0000000 --- a/mvc/widgets/gtk/tableview.py +++ /dev/null @@ -1,1557 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""tableview.py -- Wrapper for the GTKTreeView widget. It's used for the tab -list and the item list (AKA almost all of the miro). -""" - -import logging - -import itertools -import gobject -import gtk -from collections import namedtuple - -# These are probably wrong, and are placeholders for now, until custom headers -# are also implemented for GTK. -CUSTOM_HEADER_HEIGHT = 25 -HEADER_HEIGHT = 25 - -from mvc import signals -from mvc.errors import (WidgetActionError, WidgetDomainError, - WidgetRangeError, WidgetNotReadyError) -from mvc.widgets.tableselection import SelectionOwnerMixin -from mvc.widgets.tablescroll import ScrollbarOwnerMixin -import drawing -import wrappermap -from .base import Widget -from .simple import Image -from .layoutmanager import LayoutManager -from .weakconnect import weak_connect -from .tableviewcells import GTKCustomCellRenderer - - -PathInfo = namedtuple('PathInfo', 'path column x y') -Rect = namedtuple('Rect', 'x y width height') -_album_view_gtkrc_installed = False - -def _install_album_view_gtkrc(): - """Hack for styling GTKTreeView for the album view widget. - - We do a couple things: - - Remove the focus ring - - Remove any separator space. - - We do this so that we don't draw a box through the album view column for - selected rows. - """ - global _album_view_gtkrc_installed - if _album_view_gtkrc_installed: - return - rc_string = ('style "album-view-style"\n' - '{ \n' - ' GtkTreeView::vertical-separator = 0\n' - ' GtkTreeView::horizontal-separator = 0\n' - ' GtkWidget::focus-line-width = 0 \n' - '}\n' - 'widget "*.miro-album-view" style "album-view-style"\n') - gtk.rc_parse_string(rc_string) - _album_view_gtkrc_installed = True - -def rect_contains_point(rect, x, y): - return ((rect.x <= x < rect.x + rect.width) and - (rect.y <= y < rect.y + rect.height)) - -class TreeViewScrolling(object): - def __init__(self): - self.scrollbars = [] - self.scroll_positions = None, None - self.restoring_scroll = None - self.connect('parent-set', self.on_parent_set) - self.scroller = None - # hack necessary because of our weird widget hierarchy (GTK doesn't deal - # well with the Scroller's widget not being the direct parent of the - # TableView's widget.) - self._coords_working = False - - def scroll_range_changed(self): - """Faux-signal; this should all be integrated into - GTKScrollbarOwnerMixin, making this unnecessary. - """ - - @property - def manually_scrolled(self): - """Return whether the view has been scrolled explicitly by the user - since the last time it was set automatically. - """ - auto_pos = self.scroll_positions[1] - if auto_pos is None: - # if we don't have any position yet, user can't have manually - # scrolled - return False - real_pos = self.scrollbars[1].get_value() - return abs(auto_pos - real_pos) > 5 # allowing some fuzziness - - @property - def position_set(self): - """Return whether the scroll position has been set in any way.""" - return any(x is not None for x in self.scroll_positions) - - def on_parent_set(self, widget, old_parent): - """We have parent window now; we need to control its scrollbars.""" - self.set_scroller(widget.get_parent()) - - def set_scroller(self, window): - """Take control of the scrollbars of window.""" - if not isinstance(window, gtk.ScrolledWindow): - return - self.scroller = window - scrollbars = tuple(bar.get_adjustment() - for bar in (window.get_hscrollbar(), window.get_vscrollbar())) - self.scrollbars = scrollbars - for i, bar in enumerate(scrollbars): - weak_connect(bar, 'changed', self.on_scroll_range_changed, i) - if self.restoring_scroll: - self.set_scroll_position(self.restoring_scroll) - - def on_scroll_range_changed(self, adjustment, bar): - """The scrollbar might have a range now. Set its initial position if - we haven't already. - """ - self._coords_working = True - if self.restoring_scroll: - self.set_scroll_position(self.restoring_scroll) - # our wrapper handles the same thing for iters - self.scroll_range_changed() - - def set_scroll_position(self, scroll_position): - """Restore the scrollbars to a remembered state.""" - try: - self.scroll_positions = tuple(self._clip_pos(adj, x) - for adj, x in zip(self.scrollbars, scroll_position)) - except WidgetActionError, error: - logging.debug("can't scroll yet: %s", error.reason) - # try again later - self.restoring_scroll = scroll_position - else: - for adj, pos in zip(self.scrollbars, self.scroll_positions): - adj.set_value(pos) - self.restoring_scroll = None - - def _clip_pos(self, adj, pos): - lower = adj.get_lower() - upper = adj.get_upper() - adj.get_page_size() - # currently, StandardView gets an upper of 2.0 when it's not ready - # FIXME: don't count on that - if pos > upper and upper < 5: - raise WidgetRangeError("scrollable area", pos, lower, upper) - return min(max(pos, lower), upper) - - def get_path_rect(self, path): - """Return the Rect for the given item, in tree coords.""" - if not self._coords_working: - # part of solution to #17405; widget_to_tree_coords tends to return - # y=8 before the first scroll-range-changed signal. ugh. - raise WidgetNotReadyError('_coords_working') - rect = self.get_background_area(path, self.get_columns()[0]) - x, y = self.widget_to_tree_coords(rect.x, rect.y) - return Rect(x, y, rect.width, rect.height) - - @property - def _scrollbars(self): - if not self.scrollbars: - raise WidgetNotReadyError - return self.scrollbars - - def scroll_ancestor(self, newly_selected, down): - # Try to figure out what just became selected. If multiple things - # somehow became selected, select the outermost one - if len(newly_selected) == 0: - raise WidgetActionError("need at an item to scroll to") - if down: - path_to_show = max(newly_selected) - else: - path_to_show = min(newly_selected) - - if not self.scrollbars: - return - vadjustment = self.scrollbars[1] - - rect = self.get_background_area(path_to_show, self.get_columns()[0]) - _, top = self.translate_coordinates(self.scroller, 0, rect.y) - top += vadjustment.value - bottom = top + rect.height - if down: - if bottom > vadjustment.value + vadjustment.page_size: - bottom_value = min(bottom, vadjustment.upper) - vadjustment.set_value(bottom_value - vadjustment.page_size) - else: - if top < vadjustment.value: - vadjustment.set_value(max(vadjustment.lower, top)) - -class MiroTreeView(gtk.TreeView, TreeViewScrolling): - """Extends the GTK TreeView widget to help implement TableView - https://develop.participatoryculture.org/index.php/WidgetAPITableView""" - # Add a tiny bit of padding so that the user can drag feeds below - # the table, i.e. to the bottom row, as a top-level - PAD_BOTTOM = 3 - def __init__(self): - gtk.TreeView.__init__(self) - TreeViewScrolling.__init__(self) - self.height_without_pad_bottom = -1 - self.set_enable_search(False) - self.horizontal_separator = self.style_get_property("horizontal-separator") - self.expander_size = self.style_get_property("expander-size") - self.group_lines_enabled = False - self.group_line_color = (0, 0, 0) - self.group_line_width = 1 - - def do_size_request(self, req): - gtk.TreeView.do_size_request(self, req) - self.height_without_pad_bottom = req.height - req.height += self.PAD_BOTTOM - - def do_move_cursor(self, step, count): - if step == gtk.MOVEMENT_VISUAL_POSITIONS: - # GTK is asking us to move left/right. Since our TableViews don't - # support this, return False to let the key press propagate. See - # #15646 for more info. - return False - if isinstance(self.get_parent(), gtk.ScrolledWindow): - # If our parent is a ScrolledWindow, let GTK take care of this - handled = gtk.TreeView.do_move_cursor(self, step, count) - return handled - else: - # Otherwise, we have to search up the widget tree for a - # ScrolledWindow to take care of it - selection = self.get_selection() - model, start_selection = selection.get_selected_rows() - gtk.TreeView.do_move_cursor(self, step, count) - - model, end_selection = selection.get_selected_rows() - newly_selected = set(end_selection) - set(start_selection) - down = (count > 0) - - try: - self.scroll_ancestor(newly_selected, down) - except WidgetActionError: - # not possible - return False - return True - - def get_position_info(self, x, y): - """Wrapper for get_path_at_pos that converts the path_info to a named - tuple and handles rounding the coordinates. - """ - path_info = self.get_path_at_pos(int(round(x)), int(round(y))) - if path_info: - return PathInfo(*path_info) - -gobject.type_register(MiroTreeView) - -class HotspotTracker(object): - """Handles tracking hotspots. - https://develop.participatoryculture.org/index.php/WidgetAPITableView""" - - def __init__(self, treeview, event): - self.treeview = treeview - self.treeview_wrapper = wrappermap.wrapper(treeview) - self.hit = False - self.button = event.button - path_info = treeview.get_position_info(event.x, event.y) - if path_info is None: - return - self.path, self.column, background_x, background_y = path_info - # We always pack 1 renderer for each column - gtk_renderer = self.column.get_cell_renderers()[0] - if not isinstance(gtk_renderer, GTKCustomCellRenderer): - return - self.renderer = wrappermap.wrapper(gtk_renderer) - self.attr_map = self.treeview_wrapper.attr_map_for_column[self.column] - if not rect_contains_point(self.calc_cell_area(), event.x, event.y): - # Mouse is in the padding around the actual cell area - return - self.update_position(event) - self.iter = treeview.get_model().get_iter(self.path) - self.name = self.calc_hotspot() - if self.name is not None: - self.hit = True - - def is_for_context_menu(self): - return self.name == "#show-context-menu" - - def calc_cell_area(self): - cell_area = self.treeview.get_cell_area(self.path, self.column) - xpad = self.renderer._renderer.props.xpad - ypad = self.renderer._renderer.props.ypad - cell_area.x += xpad - cell_area.y += ypad - cell_area.width -= xpad * 2 - cell_area.height -= ypad * 2 - return cell_area - - def update_position(self, event): - self.x, self.y = int(event.x), int(event.y) - - def calc_cell_state(self): - if self.treeview.get_selection().path_is_selected(self.path): - if self.treeview.flags() & gtk.HAS_FOCUS: - return gtk.STATE_SELECTED - else: - return gtk.STATE_ACTIVE - else: - return gtk.STATE_NORMAL - - def calc_hotspot(self): - cell_area = self.calc_cell_area() - if rect_contains_point(cell_area, self.x, self.y): - model = self.treeview.get_model() - self.renderer.cell_data_func(self.column, self.renderer._renderer, - model, self.iter, self.attr_map) - style = drawing.DrawingStyle(self.treeview_wrapper, - use_base_color=True, state=self.calc_cell_state()) - x = self.x - cell_area.x - y = self.y - cell_area.y - return self.renderer.hotspot_test(style, - self.treeview_wrapper.layout_manager, - x, y, cell_area.width, cell_area.height) - else: - return None - - def update_hit(self): - if self.is_for_context_menu(): - return # we always keep hit = True for this one - old_hit = self.hit - self.hit = (self.calc_hotspot() == self.name) - if self.hit != old_hit: - self.redraw_cell() - - def redraw_cell(self): - # Check that the treeview is still around. We might have switched - # views in response to a hotspot being clicked. - if self.treeview.flags() & gtk.REALIZED: - cell_area = self.treeview.get_cell_area(self.path, self.column) - x, y = self.treeview.tree_to_widget_coords(cell_area.x, - cell_area.y) - self.treeview.queue_draw_area(x, y, - cell_area.width, cell_area.height) - -class TableColumn(signals.SignalEmitter): - """A single column of a TableView. - - Signals: - - clicked (table_column) -- The header for this column was clicked. - """ - # GTK hard-codes 4px of padding for each column - FIXED_PADDING = 4 - def __init__(self, title, renderer, header=None, **attrs): - # header widget not used yet in GTK (#15800) - signals.SignalEmitter.__init__(self) - self.create_signal('clicked') - self._column = gtk.TreeViewColumn(title, renderer._renderer) - self._column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) - self._column.set_clickable(True) - self.attrs = attrs - renderer.setup_attributes(self._column, attrs) - self.renderer = renderer - weak_connect(self._column, 'clicked', self._header_clicked) - self.do_horizontal_padding = True - - def set_right_aligned(self, right_aligned): - """Horizontal alignment of the header label.""" - if right_aligned: - self._column.set_alignment(1.0) - else: - self._column.set_alignment(0.0) - - def set_min_width(self, width): - self._column.props.min_width = width + TableColumn.FIXED_PADDING - - def set_max_width(self, width): - self._column.props.max_width = width - - def set_width(self, width): - self._column.set_fixed_width(width + TableColumn.FIXED_PADDING) - - def get_width(self): - return self._column.get_width() - - def _header_clicked(self, tablecolumn): - self.emit('clicked') - - def set_resizable(self, resizable): - """Set if the user can resize the column.""" - self._column.set_resizable(resizable) - - def set_do_horizontal_padding(self, horizontal_padding): - self.do_horizontal_padding = False - - def set_sort_indicator_visible(self, visible): - """Show/Hide the sort indicator for this column.""" - self._column.set_sort_indicator(visible) - - def get_sort_indicator_visible(self): - return self._column.get_sort_indicator() - - def set_sort_order(self, ascending): - """Display a sort indicator on the column header. Ascending can be - either True or False which affects the direction of the indicator. - """ - if ascending: - self._column.set_sort_order(gtk.SORT_ASCENDING) - else: - self._column.set_sort_order(gtk.SORT_DESCENDING) - - def get_sort_order_ascending(self): - """Returns if the sort indicator is displaying that the sort is - ascending. - """ - return self._column.get_sort_order() == gtk.SORT_ASCENDING - -class GTKSelectionOwnerMixin(SelectionOwnerMixin): - """GTK-specific methods for selection management. - - This subclass should not define any behavior. Methods that cannot be - completed in this widget state should raise WidgetActionError. - """ - def __init__(self): - SelectionOwnerMixin.__init__(self) - self.selection = self._widget.get_selection() - weak_connect(self.selection, 'changed', self.on_selection_changed) - - def _set_allow_multiple_select(self, allow): - if allow: - mode = gtk.SELECTION_MULTIPLE - else: - mode = gtk.SELECTION_SINGLE - self.selection.set_mode(mode) - - def _get_allow_multiple_select(self): - return self.selection.get_mode() == gtk.SELECTION_MULTIPLE - - def _get_selected_iters(self): - iters = [] - def collect(treemodel, path, iter_): - iters.append(iter_) - self.selection.selected_foreach(collect) - return iters - - def _get_selected_iter(self): - model, iter_ = self.selection.get_selected() - return iter_ - - @property - def num_rows_selected(self): - return self.selection.count_selected_rows() - - def _is_selected(self, iter_): - return self.selection.iter_is_selected(iter_) - - def _select(self, iter_): - self.selection.select_iter(iter_) - - def _unselect(self, iter_): - self.selection.unselect_iter(iter_) - - def _unselect_all(self): - self.selection.unselect_all() - - def _iter_to_string(self, iter_): - return self._model.get_string_from_iter(iter_) - - def _iter_from_string(self, string): - try: - return self._model.get_iter_from_string(string) - except ValueError: - raise WidgetDomainError( - "model iters", string, "%s other iters" % len(self.model)) - - def select_path(self, path): - self.selection.select_path(path) - - def _validate_iter(self, iter_): - if self.get_path(iter_) is None: - raise WidgetDomainError( - "model iters", iter_, "%s other iters" % len(self.model)) - real_model = self._widget.get_model() - if not real_model: - raise WidgetActionError("no model") - elif real_model != self._model: - raise WidgetActionError("wrong model?") - - def get_cursor(self): - """Return the path of the 'focused' item.""" - path, column = self._widget.get_cursor() - return path - - def set_cursor(self, path): - """Set the path of the 'focused' item.""" - if path is None: - # XXX: is there a way to clear the cursor? - return - path_as_string = ':'.join(str(component) for component in path) - with self.preserving_selection(): # set_cursor() messes up the selection - self._widget.set_cursor(path_as_string) - -class DNDHandlerMixin(object): - """TableView row DnD. - - Depends on arbitrary TableView methods; otherwise self-contained except: - on_button_press: may call start_drag - on_button_release: may unset drag_button_down - on_motion_notify: may call potential_drag_motion - """ - def __init__(self): - self.drag_button_down = False - self.drag_data = {} - self.drag_source = self.drag_dest = None - self.drag_start_x, self.drag_start_y = None, None - self.wrapped_widget_connect('drag-data-get', self.on_drag_data_get) - self.wrapped_widget_connect('drag-end', self.on_drag_end) - self.wrapped_widget_connect('drag-motion', self.on_drag_motion) - self.wrapped_widget_connect('drag-leave', self.on_drag_leave) - self.wrapped_widget_connect('drag-drop', self.on_drag_drop) - self.wrapped_widget_connect('drag-data-received', - self.on_drag_data_received) - self.wrapped_widget_connect('unrealize', self.on_drag_unrealize) - - def set_drag_source(self, drag_source): - self.drag_source = drag_source - # XXX: the following note no longer seems accurate: - # No need to call enable_model_drag_source() here, we handle it - # ourselves in on_motion_notify() - - def set_drag_dest(self, drag_dest): - """Set the drop handler.""" - self.drag_dest = drag_dest - if drag_dest is not None: - targets = self._gtk_target_list(drag_dest.allowed_types()) - self._widget.enable_model_drag_dest(targets, - drag_dest.allowed_actions()) - self._widget.drag_dest_set(0, targets, - drag_dest.allowed_actions()) - else: - self._widget.unset_rows_drag_dest() - self._widget.drag_dest_unset() - - def start_drag(self, treeview, event, path_info): - """Check whether the event is a drag event; return whether handled - here. - """ - if event.state & (gtk.gdk.CONTROL_MASK | gtk.gdk.SHIFT_MASK): - return False - model, row_paths = treeview.get_selection().get_selected_rows() - - if path_info.path not in row_paths: - # something outside the selection is being dragged. - # make it the new selection. - self.unselect_all(signal=False) - self.select_path(path_info.path) - row_paths = [path_info.path] - rows = self.model.get_rows(row_paths) - self.drag_data = rows and self.drag_source.begin_drag(self, rows) - self.drag_button_down = bool(self.drag_data) - if self.drag_button_down: - self.drag_start_x = int(event.x) - self.drag_start_y = int(event.y) - - if len(row_paths) > 1 and path_info.path in row_paths: - # handle multiple selection. If the current row is already - # selected, stop propagating the signal. We will only change - # the selection if the user doesn't start a DnD operation. - # This makes it more natural for the user to drag a block of - # selected items. - renderer = path_info.column.get_cell_renderers()[0] - if (not self._x_coord_in_expander(treeview, path_info) - and not isinstance(renderer, GTKCheckboxCellRenderer)): - self.delaying_press = True - # grab keyboard focus since we handled the event - self.focus() - return True - - def on_drag_data_get(self, treeview, context, selection, info, timestamp): - for typ, data in self.drag_data.items(): - selection.set(typ, 8, repr(data)) - - def on_drag_end(self, treeview, context): - self.drag_data = {} - - def find_type(self, drag_context): - return self._widget.drag_dest_find_target(drag_context, - self._widget.drag_dest_get_target_list()) - - def calc_positions(self, x, y): - """Given x and y coordinates, generate a list of drop positions to - try. The values are tuples in the form of (parent_path, position, - gtk_path, gtk_position), where parent_path and position is the - position to send to the Miro code, and gtk_path and gtk_position is an - equivalent position to send to the GTK code if the drag_dest validates - the drop. - """ - model = self._model - try: - gtk_path, gtk_position = self._widget.get_dest_row_at_pos(x, y) - except TypeError: - # Below the last row - yield (None, len(model), None, None) - return - - iter_ = model.get_iter(gtk_path) - if gtk_position in (gtk.TREE_VIEW_DROP_INTO_OR_BEFORE, - gtk.TREE_VIEW_DROP_INTO_OR_AFTER): - yield (iter_, -1, gtk_path, gtk_position) - - if hasattr(model, 'iter_is_valid'): - # tablist has this; item list does not - assert model.iter_is_valid(iter_) - parent_iter = model.iter_parent(iter_) - position = gtk_path[-1] - if gtk_position in (gtk.TREE_VIEW_DROP_BEFORE, - gtk.TREE_VIEW_DROP_INTO_OR_BEFORE): - # gtk gave us a "before" position, no need to change it - yield (parent_iter, position, gtk_path, gtk.TREE_VIEW_DROP_BEFORE) - else: - # gtk gave us an "after" position, translate that to before the - # next row for miro. - if (self._widget.row_expanded(gtk_path) and - model.iter_has_child(iter_)): - child_path = gtk_path + (0,) - yield (iter_, 0, child_path, gtk.TREE_VIEW_DROP_BEFORE) - else: - yield (parent_iter, position+1, gtk_path, - gtk.TREE_VIEW_DROP_AFTER) - - def on_drag_motion(self, treeview, drag_context, x, y, timestamp): - if not self.drag_dest: - return True - type = self.find_type(drag_context) - if type == "NONE": - drag_context.drag_status(0, timestamp) - return True - drop_action = 0 - for pos_info in self.calc_positions(x, y): - drop_action = self.drag_dest.validate_drop(self, self.model, type, - drag_context.actions, pos_info[0], pos_info[1]) - if isinstance(drop_action, (list, tuple)): - drop_action, iter = drop_action - path = self.model.get_path(iter) - pos = gtk.TREE_VIEW_DROP_INTO_OR_BEFORE - else: - path, pos = pos_info[2:4] - - if drop_action: - self.set_drag_dest_row(path, pos) - break - else: - self.unset_drag_dest_row() - drag_context.drag_status(drop_action, timestamp) - return True - - def set_drag_dest_row(self, path, position): - self._widget.set_drag_dest_row(path, position) - - def unset_drag_dest_row(self): - self._widget.unset_drag_dest_row() - - def on_drag_leave(self, treeview, drag_context, timestamp): - treeview.unset_drag_dest_row() - - def on_drag_drop(self, treeview, drag_context, x, y, timestamp): - # prevent the default handler - treeview.emit_stop_by_name('drag-drop') - target = self.find_type(drag_context) - if target == "NONE": - return False - treeview.drag_get_data(drag_context, target, timestamp) - treeview.unset_drag_dest_row() - - def on_drag_data_received(self, - treeview, drag_context, x, y, selection, info, timestamp): - # prevent the default handler - treeview.emit_stop_by_name('drag-data-received') - if not self.drag_dest: - return - type = self.find_type(drag_context) - if type == "NONE": - return - if selection.data is None: - return - drop_action = 0 - for pos_info in self.calc_positions(x, y): - drop_action = self.drag_dest.validate_drop(self, self.model, type, - drag_context.actions, pos_info[0], pos_info[1]) - if drop_action: - self.drag_dest.accept_drop(self, self.model, type, - drag_context.actions, pos_info[0], pos_info[1], - eval(selection.data)) - return True - return False - - def on_drag_unrealize(self, treeview): - self.drag_button_down = False - - def potential_drag_motion(self, treeview, event): - """A motion event has occurred and did not hit a hotspot; start a drag - if applicable. - """ - if (self.drag_data and self.drag_button_down and - treeview.drag_check_threshold(self.drag_start_x, - self.drag_start_y, int(event.x), int(event.y))): - self.delaying_press = False - treeview.drag_begin(self._gtk_target_list(self.drag_data.keys()), - self.drag_source.allowed_actions(), 1, event) - - @staticmethod - def _gtk_target_list(types): - count = itertools.count() - return [(type, gtk.TARGET_SAME_APP, count.next()) for type in types] - -class HotspotTrackingMixin(object): - def __init__(self): - self.hotspot_tracker = None - self.create_signal('hotspot-clicked') - self._hotspot_callback_handles = [] - self._connect_hotspot_signals() - self.wrapped_widget_connect('unrealize', self.on_hotspot_unrealize) - - def _connect_hotspot_signals(self): - SIGNALS = { - 'row-inserted': self.on_row_inserted, - 'row-deleted': self.on_row_deleted, - 'row-changed': self.on_row_changed, - } - self._hotspot_callback_handles.extend( - weak_connect(self._model, signal, handler) - for signal, handler in SIGNALS.iteritems()) - - def _disconnect_hotspot_signals(self): - for handle in self._hotspot_callback_handles: - self._model.disconnect(handle) - self._hotspot_callback_handles = [] - - def on_row_inserted(self, model, path, iter_): - if self.hotspot_tracker: - self.hotspot_tracker.redraw_cell() - self.hotspot_tracker = None - - def on_row_deleted(self, model, path): - if self.hotspot_tracker: - self.hotspot_tracker.redraw_cell() - self.hotspot_tracker = None - - def on_row_changed(self, model, path, iter_): - if self.hotspot_tracker: - self.hotspot_tracker.update_hit() - - def handle_hotspot_hit(self, treeview, event): - """Check whether the event is a hotspot event; return whether handled - here. - """ - if self.hotspot_tracker: - return - hotspot_tracker = HotspotTracker(treeview, event) - if hotspot_tracker.hit: - self.hotspot_tracker = hotspot_tracker - hotspot_tracker.redraw_cell() - if hotspot_tracker.is_for_context_menu(): - menu = self._popup_context_menu(self.hotspot_tracker.path, event) - if menu: - menu.connect('selection-done', - self._on_hotspot_context_menu_selection_done) - # grab keyboard focus since we handled the event - self.focus() - return True - - def _on_hotspot_context_menu_selection_done(self, menu): - # context menu is closed, we won't get the button-release-event in - # this case, but we can unset hotspot tracker here. - if self.hotspot_tracker: - self.hotspot_tracker.redraw_cell() - self.hotspot_tracker = None - - def on_hotspot_unrealize(self, treeview): - self.hotspot_tracker = None - - def release_on_hotspot(self, event): - """A button_release occurred; return whether it has been handled as a - hotspot hit. - """ - hotspot_tracker = self.hotspot_tracker - if hotspot_tracker and event.button == hotspot_tracker.button: - hotspot_tracker.update_position(event) - hotspot_tracker.update_hit() - if (hotspot_tracker.hit and - not hotspot_tracker.is_for_context_menu()): - self.emit('hotspot-clicked', hotspot_tracker.name, - hotspot_tracker.iter) - hotspot_tracker.redraw_cell() - self.hotspot_tracker = None - return True - - def hotspot_model_changed(self): - """A bulk change has ended; reconnect signals and update hotspots.""" - self._connect_hotspot_signals() - if self.hotspot_tracker: - self.hotspot_tracker.redraw_cell() - self.hotspot_tracker.update_hit() - -class ColumnOwnerMixin(object): - """Keeps track of the table's columns - including the list of columns, and - properties that we set for a table but need to apply to each column. - - This manages: - columns - attr_map_for_column - gtk_column_to_wrapper - for use throughout tableview. - """ - def __init__(self): - self._columns_draggable = False - self._renderer_xpad = self._renderer_ypad = 0 - self.columns = [] - self.attr_map_for_column = {} - self.gtk_column_to_wrapper = {} - self.create_signal('reallocate-columns') # not emitted on GTK - - def remove_column(self, index): - """Remove a column from the display and forget it from the column lists. - """ - column = self.columns.pop(index) - del self.attr_map_for_column[column._column] - del self.gtk_column_to_wrapper[column._column] - self._widget.remove_column(column._column) - - def get_columns(self): - """Returns the current columns, in order, by title.""" - # FIXME: this should probably return column objects, and really should - # not be keeping track of columns by title at all - titles = [column.get_title().decode('utf-8') - for column in self._widget.get_columns()] - return titles - - def add_column(self, column): - """Append a column to this table; setup all necessary mappings, and - setup the new column's properties to match the table's settings. - """ - self.model.check_new_column(column) - self._widget.append_column(column._column) - self.columns.append(column) - self.attr_map_for_column[column._column] = column.attrs - self.gtk_column_to_wrapper[column._column] = column - self.setup_new_column(column) - - def setup_new_column(self, column): - """Apply properties that we keep track of at the table level to a - newly-created column. - """ - if self.background_color: - column.renderer._renderer.set_property('cell-background-gdk', - self.background_color) - column._column.set_reorderable(self._columns_draggable) - if column.do_horizontal_padding: - column.renderer._renderer.set_property('xpad', self._renderer_xpad) - column.renderer._renderer.set_property('ypad', self._renderer_ypad) - - def set_column_spacing(self, space): - """Set the amount of space between columns.""" - self._renderer_xpad = space / 2 - for column in self.columns: - if column.do_horizontal_padding: - column.renderer._renderer.set_property('xpad', - self._renderer_xpad) - - def set_row_spacing(self, space): - """Set the amount of space between columns.""" - self._renderer_ypad = space / 2 - for column in self.columns: - column.renderer._renderer.set_property('ypad', self._renderer_ypad) - - def set_columns_draggable(self, setting): - """Set the draggability of existing and future columns.""" - self._columns_draggable = setting - for column in self.columns: - column._column.set_reorderable(setting) - - def set_column_background_color(self): - """Set the background color of existing columns to the table's - background_color. - """ - for column in self.columns: - column.renderer._renderer.set_property('cell-background-gdk', - self.background_color) - - def set_auto_resizes(self, setting): - # FIXME: to be implemented. - # At this point, GTK somehow does the right thing anyway in terms of - # auto-resizing. I'm not sure exactly what's happening, but I believe - # that if the column widths don't add up to the total width, - # gtk.TreeView allocates extra width for the last column. This works - # well enough for the tab list and item list, since there's only one - # column. - pass - -class HoverTrackingMixin(object): - """Handle mouse hover events - tooltips for some cells and hover events for - renderers which support them. - """ - def __init__(self): - self.hover_info = None - self.hover_pos = None - if hasattr(self, 'get_tooltip'): - # this should probably be something like self.set_tooltip_source - self._widget.set_property('has-tooltip', True) - self.wrapped_widget_connect('query-tooltip', self.on_tooltip) - self._last_tooltip_place = None - - def on_tooltip(self, treeview, x, y, keyboard_mode, tooltip): - # x, y are relative to the entire widget, but we want them to be - # relative to our bin window. The bin window doesn't include things - # like the column headers. - origin = treeview.window.get_origin() - bin_origin = treeview.get_bin_window().get_origin() - x += origin[0] - bin_origin[0] - y += origin[1] - bin_origin[1] - path_info = treeview.get_position_info(x, y) - if path_info is None: - self._last_tooltip_place = None - return False - if (self._last_tooltip_place is not None and - path_info[:2] != self._last_tooltip_place): - # the default GTK behavior is to keep the tooltip in the same - # position, but this is looks bad when we move to a different row. - # So return False once to stop this. - self._last_tooltip_place = None - return False - self._last_tooltip_place = path_info[:2] - iter_ = treeview.get_model().get_iter(path_info.path) - column = self.gtk_column_to_wrapper[path_info.column] - text = self.get_tooltip(iter_, column) - if text is None: - return False - pygtkhacks.set_tooltip_text(tooltip, text) - return True - - def _update_hover(self, treeview, event): - old_hover_info, old_hover_pos = self.hover_info, self.hover_pos - path_info = treeview.get_position_info(event.x, event.y) - if (path_info and - self.gtk_column_to_wrapper[path_info.column].renderer.want_hover): - self.hover_info = path_info.path, path_info.column - self.hover_pos = path_info.x, path_info.y - else: - self.hover_info = None - self.hover_pos = None - if (old_hover_info != self.hover_info or - old_hover_pos != self.hover_pos): - if (old_hover_info != self.hover_info and - old_hover_info is not None): - self._redraw_cell(treeview, *old_hover_info) - if self.hover_info is not None: - self._redraw_cell(treeview, *self.hover_info) - -class GTKScrollbarOwnerMixin(ScrollbarOwnerMixin): - # XXX this is half a wrapper for TreeViewScrolling. A lot of things will - # become much simpler when we integrate TVS into this - def __init__(self): - ScrollbarOwnerMixin.__init__(self) - # super uses this for postponed scroll_to_iter - # it's a faux-signal from our _widget; this hack is only necessary until - # we integrate TVS - self._widget.scroll_range_changed = (lambda *a: - self.emit('scroll-range-changed')) - - def set_scroller(self, scroller): - """Set the Scroller object for this widget, if its ScrolledWindow is - not a direct ancestor of the object. Standard View needs this. - """ - self._widget.set_scroller(scroller._widget) - - def _set_scroll_position(self, scroll_pos): - self._widget.set_scroll_position(scroll_pos) - - def _get_item_area(self, iter_): - return self._widget.get_path_rect(self.get_path(iter_)) - - @property - def _manually_scrolled(self): - return self._widget.manually_scrolled - - @property - def _position_set(self): - return self._widget.position_set - - def _get_visible_area(self): - """Return the Rect of the visible area, in tree coords. - - get_visible_rect gets this wrong for StandardView, always returning an - origin of (0, 0) - this is because our ScrolledWindow is not our direct - parent. - """ - bars = self._widget._scrollbars - x, y = (int(adj.get_value()) for adj in bars) - width, height = (int(adj.get_page_size()) for adj in bars) - if height == 0: - # this happens even after _widget._coords_working - raise WidgetNotReadyError('visible height') - return Rect(x, y, width, height) - - def _get_scroll_position(self): - """Get the current position of both scrollbars, to restore later.""" - try: - return tuple(int(bar.get_value()) for bar in self._widget._scrollbars) - except WidgetNotReadyError: - return None - -class TableView(Widget, GTKSelectionOwnerMixin, DNDHandlerMixin, - HotspotTrackingMixin, ColumnOwnerMixin, HoverTrackingMixin, - GTKScrollbarOwnerMixin): - """https://develop.participatoryculture.org/index.php/WidgetAPITableView""" - - draws_selection = True - - def __init__(self, model, custom_headers=False): - Widget.__init__(self) - self.set_widget(MiroTreeView()) - self.model = model - self.model.add_to_tableview(self._widget) - self._model = self._widget.get_model() - wrappermap.add(self._model, model) - self._setup_colors() - self.background_color = None - self.context_menu_callback = None - self.in_bulk_change = False - self.delaying_press = False - self._use_custom_headers = False - self.layout_manager = LayoutManager(self._widget) - self.height_changed = None # 17178 hack - self._connect_signals() - # setting up mixins after general TableView init - GTKSelectionOwnerMixin.__init__(self) - DNDHandlerMixin.__init__(self) - HotspotTrackingMixin.__init__(self) - ColumnOwnerMixin.__init__(self) - HoverTrackingMixin.__init__(self) - GTKScrollbarOwnerMixin.__init__(self) - if custom_headers: - self._enable_custom_headers() - - # FIXME: should implement set_model() and make None a special case. - def unset_model(self): - """Disconnect our model from this table view. - - This should be called when you want to destroy a TableView and - there's a new TableView sharing its model. - """ - self._widget.set_model(None) - self.model = None - - def _connect_signals(self): - self.create_signal('row-expanded') - self.create_signal('row-collapsed') - self.create_signal('row-clicked') - self.create_signal('row-activated') - self.wrapped_widget_connect('row-activated', self.on_row_activated) - self.wrapped_widget_connect('row-expanded', self.on_row_expanded) - self.wrapped_widget_connect('row-collapsed', self.on_row_collapsed) - self.wrapped_widget_connect('button-press-event', self.on_button_press) - self.wrapped_widget_connect('button-release-event', - self.on_button_release) - self.wrapped_widget_connect('motion-notify-event', - self.on_motion_notify) - - def set_gradient_highlight(self, gradient): - # This is just an OS X thing. - pass - - def set_background_color(self, color): - self.background_color = self.make_color(color) - self.modify_style('base', gtk.STATE_NORMAL, self.background_color) - if not self.draws_selection: - self.modify_style('base', gtk.STATE_SELECTED, - self.background_color) - self.modify_style('base', gtk.STATE_ACTIVE, self.background_color) - if self.use_custom_style: - self.set_column_background_color() - - def set_group_lines_enabled(self, enabled): - """Enable/Disable group lines. - - This only has an effect if our model is an InfoListModel and it has a - grouping set. - - If group lines are enabled, we will draw a line below the last item in - the group. Use set_group_line_style() to change the look of the line. - """ - self._widget.group_lines_enabled = enabled - self.queue_redraw() - - def set_group_line_style(self, color, width): - self._widget.group_line_color = color - self._widget.group_line_width = width - self.queue_redraw() - - def handle_custom_style_change(self): - if self.background_color is not None: - if self.use_custom_style: - self.set_column_background_color() - else: - for column in self.columns: - column.renderer._renderer.set_property( - 'cell-background-set', False) - - def set_alternate_row_backgrounds(self, setting): - self._widget.set_rules_hint(setting) - - def set_grid_lines(self, horizontal, vertical): - if horizontal and vertical: - setting = gtk.TREE_VIEW_GRID_LINES_BOTH - elif horizontal: - setting = gtk.TREE_VIEW_GRID_LINES_HORIZONTAL - elif vertical: - setting = gtk.TREE_VIEW_GRID_LINES_VERTICAL - else: - setting = gtk.TREE_VIEW_GRID_LINES_NONE - self._widget.set_grid_lines(setting) - - def width_for_columns(self, total_width): - """Given the width allocated for the TableView, return how much of that - is available to column contents. Note that this depends on the number of - columns. - """ - column_spacing = TableColumn.FIXED_PADDING * len(self.columns) - return total_width - column_spacing - - def enable_album_view_focus_hack(self): - _install_album_view_gtkrc() - self._widget.set_name("miro-album-view") - - def focus(self): - self._widget.grab_focus() - - def _enable_custom_headers(self): - # NB: this is currently not used because the GTK tableview does not - # support custom headers. - self._use_custom_headers = True - - def set_show_headers(self, show): - self._widget.set_headers_visible(show) - self._widget.set_headers_clickable(show) - - def _setup_colors(self): - style = self._widget.style - if not self.draws_selection: - # if we don't want to draw selection, make the selected/active - # colors the same as the normal ones - self.modify_style('base', gtk.STATE_SELECTED, - style.base[gtk.STATE_NORMAL]) - self.modify_style('base', gtk.STATE_ACTIVE, - style.base[gtk.STATE_NORMAL]) - - def set_search_column(self, model_index): - self._widget.set_search_column(model_index) - - def set_fixed_height(self, fixed_height): - self._widget.set_fixed_height_mode(fixed_height) - - def set_row_expanded(self, iter_, expanded): - """Expand or collapse the row specified by iter_. Succeeds or raises - WidgetActionError. Causes row-expanded or row-collapsed to be emitted - when successful. - """ - path = self.get_path(iter_) - if expanded: - self._widget.expand_row(path, False) - else: - self._widget.collapse_row(path) - if bool(self._widget.row_expanded(path)) != bool(expanded): - raise WidgetActionError("cannot expand the given item - it " - "probably has no children.") - - def is_row_expanded(self, iter_): - path = self.get_path(iter_) - return self._widget.row_expanded(path) - - def set_context_menu_callback(self, callback): - self.context_menu_callback = callback - - # GTK is really good and it is safe to operate on table even when - # cells may be constantly changing in flux. - def set_volatile(self, volatile): - return - - def on_row_expanded(self, _widget, iter_, path): - self.emit('row-expanded', iter_, path) - - def on_row_collapsed(self, _widget, iter_, path): - self.emit('row-collapsed', iter_, path) - - def on_button_press(self, treeview, event): - """Handle a mouse button press""" - if event.type == gtk.gdk._2BUTTON_PRESS: - # already handled as row-activated - return False - - path_info = treeview.get_position_info(event.x, event.y) - if not path_info: - # no item was clicked, so it's not going to be a hotspot, drag, or - # context menu - return False - if event.type == gtk.gdk.BUTTON_PRESS: - # single click; emit the event but keep on running so we can handle - # stuff like drag and drop. - if not self._x_coord_in_expander(treeview, path_info): - iter_ = treeview.get_model().get_iter(path_info.path) - self.emit('row-clicked', iter_) - - if (event.button == 1 and self.handle_hotspot_hit(treeview, event)): - return True - if event.window != treeview.get_bin_window(): - # click is outside the content area, don't try to handle this. - # In particular, our DnD code messes up resizing table columns. - return False - if (event.button == 1 and self.drag_source and - not self._x_coord_in_expander(treeview, path_info)): - return self.start_drag(treeview, event, path_info) - elif event.button == 3 and self.context_menu_callback: - self.show_context_menu(treeview, event, path_info) - return True - - # FALLTHROUGH - return False - - def show_context_menu(self, treeview, event, path_info): - """Pop up a context menu for the given click event (which is a - right-click on a row). - """ - # hack for album view - if (treeview.group_lines_enabled and - path_info.column == treeview.get_columns()[0]): - self._select_all_rows_in_group(treeview, path_info.path) - self._popup_context_menu(path_info.path, event) - # grab keyboard focus since we handled the event - self.focus() - - def _select_all_rows_in_group(self, treeview, path): - """Select all items in the group """ - - # FIXME: this is very tightly coupled with the portable code. - - infolist = self.model - gtk_model = treeview.get_model() - if (not isinstance(infolist, InfoListModel) or - infolist.get_grouping() is None): - return - it = gtk_model.get_iter(path) - info, attrs, group_info = infolist.row_for_iter(it) - start_row = path[0] - group_info[0] - total_rows = group_info[1] - - with self._ignoring_changes(): - self.unselect_all() - for row in xrange(start_row, start_row + total_rows): - self.select_path((row,)) - self.emit('selection-changed') - - def _popup_context_menu(self, path, event): - if not self.selection.path_is_selected(path): - self.unselect_all(signal=False) - self.select_path(path) - menu = self.make_context_menu() - if menu: - menu.popup(None, None, None, event.button, event.time) - return menu - else: - return None - - # XXX treeview.get_cell_area handles what we're trying to use this for - def _x_coord_in_expander(self, treeview, path_info): - """Calculate if an x coordinate is over the expander triangle - - :param treeview: Gtk.TreeView - :param path_info: PathInfo( - tree path for the cell, - Gtk.TreeColumn, - x coordinate relative to column's cell area, - y coordinate relative to column's cell area (ignored), - ) - """ - if path_info.column != treeview.get_expander_column(): - return False - model = treeview.get_model() - if not model.iter_has_child(model.get_iter(path_info.path)): - return False - # GTK allocateds an extra 4px to the right of the expanders. This - # seems to be hardcoded as EXPANDER_EXTRA_PADDING in the source code. - total_exander_size = treeview.expander_size + 4 - # include horizontal_separator - # XXX: should this value be included in total_exander_size ? - offset = treeview.horizontal_separator / 2 - # allocate space for expanders for parent nodes - expander_start = total_exander_size * (len(path_info.path) - 1) + offset - expander_end = expander_start + total_exander_size + offset - return expander_start <= path_info.x < expander_end - - def on_row_activated(self, treeview, path, view_column): - iter_ = treeview.get_model().get_iter(path) - self.emit('row-activated', iter_) - - def make_context_menu(self): - def gen_menu(menu_items): - menu = gtk.Menu() - for menu_item_info in menu_items: - if menu_item_info is None: - item = gtk.SeparatorMenuItem() - else: - label, callback = menu_item_info - - if isinstance(label, tuple) and len(label) == 2: - text_label, icon_path = label - pixbuf = gtk.gdk.pixbuf_new_from_file(icon_path) - image = gtk.Image() - image.set_from_pixbuf(pixbuf) - item = gtk.ImageMenuItem(text_label) - item.set_image(image) - else: - item = gtk.MenuItem(label) - - if callback is None: - item.set_sensitive(False) - elif isinstance(callback, list): - item.set_submenu(gen_menu(callback)) - else: - item.connect('activate', self.on_context_menu_activate, - callback) - menu.append(item) - item.show() - return menu - - items = self.context_menu_callback(self) - if items: - return gen_menu(items) - else: - return None - - def on_context_menu_activate(self, item, callback): - callback() - - def on_button_release(self, treeview, event): - if self.release_on_hotspot(event): - return True - if event.button == 1: - self.drag_button_down = False - - if self.delaying_press: - # if dragging did not happen, unselect other rows and - # select current row - path_info = treeview.get_position_info(event.x, event.y) - if path_info is not None: - self.unselect_all(signal=False) - self.select_path(path_info.path) - self.delaying_press = False - - def _redraw_cell(self, treeview, path, column): - cell_area = treeview.get_cell_area(path, column) - x, y = treeview.convert_bin_window_to_widget_coords(cell_area.x, - cell_area.y) - treeview.queue_draw_area(x, y, cell_area.width, cell_area.height) - - def on_motion_notify(self, treeview, event): - self._update_hover(treeview, event) - - if self.hotspot_tracker: - self.hotspot_tracker.update_position(event) - self.hotspot_tracker.update_hit() - return True - - self.potential_drag_motion(treeview, event) - return None # XXX: used to fall through; not sure what retval does here - - def start_bulk_change(self): - self._widget.freeze_child_notify() - self._widget.set_model(None) - self._disconnect_hotspot_signals() - self.in_bulk_change = True - - def model_changed(self): - if self.in_bulk_change: - self._widget.set_model(self._model) - self._widget.thaw_child_notify() - self.hotspot_model_changed() - self.in_bulk_change = False - - def get_path(self, iter_): - """Always use this rather than the model's get_path directly - - if the iter isn't valid, a GTK assertion causes us to exit - without warning; this wrapper changes that to a much more useful - AssertionError. Example related bug: #17362. - """ - assert self.model.iter_is_valid(iter_) - return self._model.get_path(iter_) - -class TableModel(object): - """https://develop.participatoryculture.org/index.php/WidgetAPITableView""" - MODEL_CLASS = gtk.ListStore - - def __init__(self, *column_types): - self._model = self.MODEL_CLASS(*self.map_types(column_types)) - self._column_types = column_types - if 'image' in self._column_types: - self.convert_row_for_gtk = self.convert_row_for_gtk_slow - self.convert_value_for_gtk = self.convert_value_for_gtk_slow - else: - self.convert_row_for_gtk = self.convert_row_for_gtk_fast - self.convert_value_for_gtk = self.convert_value_for_gtk_fast - - def add_to_tableview(self, widget): - widget.set_model(self._model) - - def map_types(self, miro_column_types): - type_map = { - 'boolean': bool, - 'numeric': float, - 'integer': int, - 'text': str, - 'image': gtk.gdk.Pixbuf, - 'datetime': object, - 'object': object, - } - try: - return [type_map[type] for type in miro_column_types] - except KeyError, e: - raise ValueError("Unknown column type: %s" % e[0]) - - # If we store image data, we need to do some work to convert row data to - # send to GTK - def convert_value_for_gtk_slow(self, column_value): - if isinstance(column_value, Image): - return column_value.pixbuf - else: - return column_value - - def convert_row_for_gtk_slow(self, column_values): - return tuple(self.convert_value_for_gtk(c) for c in column_values) - - def check_new_column(self, column): - for value in column.attrs.values(): - if not isinstance(value, int): - msg = "Attribute values must be integers, not %r" % value - raise TypeError(msg) - if value < 0 or value >= len(self._column_types): - raise ValueError("Attribute index out of range: %s" % value) - - # If we don't store image data, we can don't need to do any work to - # convert row data to gtk - def convert_value_for_gtk_fast(self, value): - return value - - def convert_row_for_gtk_fast(self, column_values): - return column_values - - def append(self, *column_values): - return self._model.append(self.convert_row_for_gtk(column_values)) - - def update_value(self, iter_, index, value): - assert self._model.iter_is_valid(iter_) - self._model.set(iter_, index, self.convert_value_for_gtk(value)) - - def update(self, iter_, *column_values): - self._model[iter_] = self.convert_value_for_gtk(column_values) - - def remove(self, iter_): - if self._model.remove(iter_): - return iter_ - else: - return None - - def insert_before(self, iter_, *column_values): - row = self.convert_row_for_gtk(column_values) - return self._model.insert_before(iter_, row) - - def first_iter(self): - return self._model.get_iter_first() - - def next_iter(self, iter_): - return self._model.iter_next(iter_) - - def nth_iter(self, index): - assert index >= 0 - return self._model.iter_nth_child(None, index) - - def __iter__(self): - return iter(self._model) - - def __len__(self): - return len(self._model) - - def __getitem__(self, iter_): - return self._model[iter_] - - def get_rows(self, row_paths): - return [self._model[path] for path in row_paths] - - def get_path(self, iter_): - return self._model.get_path(iter_) - - def iter_is_valid(self, iter_): - return self._model.iter_is_valid(iter_) - -class TreeTableModel(TableModel): - """https://develop.participatoryculture.org/index.php/WidgetAPITableView""" - MODEL_CLASS = gtk.TreeStore - - def append(self, *column_values): - return self._model.append(None, self.convert_row_for_gtk( - column_values)) - - def insert_before(self, iter_, *column_values): - parent = self.parent_iter(iter_) - row = self.convert_row_for_gtk(column_values) - return self._model.insert_before(parent, iter_, row) - - def append_child(self, iter_, *column_values): - return self._model.append(iter_, self.convert_row_for_gtk( - column_values)) - - def child_iter(self, iter_): - return self._model.iter_children(iter_) - - def nth_child_iter(self, iter_, index): - assert index >= 0 - return self._model.iter_nth_child(iter_, index) - - def has_child(self, iter_): - return self._model.iter_has_child(iter_) - - def children_count(self, iter_): - return self._model.iter_n_children(iter_) - - def parent_iter(self, iter_): - assert self._model.iter_is_valid(iter_) - return self._model.iter_parent(iter_) diff --git a/mvc/widgets/gtk/tableviewcells.py b/mvc/widgets/gtk/tableviewcells.py deleted file mode 100644 index 33ac6f8..0000000 --- a/mvc/widgets/gtk/tableviewcells.py +++ /dev/null @@ -1,249 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""tableviewcells.py - Cell renderers for TableView.""" - -import gobject -import gtk -import pango - -from mvc import signals -from mvc.widgets import widgetconst -import drawing -import wrappermap -from .base import make_gdk_color - -class CellRenderer(object): - """Simple Cell Renderer - https://develop.participatoryculture.org/index.php/WidgetAPITableView""" - def __init__(self): - self._renderer = gtk.CellRendererText() - self.want_hover = False - - def setup_attributes(self, column, attr_map): - column.add_attribute(self._renderer, 'text', attr_map['value']) - - def set_align(self, align): - if align == 'left': - self._renderer.props.xalign = 0.0 - elif align == 'center': - self._renderer.props.xalign = 0.5 - elif align == 'right': - self._renderer.props.xalign = 1.0 - else: - raise ValueError("unknown alignment: %s" % align) - - def set_color(self, color): - self._renderer.props.foreground_gdk = make_gdk_color(color) - - def set_bold(self, bold): - font_desc = self._renderer.props.font_desc - if bold: - font_desc.set_weight(pango.WEIGHT_BOLD) - else: - font_desc.set_weight(pango.WEIGHT_NORMAL) - self._renderer.props.font_desc = font_desc - - def set_text_size(self, size): - if size == widgetconst.SIZE_NORMAL: - self._renderer.props.scale = 1.0 - elif size == widgetconst.SIZE_SMALL: - # FIXME: on 3.5 we just ignored the call. Always setting scale to - # 1.0 basically replicates that behavior, but should we actually - # try to implement the semantics of SIZE_SMALL? - self._renderer.props.scale = 1.0 - else: - raise ValueError("unknown size: %s" % size) - - def set_font_scale(self, scale_factor): - self._renderer.props.scale = scale_factor - -class ImageCellRenderer(object): - """Cell Renderer for images - https://develop.participatoryculture.org/index.php/WidgetAPITableView""" - def __init__(self): - self._renderer = gtk.CellRendererPixbuf() - self.want_hover = False - - def setup_attributes(self, column, attr_map): - column.add_attribute(self._renderer, 'pixbuf', attr_map['image']) - -class GTKCheckboxCellRenderer(gtk.CellRendererToggle): - def do_activate(self, event, treeview, path, background_area, cell_area, - flags): - iter = treeview.get_model().get_iter(path) - self.set_active(not self.get_active()) - wrappermap.wrapper(self).emit('clicked', iter) - -gobject.type_register(GTKCheckboxCellRenderer) - -class CheckboxCellRenderer(signals.SignalEmitter): - """Cell Renderer for booleans - https://develop.participatoryculture.org/index.php/WidgetAPITableView""" - def __init__(self): - signals.SignalEmitter.__init__(self) - self.create_signal("clicked") - self._renderer = GTKCheckboxCellRenderer() - wrappermap.add(self._renderer, self) - self.want_hover = False - - def set_control_size(self, size): - pass - - def setup_attributes(self, column, attr_map): - column.add_attribute(self._renderer, 'active', attr_map['value']) - -class GTKCustomCellRenderer(gtk.GenericCellRenderer): - """Handles the GTK hide of CustomCellRenderer - https://develop.participatoryculture.org/index.php/WidgetAPITableView""" - - def on_get_size(self, widget, cell_area=None): - wrapper = wrappermap.wrapper(self) - widget_wrapper = wrappermap.wrapper(widget) - style = drawing.DrawingStyle(widget_wrapper, use_base_color=True) - # NOTE: CustomCellRenderer.cell_data_func() sets up its attributes - # from the model itself, so we don't have to worry about setting them - # here. - width, height = wrapper.get_size(style, widget_wrapper.layout_manager) - x_offset = self.props.xpad - y_offset = self.props.ypad - width += self.props.xpad * 2 - height += self.props.ypad * 2 - if cell_area: - x_offset += cell_area.x - y_offset += cell_area.x - extra_width = max(0, cell_area.width - width) - extra_height = max(0, cell_area.height - height) - x_offset += int(round(self.props.xalign * extra_width)) - y_offset += int(round(self.props.yalign * extra_height)) - return x_offset, y_offset, width, height - - def on_render(self, window, widget, background_area, cell_area, expose_area, - flags): - widget_wrapper = wrappermap.wrapper(widget) - cell_wrapper = wrappermap.wrapper(self) - - selected = (flags & gtk.CELL_RENDERER_SELECTED) - if selected: - if widget.flags() & gtk.HAS_FOCUS: - state = gtk.STATE_SELECTED - else: - state = gtk.STATE_ACTIVE - else: - state = gtk.STATE_NORMAL - if cell_wrapper.IGNORE_PADDING: - area = background_area - else: - xpad = self.props.xpad - ypad = self.props.ypad - area = gtk.gdk.Rectangle(cell_area.x + xpad, cell_area.y + ypad, - cell_area.width - xpad * 2, cell_area.height - ypad * 2) - context = drawing.DrawingContext(window, area, expose_area) - if (selected and not widget_wrapper.draws_selection and - widget_wrapper.use_custom_style): - # Draw the base color as our background. This erases the gradient - # that GTK draws for selected items. - window.draw_rectangle(widget.style.base_gc[state], True, - background_area.x, background_area.y, - background_area.width, background_area.height) - context.style = drawing.DrawingStyle(widget_wrapper, - use_base_color=True, state=state) - widget_wrapper.layout_manager.update_cairo_context(context.context) - hotspot_tracker = widget_wrapper.hotspot_tracker - if (hotspot_tracker and hotspot_tracker.hit and - hotspot_tracker.column == self.column and - hotspot_tracker.path == self.path): - hotspot = hotspot_tracker.name - else: - hotspot = None - if (self.path, self.column) == widget_wrapper.hover_info: - hover = widget_wrapper.hover_pos - hover = (hover[0] - xpad, hover[1] - ypad) - else: - hover = None - # NOTE: CustomCellRenderer.cell_data_func() sets up its attributes - # from the model itself, so we don't have to worry about setting them - # here. - widget_wrapper.layout_manager.reset() - cell_wrapper.render(context, widget_wrapper.layout_manager, selected, - hotspot, hover) - - def on_activate(self, event, widget, path, background_area, cell_area, - flags): - pass - - def on_start_editing(self, event, widget, path, background_area, - cell_area, flags): - pass -gobject.type_register(GTKCustomCellRenderer) - -class CustomCellRenderer(object): - """Customizable Cell Renderer - https://develop.participatoryculture.org/index.php/WidgetAPITableView""" - - IGNORE_PADDING = False - - def __init__(self): - self._renderer = GTKCustomCellRenderer() - self.want_hover = False - wrappermap.add(self._renderer, self) - - def setup_attributes(self, column, attr_map): - column.set_cell_data_func(self._renderer, self.cell_data_func, - attr_map) - - def cell_data_func(self, column, cell, model, iter, attr_map): - cell.column = column - cell.path = model.get_path(iter) - row = model[iter] - # Set attributes on self instead cell This works because cell is just - # going to turn around and call our methods to do the rendering. - for name, index in attr_map.items(): - setattr(self, name, row[index]) - - def hotspot_test(self, style, layout, x, y, width, height): - return None - -class InfoListRenderer(CustomCellRenderer): - """Custom Renderer for InfoListModels - https://develop.participatoryculture.org/index.php/WidgetAPITableView""" - - def cell_data_func(self, column, cell, model, iter, attr_map): - self.info, self.attrs, self.group_info = \ - wrappermap.wrapper(model).row_for_iter(iter) - cell.column = column - cell.path = model.get_path(iter) - -class InfoListRendererText(CellRenderer): - """Renderer for InfoListModels that only display text - https://develop.participatoryculture.org/index.php/WidgetAPITableView""" - - def setup_attributes(self, column, attr_map): - infolist.gtk.setup_text_cell_data_func(column, self._renderer, - self.get_value) diff --git a/mvc/widgets/gtk/weakconnect.py b/mvc/widgets/gtk/weakconnect.py deleted file mode 100644 index b8b9526..0000000 --- a/mvc/widgets/gtk/weakconnect.py +++ /dev/null @@ -1,56 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""weakconnect.py -- Connect to a signal of a GObject using a weak method -reference. This means that this connection will not keep the object alive. -This is a good thing because it prevents circular references between wrapper -widgets and the wrapped GTK widget. -""" - -from mvc import signals - -class WeakSignalHandler(object): - def __init__(self, method): - self.method = signals.WeakMethodReference(method) - - def connect(self, obj, signal, *user_args): - self.user_args = user_args - self.signal_handle = obj.connect(signal, self.handle_callback) - return self.signal_handle - - def handle_callback(self, obj, *args): - real_method = self.method() - if real_method is not None: - return real_method(obj, *(args + self.user_args)) - else: - obj.disconnect(self.signal_handle) - -def weak_connect(gobject, signal, method, *user_args): - handler = WeakSignalHandler(method) - return handler.connect(gobject, signal, *user_args) diff --git a/mvc/widgets/gtk/widgets.py b/mvc/widgets/gtk/widgets.py deleted file mode 100644 index 6c4280d..0000000 --- a/mvc/widgets/gtk/widgets.py +++ /dev/null @@ -1,47 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -""".widgets -- Contains portable implementations of -the GTK Widgets. These are shared between the windows port and the x11 port. -""" - -import gtk - -# Just use the GDK Rectangle class -class Rect(gtk.gdk.Rectangle): - @classmethod - def from_string(cls, rect_string): - x, y, width, height = [int(i) for i in rect_string.split(',')] - return Rect(x, y, width, height) - - def __str__(self): - return "%d,%d,%d,%d" % (self.x, self.y, self.width, self.height) - - def get_width(self): - return self.width diff --git a/mvc/widgets/gtk/widgetset.py b/mvc/widgets/gtk/widgetset.py deleted file mode 100644 index c63855c..0000000 --- a/mvc/widgets/gtk/widgetset.py +++ /dev/null @@ -1,63 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -from .base import Widget, Bin -from .const import * -from .controls import TextEntry, NumberEntry, \ - SecureTextEntry, MultilineTextEntry, Checkbox, RadioButton, \ - RadioButtonGroup, OptionMenu, Button -from .customcontrols import ( - CustomButton, DragableCustomButton, CustomSlider, - ClickableImageButton) -# VolumeSlider and VolumeMuter aren't defined if gtk.VolumeButton -# doesn't have get_popup. -try: - from .customcontrols import ( - VolumeSlider, VolumeMuter) -except ImportError: - pass -from .contextmenu import ContextMenu -from .drawing import ImageSurface, DrawingContext, \ - DrawingArea, Background, Gradient -from .layout import HBox, VBox, Alignment, \ - Splitter, Table, TabContainer, DetachedWindowHolder -from .window import Window, MainWindow, Dialog, \ - FileOpenDialog, FileSaveDialog, DirectorySelectDialog, AboutDialog, \ - AlertDialog, DialogWindow -from .tableview import (TableView, TableModel, - TableColumn, TreeTableModel, CUSTOM_HEADER_HEIGHT) -from .tableviewcells import (CellRenderer, - ImageCellRenderer, CheckboxCellRenderer, CustomCellRenderer, - InfoListRenderer, InfoListRendererText) -from .simple import (Image, ImageDisplay, - AnimatedImageDisplay, Label, Scroller, Expander, SolidBackground, - ProgressBar, HLine) -from .widgets import Rect -from .gtkmenus import (MenuItem, RadioMenuItem, CheckMenuItem, Separator, - Menu, MenuBar, MainWindowMenuBar) diff --git a/mvc/widgets/gtk/window.py b/mvc/widgets/gtk/window.py deleted file mode 100644 index 3859a1a..0000000 --- a/mvc/widgets/gtk/window.py +++ /dev/null @@ -1,708 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Jesus Eduardo (Heckyel) | 2017 -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -""".window -- GTK Window widget.""" - -import gobject -import gtk -import os - -from mvc import resources -from mvc import signals - -import keymap -import layout -import widgets -import wrappermap - -# keeps the objects alive until destroy() is called -alive_windows = set() -running_dialogs = set() - -class WrappedWindow(gtk.Window): - def do_map(self): - gtk.Window.do_map(self) - wrappermap.wrapper(self).emit('show') - - def do_unmap(self): - gtk.Window.do_unmap(self) - wrappermap.wrapper(self).emit('hide') - def do_focus_in_event(self, event): - gtk.Window.do_focus_in_event(self, event) - wrappermap.wrapper(self).emit('active-change') - def do_focus_out_event(self, event): - gtk.Window.do_focus_out_event(self, event) - wrappermap.wrapper(self).emit('active-change') - - def do_key_press_event(self, event): - if self.activate_key(event): # event activated a menu item - return - - if self.propagate_key_event(event): # event handled by widget - return - - ret = keymap.translate_gtk_event(event) - if ret is not None: - key, modifiers = ret - rv = wrappermap.wrapper(self).emit('key-press', key, modifiers) - if not rv: - gtk.Window.do_key_press_event(self, event) - - def _get_focused_wrapper(self): - """Get the wrapper of the widget with keyboard focus""" - focused = self.get_focus() - # some of our widgets created children for their use - # (GtkSearchTextEntry). If we don't find a wrapper for - # focused, try it's parents - while focused is not None: - try: - wrapper = wrappermap.wrapper(focused) - except KeyError: - focused = focused.get_parent() - else: - return wrapper - return None - - def change_focus_using_wrapper(self, direction): - my_wrapper = wrappermap.wrapper(self) - focused_wrapper = self._get_focused_wrapper() - if direction == gtk.DIR_TAB_FORWARD: - to_focus = my_wrapper.get_next_tab_focus(focused_wrapper, True) - elif direction == gtk.DIR_TAB_BACKWARD: - to_focus = my_wrapper.get_next_tab_focus(focused_wrapper, False) - else: - return False - if to_focus is not None: - to_focus.focus() - return True - return False - - def do_focus(self, direction): - if not self.change_focus_using_wrapper(direction): - gtk.Window.do_focus(self, direction) - -gobject.type_register(WrappedWindow) - -class WindowBase(signals.SignalEmitter): - def __init__(self): - signals.SignalEmitter.__init__(self) - self.create_signal('use-custom-style-changed') - self.create_signal('key-press') - self.create_signal('show') - self.create_signal('hide') - - def set_window(self, window): - self._window = window - window.connect('style-set', self.on_style_set) - wrappermap.add(window, self) - self.calc_use_custom_style() - - def on_style_set(self, widget, old_style): - old_use_custom_style = self.use_custom_style - self.calc_use_custom_style() - if old_use_custom_style != self.use_custom_style: - self.emit('use-custom-style-changed') - - def calc_use_custom_style(self): - if self._window is not None: - base = self._window.style.base[gtk.STATE_NORMAL] - # Decide if we should use a custom style. Right now the - # formula is the base color is a very light shade of - # gray/white (lighter than #f0f0f0). - self.use_custom_style = ((base.red == base.green == base.blue) and - base.red >= 61680) - - -class Window(WindowBase): - """The main Libre window. """ - - def __init__(self, title, rect=None): - """Create the Libre Main Window. Title is the name to give the - window, rect specifies the position it should have on screen. - """ - WindowBase.__init__(self) - self.set_window(self._make_gtk_window()) - self._window.set_title(title) - self.setup_icon() - if rect: - self._window.set_default_size(rect.width, rect.height) - self._window.set_default_size(rect.width, rect.height) - self._window.set_gravity(gtk.gdk.GRAVITY_CENTER) - self._window.move(rect.x, rect.y) - - self.create_signal('active-change') - self.create_signal('will-close') - self.create_signal('did-move') - self.create_signal('file-drag-motion') - self.create_signal('file-drag-received') - self.create_signal('file-drag-leave') - self.create_signal('on-shown') - self.drag_signals = [] - alive_windows.add(self) - - self._window.connect('delete-event', self.on_delete_window) - self._window.connect('map-event', lambda w, a: self.emit('on-shown')) - # XXX: Define MVCWindow/MiroWindow style not hard code this - self._window.set_resizable(False) - - def setup_icon(self): - icon_pixbuf = gtk.gdk.pixbuf_new_from_file( - resources.image_path("mvc-logo.png")) - self._window.set_icon(icon_pixbuf) - - - def accept_file_drag(self, val): - if not val: - self._window.drag_dest_set(0, [], 0) - for handle in self.drag_signals: - self.disconnect(handle) - self.drag_signals = [] - else: - self._window.drag_dest_set( - gtk.DEST_DEFAULT_MOTION | gtk.DEST_DEFAULT_DROP, - [('text/uri-list', 0, 0)], - gtk.gdk.ACTION_COPY) - for signal, callback in ( - ('drag-motion', self.on_drag_motion), - ('drag-data-received', self.on_drag_data_received), - ('drag-leave', self.on_drag_leave)): - self.drag_signals.append( - self._window.connect(signal, callback)) - - def on_drag_motion(self, widget, context, x, y, time): - self.emit('file-drag-motion') - - def on_drag_data_received(self, widget, context, x, y, selection_data, - info, time): - self.emit('file-drag-received', selection_data.get_uris()) - - def on_drag_leave(self, widget, context, time): - self.emit('file-drag-leave') - - def on_delete_window(self, widget, event): - # when the user clicks on the X in the corner of the window we - # want that to close the window, but also trigger our - # will-close signal and all that machinery unless the window - # is currently hidden--then we don't do anything. - if not self._window.window.is_visible(): - return - self.close() - return True - - def _make_gtk_window(self): - return WrappedWindow() - - def set_title(self, title): - self._window.set_title(title) - - def get_title(self): - self._window.get_title() - - def center(self): - self._window.set_position(gtk.WIN_POS_CENTER) - - def show(self): - if self not in alive_windows: - raise ValueError("Window destroyed") - self._window.show() - - def close(self): - if hasattr(self, "_closing"): - return - self._closing = True - # Keep a reference to the widget in case will-close signal handler - # calls destroy() - old_window = self._window - self.emit('will-close') - old_window.hide() - del self._closing - - def destroy(self): - self.close() - self._window = None - alive_windows.discard(self) - - def is_active(self): - return self._window.is_active() - - def is_visible(self): - return self._window.props.visible - - def get_next_tab_focus(self, current, is_forward): - return None - - def set_content_widget(self, widget): - """Set the widget that will be drawn in the content area for this - window. - - It will be allocated the entire area of the widget, except the - space needed for the titlebar, frame and other decorations. - When the window is resized, content should also be resized. - """ - self._add_content_widget(widget) - widget._widget.show() - self.content_widget = widget - - def _add_content_widget(self, widget): - self._window.add(widget._widget) - - def get_content_widget(self, widget): - """Get the current content widget.""" - return self.content_widget - - def get_frame(self): - pos = self._window.get_position() - size = self._window.get_size() - return widgets.Rect(pos[0], pos[1], size[0], size[1]) - - def set_frame(self, x=None, y=None, width=None, height=None): - if x is not None or y is not None: - pos = self._window.get_position() - x = x if x is not None else pos[0] - y = y if y is not None else pos[1] - self._window.move(x, y) - - if width is not None or height is not None: - size = self._window.get_size() - width = width if width is not None else size[0] - height = height if height is not None else size[1] - self._window.resize(width, height) - - def get_monitor_geometry(self): - """Returns a Rect of the geometry of the monitor that this - window is currently on. - - :returns: Rect - """ - gtkwindow = self._window - gdkwindow = gtkwindow.window - screen = gtkwindow.get_screen() - - monitor = screen.get_monitor_at_window(gdkwindow) - return screen.get_monitor_geometry(monitor) - - def check_position_and_fix(self): - """This pulls the geometry of the monitor of the screen this - window is on as well as the position of the window. - - It then makes sure that the position y is greater than the - monitor geometry y. This makes sure that the titlebar of - the window is showing. - """ - gtkwindow = self._window - gdkwindow = gtkwindow.window - monitor_geom = self.get_monitor_geometry() - - frame_extents = gdkwindow.get_frame_extents() - position = gtkwindow.get_position() - - # if the frame is not visible, then we move the window so that - # it is - if frame_extents.y < monitor_geom.y: - gtkwindow.move(position[0], - monitor_geom.y + (position[1] - frame_extents.y)) - - - -class DialogWindow(Window): - def __init__(self, title, rect=None): - Window.__init__(self, title, rect) - self._window.set_resizable(False) - -class MainWindow(Window): - def __init__(self, title, rect): - Window.__init__(self, title, rect) - self.vbox = gtk.VBox() - self._window.add(self.vbox) - self.vbox.show() - self._add_app_menubar() - self.create_signal('save-dimensions') - self.create_signal('save-maximized') - self._window.connect('key-release-event', self.on_key_release) - self._window.connect('window-state-event', self.on_window_state_event) - self._window.connect('configure-event', self.on_configure_event) - - def _make_gtk_window(self): - return WrappedWindow() - - def on_delete_window(self, widget, event): - return True - - def on_configure_event(self, widget, event): - (x, y) = self._window.get_position() - (width, height) = self._window.get_size() - self.emit('save-dimensions', x, y, width, height) - - def on_window_state_event(self, widget, event): - maximized = bool( - event.new_window_state & gtk.gdk.WINDOW_STATE_MAXIMIZED) - self.emit('save-maximized', maximized) - - def on_key_release(self, widget, event): - if app.playback_manager.is_playing: - if gtk.gdk.keyval_name(event.keyval) in ('Right', 'Left', - 'Up', 'Down'): - return True - - def _add_app_menubar(self): - self.menubar = app.widgetapp.menubar - self.vbox.pack_start(self.menubar._widget, expand=False) - self.connect_menu_keyboard_shortcuts() - - def _add_content_widget(self, widget): - self.vbox.pack_start(widget._widget, expand=True) - - -class DialogBase(WindowBase): - def set_transient_for(self, window): - self._window.set_transient_for(window._window) - - def run(self): - running_dialogs.add(self) - try: - return self._run() - finally: - running_dialogs.remove(self) - self._window = None - - def _run(self): - """Run the dialog. Must be implemented by subclasses.""" - raise NotImplementedError() - - def destroy(self): - if self._window is not None: - self._window.response(gtk.RESPONSE_NONE) - # don't set self._window to None yet. We will unset it when we - # return from the _run() method - -class Dialog(DialogBase): - def __init__(self, title, description=None): - """Create a dialog.""" - DialogBase.__init__(self) - self.create_signal('open') - self.create_signal('close') - self.set_window(gtk.Dialog(title)) - self._window.set_default_size(425, -1) - self.extra_widget = None - self.buttons_to_add = [] - wrappermap.add(self._window, self) - self.description = description - - def build_content(self): - packing_vbox = layout.VBox(spacing=20) - packing_vbox._widget.set_border_width(6) - if self.description is not None: - label = gtk.Label(self.description) - label.set_line_wrap(True) - label.set_size_request(390, -1) - label.set_selectable(True) - packing_vbox._widget.pack_start(label) - if self.extra_widget: - packing_vbox._widget.pack_start(self.extra_widget._widget) - return packing_vbox - - def add_button(self, text): - from mvc.widgets import dialogs - _stock = { - dialogs.BUTTON_OK.text: gtk.STOCK_OK, - dialogs.BUTTON_CANCEL.text: gtk.STOCK_CANCEL, - dialogs.BUTTON_YES.text: gtk.STOCK_YES, - dialogs.BUTTON_NO.text: gtk.STOCK_NO, - dialogs.BUTTON_QUIT.text: gtk.STOCK_QUIT, - dialogs.BUTTON_REMOVE.text: gtk.STOCK_REMOVE, - dialogs.BUTTON_DELETE.text: gtk.STOCK_DELETE, - } - if text in _stock: - # store both the text and the stock ID - text = _stock[text], text - self.buttons_to_add.append(text) - - def pack_buttons(self): - # There's a couple tricky things here: - # 1) We need to add them in the reversed order we got them, since GTK - # lays them out left-to-right - # - # 2) We can't use 0 as a response-id. GTK only reserves positive - # response_ids for the user. - response_id = len(self.buttons_to_add) - for text in reversed(self.buttons_to_add): - label = None - if isinstance(text, tuple): # stock ID, text - text, label = text - button = self._window.add_button(text, response_id) - if label is not None: - button.set_label(label) - response_id -= 1 - self.buttons_to_add = [] - self._window.set_default_response(1) - - def _run(self): - self.pack_buttons() - packing_vbox = self.build_content() - self._window.vbox.pack_start(packing_vbox._widget, True, True) - self._window.show_all() - response = self._window.run() - self._window.hide() - if response == gtk.RESPONSE_DELETE_EVENT: - return -1 - else: - return response - 1 # response IDs started at 1 - - def set_extra_widget(self, widget): - self.extra_widget = widget - - def get_extra_widget(self): - return self.extra_widget - -class FileDialogBase(DialogBase): - def _run(self): - ret = self._window.run() - self._window.hide() - if ret == gtk.RESPONSE_OK: - self._files = self._window.get_filenames() - return 0 - -class FileOpenDialog(FileDialogBase): - def __init__(self, title): - FileDialogBase.__init__(self) - self._files = None - fcd = gtk.FileChooserDialog(title, - action=gtk.FILE_CHOOSER_ACTION_OPEN, - buttons=(gtk.STOCK_CANCEL, - gtk.RESPONSE_CANCEL, - gtk.STOCK_OPEN, - gtk.RESPONSE_OK)) - - self.set_window(fcd) - - def set_filename(self, text): - self._window.set_filename(text) - - def set_select_multiple(self, value): - self._window.set_select_multiple(value) - - def add_filters(self, filters): - for name, ext_list in filters: - f = gtk.FileFilter() - f.set_name(name) - for mem in ext_list: - f.add_pattern('*.%s' % mem) - self._window.add_filter(f) - - f = gtk.FileFilter() - f.set_name(_('All files')) - f.add_pattern('*') - self._window.add_filter(f) - - def get_filenames(self): - return [unicode(f) for f in self._files] - - def get_filename(self): - if self._files is None: - # clicked Cancel - return None - else: - return unicode(self._files[0]) - - # provide a common interface for file chooser dialogs - get_path = get_filename - def set_path(self, path): - # set_filename puts the whole path in the filename field - self._window.set_current_folder(os.path.dirname(path)) - self._window.set_current_name(os.path.basename(path)) - -class FileSaveDialog(FileDialogBase): - def __init__(self, title): - FileDialogBase.__init__(self) - self._files = None - fcd = gtk.FileChooserDialog(title, - action=gtk.FILE_CHOOSER_ACTION_SAVE, - buttons=(gtk.STOCK_CANCEL, - gtk.RESPONSE_CANCEL, - gtk.STOCK_SAVE, - gtk.RESPONSE_OK)) - self.set_window(fcd) - - def set_filename(self, text): - self._window.set_current_name(text) - - def get_filename(self): - if self._files is None: - # clicked Cancel - return None - else: - return unicode(self._files[0]) - - # provide a common interface for file chooser dialogs - get_path = get_filename - def set_path(self, path): - # set_filename puts the whole path in the filename field - self._window.set_current_folder(os.path.dirname(path)) - self._window.set_current_name(os.path.basename(path)) - -class DirectorySelectDialog(FileDialogBase): - def __init__(self, title): - FileDialogBase.__init__(self) - self._files = None - choose_str = 'Choose' - fcd = gtk.FileChooserDialog( - title, - action=gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER, - buttons=(gtk.STOCK_CANCEL, - gtk.RESPONSE_CANCEL, - choose_str, gtk.RESPONSE_OK)) - self.set_window(fcd) - - def set_directory(self, text): - self._window.set_filename(text) - - def get_directory(self): - if self._files is None: - # clicked Cancel - return None - else: - return unicode(self._files[0]) - - # provide a common interface for file chooser dialogs - get_path = get_directory - set_path = set_directory - -class AboutDialog(Dialog): - def __init__(self): - Dialog.__init__(self, "Libre Video Converter") -# _("About %(appname)s", -# {'appname': app.config.get(prefs.SHORT_APP_NAME)})) -# self.add_button(_("Close")) - self.add_button("Close") - self._window.set_has_separator(False) - - def build_content(self): - packing_vbox = layout.VBox(spacing=20) - #icon_pixbuf = gtk.gdk.pixbuf_new_from_file_at_size( - # resources.share_path('icons/hicolor/128x128/apps/miro.png'), - # 48, 48) - #packing_vbox._widget.pack_start(gtk.image_new_from_pixbuf(icon_pixbuf)) - #if app.config.get(prefs.APP_REVISION_NUM): - # version = "%s (%s)" % ( - # app.config.get(prefs.APP_VERSION), - # app.config.get(prefs.APP_REVISION_NUM)) - #else: - # version = "%s" % app.config.get(prefs.APP_VERSION) - version = '3.0' - #name_label = gtk.Label( - # '<span size="xx-large" weight="bold">%s %s</span>' % ( - # app.config.get(prefs.SHORT_APP_NAME), version)) - name_label = gtk.Label( - '<span size="xx-large" weight="bold">%s %s</span>' % ( - 'Libre Video Converter', version)) - name_label.set_use_markup(True) - packing_vbox._widget.pack_start(name_label) - copyright_text = 'Copyright (c) Jesus Eduardo (Heckyel) | 2017' - copyright_label = gtk.Label('<small>%s</small>' % copyright_text) - copyright_label.set_use_markup(True) - copyright_label.set_justify(gtk.JUSTIFY_CENTER) - packing_vbox._widget.pack_start(copyright_label) - - # FIXME - make the project url clickable - #packing_vbox._widget.pack_start( - # gtk.Label(app.config.get(prefs.PROJECT_URL))) - - #contributor_label = gtk.Label( - # _("Thank you to all the people who contributed to %(appname)s " - # "%(version)s:", - # {"appname": app.config.get(prefs.SHORT_APP_NAME), - # "version": app.config.get(prefs.APP_VERSION)})) - #contributor_label.set_justify(gtk.JUSTIFY_CENTER) - #packing_vbox._widget.pack_start(contributor_label) - - # get contributors, remove newlines and wrap it - #contributors = open(resources.path('CREDITS'), 'r').readlines() - #contributors = [c[2:].strip() - # for c in contributors if c.startswith("* ")] - #contributors = ", ".join(contributors) - - # show contributors - #contrib_buffer = gtk.TextBuffer() - #contrib_buffer.set_text(contributors) - - #contrib_view = gtk.TextView(contrib_buffer) - #contrib_view.set_editable(False) - #contrib_view.set_cursor_visible(False) - #contrib_view.set_wrap_mode(gtk.WRAP_WORD) - #contrib_window = gtk.ScrolledWindow() - #contrib_window.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS) - #contrib_window.add(contrib_view) - #contrib_window.set_size_request(-1, 100) - #packing_vbox._widget.pack_start(contrib_window) - - # FIXME - make the project url clickable - #donate_label = gtk.Label( - # _("To help fund continued %(appname)s development, visit the " - # "donation page at:", - # {"appname": app.config.get(prefs.SHORT_APP_NAME)})) - #donate_label.set_justify(gtk.JUSTIFY_CENTER) - #packing_vbox._widget.pack_start(donate_label) - - #packing_vbox._widget.pack_start( - # gtk.Label(app.config.get(prefs.DONATE_URL))) - return packing_vbox - - def on_contrib_link_event(self, texttag, widget, event, iter_): - if event.type == gtk.gdk.BUTTON_PRESS: - resources.open_url('https://notabug.org/heckyel/librevideoconverter') - -type_map = { - 0: gtk.MESSAGE_WARNING, - 1: gtk.MESSAGE_INFO, - 2: gtk.MESSAGE_ERROR -} - -class AlertDialog(DialogBase): - def __init__(self, title, description, alert_type): - DialogBase.__init__(self) - message_type = type_map.get(alert_type, gtk.MESSAGE_INFO) - self.set_window(gtk.MessageDialog(type=message_type, - message_format=description)) - self._window.set_title(title) - self.description = description - - def add_button(self, text): - self._window.add_button(_stock.get(text, text), 1) - self._window.set_default_response(1) - - def _run(self): - self._window.set_modal(False) - self._window.show_all() - response = self._window.run() - self._window.hide() - if response == gtk.RESPONSE_DELETE_EVENT: - return -1 - else: - # response IDs start at 1 - return response - 1 diff --git a/mvc/widgets/gtk/wrappermap.py b/mvc/widgets/gtk/wrappermap.py deleted file mode 100644 index c2b2aad..0000000 --- a/mvc/widgets/gtk/wrappermap.py +++ /dev/null @@ -1,50 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -""".wrappermap -- Map GTK Widgets to the Libre Widget -that wraps them. -""" - -import weakref - -# Maps gtk windows -> wrapper objects. We use a weak references to prevent -# circular references between the GTK widget and it's wrapper. (Keeping a -# reference to the GTK widget is fine, since if the wrapper is alive, the GTK -# widget should be). -widget_mapping = weakref.WeakValueDictionary() - -def wrapper(gtk_widget): - """Find the wrapper widget for a GTK widget.""" - try: - return widget_mapping[gtk_widget] - except KeyError: - raise KeyError("Widget wrapper no longer exists") - -def add(gtk_widget, wrapper): - widget_mapping[gtk_widget] = wrapper diff --git a/mvc/widgets/keyboard.py b/mvc/widgets/keyboard.py deleted file mode 100644 index 6700de2..0000000 --- a/mvc/widgets/keyboard.py +++ /dev/null @@ -1,69 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""Define keyboard input in a platform-independant way.""" - -(CTRL, ALT, SHIFT, CMD, MOD, RIGHT_ARROW, LEFT_ARROW, UP_ARROW, - DOWN_ARROW, SPACE, ENTER, DELETE, BKSPACE, ESCAPE, - F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12) = range(26) - -class Shortcut: - """Defines a shortcut key combination used to trigger this - menu item. - - The first argument is the shortcut key. Other arguments are - modifiers. - - Examples: - - >>> Shortcut("x", MOD) - >>> Shortcut(BKSPACE, MOD) - - This is wrong: - - >>> Shortcut(MOD, "x") - """ - def __init__(self, shortcut, *modifiers): - self.shortcut = shortcut - self.modifiers = modifiers - - def _get_key_symbol(self, value): - """Translate key values to their symbolic names.""" - if isinstance(self.shortcut, int): - shortcut_string = '<Unknown>' - for name, value in globals().iteritems(): - if value == self.shortcut: - return name - return repr(value) - - def __str__(self): - shortcut_string = self._get_key_symbol(self.shortcut) - mod_string = repr(set(self._get_key_symbol(k) for k in - self.modifiers)) - return "Shortcut(%s, %s)" % (shortcut_string, mod_string) diff --git a/mvc/widgets/menus.py b/mvc/widgets/menus.py deleted file mode 100644 index 62b0c68..0000000 --- a/mvc/widgets/menus.py +++ /dev/null @@ -1,268 +0,0 @@ -# menus.py -# -# Most of these are taken from libs/frontends/widgets/menus.py in the miro -# project. -# -# TODO: merge common bits! - -import collections - -from mvc import signals -from mvc.widgets import widgetutil -from mvc.widgets import widgetset -from mvc.widgets import app - -from mvc.widgets.keyboard import (Shortcut, CTRL, ALT, SHIFT, CMD, - MOD, RIGHT_ARROW, LEFT_ARROW, UP_ARROW, DOWN_ARROW, SPACE, ENTER, DELETE, - BKSPACE, ESCAPE, F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12) - -# XXX hack: - -def _(text, *params): - if params: - return text % params[0] - return text - -class MenuItem(widgetset.MenuItem): - """Portable MenuItem class. - - This adds group handling to the platform menu items. - """ - # group_map is used for the legacy menu updater code - group_map = collections.defaultdict(set) - - def __init__(self, label, name, shortcut=None, groups=None, - **state_labels): - widgetset.MenuItem.__init__(self, label, name, shortcut) - # state_labels is used for the legacy menu updater code - self.state_labels = state_labels - if groups: - if len(groups) > 1: - raise ValueError("only support one group") - MenuItem.group_map[groups[0]].add(self) - -class MenuItemFetcher(object): - """Get MenuItems by their name quickly. """ - - def __init__(self): - self._cache = {} - - def __getitem__(self, name): - if name in self._cache: - return self._cache[name] - else: - menu_item = app.widgetapp.menubar.find(name) - self._cache[name] = menu_item - return menu_item - -def get_app_menu(): - """Returns the default menu structure.""" - - app_name = "Libre Video Converter" # XXX HACK - - file_menu = widgetset.Menu(_("_File"), "FileMenu", [ - MenuItem(_("_Open"), "Open", Shortcut("o", MOD), - groups=["NonPlaying"]), - MenuItem(_("_Quit"), "Quit", Shortcut("q", MOD)), - ]) - help_menu = widgetset.Menu(_("_Help"), "HelpMenu", [ - MenuItem(_("About %(name)s", - {'name': app_name}), - "About") - ]) - - all_menus = [file_menu, help_menu] - return all_menus - -action_handlers = {} -group_action_handlers = {} - -def on_menubar_activate(menubar, action_name): - callback = lookup_handler(action_name) - if callback is not None: - callback() - -def lookup_handler(action_name): - """For a given action name, get a callback to handle it. Return - None if no callback is found. - """ - - retval = _lookup_group_handler(action_name) - if retval is None: - retval = action_handlers.get(action_name) - return retval - -def _lookup_group_handler(action_name): - try: - group_name, callback_arg = action_name.split('-', 1) - except ValueError: - return None # split return tuple of length 1 - try: - group_handler = group_action_handlers[group_name] - except KeyError: - return None - else: - return lambda: group_handler(callback_arg) - -def action_handler(name): - """Decorator for functions that handle menu actions.""" - def decorator(func): - action_handlers[name] = func - return func - return decorator - -def group_action_handler(action_prefix): - def decorator(func): - group_action_handlers[action_prefix] = func - return func - return decorator - -# File menu -@action_handler("Open") -def on_open(): - app.widgetapp.choose_file() - -@action_handler("Quit") -def on_quit(): - app.widgetapp.quit() - -# Help menu -@action_handler("About") -def on_about(): - app.widgetapp.about() - -class MenuManager(signals.SignalEmitter): - """Updates the menu based on the current selection. - - This includes enabling/disabling menu items, changing menu text - for plural selection and enabling/disabling the play button. The - play button is obviously not a menu item, but it's pretty closely - related - - Whenever code makes a change that could possibly affect which menu - items should be enabled/disabled, it should call the - update_menus() method. - - Signals: - - menus-updated(reasons): Emitted whenever update_menus() is called - """ - def __init__(self): - signals.SignalEmitter.__init__(self, 'menus-updated') - self.menu_item_fetcher = MenuItemFetcher() - #self.subtitle_encoding_updater = SubtitleEncodingMenuUpdater() - self.subtitle_encoding_updater = None - - def setup_menubar(self, menubar): - """Setup the main miro menubar. - """ - menubar.add_initial_menus(get_app_menu()) - menubar.connect("activate", on_menubar_activate) - self.menu_updaters = [] - - def _set_play_pause(self): - if ((not app.playback_manager.is_playing - or app.playback_manager.is_paused)): - label = _('Play') - else: - label = _('Pause') - self.menu_item_fetcher['PlayPauseItem'].set_label(label) - - def add_subtitle_encoding_menu(self, category_label, *encodings): - """Set up a subtitles encoding menu. - - This method should be called for each category of subtitle encodings - (East Asian, Western European, Unicode, etc). Pass it the list of - encodings for that category. - - :param category_label: human-readable name for the category - :param encodings: list of (label, encoding) tuples. label is a - human-readable name, and encoding is a value that we can pass to - VideoDisplay.select_subtitle_encoding() - """ - self.subtitle_encoding_updater.add_menu(category_label, encodings) - - def select_subtitle_encoding(self, encoding): - if not self.subtitle_encoding_updater.has_encodings(): - # OSX never sets up the subtitle encoding menu - return - menu_item_name = self.subtitle_encoding_updater.action_name(encoding) - try: - self.menu_item_fetcher[menu_item_name].set_state(True) - except KeyError: - logging.warn("Error enabling subtitle encoding menu item: %s", - menu_item_name) - - def update_menus(self, *reasons): - """Call this when a change is made that could change the menus - - Use reasons to describe why the menus could change. Some MenuUpdater - objects will do some optimizations based on that - """ - reasons = set(reasons) - self._set_play_pause() - for menu_updater in self.menu_updaters: - menu_updater.update(reasons) - self.emit('menus-updated', reasons) - -class MenuUpdater(object): - """Base class for objects that dynamically update menus.""" - def __init__(self, menu_name): - self.menu_name = menu_name - self.first_update = False - - # we lazily access our menu item, since we are created before the menubar - # is fully setup. - def get_menu(self): - try: - return self._menu - except AttributeError: - self._menu = app.widgetapp.menubar.find(self.menu_name) - return self._menu - menu = property(get_menu) - - def update(self, reasons): - if not self.first_update and not self.should_process_update(reasons): - return - self.first_update = False - self.start_update() - if not self.should_show_menu(): - self.menu.hide() - return - - self.menu.show() - if self.should_rebuild_menu(): - self.clear_menu() - self.populate_menu() - self.update_items() - - def should_process_update(self, reasons): - """Test if we should ignore the update call. - - :param reasons: the reasons passed in to MenuManager.update_menus() - """ - return True - - def clear_menu(self): - """Remove items from our menu before rebuilding it.""" - for child in self.menu.get_children(): - self.menu.remove(child) - - def start_update(self): - """Called at the very start of the update method. """ - pass - - def should_show_menu(self): - """Should we display the menu? """ - return True - - def should_rebuild_menu(self): - """Should we rebuild the menu structure?""" - return False - - def populate_menu(self): - """Add MenuItems to our menu.""" - pass - - def update_items(self): - """Update our menu items.""" - pass diff --git a/mvc/widgets/osx/Resources-Widgets/MainMenu.nib/designable.nib b/mvc/widgets/osx/Resources-Widgets/MainMenu.nib/designable.nib deleted file mode 100644 index b7fefd6..0000000 --- a/mvc/widgets/osx/Resources-Widgets/MainMenu.nib/designable.nib +++ /dev/null @@ -1,145 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<archive type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="8.00"> - <data> - <int key="IBDocument.SystemTarget">1060</int> - <string key="IBDocument.SystemVersion">12A269</string> - <string key="IBDocument.InterfaceBuilderVersion">2549</string> - <string key="IBDocument.AppKitVersion">1187</string> - <string key="IBDocument.HIToolboxVersion">624.00</string> - <object class="NSMutableDictionary" key="IBDocument.PluginVersions"> - <string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="NS.object.0">2549</string> - </object> - <array key="IBDocument.IntegratedClassDependencies"> - <string>NSCustomObject</string> - <string>NSMenu</string> - <string>NSMenuItem</string> - </array> - <array key="IBDocument.PluginDependencies"> - <string>com.apple.InterfaceBuilder.CocoaPlugin</string> - </array> - <object class="NSMutableDictionary" key="IBDocument.Metadata"> - <string key="NS.key.0">PluginDependencyRecalculationVersion</string> - <integer value="1" key="NS.object.0"/> - </object> - <array class="NSMutableArray" key="IBDocument.RootObjects" id="864178278"> - <object class="NSCustomObject" id="422340081"> - <object class="NSMutableString" key="NSClassName"> - <characters key="NS.bytes">NSApplication</characters> - </object> - </object> - <object class="NSCustomObject" id="99063961"> - <string key="NSClassName">FirstResponder</string> - </object> - <object class="NSCustomObject" id="399126242"> - <string key="NSClassName">NSApplication</string> - </object> - <object class="NSMenu" id="603720448"> - <string key="NSTitle">MainMenu</string> - <array class="NSMutableArray" key="NSMenuItems"> - <object class="NSMenuItem" id="726726549"> - <reference key="NSMenu" ref="603720448"/> - <string key="NSTitle">Libre Video Converter</string> - <string key="NSKeyEquiv"/> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <object class="NSCustomResource" key="NSOnImage"> - <string key="NSClassName">NSImage</string> - <string key="NSResourceName">NSMenuCheckmark</string> - </object> - <object class="NSCustomResource" key="NSMixedImage"> - <string key="NSClassName">NSImage</string> - <string key="NSResourceName">NSMenuMixedState</string> - </object> - <string key="NSAction">submenuAction:</string> - <object class="NSMenu" key="NSSubmenu" id="530441688"> - <string key="NSTitle">Libre Video Converter</string> - <array class="NSMutableArray" key="NSMenuItems"/> - <string key="NSName">_NSAppleMenu</string> - </object> - </object> - </array> - <string key="NSName">_NSMainMenu</string> - </object> - </array> - <object class="IBObjectContainer" key="IBDocument.Objects"> - <array class="NSMutableArray" key="connectionRecords"/> - <object class="IBMutableOrderedSet" key="objectRecords"> - <array key="orderedObjects"> - <object class="IBObjectRecord"> - <int key="objectID">0</int> - <array key="object" id="0"/> - <reference key="children" ref="864178278"/> - <nil key="parent"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">-2</int> - <reference key="object" ref="422340081"/> - <reference key="parent" ref="0"/> - <string key="objectName">File's Owner</string> - </object> - <object class="IBObjectRecord"> - <int key="objectID">-1</int> - <reference key="object" ref="99063961"/> - <reference key="parent" ref="0"/> - <string key="objectName">First Responder</string> - </object> - <object class="IBObjectRecord"> - <int key="objectID">29</int> - <reference key="object" ref="603720448"/> - <array class="NSMutableArray" key="children"> - <reference ref="726726549"/> - </array> - <reference key="parent" ref="0"/> - <string key="objectName">MainMenu</string> - </object> - <object class="IBObjectRecord"> - <int key="objectID">56</int> - <reference key="object" ref="726726549"/> - <array class="NSMutableArray" key="children"> - <reference ref="530441688"/> - </array> - <reference key="parent" ref="603720448"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">57</int> - <reference key="object" ref="530441688"/> - <reference key="parent" ref="726726549"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">-3</int> - <reference key="object" ref="399126242"/> - <reference key="parent" ref="0"/> - <string key="objectName">Application</string> - </object> - </array> - </object> - <dictionary class="NSMutableDictionary" key="flattenedProperties"> - <string key="-1.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="-2.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="-3.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="29.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="56.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="57.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - </dictionary> - <dictionary class="NSMutableDictionary" key="unlocalizedProperties"/> - <nil key="activeLocalization"/> - <dictionary class="NSMutableDictionary" key="localizations"/> - <nil key="sourceID"/> - <int key="maxID">248</int> - </object> - <object class="IBClassDescriber" key="IBDocument.Classes"/> - <int key="IBDocument.localizationMode">0</int> - <string key="IBDocument.TargetRuntimeIdentifier">IBCocoaFramework</string> - <object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDependencies"> - <string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin.macosx</string> - <real value="1060" key="NS.object.0"/> - </object> - <bool key="IBDocument.PluginDeclaredDependenciesTrackSystemTargetVersion">YES</bool> - <int key="IBDocument.defaultPropertyAccessControl">3</int> - <dictionary class="NSMutableDictionary" key="IBDocument.LastKnownImageSizes"> - <string key="NSMenuCheckmark">{11, 11}</string> - <string key="NSMenuMixedState">{10, 3}</string> - </dictionary> - </data> -</archive> diff --git a/mvc/widgets/osx/Resources-Widgets/MainMenu.nib/keyedobjects.nib b/mvc/widgets/osx/Resources-Widgets/MainMenu.nib/keyedobjects.nib Binary files differdeleted file mode 100644 index 963b444..0000000 --- a/mvc/widgets/osx/Resources-Widgets/MainMenu.nib/keyedobjects.nib +++ /dev/null diff --git a/mvc/widgets/osx/__init__.py b/mvc/widgets/osx/__init__.py deleted file mode 100644 index f227b35..0000000 --- a/mvc/widgets/osx/__init__.py +++ /dev/null @@ -1,74 +0,0 @@ -import sys - -from objc import * -from Foundation import * -from AppKit import * - -from PyObjCTools import AppHelper - -size_request_manager = None - -class AppController(NSObject): - def applicationDidFinishLaunching_(self, notification): - from mvc.widgets.osx.osxmenus import MenuBar - self.portableApp.menubar = MenuBar() - self.portableApp.startup() - self.portableApp.run() - - def setPortableApp_(self, portableApp): - self.portableApp = portableApp - - def handleMenuActivate_(self, menu_item): - from mvc.widgets.osx import osxmenus - osxmenus.handle_menu_activate(menu_item) - -def initialize(app): - nsapp = NSApplication.sharedApplication() - delegate = AppController.alloc().init() - delegate.setPortableApp_(app) - nsapp.setDelegate_(delegate) - - global size_request_manager - from mvc.widgets.osx.widgetupdates import SizeRequestManager - size_request_manager = SizeRequestManager() - - NSApplicationMain(sys.argv) - -def attach_menubar(): - pass - -def mainloop_start(): - pass - -def mainloop_stop(): - NSApplication.sharedApplication().terminate_(nil) - -def idle_add(callback, periodic=None): - def wrapper(): - callback() - if periodic is not None: - AppHelper.callLater(periodic, wrapper) - if periodic is not None and periodic < 0: - raise ValueError('periodic cannot be negative') - # XXX: we have a lousy thread API that doesn't allocate pools for us... - pool = NSAutoreleasePool.alloc().init() - if periodic is not None: - AppHelper.callLater(periodic, wrapper) - else: - AppHelper.callAfter(wrapper) - del pool - -def idle_remove(id_): - pass - -def reveal_file(filename): - # XXX: dumb lousy type conversions ... - path = NSURL.fileURLWithPath_(filename.decode('utf-8')).path() - NSWorkspace.sharedWorkspace().selectFile_inFileViewerRootedAtPath_( - path, nil) - -def get_conversion_directory(): - url, error = NSFileManager.defaultManager().URLForDirectory_inDomain_appropriateForURL_create_error_(NSMoviesDirectory, NSUserDomainMask, nil, YES, None) - if error: - return None - return url.path().encode('utf-8') diff --git a/mvc/widgets/osx/base.py b/mvc/widgets/osx/base.py deleted file mode 100644 index 913b372..0000000 --- a/mvc/widgets/osx/base.py +++ /dev/null @@ -1,367 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -""".base.py -- Widget base classes.""" - -from AppKit import * -from Foundation import * -from objc import YES, NO, nil - -from mvc import signals -import wrappermap -from .viewport import Viewport, BorrowedViewport - -class Widget(signals.SignalEmitter): - """Base class for Cocoa widgets. - - attributes: - - CREATES_VIEW -- Does the widget create a view for itself? If this is True - the widget must have an attribute named view, which is the view that the - widget uses. - - placement -- What portion of view the widget occupies. - """ - - CREATES_VIEW = True - - def __init__(self): - signals.SignalEmitter.__init__(self, 'size-request-changed', - 'size-allocated', 'key-press', 'focus-out') - self.create_signal('place-in-scroller') - self.viewport = None - self.parent_is_scroller = False - self.manual_size_request = None - self.cached_size_request = None - self._disabled = False - - def set_can_focus(self, allow): - assert isinstance(self.view, NSControl) - self.view.setRefusesFirstResponder_(not allow) - - def set_size_request(self, width, height): - self.manual_size_request = (width, height) - self.invalidate_size_request() - - def clear_size_request_cache(self): - from mvc.widgets.osx import size_request_manager - if size_request_manager is not None: - while size_request_manager.widgets_to_request: - size_request_manager._run_requests() - - def get_size_request(self): - if self.manual_size_request: - width, height = self.manual_size_request - if width == -1: - width = self.get_natural_size_request()[0] - if height == -1: - height = self.get_natural_size_request()[1] - return width, height - return self.get_natural_size_request() - - def get_natural_size_request(self): - if self.cached_size_request: - return self.cached_size_request - else: - self.cached_size_request = self.calc_size_request() - return self.cached_size_request - - def invalidate_size_request(self): - from mvc.widgets.osx import size_request_manager - if size_request_manager is not None: - size_request_manager.add_widget(self) - - def do_invalidate_size_request(self): - """Recalculate the size request for this widget.""" - old_size_request = self.cached_size_request - self.cached_size_request = None - self.emit('size-request-changed', old_size_request) - - def calc_size_request(self): - """Return the minimum size needed to display this widget. - Must be Implemented by subclasses. - """ - raise NotImplementedError() - - def _debug_size_request(self, nesting_level=0): - """Debug size request calculations. - - This method recursively prints out the size request for each widget. - """ - request = self.calc_size_request() - width = int(request[0]) - height = int(request[1]) - indent = ' ' * nesting_level - me = str(self.__class__).split('.')[-1] - print '%s%s: %sx%s' % (indent, me, width, height) - - def place(self, rect, containing_view): - """Place this widget on a view. """ - if self.viewport is None: - if self.CREATES_VIEW: - self.viewport = Viewport(self.view, rect) - containing_view.addSubview_(self.view) - wrappermap.add(self.view, self) - else: - self.viewport = BorrowedViewport(containing_view, rect) - self.viewport_created() - else: - if not self.viewport.at_position(rect): - self.viewport.reposition(rect) - self.viewport_repositioned() - self.emit('size-allocated', rect.size.width, rect.size.height) - - def remove_viewport(self): - if self.viewport is not None: - self.viewport.remove() - self.viewport = None - if self.CREATES_VIEW: - wrappermap.remove(self.view) - - def viewport_created(self): - """Called after we first create a viewport. Subclasses can override - this method if they want to handle this event. - """ - - def viewport_repositioned(self): - """Called when we reposition our viewport. Subclasses can override - this method if they want to handle this event. - """ - - def viewport_scrolled(self): - """Called by the Scroller widget on it's child widget when it is - scrolled. - """ - - def get_width(self): - return int(self.viewport.get_width()) - width = property(get_width) - - def get_height(self): - return int(self.viewport.get_height()) - height = property(get_height) - - def get_window(self): - if not self.viewport.view: - return None - return wrappermap.wrapper(self.viewport.view.window()) - - def queue_redraw(self): - if self.viewport: - self.viewport.queue_redraw() - - def redraw_now(self): - if self.viewport: - self.viewport.redraw_now() - - def relative_position(self, other_widget): - """Get the position of another widget, relative to this widget.""" - basePoint = self.viewport.view.convertPoint_fromView_( - other_widget.viewport.area().origin, - other_widget.viewport.view) - return (basePoint.x - self.viewport.area().origin.x, - basePoint.y - self.viewport.area().origin.y) - - def make_color(self, (red, green, blue)): - return NSColor.colorWithDeviceRed_green_blue_alpha_(red, green, blue, - 1.0) - - def enable(self): - self._disabled = False - - def disable(self): - self._disabled = True - - def set_disabled(self, disabled): - if disabled: - self.disable() - else: - self.enable() - - def get_disabled(self): - return self._disabled - -class Container(Widget): - """Widget that holds other widgets. """ - - def __init__(self): - Widget.__init__(self) - self.callback_handles = {} - - def on_child_size_request_changed(self, child, old_size): - self.invalidate_size_request() - - def connect_child_signals(self, child): - handle = child.connect_weak('size-request-changed', - self.on_child_size_request_changed) - self.callback_handles[child] = handle - - def disconnect_child_signals(self, child): - child.disconnect(self.callback_handles.pop(child)) - - def remove_viewport(self): - for child in self.children: - child.remove_viewport() - Widget.remove_viewport(self) - - def child_added(self, child): - """Must be called by subclasses when a child is added to the - Container.""" - self.connect_child_signals(child) - self.children_changed() - - def child_removed(self, child): - """Must be called by subclasses when a child is removed from the - Container.""" - self.disconnect_child_signals(child) - child.remove_viewport() - self.children_changed() - - def child_changed(self, old_child, new_child): - """Must be called by subclasses when a child is replaced by a new - child in the Container. To simplify things a bit for subclasses, - old_child can be None in which case this is the same as - child_added(new_child). - """ - if old_child is not None: - self.disconnect_child_signals(old_child) - old_child.remove_viewport() - self.connect_child_signals(new_child) - self.children_changed() - - def children_changed(self): - """Invoked when the set of children for this widget changes.""" - self.do_invalidate_size_request() - - def do_invalidate_size_request(self): - Widget.do_invalidate_size_request(self) - if self.viewport: - self.place_children() - - def viewport_created(self): - self.place_children() - - def viewport_repositioned(self): - self.place_children() - - def viewport_scrolled(self): - for child in self.children: - child.viewport_scrolled() - - def place_children(self): - """Layout our child widgets. Must be implemented by subclasses.""" - raise NotImplementedError() - - def _debug_size_request(self, nesting_level=0): - for child in self.children: - child._debug_size_request(nesting_level+1) - Widget._debug_size_request(self, nesting_level) - -class Bin(Container): - """Container that only has one child widget.""" - - def __init__(self, child=None): - Container.__init__(self) - self.child = None - if child is not None: - self.add(child) - - def get_children(self): - if self.child: - return [self.child] - else: - return [] - children = property(get_children) - - def add(self, child): - if self.child is not None: - raise ValueError("Already have a child: %s" % self.child) - self.child = child - self.child_added(self.child) - - def remove(self): - if self.child is not None: - old_child = self.child - self.child = None - self.child_removed(old_child) - - def set_child(self, new_child): - old_child = self.child - self.child = new_child - self.child_changed(old_child, new_child) - - def enable(self): - Container.enable(self) - self.child.enable() - - def disable(self): - Container.disable(self) - self.child.disable() - -class SimpleBin(Bin): - """Bin that whose child takes up it's entire space.""" - - def calc_size_request(self): - if self.child is None: - return (0, 0) - else: - return self.child.get_size_request() - - def place_children(self): - if self.child: - self.child.place(self.viewport.area(), self.viewport.view) - -class FlippedView(NSView): - """Flipped NSView. We use these internally to lessen the differences - between Cocoa and GTK. - """ - - def init(self): - self = super(FlippedView, self).init() - self.background = None - return self - - def initWithFrame_(self, rect): - self = super(FlippedView, self).initWithFrame_(rect) - self.background = None - return self - - def isFlipped(self): - return YES - - def isOpaque(self): - return self.background is not None - - def setBackgroundColor_(self, color): - self.background = color - - def drawRect_(self, rect): - if self.background: - self.background.set() - NSBezierPath.fillRect_(rect) diff --git a/mvc/widgets/osx/const.py b/mvc/widgets/osx/const.py deleted file mode 100644 index ae0da40..0000000 --- a/mvc/widgets/osx/const.py +++ /dev/null @@ -1,44 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -from AppKit import * - -"""const.py -- Constants""" - -DRAG_ACTION_NONE = NSDragOperationNone -DRAG_ACTION_COPY = NSDragOperationCopy -DRAG_ACTION_MOVE = NSDragOperationMove -DRAG_ACTION_LINK = NSDragOperationLink -DRAG_ACTION_ALL = (DRAG_ACTION_COPY | DRAG_ACTION_MOVE | DRAG_ACTION_LINK) - -ITEM_TITLE_FONT = "Helvetica" -ITEM_DESC_FONT = "Helvetica" -ITEM_INFO_FONT = "Lucida Grande" - -TOOLBAR_GRAY = (0.19, 0.19, 0.19) diff --git a/mvc/widgets/osx/contextmenu.py b/mvc/widgets/osx/contextmenu.py deleted file mode 100644 index 7a8fa55..0000000 --- a/mvc/widgets/osx/contextmenu.py +++ /dev/null @@ -1,84 +0,0 @@ -from AppKit import * -from objc import nil - -from .base import Widget - -class ContextMenuHandler(NSObject): - def initWithCallback_widget_i_(self, callback, widget, i): - self = super(ContextMenuHandler, self).init() - self.callback = callback - self.widget = widget - self.i = i - return self - - def handleMenuItem_(self, sender): - self.callback(self.widget, self.i) - - -class MiroContextMenu(NSMenu): - # Works exactly like NSMenu, except it keeps a reference to the menu - # handler objects. - def init(self): - self = super(MiroContextMenu, self).init() - self.handlers = set() - return self - - def addItem_(self, item): - if isinstance(item.target(), ContextMenuHandler): - self.handlers.add(item.target()) - return NSMenu.addItem_(self, item) - - -class ContextMenu(object): - - def __init__(self, options): - super(ContextMenu, self).__init__() - self.menu = MiroContextMenu.alloc().init() - for i, item_info in enumerate(options): - if item_info is None: - nsitem = NSMenuItem.separatorItem() - else: - label, callback = item_info - nsitem = NSMenuItem.alloc().init() - font_size = NSFont.systemFontSize() - font = NSFont.fontWithName_size_("Lucida Sans Italic", font_size) - if font is None: - font = NSFont.systemFontOfSize_(font_size) - attributes = {NSFontAttributeName: font} - attributed_label = NSAttributedString.alloc().initWithString_attributes_(label, attributes) - nsitem.setAttributedTitle_(attributed_label) - else: - nsitem.setTitle_(label) - if isinstance(callback, list): - submenu = ContextMenu(callback) - self.menu.setSubmenu_forItem_(submenu.menu, nsitem) - else: - handler = ContextMenuHandler.alloc().initWithCallback_widget_i_(callback, self, i) - nsitem.setTarget_(handler) - nsitem.setAction_('handleMenuItem:') - self.menu.addItem_(nsitem) - - def popup(self): - # support for non-window based popups thanks to - # http://stackoverflow.com/questions/9033534/how-can-i-pop-up-nsmenu-at-mouse-cursor-position - location = NSEvent.mouseLocation() - frame = NSMakeRect(location.x, location.y, 200, 200) - window = NSWindow.alloc().initWithContentRect_styleMask_backing_defer_( - frame, - NSBorderlessWindowMask, - NSBackingStoreBuffered, - NO) - window.setAlphaValue_(0) - window.makeKeyAndOrderFront_(NSApp) - location_in_window = window.convertScreenToBase_(location) - event = NSEvent.mouseEventWithType_location_modifierFlags_timestamp_windowNumber_context_eventNumber_clickCount_pressure_( - NSLeftMouseDown, - location_in_window, - 0, - 0, - window.windowNumber(), - nil, - 0, - 0, - 0) - NSMenu.popUpContextMenu_withEvent_forView_(self.menu, event, window.contentView()) diff --git a/mvc/widgets/osx/control.py b/mvc/widgets/osx/control.py deleted file mode 100644 index ed6ea34..0000000 --- a/mvc/widgets/osx/control.py +++ /dev/null @@ -1,530 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -""".control - Controls.""" - -from AppKit import * -from Foundation import * -from objc import YES, NO, nil - -from mvc.widgets import widgetconst -import wrappermap -from .base import Widget -from .helpers import NotificationForwarder - -class SizedControl(Widget): - def set_size(self, size): - if size == widgetconst.SIZE_NORMAL: - self.view.cell().setControlSize_(NSRegularControlSize) - font = NSFont.systemFontOfSize_(NSFont.systemFontSize()) - self.font_size = NSFont.systemFontSize() - elif size == widgetconst.SIZE_SMALL: - font = NSFont.systemFontOfSize_(NSFont.smallSystemFontSize()) - self.view.cell().setControlSize_(NSSmallControlSize) - self.font_size = NSFont.smallSystemFontSize() - else: - self.view.cell().setControlSize_(NSRegularControlSize) - font = NSFont.systemFontOfSize_(NSFont.systemFontSize() * size) - self.font_size = NSFont.systemFontSize() * size - self.view.setFont_(font) - -class BaseTextEntry(SizedControl): - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - def __init__(self, initial_text=None): - SizedControl.__init__(self) - self.view = self.make_view() - self.font = NSFont.systemFontOfSize_(NSFont.systemFontSize()) - self.view.setFont_(self.font) - self.view.setEditable_(YES) - self.view.cell().setScrollable_(YES) - self.view.cell().setLineBreakMode_(NSLineBreakByClipping) - self.sizer_cell = self.view.cell().copy() - if initial_text: - self.view.setStringValue_(initial_text) - self.set_width(len(initial_text)) - else: - self.set_width(10) - - self.notifications = NotificationForwarder.create(self.view) - - self.create_signal('activate') - self.create_signal('changed') - self.create_signal('validate') - - def focus(self): - if self.view.window() is not None: - self.view.window().makeFirstResponder_(self.view) - - def start_editing(self, initial_text): - self.set_text(initial_text) - self.focus() - # unselect the text and locate the cursor at the end of the entry - text_field = self.view.window().fieldEditor_forObject_(YES, self.view) - text_field.setSelectedRange_(NSMakeRange(len(self.get_text()), 0)) - - def viewport_created(self): - SizedControl.viewport_created(self) - self.notifications.connect(self.on_changed, 'NSControlTextDidChangeNotification') - self.notifications.connect(self.on_end_editing, - 'NSControlTextDidEndEditingNotification') - - def remove_viewport(self): - SizedControl.remove_viewport(self) - self.notifications.disconnect() - - def baseline(self): - return -self.view.font().descender() + 2 - - def on_changed(self, notification): - self.emit('changed') - - def on_end_editing(self, notification): - self.emit('focus-out') - - def calc_size_request(self): - size = self.sizer_cell.cellSize() - return size.width, size.height - - def set_text(self, text): - self.view.setStringValue_(text) - self.emit('changed') - - def get_text(self): - return self.view.stringValue() - - def set_width(self, chars): - self.sizer_cell.setStringValue_('X' * chars) - self.invalidate_size_request() - - def set_activates_default(self, setting): - pass - - def enable(self): - SizedControl.enable(self) - self.view.setEnabled_(True) - - def disable(self): - SizedControl.disable(self) - self.view.setEnabled_(False) - -class MiroTextField(NSTextField): - def textDidEndEditing_(self, notification): - wrappermap.wrapper(self).emit('activate') - return NSTextField.textDidEndEditing_(self, notification) - -class TextEntry(BaseTextEntry): - def make_view(self): - return MiroTextField.alloc().init() - -class NumberEntry(BaseTextEntry): - def make_view(self): - return MiroTextField.alloc().init() - - def set_max_length(self, length): - # TODO - pass - - def _filter_value(self): - """Discard any non-numeric characters""" - digits = ''.join(x for x in self.view.stringValue() if x.isdigit()) - self.view.setStringValue_(digits) - - def on_changed(self, notification): - # overriding on_changed rather than connecting to it ensures that we - # filter the value before anything else connected to the signal sees it - self._filter_value() - BaseTextEntry.on_changed(self, notification) - - def get_text(self): - # handles get_text between when text is entered and when on_changed - # filters it, in case that's possible - self._filter_value() - return BaseTextEntry.get_text(self) - -class MiroSecureTextField(NSSecureTextField): - def textDidEndEditing_(self, notification): - wrappermap.wrapper(self).emit('activate') - return NSSecureTextField.textDidEndEditing_(self, notification) - -class SecureTextEntry(BaseTextEntry): - def make_view(self): - return MiroSecureTextField.alloc().init() - -class MultilineTextEntry(Widget): - def __init__(self, initial_text=None): - Widget.__init__(self) - if initial_text is None: - initial_text = "" - self.view = NSTextView.alloc().initWithFrame_(NSRect((0,0),(50,50))) - self.view.setMaxSize_((1.0e7, 1.0e7)) - self.view.setHorizontallyResizable_(NO) - self.view.setVerticallyResizable_(YES) - self.notifications = NotificationForwarder.create(self.view) - self.create_signal('changed') - self.create_signal('focus-out') - if initial_text is not None: - self.set_text(initial_text) - self.set_size(widgetconst.SIZE_NORMAL) - - def set_size(self, size): - if size == widgetconst.SIZE_NORMAL: - font = NSFont.systemFontOfSize_(NSFont.systemFontSize()) - elif size == widgetconst.SIZE_SMALL: - self.view.cell().setControlSize_(NSSmallControlSize) - else: - raise ValueError("Unknown size: %s" % size) - self.view.setFont_(font) - - def viewport_created(self): - Widget.viewport_created(self) - self.notifications.connect(self.on_changed, 'NSTextDidChangeNotification') - self.notifications.connect(self.on_end_editing, - 'NSControlTextDidEndEditingNotification') - self.invalidate_size_request() - - def remove_viewport(self): - Widget.remove_viewport(self) - self.notifications.disconnect() - - def focus(self): - if self.view.window() is not None: - self.view.window().makeFirstResponder_(self.view) - - def set_text(self, text): - self.view.setString_(text) - self.invalidate_size_request() - - def get_text(self): - return self.view.string() - - def on_changed(self, notification): - self.invalidate_size_request() - self.emit("changed") - - def on_end_editing(self, notification): - self.emit("focus-out") - - def calc_size_request(self): - layout_manager = self.view.layoutManager() - text_container = self.view.textContainer() - # The next line is there just to force cocoa to layout the text - layout_manager.glyphRangeForTextContainer_(text_container) - rect = layout_manager.usedRectForTextContainer_(text_container) - return rect.size.width, rect.size.height - - def set_editable(self, editable): - if editable: - self.view.setEditable_(YES) - else: - self.view.setEditable_(NO) - - -class MiroButton(NSButton): - - def initWithSignal_(self, signal): - self = super(MiroButton, self).init() - self.signal = signal - return self - - def sendAction_to_(self, action, to): - # We override the Cocoa machinery here and just send it to our wrapper - # widget. - wrappermap.wrapper(self).emit(self.signal) - return YES - -class Checkbox(SizedControl): - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - def __init__(self, text="", bold=False, color=None): - SizedControl.__init__(self) - self.create_signal('toggled') - self.view = MiroButton.alloc().initWithSignal_('toggled') - self.view.setButtonType_(NSSwitchButton) - self.bold = bold - self.title = text - self.font_size = NSFont.systemFontSize() - self.color = self.make_color(color) - self._set_title() - - def set_size(self, size): - SizedControl.set_size(self, size) - self._set_title() - - def _set_title(self): - if self.color is None: - self.view.setTitle_(self.title) - else: - attributes = { - NSForegroundColorAttributeName: self.color, - NSFontAttributeName: NSFont.systemFontOfSize_(self.font_size) - } - string = NSAttributedString.alloc().initWithString_attributes_( - self.title, attributes) - self.view.setAttributedTitle_(string) - - def calc_size_request(self): - if self.manual_size_request: - width, height = self.manual_size_request - if width == -1: - width = 10000 - if height == -1: - height = 10000 - size = self.view.cell().cellSizeForBounds_( - NSRect((0, 0), (width, height))) - else: - size = self.view.cell().cellSize() - return (size.width, size.height) - - def baseline(self): - return -self.view.font().descender() + 1 - - def get_checked(self): - return self.view.state() == NSOnState - - def set_checked(self, value): - if value: - self.view.setState_(NSOnState) - else: - self.view.setState_(NSOffState) - - def enable(self): - SizedControl.enable(self) - self.view.setEnabled_(True) - - def disable(self): - SizedControl.disable(self) - self.view.setEnabled_(False) - - def get_text_padding(self): - """ - Returns the amount of space the checkbox takes up before the label. - """ - # XXX FIXME - return 18 - -class Button(SizedControl): - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - def __init__(self, label, style='normal', width=0): - SizedControl.__init__(self) - self.color = None - self.title = label - self.create_signal('clicked') - self.view = MiroButton.alloc().initWithSignal_('clicked') - self.view.setButtonType_(NSMomentaryPushInButton) - self._set_title() - self.setup_style(style) - self.min_width = width - - def set_text(self, label): - self.title = label - self._set_title() - - def set_color(self, color): - self.color = self.make_color(color) - self._set_title() - - def _set_title(self): - if self.color is None: - self.view.setTitle_(self.title) - else: - attributes = { - NSForegroundColorAttributeName: self.color, - NSFontAttributeName: self.view.font() - } - string = NSAttributedString.alloc().initWithString_attributes_( - self.title, attributes) - self.view.setAttributedTitle_(string) - - def setup_style(self, style): - if style == 'normal': - self.view.setBezelStyle_(NSRoundedBezelStyle) - self.pad_height = 0 - self.pad_width = 10 - self.min_width = 112 - elif style == 'smooth': - self.view.setBezelStyle_(NSRoundRectBezelStyle) - self.pad_width = 0 - self.pad_height = 4 - self.paragraph_style = NSMutableParagraphStyle.alloc().init() - self.paragraph_style.setAlignment_(NSCenterTextAlignment) - - def make_default(self): - self.view.setKeyEquivalent_("\r") - - def calc_size_request(self): - size = self.view.cell().cellSize() - width = max(self.min_width, size.width + self.pad_width) - height = size.height + self.pad_height - return width, height - - def baseline(self): - return -self.view.font().descender() + 10 + self.pad_height - - def enable(self): - SizedControl.enable(self) - self.view.setEnabled_(True) - - def disable(self): - SizedControl.disable(self) - self.view.setEnabled_(False) - -class MiroPopupButton(NSPopUpButton): - - def init(self): - self = super(MiroPopupButton, self).init() - self.setTarget_(self) - self.setAction_('handleChange:') - return self - - def handleChange_(self, sender): - wrappermap.wrapper(self).emit('changed', self.indexOfSelectedItem()) - -class OptionMenu(SizedControl): - def __init__(self, options): - SizedControl.__init__(self) - self.create_signal('changed') - self.view = MiroPopupButton.alloc().init() - self.options = options - for option, value in options: - self.view.addItemWithTitle_(option) - - def baseline(self): - if self.view.cell().controlSize() == NSRegularControlSize: - return -self.view.font().descender() + 6 - else: - return -self.view.font().descender() + 5 - - def calc_size_request(self): - return self.view.cell().cellSize() - - def set_selected(self, index): - self.view.selectItemAtIndex_(index) - - def get_selected(self): - return self.view.indexOfSelectedItem() - - def enable(self): - SizedControl.enable(self) - self.view.setEnabled_(True) - - def disable(self): - SizedControl.disable(self) - self.view.setEnabled_(False) - - def set_width(self, width): - # TODO - pass - -class RadioButtonGroup: - def __init__(self): - self._buttons = [] - - def handle_click(self, widget): - self.set_selected(widget) - - def add_button(self, button): - self._buttons.append(button) - button.connect('clicked', self.handle_click) - if len(self._buttons) == 1: - button.view.setState_(NSOnState) - else: - button.view.setState_(NSOffState) - - def get_buttons(self): - return self._buttons - - def get_selected(self): - for mem in self._buttons: - if mem.get_selected(): - return mem - - def set_selected(self, button): - for mem in self._buttons: - if button is mem: - mem.view.setState_(NSOnState) - else: - mem.view.setState_(NSOffState) - -class RadioButton(SizedControl): - def __init__(self, label, group=None, bold=False, color=None): - SizedControl.__init__(self) - self.create_signal('clicked') - self.view = MiroButton.alloc().initWithSignal_('clicked') - self.view.setButtonType_(NSRadioButton) - self.color = self.make_color(color) - self.title = label - self.bold = bold - self.font_size = NSFont.systemFontSize() - self._set_title() - - if group is not None: - self.group = group - else: - self.group = RadioButtonGroup() - - self.group.add_button(self) - - def set_size(self, size): - SizedControl.set_size(self, size) - self._set_title() - - def _set_title(self): - if self.color is None: - self.view.setTitle_(self.title) - else: - attributes = { - NSForegroundColorAttributeName: self.color, - NSFontAttributeName: NSFont.systemFontOfSize_(self.font_size) - } - string = NSAttributedString.alloc().initWithString_attributes_( - self.title, attributes) - self.view.setAttributedTitle_(string) - - def calc_size_request(self): - size = self.view.cell().cellSize() - return (size.width, size.height) - - def baseline(self): - -self.view.font().descender() + 2 - - def get_group(self): - return self.group - - def get_selected(self): - return self.view.state() == NSOnState - - def set_selected(self): - self.group.set_selected(self) - - def enable(self): - SizedControl.enable(self) - self.view.setEnabled_(True) - - def disable(self): - SizedControl.disable(self) - self.view.setEnabled_(False) diff --git a/mvc/widgets/osx/customcontrol.py b/mvc/widgets/osx/customcontrol.py deleted file mode 100644 index d100f33..0000000 --- a/mvc/widgets/osx/customcontrol.py +++ /dev/null @@ -1,436 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -""".customcontrol -- CustomControl handlers. """ - -import collections - -from AppKit import * -from Foundation import * -from objc import YES, NO, nil - -from mvc.widgets import widgetconst -import wrappermap -from .base import Widget -import drawing -from .layoutmanager import LayoutManager - -class DrawableButtonCell(NSButtonCell): - def startTrackingAt_inView_(self, point, view): - view.setState_(NSOnState) - return YES - - def continueTracking_at_inView_(self, lastPoint, at, view): - view.setState_(NSOnState) - return YES - - def stopTracking_at_inView_mouseIsUp_(self, lastPoint, at, view, mouseIsUp): - if not mouseIsUp: - view.mouse_inside = False - view.setState_(NSOffState) - -class DrawableButton(NSButton): - def init(self): - self = super(DrawableButton, self).init() - self.layout_manager = LayoutManager() - self.tracking_area = None - self.mouse_inside = False - self.custom_cursor = None - return self - - def resetCursorRects(self): - if self.custom_cursor is not None: - self.addCursorRect_cursor_(self.visibleRect(), self.custom_cursor) - self.custom_cursor.setOnMouseEntered_(YES) - - def updateTrackingAreas(self): - # remove existing tracking area if needed - if self.tracking_area: - self.removeTrackingArea_(self.tracking_area) - - # create a new tracking area for the entire view. This allows us to - # get mouseMoved events whenever the mouse is inside our view. - self.tracking_area = NSTrackingArea.alloc() - self.tracking_area.initWithRect_options_owner_userInfo_( - self.visibleRect(), - NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | - NSTrackingActiveInKeyWindow, - self, - nil) - self.addTrackingArea_(self.tracking_area) - - def mouseEntered_(self, event): - window = self.window() - if window is not nil and window.isMainWindow(): - self.mouse_inside = True - self.setNeedsDisplay_(YES) - - def mouseExited_(self, event): - window = self.window() - if window is not nil and window.isMainWindow(): - self.mouse_inside = False - self.setNeedsDisplay_(YES) - - def isOpaque(self): - return wrappermap.wrapper(self).is_opaque() - - def drawRect_(self, rect): - context = drawing.DrawingContext(self, self.bounds(), rect) - context.style = drawing.DrawingStyle() - wrapper = wrappermap.wrapper(self) - wrapper.state = 'normal' - disabled = wrapper.get_disabled() - if not disabled: - if self.state() == NSOnState: - wrapper.state = 'pressed' - elif self.mouse_inside: - wrapper.state = 'hover' - else: - wrapper.state = 'normal' - - wrapper.draw(context, self.layout_manager) - self.layout_manager.reset() - - def sendAction_to_(self, action, to): - # We override the Cocoa machinery here and just send it to our wrapper - # widget. - wrapper = wrappermap.wrapper(self) - disabled = wrapper.get_disabled() - if not disabled: - wrapper.emit('clicked') - # Tell Cocoa we handled it anyway, just not emit the actual clicked - # event. - return YES -DrawableButton.setCellClass_(DrawableButtonCell) - -class ContinousButtonCell(DrawableButtonCell): - def stopTracking_at_inView_mouseIsUp_(self, lastPoint, at, view, mouseIsUp): - view.onStopTracking(at) - NSButtonCell.stopTracking_at_inView_mouseIsUp_(self, lastPoint, at, - view, mouseIsUp) - -class ContinuousDrawableButton(DrawableButton): - def init(self): - self = super(ContinuousDrawableButton, self).init() - self.setContinuous_(YES) - return self - - def mouseDown_(self, event): - self.releaseInbounds = self.stopTracking = self.firedOnce = False - self.cell().trackMouse_inRect_ofView_untilMouseUp_(event, - self.bounds(), self, YES) - wrapper = wrappermap.wrapper(self) - if not wrapper.get_disabled(): - if self.firedOnce: - wrapper.emit('released') - elif self.releaseInbounds: - wrapper.emit('clicked') - - def sendAction_to_(self, action, to): - if self.stopTracking: - return NO - self.firedOnce = True - wrapper = wrappermap.wrapper(self) - if not wrapper.get_disabled(): - wrapper.emit('held-down') - return YES - - def onStopTracking(self, mouseLocation): - self.releaseInbounds = NSPointInRect(mouseLocation, self.bounds()) - self.stopTracking = True -ContinuousDrawableButton.setCellClass_(ContinousButtonCell) - -class DragableButtonCell(NSButtonCell): - def startTrackingAt_inView_(self, point, view): - self.start_x = point.x - return YES - - def continueTracking_at_inView_(self, lastPoint, at, view): - DRAG_THRESHOLD = 15 - wrapper = wrappermap.wrapper(view) - if not wrapper.get_disabled(): - if (view.last_drag_event != 'right' and - at.x > self.start_x + DRAG_THRESHOLD): - wrapper.emit("dragged-right") - view.last_drag_event = 'right' - elif (view.last_drag_event != 'left' and - at.x < self.start_x - DRAG_THRESHOLD): - view.last_drag_event = 'left' - wrapper.emit("dragged-left") - return YES - -class DragableDrawableButton(DrawableButton): - def mouseDown_(self, event): - self.last_drag_event = None - self.cell().trackMouse_inRect_ofView_untilMouseUp_(event, - self.bounds(), self, YES) - - def sendAction_to_(self, action, to): - # only send the click event if we didn't send a - # dragged-left/dragged-right event - wrapper = wrappermap.wrapper(self) - if self.last_drag_event is None and not wrapper.get_disabled(): - wrapper.emit('clicked') - return YES -DragableDrawableButton.setCellClass_(DragableButtonCell) - -MouseTrackingInfo = collections.namedtuple("MouseTrackingInfo", - "start_pos click_pos") - -class CustomSliderCell(NSSliderCell): - def calc_slider_amount(self, view, pos, size): - slider_size = wrappermap.wrapper(view).slider_size() - pos -= slider_size / 2 - size -= slider_size - return max(0, min(1, float(pos) / size)) - - def get_slider_pos(self, view, value=None): - if value is None: - value = view.floatValue() - if view.isVertical(): - size = view.bounds().size.height - else: - size = view.bounds().size.width - slider_size = view.knobThickness() - size -= slider_size - start_pos = slider_size / 2.0 - ratio = ((value - view.minValue()) / - view.maxValue() - view.minValue()) - return start_pos + (ratio * size) - - def startTrackingAt_inView_(self, at, view): - wrapper = wrappermap.wrapper(view) - start_pos = self.get_slider_pos(view) - if self.isVertical(): - click_pos = at.y - else: - click_pos = at.x - # only move the cursor if the click was outside the slider - if abs(click_pos - start_pos) > view.knobThickness() / 2: - self.moveSliderTo(view, click_pos) - start_pos = click_pos - view.mouse_tracking_info = MouseTrackingInfo(start_pos, click_pos) - if not wrapper.get_disabled(): - wrapper.emit('pressed') - return YES - - def moveSliderTo(self, view, pos): - if view.isVertical(): - size = view.bounds().size.height - else: - size = view.bounds().size.width - - slider_amount = self.calc_slider_amount(view, pos, size) - value = (self.maxValue() - self.minValue()) * slider_amount - self.setFloatValue_(value) - wrapper = wrappermap.wrapper(view) - if not wrapper.get_disabled(): - wrapper.emit('moved', value) - if self.isContinuous(): - wrapper.emit('changed', value) - - def continueTracking_at_inView_(self, lastPoint, at, view): - if view.isVertical(): - mouse_pos = at.y - else: - mouse_pos = at.x - - info = view.mouse_tracking_info - new_pos = info.start_pos + (mouse_pos - info.click_pos) - self.moveSliderTo(view, new_pos) - return YES - - def stopTracking_at_inView_mouseIsUp_(self, lastPoint, at, view, mouseUp): - wrapper = wrappermap.wrapper(view) - if not wrapper.get_disabled(): - wrapper.emit('released') - view.mouse_tracking_info = None - -class CustomSliderView(NSSlider): - def init(self): - self = super(CustomSliderView, self).init() - self.layout_manager = LayoutManager() - self.custom_cursor = None - self.mouse_tracking_info = None - return self - - def get_slider_pos(self, value=None): - return self.cell().get_slider_pos(self, value) - - def resetCursorRects(self): - if self.custom_cursor is not None: - self.addCursorRect_cursor_(self.visibleRect(), self.custom_cursor) - self.custom_cursor.setOnMouseEntered_(YES) - - def isOpaque(self): - return wrappermap.wrapper(self).is_opaque() - - def knobThickness(self): - return wrappermap.wrapper(self).slider_size() - - def scrollWheel_(self, event): - wrapper = wrappermap.wrapper(self) - if wrapper.get_disabled(): - return - # NOTE: we ignore the scroll_step value passed into set_increments() - # and calculate the change using deltaY, which is in device - # coordinates. - slider_size = wrapper.slider_size() - if wrapper.is_horizontal(): - size = self.bounds().size.width - else: - size = self.bounds().size.height - size -= slider_size - - range = self.maxValue() - self.minValue() - value_change = (event.deltaY() / size) * range - self.setFloatValue_(self.floatValue() + value_change) - wrapper.emit('pressed') - wrapper.emit('changed', self.floatValue()) - wrapper.emit('released') - - def isVertical(self): - return not wrappermap.wrapper(self).is_horizontal() - - def drawRect_(self, rect): - context = drawing.DrawingContext(self, self.bounds(), rect) - context.style = drawing.DrawingStyle() - wrappermap.wrapper(self).draw(context, self.layout_manager) - self.layout_manager.reset() - - def sendAction_to_(self, action, to): - # We override the Cocoa machinery here and just send it to our wrapper - # widget. - wrapper = wrappermap.wrapper(self) - disabled = wrapper.get_disabled() - if not disabled: - wrapper.emit('changed', self.floatValue()) - # Total Cocoa we handled it anyway to prevent the event passed to - # upper layer. - return YES -CustomSliderView.setCellClass_(CustomSliderCell) - -class CustomControlBase(drawing.DrawingMixin, Widget): - def set_cursor(self, cursor): - if cursor == widgetconst.CURSOR_NORMAL: - self.view.custom_cursor = None - elif cursor == widgetconst.CURSOR_POINTING_HAND: - self.view.custom_cursor = NSCursor.pointingHandCursor() - else: - raise ValueError("Unknown cursor: %s" % cursor) - if self.view.window(): - self.view.window().invalidateCursorRectsForView_(self.view) - -class CustomButton(CustomControlBase): - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - def __init__(self): - CustomControlBase.__init__(self) - self.create_signal('clicked') - self.view = DrawableButton.alloc().init() - self.view.setRefusesFirstResponder_(NO) - self.view.setEnabled_(True) - - def enable(self): - Widget.enable(self) - self.view.setNeedsDisplay_(YES) - - def disable(self): - Widget.disable(self) - self.view.setNeedsDisplay_(YES) - -class ContinuousCustomButton(CustomButton): - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - def __init__(self): - CustomButton.__init__(self) - self.create_signal('held-down') - self.create_signal('released') - self.view = ContinuousDrawableButton.alloc().init() - self.view.setRefusesFirstResponder_(NO) - - def set_delays(self, initial, repeat): - self.view.cell().setPeriodicDelay_interval_(initial, repeat) - -class DragableCustomButton(CustomButton): - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - def __init__(self): - CustomButton.__init__(self) - self.create_signal('dragged-left') - self.create_signal('dragged-right') - self.view = DragableDrawableButton.alloc().init() - -class CustomSlider(CustomControlBase): - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - def __init__(self): - CustomControlBase.__init__(self) - self.create_signal('pressed') - self.create_signal('released') - self.create_signal('changed') - self.create_signal('moved') - self.view = CustomSliderView.alloc().init() - self.view.setRefusesFirstResponder_(NO) - if self.is_continuous(): - self.view.setContinuous_(YES) - else: - self.view.setContinuous_(NO) - self.view.setEnabled_(True) - - def get_slider_pos(self, value=None): - return self.view.get_slider_pos(value) - - def viewport_created(self): - self.view.cell().setKnobThickness_(self.slider_size()) - - def get_value(self): - return self.view.floatValue() - - def set_value(self, value): - self.view.setFloatValue_(value) - - def get_range(self): - return self.view.minValue(), self.view.maxValue() - - def set_range(self, min_value, max_value): - self.view.setMinValue_(min_value) - self.view.setMaxValue_(max_value) - - def set_increments(self, small_step, big_step, scroll_step=None): - # NOTE: we ignore all of these parameters. - # - # Cocoa doesn't have a concept of changing the increments for - # NSScroller. scroll_step is isn't really compatible with - # the event object that's passed to scrollWheel_() - pass - - def enable(self): - Widget.enable(self) - self.view.setNeedsDisplay_(YES) - - def disable(self): - Widget.disable(self) - self.view.setNeedsDisplay_(YES) diff --git a/mvc/widgets/osx/drawing.py b/mvc/widgets/osx/drawing.py deleted file mode 100644 index aaad1e9..0000000 --- a/mvc/widgets/osx/drawing.py +++ /dev/null @@ -1,289 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""miro.plat.frontend.widgets.drawing -- Draw on Views.""" - -import math - -from Foundation import * -from AppKit import * -#from Quartz import * -from objc import YES, NO, nil - - -class ImageSurface: - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - def __init__(self, image): - """Create a new ImageSurface.""" - self.image = image.nsimage.copy() - self.width = image.width - self.height = image.height - - def get_size(self): - return self.width, self.height - - def draw(self, context, x, y, width, height, fraction=1.0): - if self.width == 0 or self.height == 0: - return - current_context = NSGraphicsContext.currentContext() - current_context.setShouldAntialias_(YES) - current_context.setImageInterpolation_(NSImageInterpolationHigh) - current_context.saveGraphicsState() - flip_context(y + height) - dest_rect = NSMakeRect(x, 0, width, height) - if self.width >= width and self.height >= height: - # drawing to area smaller than our image - source_rect = NSMakeRect(0, 0, width, height) - self.image.drawInRect_fromRect_operation_fraction_( - dest_rect, source_rect, NSCompositeSourceOver, fraction) - else: - # drawing to area larger than our image. Need to tile it. - NSColor.colorWithPatternImage_(self.image).set() - current_context.setPatternPhase_( - self._calc_pattern_phase(context, x, y)) - NSBezierPath.fillRect_(dest_rect) - current_context.restoreGraphicsState() - - def draw_rect(self, context, dest_x, dest_y, source_x, source_y, width, - height, fraction=1.0): - if width == 0 or height == 0: - return - current_context = NSGraphicsContext.currentContext() - current_context.setShouldAntialias_(YES) - current_context.setImageInterpolation_(NSImageInterpolationHigh) - current_context.saveGraphicsState() - flip_context(dest_y + height) - dest_y = 0 - dest_rect = NSMakeRect(dest_x, dest_y, width, height) - source_rect = NSMakeRect(source_x, self.height-source_y-height, - width, height) - self.image.drawInRect_fromRect_operation_fraction_( - dest_rect, source_rect, NSCompositeSourceOver, fraction) - current_context.restoreGraphicsState() - - def _calc_pattern_phase(self, context, x, y): - """Calculate the pattern phase to draw tiled images. - - When we draw with a pattern, we want the image in the pattern to start - at the top-left of where we're drawing to. This function does the - dirty work necessary. - - :returns: NSPoint to send to setPatternPhase_ - """ - # convert to view coords - view_point = NSPoint(context.origin.x + x, context.origin.y + y) - # convert to window coords, which is setPatternPhase_ uses - return context.view.convertPoint_toView_(view_point, nil) - -def convert_cocoa_color(color): - rgb = color.colorUsingColorSpaceName_(NSDeviceRGBColorSpace) - return (rgb.redComponent(), rgb.greenComponent(), rgb.blueComponent()) - -def convert_widget_color(color, alpha=1.0): - return NSColor.colorWithDeviceRed_green_blue_alpha_(color[0], color[1], - color[2], alpha) -def flip_context(height): - """Make the current context's coordinates flipped. - - This is useful for drawing images, since they use the normal cocoa - coordinates and we use flipped versions. - - :param height: height of the current area we are drawing to. - """ - xform = NSAffineTransform.transform() - xform.translateXBy_yBy_(0, height) - xform.scaleXBy_yBy_(1.0, -1.0) - xform.concat() - -class DrawingStyle(object): - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - def __init__(self, bg_color=None, text_color=None): - self.use_custom_style = True - if text_color is None: - self.text_color = self.default_text_color - else: - self.text_color = convert_cocoa_color(text_color) - if bg_color is None: - self.bg_color = self.default_bg_color - else: - self.bg_color = convert_cocoa_color(bg_color) - - default_text_color = convert_cocoa_color(NSColor.textColor()) - default_bg_color = convert_cocoa_color(NSColor.textBackgroundColor()) - -class DrawingContext: - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - def __init__(self, view, drawing_area, rect): - self.view = view - self.path = NSBezierPath.bezierPath() - self.color = NSColor.blackColor() - self.width = drawing_area.size.width - self.height = drawing_area.size.height - self.origin = drawing_area.origin - if drawing_area.origin != NSZeroPoint: - xform = NSAffineTransform.transform() - xform.translateXBy_yBy_(drawing_area.origin.x, - drawing_area.origin.y) - xform.concat() - - def move_to(self, x, y): - self.path.moveToPoint_(NSPoint(x, y)) - - def rel_move_to(self, dx, dy): - self.path.relativeMoveToPoint_(NSPoint(dx, dy)) - - def line_to(self, x, y): - self.path.lineToPoint_(NSPoint(x, y)) - - def rel_line_to(self, dx, dy): - self.path.relativeLineToPoint_(NSPoint(dx, dy)) - - def curve_to(self, x1, y1, x2, y2, x3, y3): - self.path.curveToPoint_controlPoint1_controlPoint2_( - NSPoint(x3, y3), NSPoint(x1, y1), NSPoint(x2, y2)) - - def rel_curve_to(self, dx1, dy1, dx2, dy2, dx3, dy3): - self.path.relativeCurveToPoint_controlPoint1_controlPoint2_( - NSPoint(dx3, dy3), NSPoint(dx1, dy1), NSPoint(dx2, dy2)) - - def arc(self, x, y, radius, angle1, angle2): - angle1 = (angle1 * 360) / (2 * math.pi) - angle2 = (angle2 * 360) / (2 * math.pi) - center = NSPoint(x, y) - self.path.appendBezierPathWithArcWithCenter_radius_startAngle_endAngle_(center, radius, angle1, angle2) - - def arc_negative(self, x, y, radius, angle1, angle2): - angle1 = (angle1 * 360) / (2 * math.pi) - angle2 = (angle2 * 360) / (2 * math.pi) - center = NSPoint(x, y) - self.path.appendBezierPathWithArcWithCenter_radius_startAngle_endAngle_clockwise_(center, radius, angle1, angle2, YES) - - def rectangle(self, x, y, width, height): - rect = NSMakeRect(x, y, width, height) - self.path.appendBezierPathWithRect_(rect) - - def set_color(self, color, alpha=1.0): - self.color = convert_widget_color(color, alpha) - self.color.set() - - def set_shadow(self, color, opacity, offset, blur_radius): - shadow = NSShadow.alloc().init() - # shadow offset is always in the cocoa coordinates, so we need to - # reverse the y part - shadow.setShadowOffset_(NSPoint(offset[0], -offset[1])) - shadow.setShadowBlurRadius_(blur_radius) - shadow.setShadowColor_(convert_widget_color(color, opacity)) - shadow.set() - - def set_line_width(self, width): - self.path.setLineWidth_(width) - - def stroke(self): - self.path.stroke() - self.path.removeAllPoints() - - def stroke_preserve(self): - self.path.stroke() - - def fill(self): - self.path.fill() - self.path.removeAllPoints() - - def fill_preserve(self): - self.path.fill() - - def clip(self): - self.path.addClip() - self.path.removeAllPoints() - - def save(self): - NSGraphicsContext.currentContext().saveGraphicsState() - - def restore(self): - NSGraphicsContext.currentContext().restoreGraphicsState() - - def gradient_fill(self, gradient): - self.gradient_fill_preserve(gradient) - self.path.removeAllPoints() - - def gradient_fill_preserve(self, gradient): - context = NSGraphicsContext.currentContext() - context.saveGraphicsState() - self.path.addClip() - gradient.draw() - context.restoreGraphicsState() - -class Gradient(object): - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - def __init__(self, x1, y1, x2, y2): - self.x1, self.y1, self.x2, self.y2 = x1, y1, x2, y2 - self.start_color = None - self.end_color = None - - def set_start_color(self, (red, green, blue)): - self.start_color = (red, green, blue) - - def set_end_color(self, (red, green, blue)): - self.end_color = (red, green, blue) - - def draw(self): - start_color = convert_widget_color(self.start_color) - end_color = convert_widget_color(self.end_color) - nsgradient = NSGradient.alloc().initWithStartingColor_endingColor_(start_color, end_color) - start_point = NSPoint(self.x1, self.y1) - end_point = NSPoint(self.x2, self.y2) - nsgradient.drawFromPoint_toPoint_options_(start_point, end_point, 0) - -class DrawingMixin(object): - def calc_size_request(self): - return self.size_request(self.view.layout_manager) - - # squish width / squish height only make sense on GTK - def set_squish_width(self, setting): - pass - - def set_squish_height(self, setting): - pass - - # Default implementations for methods that subclasses override. - - def is_opaque(self): - return False - - def size_request(self, layout_manager): - return 0, 0 - - def draw(self, context, layout_manager): - pass - - def viewport_repositioned(self): - # since this is a Mixin class, we want to make sure that our other - # classes see the viewport_repositioned() call. - super(DrawingMixin, self).viewport_repositioned() - self.queue_redraw() diff --git a/mvc/widgets/osx/drawingwidgets.py b/mvc/widgets/osx/drawingwidgets.py deleted file mode 100644 index 74e8232..0000000 --- a/mvc/widgets/osx/drawingwidgets.py +++ /dev/null @@ -1,67 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""drawingviews.py -- views that support custom drawing.""" - -import wrappermap -import drawing -from .base import Widget, SimpleBin, FlippedView -from .layoutmanager import LayoutManager - -class DrawingView(FlippedView): - def init(self): - self = super(DrawingView, self).init() - self.layout_manager = LayoutManager() - return self - - def isOpaque(self): - return wrappermap.wrapper(self).is_opaque() - - def drawRect_(self, rect): - context = drawing.DrawingContext(self, self.bounds(), rect) - context.style = drawing.DrawingStyle() - wrappermap.wrapper(self).draw(context, self.layout_manager) - -class DrawingArea(drawing.DrawingMixin, Widget): - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - def __init__(self): - Widget.__init__(self) - self.view = DrawingView.alloc().init() - -class Background(drawing.DrawingMixin, SimpleBin): - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - def __init__(self): - SimpleBin.__init__(self) - self.view = DrawingView.alloc().init() - - def calc_size_request(self): - drawing_size = drawing.DrawingMixin.calc_size_request(self) - container_size = SimpleBin.calc_size_request(self) - return (max(container_size[0], drawing_size[0]), - max(container_size[1], drawing_size[1])) diff --git a/mvc/widgets/osx/fasttypes.c b/mvc/widgets/osx/fasttypes.c deleted file mode 100644 index 72d3b5b..0000000 --- a/mvc/widgets/osx/fasttypes.c +++ /dev/null @@ -1,540 +0,0 @@ -/* -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - */ - -#include <Python.h> - -/* - * fasttypes.c - * - * Datastructures written in C to be fast. This used to be a big C++ file - * that depended on boost. Nowadays we only define LinkedList, which is easy - * enough to implement in pure C. - */ - -static int nodes_deleted = 0; // debugging only - -/* forward define python type objects */ - -static PyTypeObject LinkedListType; -static PyTypeObject LinkedListIterType; - -/* Structure definitions */ - -typedef struct LinkedListNode { - PyObject *obj; - struct LinkedListNode* next; - struct LinkedListNode* prev; - int deleted; // Has this node been removed? - int iter_count; // How many LinkedListIters point to this node? -} LinkedListNode; - -typedef struct { - PyObject_HEAD - int count; - LinkedListNode* sentinal; - // sentinal object to make list operations simpler/faster and equivalent - // to the boost API. It's prev node is the last element in the list and - // it's next node is the first -} LinkedListObject; - -typedef struct { - PyObject_HEAD - LinkedListNode* node; - LinkedListObject* list; -} LinkedListIterObject; - -/* LinkedListNode */ - -void check_node_deleted(LinkedListNode* node) -{ - if(node->iter_count <= 0 && node->deleted) { - free(node); - nodes_deleted += 1; - } -} - -static int remove_node(LinkedListObject* self, LinkedListNode* node) -{ - if(node->obj == NULL) { - PyErr_SetString(PyExc_IndexError, "can't remove lastIter()"); - return 0; - } - node->next->prev = node->prev; - node->prev->next = node->next; - node->deleted = 1; - self->count -= 1; - Py_DECREF(node->obj); - check_node_deleted(node); - return 1; -} - -/* LinkedListIter */ - -void switch_node(LinkedListIterObject* self, LinkedListNode* new_node) -{ - LinkedListNode* old_node; - - old_node = self->node; - self->node = new_node; - old_node->iter_count--; - self->node->iter_count++; - check_node_deleted(old_node); -} - -// Note that we don't expose the new method to python. We create -// LinkedListIters in the factory methods firstIter() and lastIter() -static LinkedListIterObject* LinkedListIterObject_new(LinkedListObject*list, - LinkedListNode* node) -{ - LinkedListIterObject* self; - - self = (LinkedListIterObject*)PyType_GenericAlloc(&LinkedListIterType, 0); - if(self != NULL) { - self->node = node; - self->list = list; - node->iter_count++; - } - return self; -} - -static void LinkedListIterObject_dealloc(LinkedListIterObject* self) -{ - self->node->iter_count--; - check_node_deleted(self->node); -} - -static PyObject *LinkedListIter_forward(LinkedListIterObject* self, PyObject *obj) -{ - switch_node(self, self->node->next); - Py_RETURN_NONE; -} - -static PyObject *LinkedListIter_back(LinkedListIterObject* self, PyObject *obj) -{ - switch_node(self, self->node->prev); - Py_RETURN_NONE; -} - -static PyObject *LinkedListIter_value(LinkedListIterObject* self, PyObject *obj) -{ - PyObject* retval; - - if(self->node->deleted) { - PyErr_SetString(PyExc_ValueError, "Node deleted"); - return NULL; - } - retval = self->node->obj; - if(retval == NULL) { - PyErr_SetString(PyExc_IndexError, "can't get value of lastIter()"); - return NULL; - } - Py_INCREF(retval); - return retval; -} - -static PyObject *LinkedListIter_copy(LinkedListIterObject* self, PyObject *obj) -{ - return (PyObject*)LinkedListIterObject_new(self->list, self->node); -} - -static PyObject *LinkedListIter_valid(LinkedListIterObject* self, PyObject *obj) -{ - return PyBool_FromLong(self->node->deleted == 0); -} - -PyObject* LinkedListIter_richcmp(LinkedListIterObject *o1, - LinkedListIterObject *o2, int opid) -{ - if(!PyObject_TypeCheck(o1, &LinkedListIterType) || - !PyObject_TypeCheck(o2, &LinkedListIterType)) { - return Py_NotImplemented; - } - switch(opid) { - case Py_EQ: - if(o1->node == o2->node) Py_RETURN_TRUE; - else Py_RETURN_FALSE; - case Py_NE: - if(o1->node != o2->node) Py_RETURN_TRUE; - else Py_RETURN_FALSE; - default: - return Py_NotImplemented; - } -} - -static PyMethodDef LinkedListIter_methods[] = { - {"forward", (PyCFunction)LinkedListIter_forward, METH_NOARGS, - "Move to the next element", - }, - {"back", (PyCFunction)LinkedListIter_back, METH_NOARGS, - "Move to the previous element", - }, - {"value", (PyCFunction)LinkedListIter_value, METH_NOARGS, - "Return the current element", - }, - {"copy", (PyCFunction)LinkedListIter_copy, METH_NOARGS, - "Duplicate iter", - }, - {"valid", (PyCFunction)LinkedListIter_valid, METH_NOARGS, - "Test if the iter is valid", - }, - {NULL}, -}; - -static PyTypeObject LinkedListIterType = { - PyObject_HEAD_INIT(NULL) - 0, /* ob_size */ - "fasttypes.LinkedListIter", /* tp_name */ - sizeof(LinkedListIterObject), /* tp_basicsize */ - 0, /* tp_itemsize */ - (destructor)LinkedListIterObject_dealloc, /* tp_dealloc */ - 0, /* tp_print */ - 0, /* tp_getattr */ - 0, /* tp_setattr */ - 0, /* tp_compare */ - 0, /* tp_repr */ - 0, /* tp_as_number */ - 0, /* tp_as_sequence */ - 0, /* tp_as_mapping */ - 0, /* tp_hash */ - 0, /* tp_call */ - 0, /* tp_str */ - 0, /* tp_getattro */ - 0, /* tp_setattro */ - 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT|Py_TPFLAGS_HAVE_RICHCOMPARE, /* tp_flags */ - "fasttypes LinkedListIter", /* tp_doc */ - 0, /* tp_traverse */ - 0, /* tp_clear */ - (richcmpfunc)LinkedListIter_richcmp, /* tp_richcompare */ - 0, /* tp_weaklistoffset */ - 0, /* tp_iter */ - 0, /* tp_iternext */ - LinkedListIter_methods, /* tp_methods */ - 0, /* tp_members */ - 0, /* tp_getset */ - 0, /* tp_base */ - 0, /* tp_dict */ - 0, /* tp_descr_get */ - 0, /* tp_descr_set */ - 0, /* tp_dictoffset */ - 0, /* tp_init */ - 0, /* tp_alloc */ - 0, /* tp_new */ -}; - -/* LinkedList */ - -LinkedListNode* make_new_node(PyObject* obj, LinkedListNode* prev, - LinkedListNode* next) -{ - LinkedListNode* retval; - retval = malloc(sizeof(LinkedListNode)); - if(!retval) { - PyErr_SetString(PyExc_MemoryError, "can't create new node"); - return NULL; - } - Py_XINCREF(obj); - retval->obj = obj; - retval->prev = prev; - retval->next = next; - retval->iter_count = retval->deleted = 0; - return retval; -} - -void set_iter_type_error(PyObject* obj) -{ - // Set an exception when we expected a LinkedListIter and got something - // else - PyObject* args; - PyObject* fmt; - PyObject* err_str; - - args = Py_BuildValue("(O)", obj); - fmt = PyString_FromString("Expected LinkedListIter, got %r"); - err_str = PyString_Format(fmt, args); - PyErr_SetObject(PyExc_TypeError, err_str); - Py_DECREF(fmt); - Py_DECREF(err_str); - Py_DECREF(args); -} - -static PyObject* insert_before(LinkedListObject* self, LinkedListNode* node, - PyObject* obj) -{ - LinkedListNode* new_node; - PyObject* retval; - - new_node = make_new_node(obj, node->prev, node); - if(!new_node) return NULL; - node->prev->next = new_node; - node->prev = new_node; - self->count += 1; - retval = (PyObject*)LinkedListIterObject_new(self, new_node); - return retval; -} - -static PyObject* LinkedList_new(PyTypeObject *type, PyObject *args, PyObject *kwds) -{ - LinkedListObject *self; - LinkedListNode *sentinal; - - self = (LinkedListObject *)type->tp_alloc(type, 0); - if (self == NULL) return NULL; - - sentinal = make_new_node(NULL, NULL, NULL); - if(!sentinal) { - Py_DECREF(self); - return NULL; - } - self->sentinal = sentinal->next = sentinal->prev = sentinal; - sentinal->iter_count = 1; // prevent the sentinal from being deleted - self->count = 0; - - return (PyObject *)self; -} - -static void LinkedList_dealloc(LinkedListObject* self) -{ - LinkedListNode *node, *tmp; - - node = self->sentinal->next; - while(node != self->sentinal) { - node->deleted = 1; - tmp = node->next; - check_node_deleted(node); - node = tmp; - } - - self->sentinal->iter_count -= 1; - check_node_deleted(self->sentinal); - return; -} - -static int LinkedList_init(LinkedListObject *self) -{ - self->count = 0; - return 0; -} - -static Py_ssize_t LinkedList_len(LinkedListObject *self) -{ - return self->count; -} - -static PyObject* LinkedList_get(LinkedListObject *self, - LinkedListIterObject *iter) -{ - if(!PyObject_TypeCheck(iter, &LinkedListIterType)) { - set_iter_type_error((PyObject*)iter); - return NULL; - } - return PyObject_CallMethod((PyObject*)iter, "value", "()"); -} -int LinkedList_set(LinkedListObject *self, LinkedListIterObject *iter, - PyObject *value) -{ - if(!PyObject_TypeCheck(iter, &LinkedListIterType)) { - set_iter_type_error((PyObject*)iter); - return -1; - } - if(iter->node->deleted) { - PyErr_SetString(PyExc_ValueError, "Node deleted"); - return -1; - } - if(iter->node->obj == NULL) { - PyErr_SetString(PyExc_IndexError, "can't set value of lastIter()"); - return -1; - } - if(value == NULL) { - if(!remove_node(self, iter->node)) return -1; - return 0; - } - Py_INCREF(value); - Py_DECREF(iter->node->obj); - iter->node->obj = value; - return 0; -} - -static PyObject *LinkedList_insertBefore(LinkedListObject* self, PyObject *args) -{ - LinkedListIterObject *iter; - PyObject *obj; - - if(!PyArg_ParseTuple(args, "OO", &iter, &obj)) return NULL; - if(!PyObject_TypeCheck(iter, &LinkedListIterType)) { - set_iter_type_error(obj); - return NULL; - } - - return insert_before(self, iter->node, obj); -} - -static PyObject *LinkedList_append(LinkedListObject* self, PyObject *obj) -{ - return insert_before(self, self->sentinal, obj); -} - -static PyObject *LinkedList_remove(LinkedListObject* self, - LinkedListIterObject *iter) -{ - LinkedListNode* next_node; - if(!PyObject_TypeCheck(iter, &LinkedListIterType)) { - set_iter_type_error((PyObject*)iter); - return NULL; - } - - next_node = iter->node->next; - if(!remove_node(self, iter->node)) return NULL; - return (PyObject*)LinkedListIterObject_new(self, next_node); -} - -static PyObject *LinkedList_firstIter(LinkedListObject* self, PyObject *obj) -{ - PyObject* retval; - retval = (PyObject*)LinkedListIterObject_new(self, self->sentinal->next); - return retval; -} - -static PyObject *LinkedList_lastIter(LinkedListObject* self, PyObject *obj) -{ - PyObject* retval; - retval = (PyObject*)LinkedListIterObject_new(self, self->sentinal); - return retval; -} - -static PyMappingMethods LinkedListMappingMethods = { - (lenfunc)LinkedList_len, - (binaryfunc)LinkedList_get, - (objobjargproc)LinkedList_set, -}; - -static PyMethodDef LinkedList_methods[] = { - {"insertBefore", (PyCFunction)LinkedList_insertBefore, METH_VARARGS, - "insert an element before iter", - }, - {"append", (PyCFunction)LinkedList_append, METH_O, - "append an element to the list", - }, - {"remove", (PyCFunction)LinkedList_remove, METH_O, - "remove an element to the list", - }, - {"firstIter", (PyCFunction)LinkedList_firstIter, METH_NOARGS, - "get an iter pointing to the first element in the list", - }, - {"lastIter", (PyCFunction)LinkedList_lastIter, METH_NOARGS, - "get an iter pointing to the last element in the list", - }, - {NULL}, -}; - -static PyTypeObject LinkedListType = { - PyObject_HEAD_INIT(NULL) - 0, /* ob_size */ - "fasttypes.LinkedList", /* tp_name */ - sizeof(LinkedListObject), /* tp_basicsize */ - 0, /* tp_itemsize */ - (destructor)LinkedList_dealloc, /* tp_dealloc */ - 0, /* tp_print */ - 0, /* tp_getattr */ - 0, /* tp_setattr */ - 0, /* tp_compare */ - 0, /* tp_repr */ - 0, /* tp_as_number */ - 0, /* tp_as_sequence */ - &LinkedListMappingMethods, /* tp_as_mapping */ - 0, /* tp_hash */ - 0, /* tp_call */ - 0, /* tp_str */ - 0, /* tp_getattro */ - 0, /* tp_setattro */ - 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT, /* tp_flags */ - "fasttypes LinkedList", /* tp_doc */ - 0, /* tp_traverse */ - 0, /* tp_clear */ - 0, /* tp_richcompare */ - 0, /* tp_weaklistoffset */ - 0, /* tp_iter */ - 0, /* tp_iternext */ - LinkedList_methods, /* tp_methods */ - 0, /* tp_members */ - 0, /* tp_getset */ - 0, /* tp_base */ - 0, /* tp_dict */ - 0, /* tp_descr_get */ - 0, /* tp_descr_set */ - 0, /* tp_dictoffset */ - (initproc)LinkedList_init, /* tp_init */ - 0, /* tp_alloc */ - LinkedList_new, /* tp_new */ -}; - -/* Module-level stuff */ - -static PyObject *count_nodes_deleted(PyObject *obj) -{ - return PyInt_FromLong(nodes_deleted); -} - -static PyObject *reset_nodes_deleted(PyObject *obj) -{ - nodes_deleted = 0; - Py_RETURN_NONE; -} - - -static PyMethodDef FasttypesMethods[] = -{ - {"_count_nodes_deleted", (PyCFunction)count_nodes_deleted, METH_NOARGS, - "get a count of how many nodes have been deleted (DEBUGGING ONLY)", - }, - {"_reset_nodes_deleted", (PyCFunction)reset_nodes_deleted, METH_NOARGS, - "reset the count of how many nodes have been deleted (DEBUGGING ONLY)", - }, - { NULL, NULL, 0, NULL } -}; - -PyMODINIT_FUNC initfasttypes(void) -{ - PyObject *m; - - if (PyType_Ready(&LinkedListType) < 0) - return; - - if (PyType_Ready(&LinkedListIterType) < 0) - return; - - m = Py_InitModule("fasttypes", FasttypesMethods); - - Py_INCREF(&LinkedListType); - Py_INCREF(&LinkedListIterType); - PyModule_AddObject(m, "LinkedList", (PyObject *)&LinkedListType); -} diff --git a/mvc/widgets/osx/helpers.py b/mvc/widgets/osx/helpers.py deleted file mode 100644 index e4aa23a..0000000 --- a/mvc/widgets/osx/helpers.py +++ /dev/null @@ -1,95 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""helper classes.""" - -import logging -import traceback - -from Foundation import * -from objc import nil - -class NotificationForwarder(NSObject): - """Forward notifications from a Cocoa object to a python class. - """ - - def initWithNSObject_center_(self, nsobject, center): - """Initialize the NotificationForwarder nsobject is the NSObject to - forward notifications for. It can be nil in which case notifications - from all objects will be forwarded. - - center is the NSNotificationCenter to get notifications from. It can - be None, in which cas the default notification center is used. - """ - self.nsobject = nsobject - self.callback_map = {} - if center is None: - self.center = NSNotificationCenter.defaultCenter() - else: - self.center = center - return self - - @classmethod - def create(cls, object, center=None): - """Helper method to call aloc() then initWithNSObject_center_().""" - return cls.alloc().initWithNSObject_center_(object, center) - - def connect(self, callback, name): - """Register to listen for notifications. - Only one callback for each notification name can be connected. - """ - - if name in self.callback_map: - raise ValueError("%s already connected" % name) - - self.callback_map[name] = callback - self.center.addObserver_selector_name_object_(self, 'observe:', name, - self.nsobject) - - def disconnect(self, name=None): - if name is not None: - self.center.removeObserver_name_object_(self, name, self.nsobject) - self.callback_map.pop(name) - else: - self.center.removeObserver_(self) - self.callback_map.clear() - - def observe_(self, notification): - name = notification.name() - callback = self.callback_map[name] - if callback is None: - logging.warn("Callback for %s is dead", name) - self.center.removeObverser_name_object_(self, name, self.nsobject) - return - try: - callback(notification) - except: - logging.warn("Callback for %s raised exception:%s\n", - name.encode('utf-8'), - traceback.format_exc()) diff --git a/mvc/widgets/osx/layout.py b/mvc/widgets/osx/layout.py deleted file mode 100644 index 0238975..0000000 --- a/mvc/widgets/osx/layout.py +++ /dev/null @@ -1,748 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -""".layout -- Widgets that handle laying out other -widgets. - -We basically follow GTK's packing model. Widgets are packed into vboxes, -hboxes or other container widgets. The child widgets request a minimum size, -and the container widgets allocate space for their children. Widgets may get -more size then they requested in which case they have to deal with it. In -rare cases, widgets may get less size then they requested in which case they -should just make sure they don't throw an exception or segfault. - -Check out the GTK tutorial for more info. -""" - -import itertools - -from AppKit import * -from Foundation import * -from objc import YES, NO, nil, signature, loadBundle - -import tableview -import wrappermap -from .base import Container, Bin, FlippedView -from mvc.utils import Matrix - -# These don't seem to be in pyobjc's AppKit (yet) -NSScrollerKnobStyleDefault = 0 -NSScrollerKnobStyleDark = 1 -NSScrollerKnobStyleLight = 2 - -NSScrollerStyleLegacy = 0 -NSScrollerStyleOverlay = 1 - -def _extra_space_iter(extra_length, count): - """Utility function to allocate extra space left over in containers.""" - if count == 0: - return - extra_space, leftover = divmod(extra_length, count) - while leftover >= 1: - yield extra_space + 1 - leftover -= 1 - yield extra_space + leftover - while True: - yield extra_space - -class BoxPacking: - """Utility class to store how we are packing a single widget.""" - - def __init__(self, widget, expand, padding): - self.widget = widget - self.expand = expand - self.padding = padding - -class Box(Container): - """Base class for HBox and VBox. """ - CREATES_VIEW = False - - def __init__(self, spacing=0): - self.spacing = spacing - Container.__init__(self) - self.packing_start = [] - self.packing_end = [] - self.expand_count = 0 - - def packing_both(self): - return itertools.chain(self.packing_start, self.packing_end) - - def get_children(self): - for packing in self.packing_both(): - yield packing.widget - children = property(get_children) - - # Internally Boxes use a (length, breadth) coordinate system. length and - # breadth will be either x or y depending on which way the box is - # oriented. The subclasses must provide methods to translate between the - # 2 coordinate systems. - - def translate_size(self, size): - """Translate a (width, height) tulple to (length, breadth).""" - raise NotImplementedError() - - def untranslate_size(self, size): - """Reverse the work of translate_size.""" - raise NotImplementedError() - - def make_child_rect(self, position, length): - """Create a rect to position a child with.""" - raise NotImplementedError() - - def pack_start(self, child, expand=False, padding=0): - self.packing_start.append(BoxPacking(child, expand, padding)) - if expand: - self.expand_count += 1 - self.child_added(child) - - def pack_end(self, child, expand=False, padding=0): - self.packing_end.append(BoxPacking(child, expand, padding)) - if expand: - self.expand_count += 1 - self.child_added(child) - - def _remove_from_packing(self, child): - for i in xrange(len(self.packing_start)): - if self.packing_start[i].widget is child: - return self.packing_start.pop(i) - for i in xrange(len(self.packing_end)): - if self.packing_end[i].widget is child: - return self.packing_end.pop(i) - raise LookupError("%s not found" % child) - - def remove(self, child): - packing = self._remove_from_packing(child) - if packing.expand: - self.expand_count -= 1 - self.child_removed(child) - - def translate_widget_size(self, widget): - return self.translate_size(widget.get_size_request()) - - def calc_size_request(self): - length = breadth = 0 - for packing in self.packing_both(): - child_length, child_breadth = \ - self.translate_widget_size(packing.widget) - length += child_length - if packing.padding: - length += packing.padding * 2 # Need to pad on both sides - breadth = max(breadth, child_breadth) - spaces = max(0, len(self.packing_start) + len(self.packing_end) - 1) - length += spaces * self.spacing - return self.untranslate_size((length, breadth)) - - def place_children(self): - request_length, request_breadth = self.translate_widget_size(self) - ps = self.viewport.placement.size - total_length, dummy = self.translate_size((ps.width, ps.height)) - total_extra_space = total_length - request_length - extra_space_iter = _extra_space_iter(total_extra_space, - self.expand_count) - start_end = self._place_packing_list(self.packing_start, - extra_space_iter, 0) - if self.expand_count == 0 and total_extra_space > 0: - # account for empty space after the end of pack_start list and - # before the pack_end list. - self.draw_empty_space(start_end, total_extra_space) - start_end += total_extra_space - self._place_packing_list(reversed(self.packing_end), extra_space_iter, - start_end) - - def draw_empty_space(self, start, length): - empty_rect = self.make_child_rect(start, length) - my_view = self.viewport.view - opaque_view = my_view.opaqueAncestor() - if opaque_view is not None: - empty_rect2 = opaque_view.convertRect_fromView_(empty_rect, my_view) - opaque_view.setNeedsDisplayInRect_(empty_rect2) - - def _place_packing_list(self, packing_list, extra_space_iter, position): - for packing in packing_list: - child_length, child_breadth = \ - self.translate_widget_size(packing.widget) - if packing.expand: - child_length += extra_space_iter.next() - if packing.padding: # space before - self.draw_empty_space(position, packing.padding) - position += packing.padding - child_rect = self.make_child_rect(position, child_length) - if packing.padding: # space after - self.draw_empty_space(position, packing.padding) - position += packing.padding - packing.widget.place(child_rect, self.viewport.view) - position += child_length - if self.spacing > 0: - self.draw_empty_space(position, self.spacing) - position += self.spacing - return position - - def enable(self): - Container.enable(self) - for mem in self.children: - mem.enable() - - def disable(self): - Container.disable(self) - for mem in self.children: - mem.disable() - -class VBox(Box): - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - def translate_size(self, size): - return (size[1], size[0]) - - def untranslate_size(self, size): - return (size[1], size[0]) - - def make_child_rect(self, position, length): - placement = self.viewport.placement - return NSMakeRect(placement.origin.x, placement.origin.y + position, - placement.size.width, length) - -class HBox(Box): - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - def translate_size(self, size): - return (size[0], size[1]) - - def untranslate_size(self, size): - return (size[0], size[1]) - - def make_child_rect(self, position, length): - placement = self.viewport.placement - return NSMakeRect(placement.origin.x + position, placement.origin.y, - length, placement.size.height) - -class Alignment(Bin): - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - CREATES_VIEW = False - - def __init__(self, xalign=0.0, yalign=0.0, xscale=0.0, yscale=0.0, - top_pad=0, bottom_pad=0, left_pad=0, right_pad=0): - Bin.__init__(self) - self.xalign = xalign - self.yalign = yalign - self.xscale = xscale - self.yscale = yscale - self.top_pad = top_pad - self.bottom_pad = bottom_pad - self.left_pad = left_pad - self.right_pad = right_pad - if self.child is not None: - self.place_children() - - def set(self, xalign=0.0, yalign=0.0, xscale=0.0, yscale=0.0): - self.xalign = xalign - self.yalign = yalign - self.xscale = xscale - self.yscale = yscale - if self.child is not None: - self.place_children() - - def set_padding(self, top_pad=0, bottom_pad=0, left_pad=0, right_pad=0): - self.top_pad = top_pad - self.bottom_pad = bottom_pad - self.left_pad = left_pad - self.right_pad = right_pad - if self.child is not None and self.viewport is not None: - self.place_children() - - def vertical_pad(self): - return self.top_pad + self.bottom_pad - - def horizontal_pad(self): - return self.left_pad + self.right_pad - - def calc_size_request(self): - if self.child: - child_width, child_height = self.child.get_size_request() - return (child_width + self.horizontal_pad(), - child_height + self.vertical_pad()) - else: - return (0, 0) - - def calc_size(self, requested, total, scale): - extra_width = max(0, total - requested) - return requested + int(round(extra_width * scale)) - - def calc_position(self, size, total, align): - return int(round((total - size) * align)) - - def place_children(self): - if self.child is None: - return - - total_width = self.viewport.placement.size.width - total_height = self.viewport.placement.size.height - total_width -= self.horizontal_pad() - total_height -= self.vertical_pad() - request_width, request_height = self.child.get_size_request() - - child_width = self.calc_size(request_width, total_width, self.xscale) - child_height = self.calc_size(request_height, total_height, self.yscale) - child_x = self.calc_position(child_width, total_width, self.xalign) - child_y = self.calc_position(child_height, total_height, self.yalign) - child_x += self.left_pad - child_y += self.top_pad - - my_origin = self.viewport.area().origin - child_rect = NSMakeRect(my_origin.x + child_x, my_origin.y + child_y, child_width, child_height) - self.child.place(child_rect, self.viewport.view) - # Make sure the space not taken up by our child is redrawn. - self.viewport.queue_redraw() - -class DetachedWindowHolder(Alignment): - def __init__(self): - Alignment.__init__(self, bottom_pad=16, xscale=1.0, yscale=1.0) - -class _TablePacking(object): - """Utility class to help with packing Table widgets.""" - def __init__(self, widget, column, row, column_span, row_span): - self.widget = widget - self.column = column - self.row = row - self.column_span = column_span - self.row_span = row_span - - def column_indexes(self): - return range(self.column, self.column + self.column_span) - - def row_indexes(self): - return range(self.row, self.row + self.row_span) - -class Table(Container): - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - CREATES_VIEW = False - - def __init__(self, columns, rows): - Container.__init__(self) - self._cells = Matrix(columns, rows) - self._children = [] # List of _TablePacking objects - self._children_sorted = True - self.rows = rows - self.columns = columns - self.row_spacing = self.column_spacing = 0 - - def _ensure_children_sorted(self): - if not self._children_sorted: - def cell_area(table_packing): - return table_packing.column_span * table_packing.row_span - self._children.sort(key=cell_area) - self._children_sorted = True - - def get_children(self): - return [cell.widget for cell in self._children] - children = property(get_children) - - def calc_size_request(self): - self._ensure_children_sorted() - self._calc_dimensions() - return self.total_width, self.total_height - - def _calc_dimensions(self): - self.column_widths = [0] * self.columns - self.row_heights = [0] * self.rows - - for tp in self._children: - child_width, child_height = tp.widget.get_size_request() - # recalc the width of the child's columns - self._recalc_dimension(child_width, self.column_widths, - tp.column_indexes()) - # recalc the height of the child's rows - self._recalc_dimension(child_height, self.row_heights, - tp.row_indexes()) - - self.total_width = (self.column_spacing * (self.columns - 1) + - sum(self.column_widths)) - self.total_height = (self.row_spacing * (self.rows - 1) + - sum(self.row_heights)) - - def _recalc_dimension(self, child_size, size_array, positions): - current_size = sum(size_array[p] for p in positions) - child_size_needed = child_size - current_size - if child_size_needed > 0: - iter = _extra_space_iter(child_size_needed, len(positions)) - for p in positions: - size_array[p] += iter.next() - - def place_children(self): - # This method depepnds on us calling _calc_dimensions() in - # calc_size_request(). Ensure that this happens. - if self.cached_size_request is None: - self.get_size_request() - column_positions = [0] - for width in self.column_widths[:-1]: - column_positions.append(width + column_positions[-1] + self.column_spacing) - row_positions = [0] - for height in self.row_heights[:-1]: - row_positions.append(height + row_positions[-1] + self.row_spacing) - - my_x= self.viewport.placement.origin.x - my_y = self.viewport.placement.origin.y - for tp in self._children: - x = my_x + column_positions[tp.column] - y = my_y + row_positions[tp.row] - width = sum(self.column_widths[i] for i in tp.column_indexes()) - height = sum(self.row_heights[i] for i in tp.row_indexes()) - rect = NSMakeRect(x, y, width, height) - tp.widget.place(rect, self.viewport.view) - - def pack(self, widget, column, row, column_span=1, row_span=1): - tp = _TablePacking(widget, column, row, column_span, row_span) - for c in tp.column_indexes(): - for r in tp.row_indexes(): - if self._cells[c, r]: - raise ValueError("Cell %d x %d is already taken" % (c, r)) - self._cells[column, row] = widget - self._children.append(tp) - self._children_sorted = False - self.child_added(widget) - - def remove(self, child): - for i in xrange(len(self._children)): - if self._children[i].widget is child: - self._children.remove(i) - break - else: - raise ValueError("%s is not a child of this Table" % child) - self._cells.remove(child) - self.child_removed(widget) - - def set_column_spacing(self, spacing): - self.column_spacing = spacing - self.invalidate_size_request() - - def set_row_spacing(self, spacing): - self.row_spacing = spacing - self.invalidate_size_request() - - def enable(self, row=None, column=None): - Container.enable(self) - if row != None and column != None: - if self._cells[column, row]: - self._cells[column, row].enable() - elif row != None: - for mem in self._cells.row(row): - if mem: mem.enable() - elif column != None: - for mem in self._cells.column(column): - if mem: mem.enable() - else: - for mem in self._cells: - if mem: mem.enable() - - def disable(self, row=None, column=None): - Container.disable(self) - if row != None and column != None: - if self._cells[column, row]: - self._cells[column, row].disable() - elif row != None: - for mem in self._cells.row(row): - if mem: mem.disable() - elif column != None: - for mem in self._cells.column(column): - if mem: mem.disable() - else: - for mem in self._cells: - if mem: mem.disable() - -class MiroScrollView(NSScrollView): - def tile(self): - NSScrollView.tile(self) - # tile is called when we need to layout our child view and scrollers. - # This probably means that we've either hidden or shown a scrollbar so - # call invalidate_size_request to ensure that things get re-layed out - # correctly. (#see 13842) - wrapper = wrappermap.wrapper(self) - if wrapper is not None: - wrapper.invalidate_size_request() - -class Scroller(Bin): - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - def __init__(self, horizontal, vertical): - Bin.__init__(self) - self.view = MiroScrollView.alloc().init() - self.view.setAutohidesScrollers_(YES) - self.view.setHasHorizontalScroller_(horizontal) - self.view.setHasVerticalScroller_(vertical) - self.document_view = FlippedView.alloc().init() - self.view.setDocumentView_(self.document_view) - - def prepare_for_dark_content(self): - try: - self.view.setScrollerKnobStyle_(NSScrollerKnobStyleLight) - except AttributeError: - # This only works on 10.7 and abvoe - pass - - def set_has_borders(self, has_border): - self.view.setBorderType_(NSBezelBorder) - - def viewport_repositioned(self): - # If the window is resized, this translates to a - # viewport_repositioned() event. Instead of calling - # place_children() one, which is what our suporclass does, we need - # some extra logic here. place the chilren to work out if we need a - # scrollbar, then get the new size, then replace the children (which - # now takes into account of scrollbar size.) - super(Scroller, self).viewport_repositioned() - self.cached_size_request = self.calc_size_request() - self.place_children() - - def set_background_color(self, color): - self.view.setBackgroundColor_(self.make_color(color)) - - def add(self, child): - child.parent_is_scroller = True - Bin.add(self, child) - - def remove(self): - child.parent_is_scroller = False - Bin.remove(self) - - def children_changed(self): - # since our size isn't dependent on our children, don't call - # invalidate_size_request() here. Just call place_children() so that - # they get positioned correctly in the document view. - # - # XXX dodgy - why are we laying out the children twice? When the - # children change, the scroller could appear/disappear. But you have - # no idea if that's going to happen without knowing how big your - # children are. So we lay it out, get the size, then, place the - # children again. This makes sure that the right side of the children - # are redrawn. There's got to be a better way?? - self.place_children() - self.cached_size_request = self.calc_size_request() - self.place_children() - - def calc_size_request(self): - if self.child: - width = height = 0 - try: - legacy = self.view.scrollerStyle() == NSScrollerStyleLegacy - except AttributeError: - legacy = True - if not self.view.hasHorizontalScroller(): - width = self.child.get_size_request()[0] - if not self.view.hasVerticalScroller(): - height = self.child.get_size_request()[1] - # Add a little room for the scrollbars (if necessary) - if legacy and self.view.hasHorizontalScroller(): - height += NSScroller.scrollerWidth() - if legacy and self.view.hasVerticalScroller(): - width += NSScroller.scrollerWidth() - return width, height - else: - return 0, 0 - - def place_children(self): - if self.child is not None: - scroll_view_size = self.view.contentView().frame().size - child_width, child_height = self.child.get_size_request() - child_width = max(child_width, scroll_view_size.width) - child_height = max(child_height, scroll_view_size.height) - frame = NSRect(NSPoint(0,0), NSSize(child_width, child_height)) - if isinstance(self.child, tableview.TableView) and self.child.is_showing_headers(): - # Hack to allow the content of a table view to scroll, but not - # the headers - self.child.place(frame, self.document_view) - if self.view.documentView() is not self.child.tableview: - self.view.setDocumentView_(self.child.tableview) - else: - self.child.place(frame, self.document_view) - self.document_view.setFrame_(frame) - self.document_view.setNeedsDisplay_(YES) - self.view.setNeedsDisplay_(YES) - self.child.emit('place-in-scroller') - -class ExpanderView(FlippedView): - def init(self): - self = super(ExpanderView, self).init() - self.label_rect = None - self.content_view = None - self.button = NSButton.alloc().init() - self.button.setState_(NSOffState) - self.button.setTitle_("") - self.button.setBezelStyle_(NSDisclosureBezelStyle) - self.button.setButtonType_(NSPushOnPushOffButton) - self.button.sizeToFit() - self.addSubview_(self.button) - self.button.setTarget_(self) - self.button.setAction_('buttonChanged:') - self.content_view = FlippedView.alloc().init() - return self - - def buttonChanged_(self, button): - if button.state() == NSOnState: - self.addSubview_(self.content_view) - else: - self.content_view.removeFromSuperview() - if self.window(): - wrappermap.wrapper(self).invalidate_size_request() - - def mouseDown_(self, event): - pass # Just need to respond to the selector so we get mouseUp_ - - def mouseUp_(self, event): - position = event.locationInWindow() - window_label_rect = self.convertRect_toView_(self.label_rect, None) - if NSPointInRect(position, window_label_rect): - self.button.setNextState() - self.buttonChanged_(self.button) - -class Expander(Bin): - BUTTON_PAD_TOP = 2 - BUTTON_PAD_LEFT = 4 - LABEL_SPACING = 4 - - def __init__(self, child): - Bin.__init__(self) - if child: - self.add(child) - self.label = None - self.spacing = 0 - self.view = ExpanderView.alloc().init() - self.button = self.view.button - self.button.setFrameOrigin_(NSPoint(self.BUTTON_PAD_LEFT, - self.BUTTON_PAD_TOP)) - self.content_view = self.view.content_view - - def remove_viewport(self): - Bin.remove_viewport(self) - if self.label is not None: - self.label.remove_viewport() - - def set_spacing(self, spacing): - self.spacing = spacing - - def set_label(self, widget): - if self.label is not None: - self.label.remove_viewport() - self.label = widget - self.children_changed() - - def set_expanded(self, expanded): - if expanded: - self.button.setState_(NSOnState) - else: - self.button.setState_(NSOffState) - self.view.buttonChanged_(self.button) - - def calc_top_size(self): - width = self.button.bounds().size.width - height = self.button.bounds().size.height - if self.label is not None: - label_width, label_height = self.label.get_size_request() - width += self.LABEL_SPACING + label_width - height = max(height, label_height) - width += self.BUTTON_PAD_LEFT - height += self.BUTTON_PAD_TOP - return width, height - - def calc_size_request(self): - width, height = self.calc_top_size() - if self.child is not None and self.button.state() == NSOnState: - child_width, child_height = self.child.get_size_request() - width = max(width, child_width) - height += self.spacing + child_height - return width, height - - def place_children(self): - top_width, top_height = self.calc_top_size() - if self.label: - label_width, label_height = self.label.get_size_request() - button_width = self.button.bounds().size.width - label_x = self.BUTTON_PAD_LEFT + button_width + self.LABEL_SPACING - label_rect = NSMakeRect(label_x, self.BUTTON_PAD_TOP, - label_width, label_height) - self.label.place(label_rect, self.viewport.view) - self.view.label_rect = label_rect - if self.child: - size = self.viewport.area().size - child_rect = NSMakeRect(0, 0, size.width, size.height - - top_height) - self.content_view.setFrame_(NSMakeRect(0, top_height, size.width, - size.height - top_height)) - self.child.place(child_rect, self.content_view) - - -class TabViewDelegate(NSObject): - def tabView_willSelectTabViewItem_(self, tab_view, tab_view_item): - try: - wrapper = wrappermap.wrapper(tab_view) - except KeyError: - pass # The NSTabView hasn't been placed yet, don't worry about it. - else: - wrapper.place_child_with_item(tab_view_item) - -class TabContainer(Container): - def __init__(self): - Container.__init__(self) - self.children = [] - self.item_to_child = {} - self.view = NSTabView.alloc().init() - self.view.setAllowsTruncatedLabels_(NO) - self.delegate = TabViewDelegate.alloc().init() - self.view.setDelegate_(self.delegate) - - def append_tab(self, child_widget, label, image): - item = NSTabViewItem.alloc().init() - item.setLabel_(label) - item.setView_(FlippedView.alloc().init()) - self.view.addTabViewItem_(item) - self.children.append(child_widget) - self.child_added(child_widget) - self.item_to_child[item] = child_widget - - def select_tab(self, index): - self.view.selectTabViewItemAtIndex_(index) - - def place_children(self): - self.place_child_with_item(self.view.selectedTabViewItem()) - - def place_child_with_item(self, tab_view_item): - child = self.item_to_child[tab_view_item] - child_view = tab_view_item.view() - content_rect =self.view.contentRect() - child_view.setFrame_(content_rect) - child.place(child_view.bounds(), child_view) - - def calc_size_request(self): - tab_size = self.view.minimumSize() - # make sure there's enough room for the tabs, plus a little extra - # space to make things look good - max_width = tab_size.width + 60 - max_height = 0 - for child in self.children: - width, height = child.get_size_request() - max_width = max(width, max_width) - max_height = max(height, max_height) - max_height += tab_size.height - - return max_width, max_height diff --git a/mvc/widgets/osx/layoutmanager.py b/mvc/widgets/osx/layoutmanager.py deleted file mode 100644 index de4301b..0000000 --- a/mvc/widgets/osx/layoutmanager.py +++ /dev/null @@ -1,445 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""textlayout.py -- Contains the LayoutManager class. It handles laying text, -buttons, getting font metrics and other tasks that are required to size -things. -""" -import logging -import math - -from AppKit import * -from Foundation import * -from objc import YES, NO, nil - -import drawing - -INFINITE = 1000000 # size of an "infinite" dimension - -class MiroLayoutManager(NSLayoutManager): - """Overide NSLayoutManager to draw better underlines.""" - - def drawUnderlineForGlyphRange_underlineType_baselineOffset_lineFragmentRect_lineFragmentGlyphRange_containerOrigin_(self, glyph_range, typ, offset, line_rect, line_glyph_range, container_origin): - container, _ = self.textContainerForGlyphAtIndex_effectiveRange_(glyph_range.location, None) - rect = self.boundingRectForGlyphRange_inTextContainer_(glyph_range, container) - x = container_origin.x + rect.origin.x - y = (container_origin.y + rect.origin.y + rect.size.height - offset) - underline_height, offset = self.calc_underline_extents(glyph_range) - y = math.ceil(y + offset) + underline_height / 2.0 - path = NSBezierPath.bezierPath() - path.setLineWidth_(underline_height) - path.moveToPoint_(NSPoint(x, y)) - path.relativeLineToPoint_(NSPoint(rect.size.width, 0)) - path.stroke() - - def calc_underline_extents(self, line_glyph_range): - index = self.characterIndexForGlyphAtIndex_(line_glyph_range.location) - font, _ = self.textStorage().attribute_atIndex_effectiveRange_(NSFontAttributeName, index, None) - # we use a couple of magic numbers that seems to work okay. I (BDK) - # got it from some old mozilla code. - height = font.ascender() - font.descender() - height = max(1.0, round(0.05 * height)) - offset = max(1.0, round(0.1 * height)) - return height, offset - -class TextBoxPool(object): - """Handles a pool of TextBox objects. We monitor the TextBox objects and - when those objects die, we reclaim them for the pool. - - Creating TextBoxes is fairly expensive and NSLayoutManager do a lot of - caching, so it's useful to keep them around rather than destroying them. - """ - - def __init__(self): - self.used_text_boxes = [] - self.available_text_boxes = [] - - def get(self): - """Get a NSLayoutManager, either from the pool or by creating a new - one. - """ - try: - rv = self.available_text_boxes.pop() - except IndexError: - rv = TextBox() - self.used_text_boxes.append(rv) - return rv - - def reclaim_textboxes(self): - """Move used TextBoxes back to the available pool. This should be - called after the code using text boxes is done using all of them. - """ - self.available_text_boxes.extend(self.used_text_boxes) - self.used_text_boxes[:] = [] - -text_box_pool = TextBoxPool() - -class Font(object): - line_height_sizer = NSLayoutManager.alloc().init() - - def __init__(self, nsfont): - self.nsfont = nsfont - - def ascent(self): - return self.nsfont.ascender() - - def descent(self): - return -self.nsfont.descender() - - def line_height(self): - return Font.line_height_sizer.defaultLineHeightForFont_(self.nsfont) - -class FontPool(object): - def __init__(self): - self._cached_fonts = {} - - def get(self, scale_factor, bold, italic, family): - cache_key = (scale_factor, bold, italic, family) - try: - return self._cached_fonts[cache_key] - except KeyError: - font = self._create(scale_factor, bold, italic, family) - self._cached_fonts[cache_key] = font - return font - - def _create(self, scale_factor, bold, italic, family): - size = round(scale_factor * NSFont.systemFontSize()) - nsfont = None - if family is not None: - if bold: - nsfont = NSFont.fontWithName_size_(family + " Bold", size) - else: - nsfont = NSFont.fontWithName_size_(family, size) - if nsfont is None: - logging.error('FontPool: family %s scale %s bold %s ' - 'italic %s not found', - family, scale_factor, bold, italic) - # Att his point either we have requested a custom font that failed - # to load or the system font was requested. - if nsfont is None: - if bold: - nsfont = NSFont.boldSystemFontOfSize_(size) - else: - nsfont = NSFont.systemFontOfSize_(size) - return Font(nsfont) - -class LayoutManager(object): - font_pool = FontPool() - default_font = font_pool.get(1.0, False, False, None) - - def __init__(self): - self.current_font = self.default_font - self.set_text_color((0, 0, 0)) - self.set_text_shadow(None) - - def font(self, scale_factor, bold=False, italic=False, family=None): - return self.font_pool.get(scale_factor, bold, italic, family) - - def set_font(self, scale_factor, bold=False, italic=False, family=None): - self.current_font = self.font(scale_factor, bold, italic, family) - - def set_text_color(self, color): - self.text_color = color - - def set_text_shadow(self, shadow): - self.shadow = shadow - - def textbox(self, text, underline=False): - text_box = text_box_pool.get() - color = NSColor.colorWithDeviceRed_green_blue_alpha_(self.text_color[0], self.text_color[1], self.text_color[2], 1.0) - text_box.reset(text, self.current_font, color, self.shadow, underline) - return text_box - - def button(self, text, pressed=False, disabled=False, style='normal'): - if style == 'webby': - return StyledButton(text, self.current_font, pressed, disabled) - else: - return NativeButton(text, self.current_font, pressed, disabled) - - def reset(self): - text_box_pool.reclaim_textboxes() - self.current_font = self.default_font - self.text_color = (0, 0, 0) - self.shadow = None - -class TextBox(object): - def __init__(self): - self.layout_manager = MiroLayoutManager.alloc().init() - container = NSTextContainer.alloc().init() - container.setLineFragmentPadding_(0) - self.layout_manager.addTextContainer_(container) - self.layout_manager.setUsesFontLeading_(NO) - self.text_storage = NSTextStorage.alloc().init() - self.text_storage.addLayoutManager_(self.layout_manager) - self.text_container = self.layout_manager.textContainers()[0] - - def reset(self, text, font, color, shadow, underline): - """Reset the text box so it's ready to be used by a new owner.""" - self.text_storage.deleteCharactersInRange_(NSRange(0, - self.text_storage.length())) - self.text_container.setContainerSize_(NSSize(INFINITE, INFINITE)) - self.paragraph_style = NSMutableParagraphStyle.alloc().init() - self.font = font - self.color = color - self.shadow = shadow - self.width = None - self.set_text(text, underline=underline) - - def make_attr_string(self, text, color, font, underline): - attributes = NSMutableDictionary.alloc().init() - if color is not None: - nscolor = NSColor.colorWithDeviceRed_green_blue_alpha_(color[0], color[1], color[2], 1.0) - attributes.setObject_forKey_(nscolor, NSForegroundColorAttributeName) - else: - attributes.setObject_forKey_(self.color, NSForegroundColorAttributeName) - if font is not None: - attributes.setObject_forKey_(font.nsfont, NSFontAttributeName) - else: - attributes.setObject_forKey_(self.font.nsfont, NSFontAttributeName) - if underline: - attributes.setObject_forKey_(NSUnderlineStyleSingle, NSUnderlineStyleAttributeName) - attributes.setObject_forKey_(self.paragraph_style.copy(), NSParagraphStyleAttributeName) - if text is None: - text = "" - return NSAttributedString.alloc().initWithString_attributes_(text, attributes) - - def set_text(self, text, color=None, font=None, underline=False): - string = self.make_attr_string(text, color, font, underline) - self.text_storage.setAttributedString_(string) - - def append_text(self, text, color=None, font=None, underline=False): - string = self.make_attr_string(text, color, font, underline) - self.text_storage.appendAttributedString_(string) - - def set_width(self, width): - if width is not None: - self.text_container.setContainerSize_(NSSize(width, INFINITE)) - else: - self.text_container.setContainerSize_(NSSize(INFINITE, INFINITE)) - self.width = width - - def update_paragraph_style(self): - attr = NSParagraphStyleAttributeName - value = self.paragraph_style.copy() - rnge = NSMakeRange(0, self.text_storage.length()) - self.text_storage.addAttribute_value_range_(attr, value, rnge) - - def set_wrap_style(self, wrap): - if wrap == 'word': - self.paragraph_style.setLineBreakMode_(NSLineBreakByWordWrapping) - elif wrap == 'char': - self.paragraph_style.setLineBreakMode_(NSLineBreakByCharWrapping) - elif wrap == 'truncated-char': - self.paragraph_style.setLineBreakMode_(NSLineBreakByTruncatingTail) - else: - raise ValueError("Unknown wrap value: %s" % wrap) - self.update_paragraph_style() - - def set_alignment(self, align): - if align == 'left': - self.paragraph_style.setAlignment_(NSLeftTextAlignment) - elif align == 'right': - self.paragraph_style.setAlignment_(NSRightTextAlignment) - elif align == 'center': - self.paragraph_style.setAlignment_(NSCenterTextAlignment) - else: - raise ValueError("Unknown align value: %s" % align) - self.update_paragraph_style() - - def get_size(self): - # The next line is there just to force cocoa to layout the text - self.layout_manager.glyphRangeForTextContainer_(self.text_container) - rect = self.layout_manager.usedRectForTextContainer_(self.text_container) - return rect.size.width, rect.size.height - - def char_at(self, x, y): - width, height = self.get_size() - if 0 <= x < width and 0 <= y < height: - index, _ = self.layout_manager.glyphIndexForPoint_inTextContainer_fractionOfDistanceThroughGlyph_(NSPoint(x, y), self.text_container, None) - return index - else: - return None - - def draw(self, context, x, y, width, height): - if self.shadow is not None: - context.save() - context.set_shadow(self.shadow.color, self.shadow.opacity, self.shadow.offset, self.shadow.blur_radius) - self.width = width - self.text_container.setContainerSize_(NSSize(width, height)) - glyph_range = self.layout_manager.glyphRangeForTextContainer_(self.text_container) - self.layout_manager.drawGlyphsForGlyphRange_atPoint_(glyph_range, NSPoint(x, y)) - if self.shadow is not None: - context.restore() - context.path.removeAllPoints() - -class NativeButton(object): - - def __init__(self, text, font, pressed, disabled=False): - self.min_width = 0 - self.cell = NSButtonCell.alloc().init() - self.cell.setBezelStyle_(NSRoundRectBezelStyle) - self.cell.setButtonType_(NSMomentaryPushInButton) - self.cell.setFont_(font.nsfont) - self.cell.setEnabled_(not disabled) - self.cell.setTitle_(text) - if pressed: - self.cell.setState_(NSOnState) - else: - self.cell.setState_(NSOffState) - self.cell.setImagePosition_(NSImageLeft) - - def set_icon(self, icon): - image = icon.image.copy() - image.setFlipped_(NO) - self.cell.setImage_(image) - - def get_size(self): - size = self.cell.cellSize() - return size.width, size.height - - def draw(self, context, x, y, width, height): - rect = NSMakeRect(x, y, width, height) - NSGraphicsContext.currentContext().saveGraphicsState() - self.cell.drawWithFrame_inView_(rect, context.view) - NSGraphicsContext.currentContext().restoreGraphicsState() - context.path.removeAllPoints() - -class StyledButton(object): - PAD_HORIZONTAL = 11 - BIG_PAD_VERTICAL = 4 - SMALL_PAD_VERTICAL = 2 - TOP_COLOR = (1, 1, 1) - BOTTOM_COLOR = (0.86, 0.86, 0.86) - LINE_COLOR_TOP = (0.71, 0.71, 0.71) - LINE_COLOR_BOTTOM = (0.45, 0.45, 0.45) - TEXT_COLOR = (0.19, 0.19, 0.19) - DISABLED_COLOR = (0.86, 0.86, 0.86) - DISABLED_TEXT_COLOR = (0.43, 0.43, 0.43) - ICON_PAD = 8 - - def __init__(self, text, font, pressed, disabled=False): - self.pressed = pressed - self.disabled = disabled - attributes = NSMutableDictionary.alloc().init() - attributes.setObject_forKey_(font.nsfont, NSFontAttributeName) - if self.disabled: - color = self.DISABLED_TEXT_COLOR - else: - color = self.TEXT_COLOR - nscolor = NSColor.colorWithDeviceRed_green_blue_alpha_(color[0], color[1], color[2], 1.0) - attributes.setObject_forKey_(nscolor, NSForegroundColorAttributeName) - self.title = NSAttributedString.alloc().initWithString_attributes_(text, attributes) - self.image = None - - def set_icon(self, icon): - self.image = icon.image.copy() - self.image.setFlipped_(YES) - - def get_size(self): - width, height = self.get_text_size() - if self.image is not None: - width += self.image.size().width + self.ICON_PAD - height = max(height, self.image.size().height) - height += self.BIG_PAD_VERTICAL * 2 - else: - height += self.SMALL_PAD_VERTICAL * 2 - if height % 2 == 1: - # make height even so that the radius of our circle is whole - height += 1 - width += self.PAD_HORIZONTAL * 2 - return width, height - - def get_text_size(self): - size = self.title.size() - return size.width, size.height - - def draw(self, context, x, y, width, height): - self._draw_button(context, x, y, width, height) - self._draw_title(context, x, y) - context.path.removeAllPoints() - - def _draw_button(self, context, x, y, width, height): - radius = height / 2 - self._draw_path(context, x, y, width, height, radius) - if self.disabled: - end_color = self.DISABLED_COLOR - start_color = self.DISABLED_COLOR - elif self.pressed: - end_color = self.TOP_COLOR - start_color = self.BOTTOM_COLOR - else: - context.set_line_width(1) - start_color = self.TOP_COLOR - end_color = self.BOTTOM_COLOR - gradient = drawing.Gradient(x, y, x, y+height) - gradient.set_start_color(start_color) - gradient.set_end_color(end_color) - context.gradient_fill(gradient) - self._draw_border(context, x, y, width, height, radius) - - def _draw_path(self, context, x, y, width, height, radius): - inner_width = width - radius * 2 - context.move_to(x + radius, y) - context.rel_line_to(inner_width, 0) - context.arc(x + width - radius, y+radius, radius, -math.pi/2, math.pi/2) - context.rel_line_to(-inner_width, 0) - context.arc(x + radius, y+radius, radius, math.pi/2, -math.pi/2) - - def _draw_path_reverse(self, context, x, y, width, height, radius): - inner_width = width - radius * 2 - context.move_to(x + radius, y) - context.arc_negative(x + radius, y+radius, radius, -math.pi/2, math.pi/2) - context.rel_line_to(inner_width, 0) - context.arc_negative(x + width - radius, y+radius, radius, math.pi/2, -math.pi/2) - context.rel_line_to(-inner_width, 0) - - def _draw_border(self, context, x, y, width, height, radius): - self._draw_path(context, x, y, width, height, radius) - self._draw_path_reverse(context, x+1, y+1, width-2, height-2, radius-1) - gradient = drawing.Gradient(x, y, x, y+height) - gradient.set_start_color(self.LINE_COLOR_TOP) - gradient.set_end_color(self.LINE_COLOR_BOTTOM) - context.save() - context.clip() - context.rectangle(x, y, width, height) - context.gradient_fill(gradient) - context.restore() - - def _draw_title(self, context, x, y): - c_width, c_height = self.get_size() - t_width, t_height = self.get_text_size() - x = x + self.PAD_HORIZONTAL - y = y + (c_height - t_height) / 2 - if self.image is not None: - self.image.drawAtPoint_fromRect_operation_fraction_( - NSPoint(x, y+3), NSZeroRect, NSCompositeSourceOver, 1.0) - x += self.image.size().width + self.ICON_PAD - else: - y += 0.5 - self.title.drawAtPoint_(NSPoint(x, y)) diff --git a/mvc/widgets/osx/osxmenus.py b/mvc/widgets/osx/osxmenus.py deleted file mode 100644 index 32ca469..0000000 --- a/mvc/widgets/osx/osxmenus.py +++ /dev/null @@ -1,571 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""menus.py -- Menu handling code.""" - -import logging -import struct - -from objc import nil, NO, YES -import AppKit -from AppKit import * -from Foundation import * - -from mvc import signals -from mvc.widgets import keyboard -# import these names directly into our namespace for easy access -from mvc.widgets.keyboard import Shortcut, MOD - -# XXX hacks -def _(text, *params): - if params: - return text % params[0] - return text - -MODIFIERS_MAP = { - keyboard.MOD: NSCommandKeyMask, - keyboard.CMD: NSCommandKeyMask, - keyboard.SHIFT: NSShiftKeyMask, - keyboard.CTRL: NSControlKeyMask, - keyboard.ALT: NSAlternateKeyMask -} - -if isinstance(NSBackspaceCharacter, int): - backspace = NSBackspaceCharacter -else: - backspace = ord(NSBackspaceCharacter) - -KEYS_MAP = { - keyboard.SPACE: " ", - keyboard.ENTER: "\r", - keyboard.BKSPACE: struct.pack("H", backspace), - keyboard.DELETE: NSDeleteFunctionKey, - keyboard.RIGHT_ARROW: NSRightArrowFunctionKey, - keyboard.LEFT_ARROW: NSLeftArrowFunctionKey, - keyboard.UP_ARROW: NSUpArrowFunctionKey, - keyboard.DOWN_ARROW: NSDownArrowFunctionKey, - '.': '.', - ',': ',' -} -# add function keys -for i in range(1, 13): - portable_key = getattr(keyboard, "F%s" % i) - osx_key = getattr(AppKit, "NSF%sFunctionKey" % i) - KEYS_MAP[portable_key] = osx_key - -REVERSE_MODIFIERS_MAP = dict((i[1], i[0]) for i in MODIFIERS_MAP.items()) -REVERSE_KEYS_MAP = dict((i[1], i[0]) for i in KEYS_MAP.items() - if i[0] != keyboard.BKSPACE) -REVERSE_KEYS_MAP[u'\x7f'] = keyboard.BKSPACE -REVERSE_KEYS_MAP[u'\x1b'] = keyboard.ESCAPE - -def make_modifier_mask(shortcut): - mask = 0 - for modifier in shortcut.modifiers: - mask |= MODIFIERS_MAP[modifier] - return mask - -VIEW_ITEM_MAP = {} - -def _remove_mnemonic(label): - """Remove the underscore used by GTK for mnemonics. - - We totally ignore them on OSX, since they are now deprecated. - """ - return label.replace("_", "") - -def handle_menu_activate(ns_menu_item): - """Handle a menu item being activated. - - This gets called by our application delegate. - """ - - menu_item = ns_menu_item.representedObject() - menu_item.emit("activate") - menubar = menu_item._find_menubar() - if menubar is not None: - menubar.emit("activate", menu_item.name) - -class MenuItemBase(signals.SignalEmitter): - """Base class for MenuItem and Separator""" - def __init__(self): - signals.SignalEmitter.__init__(self) - self.name = None - self.parent = None - - def show(self): - self._menu_item.setHidden_(False) - - def hide(self): - self._menu_item.setHidden_(True) - - def enable(self): - self._menu_item.setEnabled_(True) - - def disable(self): - self._menu_item.setEnabled_(False) - - def remove_from_parent(self): - """Remove this menu item from it's parent Menu.""" - if self.parent is not None: - self.parent.remove(self) - -class MenuItem(MenuItemBase): - """See the GTK version of this method for the current docstring.""" - - # map Miro action names to standard OSX actions. - _STD_ACTION_MAP = { - "HideMiro": (NSApp(), 'hide:'), - "HideOthers": (NSApp(), 'hideOtherApplications:'), - "ShowAll": (NSApp(), 'unhideAllApplications:'), - "Cut": (nil, 'cut:'), - "Copy": (nil, 'copy:'), - "Paste": (nil, 'paste:'), - "Delete": (nil, 'delete:'), - "SelectAll": (nil, 'selectAll:'), - "Zoom": (nil, 'performZoom:'), - "Minimize": (nil, 'performMiniaturize:'), - "BringAllToFront": (nil, 'arrangeInFront:'), - "CloseWindow": (nil, 'performClose:'), - } - - def __init__(self, label, name, shortcut=None): - MenuItemBase.__init__(self) - self.name = name - self._menu_item = self._make_menu_item(label) - self.create_signal('activate') - self._setup_shortcut(shortcut) - - def _make_menu_item(self, label): - menu_item = NSMenuItem.alloc().init() - menu_item.setTitle_(_remove_mnemonic(label)) - # we set ourselves as the represented object for the menu item so we - # can easily translate one to the other - menu_item.setRepresentedObject_(self) - if self.name in self._STD_ACTION_MAP: - menu_item.setTarget_(self._STD_ACTION_MAP[self.name][0]) - menu_item.setAction_(self._STD_ACTION_MAP[self.name][1]) - else: - menu_item.setTarget_(NSApp().delegate()) - menu_item.setAction_('handleMenuActivate:') - return menu_item - - def _setup_shortcut(self, shortcut): - if shortcut is None: - key = '' - modifier_mask = 0 - elif isinstance(shortcut.shortcut, str): - key = shortcut.shortcut - modifier_mask = make_modifier_mask(shortcut) - elif shortcut.shortcut in KEYS_MAP: - key = KEYS_MAP[shortcut.shortcut] - modifier_mask = make_modifier_mask(shortcut) - else: - logging.warn("Don't know how to handle shortcut: %s", shortcut) - return - self._menu_item.setKeyEquivalent_(key) - self._menu_item.setKeyEquivalentModifierMask_(modifier_mask) - - def _change_shortcut(self, shortcut): - self._setup_shortcut(shortcut) - - def set_label(self, new_label): - self._menu_item.setTitle_(new_label) - - def get_label(self): - self._menu_item.title() - - def _find_menubar(self): - """Remove this menu item from it's parent Menu.""" - menu_item = self - while menu_item.parent is not None: - menu_item = menu_item.parent - if isinstance(menu_item, MenuBar): - return menu_item - else: - return None - -class CheckMenuItem(MenuItem): - """See the GTK version of this method for the current docstring.""" - def set_state(self, active): - if active is None: - state = NSMixedState - elif active: - state = NSOnState - else: - state = NSOffState - self._menu_item.setState_(state) - - def get_state(self): - return self._menu_item.state() == NSOnState - - def do_activate(self): - if self._menu_item.state() == NSOffState: - self._menu_item.setState_(NSOnState) - else: - self._menu_item.setState_(NSOffState) - -class RadioMenuItem(CheckMenuItem): - """See the GTK version of this method for the current docstring.""" - def __init__(self, label, name, shortcut=None): - CheckMenuItem.__init__(self, label, name, shortcut) - # The leader of a radio group stores the list of all items in the - # group - self.group_leader = None - self.others_in_group = set() - - def set_group(self, group_item): - if self.group_leader is not None: - raise ValueError("%s is already in a group" % self) - if group_item.group_leader is None: - group_leader = group_item - else: - group_leader = group_item.group_leader - if group_leader.group_leader is not None: - raise AssertionError("group_leader structure is wrong") - self.group_leader = group_leader - group_leader.others_in_group.add(self) - - def remove_from_group(self): - """Remove this RadioMenuItem from its current group.""" - if self.group_leader is not None: - # we have a group leader, remove ourself from their list. - # Note that this code will work even if we're the last item in - # others_in_group. - self.group_leader.others_in_group.remove(self) - self.group_leader = None - elif len(self.others_in_group) > 1: - # we're the group leader, hand off the leader to a different item - first_item = iter(self.others_in_group).next() - for other in self.others_in_group: - if other is first_item: - other.others_in_group = self.others_in_group - other.others_in_group.remove(first_item) - other.group_leader = None - else: - other.group_leader = first_item - self.others_in_group = set() - elif len(self.others_in_group) == 1: - # we're the group leader, but there's only 1 other item. unset - # everything. - for other in self.others_in_group: - other.group_leader = None - self.others_in_group = set() - - def _items_in_group(self): - if self.group_leader is not None: # we have a group leader - yield self.group_leader - for other in self.group_leader.others_in_group: - yield other - elif self.others_in_group: # we're the group leader - yield self - for other in self.others_in_group: - yield other - else: # we don't have a group set - yield self - - def do_activate(self): - for item in self._items_in_group(): - if item is not self: - item.set_state(False) - CheckMenuItem.do_activate(self) - -class Separator(MenuItemBase): - """See the GTK version of this method for the current docstring.""" - def __init__(self): - MenuItemBase.__init__(self) - self._menu_item = NSMenuItem.separatorItem() - -class MenuShell(signals.SignalEmitter): - def __init__(self, nsmenu): - signals.SignalEmitter.__init__(self) - self._menu = nsmenu - self.children = [] - self.parent = None - - def append(self, menu_item): - """Add a menu item to the end of this menu.""" - self.children.append(menu_item) - self._menu.addItem_(menu_item._menu_item) - menu_item.parent = self - - def insert(self, index, menu_item): - """Insert a menu item in the middle of this menu.""" - self.children.insert(index, menu_item) - self._menu.insertItem_atIndex_(menu_item._menu_item, index) - menu_item.parent = self - - def index(self, name): - """Find the position of a child menu item.""" - for i, menu_item in enumerate(self.children): - if menu_item.name == name: - return i - return -1 - - def remove(self, menu_item): - """Remove a child menu item. - - :raises ValueError: menu_item is not a child of this menu - """ - self.children.remove(menu_item) - self._menu.removeItem_(menu_item._menu_item) - menu_item.parent = None - - def get_children(self): - """Get the child menu items in order.""" - return list(self.children) - - def find(self, name): - """Search for a menu or menu item - - This method recursively searches the entire menu structure for a Menu - or MenuItem object with a given name. - - :raises KeyError: name not found - """ - found = self._find(name) - if found is None: - raise KeyError(name) - else: - return found - - def _find(self, name): - """Low-level helper-method for find(). - - :returns: found menu item or None. - """ - for menu_item in self.get_children(): - if menu_item.name == name: - return menu_item - if isinstance(menu_item, Menu): - submenu_find = menu_item._find(name) - if submenu_find is not None: - return submenu_find - return None - -class Menu(MenuShell): - """See the GTK version of this method for the current docstring.""" - def __init__(self, label, name, child_items=None): - MenuShell.__init__(self, NSMenu.alloc().init()) - self._menu.setTitle_(_remove_mnemonic(label)) - # we will enable/disable menu items manually - self._menu.setAutoenablesItems_(False) - self.name = name - if child_items is not None: - for item in child_items: - self.append(item) - self._menu_item = NSMenuItem.alloc().init() - self._menu_item.setTitle_(_remove_mnemonic(label)) - self._menu_item.setSubmenu_(self._menu) - # Hack to set the services menu - if name == "ServicesMenu": - NSApp().setServicesMenu_(self._menu_item) - - def show(self): - self._menu_item.setHidden_(False) - - def hide(self): - self._menu_item.setHidden_(True) - - def enable(self): - self._menu_item.setEnabled_(True) - - def disable(self): - self._menu_item.setEnabled_(False) - -class AppMenu(MenuShell): - """Wrapper for the application menu (AKA the Miro menu) - - We need to special case this because OSX automatically creates the menu - item. - """ - def __init__(self): - MenuShell.__init__(self, NSApp().mainMenu().itemAtIndex_(0).submenu()) - self.name = "Libre Video Converter" - -class MenuBar(MenuShell): - """See the GTK version of this method for the current docstring.""" - def __init__(self): - MenuShell.__init__(self, NSApp().mainMenu()) - self.create_signal('activate') - self._add_app_menu() - - def _add_app_menu(self): - """Add the app menu to this menu bar. - - We need to special case this because OSX automatically adds the - NSMenuItem for the app menu, we just need to set up our wrappers. - """ - self._app_menu = AppMenu() - self.children.append(self._app_menu) - self._app_menu.parent = self - - def add_initial_menus(self, menus): - for menu in menus: - self.append(menu) - self._modify_initial_menus() - - def _extract_menu_item(self, name): - """Helper method for changing the portable menu structure.""" - menu_item = self.find(name) - menu_item.remove_from_parent() - return menu_item - - def _modify_initial_menus(self): - short_appname = "Libre Video Converter" # XXX - - # Application menu - miroMenuItems = [ - self._extract_menu_item("About"), - Separator(), - self._extract_menu_item("Quit") - ] - - for item in miroMenuItems: - self._app_menu.append(item) - - self._app_menu.find("Quit").set_label(_("Quit %(appname)s", - {"appname": short_appname})) - - # Help Menu - #helpItem = self.find("Help") - #helpItem.set_label(_("%(appname)s Help", {"appname": short_appname})) - #helpItem._change_shortcut(Shortcut("?", MOD)) - - self._update_present_menu() - self._connect_to_signals() - - def do_activate(self, name): - # We handle a couple OSX-specific actions here - if name == "PresentActualSize": - NSApp().delegate().present_movie('natural-size') - elif name == "PresentDoubleSize": - NSApp().delegate().present_movie('double-size') - elif name == "PresentHalfSize": - NSApp().delegate().present_movie('half-size') - elif name == "ShowMain": - app.widgetapp.window.nswindow.makeKeyAndOrderFront_(self) - - def _connect_to_signals(self): - return - app.playback_manager.connect("will-play", self._on_playback_change) - app.playback_manager.connect("will-stop", self._on_playback_change) - - def _on_playback_change(self, playback_manager, *args): - self._update_present_menu() - - def _update_present_menu(self): - return - if self._should_enable_present_menu(): - for menu_item in self.present_menu.get_children(): - menu_item.enable() - else: - for menu_item in self.present_menu.get_children(): - menu_item.disable() - - def _should_enable_present_menu(self): - return False - if (app.playback_manager.is_playing and - not app.playback_manager.is_playing_audio): - # we're currently playing video, allow the user to fullscreen - return True - selection_info = app.item_list_controller_manager.get_selection_info() - if (selection_info.has_download and - selection_info.has_file_type('video')): - # A downloaded video is selected, allow the user to start playback - # in fullscreen - return True - return False - -#class ContextMenuHandler(NSObject): -# def initWithCallback_(self, callback): -# self = super(ContextMenuHandler, self).init() -# self.callback = callback -# return self -# -# def handleMenuItem_(self, sender): -# self.callback() -# -#class MiroContextMenu(NSMenu): -# # Works exactly like NSMenu, except it keeps a reference to the menu -# # handler objects. -# def init(self): -# self = super(MiroContextMenu, self).init() -# self.handlers = set() -# return self -# -# def addItem_(self, item): -# if isinstance(item.target(), ContextMenuHandler): -# self.handlers.add(item.target()) -# return NSMenu.addItem_(self, item) -# -def make_context_menu(menu_items): - nsmenu = MiroContextMenu.alloc().init() - for item in menu_items: - if item is None: - nsitem = NSMenuItem.separatorItem() - else: - label, callback = item - nsitem = NSMenuItem.alloc().init() - if isinstance(label, tuple) and len(label) == 2: - label, icon_path = label - image = NSImage.alloc().initWithContentsOfFile_(icon_path) - nsitem.setImage_(image) - if callback is None: - font_size = NSFont.systemFontSize() - font = NSFont.fontWithName_size_("Lucida Sans Italic", font_size) - if font is None: - font = NSFont.systemFontOfSize_(font_size) - attributes = {NSFontAttributeName: font} - attributed_label = NSAttributedString.alloc().initWithString_attributes_(label, attributes) - nsitem.setAttributedTitle_(attributed_label) - else: - nsitem.setTitle_(label) - if isinstance(callback, list): - submenu = make_context_menu(callback) - nsmenu.setSubmenu_forItem_(submenu, nsitem) - else: - handler = ContextMenuHandler.alloc().initWithCallback_(callback) - nsitem.setTarget_(handler) - nsitem.setAction_('handleMenuItem:') - nsmenu.addItem_(nsitem) - return nsmenu - -def translate_event_modifiers(event): - mods = set() - flags = event.modifierFlags() - if flags & NSCommandKeyMask: - mods.add(keyboard.CMD) - if flags & NSControlKeyMask: - mods.add(keyboard.CTRL) - if flags & NSAlternateKeyMask: - mods.add(keyboard.ALT) - if flags & NSShiftKeyMask: - mods.add(keyboard.SHIFT) - return mods diff --git a/mvc/widgets/osx/rect.py b/mvc/widgets/osx/rect.py deleted file mode 100644 index 3c8d448..0000000 --- a/mvc/widgets/osx/rect.py +++ /dev/null @@ -1,78 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -""".rect -- Simple Rectangle class.""" - -from Foundation import NSMakeRect, NSRectFromString - -class Rect(object): - @classmethod - def from_string(cls, rect_string): - if rect_string.startswith('{{'): - return NSRectWrapper(NSRectFromString(rect_string)) - else: - try: - items = [int(i) for i in rect_string.split(',')] - return Rect(*items) - except: - return None - - def __init__(self, x, y, width, height): - self.nsrect = NSMakeRect(x, y, width, height) - - def get_x(self): - return self.nsrect.origin.x - def set_x(self, x): - self.nsrect.origin.x = x - x = property(get_x, set_x) - - def get_y(self): - return self.nsrect.origin.y - def set_y(self, y): - self.nsrect.origin.x = y - y = property(get_y, set_y) - - def get_width(self): - return self.nsrect.size.width - def set_width(self, width): - self.nsrect.size.width = width - width = property(get_width, set_width) - - def get_height(self): - return self.nsrect.size.height - def set_height(self, height): - self.nsrect.size.height = height - height = property(get_height, set_height) - - def __str__(self): - return "%d,%d,%d,%d" % (self.nsrect.origin.x, self.nsrect.origin.y, self.nsrect.size.width, self.nsrect.size.height) - -class NSRectWrapper(Rect): - def __init__(self, nsrect): - self.nsrect = nsrect diff --git a/mvc/widgets/osx/simple.py b/mvc/widgets/osx/simple.py deleted file mode 100644 index 1c12b06..0000000 --- a/mvc/widgets/osx/simple.py +++ /dev/null @@ -1,376 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -from __future__ import division -import logging -import math - -from AppKit import * -from Foundation import * -from objc import YES, NO, nil - -from mvc.widgets import widgetconst -from .base import Widget, SimpleBin, FlippedView -from .utils import filename_to_unicode -import drawing -import wrappermap - -"""A collection of various simple widgets.""" - -class Image(object): - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - def __init__(self, path): - self._set_image(NSImage.alloc().initByReferencingFile_( - filename_to_unicode(path))) - - def _set_image(self, nsimage): - self.nsimage = nsimage - self.width = self.nsimage.size().width - self.height = self.nsimage.size().height - if self.width * self.height == 0: - raise ValueError('Image has invalid size: (%d, %d)' % ( - self.width, self.height)) - - def resize(self, width, height): - return ResizedImage(self, width, height) - - def crop_and_scale(self, src_x, src_y, src_width, src_height, dest_width, - dest_height): - if dest_width <= 0 or dest_height <= 0: - logging.stacktrace("invalid dest sizes: %s %s" % (dest_width, - dest_height)) - return TransformedImage(self.nsimage) - - source_rect = NSMakeRect(src_x, src_y, src_width, src_height) - dest_rect = NSMakeRect(0, 0, dest_width, dest_height) - - dest = NSImage.alloc().initWithSize_(NSSize(dest_width, dest_height)) - dest.lockFocus() - try: - NSGraphicsContext.currentContext().setImageInterpolation_( - NSImageInterpolationHigh) - self.nsimage.drawInRect_fromRect_operation_fraction_(dest_rect, - source_rect, NSCompositeCopy, 1.0) - finally: - dest.unlockFocus() - return TransformedImage(dest) - - def resize_for_space(self, width, height): - """Returns an image scaled to fit into the specified space at the - correct height/width ratio. - """ - # this prevents division by 0. - if self.width == 0 and self.height == 0: - return self - elif self.width == 0: - ratio = height / self.height - return self.resize(self.width, ratio * self.height) - elif self.height == 0: - ratio = width / self.width - return self.resize(ratio * self.width, self.height) - - ratio = min(width / self.width, height / self.height) - return self.resize(ratio * self.width, ratio * self.height) - -class ResizedImage(Image): - def __init__(self, image, width, height): - nsimage = image.nsimage.copy() - nsimage.setCacheMode_(NSImageCacheNever) - nsimage.setScalesWhenResized_(YES) - nsimage.setSize_(NSSize(width, height)) - self._set_image(nsimage) - -class TransformedImage(Image): - def __init__(self, nsimage): - self._set_image(nsimage) - -class NSImageDisplay(NSView): - def init(self): - self = super(NSImageDisplay, self).init() - self.border = False - self.image = None - return self - - def isFlipped(self): - return YES - - def set_border(self, border): - self.border = border - - def set_image(self, image): - self.image = image - - def drawRect_(self, dest_rect): - if self.image is not None: - source_rect = self.calculateSourceRectFromDestRect_(dest_rect) - context = NSGraphicsContext.currentContext() - context.setShouldAntialias_(YES) - context.setImageInterpolation_(NSImageInterpolationHigh) - context.saveGraphicsState() - drawing.flip_context(self.bounds().size.height) - self.image.nsimage.drawInRect_fromRect_operation_fraction_( - dest_rect, source_rect, NSCompositeSourceOver, 1.0) - context.restoreGraphicsState() - if self.border: - context = drawing.DrawingContext(self, self.bounds(), dest_rect) - context.style = drawing.DrawingStyle() - context.set_line_width(1) - context.set_color((0, 0, 0)) # black - context.rectangle(0, 0, context.width, context.height) - context.stroke() - - def calculateSourceRectFromDestRect_(self, dest_rect): - """Calulate where dest_rect maps to on our image. - - This is tricky because our image might be scaled up, in which case - the rect from our image will be smaller than dest_rect. - """ - view_size = self.frame().size - x_scale = float(self.image.width) / view_size.width - y_scale = float(self.image.height) / view_size.height - - return NSMakeRect(dest_rect.origin.x * x_scale, - dest_rect.origin.y * y_scale, - dest_rect.size.width * x_scale, - dest_rect.size.height * y_scale) - - # XXX FIXME: should track mouse movement - mouseDown is not the correct - # event. - def mouseDown_(self, event): - wrappermap.wrapper(self).emit('clicked') - -class ImageDisplay(Widget): - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - def __init__(self, image=None): - Widget.__init__(self) - self.create_signal('clicked') - self.view = NSImageDisplay.alloc().init() - self.set_image(image) - - def set_image(self, image): - self.image = image - if image: - image.nsimage.setCacheMode_(NSImageCacheNever) - self.view.set_image(image) - self.invalidate_size_request() - - def set_border(self, border): - self.view.set_border(border) - - def calc_size_request(self): - if self.image is not None: - return self.image.width, self.image.height - else: - return 0, 0 - -class ClickableImageButton(ImageDisplay): - def __init__(self, image_path, max_width=None, max_height=None): - ImageDisplay.__init__(self) - self.set_border(True) - self.max_width = max_width - self.max_height = max_height - self.image = None - self._width, self._height = None, None - if image_path: - self.set_path(image_path) - - def set_path(self, path): - image = Image(path) - if self.max_width: - image = image.resize_for_space(self.max_width, self.max_height) - super(ClickableImageButton, self).set_image(image) - - def calc_size_request(self): - if self.max_width: - return self.max_width, self.max_height - else: - return ImageDisplay.calc_size_request(self) - -class MiroImageView(NSImageView): - def viewWillMoveToWindow_(self, aWindow): - self.setAnimates_(not aWindow == nil) - -class AnimatedImageDisplay(Widget): - def __init__(self, path): - Widget.__init__(self) - self.nsimage = NSImage.alloc().initByReferencingFile_( - filename_to_unicode(path)) - self.view = MiroImageView.alloc().init() - self.view.setImage_(self.nsimage) - # enabled when viewWillMoveToWindow:aWindow invoked - self.view.setAnimates_(NO) - - def calc_size_request(self): - return self.nsimage.size().width, self.nsimage.size().height - -class Label(Widget): - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - def __init__(self, text="", wrap=False, color=None): - Widget.__init__(self) - self.view = NSTextField.alloc().init() - self.view.setEditable_(NO) - self.view.setBezeled_(NO) - self.view.setBordered_(NO) - self.view.setDrawsBackground_(NO) - self.wrap = wrap - self.bold = False - self.size = NSFont.systemFontSize() - self.sizer_cell = self.view.cell().copy() - self.set_font() - self.set_text(text) - self.__color = self.view.textColor() - if color is not None: - self.set_color(color) - - def get_width(self): - return self.calc_size_request()[0] - - def set_bold(self, bold): - self.bold = bold - self.set_font() - - def set_size(self, size): - if size > 0: - self.size = NSFont.systemFontSize() * size - elif size == widgetconst.SIZE_SMALL: - self.size = NSFont.smallSystemFontSize() - elif size == widgetconst.SIZE_NORMAL: - self.size = NSFont.systemFontSize() - else: - raise ValueError("Unknown size constant: %s" % size) - - self.set_font() - - def set_color(self, color): - self.__color = self.make_color(color) - - if self.view.isEnabled(): - self.view.setTextColor_(self.__color) - else: - self.view.setTextColor_(self.__color.colorWithAlphaComponent_(0.5)) - - def set_background_color(self, color): - self.view.setBackgroundColor_(self.make_color(color)) - self.view.setDrawsBackground_(YES) - - def set_font(self): - if self.bold: - font = NSFont.boldSystemFontOfSize_(self.size) - else: - font= NSFont.systemFontOfSize_(self.size) - self.view.setFont_(font) - self.sizer_cell.setFont_(font) - self.invalidate_size_request() - - def calc_size_request(self): - if (self.wrap and self.manual_size_request is not None and - self.manual_size_request[0] > 0): - wrap_width = self.manual_size_request[0] - size = self.sizer_cell.cellSizeForBounds_(NSMakeRect(0, 0, - wrap_width, 10000)) - else: - size = self.sizer_cell.cellSize() - return math.ceil(size.width), math.ceil(size.height) - - def baseline(self): - return -self.view.font().descender() - - def set_text(self, text): - self.view.setStringValue_(text) - self.sizer_cell.setStringValue_(text) - self.invalidate_size_request() - - def get_text(self): - val = self.view.stringValue() - if not val: - val = u'' - return val - - def set_selectable(self, val): - self.view.setSelectable_(val) - - def set_alignment(self, alignment): - self.view.setAlignment_(alignment) - - def get_alignment(self, alignment): - return self.view.alignment() - - def set_wrap(self, wrap): - self.wrap = True - self.invalidate_size_request() - - def enable(self): - Widget.enable(self) - self.view.setTextColor_(self.__color) - self.view.setEnabled_(True) - - def disable(self): - Widget.disable(self) - self.view.setTextColor_(self.__color.colorWithAlphaComponent_(0.5)) - self.view.setEnabled_(False) - -class SolidBackground(SimpleBin): - def __init__(self, color=None): - SimpleBin.__init__(self) - self.view = FlippedView.alloc().init() - if color is not None: - self.set_background_color(color) - - def set_background_color(self, color): - self.view.setBackgroundColor_(self.make_color(color)) - -class ProgressBar(Widget): - def __init__(self): - Widget.__init__(self) - self.view = NSProgressIndicator.alloc().init() - self.view.setMaxValue_(1.0) - self.view.setIndeterminate_(False) - - def calc_size_request(self): - return 20, 20 - - def set_progress(self, fraction): - self.view.setIndeterminate_(False) - self.view.setDoubleValue_(fraction) - - def start_pulsing(self): - self.view.setIndeterminate_(True) - self.view.startAnimation_(nil) - - def stop_pulsing(self): - self.view.stopAnimation_(nil) - -class HLine(Widget): - def __init__(self): - Widget.__init__(self) - self.view = NSBox.alloc().init() - self.view.setBoxType_(NSBoxSeparator) - - def calc_size_request(self): - return self.view.frame().size.width, self.view.frame().size.height diff --git a/mvc/widgets/osx/tablemodel.py b/mvc/widgets/osx/tablemodel.py deleted file mode 100644 index 980b60b..0000000 --- a/mvc/widgets/osx/tablemodel.py +++ /dev/null @@ -1,532 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""tablemodel.py -- Model classes for TableView. """ - -import logging - -from AppKit import (NSDragOperationNone, NSDragOperationAll, NSTableViewDropOn, - NSOutlineViewDropOnItemIndex, protocols) -from Foundation import NSObject, NSNotFound, NSMutableIndexSet -from objc import YES, NO, nil - -from mvc import signals -from mvc.errors import WidgetActionError -import fasttypes -import wrappermap - -MIRO_DND_ITEM_LOCAL = 'miro-local-item' - -# XXX need unsigned but value comes out as signed. -NSDragOperationEvery = NSDragOperationAll - -def list_from_nsindexset(index_set): - rows = list() - index = index_set.firstIndex() - while (index != NSNotFound): - rows.append(index) - index = index_set.indexGreaterThanIndex_(index) - return rows - -class RowList(object): - """RowList is a Linked list that has some optimizations for looking up - rows by index number. - """ - def __init__(self): - self.list = fasttypes.LinkedList() - self.iter_cache = [] - - def firstIter(self): - return self.list.firstIter() - - def lastIter(self): - return self.list.lastIter() - - def insertBefore(self, iter, value): - self.iter_cache = [] - if iter is None: - return self.list.append(value) - else: - return self.list.insertBefore(iter, value) - - def append(self, value): - return self.list.append(value) - - def __len__(self): - return len(self.list) - - def __getitem__(self, iter): - return self.list[iter] - - def __iter__(self): - iter = self.firstIter() - while iter != self.lastIter(): - yield iter.value() - iter.forward() - - def remove(self, iter): - self.iter_cache = [] - return self.list.remove(iter) - - def nth_iter(self, index): - if index < 0: - raise IndexError(index) - elif index >= len(self): - raise LookupError() - if len(self.iter_cache) == 0: - self.iter_cache.append(self.firstIter()) - try: - return self.iter_cache[index].copy() - except IndexError: - pass - iter = self.iter_cache[-1].copy() - index -= len(self.iter_cache) - 1 - for x in xrange(index): - iter.forward() - self.iter_cache.append(iter.copy()) - return iter - -class TableModelBase(signals.SignalEmitter): - """Base class for TableModel and TreeTableModel.""" - def __init__(self, *column_types): - signals.SignalEmitter.__init__(self) - self.row_list = RowList() - self.column_types = column_types - self.create_signal('row-changed') - self.create_signal('structure-will-change') - - def check_column_values(self, column_values): - if len(self.column_types) != len(column_values): - raise ValueError("Wrong number of columns") - # We might want to do more typechecking here - - def get_column_data(self, row, column): - attr_map = column.attrs() - return dict((name, row[index]) for name, index in attr_map.items()) - - def update_value(self, iter, index, value): - iter.value().values[index] = value - self.emit('row-changed', iter) - - def update(self, iter, *column_values): - iter.value().update_values(column_values) - self.emit('row-changed', iter) - - def remove(self, iter): - self.emit('structure-will-change') - row_list = self.containing_list(iter) - rv = row_list.remove(iter) - if rv == row_list.lastIter(): - rv = None - return rv - - def nth_iter(self, index): - return self.row_list.nth_iter(index) - - def next_iter(self, iter): - row_list = self.containing_list(iter) - retval = iter.copy() - retval.forward() - if retval == row_list.lastIter(): - return None - else: - return retval - - def first_iter(self): - if len(self.row_list) > 0: - return self.row_list.firstIter() - else: - return None - - def __len__(self): - return len(self.row_list) - - def __getitem__(self, iter): - return iter.value() - - def __iter__(self): - return iter(self.row_list) - -class TableRow(object): - """See https://develop.participatoryculture.org/index.php/WidgetAPITableView for a description of the API for this class.""" - def __init__(self, column_values): - self.update_values(column_values) - - def update_values(self, column_values): - self.values = list(column_values) - - def __getitem__(self, index): - return self.values[index] - - def __len__(self): - return len(self.values) - - def __iter__(self): - return iter(self.values) - -class TableModel(TableModelBase): - """See https://develop.participatoryculture.org/index.php/WidgetAPITableView for a description of the API for this class.""" - def __init__(self, *column_types): - TableModelBase.__init__(self, column_types) - self.row_indexes = {} - - def remember_row_at_index(self, row, index): - if row not in self.row_indexes: - self.row_indexes[row] = index - - def row_of_iter(self, tableview, iter): - row = iter.value() - try: - return self.row_indexes[row] - except KeyError: - iter = self.row_list.firstIter() - index = 0 - while iter != self.row_list.lastIter(): - current_row = iter.value() - self.row_indexes[current_row] = index - if current_row is row: - return index - index += 1 - iter.forward() - raise LookupError("%s is not in this table" % row) - - def containing_list(self, iter): - return self.row_list - - def append(self, *column_values): - self.emit('structure-will-change') - self.row_indexes = {} - retval = self.row_list.append(TableRow(column_values)) - return retval - - def remove(self, iter): - self.row_indexes = {} - return TableModelBase.remove(self, iter) - - def insert_before(self, iter, *column_values): - self.emit('structure-will-change') - self.row_indexes = {} - row = TableRow(column_values) - retval = self.row_list.insertBefore(iter, row) - return retval - - def iter_for_row(self, tableview, row): - return self.row_list.nth_iter(row) - - -class TreeNode(NSObject, TableRow): - """A row in a TreeTableModel""" - - # Implementation note: these need to be NSObjects because we return them - # to the NSOutlineView. - - def initWithValues_parent_(self, column_values, parent): - self.children = RowList() - self.update_values(column_values) - self.parent = parent - return self - - @staticmethod - def create_(values, parent): - return TreeNode.alloc().initWithValues_parent_(values, parent) - - def iterchildren(self): - return iter(self.children) - -class TreeTableModel(TableModelBase): - """https://develop.participatoryculture.org/index.php/WidgetAPITableView""" - def __init__(self, *column_values): - TableModelBase.__init__(self, *column_values) - self.iter_for_item = {} - - def containing_list(self, iter): - return self.row_list_for_iter(iter.value().parent) - - def row_list_for_iter(self, iter): - """Return the rows of all direct children of iter.""" - if iter is None: - return self.row_list - else: - return iter.value().children - - def remember_iter(self, iter): - self.iter_for_item[iter.value()] = iter - return iter - - def append(self, *column_values): - self.emit('structure-will-change') - retval = self.row_list.append(TreeNode.create_(column_values, None)) - return self.remember_iter(retval) - - def forget_iter_for_item(self, item): - del self.iter_for_item[item] - for child in item.children: - self.forget_iter_for_item(child) - - def remove(self, iter): - item = iter.value() - rv = TableModelBase.remove(self, iter) - self.forget_iter_for_item(item) - return rv - - def insert_before(self, iter, *column_values): - self.emit('structure-will-change') - row = TreeNode.create_(column_values, self.parent_iter(iter)) - retval = self.containing_list(iter).insertBefore(iter, row) - return self.remember_iter(retval) - - def append_child(self, iter, *column_values): - self.emit('structure-will-change') - row_list = self.row_list_for_iter(iter) - retval = row_list.append(TreeNode.create_(column_values, iter)) - return self.remember_iter(retval) - - def child_iter(self, iter): - row_list = iter.value().children - if len(row_list) == 0: - return None - else: - return row_list.firstIter() - - def nth_child_iter(self, iter, index): - row_list = self.row_list_for_iter(iter) - return row_list.nth_iter(index) - - def has_child(self, iter): - return len(iter.value().children) > 0 - - def children_count(self, iter): - if iter is not None: - return len(iter.value().children) - else: - return len(self.row_list) - - def children_iters(self, iter): - return self.iters_in_rowlist(self.row_list_for_iter(iter)) - - def parent_iter(self, iter): - return iter.value().parent - - def iter_for_row(self, tableview, row): - item = tableview.itemAtRow_(row) - if item in self.iter_for_item: - return self.iter_for_item[item] - elif item == -1: - raise WidgetActionError("no item at row %s" % row) - else: - raise WidgetActionError("no iter for item %s at row %s" % - (repr(item), row)) - - def row_of_iter(self, tableview, iter): - item = iter.value() - row = tableview.rowForItem_(item) - if row == -1: - raise LookupError("%s is not in this table" % repr(item)) - return row - - def get_path(self, iter_): - """Not implemented (yet?) for Cocoa. Currently the only place this is - needed is tablistmanager, where the situation that uses paths results - from GTK peculiarities. - """ - return NotImplemented - -class DataSourceBase(NSObject): - def initWithModel_(self, model): - self.model = model - self.drag_source = None - self.drag_dest = None - return self - - def setDragSource_(self, drag_source): - self.drag_source = drag_source - - def setDragDest_(self, drag_dest): - self.drag_dest = drag_dest - - def view_writeColumnData_ToPasteboard_(self, view, data, pasteboard): - if not self.drag_source: - return NO - wrapper = wrappermap.wrapper(view) - drag_data = self.drag_source.begin_drag(wrapper, data) - if not drag_data: - return NO - pasteboard.declareTypes_owner_((MIRO_DND_ITEM_LOCAL,), self) - for typ, value in drag_data.items(): - stringval = repr((repr(value), typ)) - pasteboard.setString_forType_(stringval, MIRO_DND_ITEM_LOCAL) - return YES - - def calcType_(self, drag_info): - source_actions = drag_info.draggingSourceOperationMask() - if not (self.drag_dest and - (self.drag_dest.allowed_actions() | source_actions)): - return None - types = self.drag_dest.allowed_types() - available = drag_info.draggingPasteboard().availableTypeFromArray_( - (MIRO_DND_ITEM_LOCAL,)) - if available: - # XXX using eval() sucks. - data = eval(drag_info.draggingPasteboard().stringForType_( - MIRO_DND_ITEM_LOCAL)) - if data: - _, typ = data - return typ - return None - - def validateDrop_dragInfo_parentIter_position_(self, view, drag_info, - parent, position): - typ = self.calcType_(drag_info) - if typ: - wrapper = wrappermap.wrapper(view) - drop_action = self.drag_dest.validate_drop( - wrapper, self.model, typ, - drag_info.draggingSourceOperationMask(), parent, - position) - if not drop_action: - return NSDragOperationNone - if isinstance(drop_action, (tuple, list)): - drop_action, iter = drop_action - view.setDropRow_dropOperation_( - self.model.row_of_iter(view, iter), - NSTableViewDropOn) - return drop_action - else: - return NSDragOperationNone - - def acceptDrop_dragInfo_parentIter_position_(self, view, drag_info, - parent, position): - typ = self.calcType_(drag_info) - if typ: - # XXX using eval sucks. - data = eval(drag_info.draggingPasteboard().stringForType_(MIRO_DND_ITEM_LOCAL)) - ids, _ = data - ids = eval(ids) - wrapper = wrappermap.wrapper(view) - self.drag_dest.accept_drop(wrapper, self.model, typ, - drag_info.draggingSourceOperationMask(), parent, - position, ids) - return YES - else: - return NO - -class MiroTableViewDataSource(DataSourceBase, protocols.NSTableDataSource): - def numberOfRowsInTableView_(self, table_view): - return len(self.model) - - def tableView_objectValueForTableColumn_row_(self, table_view, column, row): - node = self.model.nth_iter(row).value() - self.model.remember_row_at_index(node, row) - return self.model.get_column_data(node.values, column) - - def tableView_writeRowsWithIndexes_toPasteboard_(self, tableview, rowIndexes, - pasteboard): - indexes = list_from_nsindexset(rowIndexes) - data = [self.model[self.model.nth_iter(i)] for i in indexes] - return self.view_writeColumnData_ToPasteboard_(tableview, data, - pasteboard) - - def translateRow_operation_(self, row, operation): - if operation == NSTableViewDropOn: - return self.model.nth_iter(row), -1 - else: - return None, row - - def tableView_validateDrop_proposedRow_proposedDropOperation_(self, - tableview, drag_info, row, operation): - parent, position = self.translateRow_operation_(row, operation) - drop_action = self.validateDrop_dragInfo_parentIter_position_(tableview, - drag_info, parent, position) - if isinstance(drop_action, (list, tuple)): - # XXX nothing uses this yet - drop_action, iter = drop_action - tableview.setDropRow_dropOperation_( - self.model.row_of_iter(tableview, iter), - NSTableViewDropOn) - return drop_action - - def tableView_acceptDrop_row_dropOperation_(self, - tableview, drag_info, row, operation): - parent, position = self.translateRow_operation_(row, operation) - return self.acceptDrop_dragInfo_parentIter_position_(tableview, - drag_info, parent, position) - - -class MiroOutlineViewDataSource(DataSourceBase, protocols.NSOutlineViewDataSource): - def outlineView_child_ofItem_(self, view, child, item): - if item is nil: - row_list = self.model.row_list - else: - row_list = item.children - return row_list.nth_iter(child).value() - - def outlineView_isItemExpandable_(self, view, item): - if item is not nil and hasattr(item, 'children'): - return len(item.children) > 0 - else: - return len(self.model) > 0 - - def outlineView_numberOfChildrenOfItem_(self, view, item): - if item is not nil and hasattr(item, 'children'): - return len(item.children) - else: - return len(self.model) - - def outlineView_objectValueForTableColumn_byItem_(self, view, column, - item): - return self.model.get_column_data(item.values, column) - - def outlineView_writeItems_toPasteboard_(self, outline_view, items, - pasteboard): - data = [i.values for i in items] - return self.view_writeColumnData_ToPasteboard_(outline_view, data, - pasteboard) - - def outlineView_validateDrop_proposedItem_proposedChildIndex_(self, - outlineview, drag_info, item, child_index): - if item is None: - iter = None - else: - iter = self.model.iter_for_item[item] - drop_action = self.validateDrop_dragInfo_parentIter_position_( - outlineview, drag_info, iter, child_index) - if isinstance(drop_action, (tuple, list)): - drop_action, iter = drop_action - outlineview.setDropItem_dropChildIndex_( - iter.value(), NSOutlineViewDropOnItemIndex) - return drop_action - - def outlineView_acceptDrop_item_childIndex_(self, outlineview, drag_info, - item, child_index): - if item is None: - iter = None - else: - iter = self.model.iter_for_item[item] - return self.acceptDrop_dragInfo_parentIter_position_(outlineview, - drag_info, iter, child_index) diff --git a/mvc/widgets/osx/tableview.py b/mvc/widgets/osx/tableview.py deleted file mode 100644 index 2d2256f..0000000 --- a/mvc/widgets/osx/tableview.py +++ /dev/null @@ -1,1629 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -""".tableview -- TableView widget and it's -associated classes. -""" - -import math -import logging -from contextlib import contextmanager -from collections import namedtuple - -from AppKit import * -from Foundation import * -from objc import YES, NO, nil - -from mvc import signals -from mvc import errors -from mvc.widgets import widgetconst -from mvc.widgets.tableselection import SelectionOwnerMixin -from mvc.widgets.tablescroll import ScrollbarOwnerMixin -from .utils import filename_to_unicode -import wrappermap -import tablemodel -import osxmenus -from .base import Widget -from .simple import Image -from .drawing import DrawingContext, DrawingStyle, Gradient, ImageSurface -from .helpers import NotificationForwarder -from .layoutmanager import LayoutManager - -EXPANDER_PADDING = 6 -HEADER_HEIGHT = 17 -CUSTOM_HEADER_HEIGHT = 25 - -def iter_range(ns_range): - """Iterate over an NSRange object""" - return xrange(ns_range.location, ns_range.location + ns_range.length) - -Rect = namedtuple('Rect', 'x y width height') -def NSRectToRect(nsrect): - origin, size = nsrect.origin, nsrect.size - return Rect(origin.x, origin.y, size.width, size.height) - -Point = namedtuple('Point', 'x y') -def NSPointToPoint(nspoint): - return Point(int(nspoint.x), int(nspoint.y)) - -class HotspotTracker(object): - """Contains the info on the currently tracked hotspot. See: - https://develop.participatoryculture.org/index.php/WidgetAPITableView - """ - def __init__(self, tableview, point): - self.tableview = tableview - self.row = tableview.rowAtPoint_(point) - self.column = tableview.columnAtPoint_(point) - if self.row == -1 or self.column == -1: - self.hit = False - return - model = tableview.dataSource().model - self.iter = model.iter_for_row(tableview, self.row) - self.table_column = tableview.tableColumns()[self.column] - self.cell = self.table_column.dataCell() - self.update_position(point) - if isinstance(self.cell, CustomTableCell): - self.name = self.calc_hotspot() - else: - self.name = None - self.hit = (self.name is not None) - - def is_for_context_menu(self): - return self.name == '#show-context-menu' - - def calc_cell_hotspot(self, column, row): - if (self.hit and self.column == column and self.row == row): - return self.name - else: - return None - - def update_position(self, point): - cell_frame = self.tableview.frameOfCellAtColumn_row_(self.column, - self.row) - self.pos = NSPoint(point.x - cell_frame.origin.x, - point.y - cell_frame.origin.y) - - def update_hit(self): - old_hit = self.hit - self.hit = (self.calc_hotspot() == self.name) - if old_hit != self.hit: - self.redraw_cell() - - def set_cell_data(self): - model = self.tableview.dataSource().model - row = model[self.iter] - value_dict = model.get_column_data(row, self.table_column) - self.cell.setObjectValue_(value_dict) - self.cell.set_wrapper_data() - - def calc_hotspot(self): - self.set_cell_data() - cell_frame = self.tableview.frameOfCellAtColumn_row_(self.column, - self.row) - style = self.cell.make_drawing_style(cell_frame, self.tableview) - layout_manager = self.cell.layout_manager - layout_manager.reset() - return self.cell.wrapper.hotspot_test(style, layout_manager, - self.pos.x, self.pos.y, cell_frame.size.width, - cell_frame.size.height) - - def redraw_cell(self): - # Check to see if we removed the table in response to a hotspot click. - if self.tableview.superview() is not nil: - cell_frame = self.tableview.frameOfCellAtColumn_row_(self.column, - self.row) - self.tableview.setNeedsDisplayInRect_(cell_frame) - -def _calc_interior_frame(total_frame, tableview): - """Calculate the inner cell area for a table cell. - - We tell cocoa that the intercell spacing is (0, 0) and instead handle the - spacing ourselves. This method calculates the area that a cell should - render to, given the total spacing. - """ - return NSMakeRect(total_frame.origin.x + tableview.column_spacing // 2, - total_frame.origin.y + tableview.row_spacing // 2, - total_frame.size.width - tableview.column_spacing, - total_frame.size.height - tableview.row_spacing) - -class MiroTableCell(NSTextFieldCell): - def init(self): - return super(MiroTableCell, self).initTextCell_('') - - def calcHeight_(self, view): - font = self.font() - return math.ceil(font.ascender() + abs(font.descender()) + - font.leading()) - - def highlightColorWithFrame_inView_(self, frame, view): - return nil - - def setObjectValue_(self, value_dict): - if isinstance(value_dict, dict): - NSCell.setObjectValue_(self, value_dict['value']) - else: - # OS X calls setObjectValue_('') on intialization - NSCell.setObjectValue_(self, value_dict) - - def drawInteriorWithFrame_inView_(self, frame, view): - return NSTextFieldCell.drawInteriorWithFrame_inView_(self, - _calc_interior_frame(frame, view), view) - -class MiroTableInfoListTextCell(MiroTableCell): - def initWithAttrGetter_(self, attr_getter): - self = self.init() - self.setWraps_(NO) - self.attr_getter = attr_getter - self._textColor = self.textColor() - return self - - def drawWithFrame_inView_(self, frame, view): - # adjust frame based on the cell spacing - frame = _calc_interior_frame(frame, view) - if (self.isHighlighted() and frame is not None and - (view.isDescendantOf_(view.window().firstResponder()) or - view.gradientHighlight) and view.window().isMainWindow()): - self.setTextColor_(NSColor.whiteColor()) - else: - self.setTextColor_(self._textColor) - return MiroTableCell.drawWithFrame_inView_(self, frame, view) - - def titleRectForBounds_(self, rect): - frame = MiroTableCell.titleRectForBounds_(self, rect) - text_size = self.attributedStringValue().size() - frame.origin.y = rect.origin.y + (rect.size.height - text_size.height) / 2.0 - return frame - - def drawInteriorWithFrame_inView_(self, frame, view): - rect = self.titleRectForBounds_(frame) - self.attributedStringValue().drawInRect_(rect) - - def setObjectValue_(self, value): - if isinstance(value, tuple): - info, attrs, group_info = value - cell_text = self.attr_getter(info) - NSCell.setObjectValue_(self, cell_text) - else: - # Getting set to a something other than a model row, usually this - # happens in initialization - NSCell.setObjectValue_(self, '') - -class MiroTableImageCell(NSImageCell): - def calcHeight_(self, view): - return self.value_dict['image'].size().height - - def highlightColorWithFrame_inView_(self, frame, view): - return nil - - def setObjectValue_(self, value_dict): - NSImageCell.setObjectValue_(self, value_dict['image']) - - def drawInteriorWithFrame_inView_(self, frame, view): - return NSImageCell.drawInteriorWithFrame_inView_(self, - _calc_interior_frame(frame, view), view) - -class MiroCheckboxCell(NSButtonCell): - def init(self): - self = super(MiroCheckboxCell, self).init() - self.setButtonType_(NSSwitchButton) - self.setTitle_('') - return self - - def calcHeight_(self, view): - return self.cellSize().height - - def highlightColorWithFrame_inView_(self, frame, view): - return nil - - def setObjectValue_(self, value_dict): - if isinstance(value_dict, dict): - NSButtonCell.setObjectValue_(self, value_dict['value']) - else: - # OS X calls setObjectValue_('') on intialization - NSCell.setObjectValue_(self, value_dict) - - def startTrackingAt_inView_(self, point, view): - return YES - - def continueTracking_at_inView_(self, lastPoint, at, view): - return YES - - def stopTracking_at_inView_mouseIsUp_(self, lastPoint, at, tableview, mouseIsUp): - if mouseIsUp: - column = tableview.columnAtPoint_(at) - row = tableview.rowAtPoint_(at) - if column != -1 and row != -1: - wrapper = wrappermap.wrapper(tableview) - column = wrapper.columns[column] - itr = wrapper.model.iter_for_row(tableview, row) - column.renderer.emit('clicked', itr) - return NSButtonCell.stopTracking_at_inView_mouseIsUp_(self, lastPoint, - at, tableview, mouseIsUp) - - def drawInteriorWithFrame_inView_(self, frame, view): - return NSButtonCell.drawInteriorWithFrame_inView_(self, - _calc_interior_frame(frame, view), view) - -class CellRendererBase(object): - DRAW_BACKGROUND = True - - def set_index(self, index): - self.index = index - - def get_index(self): - return self.index - -class CellRenderer(CellRendererBase): - def __init__(self): - self.cell = self.build_cell() - self._font_scale_factor = 1.0 - self._font_bold = False - self.set_align('left') - - def build_cell(self): - return MiroTableCell.alloc().init() - - def setDataCell_(self, column): - column.setDataCell_(self.cell) - - def set_text_size(self, size): - if size == widgetconst.SIZE_NORMAL: - self._font_scale_factor = 1.0 - elif size == widgetconst.SIZE_SMALL: - # make the scale factor such so that the font size is 11.0 - self._font_scale_factor = 11.0 / NSFont.systemFontSize() - else: - raise ValueError("Unknown size: %s" % size) - self._set_font() - - def set_font_scale(self, scale_factor): - self._font_scale_factor = scale_factor - self._set_font() - - def set_bold(self, bold): - self._font_bold = bold - self._set_font() - - def _set_font(self): - size = NSFont.systemFontSize() * self._font_scale_factor - if self._font_bold: - font = NSFont.boldSystemFontOfSize_(size) - else: - font = NSFont.systemFontOfSize_(size) - self.cell.setFont_(font) - - def set_color(self, color): - color = NSColor.colorWithDeviceRed_green_blue_alpha_(color[0], - color[1], color[2], 1.0) - self.cell._textColor = color - self.cell.setTextColor_(color) - - def set_align(self, align): - if align == 'left': - ns_alignment = NSLeftTextAlignment - elif align == 'center': - ns_alignment = NSCenterTextAlignment - elif align == 'right': - ns_alignment = NSRightTextAlignment - else: - raise ValueError("unknown alignment: %s", align) - self.cell.setAlignment_(ns_alignment) - -class ImageCellRenderer(CellRendererBase): - def setDataCell_(self, column): - column.setDataCell_(MiroTableImageCell.alloc().init()) - -class CheckboxCellRenderer(CellRendererBase, signals.SignalEmitter): - def __init__(self): - signals.SignalEmitter.__init__(self, 'clicked') - self.size = widgetconst.SIZE_NORMAL - - def set_control_size(self, size): - self.size = size - - def setDataCell_(self, column): - cell = MiroCheckboxCell.alloc().init() - if self.size == widgetconst.SIZE_SMALL: - cell.setControlSize_(NSSmallControlSize) - column.setDataCell_(cell) - -class CustomTableCell(NSCell): - def init(self): - self = super(CustomTableCell, self).init() - self.layout_manager = LayoutManager() - self.hotspot = None - self.default_drawing_style = DrawingStyle() - return self - - def highlightColorWithFrame_inView_(self, frame, view): - return nil - - def calcHeight_(self, view): - self.layout_manager.reset() - self.set_wrapper_data() - cell_size = self.wrapper.get_size(self.default_drawing_style, - self.layout_manager) - return cell_size[1] - - def make_drawing_style(self, frame, view): - text_color = None - if (self.isHighlighted() and frame is not None and - (view.isDescendantOf_(view.window().firstResponder()) or - view.gradientHighlight) and view.window().isMainWindow()): - text_color = NSColor.whiteColor() - return DrawingStyle(text_color=text_color) - - def drawInteriorWithFrame_inView_(self, frame, view): - NSGraphicsContext.currentContext().saveGraphicsState() - if not self.wrapper.IGNORE_PADDING: - # adjust frame based on the cell spacing. We also have to adjust - # the hover position to account for the new frame - original_frame = frame - frame = _calc_interior_frame(frame, view) - hover_adjustment = (frame.origin.x - original_frame.origin.x, - frame.origin.y - original_frame.origin.y) - else: - hover_adjustment = (0, 0) - if self.wrapper.outline_column: - pad_left = EXPANDER_PADDING - else: - pad_left = 0 - drawing_rect = NSMakeRect(frame.origin.x + pad_left, frame.origin.y, - frame.size.width - pad_left, frame.size.height) - context = DrawingContext(view, drawing_rect, drawing_rect) - context.style = self.make_drawing_style(frame, view) - self.layout_manager.reset() - self.set_wrapper_data() - column = self.wrapper.get_index() - hover_pos = view.get_hover(self.row, column) - if hover_pos is not None: - hover_pos = [hover_pos[0] - hover_adjustment[0], - hover_pos[1] - hover_adjustment[1]] - self.wrapper.render(context, self.layout_manager, self.isHighlighted(), - self.hotspot, hover_pos) - NSGraphicsContext.currentContext().restoreGraphicsState() - - def setObjectValue_(self, value): - self.object_value = value - - def set_wrapper_data(self): - self.wrapper.__dict__.update(self.object_value) - -class CustomCellRenderer(CellRendererBase): - CellClass = CustomTableCell - - IGNORE_PADDING = False - - def __init__(self): - self.outline_column = False - self.index = None - - def setDataCell_(self, column): - # Note that the ownership is the opposite of what happens in widgets. - # The NSObject owns it's wrapper widget. This happens for a couple - # reasons: - # 1) The data cell gets copied a bunch of times, so wrappermap won't - # work with it. - # 2) The Wrapper should only needs to stay around as long as the - # NSCell that it's wrapping is around. Once the column gets removed - # from the table, the wrapper can be deleted. - nscell = self.CellClass.alloc().init() - nscell.wrapper = self - column.setDataCell_(nscell) - - def hotspot_test(self, style, layout, x, y, width, height): - return None - -class InfoListTableCell(CustomTableCell): - def set_wrapper_data(self): - self.wrapper.info, self.wrapper.attrs, self.wrapper.group_info = \ - self.object_value - -class InfoListRenderer(CustomCellRenderer): - CellClass = InfoListTableCell - - def hotspot_test(self, style, layout, x, y, width, height): - return None - -class InfoListRendererText(CellRenderer): - def build_cell(self): - cell = MiroTableInfoListTextCell.alloc() - return cell.initWithAttrGetter_(self.get_value) - -def calc_row_height(view, model_row): - max_height = 0 - model = view.dataSource().model - for column in view.tableColumns(): - cell = column.dataCell() - data = model.get_column_data(model_row, column) - cell.setObjectValue_(data) - cell_height = cell.calcHeight_(view) - max_height = max(max_height, cell_height) - if max_height == 0: - max_height = 12 - return max_height + view.row_spacing - -class TableViewDelegate(NSObject): - def tableView_willDisplayCell_forTableColumn_row_(self, view, cell, - column, row): - column = view.column_index_map[column] - cell.column = column - cell.row = row - if view.hotspot_tracker: - cell.hotspot = view.hotspot_tracker.calc_cell_hotspot(column, row) - else: - cell.hotspot = None - - def tableView_didClickTableColumn_(self, tableview, column): - wrapper = wrappermap.wrapper(tableview) - for column_wrapper in wrapper.columns: - if column_wrapper._column is column: - column_wrapper.emit('clicked') - - def tableView_toolTipForCell_rect_tableColumn_row_mouseLocation_(self, tableview, cell, rect, column, row, location): - wrapper = wrappermap.wrapper(tableview) - iter = tableview.dataSource().model.iter_for_row(tableview, row) - for wrapper_column in wrapper.columns: - if wrapper_column._column is column: - break - return (wrapper.get_tooltip(iter, wrapper_column), rect) - -class VariableHeightTableViewDelegate(TableViewDelegate): - def tableView_heightOfRow_(self, table_view, row): - model = table_view.dataSource().model - iter = model.iter_for_row(table_view, row) - if iter is None: - return 12 - return calc_row_height(table_view, model[iter]) - - -# TableViewCommon is a hack to do a Mixin class. We want the same behaviour -# for our table views and our outline views. Normally we would use a Mixin, -# but that doesn't work with pyobjc. Instead we define the common code in -# TableViewCommon, then copy it into MiroTableView and MiroOutlineView - -class TableViewCommon(object): - def init(self): - self = super(self.__class__, self).init() - self.hotspot_tracker = None - self._tracking_rects = [] - self.hover_info = None - self.column_index_map = {} - self.setFocusRingType_(NSFocusRingTypeNone) - self.handled_last_mouse_down = False - self.gradientHighlight = False - self.tracking_area = None - self.group_lines_enabled = False - self.group_line_width = 1 - self.group_line_color = (0, 0, 0, 1.0) - # we handle cell spacing manually - self.setIntercellSpacing_(NSSize(0, 0)) - self.column_spacing = 3 - self.row_spacing = 2 - return self - - def updateTrackingAreas(self): - # remove existing tracking area if needed - if self.tracking_area: - self.removeTrackingArea_(self.tracking_area) - - # create a new tracking area for the entire view. This allows us to - # get mouseMoved events whenever the mouse is inside our view. - self.tracking_area = NSTrackingArea.alloc() - self.tracking_area.initWithRect_options_owner_userInfo_( - self.visibleRect(), - NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | - NSTrackingActiveInKeyWindow, - self, - nil) - self.addTrackingArea_(self.tracking_area) - - def addTableColumn_(self, column): - index = len(self.tableColumns()) - column.set_index(index) - self.column_index_map[column._column] = index - self.SuperClass.addTableColumn_(self, column._column) - - def removeTableColumn_(self, column): - self.SuperClass.removeTableColumn_(self, column) - removed = self.column_index_map.pop(column) - for key, index in self.column_index_map.items(): - if index > removed: - self.column_index_map[key] -= 1 - - def moveColumn_toColumn_(self, src, dest): - # Need to switch the TableColumn objects too - columns = wrappermap.wrapper(self).columns - columns[src], columns[dest] = columns[dest], columns[src] - for index, column in enumerate(columns): - column.set_index(index) - self.SuperClass.moveColumn_toColumn_(self, src, dest) - - def highlightSelectionInClipRect_(self, rect): - if wrappermap.wrapper(self).draws_selection: - if not self.gradientHighlight: - return self.SuperClass.highlightSelectionInClipRect_(self, - rect) - context = NSGraphicsContext.currentContext() - focused = self.isDescendantOf_(self.window().firstResponder()) - for row in tablemodel.list_from_nsindexset(self.selectedRowIndexes()): - self.drawBackgroundGradient(context, focused, row) - - def setFrameSize_(self, size): - if size.height == 0: - size.height = 4 - self.SuperClass.setFrameSize_(self, size) - - def drawBackgroundGradient(self, context, focused, row): - widget = wrappermap.wrapper(self) - window = widget.get_window() - if window and window.is_active(): - if focused: - start_color = (0.588, 0.717, 0.843) - end_color = (0.416, 0.568, 0.713) - top_line_color = (0.416, 0.569, 0.714, 1.0) - bottom_line_color = (0.416, 0.569, 0.714, 1.0) - else: - start_color = (168 / 255.0, 188 / 255.0, 208 / 255.0) - end_color = (129 / 255.0, 152 / 255.0, 176 / 255.0) - top_line_color = (129 / 255.0, 152 / 255.0, 175 / 255.0, 1.0) - bottom_line_color = (0.416, 0.569, 0.714, 1.0) - else: - start_color = (0.675, 0.722, 0.765) - end_color = (0.592, 0.659, 0.710) - top_line_color = (0.596, 0.635, 0.671, 1.0) - bottom_line_color = (0.522, 0.576, 0.620, 1.0) - - rect = self.rectOfRow_(row) - top = NSMakeRect(rect.origin.x, rect.origin.y, rect.size.width, 1) - context.saveGraphicsState() - # draw the top line - NSColor.colorWithDeviceRed_green_blue_alpha_(*top_line_color).set() - NSRectFill(top) - bottom = NSMakeRect(rect.origin.x, rect.origin.y + rect.size.height - 2, - rect.size.width, 1) - NSColor.colorWithDeviceRed_green_blue_alpha_(*bottom_line_color).set() - NSRectFill(bottom) - highlight = NSMakeRect(rect.origin.x, rect.origin.y + rect.size.height - 1, - rect.size.width, 1) - NSColor.colorWithDeviceRed_green_blue_alpha_(0.918, 0.925, 0.941, 1.0).set() - NSRectFill(highlight) - - # draw the gradient - rect.origin.y += 1 - rect.size.height -= 3 - NSRectClip(rect) - gradient = Gradient(rect.origin.x, rect.origin.y, - rect.origin.x, rect.origin.y + rect.size.height) - gradient.set_start_color(start_color) - gradient.set_end_color(end_color) - gradient.draw() - context.restoreGraphicsState() - - def drawBackgroundInClipRect_(self, clip_rect): - # save our graphics state, since we are about to modify the clip path - graphics_context = NSGraphicsContext.currentContext() - graphics_context.saveGraphicsState() - # create a NSBezierPath that contains the rects of the columns with - # DRAW_BACKGROUND True. - clip_path = NSBezierPath.bezierPath() - number_of_columns = len(self.tableColumns()) - for col_index in iter_range(self.columnsInRect_(clip_rect)): - column = wrappermap.wrapper(self.tableColumns()[col_index]) - column_rect = None - if column.renderer.DRAW_BACKGROUND: - # We should draw the background for this column, add it's rect - # to our clip rect. - column_rect = self.rectOfColumn_(col_index) - clip_path.appendBezierPathWithRect_(column_rect) - else: - # We shouldn't draw the background for this column. Don't add - # anything to the clip rect, but do draw the area before the - # first row and after the last row. - self.drawBackgroundOutsideContent_clipRect_(col_index, - clip_rect) - if col_index == number_of_columns - 1: # last column - if not column_rect: - column_rect = self.rectOfColumn_(col_index) - column_right = column_rect.origin.x + column_rect.size.width - clip_right = clip_rect.origin.x + clip_rect.size.width - if column_right < clip_right: - # there's space to the right, so add that to the clip_rect - remaining = clip_right - column_right - left_rect = NSMakeRect(column_right, clip_rect.origin.y, - remaining, clip_rect.size.height) - clip_path.appendBezierPathWithRect_(left_rect) - # clip to that path - clip_path.addClip() - # do the default drawing - self.SuperClass.drawBackgroundInClipRect_(self, clip_rect) - # restore graphics state - graphics_context.restoreGraphicsState() - - def drawBackgroundOutsideContent_clipRect_(self, index, clip_rect): - """Draw our background outside the rows with content - - We call this for cells with DRAW_BACKGROUND set to False. For those, - we let the cell draw their own background, but we still need to draw - the background before the first cell and after the last cell. - """ - - self.backgroundColor().set() - - total_rect = NSIntersectionRect(self.rectOfColumn_(index), clip_rect) - - if self.numberOfRows() == 0: - # if no rows are selected, draw the background over everything - NSRectFill(total_rect) - return - - # fill the area above the first row - first_row_rect = self.rectOfRow_(0) - if first_row_rect.origin.y > total_rect.origin.y: - height = first_row_rect.origin.y - total_rect.origin.y - NSRectFill(NSMakeRect(total_rect.origin.x, total_rect.origin.y, - total_rect.size.width, height)) - - # fill the area below the last row - last_row_rect = self.rectOfRow_(self.numberOfRows()-1) - if NSMaxY(last_row_rect) < NSMaxY(total_rect): - y = NSMaxY(last_row_rect) + 1 - height = NSMaxY(total_rect) - NSMaxY(last_row_rect) - NSRectFill(NSMakeRect(total_rect.origin.x, y, - total_rect.size.width, height)) - - def drawRow_clipRect_(self, row, clip_rect): - self.SuperClass.drawRow_clipRect_(self, row, clip_rect) - if self.group_lines_enabled: - self.drawGroupLine_(row) - - def drawGroupLine_(self, row): - infolist = wrappermap.wrapper(self).model - if (not isinstance(infolist, tablemodel.InfoListModel) or - infolist.get_grouping() is None): - return - - info, attrs, group_info = infolist[row] - if group_info[0] == group_info[1] - 1: - rect = self.rectOfRow_(row) - rect.origin.y = NSMaxY(rect) - self.group_line_width - rect.size.height = self.group_line_width - NSColor.colorWithDeviceRed_green_blue_alpha_( - *self.group_line_color).set() - NSRectFill(rect) - - def canDragRowsWithIndexes_atPoint_(self, indexes, point): - return YES - - def draggingSourceOperationMaskForLocal_(self, local): - drag_source = wrappermap.wrapper(self).drag_source - if drag_source and local: - return drag_source.allowed_actions() - return NSDragOperationNone - - def mouseMoved_(self, event): - location = self.convertPoint_fromView_(event.locationInWindow(), nil) - row = self.rowAtPoint_(location) - column = self.columnAtPoint_(location) - if (self.hover_info is not None and self.hover_info != (row, column)): - # left a cell, redraw it the old one - rect = self.frameOfCellAtColumn_row_(self.hover_info[1], - self.hover_info[0]) - self.setNeedsDisplayInRect_(rect) - if row == -1 or column == -1: - # corner case: we got a mouseMoved_ event, but the pointer is - # outside the view - self.hover_pos = self.hover_info = None - return - # queue a redraw on the cell currently hovered over - rect = self.frameOfCellAtColumn_row_(column, row) - self.setNeedsDisplayInRect_(rect) - # recalculate hover_pos and hover_info - self.hover_pos = (location[0] - rect[0][0], - location[0] - rect[0][1]) - self.hover_info = (row, column) - - def mouseExited_(self, event): - if self.hover_info: - # mouse left our window, unset hover and redraw the cell that the - # mouse was in - rect = self.frameOfCellAtColumn_row_(self.hover_info[1], - self.hover_info[0]) - self.setNeedsDisplayInRect_(rect) - self.hover_pos = self.hover_info = None - - def get_hover(self, row, column): - if self.hover_info == (row, column): - return self.hover_pos - else: - return None - - def mouseDown_(self, event): - if event.modifierFlags() & NSControlKeyMask: - self.handleContextMenu_(event) - self.handled_last_mouse_down = True - return - - point = self.convertPoint_fromView_(event.locationInWindow(), nil) - - if event.clickCount() == 2: - if self.handled_last_mouse_down: - return - wrapper = wrappermap.wrapper(self) - row = self.rowAtPoint_(point) - if (row != -1 and self.point_should_click(point, row)): - iter = wrapper.model.iter_for_row(self, row) - wrapper.emit('row-activated', iter) - return - - # Like clickCount() == 2 but keep running so we can get to run the - # hotspot tracker et al. - if event.clickCount() == 1: - wrapper = wrappermap.wrapper(self) - row = self.rowAtPoint_(point) - if (row != -1 and self.point_should_click(point, row)): - - iter = wrapper.model.iter_for_row(self, row) - wrapper.emit('row-clicked', iter) - - hotspot_tracker = HotspotTracker(self, point) - if hotspot_tracker.hit: - self.hotspot_tracker = hotspot_tracker - self.hotspot_tracker.redraw_cell() - self.handled_last_mouse_down = True - if hotspot_tracker.is_for_context_menu(): - self.popup_context_menu(self.hotspot_tracker.row, event) - # once we're out of that call, we know the context menu is - # gone - hotspot_tracker.redraw_cell() - self.hotspot_tracker = None - else: - self.handled_last_mouse_down = False - self.SuperClass.mouseDown_(self, event) - - def point_should_click(self, point, row): - """Should a click on a point result in a row-clicked signal? - - Subclasses can override if not every point should result in a click. - """ - return True - - def rightMouseDown_(self, event): - self.handleContextMenu_(event) - - def handleContextMenu_(self, event): - self.window().makeFirstResponder_(self) - point = self.convertPoint_fromView_(event.locationInWindow(), nil) - column = self.columnAtPoint_(point) - row = self.rowAtPoint_(point) - if self.group_lines_enabled and column == 0: - self.selectAllItemsInGroupForRow_(row) - self.popup_context_menu(row, event) - - def selectAllItemsInGroupForRow_(self, row): - wrapper = wrappermap.wrapper(self) - infolist = wrapper.model - if (not isinstance(infolist, tablemodel.InfoListModel) or - infolist.get_grouping() is None): - return - - info, attrs, group_info = infolist[row] - select_range = NSMakeRange(row - group_info[0], group_info[1]) - index_set = NSIndexSet.indexSetWithIndexesInRange_(select_range) - self.selectRowIndexes_byExtendingSelection_(index_set, NO) - - def popup_context_menu(self, row, event): - selection = self.selectedRowIndexes() - if row != -1 and not selection.containsIndex_(row): - index_set = NSIndexSet.alloc().initWithIndex_(row) - self.selectRowIndexes_byExtendingSelection_(index_set, NO) - wrapper = wrappermap.wrapper(self) - if wrapper.context_menu_callback is not None: - menu_items = wrapper.context_menu_callback(wrapper) - menu = osxmenus.make_context_menu(menu_items) - NSMenu.popUpContextMenu_withEvent_forView_(menu, event, self) - - def mouseDragged_(self, event): - if self.hotspot_tracker is not None: - point = self.convertPoint_fromView_(event.locationInWindow(), nil) - self.hotspot_tracker.update_position(point) - self.hotspot_tracker.update_hit() - else: - self.SuperClass.mouseDragged_(self, event) - - def mouseUp_(self, event): - if self.hotspot_tracker is not None: - point = self.convertPoint_fromView_(event.locationInWindow(), nil) - self.hotspot_tracker.update_position(point) - self.hotspot_tracker.update_hit() - if self.hotspot_tracker.hit: - wrappermap.wrapper(self).send_hotspot_clicked() - if self.hotspot_tracker: - self.hotspot_tracker.redraw_cell() - self.hotspot_tracker = None - else: - self.SuperClass.mouseUp_(self, event) - - def keyDown_(self, event): - mods = osxmenus.translate_event_modifiers(event) - if event.charactersIgnoringModifiers() == ' ' and len(mods) == 0: - # handle spacebar with no modifiers by sending the row-activated - # signal - wrapper = wrappermap.wrapper(self) - row = self.selectedRow() - if row >= 0: - iter = wrapper.model.iter_for_row(self, row) - wrapper.emit('row-activated', iter) - else: - self.SuperClass.keyDown_(self, event) - -class TableColumn(signals.SignalEmitter): - def __init__(self, title, renderer, header=None, **attrs): - signals.SignalEmitter.__init__(self) - self.create_signal('clicked') - self._column = MiroTableColumn.alloc().initWithIdentifier_(title) - self._column.set_attrs(attrs) - wrappermap.add(self._column, self) - header_cell = MiroTableHeaderCell.alloc().init() - self.custom_header = False - if header: - header_cell.set_widget(header) - self.custom_header = True - self._column.setHeaderCell_(header_cell) - self._column.headerCell().setStringValue_(title) - self._column.setEditable_(NO) - self._column.setResizingMask_(NSTableColumnNoResizing) - self.renderer = renderer - self.sort_order_ascending = True - self.sort_indicator_visible = False - self.do_horizontal_padding = True - self.min_width = self.max_width = None - renderer.setDataCell_(self._column) - - def set_do_horizontal_padding(self, horizontal_padding): - self.do_horizontal_padding = horizontal_padding - - def set_right_aligned(self, right_aligned): - if right_aligned: - self._column.headerCell().setAlignment_(NSRightTextAlignment) - else: - self._column.headerCell().setAlignment_(NSLeftTextAlignment) - - def set_min_width(self, width): - self.min_width = width - - def set_max_width(self, width): - self.max_width = width - - def set_width(self, width): - self._column.setWidth_(width) - - def get_width(self): - return self._column.width() - - def set_resizable(self, resizable): - mask = 0 - if resizable: - mask |= NSTableColumnUserResizingMask - self._column.setResizingMask_(mask) - - def set_sort_indicator_visible(self, visible): - self.sort_indicator_visible = visible - self._column.tableView().headerView().setNeedsDisplay_(True) - - def get_sort_indicator_visible(self): - return self.sort_indicator_visible - - def set_sort_order(self, ascending): - self.sort_order_ascending = ascending - self._column.tableView().headerView().setNeedsDisplay_(True) - - def get_sort_order_ascending(self): - return self.sort_order_ascending - - def set_index(self, index): - self.index = index - self.renderer.set_index(index) - -class MiroTableColumn(NSTableColumn): - def set_attrs(self, attrs): - self._attrs = attrs - - def attrs(self): - return self._attrs - -class MiroTableView(NSTableView): - SuperClass = NSTableView - for name, value in TableViewCommon.__dict__.items(): - locals()[name] = value - -class MiroTableHeaderView(NSTableHeaderView): - def initWithFrame_(self, frame): - # frame is not used - self = super(MiroTableHeaderView, self).initWithFrame_(frame) - self.selected = None - self.custom_header = False - return self - - def drawRect_(self, rect): - wrapper = wrappermap.wrapper(self.tableView()) - if self.selected: - self.selected.set_selected(False) - for column in wrapper.columns: - if column.sort_indicator_visible: - self.selected = column._column.headerCell() - self.selected.set_selected(True) - self.selected.set_ascending(column.sort_order_ascending) - break - NSTableHeaderView.drawRect_(self, rect) - if self.custom_header: - NSGraphicsContext.currentContext().saveGraphicsState() - # Draw the separator between the header and the contents. - context = DrawingContext(self, rect, rect) - context.set_line_width(1) - context.set_color((2 / 255.0, 2 / 255.0, 2 / 255.0)) - context.move_to(0, context.height - 0.5) - context.rel_line_to(context.width, 0) - context.stroke() - NSGraphicsContext.currentContext().restoreGraphicsState() - -class MiroTableHeaderCell(NSTableHeaderCell): - def init(self): - self = super(MiroTableHeaderCell, self).init() - self.layout_manager = LayoutManager() - self.button = None - return self - - def set_selected(self, selected): - self.button._enabled = selected - - def set_ascending(self, ascending): - self.button._ascending = ascending - - def set_widget(self, widget): - self.button = widget - - def drawWithFrame_inView_(self, frame, view): - if self.button is None: - # use the default behavior when set_widget hasn't been called - return NSTableHeaderCell.drawWithFrame_inView_(self, frame, view) - - NSGraphicsContext.currentContext().saveGraphicsState() - drawing_rect = NSMakeRect(frame.origin.x, frame.origin.y, - frame.size.width, frame.size.height) - context = DrawingContext(view, drawing_rect, drawing_rect) - context.style = self.make_drawing_style(frame, view) - self.layout_manager.reset() - columns = wrappermap.wrapper(view.tableView()).columns - header_cells = [c._column.headerCell() for c in columns] - background_only = not self in header_cells - self.button.draw(context, self.layout_manager, background_only) - NSGraphicsContext.currentContext().restoreGraphicsState() - - def make_drawing_style(self, frame, view): - text_color = None - if (self.isHighlighted() and frame is not None and - (view.isDescendantOf_(view.window().firstResponder()) or - view.gradientHighlight)): - text_color = NSColor.whiteColor() - return DrawingStyle(text_color=text_color) - -class CocoaSelectionOwnerMixin(SelectionOwnerMixin): - """Cocoa-specific methods for selection management. - - This subclass should not define any behavior. Methods that cannot be - completed in this widget state should raise WidgetActionError. - """ - - def _set_allow_multiple_select(self, allow): - self.tableview.setAllowsMultipleSelection_(allow) - - def _get_allow_multiple_select(self): - return self.tableview.allowsMultipleSelection() - - def _get_selected_iters(self): - selection = self.tableview.selectedRowIndexes() - selrows = tablemodel.list_from_nsindexset(selection) - return [self.model.iter_for_row(self.tableview, row) for row in selrows] - - def _get_selected_iter(self): - row = self.tableview.selectedRow() - if row == -1: - return None - return self.model.iter_for_row(self.tableview, row) - - def _get_selected_rows(self): - return [self.model[i] for i in self._get_selected_iters()] - - @property - def num_rows_selected(self): - return self.tableview.numberOfSelectedRows() - - def _is_selected(self, iter_): - row = self.row_of_iter(iter_) - return self.tableview.isRowSelected_(row) - - def _select(self, iter_): - row = self.row_of_iter(iter_) - index_set = NSIndexSet.alloc().initWithIndex_(row) - self.tableview.selectRowIndexes_byExtendingSelection_(index_set, YES) - - def _unselect(self, iter_): - self.tableview.deselectRow_(self.row_of_iter(iter_)) - - def _unselect_all(self): - self.tableview.deselectAll_(nil) - - def _iter_to_string(self, iter_): - return unicode(self.model.row_of_iter(self.tableview, iter_)) - - def _iter_from_string(self, row): - return self.model.iter_for_row(self.tableview, int(row)) - -class CocoaScrollbarOwnerMixin(ScrollbarOwnerMixin): - """Manages a TableView's scroll position.""" - def __init__(self): - ScrollbarOwnerMixin.__init__(self, _work_around_17153=True) - self.connect('place-in-scroller', self.on_place_in_scroller) - self.scroll_position = (0, 0) - self.clipview_notifications = None - self._position_set = False - - def _set_scroll_position(self, scroll_to): - """Restore a saved scroll position.""" - self.scroll_position = scroll_to - try: - scroller = self._scroller - except errors.WidgetNotReadyError: - return - self._position_set = True - clipview = scroller.contentView() - if not self.clipview_notifications: - self.clipview_notifications = NotificationForwarder.create(clipview) - # NOTE: intentional changes are BoundsChanged; bad changes are - # FrameChanged - clipview.setPostsFrameChangedNotifications_(YES) - self.clipview_notifications.connect(self.on_scroll_changed, - 'NSViewFrameDidChangeNotification') - # NOTE: scrollPoint_ just scrolls the point into view; we want to - # scroll the view so that the point becomes the origin - size = self.tableview.visibleRect().size - size = (size.width, size.height) - rect = NSMakeRect(scroll_to[0], scroll_to[1], size[0], size[1]) - self.tableview.scrollRectToVisible_(rect) - - def on_place_in_scroller(self, scroller): - # workaround for 17153.1 - if not self._position_set: - self._set_scroll_position(self.scroll_position) - - @property - def _manually_scrolled(self): - """Return whether the view has been scrolled explicitly by the user - since the last time it was set automatically. Ignores X coords. - """ - auto_y = self.scroll_position[1] - real_y = self.get_scroll_position()[1] - return abs(auto_y - real_y) > 5 - - def _get_item_area(self, iter_): - rect = self.tableview.rectOfRow_(self.row_of_iter(iter_)) - return NSRectToRect(rect) - - def _get_visible_area(self): - return NSRectToRect(self._scroller.contentView().documentVisibleRect()) - - def _get_scroll_position(self): - point = self._scroller.contentView().documentVisibleRect().origin - return NSPointToPoint(point) - - def on_scroll_changed(self, notification): - # we get this notification when the scroll position has been reset (when - # it should not have been); put it back - self.set_scroll_position(self.scroll_position) - # this notification also serves as the Cocoa equivalent to - # on_scroll_range_changed, which tells super when we may be ready to - # scroll to an iter - self.emit('scroll-range-changed') - - def set_scroller(self, scroller): - """For GTK; Cocoa tableview knows its enclosingScrollView""" - - @property - def _scroller(self): - """Return an NSScrollView or raise WidgetNotReadyError""" - scroller = self.tableview.enclosingScrollView() - if not scroller: - raise errors.WidgetNotReadyError('enclosingScrollView') - return scroller - -class SorterPadding(NSView): - # Why is this a Mac only widget? Because the wrappermap mechanism requires - # us to layout the widgets (so that we may call back to the portable API - # hooks of the widget. Since we only set the view component, this fake - # widget is never placed so the wrappermap mechanism fails to work. - # - # So far, this is okay because only the Mac uses custom headers. - def init(self): - self = super(SorterPadding, self).init() - image = Image(resources.path('images/headertoolbar.png')) - self.image = ImageSurface(image) - return self - - def isFlipped(self): - return YES - - def drawRect_(self, rect): - context = DrawingContext(self, self.bounds(), rect) - context.style = DrawingStyle() - self.image.draw(context, 0, 0, context.width, context.height) - # XXX this color doesn't take into account enable/disabled state - # of the sorting widgets. - edge = 72.0 / 255 - context.set_color((edge, edge, edge)) - context.set_line_width(1) - context.move_to(0.5, 0) - context.rel_line_to(0, context.height) - context.stroke() - -class TableView(CocoaSelectionOwnerMixin, CocoaScrollbarOwnerMixin, Widget): - """Displays data as a tabular list. TableView follows the GTK TreeView - widget fairly closely. - """ - - CREATES_VIEW = False - # Bit of a hack. We create several views. By setting CREATES_VIEW to - # False, we get to position the views manually. - - draws_selection = True - - def __init__(self, model, custom_headers=False): - Widget.__init__(self) - CocoaSelectionOwnerMixin.__init__(self) - CocoaScrollbarOwnerMixin.__init__(self) - self.create_signal('hotspot-clicked') - self.create_signal('row-clicked') - self.create_signal('row-activated') - self.create_signal('reallocate-columns') - self.model = model - self.columns = [] - self.drag_source = self.drag_dest = None - self.context_menu_callback = None - self.tableview = MiroTableView.alloc().init() - self.data_source = tablemodel.MiroTableViewDataSource.alloc() - types = (tablemodel.MIRO_DND_ITEM_LOCAL,) - self.tableview.registerForDraggedTypes_(types) - self.view = self.tableview - self.data_source.initWithModel_(self.model) - self.tableview.setDataSource_(self.data_source) - self.tableview.setVerticalMotionCanBeginDrag_(YES) - self.set_columns_draggable(False) - self.set_auto_resizes(False) - self.row_height_set = False - self.set_fixed_height(False) - self.auto_resizing = False - self.header_view = MiroTableHeaderView.alloc().initWithFrame_( - NSMakeRect(0, 0, 0, 0)) - self.tableview.setCornerView_(None) - self.custom_header = False - self.header_height = HEADER_HEIGHT - self.set_show_headers(True) - self.notifications = NotificationForwarder.create(self.tableview) - self.model_signal_ids = [ - self.model.connect_weak('row-changed', self.on_row_change), - self.model.connect_weak('structure-will-change', - self.on_model_structure_change), - ] - self.iters_to_update = [] - self.height_changed = self.reload_needed = False - self.old_selection = None - self._resizing = False - if custom_headers: - self._enable_custom_headers() - - def unset_model(self): - for signal_id in self.model_signal_ids: - self.model.disconnect(signal_id) - self.model = None - self.tableview.setDataSource_(None) - self.data_source = None - - def _enable_custom_headers(self): - self.custom_header = True - self.header_height = CUSTOM_HEADER_HEIGHT - self.header_view.custom_header = True - self.tableview.setCornerView_(SorterPadding.alloc().init()) - - def enable_album_view_focus_hack(self): - # this only matters on GTK - pass - - def focus(self): - if self.tableview.window() is not None: - self.tableview.window().makeFirstResponder_(self.tableview) - - def send_hotspot_clicked(self): - tracker = self.tableview.hotspot_tracker - self.emit('hotspot-clicked', tracker.name, tracker.iter) - - def on_row_change(self, model, iter): - self.iters_to_update.append(iter) - if not self.fixed_height: - self.height_changed = True - if self.tableview.hotspot_tracker is not None: - self.tableview.hotspot_tracker.update_hit() - - def on_model_structure_change(self, model): - self.will_need_reload() - self.cancel_hotspot_track() - - def will_need_reload(self): - if not self.reload_needed: - self.reload_needed = True - self.old_selection = self._get_selected_rows() - - def cancel_hotspot_track(self): - if self.tableview.hotspot_tracker is not None: - self.tableview.hotspot_tracker.redraw_cell() - self.tableview.hotspot_tracker = None - - def on_expanded(self, notification): - self.invalidate_size_request() - item = notification.userInfo()['NSObject'] - iter_ = self.model.iter_for_item[item] - self.emit('row-expanded', iter_, self.model.get_path(iter_)) - - def on_collapsed(self, notification): - self.invalidate_size_request() - item = notification.userInfo()['NSObject'] - iter_ = self.model.iter_for_item[item] - self.emit('row-collapsed', iter_, self.model.get_path(iter_)) - - def on_column_resize(self, notification): - if self.auto_resizing or self._resizing: - return - self._resizing = True - try: - column = notification.userInfo()['NSTableColumn'] - label = column.headerCell().stringValue() - self.emit('reallocate-columns', {label: column.width()}) - finally: - self._resizing = False - - def is_tree(self): - return isinstance(self.model, tablemodel.TreeTableModel) - - def set_row_expanded(self, iter, expanded): - """Expand or collapse the specified row. Succeeds or raises - WidgetActionError. - """ - item = iter.value() - if expanded: - self.tableview.expandItem_(item) - else: - self.tableview.collapseItem_(item) - if self.tableview.isItemExpanded_(item) != expanded: - raise errors.WidgetActionError( - "cannot expand iter. expandable: %r" % ( - self.tableview.isExpandable_(item),)) - self.invalidate_size_request() - - def is_row_expanded(self, iter): - return self.tableview.isItemExpanded_(iter.value()) - - def calc_size_request(self): - self.tableview.tile() - height = self.tableview.frame().size.height - if self._show_headers: - height += self.header_height - return self.calc_width(), height - - def viewport_repositioned(self): - self._do_layout() - - def viewport_created(self): - wrappermap.add(self.tableview, self) - self._do_layout() - self._add_views() - self.notifications.connect(self.on_selection_changed, - 'NSTableViewSelectionDidChangeNotification') - self.notifications.connect(self.on_column_resize, - 'NSTableViewColumnDidResizeNotification') - # scroll has been unset - self._position_set = False - - def remove_viewport(self): - if self.viewport is not None: - self._remove_views() - wrappermap.remove(self.tableview) - self.notifications.disconnect() - self.viewport = None - if self.clipview_notifications: - self.clipview_notifications.disconnect() - self.clipview_notifications = None - - def _should_place_header_view(self): - return self._show_headers and not self.parent_is_scroller - - def _add_views(self): - self.viewport.view.addSubview_(self.tableview) - if self._should_place_header_view(): - self.viewport.view.addSubview_(self.header_view) - - def _remove_views(self): - self.tableview.removeFromSuperview() - self.header_view.removeFromSuperview() - - def _do_layout(self): - x = self.viewport.placement.origin.x - y = self.viewport.placement.origin.y - width = self.viewport.get_width() - height = self.viewport.get_height() - if self._should_place_header_view(): - self.header_view.setFrame_(NSMakeRect(x, y, - width, self.header_height)) - self.tableview.setFrame_(NSMakeRect(x, y + self.header_height, - width, height - self.header_height)) - else: - self.header_view.setFrame_(NSMakeRect(x, y, - width, self.header_height)) - self.tableview.setFrame_(NSMakeRect(x, y, width, height)) - - if self.auto_resize: - self.auto_resizing = True - # ListView sizes itself in do_size_allocated; - # this is necessary for tablist and StandardView - columns = self.tableview.tableColumns() - if len(columns) == 1: - columns[0].setWidth_(self.viewport.area().size.width) - self.auto_resizing = False - self.queue_redraw() - - def calc_width(self): - if self.column_count() == 0: - return 0 - width = 0 - columns = self.tableview.tableColumns() - if self.auto_resize: - # Table auto-resizes, we can shrink to min-width for each column - width = sum(column.minWidth() for column in columns) - width += self.tableview.column_spacing * self.column_count() - else: - # Table doesn't auto-resize, the columns can't get smaller than - # their current width - width = sum(column.width() for column in columns) - return width - - def start_bulk_change(self): - # stop our model from emitting signals, which is slow if we're - # adding/removing/changing a bunch of rows. Instead, just reload the - # model afterwards. - self.will_need_reload() - self.cancel_hotspot_track() - self.model.freeze_signals() - - def model_changed(self): - if not self.row_height_set and self.fixed_height: - self.try_to_set_row_height() - self.model.thaw_signals() - size_changed = False - if self.reload_needed: - self.tableview.reloadData() - new_selection = self._get_selected_rows() - if new_selection != self.old_selection: - self.on_selection_changed(self.tableview) - self.old_selection = None - size_changed = True - elif self.iters_to_update: - if self.fixed_height or not self.height_changed: - # our rows don't change height, just update cell areas - if self.is_tree(): - for iter in self.iters_to_update: - self.tableview.reloadItem_(iter.value()) - else: - for iter in self.iters_to_update: - row = self.row_of_iter(iter) - rect = self.tableview.rectOfRow_(row) - self.tableview.setNeedsDisplayInRect_(rect) - else: - # our rows can change height inform Cocoa that their heights - # might have changed (this will redraw them) - index_set = NSMutableIndexSet.alloc().init() - for iter in self.iters_to_update: - try: - index_set.addIndex_(self.row_of_iter(iter)) - except LookupError: - # This happens when the iter's parent is unexpanded, - # just ignore. - pass - self.tableview.noteHeightOfRowsWithIndexesChanged_(index_set) - size_changed = True - else: - return - if size_changed: - self.invalidate_size_request() - self.height_changed = self.reload_needed = False - self.iters_to_update = [] - - def width_for_columns(self, width): - """If the table is width pixels big, how much width is available for - the table's columns. - """ - # XXX this used to do some calculation with the spacing of each column, - # but it doesn't appear like we need it to be that complicated anymore - # (see #18273) - return width - 2 - - def set_column_spacing(self, column_spacing): - self.tableview.column_spacing = column_spacing - - def set_row_spacing(self, row_spacing): - self.tableview.row_spacing = row_spacing - - def set_alternate_row_backgrounds(self, setting): - self.tableview.setUsesAlternatingRowBackgroundColors_(setting) - - def set_grid_lines(self, horizontal, vertical): - mask = 0 - if horizontal: - mask |= NSTableViewSolidHorizontalGridLineMask - if vertical: - mask |= NSTableViewSolidVerticalGridLineMask - self.tableview.setGridStyleMask_(mask) - - def set_gradient_highlight(self, setting): - self.tableview.gradientHighlight = setting - - def set_group_lines_enabled(self, enabled): - self.tableview.group_lines_enabled = enabled - self.queue_redraw() - - def set_group_line_style(self, color, width): - self.tableview.group_line_color = color + (1.0,) - self.tableview.group_line_width = width - self.queue_redraw() - - def get_tooltip(self, iter, column): - return None - - def add_column(self, column): - if not self.custom_header == column.custom_header: - raise ValueError('Column header does not match type ' - 'required by TableView') - self.columns.append(column) - self.tableview.addTableColumn_(column) - self._set_min_max_column_widths(column) - # Adding a column means that each row could have a different height. - # call noteNumberOfRowsChanged() to have OS X recalculate the heights - self.tableview.noteNumberOfRowsChanged() - self.invalidate_size_request() - self.try_to_set_row_height() - - def _set_min_max_column_widths(self, column): - if column.do_horizontal_padding: - spacing = self.tableview.column_spacing - else: - spacing = 0 - if column.min_width > 0: - column._column.setMinWidth_(column.min_width + spacing) - if column.max_width > 0: - column._column.setMaxWidth_(column.max_width + spacing) - - def column_count(self): - return len(self.tableview.tableColumns()) - - def remove_column(self, index): - column = self.columns.pop(index) - self.tableview.removeTableColumn_(column._column) - self.invalidate_size_request() - - def get_columns(self): - titles = [] - columns = self.tableview.tableColumns() - for column in columns: - titles.append(column.headerCell().stringValue()) - return titles - - def set_background_color(self, (red, green, blue)): - color = NSColor.colorWithDeviceRed_green_blue_alpha_(red, green, blue, - 1.0) - self.tableview.setBackgroundColor_(color) - - def set_show_headers(self, show): - self._show_headers = show - if show: - self.tableview.setHeaderView_(self.header_view) - else: - self.tableview.setHeaderView_(None) - if self.viewport is not None: - self._remove_views() - self._do_layout() - self._add_views() - self.invalidate_size_request() - self.queue_redraw() - - def is_showing_headers(self): - return self._show_headers - - def set_search_column(self, model_index): - pass - - def try_to_set_row_height(self): - if len(self.model) > 0: - first_iter = self.model.first_iter() - height = calc_row_height(self.tableview, self.model[first_iter]) - self.tableview.setRowHeight_(height) - self.row_height_set = True - - def set_auto_resizes(self, setting): - self.auto_resize = setting - - def set_columns_draggable(self, dragable): - self.tableview.setAllowsColumnReordering_(dragable) - - def set_fixed_height(self, fixed): - if fixed: - self.fixed_height = True - delegate_class = TableViewDelegate - self.row_height_set = False - self.try_to_set_row_height() - else: - self.fixed_height = False - delegate_class = VariableHeightTableViewDelegate - self.delegate = delegate_class.alloc().init() - self.tableview.setDelegate_(self.delegate) - self.tableview.reloadData() - - def row_of_iter(self, iter): - return self.model.row_of_iter(self.tableview, iter) - - def set_context_menu_callback(self, callback): - self.context_menu_callback = callback - - # disable the drag when the cells are constantly updating. Mac OS X - # deals badly with this.. - def set_volatile(self, volatile): - if volatile: - self.data_source.setDragSource_(None) - self.data_source.setDragDest_(None) - else: - self.data_source.setDragSource_(self.drag_source) - self.data_source.setDragDest_(self.drag_dest) - - def set_drag_source(self, drag_source): - self.drag_source = drag_source - self.data_source.setDragSource_(drag_source) - - def set_drag_dest(self, drag_dest): - self.drag_dest = drag_dest - if drag_dest is None: - self.data_source.setDragDest_(None) - else: - types = drag_dest.allowed_types() - self.data_source.setDragDest_(drag_dest) diff --git a/mvc/widgets/osx/utils.py b/mvc/widgets/osx/utils.py deleted file mode 100644 index c0c2d85..0000000 --- a/mvc/widgets/osx/utils.py +++ /dev/null @@ -1,2 +0,0 @@ -def filename_to_unicode(filename): - return filename.decode('utf8') diff --git a/mvc/widgets/osx/viewport.py b/mvc/widgets/osx/viewport.py deleted file mode 100644 index e6564d4..0000000 --- a/mvc/widgets/osx/viewport.py +++ /dev/null @@ -1,101 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -""".viewport.py -- Viewport classes - -A Viewport represents the area where a Widget is located. -""" - -from objc import YES, NO, nil -from Foundation import * - -class Viewport(object): - """Used when a widget creates it's own NSView.""" - def __init__(self, view, initial_frame): - self.view = view - self.view.setFrame_(initial_frame) - self.placement = initial_frame - - def at_position(self, rect): - """Check if a viewport is currently positioned at rect.""" - return self.placement == rect - - def reposition(self, rect): - """Move the viewport to a different position.""" - self.view.setFrame_(rect) - self.placement = rect - - def remove(self): - self.view.removeFromSuperview() - - def area(self): - """Area of our view that is occupied by the viewport.""" - return NSRect(self.view.bounds().origin, self.placement.size) - - def get_width(self): - return self.view.frame().size.width - - def get_height(self): - return self.view.frame().size.height - - def queue_redraw(self): - opaque_view = self.view.opaqueAncestor() - if opaque_view is not None: - rect = opaque_view.convertRect_fromView_(self.area(), self.view) - opaque_view.setNeedsDisplayInRect_(rect) - - def redraw_now(self): - self.view.displayRect_(self.area()) - -class BorrowedViewport(Viewport): - """Used when a widget uses the NSView of one of it's ancestors. We store - the view that we borrow as well as an NSRect specifying where on that view - we are placed. - """ - def __init__(self, view, placement): - self.view = view - self.placement = placement - - def at_position(self, rect): - return self.placement == rect - - def reposition(self, rect): - self.placement = rect - - def remove(self): - pass - - def area(self): - return self.placement - - def get_width(self): - return self.placement.size.width - - def get_height(self): - return self.placement.size.height diff --git a/mvc/widgets/osx/widgetset.py b/mvc/widgets/osx/widgetset.py deleted file mode 100644 index 1203566..0000000 --- a/mvc/widgets/osx/widgetset.py +++ /dev/null @@ -1,58 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -""".widgetset -- Contains all the -platform-specific widgets. This module doesn't have any actual code in it, it -just imports the widgets from their actual locations. -""" - -from .const import * -from .control import (TextEntry, NumberEntry, - SecureTextEntry, MultilineTextEntry) -from .control import Checkbox, Button, OptionMenu, RadioButtonGroup, RadioButton -from .customcontrol import (CustomButton, - ContinuousCustomButton, CustomSlider, DragableCustomButton) -from .contextmenu import ContextMenu -from .drawing import DrawingContext, ImageSurface, Gradient -from .drawingwidgets import DrawingArea, Background -from .rect import Rect -from .layout import VBox, HBox, Alignment, Table, Scroller, Expander, TabContainer, DetachedWindowHolder -from .window import Window, MainWindow, Dialog, FileSaveDialog, FileOpenDialog -from .window import DirectorySelectDialog, AboutDialog, AlertDialog, PreferencesWindow, DonateWindow, DialogWindow, get_first_time_dialog_coordinates -from .simple import (Image, ImageDisplay, Label, - SolidBackground, ClickableImageButton, AnimatedImageDisplay, - ProgressBar, HLine) -from .tableview import (TableView, TableColumn, - CellRenderer, CustomCellRenderer, ImageCellRenderer, - CheckboxCellRenderer, - CUSTOM_HEADER_HEIGHT) -from .tablemodel import (TableModel, - TreeTableModel) -from .osxmenus import (MenuBar, Menu, Separator, MenuItem, RadioMenuItem, CheckMenuItem) -from .base import Widget diff --git a/mvc/widgets/osx/widgetupdates.py b/mvc/widgets/osx/widgetupdates.py deleted file mode 100644 index 30677c2..0000000 --- a/mvc/widgets/osx/widgetupdates.py +++ /dev/null @@ -1,72 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""widgetupdates.py -- Handle updates to our widgets -""" - -from PyObjCTools import AppHelper - -class SizeRequestManager(object): - """Helper object to manage size requests - - If something changes in a widget that makes us want to request a new size, - we avoid calculating it immediately. The reason is that the - new-size-request will cascade all the way up the widget tree, and then - result in our widget being placed. We don't necessary want all of this - action to happen while we are in the middle of handling an event - (especially with TableView). It's also inefficient to calculate things - immediately, since we might do something else to invalidate the size - request in the current event. - - SizeRequestManager stores which widgets need to have their size - recalculated, then calls do_invalidate_size_request() using callAfter - """ - - def __init__(self): - self.widgets_to_request = set() - #app.widgetapp.connect("event-processed", self._on_event_processed) - - def add_widget(self, widget): - if len(self.widgets_to_request) == 0: - AppHelper.callAfter(self._run_requests) - self.widgets_to_request.add(widget) - - def _run_requests(self): - this_run = self.widgets_to_request - self.widgets_to_request = set() - for widget in this_run: - widget.do_invalidate_size_request() - - def _on_event_processed(self, app): - # once we finishing handling an event, process our size requests ASAP - # to avoid any potential weirdness. Note: that we also schedule a - # call using callAfter(), often that will do nothing, but it's - # possible size requests get scheduled outside of an event - while self.widgets_to_request: - self._run_requests() diff --git a/mvc/widgets/osx/window.py b/mvc/widgets/osx/window.py deleted file mode 100644 index b959333..0000000 --- a/mvc/widgets/osx/window.py +++ /dev/null @@ -1,896 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -""".window -- Top-level Window class. """ - -import logging - -from AppKit import * -from Foundation import * -from objc import YES, NO, nil -from PyObjCTools import AppHelper - -from mvc import signals -from mvc.widgets import widgetconst -import wrappermap -import osxmenus -from .helpers import NotificationForwarder -from .base import Widget, FlippedView -from .layout import VBox, HBox, Alignment -from .control import Button -from .simple import Label -from .rect import Rect, NSRectWrapper -from .utils import filename_to_unicode - -# Tracks all windows that haven't been destroyed. This makes sure there -# object stay alive as long as the window is alive. -alive_windows = set() - -class MiroResponderInterceptor(NSResponder): - """Intercepts cocoa events and gives our wrappers and chance to handle - them first. - """ - - def initWithResponder_(self, responder): - """Initialize a MiroResponderInterceptor - - We will give the wrapper for responder a chance to handle the event, - then pass it along to responder. - """ - self = super(MiroResponderInterceptor, self).init() - self.responder = responder - return self - - def keyDown_(self, event): - if self.sendKeyDownToWrapper_(event): - return # signal handler returned True, stop processing - - # If our responder is the last in the chain, we can stop intercepting - if self.responder.nextResponder() is None: - self.responder.keyDown_(event) - return - - # Here's the tricky part, we want to call keyDown_ on our responder, - # but if it doesn't handle the event, then it will pass it along to - # it's next responder. We need to set things up so that we will - # intercept that call. - - # Make a new MiroResponderInterceptor whose responder is the next - # responder down the chain. - next_intercepter = MiroResponderInterceptor.alloc().initWithResponder_( - self.responder.nextResponder()) - # Install the interceptor - self.responder.setNextResponder_(next_intercepter) - # Send event along - self.responder.keyDown_(event) - # Restore old nextResponder value - self.responder.setNextResponder_(next_intercepter.responder) - - def sendKeyDownToWrapper_(self, event): - """Give a keyDown event to the wrapper for our responder - - Return True if the wrapper handled the event - """ - key = event.charactersIgnoringModifiers() - if len(key) != 1 or not key.isalnum(): - key = osxmenus.REVERSE_KEYS_MAP.get(key) - mods = osxmenus.translate_event_modifiers(event) - wrapper = wrappermap.wrapper(self.responder) - if isinstance(wrapper, Widget) or isinstance(wrapper, Window): - if wrapper.emit('key-press', key, mods): - return True - return False - -class MiroWindow(NSWindow): - def initWithContentRect_styleMask_backing_defer_(self, rect, mask, - backing, defer): - self = NSWindow.initWithContentRect_styleMask_backing_defer_(self, - rect, mask, backing, defer) - self._last_focus_chain = None - return self - - def handleKeyDown_(self, event): - if self.handle_tab_navigation(event): - return - interceptor = MiroResponderInterceptor.alloc().initWithResponder_( - self.firstResponder()) - interceptor.keyDown_(event) - - def handle_tab_navigation(self, event): - """Handle tab navigation through the window. - - :returns: True if we handled the event - """ - keystr = event.charactersIgnoringModifiers() - if keystr[0] == NSTabCharacter: - # handle cycling through views with Tab. - self.focusNextKeyView_(True) - return True - elif keystr[0] == NSBackTabCharacter: - self.focusNextKeyView_(False) - return True - return False - - def acceptsMouseMovedEvents(self): - # HACK: for some reason calling setAcceptsMouseMovedEvents_() doesn't - # work, we have to forcefully override this method. - return NO - - def sendEvent_(self, event): - if event.type() == NSKeyDown: - self.handleKeyDown_(event) - else: - NSWindow.sendEvent_(self, event) - - def _calc_current_focus_wrapper(self): - responder = self.firstResponder() - while responder: - wrapper = wrappermap.wrapper(responder) - # check if we have a wrapper for the view, if not try the parent - # view - if wrapper is not None: - return wrapper - responder = responder.superview() - return None - - def focusNextKeyView_(self, is_forward): - current_focus = self._calc_current_focus_wrapper() - my_wrapper = wrappermap.wrapper(self) - next_focus = my_wrapper.get_next_tab_focus(current_focus, is_forward) - if next_focus is not None: - next_focus.focus() - - def draggingEntered_(self, info): - wrapper = wrappermap.wrapper(self) - return wrapper.draggingEntered_(info) or NSDragOperationNone - - def draggingUpdated_(self, info): - wrapper = wrappermap.wrapper(self) - return wrapper.draggingUpdated_(info) or NSDragOperationNone - - def draggingExited_(self, info): - wrapper = wrappermap.wrapper(self) - wrapper.draggingExited_(info) - - def prepareForDragOperation_(self, info): - wrapper = wrappermap.wrapper(self) - return wrapper.prepareForDragOperation_(info) or NO - - def performDragOperation_(self, info): - wrapper = wrappermap.wrapper(self) - return wrapper.performDragOperation_(info) or NO - -class MainMiroWindow(MiroWindow): - def isMovableByWindowBackground(self): - return YES - -class Window(signals.SignalEmitter): - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - def __init__(self, title, rect=None): - signals.SignalEmitter.__init__(self) - self.create_signal('active-change') - self.create_signal('will-close') - self.create_signal('did-move') - self.create_signal('key-press') - self.create_signal('show') - self.create_signal('hide') - self.create_signal('on-shown') - self.create_signal('file-drag-motion') - self.create_signal('file-drag-received') - self.create_signal('file-drag-leave') - self.is_closing = False - if rect is None: - rect = Rect(0, 0, 470, 600) - self.nswindow = MainMiroWindow.alloc().initWithContentRect_styleMask_backing_defer_( - rect.nsrect, - self.get_style_mask(), - NSBackingStoreBuffered, - NO) - self.nswindow.setTitle_(title) - self.nswindow.setMinSize_(NSSize(470, 600)) - self.nswindow.setReleasedWhenClosed_(NO) - self.content_view = FlippedView.alloc().initWithFrame_(rect.nsrect) - self.content_view.setAutoresizesSubviews_(NO) - self.nswindow.setContentView_(self.content_view) - self.content_widget = None - self.view_notifications = NotificationForwarder.create(self.content_view) - self.view_notifications.connect(self.on_frame_change, 'NSViewFrameDidChangeNotification') - self.window_notifications = NotificationForwarder.create(self.nswindow) - self.window_notifications.connect(self.on_activate, 'NSWindowDidBecomeMainNotification') - self.window_notifications.connect(self.on_deactivate, 'NSWindowDidResignMainNotification') - self.window_notifications.connect(self.on_did_move, 'NSWindowDidMoveNotification') - self.window_notifications.connect(self.on_will_close, 'NSWindowWillCloseNotification') - wrappermap.add(self.nswindow, self) - alive_windows.add(self) - - def get_next_tab_focus(self, current, is_forward): - """Return the next widget to cycle through for keyboard focus - - Subclasses can override this to for find-grained control of keyboard - focus. - - :param current: currently-focused widget - :param is_forward: are we tabbing forward? - """ - return None - - # XXX Use MainWindow not Window for MVCStyle/MiroStyle - def get_style_mask(self): - return (NSTitledWindowMask | NSClosableWindowMask | - NSMiniaturizableWindowMask) - - def set_title(self, title): - self.nswindow.setTitle_(title) - - def get_title(self): - return self.nswindow.title() - - def on_frame_change(self, notification): - self.place_child() - - def on_activate(self, notification): - self.emit('active-change') - - def on_deactivate(self, notification): - self.emit('active-change') - - def on_did_move(self, notification): - self.emit('did-move') - - def on_will_close(self, notification): - # unset the first responder. This allows text entry widgets to get - # the NSControlTextDidEndEditingNotification - if self.is_closing: - logging.info('on_will_close: already closing') - return - self.is_closing = True - self.nswindow.makeFirstResponder_(nil) - self.emit('will-close') - self.emit('hide') - self.is_closing = False - - def is_active(self): - return self.nswindow.isMainWindow() - - def is_visible(self): - return self.nswindow.isVisible() - - def show(self): - if self not in alive_windows: - raise ValueError("Window destroyed") - self.nswindow.makeKeyAndOrderFront_(nil) - self.nswindow.makeMainWindow() - self.emit('show') - # Cocoa doesn't apply default selections as forcefully as GTK, so - # currently there's no need for on-shown to actually wait until the - # window has been shown here - self.emit('on-shown') - - def close(self): - self.nswindow.close() - - def destroy(self): - self.close() - self.window_notifications.disconnect() - self.view_notifications.disconnect() - self.nswindow.setContentView_(nil) - wrappermap.remove(self.nswindow) - alive_windows.discard(self) - self.nswindow = None - - def place_child(self): - rect = self.nswindow.contentRectForFrameRect_(self.nswindow.frame()) - self.content_widget.place(NSRect(NSPoint(0, 0), rect.size), - self.content_view) - - def hookup_content_widget_signals(self): - self.size_req_handler = self.content_widget.connect('size-request-changed', - self.on_content_widget_size_request_change) - - def unhook_content_widget_signals(self): - self.content_widget.disconnect(self.size_req_handler) - self.size_req_handler = None - - def on_content_widget_size_request_change(self, widget, old_size): - self.update_size_constraints() - - def set_content_widget(self, widget): - if self.content_widget: - self.content_widget.remove_viewport() - self.unhook_content_widget_signals() - self.content_widget = widget - self.hookup_content_widget_signals() - self.place_child() - self.update_size_constraints() - - def update_size_constraints(self): - width, height = self.content_widget.get_size_request() - # It is possible the window is torn down between the size invalidate - # request and the actual size invalidation invocation. So check - # to see if nswindow is there if not then do not do anything. - if self.nswindow: - # FIXME: I'm not sure that this code does what we want it to do. - # It enforces the min-size when the user drags the window, but I - # think it should also call setContentSize_ if the window is - # currently too small to fit the content - BDK - self.nswindow.setContentMinSize_(NSSize(width, height)) - rect = self.nswindow.contentRectForFrameRect_(self.nswindow.frame()) - if rect.size.width < width or rect.size.height < height: - logging.warn("Content widget too large for this window " - "size available: %dx%d widget size: %dx%d", - rect.size.width, rect.size.height, width, height) - - def get_content_widget(self): - return self.content_widget - - def get_frame(self): - frame = self.nswindow.frame() - frame.size.height -= 22 - return NSRectWrapper(frame) - - def connect_menu_keyboard_shortcuts(self): - # All OS X windows are connected to the menu shortcuts - pass - - def accept_file_drag(self, val): - if not val: - self.drag_dest = None - else: - self.drag_dest = NSDragOperationCopy - self.nswindow.registerForDraggedTypes_([NSFilenamesPboardType]) - - def prepareForDragOperation_(self, info): - return NO if self.drag_dest is None else YES - - def performDragOperation_(self, info): - pb = info.draggingPasteboard() - available_types = set(pb.types()) & set([NSFilenamesPboardType]) - drag_ok = False - if available_types: - type_ = available_types.pop() - # DANCE! Everybody dance for portable Python code! - values = [unicode( - NSURL.fileURLWithPath_(v).filePathURL()).encode('utf-8') - for v in list(pb.propertyListForType_(type_))] - self.emit('file-drag-received', values) - drag_ok = True - self.draggingExited_(info) - return drag_ok - - def draggingEntered_(self, info): - return self.draggingUpdated_(info) - - def draggingUpdated_(self, info): - self.emit('file-drag-motion') - return self.drag_dest - - def draggingExited_(self, info): - self.emit('file-drag-leave') - - def center(self): - self.nswindow.center() - -class MainWindow(Window): - def __init__(self, title, rect): - Window.__init__(self, title, rect) - self.nswindow.setReleasedWhenClosed_(NO) - - def close(self): - self.nswindow.orderOut_(nil) - -class DialogBase(object): - def __init__(self): - self.sheet_parent = None - def set_transient_for(self, window): - self.sheet_parent = window - -class MiroPanel(NSPanel): - def cancelOperation_(self, event): - wrappermap.wrapper(self).end_with_code(-1) - -class Dialog(DialogBase): - def __init__(self, title, description=None): - DialogBase.__init__(self) - self.title = title - self.description = description - self.buttons = [] - self.extra_widget = None - self.window = None - self.running = False - - def add_button(self, text): - button = Button(text) - button.set_size(widgetconst.SIZE_NORMAL) - button.connect('clicked', self.on_button_clicked, len(self.buttons)) - self.buttons.append(button) - - def on_button_clicked(self, button, code): - self.end_with_code(code) - - def end_with_code(self, code): - if self.sheet_parent is not None: - NSApp().endSheet_returnCode_(self.window, code) - else: - NSApp().stopModalWithCode_(code) - - def build_text(self): - vbox = VBox(spacing=6) - if self.description is not None: - description_label = Label(self.description, wrap=True) - description_label.set_bold(True) - description_label.set_size_request(360, -1) - vbox.pack_start(description_label) - return vbox - - def build_buttons(self): - hbox = HBox(spacing=12) - for button in reversed(self.buttons): - hbox.pack_start(button) - alignment = Alignment(xalign=1.0, yscale=1.0) - alignment.add(hbox) - return alignment - - def build_content(self): - vbox = VBox(spacing=12) - vbox.pack_start(self.build_text()) - if self.extra_widget: - vbox.pack_start(self.extra_widget) - vbox.pack_start(self.build_buttons()) - alignment = Alignment(xscale=1.0, yscale=1.0) - alignment.set_padding(12, 12, 17, 17) - alignment.add(vbox) - return alignment - - def build_window(self): - self.content_widget = self.build_content() - width, height = self.content_widget.get_size_request() - width = max(width, 400) - window = MiroPanel.alloc() - window.initWithContentRect_styleMask_backing_defer_( - NSMakeRect(400, 400, width, height), - NSTitledWindowMask, NSBackingStoreBuffered, NO) - view = FlippedView.alloc().initWithFrame_(NSMakeRect(0, 0, width, - height)) - window.setContentView_(view) - window.setTitle_(self.title) - self.content_widget.place(view.frame(), view) - if self.buttons: - self.buttons[0].make_default() - return window - - def hookup_content_widget_signals(self): - self.size_req_handler = self.content_widget.connect( - 'size-request-changed', - self.on_content_widget_size_request_change) - - def unhook_content_widget_signals(self): - self.content_widget.disconnect(self.size_req_handler) - self.size_req_handler = None - - def on_content_widget_size_request_change(self, widget, old_size): - width, height = self.content_widget.get_size_request() - # It is possible the window is torn down between the size invalidate - # request and the actual size invalidation invocation. So check - # to see if nswindow is there if not then do not do anything. - if self.window and (width, height) != old_size: - self.change_content_size(width, height) - - def change_content_size(self, width, height): - content_rect = self.window.contentRectForFrameRect_( - self.window.frame()) - # Cocoa's coordinate system is funky, adjust y so that the top stays - # in place - content_rect.origin.y += (content_rect.size.height - height) - # change our frame to fit the new content. It would be nice to - # animate the change, but timers don't work when we are displaying a - # modal dialog - content_rect.size = NSSize(width, height) - new_frame = self.window.frameRectForContentRect_(content_rect) - self.window.setFrame_display_(new_frame, NO) - # Need to call place() again, since our window has changed size - contentView = self.window.contentView() - self.content_widget.place(contentView.frame(), contentView) - - def run(self): - self.window = self.build_window() - wrappermap.add(self.window, self) - self.hookup_content_widget_signals() - self.running = True - if self.sheet_parent is None: - response = NSApp().runModalForWindow_(self.window) - if self.window: - self.window.close() - else: - delegate = SheetDelegate.alloc().init() - NSApp().beginSheet_modalForWindow_modalDelegate_didEndSelector_contextInfo_( - self.window, self.sheet_parent.nswindow, - delegate, 'sheetDidEnd:returnCode:contextInfo:', 0) - response = NSApp().runModalForWindow_(self.window) - if self.window: - # self.window won't be around if we call destroy() to cancel - # the dialog - self.window.orderOut_(nil) - self.running = False - self.unhook_content_widget_signals() - - if response < 0: - return -1 - return response - - def destroy(self): - if self.running: - NSApp().stopModalWithCode_(-1) - - if self.window is not None: - self.window.setContentView_(None) - self.window.close() - self.window = None - self.buttons = None - self.extra_widget = None - - def set_extra_widget(self, widget): - self.extra_widget = widget - - def get_extra_widget(self): - return self.extra_widget - -class SheetDelegate(NSObject): - @AppHelper.endSheetMethod - def sheetDidEnd_returnCode_contextInfo_(self, sheet, return_code, info): - NSApp().stopModalWithCode_(return_code) - -class FileDialogBase(DialogBase): - def __init__(self): - DialogBase.__init__(self) - self._types = None - self._filename = None - self._directory = None - self._filter_on_run = True - - def run(self): - self._panel.setAllowedFileTypes_(self._types) - if self.sheet_parent is None: - if self._filter_on_run: - response = self._panel.runModalForDirectory_file_types_(self._directory, self._filename, self._types) - else: - response = self._panel.runModalForDirectory_file_(self._directory, self._filename) - else: - delegate = SheetDelegate.alloc().init() - if self._filter_on_run: - self._panel.beginSheetForDirectory_file_types_modalForWindow_modalDelegate_didEndSelector_contextInfo_( - self._directory, self._filename, self._types, - self.sheet_parent.nswindow, delegate, 'sheetDidEnd:returnCode:contextInfo:', 0) - else: - self._panel.beginSheetForDirectory_file_modalForWindow_modalDelegate_didEndSelector_contextInfo_( - self._directory, self._filename, - self.sheet_parent.nswindow, delegate, 'sheetDidEnd:returnCode:contextInfo:', 0) - response = NSApp().runModalForWindow_(self._panel) - self._panel.orderOut_(nil) - return response - -class FileSaveDialog(FileDialogBase): - def __init__(self, title): - FileDialogBase.__init__(self) - self._title = title - self._panel = NSSavePanel.savePanel() - self._panel.setCanChooseFiles_(YES) - self._panel.setCanChooseDirectories_(NO) - self._filename = None - self._filter_on_run = False - - def set_filename(self, s): - self._filename = filename_to_unicode(s) - - def get_filename(self): - # Use encode('utf-8') instead of unicode_to_filename, because - # unicode_to_filename has code to make sure nextFilename works, but it's - # more important here to not change the filename. - return self._filename.encode('utf-8') - - def run(self): - response = FileDialogBase.run(self) - if response == NSFileHandlingPanelOKButton: - self._filename = self._panel.filename() - return 0 - self._filename = "" - - def destroy(self): - self._panel = None - - set_path = set_filename - get_path = get_filename - -class FileOpenDialog(FileDialogBase): - def __init__(self, title): - FileDialogBase.__init__(self) - self._title = title - self._panel = NSOpenPanel.openPanel() - self._panel.setCanChooseFiles_(YES) - self._panel.setCanChooseDirectories_(NO) - self._filenames = None - - def set_select_multiple(self, value): - if value: - self._panel.setAllowsMultipleSelection_(YES) - else: - self._panel.setAllowsMultipleSelection_(NO) - - def set_directory(self, d): - self._directory = filename_to_unicode(d) - - def set_filename(self, s): - self._filename = filename_to_unicode(s) - - def add_filters(self, filters): - self._types = [] - for _, t in filters: - self._types += t - - def get_filename(self): - if self._filenames is None: - # canceled - return None - return self.get_filenames()[0] - - def get_filenames(self): - if self._filenames is None: - # canceled - return [] - # Use encode('utf-8') instead of unicode_to_filename, because - # unicode_to_filename has code to make sure nextFilename works, but it's - # more important here to not change the filename. - return [f.encode('utf-8') for f in self._filenames] - - def run(self): - response = FileDialogBase.run(self) - if response == NSFileHandlingPanelOKButton: - self._filenames = self._panel.filenames() - return 0 - self._filename = '' - self._filenames = None - - def destroy(self): - self._panel = None - - set_path = set_filename - get_path = get_filename - -class DirectorySelectDialog(FileDialogBase): - def __init__(self, title): - FileDialogBase.__init__(self) - self._title = title - self._panel = NSOpenPanel.openPanel() - self._panel.setCanChooseFiles_(NO) - self._panel.setCanChooseDirectories_(YES) - self._directory = None - - def set_directory(self, d): - self._directory = filename_to_unicode(d) - - def get_directory(self): - # Use encode('utf-8') instead of unicode_to_filename, because - # unicode_to_filename has code to make sure nextFilename - # works, but it's more important here to not change the - # filename. - return self._directory.encode('utf-8') - - def run(self): - response = FileDialogBase.run(self) - if response == NSFileHandlingPanelOKButton: - self._directory = self._panel.filenames()[0] - return 0 - self._directory = "" - - def destroy(self): - self._panel = None - - set_path = set_directory - get_path = get_directory - -class AboutDialog(DialogBase): - def run(self): - optionsDictionary = dict() - #revision = app.config.get(prefs.APP_REVISION_NUM) - #if revision: - # optionsDictionary['Version'] = revision - if not optionsDictionary: - optionsDictionary = nil - NSApplication.sharedApplication().orderFrontStandardAboutPanelWithOptions_(optionsDictionary) - def destroy(self): - pass - -class AlertDialog(DialogBase): - def __init__(self, title, message, alert_type): - DialogBase.__init__(self) - self._nsalert = NSAlert.alloc().init(); - self._nsalert.setMessageText_(title) - self._nsalert.setInformativeText_(message) - self._nsalert.setAlertStyle_(alert_type) - def add_button(self, text): - self._nsalert.addButtonWithTitle_(text) - def run(self): - self._nsalert.runModal() - def destroy(self): - self._nsalert = nil - -class PreferenceItem(NSToolbarItem): - - def setPanel_(self, panel): - self.panel = panel - -class PreferenceToolbarDelegate(NSObject): - - def initWithPanels_identifiers_window_(self, panels, identifiers, window): - self = super(PreferenceToolbarDelegate, self).init() - self.panels = panels - self.identifiers = identifiers - self.window = window - return self - - def toolbarAllowedItemIdentifiers_(self, toolbar): - return self.identifiers - - def toolbarDefaultItemIdentifiers_(self, toolbar): - return self.identifiers - - def toolbarSelectableItemIdentifiers_(self, toolbar): - return self.identifiers - - def toolbar_itemForItemIdentifier_willBeInsertedIntoToolbar_(self, toolbar, - itemIdentifier, - flag): - panel = self.panels[itemIdentifier] - item = PreferenceItem.alloc().initWithItemIdentifier_(itemIdentifier) - item.setLabel_(unicode(panel[1])) - item.setImage_(NSImage.imageNamed_(u"pref_tab_%s" % itemIdentifier)) - item.setAction_("switchPreferenceView:") - item.setTarget_(self) - item.setPanel_(panel[0]) - return item - - def validateToolbarItem_(self, item): - return YES - - def switchPreferenceView_(self, sender): - self.window.do_select_panel(sender.panel, YES) - -class DialogWindow(Window): - def __init__(self, title, rect, allow_miniaturize=False): - self.allow_miniaturize = allow_miniaturize - Window.__init__(self, title, rect) - self.nswindow.setShowsToolbarButton_(NO) - - def get_style_mask(self): - mask = (NSTitledWindowMask | NSClosableWindowMask) - if self.allow_miniaturize: - mask |= NSMiniaturizableWindowMask - return mask - -class DonateWindow(Window): - def __init__(self, title): - Window.__init__(self, title, Rect(0, 0, 640, 440)) - self.panels = dict() - self.identifiers = list() - self.first_show = True - self.nswindow.setShowsToolbarButton_(NO) - self.nswindow.setReleasedWhenClosed_(NO) - self.app_notifications = NotificationForwarder.create(NSApp()) - self.app_notifications.connect(self.on_app_quit, - 'NSApplicationWillTerminateNotification') - - def destroy(self): - super(PreferencesWindow, self).destroy() - self.app_notifications.disconnect() - - def get_style_mask(self): - return (NSTitledWindowMask | NSClosableWindowMask | - NSMiniaturizableWindowMask) - - def show(self): - if self.first_show: - self.nswindow.center() - self.first_show = False - Window.show(self) - - def on_app_quit(self, notification): - self.close() - -class PreferencesWindow(Window): - def __init__(self, title): - Window.__init__(self, title, Rect(0, 0, 640, 440)) - self.panels = dict() - self.identifiers = list() - self.first_show = True - self.nswindow.setShowsToolbarButton_(NO) - self.nswindow.setReleasedWhenClosed_(NO) - self.app_notifications = NotificationForwarder.create(NSApp()) - self.app_notifications.connect(self.on_app_quit, - 'NSApplicationWillTerminateNotification') - - def destroy(self): - super(PreferencesWindow, self).destroy() - self.app_notifications.disconnect() - - def get_style_mask(self): - return (NSTitledWindowMask | NSClosableWindowMask | - NSMiniaturizableWindowMask) - - def append_panel(self, name, panel, title, image_name): - self.panels[name] = (panel, title) - self.identifiers.append(name) - - def finish_panels(self): - self.tbdelegate = PreferenceToolbarDelegate.alloc().initWithPanels_identifiers_window_(self.panels, self.identifiers, self) - toolbar = NSToolbar.alloc().initWithIdentifier_(u"Preferences") - toolbar.setAllowsUserCustomization_(NO) - toolbar.setDelegate_(self.tbdelegate) - - self.nswindow.setToolbar_(toolbar) - - def select_panel(self, index): - panel = self.identifiers[index] - self.nswindow.toolbar().setSelectedItemIdentifier_(panel) - self.do_select_panel(self.panels[panel][0], NO) - - def do_select_panel(self, panel, animate): - wframe = self.nswindow.frame() - vsize = list(panel.get_size_request()) - if vsize[0] < 650: - vsize[0] = 650 - if vsize[1] < 200: - vsize[1] = 200 - - toolbarHeight = wframe.size.height - self.nswindow.contentView().frame().size.height - wframe.origin.y += wframe.size.height - vsize[1] - toolbarHeight - wframe.size = (vsize[0], vsize[1] + toolbarHeight) - - self.set_content_widget(panel) - self.nswindow.setFrame_display_animate_(wframe, YES, animate) - - def show(self): - if self.first_show: - self.nswindow.center() - self.first_show = False - Window.show(self) - - def on_app_quit(self, notification): - self.close() - -def get_first_time_dialog_coordinates(width, height): - """Returns the coordinates for the first time dialog. - """ - # windowFrame is None on first run. in that case, we want - # to put librevideoconverter in the middle. - mainscreen = NSScreen.mainScreen() - rect = mainscreen.frame() - - x = (rect.size.width - width) / 2 - y = (rect.size.height - height) / 2 - - return x, y diff --git a/mvc/widgets/osx/wrappermap.py b/mvc/widgets/osx/wrappermap.py deleted file mode 100644 index 624a496..0000000 --- a/mvc/widgets/osx/wrappermap.py +++ /dev/null @@ -1,48 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -""".wrappermap -- Map NSViews and NSWindows to the -Widget that wraps them. -""" - -# Maps NSViews/NSWinows -> wrapper objects. -wrapper_mapping = dict() - -def wrapper(wrapped): - """Find the wrapper object for an NSView/NSWindow.""" - try: - return wrapper_mapping[wrapped] - except KeyError: - return None - -def add(wrapped, wrapper): - wrapper_mapping[wrapped] = wrapper - -def remove(wrapped): - del wrapper_mapping[wrapped] diff --git a/mvc/widgets/tablescroll.py b/mvc/widgets/tablescroll.py deleted file mode 100644 index 841e62c..0000000 --- a/mvc/widgets/tablescroll.py +++ /dev/null @@ -1,154 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""tablescroll.py -- High-level scroll management. This ensures that behavior -like scroll_to_item works the same way across platforms. -""" - -from mvc.errors import WidgetActionError - - -class ScrollbarOwnerMixin(object): - """Scrollbar management for TableView. - - External methods have undecorated names; internal methods start with _. - - External methods: - - handle failure themselves (e.g. return None or retry later) - - return basic data types (e.g. (x, y) tuples) - - use "tree" coordinates - - Internal methods (intended to be used by ScrollbarOwnerMixin and the - platform implementations): - - raise WidgetActionError subclasses on failure - - use Rect/Point structs - - also use "tree" coordinates - """ - def __init__(self, _work_around_17153=False): - self.__work_around_17153 = _work_around_17153 - self._scroll_to_iter_callback = None - self.create_signal('scroll-range-changed') - - def scroll_to_iter(self, iter_, manual=True, recenter=False): - """Scroll the given item into view. - - manual: scroll even if we were not following the playing item - recenter: scroll even if item is in top half of view - """ - try: - item = self._get_item_area(iter_) - visible = self._get_visible_area() - manually_scrolled = self._manually_scrolled - except WidgetActionError: - if self._scroll_to_iter_callback: - # We just retried and failed. Do nothing; we will retry again - # next time scrollable range changes. - return - # We just tried and failed; schedule a retry when the scrollable - # range changes. - self._scroll_to_iter_callback = self.connect('scroll-range-changed', - lambda *a: self.scroll_to_iter(iter_, manual, recenter)) - return - # If the above succeeded, we know the iter's position; this means we can - # set_scroll_position to that position. That may work now or be - # postponed until later, but either way we're done with scroll_to_iter. - if self._scroll_to_iter_callback: - self.disconnect(self._scroll_to_iter_callback) - self._scroll_to_iter_callback = None - visible_bottom = visible.y + visible.height - visible_middle = visible.y + visible.height // 2 - item_bottom = item.y + item.height - item_middle = item.y + item.height // 2 - in_top = item_bottom >= visible.y and item.y <= visible_middle - in_bottom = item_bottom >= visible_middle and item.y <= visible_bottom - if self._should_scroll( - manual, in_top, in_bottom, recenter, manually_scrolled): - destination = item_middle - visible.height // 2 - self._set_vertical_scroll(destination) - # set_scroll_position will take care of scroll to the position when - # possible; this may or may not be now, but our work here is done. - - def set_scroll_position(self, position, restore_only=False, - _hack_for_17153=False): - """Scroll the top left corner to the given (x, y) offset from the origin - of the view. - - restore_only: set the value only if no other value has been set yet - """ - if _hack_for_17153 and not self.__work_around_17153: - return - if not restore_only or not self._position_set: - self._set_scroll_position(position) - - @classmethod - def _should_scroll(cls, - manual, in_top, in_bottom, recenter, manually_scrolled): - if not manual and manually_scrolled: - # The user has moved the scrollbars since we last autoscrolled, and - # we're deciding whether we should resume autoscrolling. - # We want to do that when the currently-playing item catches up to - # the center of the screen i.e. is part above the center, part below - return in_top and in_bottom - # This is a manual scroll, or we're already autoscrolling - so we no - # longer need to worry about either manual or manually_scrolled - if in_top: - # The item is in the top half; let playback catch up with the - # current scroll position, unless recentering has been requested - return recenter - if in_bottom: - # We land here when: - # - playback has begun with an item in the bottom half of the screen - # - scroll is following sequential playback - # Either way we want to jump down to the item. - return True - # We're scrolling to an item that's not in view because: - # - playback has begun with an item that is out of sight - # - we're autoscrolling on shuffle - # Either way we want to show the item. - return True - - def reset_scroll(self): - """To scroll back to the origin; platform code might want to do - something special to forget the current position when this happens. - """ - self.set_scroll_position((0, 0)) - - def get_scroll_position(self): - """Returns the current scroll position, or None if not ready.""" - try: - return tuple(self._get_scroll_position()) - except WidgetActionError: - return None - - def _set_vertical_scroll(self, pos): - """Helper to set our vertical position without affecting our horizontal - position. - """ - # FIXME: shouldn't reset horizontal position - self.set_scroll_position((0, pos)) diff --git a/mvc/widgets/tableselection.py b/mvc/widgets/tableselection.py deleted file mode 100644 index d087d34..0000000 --- a/mvc/widgets/tableselection.py +++ /dev/null @@ -1,220 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""tableselection.py -- High-level selection management. Subclasses defined in -the platform tableview modules provide the platform-specific methods used here. -""" - -from contextlib import contextmanager - -from mvc.errors import WidgetActionError, WidgetUsageError - -class SelectionOwnerMixin(object): - """Encapsulates the selection functionality of a TableView, for - consistent behavior across platforms. - - Emits: - :signal selection-changed: the selection has been changed - :signal selection-invalid: the item selected can no longer be selected - :signal deselected: all items have been deselected - """ - def __init__(self): - self._ignore_selection_changed = 0 - self._allow_multiple_select = None - self.create_signal('selection-changed') - self.create_signal('selection-invalid') - self.create_signal('deselected') - - @property - def allow_multiple_select(self): - """Return whether the widget allows multiple selection.""" - if self._allow_multiple_select is None: - self._allow_multiple_select = self._get_allow_multiple_select() - return self._allow_multiple_select - - @allow_multiple_select.setter - def allow_multiple_select(self, allow): - """Set whether to allow multiple selection; this method is expected - always to work. - """ - if self._allow_multiple_select != allow: - self._set_allow_multiple_select(allow) - self._allow_multiple_select = allow - - @property - def num_rows_selected(self): - """Override on platforms with a way to count rows without having to - retrieve them. - """ - if self.allow_multiple_select: - return len(self._get_selected_iters()) - else: - return int(self._get_selected_iter() is not None) - - def select(self, iter_, signal=False): - """Try to select an iter. - - :raises WidgetActionError: iter does not exist or is not selectable - """ - self.select_iters((iter_,), signal) - - def select_iters(self, iters, signal=False): - """Try to select multiple iters (signaling at most once). - - :raises WidgetActionError: iter does not exist or is not selectable - """ - with self._ignoring_changes(not signal): - for iter_ in iters: - self._select(iter_) - if not all(self._is_selected(iter_) for iter_ in iters): - raise WidgetActionError("the specified iter cannot be selected") - - def is_selected(self, iter_): - """Test if an iter is selected""" - return self._is_selected(iter_) - - def unselect(self, iter_): - """Unselect an Iter. Fails silently if the Iter is not selected. - """ - self._validate_iter(iter_) - with self._ignoring_changes(): - self._unselect(iter_) - - def unselect_iters(self, iters): - """Unselect iters. Fails silently if the iters are not selected.""" - with self._ignoring_changes(): - for iter_ in iters: - self.unselect(iter_) - - def unselect_all(self, signal=True): - """Unselect all. emits only the 'deselected' signal.""" - with self._ignoring_changes(): - self._unselect_all() - if signal: - self.emit('deselected') - - def on_selection_changed(self, _widget_or_notification): - """When we receive a selection-changed signal, we forward it if we're - not in a 'with _ignoring_changes' block. Selection-changed - handlers are run in an ignoring block, and anything that changes the - selection to reflect the current state. - """ - # don't bother sending out a second selection-changed signal if - # the handler changes the selection (#15767) - if not self._ignore_selection_changed: - with self._ignoring_changes(): - self.emit('selection-changed') - - def get_selection_as_strings(self): - """Returns the current selection as a list of strings. - """ - return [self._iter_to_string(iter_) for iter_ in self.get_selection()] - - def set_selection_as_strings(self, selected): - """Given a list of selection strings, selects each Iter represented by - the strings. - - Raises WidgetActionError upon failure. - """ - # iter may not be destringable (yet) - bounds error - # destringed iter not selectable if parent isn't open (yet) - self.set_selection(self._iter_from_string(sel) for sel in selected) - - def get_cursor(self): - """Get the location of the keyboard cursor for the tableview. - - Returns a string that represents the row that the keyboard cursor is - on. - """ - - def set_cursor(self, location): - """Set the location of the keyboard cursor for the tableview. - - :param location: return value from a call to get_cursor() - - Raises WidgetActionError upon failure. - """ - - def get_selection(self): - """Returns a list of GTK Iters. Works regardless of whether multiple - selection is enabled. - """ - return self._get_selected_iters() - - def get_selected(self): - """Return the single selected item. - - :raises WidgetUsageError: multiple selection is enabled - """ - if self.allow_multiple_select: - raise WidgetUsageError("table allows multiple selection") - return self._get_selected_iter() - - def _validate_iter(self, iter_): - """Check whether an iter is valid. - - :raises WidgetDomainError: the iter is not valid - :raises WidgetActionError: there is no model right now - """ - - @contextmanager - def _ignoring_changes(self, ignoring=True): - """Use this with with to prevent sending signals when we're changing - our own selection; that way, when we get a signal, we know it's - something important. - """ - if ignoring: - self._ignore_selection_changed += 1 - try: - yield - finally: - if ignoring: - self._ignore_selection_changed -= 1 - - @contextmanager - def preserving_selection(self): - """Prevent selection changes in a block from having any effect or - sticking - no signals will be sent, and the selection will be restored - to its original value when the block exits. - """ - iters = self._get_selected_iters() - with self._ignoring_changes(): - try: - yield - finally: - self.set_selection(iters) - - def set_selection(self, iters, signal=False): - """Set the selection to the given iters, replacing any previous - selection and signaling at most once. - """ - self.unselect_all(signal=False) - for iter_ in iters: - self.select(iter_, signal=False) - if signal: self.emit('selection-changed') diff --git a/mvc/widgets/widgetconst.py b/mvc/widgets/widgetconst.py deleted file mode 100644 index bbb513c..0000000 --- a/mvc/widgets/widgetconst.py +++ /dev/null @@ -1,44 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""``miro.frontends.widgets.widgetconst`` -- Constants for the widgets -frontend. -""" - -# Control sizes -SIZE_NORMAL = -1 -SIZE_SMALL = -2 - -TEXT_JUSTIFY_LEFT = 0 -TEXT_JUSTIFY_RIGHT = 1 -TEXT_JUSTIFY_CENTER = 2 - -# cursors -CURSOR_NORMAL = 0 -CURSOR_POINTING_HAND = 1 diff --git a/mvc/widgets/widgetutil.py b/mvc/widgets/widgetutil.py deleted file mode 100644 index 5fb3db2..0000000 --- a/mvc/widgets/widgetutil.py +++ /dev/null @@ -1,225 +0,0 @@ -from math import pi as PI -from mvc.widgets import widgetset -from mvc.resources import image_path - -def make_surface(image_name, height=None): - path = image_path(image_name + '.png') - image = widgetset.Image(path) - if height is not None: - image = image.resize(image.width, height) - return widgetset.ImageSurface(image) - -def font_scale_from_osx_points(points): - """Create a font scale so that it's points large on OS X. - - Assumptions (these should be true for OS X) - - the default font size is 13pt - - the DPI is 72ppi - """ - return points / 13.0 - -def css_to_color(css_string): - parts = (css_string[1:3], css_string[3:5], css_string[5:7]) - return tuple((int(value, 16) / 255.0) for value in parts) - -def align(widget, xalign=0, yalign=0, xscale=0, yscale=0, - top_pad=0, bottom_pad=0, left_pad=0, right_pad=0): - """Create an alignment, then add widget to it and return the alignment. - """ - alignment = widgetset.Alignment(xalign, yalign, xscale, yscale, - top_pad, bottom_pad, left_pad, right_pad) - alignment.add(widget) - return alignment - -def align_center(widget, top_pad=0, bottom_pad=0, left_pad=0, right_pad=0): - """Wrap a widget in an Alignment that will center it horizontally. - """ - return align(widget, 0.5, 0, 0, 1, - top_pad, bottom_pad, left_pad, right_pad) - -def align_right(widget, top_pad=0, bottom_pad=0, left_pad=0, right_pad=0): - """Wrap a widget in an Alignment that will align it left. - """ - return align(widget, 1, 0, 0, 1, top_pad, bottom_pad, left_pad, right_pad) - -def align_left(widget, top_pad=0, bottom_pad=0, left_pad=0, right_pad=0): - """Wrap a widget in an Alignment that will align it right. - """ - return align(widget, 0, 0, 0, 1, top_pad, bottom_pad, left_pad, right_pad) - -def align_middle(widget, top_pad=0, bottom_pad=0, left_pad=0, right_pad=0): - """Wrap a widget in an Alignment that will center it vertically. - """ - return align(widget, 0, 0.5, 1, 0, - top_pad, bottom_pad, left_pad, right_pad) - -def align_top(widget, top_pad=0, bottom_pad=0, left_pad=0, right_pad=0): - """Wrap a widget in an Alignment that will align to the top. - """ - return align(widget, 0, 0, 1, 0, top_pad, bottom_pad, left_pad, right_pad) - -def align_bottom(widget, top_pad=0, bottom_pad=0, left_pad=0, right_pad=0): - """Wrap a widget in an Alignment that will align to the bottom. - """ - return align(widget, 0, 1, 1, 0, top_pad, bottom_pad, left_pad, right_pad) - -def pad(widget, top=0, bottom=0, left=0, right=0): - """Wrap a widget in an Alignment that will pad it. - """ - alignment = widgetset.Alignment(0, 0, 1, 1, - top, bottom, left, right) - alignment.add(widget) - return alignment - -def round_rect(context, x, y, width, height, edge_radius): - """Specifies path of a rectangle with rounded corners. - """ - edge_radius = min(edge_radius, min(width, height)/2.0) - inner_width = width - edge_radius*2 - inner_height = height - edge_radius*2 - x_inner1 = x + edge_radius - x_inner2 = x + width - edge_radius - y_inner1 = y + edge_radius - y_inner2 = y + height - edge_radius - - context.move_to(x+edge_radius, y) - context.rel_line_to(inner_width, 0) - context.arc(x_inner2, y_inner1, edge_radius, -PI/2, 0) - context.rel_line_to(0, inner_height) - context.arc(x_inner2, y_inner2, edge_radius, 0, PI/2) - context.rel_line_to(-inner_width, 0) - context.arc(x_inner1, y_inner2, edge_radius, PI/2, PI) - context.rel_line_to(0, -inner_height) - context.arc(x_inner1, y_inner1, edge_radius, PI, PI*3/2) - -def round_rect_reverse(context, x, y, width, height, edge_radius): - """Specifies path of a rectangle with rounded corners. - - This specifies the rectangle in a counter-clockwise fashion. - """ - edge_radius = min(edge_radius, min(width, height)/2.0) - inner_width = width - edge_radius*2 - inner_height = height - edge_radius*2 - x_inner1 = x + edge_radius - x_inner2 = x + width - edge_radius - y_inner1 = y + edge_radius - y_inner2 = y + height - edge_radius - - context.move_to(x+edge_radius, y) - context.arc_negative(x_inner1, y_inner1, edge_radius, PI*3/2, PI) - context.rel_line_to(0, inner_height) - context.arc_negative(x_inner1, y_inner2, edge_radius, PI, PI/2) - context.rel_line_to(inner_width, 0) - context.arc_negative(x_inner2, y_inner2, edge_radius, PI/2, 0) - context.rel_line_to(0, -inner_height) - context.arc_negative(x_inner2, y_inner1, edge_radius, 0, -PI/2) - context.rel_line_to(-inner_width, 0) - -def circular_rect(context, x, y, width, height): - """Make a path for a rectangle with the left/right side being circles. - """ - radius = height / 2.0 - inner_width = width - height - inner_y = y + radius - inner_x1 = x + radius - inner_x2 = inner_x1 + inner_width - - context.move_to(inner_x1, y) - context.rel_line_to(inner_width, 0) - context.arc(inner_x2, inner_y, radius, -PI/2, PI/2) - context.rel_line_to(-inner_width, 0) - context.arc(inner_x1, inner_y, radius, PI/2, -PI/2) - -def circular_rect_negative(context, x, y, width, height): - """The same path as ``circular_rect()``, but going counter clockwise. - """ - radius = height / 2.0 - inner_width = width - height - inner_y = y + radius - inner_x1 = x + radius - inner_x2 = inner_x1 + inner_width - - context.move_to(inner_x1, y) - context.arc_negative(inner_x1, inner_y, radius, -PI/2, PI/2) - context.rel_line_to(inner_width, 0) - context.arc_negative(inner_x2, inner_y, radius, PI/2, -PI/2) - context.rel_line_to(-inner_width, 0) - -class Shadow(object): - """Encapsulates all parameters required to draw shadows. - """ - def __init__(self, color, opacity, offset, blur_radius): - self.color = color - self.opacity = opacity - self.offset = offset - self.blur_radius = blur_radius - -class ThreeImageSurface(object): - """Takes a left, center and right image and draws them to an arbitrary - width. If the width is greater than the combined width of the 3 images, - then the center image will be tiled to compensate. - - Example: - - >>> timelinebar = ThreeImageSurface("timelinebar") - - This creates a ``ThreeImageSurface`` using the images - ``images/timelinebar_left.png``, ``images/timelinebar_center.png``, and - ``images/timelinebar_right.png``. - - Example: - - >>> timelinebar = ThreeImageSurface() - >>> img_left = make_surface("timelinebar_left") - >>> img_center = make_surface("timelinebar_center") - >>> img_right = make_surface("timelinebar_right") - >>> timelinebar.set_images(img_left, img_center, img_right) - - This does the same thing, but allows you to explicitly set which images - get used. - """ - def __init__(self, basename=None, height=None): - self.left = self.center = self.right = None - self.height = 0 - self.width = None - if basename is not None: - left = make_surface(basename + '_left', height) - center = make_surface(basename + '_center', height) - right = make_surface(basename + '_right', height) - self.set_images(left, center, right) - - def set_images(self, left, center, right): - """Sets the left, center and right images to use. - """ - self.left = left - self.center = center - self.right = right - if not (self.left.height == self.center.height == self.right.height): - raise ValueError("Images aren't the same height") - self.height = self.left.height - - def set_width(self, width): - """Manually set a width. - - When ThreeImageSurface have a width, then they have pretty much the - same API as ImageSurface does. In particular, they can now be nested - in another ThreeImageSurface. - """ - self.width = width - - def get_size(self): - return self.width, self.height - - def draw(self, context, x, y, width, fraction=1.0): - left_width = min(self.left.width, width) - self.left.draw(context, x, y, left_width, self.height, fraction) - self.draw_right(context, x + left_width, y, width - left_width, fraction) - - def draw_right(self, context, x, y, width, fraction=1.0): - # draws only the right two images - - right_width = min(self.right.width, width) - center_width = int(width - right_width) - - self.center.draw(context, x, y, center_width, self.height, fraction) - self.right.draw(context, x + center_width, y, right_width, self.height, fraction) |