Merge changes I17adbd9b,I436b1eb9,I4de2d56e,I5e26ff2a

* changes:
  Combine figure and main window
  Add toolbar to ArmUi
  Combine ArmUi and segment selector
  Add option to stop editing control points
diff --git a/y2023/control_loops/python/graph_edit.py b/y2023/control_loops/python/graph_edit.py
index 4f22562..bbc7a1b 100644
--- a/y2023/control_loops/python/graph_edit.py
+++ b/y2023/control_loops/python/graph_edit.py
@@ -3,6 +3,8 @@
 from __future__ import print_function
 # matplotlib overrides fontconfig locations, so it needs to be imported before gtk.
 import matplotlib.pyplot as plt
+from matplotlib.backends.backend_gtk3agg import (FigureCanvasGTK3Agg as
+                                                 FigureCanvas)
 import os
 from frc971.control_loops.python import basic_window
 from frc971.control_loops.python.color import Color, palette
@@ -23,6 +25,8 @@
 import shapely
 from shapely.geometry import Polygon
 
+from frc971.control_loops.python.constants import *
+
 
 def px(cr):
     return OverrideMatrix(cr, identity)
@@ -227,30 +231,24 @@
             self.current_path_index = id
 
 
+ARM_AREA_WIDTH = 2 * (SCREEN_SIZE - 200)
+ARM_AREA_HEIGHT = SCREEN_SIZE
+
+
 # Create a GTK+ widget on which we will draw using Cairo
-class ArmUi(basic_window.BaseWindow):
+class ArmUi(Gtk.DrawingArea):
 
     def __init__(self, segments):
         super(ArmUi, self).__init__()
 
-        self.window = Gtk.Window()
-        self.window.set_title("DrawingArea")
-
-        self.window.set_events(Gdk.EventMask.BUTTON_PRESS_MASK
-                               | Gdk.EventMask.BUTTON_RELEASE_MASK
-                               | Gdk.EventMask.POINTER_MOTION_MASK
-                               | Gdk.EventMask.SCROLL_MASK
-                               | Gdk.EventMask.KEY_PRESS_MASK)
-        self.method_connect("key-press-event", self.do_key_press)
-        self.method_connect("motion-notify-event", self.do_motion)
-        self.method_connect("button-press-event",
-                            self._do_button_press_internal)
-        self.method_connect("configure-event", self._do_configure)
-        self.window.add(self)
-        self.window.show_all()
-
+        self.set_size_request(ARM_AREA_WIDTH, ARM_AREA_HEIGHT)
+        self.center = (0, 0)
+        self.shape = (ARM_AREA_WIDTH, ARM_AREA_HEIGHT)
         self.theta_version = False
-        self.reinit_extents()
+
+        self.init_extents()
+
+        self.connect('draw', self.on_draw)
 
         self.last_pos = to_xy(*graph_paths.points['Neutral'][:2])
         self.circular_index_select = 1
@@ -270,7 +268,6 @@
             self.fig.add_subplot(3, 1, 3)
         ]
         self.fig.subplots_adjust(hspace=1.0)
-        plt.show(block=False)
 
         self.index = 0
 
@@ -281,16 +278,20 @@
                                     [DRIVER_CAM_X, DRIVER_CAM_Y],
                                     DRIVER_CAM_WIDTH, DRIVER_CAM_HEIGHT)
 
-        self.segment_selector = SegmentSelector(self.segments)
-        self.segment_selector.show()
-
         self.show_indicators = True
         # Lets you only view selected path
         self.view_current = False
 
+        self.editing = True
+
+        self.x_offset = 0
+        self.y_offset = 0
+
     def _do_button_press_internal(self, event):
         o_x = event.x
         o_y = event.y
+        event.y -= self.y_offset
+        event.x -= self.x_offset
         x = event.x - self.window_shape[0] / 2
         y = self.window_shape[1] / 2 - event.y
         scale = self.get_current_scale()
@@ -300,22 +301,7 @@
         event.x = o_x
         event.y = o_y
 
-    def _do_configure(self, event):
-        self.window_shape = (event.width, event.height)
-
-    def redraw(self):
-        if not self.needs_redraw:
-            self.needs_redraw = True
-            self.window.queue_draw()
-
-    def method_connect(self, event, cb):
-
-        def handler(obj, *args):
-            cb(*args)
-
-        self.window.connect(event, handler)
-
-    def reinit_extents(self):
+    def init_extents(self):
         if self.theta_version:
             self.extents_x_min = -np.pi * 2
             self.extents_x_max = np.pi * 2
@@ -327,18 +313,41 @@
             self.extents_y_min = -4.0 * 0.0254
             self.extents_y_max = 110.0 * 0.0254
 
