changeset 1492:6616530c4180

Show hint when mouse over a transition
author Thinker K.F. Li <thinker@codemud.net>
date Mon, 02 May 2011 23:23:06 +0800
parents 06c101bba830
children b0e113605382
files pyink/FSM_window.glade pyink/FSM_window.py
diffstat 2 files changed, 489 insertions(+), 17 deletions(-) [+]
line wrap: on
line diff
--- a/pyink/FSM_window.glade	Sun May 01 00:09:56 2011 +0800
+++ b/pyink/FSM_window.glade	Mon May 02 23:23:06 2011 +0800
@@ -306,12 +306,14 @@
               <object class="GtkLabel" id="label2">
                 <property name="visible">True</property>
                 <property name="label" translatable="yes">Name:</property>
+                <property name="justify">right</property>
               </object>
             </child>
             <child>
               <object class="GtkLabel" id="label3">
                 <property name="visible">True</property>
                 <property name="label" translatable="yes">Radius:</property>
+                <property name="justify">right</property>
               </object>
               <packing>
                 <property name="top_attach">1</property>
@@ -343,10 +345,28 @@
               </packing>
             </child>
             <child>
-              <placeholder/>
+              <object class="GtkLabel" id="label4">
+                <property name="visible">True</property>
+                <property name="label" translatable="yes">Entry Action:</property>
+                <property name="justify">right</property>
+              </object>
+              <packing>
+                <property name="top_attach">2</property>
+                <property name="bottom_attach">3</property>
+              </packing>
             </child>
             <child>
-              <placeholder/>
+              <object class="GtkEntry" id="state_entry_action">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="invisible_char">&#x25CF;</property>
+              </object>
+              <packing>
+                <property name="left_attach">1</property>
+                <property name="right_attach">2</property>
+                <property name="top_attach">2</property>
+                <property name="bottom_attach">3</property>
+              </packing>
             </child>
           </object>
           <packing>
@@ -501,4 +521,117 @@
       </object>
     </child>
   </object>
+  <object class="GtkDialog" id="transition_editor">
+    <property name="border_width">5</property>
+    <property name="modal">True</property>
+    <property name="window_position">center-always</property>
+    <property name="default_height">200</property>
+    <property name="type_hint">normal</property>
+    <property name="transient_for">FSM_main_win</property>
+    <child internal-child="vbox">
+      <object class="GtkVBox" id="dialog-vbox3">
+        <property name="visible">True</property>
+        <property name="orientation">vertical</property>
+        <property name="spacing">2</property>
+        <child>
+          <object class="GtkTable" id="table2">
+            <property name="visible">True</property>
+            <property name="n_rows">2</property>
+            <property name="n_columns">2</property>
+            <child>
+              <object class="GtkLabel" id="label5">
+                <property name="visible">True</property>
+                <property name="label" translatable="yes">Condition:</property>
+                <property name="justify">right</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel" id="label6">
+                <property name="visible">True</property>
+                <property name="label" translatable="yes">Action:</property>
+                <property name="justify">right</property>
+              </object>
+              <packing>
+                <property name="top_attach">1</property>
+                <property name="bottom_attach">2</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkEntry" id="transition_cond">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="invisible_char">&#x25CF;</property>
+              </object>
+              <packing>
+                <property name="left_attach">1</property>
+                <property name="right_attach">2</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkEntry" id="transition_action">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="invisible_char">&#x25CF;</property>
+              </object>
+              <packing>
+                <property name="left_attach">1</property>
+                <property name="right_attach">2</property>
+                <property name="top_attach">1</property>
+                <property name="bottom_attach">2</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+        <child internal-child="action_area">
+          <object class="GtkHButtonBox" id="dialog-action_area3">
+            <property name="visible">True</property>
+            <property name="layout_style">end</property>
+            <child>
+              <object class="GtkButton" id="transition_apply">
+                <property name="label">gtk-apply</property>
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">True</property>
+                <property name="use_stock">True</property>
+                <signal name="clicked" handler="on_transition_apply_clicked"/>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">False</property>
+                <property name="position">0</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkButton" id="transition_cancel">
+                <property name="label">gtk-cancel</property>
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">True</property>
+                <property name="use_stock">True</property>
+                <signal name="clicked" handler="on_transition_cancel_clicked"/>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">False</property>
+                <property name="position">1</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="pack_type">end</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+    <action-widgets>
+      <action-widget response="0">transition_apply</action-widget>
+      <action-widget response="0">transition_cancel</action-widget>
+    </action-widgets>
+  </object>
 </interface>
--- a/pyink/FSM_window.py	Sun May 01 00:09:56 2011 +0800
+++ b/pyink/FSM_window.py	Mon May 02 23:23:06 2011 +0800
@@ -4,6 +4,78 @@
 import data_monitor
 import pybInkscape
 
