class Canvas(Gtk.DrawingArea, Gtk.Scrollable): """ Canvas are area where Drawer can draw: The main canvas is where page(s) are drawn. This is the biggest and most important part of the main window. """ hadjustment = GObject.property(type=Gtk.Adjustment, default=Gtk.Adjustment(), flags=GObject.PARAM_READWRITE) hscroll_policy = GObject.property(type=Gtk.ScrollablePolicy, default=Gtk.ScrollablePolicy.MINIMUM, flags=GObject.PARAM_READWRITE) vadjustment = GObject.property(type=Gtk.Adjustment, default=Gtk.Adjustment(), flags=GObject.PARAM_READWRITE) vscroll_policy = GObject.property(type=Gtk.ScrollablePolicy, default=Gtk.ScrollablePolicy.MINIMUM, flags=GObject.PARAM_READWRITE) __gsignals__ = { 'absolute-button-press-event': (GObject.SignalFlags.RUN_LAST, None, (GObject.TYPE_PYOBJECT, )), 'absolute-motion-notify-event': (GObject.SignalFlags.RUN_LAST, None, (GObject.TYPE_PYOBJECT, )), 'absolute-button-release-event': (GObject.SignalFlags.RUN_LAST, None, (GObject.TYPE_PYOBJECT, )), 'window-moved': (GObject.SignalFlags.RUN_LAST, None, ()), } TICK_INTERVAL = (1000.0 / 16) def __init__(self, scrollbars): Gtk.DrawingArea.__init__(self) hadj = scrollbars.get_hadjustment() vadj = scrollbars.get_vadjustment() self.full_size = (1, 1) self.visible_size = (1, 1) self.mouse_position = (0, 0) self.drawers = PriorityQueue() self.tick_counter_lock = threading.Lock() self.set_hadjustment(hadj) self.set_vadjustment(vadj) self.add_events(Gdk.EventMask.SCROLL_MASK) self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK) self.add_events(Gdk.EventMask.BUTTON_RELEASE_MASK) self.add_events(Gdk.EventMask.POINTER_MOTION_MASK) self.add_events(Gdk.EventMask.KEY_PRESS_MASK) self.add_events(Gdk.EventMask.LEAVE_NOTIFY_MASK) super(Canvas, self).connect("size-allocate", self.__on_size_allocate) super(Canvas, self).connect("draw", self.__on_draw) super(Canvas, self).connect("button-press-event", self.__on_button_pressed) super(Canvas, self).connect("motion-notify-event", self.__on_motion) super(Canvas, self).connect("button-release-event", self.__on_button_released) super(Canvas, self).connect("key-press-event", self.__on_key_pressed) super(Canvas, self).connect("leave-notify-event", self.__on_mouse_leave) hadj.connect("value-changed", self.__on_adjustment_changed) vadj.connect("value-changed", self.__on_adjustment_changed) self.set_size_request(-1, -1) self.set_can_focus(True) self.need_ticks = 0 self.need_stop_ticks = 0 self._drawer_connections = {} # drawer --> [('signal', func), ...] self.__scroll_origin = (0, 0) self.__cursor_drawer = None def _tick(self): for drawer in self.drawers: drawer.on_tick() self.__apply_scrolling() self.tick_counter_lock.acquire() try: if self.need_stop_ticks > 0: self.need_stop_ticks -= 1 assert (self.need_ticks >= 0) return False return (self.need_ticks > 0) finally: self.tick_counter_lock.release() def start_ticks(self): self.tick_counter_lock.acquire() try: self.need_ticks += 1 if self.need_ticks == 1: GLib.timeout_add(self.TICK_INTERVAL, self._tick) logger.info("Animators: %d" % self.need_ticks) finally: self.tick_counter_lock.release() def stop_ticks(self): self.tick_counter_lock.acquire() try: self.need_ticks -= 1 logger.info("Animators: %d" % self.need_ticks) if self.need_ticks <= 0: self.need_stop_ticks += 1 assert (self.need_ticks >= 0) finally: self.tick_counter_lock.release() def get_hadjustment(self): return self.hadjustment def set_hadjustment(self, h): self.upd_adjustments(upd_scrollbar_values=False) Gtk.Scrollable.set_hadjustment(self, h) self.set_property("hadjustment", h) h.connect("value-changed", self.__on_adjustment_changed) def get_vadjustment(self): return self.vadjustment def set_vadjustment(self, v): self.upd_adjustments(upd_scrollbar_values=False) Gtk.Scrollable.set_vadjustment(self, v) self.set_property("vadjustment", v) v.connect("value-changed", self.__on_adjustment_changed) def __on_adjustment_changed(self, adjustment): self.redraw() self.emit('window-moved') def __on_size_allocate(self, _, size_allocate): self.visible_size = (size_allocate.width, size_allocate.height) self.upd_adjustments(upd_scrollbar_values=False) self.redraw() def recompute_size(self, upd_scrollbar_values=False): (full_x, full_y) = (1, 1) for drawer in self.drawers: x = drawer.position[0] + drawer.size[0] y = drawer.position[1] + drawer.size[1] if (full_x < x): full_x = x if (full_y < y): full_y = y new_size = (full_x, full_y) if (new_size[0] != self.full_size[0] or new_size[1] != self.full_size[1]): self.full_size = new_size self.set_size_request(new_size[0], new_size[1]) self.upd_adjustments(upd_scrollbar_values) def upd_adjustments(self, upd_scrollbar_values=False): max_h = max(float(self.visible_size[0]), float(self.full_size[0]), 100.0) max_v = max(float(self.visible_size[1]), float(self.full_size[1]), 100.0) current = [ float(self.hadjustment.get_value()), float(self.vadjustment.get_value()) ] if not upd_scrollbar_values: vals = list(current) else: if self.mouse_position == (0, 0): current_center = [ current[0] + (self.visible_size[0] / 2), current[1] + (self.visible_size[1] / 2) ] # special cases: extreme values if current[0] == 0: current_center[0] = 0 if current[1] == 0: current_center[1] = 0 if current[0] >= self.hadjustment.get_upper(): current_center[0] = self.hadjustment.get_upper() if current[1] >= self.vadjustment.get_upper(): current_center[1] = self.vadjustment.get_upper() else: # track the mouse pointer current_center = (current[0] + self.mouse_position[0], current[1] + self.mouse_position[1]) vals = [0, 0] for (adj, pos, new_max, idx) in [(self.hadjustment, current_center[0], max_h, 0), (self.vadjustment, current_center[1], max_v, 1)]: adj_min = adj.get_lower() adj_max = max(1, adj.get_upper()) proportional = ((float(pos) - adj_min) / adj_max) vals[idx] = int(proportional * new_max) if self.mouse_position == (0, 0): vals[idx] -= self.visible_size[idx] / 2 else: vals[idx] -= self.mouse_position[idx] for (adj, pos, new_max, idx) in [(self.hadjustment, self.mouse_position[0], max_h, 0), (self.vadjustment, self.mouse_position[1], max_v, 1)]: if vals[idx] > self.full_size[idx]: vals[idx] = self.full_size[idx] adj.set_lower(0) adj.set_upper(new_max) adj.set_page_size(self.visible_size[idx]) adj.set_value(int(vals[idx])) def __on_draw(self, _, cairo_ctx): self.recompute_size(upd_scrollbar_values=False) for drawer in self.drawers: cairo_ctx.save() try: offset = self.offset cairo_ctx.translate(-offset[0], -offset[1]) drawer.draw(cairo_ctx) finally: cairo_ctx.restore() def __get_offset(self): x = int(self.hadjustment.get_value()) y = int(self.vadjustment.get_value()) return (x, y) offset = property(__get_offset) def __get_visible_size(self): return self.visible_size size = property(__get_visible_size) def add_drawer(self, drawer): drawer.set_canvas(self) self.drawers.add(drawer.layer, drawer) drawer.show() self.recompute_size(upd_scrollbar_values=False) self.redraw((drawer.relative_position, drawer.relative_size)) def get_drawer_at(self, position): (x, y) = position for drawer in self.drawers: pt_a = drawer.position pt_b = (drawer.position[0] + drawer.size[0], drawer.position[1] + drawer.size[1]) if (x >= pt_a[0] and x < pt_b[0] and y >= pt_a[1] and y < pt_b[1]): return drawer return None def connect(self, drawer, signal, func, *args, **kwargs): """ Force the caller to declare a drawer for this connection. So when the drawer is removed, we can automatically remove its connections Arguments: drawer --- None allowed """ handler_id = super(Canvas, self).connect(signal, func, *args, **kwargs) if drawer is not None: if drawer not in self._drawer_connections: self._drawer_connections[drawer] = [handler_id] else: self._drawer_connections[drawer].append(handler_id) def disconnect_drawer(self, drawer): if drawer not in self._drawer_connections: return connections = self._drawer_connections.pop(drawer) for handler_id in connections: super(Canvas, self).disconnect(handler_id) def remove_drawer(self, drawer): self.disconnect_drawer(drawer) drawer.hide() self.drawers.remove(drawer) self.recompute_size(upd_scrollbar_values=False) self.redraw() def remove_drawers(self, drawers): for drawer in drawers: self.disconnect_drawer(drawer) drawer.hide() self.drawers.remove(drawer) self.recompute_size(upd_scrollbar_values=False) self.redraw() def remove_all_drawers(self): for drawer in self.drawers: self.disconnect_drawer(drawer) drawer.hide() self.drawers.purge() self.recompute_size(upd_scrollbar_values=False) self.redraw() def redraw(self, area=None): if area is None: self.queue_draw() else: offset = self.offset visible = self.visible_size position = [area[0][0] - offset[0], area[0][1] - offset[1]] size = [area[1][0], area[1][1]] if position[0] < 0: size[0] += position[0] position[0] = 0 if position[1] < 0: size[1] += position[1] position[1] = 0 if (position[0] > visible[0] or position[1] > visible[1] or size[0] <= 0 or size[1] <= 0): logger.warning("Ignore useless call to redraw()") return self.queue_draw_area(position[0], position[1], size[0], size[1]) def __get_absolute_event(self, event): off_x = int(self.hadjustment.get_value()) off_y = int(self.vadjustment.get_value()) return AbsoluteEvent(event, (off_x, off_y)) def __on_button_pressed(self, _, event): if event.button == 2: # middle button self.__scroll_origin = (event.x, event.y) logger.info("Start scrolling with 3rd button ({})".format( self.__scroll_origin)) self.start_ticks() display = self.get_display() try: mouse_cursor = Gdk.Cursor.new_from_name(display, "all-scroll") origin_cursor = Gdk.Cursor.new_from_name(display, "crosshair") except: mouse_cursor = Gdk.Cursor.new_for_display( display, Gdk.CursorType.FLEUR) origin_cursor = Gdk.Cursor.new_for_display( display, Gdk.CursorType.CROSS) self.get_window().set_cursor(mouse_cursor) self.__cursor_drawer = CursorDrawer(origin_cursor, (event.x, event.y)) self.add_drawer(self.__cursor_drawer) return False self.grab_focus() event = self.__get_absolute_event(event) self.emit('absolute-button-press-event', event) def __on_motion(self, _, event): self.mouse_position = (event.x, event.y) event = self.__get_absolute_event(event) self.emit('absolute-motion-notify-event', event) def __on_button_released(self, _, event): if self.__scroll_origin != (0, 0): self.stop_ticks() self.__scroll_origin = (0, 0) self.get_window().set_cursor(None) self.remove_drawer(self.__cursor_drawer) event = self.__get_absolute_event(event) self.emit('absolute-button-release-event', event) def __scroll(self, offset): h = self.hadjustment.get_value() v = self.vadjustment.get_value() h += offset[0] v += offset[1] if h != self.hadjustment.get_value(): if h < self.hadjustment.get_lower(): h = self.hadjustment.get_lower() if h > self.hadjustment.get_upper(): h = self.hadjustment.get_upper() if h != self.hadjustment.get_value(): self.hadjustment.set_value(h) if v != self.vadjustment.get_value(): if v < self.vadjustment.get_lower(): v = self.vadjustment.get_lower() if v > self.vadjustment.get_upper(): v = self.vadjustment.get_upper() if v != self.vadjustment.get_value(): self.vadjustment.set_value(v) return True def __on_key_pressed(self, _, event): h_offset = 100 v_offset = 100 ops = { Gdk.KEY_Left: (-h_offset, 0), Gdk.KEY_Right: (h_offset, 0), Gdk.KEY_Up: (0, -v_offset), Gdk.KEY_Down: (0, +v_offset), } if event.keyval not in ops: return False offset = ops[event.keyval] return self.__scroll(offset) def __on_mouse_leave(self, _, event): self.mouse_position = (0, 0) def __apply_scrolling(self): if (self.__scroll_origin == (0, 0) or self.mouse_position == (0, 0)): # no scrolling for now return SCROLLING_REDUCTION_FACTOR = 2 scroll_x = self.mouse_position[0] - self.__scroll_origin[0] scroll_y = self.mouse_position[1] - self.__scroll_origin[1] scroll_x /= SCROLLING_REDUCTION_FACTOR scroll_y /= SCROLLING_REDUCTION_FACTOR scroll_x = max(min(scroll_x, 50), -50) scroll_y = max(min(scroll_y, 50), -50) return self.__scroll((scroll_x, scroll_y)) def __get_position(self): return (int(self.hadjustment.get_value()), int(self.vadjustment.get_value())) position = property(__get_position)
class Canvas(Gtk.DrawingArea, Gtk.Scrollable): """ Canvas are area where Drawer can draw: The main canvas is where page(s) are drawn. This is the biggest and most important part of the main window. """ hadjustment = GObject.property(type=Gtk.Adjustment, default=Gtk.Adjustment(), flags=GObject.PARAM_READWRITE) hscroll_policy = GObject.property(type=Gtk.ScrollablePolicy, default=Gtk.ScrollablePolicy.MINIMUM, flags=GObject.PARAM_READWRITE) vadjustment = GObject.property(type=Gtk.Adjustment, default=Gtk.Adjustment(), flags=GObject.PARAM_READWRITE) vscroll_policy = GObject.property(type=Gtk.ScrollablePolicy, default=Gtk.ScrollablePolicy.MINIMUM, flags=GObject.PARAM_READWRITE) __gsignals__ = { 'absolute-button-press-event': (GObject.SignalFlags.RUN_LAST, None, (GObject.TYPE_PYOBJECT,)), 'absolute-motion-notify-event': (GObject.SignalFlags.RUN_LAST, None, (GObject.TYPE_PYOBJECT,)), 'absolute-button-release-event': (GObject.SignalFlags.RUN_LAST, None, (GObject.TYPE_PYOBJECT,)), 'window-moved': (GObject.SignalFlags.RUN_LAST, None, ()), } TICK_INTERVAL = (1000.0 / 5) def __init__(self, scrollbars): Gtk.DrawingArea.__init__(self) self.redraw_queued = False hadj = scrollbars.get_hadjustment() vadj = scrollbars.get_vadjustment() self.full_size = (1, 1) self.visible_size = (1, 1) self.mouse_position = (0, 0) self.drawers = PriorityQueue() self.tick_counter_lock = threading.Lock() self.set_hadjustment(hadj) self.set_vadjustment(vadj) self.add_events(Gdk.EventMask.SCROLL_MASK) self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK) self.add_events(Gdk.EventMask.BUTTON_RELEASE_MASK) self.add_events(Gdk.EventMask.POINTER_MOTION_MASK) self.add_events(Gdk.EventMask.KEY_PRESS_MASK) self.add_events(Gdk.EventMask.LEAVE_NOTIFY_MASK) super(Canvas, self).connect("size-allocate", self.__on_size_allocate) super(Canvas, self).connect("draw", self.__on_draw) super(Canvas, self).connect("button-press-event", self.__on_button_pressed) super(Canvas, self).connect("motion-notify-event", self.__on_motion) super(Canvas, self).connect("button-release-event", self.__on_button_released) super(Canvas, self).connect("key-press-event", self.__on_key_pressed) super(Canvas, self).connect("leave-notify-event", self.__on_mouse_leave) hadj.connect("value-changed", self.__on_adjustment_changed) vadj.connect("value-changed", self.__on_adjustment_changed) self.set_size_request(-1, -1) self.set_can_focus(True) self.need_ticks = 0 self.need_stop_ticks = 0 self._drawer_connections = {} # drawer --> [('signal', func), ...] self.__scroll_origin = (0, 0) self.__cursor_drawer = None def _tick(self): for drawer in self.drawers: drawer.on_tick() self.__apply_scrolling() with self.tick_counter_lock: if self.need_stop_ticks > 0: self.need_stop_ticks -= 1 assert(self.need_ticks >= 0) return False return (self.need_ticks > 0) def start_ticks(self): with self.tick_counter_lock: self.need_ticks += 1 if self.need_ticks == 1: GLib.timeout_add(self.TICK_INTERVAL, self._tick) logger.info("Animators: %d" % self.need_ticks) def stop_ticks(self): with self.tick_counter_lock: self.need_ticks -= 1 logger.info("Animators: %d" % self.need_ticks) if self.need_ticks <= 0: self.need_stop_ticks += 1 assert(self.need_ticks >= 0) def get_hadjustment(self): return self.hadjustment def set_hadjustment(self, h): self.upd_adjustments(upd_scrollbar_values=False) Gtk.Scrollable.set_hadjustment(self, h) self.set_property("hadjustment", h) h.connect("value-changed", self.__on_adjustment_changed) def get_vadjustment(self): return self.vadjustment def set_vadjustment(self, v): self.upd_adjustments(upd_scrollbar_values=False) Gtk.Scrollable.set_vadjustment(self, v) self.set_property("vadjustment", v) v.connect("value-changed", self.__on_adjustment_changed) def __on_adjustment_changed(self, adjustment): self.redraw() self.emit('window-moved') def __on_size_allocate(self, _, size_allocate): self.visible_size = (size_allocate.width, size_allocate.height) self.upd_adjustments(upd_scrollbar_values=False) self.redraw(checked=True) def recompute_size(self, upd_scrollbar_values=False): (full_x, full_y) = (1, 1) for drawer in self.drawers: x = drawer.position[0] + drawer.size[0] y = drawer.position[1] + drawer.size[1] if (full_x < x): full_x = x if (full_y < y): full_y = y new_size = (full_x, full_y) if (new_size[0] != self.full_size[0] or new_size[1] != self.full_size[1]): self.full_size = new_size self.set_size_request(new_size[0], new_size[1]) self.upd_adjustments(upd_scrollbar_values) def upd_adjustments(self, upd_scrollbar_values=False): max_h = max(float(self.visible_size[0]), float(self.full_size[0]), 100.0) max_v = max(float(self.visible_size[1]), float(self.full_size[1]), 100.0) current = [ float(self.hadjustment.get_value()), float(self.vadjustment.get_value()) ] if not upd_scrollbar_values: vals = list(current) else: if self.mouse_position == (0, 0): current_center = [ current[0] + (self.visible_size[0] / 2), current[1] + (self.visible_size[1] / 2) ] # special cases: extreme values if current[0] == 0: current_center[0] = 0 if current[1] == 0: current_center[1] = 0 if current[0] >= self.hadjustment.get_upper(): current_center[0] = self.hadjustment.get_upper() if current[1] >= self.vadjustment.get_upper(): current_center[1] = self.vadjustment.get_upper() else: # track the mouse pointer current_center = ( current[0] + self.mouse_position[0], current[1] + self.mouse_position[1] ) vals = [0, 0] for (adj, pos, new_max, idx) in [ (self.hadjustment, current_center[0], max_h, 0), (self.vadjustment, current_center[1], max_v, 1) ]: adj_min = adj.get_lower() adj_max = max(1, adj.get_upper()) proportional = ((float(pos) - adj_min) / adj_max) vals[idx] = int(proportional * new_max) if self.mouse_position == (0, 0): vals[idx] -= self.visible_size[idx] / 2 else: vals[idx] -= self.mouse_position[idx] for (adj, pos, new_max, idx) in [ (self.hadjustment, self.mouse_position[0], max_h, 0), (self.vadjustment, self.mouse_position[1], max_v, 1) ]: if vals[idx] > self.full_size[idx]: vals[idx] = self.full_size[idx] adj.set_lower(0) adj.set_upper(new_max) adj.set_page_size(self.visible_size[idx]) adj.set_value(int(vals[idx])) def __on_draw(self, _, cairo_ctx): self.redraw_queued = False self.recompute_size(upd_scrollbar_values=False) for drawer in self.drawers: cairo_ctx.save() try: offset = self.offset cairo_ctx.translate(-offset[0], -offset[1]) drawer.draw(cairo_ctx) finally: cairo_ctx.restore() def __get_offset(self): x = int(self.hadjustment.get_value()) y = int(self.vadjustment.get_value()) return (x, y) offset = property(__get_offset) def __get_visible_size(self): return self.visible_size size = property(__get_visible_size) def add_drawer(self, drawer): drawer.set_canvas(self) self.drawers.add(drawer.layer, drawer) drawer.show() self.recompute_size(upd_scrollbar_values=False) drawer.redraw() def get_drawer_at(self, position): (x, y) = position for drawer in self.drawers: pt_a = drawer.position pt_b = (drawer.position[0] + drawer.size[0], drawer.position[1] + drawer.size[1]) if (x >= pt_a[0] and x < pt_b[0] and y >= pt_a[1] and y < pt_b[1]): return drawer return None def connect(self, drawer, signal, func, *args, **kwargs): """ Force the caller to declare a drawer for this connection. So when the drawer is removed, we can automatically remove its connections Arguments: drawer --- None allowed """ handler_id = super(Canvas, self).connect(signal, func, *args, **kwargs) if drawer is not None: if drawer not in self._drawer_connections: self._drawer_connections[drawer] = [handler_id] else: self._drawer_connections[drawer].append(handler_id) def disconnect_drawer(self, drawer): if drawer not in self._drawer_connections: return connections = self._drawer_connections.pop(drawer) for handler_id in connections: super(Canvas, self).disconnect(handler_id) def remove_drawer(self, drawer): self.disconnect_drawer(drawer) drawer.hide() self.drawers.remove(drawer) self.recompute_size(upd_scrollbar_values=False) self.redraw((drawer.position, drawer.size)) def remove_drawers(self, drawers): for drawer in drawers: self.disconnect_drawer(drawer) drawer.hide() self.drawers.remove(drawer) self.redraw((drawer.position, drawer.size)) self.recompute_size(upd_scrollbar_values=False) def remove_all_drawers(self): for drawer in self.drawers: self.disconnect_drawer(drawer) drawer.hide() self.drawers.purge() self.recompute_size(upd_scrollbar_values=False) self.redraw(checked=True) def redraw(self, area=None, checked=False): if self.redraw_queued: # a global redraw has already been asked return if area is None: if not checked and CANVAS_ERROR_ON_USELESS: raise CanvasException("Unchecked global call to redraw()") self.queue_draw() self.redraw_queued = True else: offset = self.offset visible = self.visible_size position = [area[0][0] - offset[0], area[0][1] - offset[1]] size = [area[1][0], area[1][1]] if position[0] < 0: size[0] += position[0] position[0] = 0 if position[1] < 0: size[1] += position[1] position[1] = 0 if (position[0] > visible[0] or position[1] > visible[1] or size[0] <= 0 or size[1] <= 0): if CANVAS_ERROR_ON_USELESS: if (visible <= (1, 1)): # Main window isn't visible yet return raise CanvasException( "Useless call to redraw():" " {} --> {} (visible: {})".format( area, (position, size), visible ) ) return self.queue_draw_area( position[0], position[1], size[0], size[1] ) def __get_absolute_event(self, event): off_x = int(self.hadjustment.get_value()) off_y = int(self.vadjustment.get_value()) return AbsoluteEvent(event, (off_x, off_y)) def __on_button_pressed(self, _, event): if event.button == 2: # middle button self.__scroll_origin = (event.x, event.y) logger.info("Start scrolling with 3rd button ({})".format( self.__scroll_origin )) self.start_ticks() display = self.get_display() try: mouse_cursor = Gdk.Cursor.new_from_name(display, "all-scroll") origin_cursor = Gdk.Cursor.new_from_name(display, "crosshair") except: # noqa: E722 mouse_cursor = Gdk.Cursor.new_for_display( display, Gdk.CursorType.FLEUR ) origin_cursor = Gdk.Cursor.new_for_display( display, Gdk.CursorType.CROSS ) self.get_window().set_cursor(mouse_cursor) self.__cursor_drawer = CursorDrawer( origin_cursor, (event.x, event.y) ) self.add_drawer(self.__cursor_drawer) return False self.grab_focus() event = self.__get_absolute_event(event) self.emit('absolute-button-press-event', event) def __on_motion(self, _, event): self.mouse_position = (event.x, event.y) event = self.__get_absolute_event(event) self.emit('absolute-motion-notify-event', event) def __on_button_released(self, _, event): if self.__scroll_origin != (0, 0): self.stop_ticks() self.__scroll_origin = (0, 0) self.get_window().set_cursor(None) self.remove_drawer(self.__cursor_drawer) event = self.__get_absolute_event(event) self.emit('absolute-button-release-event', event) def __scroll(self, offset): h = self.hadjustment.get_value() v = self.vadjustment.get_value() h += offset[0] v += offset[1] if h != self.hadjustment.get_value(): if h < self.hadjustment.get_lower(): h = self.hadjustment.get_lower() if h > self.hadjustment.get_upper(): h = self.hadjustment.get_upper() if h != self.hadjustment.get_value(): self.hadjustment.set_value(h) if v != self.vadjustment.get_value(): if v < self.vadjustment.get_lower(): v = self.vadjustment.get_lower() if v > self.vadjustment.get_upper(): v = self.vadjustment.get_upper() if v != self.vadjustment.get_value(): self.vadjustment.set_value(v) return True def __on_key_pressed(self, _, event): h_offset = 100 v_offset = 100 ops = { Gdk.KEY_Left: (-h_offset, 0), Gdk.KEY_Right: (h_offset, 0), Gdk.KEY_Up: (0, -v_offset), Gdk.KEY_Down: (0, +v_offset), } if event.keyval not in ops: return False offset = ops[event.keyval] return self.__scroll(offset) def __on_mouse_leave(self, _, event): self.mouse_position = (0, 0) def __apply_scrolling(self): if (self.__scroll_origin == (0, 0) or self.mouse_position == (0, 0)): # no scrolling for now return SCROLLING_REDUCTION_FACTOR = 2 scroll_x = self.mouse_position[0] - self.__scroll_origin[0] scroll_y = self.mouse_position[1] - self.__scroll_origin[1] scroll_x /= SCROLLING_REDUCTION_FACTOR scroll_y /= SCROLLING_REDUCTION_FACTOR scroll_x = max(min(scroll_x, 50), -50) scroll_y = max(min(scroll_y, 50), -50) return self.__scroll((scroll_x, scroll_y)) def __get_position(self): if self.hadjustment is None or self.vadjustment is None: # May happen when Paperwork is closing return (0, 0) return (int(self.hadjustment.get_value()), int(self.vadjustment.get_value())) position = property(__get_position)
class Canvas(Gtk.DrawingArea, Gtk.Scrollable): """ Canvas are area where Drawer can draw: The main canvas is where page(s) are drawn. This is the biggest and most important part of the main window. """ hadjustment = GObject.property(type=Gtk.Adjustment, default=Gtk.Adjustment(), flags=GObject.PARAM_READWRITE) hscroll_policy = GObject.property(type=Gtk.ScrollablePolicy, default=Gtk.ScrollablePolicy.MINIMUM, flags=GObject.PARAM_READWRITE) vadjustment = GObject.property(type=Gtk.Adjustment, default=Gtk.Adjustment(), flags=GObject.PARAM_READWRITE) vscroll_policy = GObject.property(type=Gtk.ScrollablePolicy, default=Gtk.ScrollablePolicy.MINIMUM, flags=GObject.PARAM_READWRITE) __gsignals__ = { 'absolute-button-press-event': (GObject.SignalFlags.RUN_LAST, None, (GObject.TYPE_PYOBJECT, )), 'absolute-motion-notify-event': (GObject.SignalFlags.RUN_LAST, None, (GObject.TYPE_PYOBJECT, )), 'absolute-button-release-event': (GObject.SignalFlags.RUN_LAST, None, (GObject.TYPE_PYOBJECT, )), 'window-moved': (GObject.SignalFlags.RUN_LAST, None, ()), } TICK_INTERVAL = (1000 / 10) def __init__(self, scrollbars): Gtk.DrawingArea.__init__(self) hadj = scrollbars.get_hadjustment() vadj = scrollbars.get_vadjustment() self.full_size = (1, 1) self.visible_size = (1, 1) self.drawers = PriorityQueue() self.tick_counter_lock = threading.Lock() self.set_hadjustment(hadj) self.set_vadjustment(vadj) self.add_events(Gdk.EventMask.SCROLL_MASK) self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK) self.add_events(Gdk.EventMask.BUTTON_RELEASE_MASK) self.add_events(Gdk.EventMask.POINTER_MOTION_MASK) self.connect("size-allocate", self.__on_size_allocate) self.connect("draw", self.__on_draw) self.connect("button-press-event", self.__on_button_pressed) self.connect("motion-notify-event", self.__on_motion) self.connect("button-release-event", self.__on_button_released) self.connect("key-press-event", self.__on_key_pressed) hadj.connect("value-changed", self.__on_adjustment_changed) vadj.connect("value-changed", self.__on_adjustment_changed) self.set_size_request(-1, -1) self.set_can_focus(True) self.need_ticks = 0 def _tick(self): for drawer in self.drawers: drawer.on_tick() self.tick_counter_lock.acquire() try: if self.need_ticks > 0: GLib.timeout_add(self.TICK_INTERVAL, self._tick) finally: self.tick_counter_lock.release() def start_ticks(self): self.tick_counter_lock.acquire() try: self.need_ticks += 1 if self.need_ticks == 1: GLib.timeout_add(self.TICK_INTERVAL, self._tick) logger.info("Animators: %d" % self.need_ticks) finally: self.tick_counter_lock.release() def stop_ticks(self): self.tick_counter_lock.acquire() try: self.need_ticks -= 1 logger.info("Animators: %d" % self.need_ticks) assert (self.need_ticks >= 0) finally: self.tick_counter_lock.release() def get_hadjustment(self): return self.hadjustment def set_hadjustment(self, h): Gtk.Scrollable.set_hadjustment(self, h) self.set_property("hadjustment", h) self.upd_adjustments() h.connect("value-changed", self.__on_adjustment_changed) def get_vadjustment(self): return self.vadjustment def set_vadjustment(self, v): Gtk.Scrollable.set_vadjustment(self, v) self.set_property("vadjustment", v) self.upd_adjustments() v.connect("value-changed", self.__on_adjustment_changed) def __on_adjustment_changed(self, adjustment): self.redraw() self.emit('window-moved') def __on_size_allocate(self, _, size_allocate): self.visible_size = (size_allocate.width, size_allocate.height) self.upd_adjustments() self.redraw() def recompute_size(self): (full_x, full_y) = (1, 1) for drawer in self.drawers: x = drawer.position[0] + drawer.size[0] y = drawer.position[1] + drawer.size[1] if (full_x < x): full_x = x if (full_y < y): full_y = y new_size = (full_x, full_y) if (new_size[0] != self.full_size[0] or new_size[1] != self.full_size[1]): self.full_size = new_size self.set_size_request(new_size[0], new_size[1]) self.upd_adjustments() def upd_adjustments(self): val_h = float(self.hadjustment.get_value()) val_v = float(self.vadjustment.get_value()) max_h = max(float(self.visible_size[0]), float(self.full_size[0]), 100.0) max_v = max(float(self.visible_size[1]), float(self.full_size[1]), 100.0) if val_h > self.full_size[0]: val_h = self.full_size[0] if val_v > self.full_size[1]: val_v = self.full_size[1] self.hadjustment.set_lower(0) self.vadjustment.set_lower(0) self.hadjustment.set_upper(max_h) self.vadjustment.set_upper(max_v) self.hadjustment.set_page_size(self.visible_size[0]) self.vadjustment.set_page_size(self.visible_size[1]) self.hadjustment.set_value(int(val_h)) self.vadjustment.set_value(int(val_v)) def __on_draw(self, _, cairo_ctx): self.recompute_size() for drawer in self.drawers: cairo_ctx.save() try: drawer.draw(cairo_ctx) finally: cairo_ctx.restore() def __get_offset(self): x = int(self.hadjustment.get_value()) y = int(self.vadjustment.get_value()) return (x, y) offset = property(__get_offset) def __get_visible_size(self): return self.visible_size size = property(__get_visible_size) def add_drawer(self, drawer): drawer.set_canvas(self) self.drawers.add(drawer.layer, drawer) drawer.show() self.recompute_size() self.redraw((drawer.relative_position, drawer.relative_size)) def get_drawer_at(self, position): (x, y) = position for drawer in self.drawers: pt_a = drawer.position pt_b = (drawer.position[0] + drawer.size[0], drawer.position[1] + drawer.size[1]) if (x >= pt_a[0] and x < pt_b[0] and y >= pt_a[1] and y < pt_b[1]): return drawer return None def remove_drawer(self, drawer): drawer.hide() self.drawers.remove(drawer) self.recompute_size() self.redraw() def remove_drawers(self, drawers): for drawer in drawers: drawer.hide() self.drawers.remove(drawer) self.recompute_size() self.redraw() def remove_all_drawers(self): for drawer in self.drawers: drawer.hide() self.drawers.purge() self.recompute_size() self.redraw() def redraw(self, area=None): if area is None: self.queue_draw() else: self.queue_draw_area(area[0][0], area[0][1], area[1][0], area[1][1]) def __get_absolute_event(self, event): off_x = int(self.hadjustment.get_value()) off_y = int(self.vadjustment.get_value()) event = event.copy() event.x += off_x event.y += off_y return event def __on_button_pressed(self, _, event): self.grab_focus() event = self.__get_absolute_event(event) self.emit('absolute-button-press-event', event) def __on_motion(self, _, event): event = self.__get_absolute_event(event) self.emit('absolute-motion-notify-event', event) def __on_button_released(self, _, event): event = self.__get_absolute_event(event) self.emit('absolute-button-release-event', event) def __on_key_pressed(self, _, event): h = self.hadjustment.get_value() v = self.vadjustment.get_value() h_offset = 100 v_offset = 100 v_page = self.vadjustment.get_page_size() ops = { Gdk.KEY_Left: lambda: (h - h_offset, v), Gdk.KEY_Right: lambda: (h + h_offset, v), Gdk.KEY_Up: lambda: (h, v - v_offset), Gdk.KEY_Down: lambda: (h, v + v_offset), Gdk.KEY_Page_Up: lambda: (h, v - v_page), Gdk.KEY_Page_Down: lambda: (h, v + v_page), } if event.keyval not in ops: return False (h, v) = ops[event.keyval]() if h != self.hadjustment.get_value(): if h < self.hadjustment.get_lower(): h = self.hadjustment.get_lower() if h > self.hadjustment.get_upper(): h = self.hadjustment.get_upper() self.hadjustment.set_value(h) if v != self.vadjustment.get_value(): if v < self.vadjustment.get_lower(): v = self.vadjustment.get_lower() if v > self.vadjustment.get_upper(): v = self.vadjustment.get_upper() self.vadjustment.set_value(v) return True def __get_position(self): return (int(self.hadjustment.get_value()), int(self.vadjustment.get_value())) position = property(__get_position)