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