-        self.init_extents(
-            (0.5 * (self.extents_x_min + self.extents_x_max), 0.5 *
-             (self.extents_y_max + self.extents_y_min)),
-            (1.0 * (self.extents_x_max - self.extents_x_min), 1.0 *
-             (self.extents_y_max - self.extents_y_min)))
+        self.center = (0.5 * (self.extents_x_min + self.extents_x_max),
+                       0.5 * (self.extents_y_max + self.extents_y_min))
+        self.shape = (1.0 * (self.extents_x_max - self.extents_x_min),
+                      1.0 * (self.extents_y_max - self.extents_y_min))
+
+    def get_current_scale(self):
+        w_w, w_h = self.window_shape
+        w, h = self.shape
+        return min((w_w / w), (w_h / h))
+
+    def on_draw(self, widget, event):
+        cr = self.get_window().cairo_create()
+
+        self.window_shape = (self.get_window().get_geometry().width,
+                             self.get_window().get_geometry().height)
+
+        cr.save()
+        cr.set_font_size(20)
+        cr.translate(self.window_shape[0] / 2, self.window_shape[1] / 2)
+        scale = self.get_current_scale()
+        cr.scale(scale, -scale)
+        cr.translate(-self.center[0], -self.center[1])
+        cr.reset_clip()
+        self.handle_draw(cr)
+        cr.restore()
+
+    def method_connect(self, event, cb):
+
+        def handler(obj, *args):
+            cb(*args)
+
+        self.window.connect(event, handler)
 
     # Handle the expose-event by drawing
     def handle_draw(self, cr):
-        # use "with px(cr): blah;" to transform to pixel coordinates.
-        if self.segment_selector.current_path_index is not None:
-            self.index = self.segment_selector.current_path_index
-
         # Fill the background color of the window with grey
         set_color(cr, palette["GREY"])
         cr.paint()
@@ -484,6 +493,8 @@
     def do_motion(self, event):
         o_x = event.x
         o_y = event.y
+        event.x -= self.x_offset
+        event.y -= self.y_offset
         x = event.x - self.window_shape[0] / 2
         y = self.window_shape[1] / 2 - event.y
         scale = self.get_current_scale()
@@ -514,7 +525,21 @@
         event.x = o_x
         event.y = o_y
 
-        self.redraw()
+        self.queue_draw()
+
+    def switch_theta(self):
+        # Toggle between theta and xy renderings
+        if self.theta_version:
+            theta1, theta2 = self.last_pos
+            data = to_xy(theta1, theta2)
+            self.circular_index_select = int(
+                np.floor((theta2 - theta1) / np.pi))
+            self.last_pos = (data[0], data[1])
+        else:
+            self.last_pos = self.cur_pt_in_theta()
+
+        self.theta_version = not self.theta_version
+        self.init_extents()
 
     def do_key_press(self, event):
         keyval = Gdk.keyval_to_lower(event.keyval)
@@ -569,22 +594,14 @@
             print("Switched to segment:", self.segments[self.index].name)
             self.segments[self.index].Print(graph_paths.points)
 
+        elif keyval == Gdk.KEY_d:
+            self.editing = not self.editing
+
         elif keyval == Gdk.KEY_l:
             self.view_current = not self.view_current
 
         elif keyval == Gdk.KEY_t:
-            # Toggle between theta and xy renderings
-            if self.theta_version:
-                theta1, theta2 = self.last_pos
-                data = to_xy(theta1, theta2)
-                self.circular_index_select = int(
-                    np.floor((theta2 - theta1) / np.pi))
-                self.last_pos = (data[0], data[1])
-            else:
-                self.last_pos = self.cur_pt_in_theta()
-
-            self.theta_version = not self.theta_version
-            self.reinit_extents()
+            self.switch_theta()
 
         elif keyval == Gdk.KEY_z:
             self.edit_control1 = not self.edit_control1
@@ -601,9 +618,10 @@
             print("self.last_pos: ", self.last_pos, " ci: ",
                   self.circular_index_select)
 
-        self.redraw()
+        self.queue_draw()
 
     def do_button_press(self, event):
+
         last_pos = self.last_pos
         self.last_pos = (event.x, event.y)
         pt_theta = self.cur_pt_in_theta()
@@ -613,10 +631,11 @@
 
         self.now_segment_pt = np.array(shift_angles(pt_theta))
 
-        if self.edit_control1:
-            self.segments[self.index].control1 = self.now_segment_pt
-        else:
-            self.segments[self.index].control2 = self.now_segment_pt
+        if self.editing:
+            if self.edit_control1:
+                self.segments[self.index].control1 = self.now_segment_pt
+            else:
+                self.segments[self.index].control2 = self.now_segment_pt
 
         print('Clicked at theta: np.array([%s, %s])' %
               (self.now_segment_pt[0], self.now_segment_pt[1]))