+
+class _dragger(object):
+    _node = None
+    _start_x = None
+    _start_y = None
+    _state = 0
+    
+    def __init__(self):
+        pass
+    
+    def mouse_event(self, evtype, button, x, y):
+        raise RuntimeError, 'should not be here'
+    
+    def mouse_event_waiting(self, evtype, button, x, y):
+        if evtype == pybInkscape.PYSPItem.PYB_EVENT_BUTTON_PRESS and \
+                button == 1:
+            self._start_x = x
+            self._start_y = y
+            self.mouse_event = self.mouse_event_pressed
+            self.start_drag()
+            pass
+        pass
+    
+    def mouse_event_pressed(self, evtype, button, x, y):
+        rx = x - self._start_x
+        ry = y - self._start_y
+        
+        if evtype == pybInkscape.PYSPItem.PYB_EVENT_BUTTON_RELEASE:
+            self.mouse_event = self.mouse_event_waiting
+            self.stop_drag(rx, ry)
+            pass
+
+        self.update(rx, ry)
+        pass
+
+    def start(self):
+        self.mouse_event = self.mouse_event_waiting
+        pass
+
+    def stop(self):
+        pass
+
+    def connect(self, node):
+        self.start()
+        
+        def handler(item, evtype, button, x, y):
+            self.mouse_event(evtype, button, x, y)
+            pass
+        
+        self._node = node
+        hdl_id = node.spitem.connect('mouse-event', handler)
+        self._hdl_id = hdl_id
+        pass
+
+    def disconnect(self):
+        self.stop()
+        node = self._node
+        hdl_id = self._hdl_id
+        node.disconnect(hdl_id)
+        pass
+
+    def start_drag(self):
+        pass
+
+    def stop_drag(self, rx, ry):
+        pass
+
+    def update(self, rx, ry):
+        pass
+    pass
+
+
 class FSM_window_base(object):
     _add_state_button = None
     _move_state_button = None
@@ -152,8 +224,15 @@
 
     def on_edit_state_activate(self, *args):
         pass
+
+    def on_transition_apply_clicked(self, *args):
+        pass
+
+    def on_transition_cancel_clicked(self, *args):
+        pass
     pass
 
+
 class FSM_transition(object):
     _doc = None
     _domview = None
@@ -165,6 +244,7 @@
     trn_g = None
     _arrow_node = None
     _path_node = None
+    _control_points = None
 
     def __init__(self, trn_cond):
        self.trn_cond = trn_cond
@@ -271,9 +351,13 @@
         trn = domview.get_transition(state_name, trn_cond)
         return trn[1]
 
+    @property
+    def state(self):
+        return self._state
+
     def draw(self, parent):
         path = self.path
-        trn_g, arrow_node, path_node = self._draw_transition_real(parent, path)
+        trn_g, path_node, arrow_node = self._draw_transition_real(parent, path)
         self.trn_g = trn_g
         self._arrow_node = arrow_node
         self._path_node = path_node
@@ -333,6 +417,91 @@
         domview = self._domview
         domview.set_transition_path(state_name, trn_cond, new_path)
         pass
+
+    def show_control_points(self):
+        if not self._control_points:
+            doc = self._doc
+            
+            c1 = doc.createElement('svg:circle')
+            c1.setAttribute('r', '3')
+            c1.setAttribute('style', 'stroke: black; stroke-width: 1; '
+                            'fill: white')
+            l01 = doc.createElement('svg:line')
+            l01.setAttribute('style', 'stroke: black; stroke-width: 1; '
+                             'stroke-dasharray: 3 2')
+
+            c2 = doc.createElement('svg:circle')
+            c2.setAttribute('r', '3')
+            c2.setAttribute('style', 'stroke: black; stroke-width: 1; '
+                            'fill: white')
+            l32 = doc.createElement('svg:line')
+            l32.setAttribute('style', 'stroke: black; stroke-width: 1; '
+                             'stroke-dasharray: 3 2')
+
+            control_layer = self._control_layer
+            
+            control_layer.appendChild(c1)
+            control_layer.appendChild(l01)
+            control_layer.appendChild(c2)
+            control_layer.appendChild(l32)
+            self._control_points = (c1, l01, c2, l32)
+            pass
+
+        c1, l01, c2, l32 = self._control_points
+        path = self.path
+        c0x, c0y, c1x, c1y, c2x, c2y, c3x, c3y = tuple(path)
+        
+        c1.setAttribute('cx', str(c1x))
+        c1.setAttribute('cy', str(c1y))
+        l01.setAttribute('x1', str(c0x))
+        l01.setAttribute('y1', str(c0y))
+        l01.setAttribute('x2', str(c1x))
+        l01.setAttribute('y2', str(c1y))
+        
+        c2.setAttribute('cx', str(c2x))
+        c2.setAttribute('cy', str(c2y))
+        l32.setAttribute('x1', str(c3x))
+        l32.setAttribute('y1', str(c3y))
+        l32.setAttribute('x2', str(c2x))
+        l32.setAttribute('y2', str(c2y))
+        pass
+
+    def hide_control_points(self):
+        if not self._control_points:
+            return
+
+        control_layer = self._control_layer
+        for node in self._control_points:
+            control_layer.removeChild(node)
+            pass
+        self._control_points = None
+        pass
+
+    def start_hint(self):
+        path_node = self._path_node
+        arrow_node = self._arrow_node
+        if path_node:
+            path_node.setAttribute('style',
+                                   'stroke: #404040; stroke-width: 3; '
+                                   'fill: none')
+            arrow_node.setAttribute('style',
+                                    'stroke: #404040; stroke-width: 2; '
+                                    'fill: #404040')
+            pass
+        pass
+
+    def stop_hint(self):
+        path_node = self._path_node
+        arrow_node = self._arrow_node
+        if path_node:
+            path_node.setAttribute('style',
+                                   'stroke: #000000; stroke-width: 1; ' \
+                                       'fill: none')
+            arrow_node.setAttribute('style',
+                                    'stroke: #000000; stroke-width: 1; ' \
+                                        'fill: #000000')
+            pass
+        pass
     pass
 
 class FSM_state(object):
