f46f098bd21713c841759ec15c6de7f7b2e163b2
[hooke.git] / hooke / ui / gui / panel / commands.py
1 #!/usr/bin/env python\r
2 \r
3 """Commands and settings panel for Hooke.\r
4 \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
10 \r
11 # TODO: command arguments.\r
12 """\r
13 \r
14 import types\r
15 \r
16 import wx\r
17 \r
18 from ....util.callback import callback, in_callback\r
19 from . import Panel\r
20 \r
21 \r
22 class Tree (wx.TreeCtrl):\r
23     """The main widget of :class:`CommandsPanel`.\r
24 \r
25     `callbacks` is shared with the parent :class:`Commands`.\r
26     """\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
35         self.image = {\r
36             'root': 0,\r
37             'plugin': 0,\r
38             'command': 1,\r
39             }\r
40         self._c = {\r
41             'root': self.AddRoot(\r
42                 text='Commands and Settings', image=self.image['root']),\r
43             }\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
47 \r
48         self._callbacks = callbacks\r
49         self._setup_commands(commands, selected)\r
50         self._last_tooltip = None\r
51 \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
55 \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
61 \r
62         selected = None\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
69                 text=plugin.name,\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
78                 text=command.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
83                 selected = _id\r
84 \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
91 \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
94         """\r
95         # Plugin names are strings, Command names are tuples.\r
96         # See ._setup_commands().\r
97         return not isinstance(name, types.StringTypes)\r
98 \r
99     def _canonical_id(self, _id):\r
100         """Return a canonical form of `_id` suitable for accessing `._name_for_id`.\r
101 \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
109         """\r
110         for c_id in self._name_for_id.keys():\r
111             if c_id == _id:\r
112                 return c_id\r
113         raise KeyError(_id)\r
114 \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
121         else:\r
122             self.select_plugin(self._plugins[name])\r
123 \r
124     def _on_execute(self, event):\r
125         self.execute()\r
126 \r
127     def _on_motion(self, event):\r
128         """Enable tooltips.\r
129         """\r
130         hit_id,hit_flags = self.HitTest(event.GetPosition())\r
131         try:\r
132             hit_id = self._canonical_id(hit_id)\r
133         except KeyError:\r
134             hit_id = None\r
135         if hit_id == None:\r
136             msg = ''\r
137         else:\r
138             name = self._name_for_id[hit_id]\r
139             if self._is_command(name):\r
140                 msg = self._commands[name].help()\r
141             else:\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
146 \r
147     def select_plugin(self, plugin):\r
148         in_callback(self, plugin)\r
149 \r
150     def select_command(self, command):\r
151         in_callback(self, command)\r
152 \r
153     def execute(self):\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
159 \r
160 \r
161 class CommandsPanel (Panel, wx.Panel):\r
162     """UI for selecting from available commands.\r
163 \r
164     `callbacks` is shared with the underlying :class:`Tree`.\r
165     """\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
169         self._c = {\r
170             'tree': Tree(\r
171                 commands=commands,\r
172                 selected=selected,\r
173                 callbacks=callbacks,\r
174                 parent=self,\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
179             }\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
186         sizer.Fit(self)\r
187 \r
188         self.Bind(wx.EVT_BUTTON, self._on_execute_button)\r
189 \r
190     def _on_execute_button(self, event):\r
191         self._c['tree'].execute()\r