@@ -628,10 +647,107 @@
 
         self.segments[self.index].Print(graph_paths.points)
 
-        self.redraw()
+        self.queue_draw()
 
 
-arm_ui = ArmUi(graph_paths.segments)
-print('Starting with segment: ', arm_ui.segments[arm_ui.index].name)
-arm_ui.segments[arm_ui.index].Print(graph_paths.points)
+class Window(Gtk.Window):
+
+    def __init__(self, segments):
+        super().__init__(title="Drawing Area")
+
+        self.segment_store = Gtk.ListStore(int, str)
+
+        for i, segment in enumerate(segments):
+            self.segment_store.append([i, segment.name])
+
+        self.segment_box = Gtk.ComboBox.new_with_model_and_entry(
+            self.segment_store)
+        self.segment_box.connect("changed", self.on_combo_changed)
+        self.segment_box.set_entry_text_column(1)
+
+        self.arm_draw = ArmUi(segments)
+
+        self.arm_draw.y_offset = self.segment_box.get_allocation().width
+
+        print('Starting with segment: ',
+              self.arm_draw.segments[self.arm_draw.index].name)
+        self.arm_draw.segments[self.arm_draw.index].Print(graph_paths.points)
+
+        self.set_events(Gdk.EventMask.BUTTON_PRESS_MASK
+                        | Gdk.EventMask.BUTTON_RELEASE_MASK
+                        | Gdk.EventMask.POINTER_MOTION_MASK
+                        | Gdk.EventMask.SCROLL_MASK
+                        | Gdk.EventMask.KEY_PRESS_MASK)
+        self.method_connect('map-event', self.do_map_event)
+        self.method_connect("key-press-event", self.arm_draw.do_key_press)
+        self.method_connect("motion-notify-event", self.arm_draw.do_motion)
+        self.method_connect("button-press-event",
+                            self.arm_draw._do_button_press_internal)
+
+        self.grid = Gtk.Grid()
+        self.add(self.grid)
+
+        self.grid.attach(self.arm_draw, 0, 1, 1, 1)
+
+        self.isolate_button = Gtk.Button(label="Toggle Path Isolation")
+        self.isolate_button.connect('clicked', self.on_button_click)
+
+        self.theta_button = Gtk.Button(label="Toggle Theta Mode")
+        self.theta_button.connect('clicked', self.on_button_click)
+
+        self.editing_button = Gtk.Button(label="Toggle Editing Mode")
+        self.editing_button.connect('clicked', self.on_button_click)
+
+        self.indicator_button = Gtk.Button(
+            label="Toggle Control Point Indicators")
+        self.indicator_button.connect('clicked', self.on_button_click)
+
+        self.box = Gtk.Box(spacing=6)
+        self.grid.attach(self.box, 0, 0, 1, 1)
+
+        self.figure_canvas = FigureCanvas(self.arm_draw.fig)
+        self.figure_canvas.set_size_request(500, 300)
+
+        self.grid.attach(self.figure_canvas, 1, 1, 1, 1)
+
+        self.box.pack_start(self.segment_box, False, False, 0)
+        self.box.pack_start(self.isolate_button, False, False, 0)
+        self.box.pack_start(self.theta_button, False, False, 0)
+        self.box.pack_start(self.editing_button, False, False, 0)
+        self.box.pack_start(self.indicator_button, False, False, 0)
+
+    def on_combo_changed(self, combo):
+        iter = combo.get_active_iter()
+
+        if iter is not None:
+            model = combo.get_model()
+            id, name = model[iter][:2]
+            print("Selected: ID=%d, name=%s" % (id, name))
+            self.arm_draw.index = id
+            self.arm_draw.queue_draw()
+
+    def method_connect(self, event, cb):
+
+        def handler(obj, *args):
+            cb(*args)
+
+        self.connect(event, handler)
+
+    def do_map_event(self, event):
+        self.arm_draw.y_offset = self.box.get_allocation().height
+
+    def on_button_click(self, button):
+        if self.isolate_button == button:
+            self.arm_draw.view_current = not self.arm_draw.view_current
+        elif self.theta_button == button:
+            self.arm_draw.switch_theta()
+        elif self.editing_button == button:
+            self.arm_draw.editing = not self.arm_draw.editing
+        elif self.indicator_button == button:
+            self.arm_draw.show_indicators = not self.arm_draw.show_indicators
+        self.arm_draw.queue_draw()
+
+
+window = Window(graph_paths.segments)
+window.show_all()
 basic_window.RunApp()