@@ -597,7 +766,8 @@
     __data_monitor_prefix__ = 'on_'
     
     _window = None
-    _selected_state = None
+    _domview = None
+    _selected_cleaner = None
     
     def __init__(self, window, domview_ui):
         super(_FSM_move_state_mode, self).__init__()
@@ -608,24 +778,28 @@
         pass
 
     def on_move_state_background(self, item, evtype, button, x, y):
+        if self._selected_cleaner is None:
+            return
+
+        if evtype == pybInkscape.PYSPItem.PYB_EVENT_BUTTON_RELEASE:
+            self._clean_select()
+            pass
         pass
 
     def _select_state(self, state):
-        if self._selected_state:
-            self._selected_state.hide_selected()
-            pass
-        self._selected_state = state
+        self._clean_select()
+        self._selected_cleaner = state.hide_selected
         state.show_selected()
         pass
 
-    def _clear_select(self):
-        if self._selected_state:
-            self._selected_state.hide_selected()
+    def _clean_select(self):
+        if self._selected_cleaner:
+            self._selected_cleaner()
             pass
-        self._selected_state = None
+        self._selected_cleaner = None
         pass
 
-    def handle_move_state_state(self, state, evtype, button, x, y):
+    def _handle_move_state_state(self, state, evtype, button, x, y):
         window = self._window
 
         def moving_state(item, evtype, button, x, y):
@@ -660,6 +834,143 @@
             pass
         pass
 
