1"""
2Copyright 2007, 2008, 2009, 2010 Free Software Foundation, Inc.
3This file is part of GNU Radio
4
5GNU Radio Companion is free software; you can redistribute it and/or
6modify it under the terms of the GNU General Public License
7as published by the Free Software Foundation; either version 2
8of the License, or (at your option) any later version.
9
10GNU Radio Companion is distributed in the hope that it will be useful,
11but WITHOUT ANY WARRANTY; without even the implied warranty of
12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13GNU General Public License for more details.
14
15You should have received a copy of the GNU General Public License
16along with this program; if not, write to the Free Software
17Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
18"""
19
20from __future__ import absolute_import
21
22from gi.repository import Gtk, Gdk
23
24from .canvas.colors import FLOWGRAPH_BACKGROUND_COLOR
25from . import Constants
26from . import Actions
27
28
29class DrawingArea(Gtk.DrawingArea):
30    """
31    DrawingArea is the gtk pixel map that graphical elements may draw themselves on.
32    The drawing area also responds to mouse and key events.
33    """
34
35    def __init__(self, flow_graph):
36        """
37        DrawingArea constructor.
38        Connect event handlers.
39
40        Args:
41            main_window: the main_window containing all flow graphs
42        """
43        Gtk.DrawingArea.__init__(self)
44
45        self._flow_graph = flow_graph
46        self.set_property('can_focus', True)
47
48        self.zoom_factor = 1.0
49        self._update_after_zoom = False
50        self.ctrl_mask = False
51        self.mod1_mask = False
52        self.button_state = [False] * 10
53
54        # self.set_size_request(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT)
55        self.connect('realize', self._handle_window_realize)
56        self.connect('draw', self.draw)
57        self.connect('motion-notify-event', self._handle_mouse_motion)
58        self.connect('button-press-event', self._handle_mouse_button_press)
59        self.connect('button-release-event', self._handle_mouse_button_release)
60        self.connect('scroll-event', self._handle_mouse_scroll)
61        self.add_events(
62            Gdk.EventMask.BUTTON_PRESS_MASK |
63            Gdk.EventMask.POINTER_MOTION_MASK |
64            Gdk.EventMask.BUTTON_RELEASE_MASK |
65            Gdk.EventMask.SCROLL_MASK |
66            Gdk.EventMask.LEAVE_NOTIFY_MASK |
67            Gdk.EventMask.ENTER_NOTIFY_MASK
68            # Gdk.EventMask.FOCUS_CHANGE_MASK
69        )
70
71        # This may not be the correct place to be handling the user events
72        # Should this be in the page instead?
73        # Or should more of the page functionality move here?
74
75        # setup drag and drop
76        self.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)
77        self.connect('drag-data-received', self._handle_drag_data_received)
78        self.drag_dest_set_target_list(None)
79        self.drag_dest_add_text_targets()
80
81        # setup the focus flag
82        self._focus_flag = False
83        self.get_focus_flag = lambda: self._focus_flag
84
85        def _handle_notify_event(widget, event, focus_flag):
86            self._focus_flag = focus_flag
87
88        self.connect('leave-notify-event', _handle_notify_event, False)
89        self.connect('enter-notify-event', _handle_notify_event, True)
90
91        self.set_can_focus(True)
92        self.connect('focus-out-event', self._handle_focus_lost_event)
93
94
95    ##########################################################################
96    # Handlers
97    ##########################################################################
98    def _handle_drag_data_received(self, widget, drag_context, x, y, selection_data, info, time):
99        """
100        Handle a drag and drop by adding a block at the given coordinate.
101        """
102        coords = x / self.zoom_factor, y / self.zoom_factor
103        self._flow_graph.add_new_block(selection_data.get_text(), coords)
104
105    def _handle_mouse_scroll(self, widget, event):
106        if event.get_state() & Gdk.ModifierType.CONTROL_MASK:
107            change = 1.2 if event.direction == Gdk.ScrollDirection.UP else 1/1.2
108            zoom_factor = min(max(self.zoom_factor * change, 0.1), 5.0)
109
110            if zoom_factor != self.zoom_factor:
111                self.zoom_factor = zoom_factor
112                self._update_after_zoom = True
113                self.queue_draw()
114            return True
115
116        return False
117
118    def _handle_mouse_button_press(self, widget, event):
119        """
120        Forward button click information to the flow graph.
121        """
122        self.grab_focus()
123
124        self.ctrl_mask = event.get_state() & Gdk.ModifierType.CONTROL_MASK
125        self.mod1_mask = event.get_state() & Gdk.ModifierType.MOD1_MASK
126        self.button_state[event.button] = True
127
128        if event.button == 1:
129            double_click = (event.type == Gdk.EventType._2BUTTON_PRESS)
130            self.button_state[1] = not double_click
131            self._flow_graph.handle_mouse_selector_press(
132                double_click=double_click,
133                coordinate=self._translate_event_coords(event),
134            )
135        elif event.button == 3:
136            self._flow_graph.handle_mouse_context_press(
137                coordinate=self._translate_event_coords(event),
138                event=event,
139            )
140
141    def _handle_mouse_button_release(self, widget, event):
142        """
143        Forward button release information to the flow graph.
144        """
145        self.ctrl_mask = event.get_state() & Gdk.ModifierType.CONTROL_MASK
146        self.mod1_mask = event.get_state() & Gdk.ModifierType.MOD1_MASK
147        self.button_state[event.button] = False
148        if event.button == 1:
149            self._flow_graph.handle_mouse_selector_release(
150                coordinate=self._translate_event_coords(event),
151            )
152
153    def _handle_mouse_motion(self, widget, event):
154        """
155        Forward mouse motion information to the flow graph.
156        """
157        self.ctrl_mask = event.get_state() & Gdk.ModifierType.CONTROL_MASK
158        self.mod1_mask = event.get_state() & Gdk.ModifierType.MOD1_MASK
159
160        if self.button_state[1]:
161            self._auto_scroll(event)
162
163        self._flow_graph.handle_mouse_motion(
164            coordinate=self._translate_event_coords(event),
165        )
166
167    def _update_size(self):
168        w, h = self._flow_graph.get_extents()[2:]
169        self.set_size_request(w * self.zoom_factor + 100, h * self.zoom_factor + 100)
170
171    def _auto_scroll(self, event):
172        x, y = event.x, event.y
173        scrollbox = self.get_parent().get_parent()
174
175        self._update_size()
176
177        def scroll(pos, adj):
178            """scroll if we moved near the border"""
179            adj_val = adj.get_value()
180            adj_len = adj.get_page_size()
181            if pos - adj_val > adj_len - Constants.SCROLL_PROXIMITY_SENSITIVITY:
182                adj.set_value(adj_val + Constants.SCROLL_DISTANCE)
183                adj.emit('changed')
184            elif pos - adj_val < Constants.SCROLL_PROXIMITY_SENSITIVITY:
185                adj.set_value(adj_val - Constants.SCROLL_DISTANCE)
186                adj.emit('changed')
187
188        scroll(x, scrollbox.get_hadjustment())
189        scroll(y, scrollbox.get_vadjustment())
190
191    def _handle_window_realize(self, widget):
192        """
193        Called when the window is realized.
194        Update the flowgraph, which calls new pixmap.
195        """
196        self._flow_graph.update()
197        self._update_size()
198
199    def draw(self, widget, cr):
200        width = widget.get_allocated_width()
201        height = widget.get_allocated_height()
202
203        cr.set_source_rgba(*FLOWGRAPH_BACKGROUND_COLOR)
204        cr.rectangle(0, 0, width, height)
205        cr.fill()
206
207        cr.scale(self.zoom_factor, self.zoom_factor)
208        cr.set_line_width(2.0 / self.zoom_factor)
209
210        if self._update_after_zoom:
211            self._flow_graph.create_labels(cr)
212            self._flow_graph.create_shapes()
213            self._update_size()
214            self._update_after_zoom = False
215
216        self._flow_graph.draw(cr)
217
218    def _translate_event_coords(self, event):
219        return event.x / self.zoom_factor, event.y / self.zoom_factor
220
221    def _handle_focus_lost_event(self, widget, event):
222        # don't clear selection while context menu is active
223        if not self._flow_graph.get_context_menu()._menu.get_visible():
224            self._flow_graph.unselect()
225            self._flow_graph.update_selected()
226            self.queue_draw()
227            Actions.ELEMENT_SELECT()
228