Ran update_copyright.py
[hooke.git] / hooke / ui / gui / panel / commands.py
1 #!/usr/bin/env python
2
3 """Commands and settings panel for Hooke.
4
5 This panel handles command generation of
6 :class:`hooke.ui.CommandMessage`\s for all of the commands that don't
7 have panels of their own.  Actually it can generate
8 :class:`hooke.ui.CommandMessage`\s for those as well, but the
9 command-specific panel will probably have a nicer interface.
10
11 # TODO: command arguments.
12 """
13
14 import types
15
16 import wx
17
18 from ....util.callback import callback, in_callback
19 from . import Panel
20
21
22 class Tree (wx.TreeCtrl):
23     """The main widget of :class:`CommandsPanel`.
24
25     `callbacks` is shared with the parent :class:`Commands`.
26     """
27     def __init__(self, commands, selected, callbacks, *args, **kwargs):
28         super(Tree, self).__init__(*args, **kwargs)
29         imglist = wx.ImageList(width=16, height=16, mask=True, initialCount=2)
30         imglist.Add(wx.ArtProvider.GetBitmap(
31                 wx.ART_FOLDER, wx.ART_OTHER, wx.Size(16, 16)))
32         imglist.Add(wx.ArtProvider.GetBitmap(
33                 wx.ART_EXECUTABLE_FILE, wx.ART_OTHER, wx.Size(16, 16)))
34         self.AssignImageList(imglist)
35         self.image = {
36             'root': 0,
37             'plugin': 0,
38             'command': 1,
39             }
40         self._c = {
41             'root': self.AddRoot(
42                 text='Commands and Settings', image=self.image['root']),
43             }
44         self.Bind(wx.EVT_TREE_SEL_CHANGED, self._on_selection_changed)
45         self.Bind(wx.EVT_TREE_ITEM_ACTIVATED, self._on_execute)
46         self.Bind(wx.EVT_MOTION, self._on_motion)
47
48         self._callbacks = callbacks
49         self._setup_commands(commands, selected)
50         self._last_tooltip = None
51
52     def _setup_commands(self, commands, selected):
53         self._plugins = {}    # {name: hooke.plugin.Plugin()}
54         self._commands = {}   # {name: hooke.command.Command()}
55
56         # In both of the following dicts, command names are
57         # (plugin.name, command.name) to avoid cross-plugin
58         # collisions.  See ._is_command().
59         self._id_for_name = {}  # {name: id}
60         self._name_for_id = {}  # {id: name}
61
62         selected = None
63         plugins = sorted(set([c.plugin for c in commands]),
64                          key=lambda p:p.name)
65         for plugin in plugins:
66             self._plugins[plugin.name] = plugin
67             _id = self.AppendItem(
68                 parent=self._c['root'],
69                 text=plugin.name,
70                 image=self.image['plugin'])
71             self._id_for_name[plugin.name] = _id
72             self._name_for_id[_id] = plugin.name
73         for command in sorted(commands, key=lambda c:c.name):
74             name = (command.plugin.name, command.name)
75             self._commands[name] = command
76             _id = self.AppendItem(
77                 parent=self._id_for_name[command.plugin.name],
78                 text=command.name,
79                 image=self.image['command'])
80             self._id_for_name[name] = _id
81             self._name_for_id[_id] = name
82             if command.name == selected:
83                 selected = _id
84
85         #for plugin in self._plugins.values():
86         #    self.Expand(self._id_for_name[plugin.name])
87         # make sure the selected command/plugin is visible in the tree
88         if selected is not None:
89             self.SelectItem(selected, True)
90             self.EnsureVisible(selected)
91
92     def _is_command(self, name):  # name from ._id_for_name / ._name_for_id
93         """Return `True` if `name` corresponds to a :class:`hooke.command.Command`.
94         """
95         # Plugin names are strings, Command names are tuples.
96         # See ._setup_commands().
97         return not isinstance(name, types.StringTypes)
98
99     def _canonical_id(self, _id):
100         """Return a canonical form of `_id` suitable for accessing `._name_for_id`.
101
102         For some reason, `.GetSelection()`, etc. return items that
103         hash differently than the original `.AppendItem()`-returned
104         IDs.  This means that `._name_for_id[self.GetSelection()]`
105         will raise `KeyError`, even if there is an id `X` in
106         `._name_for_id` for which `X == self.GetSelection()` will
107         return `True`.  This method "canonicalizes" IDs so that the
108         hashing is consistent.
109         """
110         for c_id in self._name_for_id.keys():
111             if c_id == _id:
112                 return c_id
113         raise KeyError(_id)
114
115     def _on_selection_changed(self, event):
116         active_id = event.GetItem()
117         selected_id = self.GetSelection()
118         name = self._name_for_id[self._canonical_id(selected_id)]
119         if self._is_command(name):
120             self.select_command(self._commands[name])
121         else:
122             self.select_plugin(self._plugins[name])
123
124     def _on_execute(self, event):
125         self.execute()
126
127     def _on_motion(self, event):
128         """Enable tooltips.
129         """
130         hit_id,hit_flags = self.HitTest(event.GetPosition())
131         try:
132             hit_id = self._canonical_id(hit_id)
133         except KeyError:
134             hit_id = None
135         if hit_id == None:
136             msg = ''
137         else:
138             name = self._name_for_id[hit_id]
139             if self._is_command(name):
140                 msg = self._commands[name].help()
141             else:
142                 msg = ''  # self._plugins[name].help() TODO: Plugin.help method
143         if msg != self._last_tooltip:
144             self._last_tooltip = msg
145             event.GetEventObject().SetToolTipString(msg)
146
147     def select_plugin(self, plugin):
148         in_callback(self, plugin)
149
150     def select_command(self, command):
151         in_callback(self, command)
152
153     def execute(self):
154         _id = self.GetSelection()
155         name = self._name_for_id[self._canonical_id(_id)]
156         if self._is_command(name):
157             command = self._commands[name]
158             in_callback(self, command)
159
160
161 class CommandsPanel (Panel, wx.Panel):
162     """UI for selecting from available commands.
163
164     `callbacks` is shared with the underlying :class:`Tree`.
165     """
166     def __init__(self, callbacks=None, commands=None, selected=None, **kwargs):
167         super(CommandsPanel, self).__init__(
168             name='commands', callbacks=callbacks, **kwargs)
169         self._c = {
170             'tree': Tree(
171                 commands=commands,
172                 selected=selected,
173                 callbacks=callbacks,
174                 parent=self,
175                 pos=wx.Point(0, 0),
176                 size=wx.Size(160, 250),
177                 style=wx.TR_DEFAULT_STYLE|wx.NO_BORDER|wx.TR_HIDE_ROOT),
178             'execute': wx.Button(self, label='Execute'),
179             }
180         sizer = wx.BoxSizer(wx.VERTICAL)
181         sizer.Add(self._c['execute'], 0, wx.EXPAND)
182         sizer.Add(self._c['tree'], 1, wx.EXPAND)
183         # Put 'tree' second because its min size may be large enough
184         # to push the button out of view.
185         self.SetSizer(sizer)
186         sizer.Fit(self)
187
188         self.Bind(wx.EVT_BUTTON, self._on_execute_button)
189
190     def _on_execute_button(self, event):
191         self._c['tree'].execute()