+    def _install_transition_mouse_event_handler(self, trn):
+        c1, l01, c2, l32 = trn._control_points
+        path = trn.path
+        c0x, c0y, c1x, c1y, c2x, c2y, c3x, c3y = tuple(path)
+
+        state_src = trn.state
+        target_name = trn.target
+        states = trn._states
+        state_target = states[target_name]
+        domview = self._domview
+        window = self._window
+
+        def c1_update(rx, ry):
+            nc1x = c1x + rx
+            nc1y = c1y + ry
+            cx, cy = state_src.xy
+            r = state_src.r
+            
+            cv = nc1x - cx, nc1y - cy
+            cv_len = math.sqrt(cv[0] ** 2 + cv[1] ** 2)
+            nc0x = cx + cv[0] * r / cv_len
+            nc0y = cy + cv[1] * r / cv_len
+            
+            path = list(trn.path)
+            path[:4] = [nc0x, nc0y, nc1x, nc1y]
+
+            state_name = state_src.state_name
+            cond = trn.trn_cond
+            domview.set_transition_path(state_name, cond, path)
+
+            trn.show_control_points()
+            trn.update()
+            pass
+
+        def c1_start():
+            def relay_event(item, evtype, button, x, y):
+                c1_dragger.mouse_event(evtype, button, x, y)
+                pass
+            
+            window.ungrab_bg()
+            window.grab_bg(relay_event)
+            pass
+        
+        def c1_stop(rx, ry):
+            window.ungrab_bg()
+            window.grab_bg(self.on_move_state_background)
+            pass
+        
+        def c2_update(rx, ry):
+            nc2x = c2x + rx
+            nc2y = c2y + ry
+            cx, cy = state_target.xy
+            r = state_target.r
+            
+            cv = nc2x - cx, nc2y - cy
+            cv_len = math.sqrt(cv[0] ** 2 + cv[1] ** 2)
+            nc3x = cx + cv[0] * r / cv_len
+            nc3y = cy + cv[1] * r / cv_len
+            
+            path = list(trn.path)
+            path[4:] = [nc2x, nc2y, nc3x, nc3y]
+
+            state_name = state_src.state_name
+            cond = trn.trn_cond
+            domview.set_transition_path(state_name, cond, path)
+
+            trn.show_control_points()
+            trn.update()
+            pass
+
+        def c2_start():
+            def relay_event(item, evtype, button, x, y):
+                c2_dragger.mouse_event(evtype, button, x, y)
+                pass
+            
+            window.ungrab_bg()
+            window.grab_bg(relay_event)
+            pass
+
+        def c2_stop(rx, ry):
+            window.ungrab_bg()
+            window.grab_bg(self.on_move_state_background)
+            pass
+        
+        c1_dragger = _dragger()
+        c1_dragger.update = c1_update
+        c1_dragger.start_drag = c1_start
+        c1_dragger.stop_drag = c1_stop
+        c1_dragger.connect(c1)
+
+        c2_dragger = _dragger()
+        c2_dragger.update = c2_update
+        c2_dragger.start_drag = c2_start
+        c2_dragger.stop_drag = c2_stop
+        c2_dragger.connect(c2)
+        pass
+
+    def _select_transition(self, trn):
+        def cleaner():
+            trn.hide_control_points()
+            del self._hint_transition
+            pass
+        self._clean_select()
+        self._selected_cleaner = cleaner
+        trn.show_control_points()
+        
+        trn.stop_hint()
+        self._hint_transition = lambda *args: None
+        window = self._window
+        window.ungrab_bg()
+        
+        self._install_transition_mouse_event_handler(trn)
+        pass
+
+    def _hint_transition(self, trn):
+        def stop_hint(*args):
+            trn.stop_hint()
+            window.ungrab_bg()
+            window.grab_bg(self.on_move_state_background)
+            pass
+
+        trn.start_hint()
+        
+        window = self._window
+        window.ungrab_bg()
+        window.grab_bg(stop_hint)
+        pass
+
+    def _handle_transitoin_mouse_events(self, trn, evtype, button, x, y):
+        if evtype == pybInkscape.PYSPItem.PYB_EVENT_BUTTON_RELEASE and \
+                button == 1:
+            self._select_transition(trn)
+        elif evtype == pybInkscape.PYSPItem.PYB_EVENT_MOUSE_ENTER:
+            self._hint_transition(trn)
+            pass
+        pass
+
     def activate(self):
         window = self._window
         window._emit_leave_mode()
@@ -667,13 +978,12 @@
         window.ungrab_all()
         
         window.grab_bg(self.on_move_state_background)
-        window.grab_state(self.handle_move_state_state)
+        window.grab_state(self._handle_move_state_state)
+        window.grab_transition(self._handle_transitoin_mouse_events)
         pass
 
     def deactivate(self):
-        if self._selected_state:
-            self._clear_select()
-            pass
+        self._clean_select()
         pass
     pass
 
@@ -846,6 +1156,7 @@
     _add_state_mode = None
     _state_mouse_event_handler = None
     _add_transition_cb = None
+    _transition_mouse_event_handler = None
     
     def __init__(self, domview_ui, close_cb, destroy_cb):
         super(FSM_window, self).__init__()
@@ -950,6 +1261,7 @@
         self.ungrab_mouse()
         self.ungrab_state()
         self.ungrab_add_transition()
+        self.ungrab_transition()
         pass
 
     def on_state_mouse_event(self, state, evtype, button, x, y):
@@ -965,6 +1277,29 @@
         state.grab(mouse_event_handler)
         pass
 
+    def on_transition_mouse_event(self, trn, evtype, button, x, y):
+        if self._transition_mouse_event_handler:
+            self._transition_mouse_event_handler(trn, evtype, button, x, y)
+            pass
+        pass
+
+    def _install_transition_event_handler(self, trn):
+        def mouse_event_handler(item, evtype, button, x, y):
+            self.on_transition_mouse_event(trn, evtype, button, x, y)
+            pass
+        trn_g = trn.trn_g
+        trn_g.spitem.connect('mouse-event', mouse_event_handler)
+        pass
+
+    def grab_transition(self, callback):
+        assert self._transition_mouse_event_handler is None
+        self._transition_mouse_event_handler = callback
+        pass
+
+    def ungrab_transition(self):
+        self._transition_mouse_event_handler = None
+        pass
+
     def grab_state(self, callback):
         assert self._state_mouse_event_handler is None
         self._state_mouse_event_handler = callback
@@ -989,6 +1324,10 @@
         self._draw_state_domview(state_name)
         state = states[state_name]
         self._install_state_event_handler(state)
+
+        for trn in state.transitions.values():
+            self._install_transition_event_handler(trn)
+            pass
         pass
 
     ## \brief Load new state incrementally.