1d8313ae1812bb694e9f227d3c9c092910bf5678
[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 \r
20 \r
21 class Tree (wx.TreeCtrl):\r
22     """\r
23 \r
24     `callbacks` is shared with the parent :class:`Commands`.\r
25     """\r
26     def __init__(self, commands, selected, callbacks, *args, **kwargs):\r
27         super(Tree, self).__init__(*args, **kwargs)\r
28         imglist = wx.ImageList(width=16, height=16, mask=True, initialCount=2)\r
29         imglist.Add(wx.ArtProvider.GetBitmap(\r
30                 wx.ART_FOLDER, wx.ART_OTHER, wx.Size(16, 16)))\r
31         imglist.Add(wx.ArtProvider.GetBitmap(\r
32                 wx.ART_EXECUTABLE_FILE, wx.ART_OTHER, wx.Size(16, 16)))\r
33         self.AssignImageList(imglist)\r
34         self.image = {\r
35             'root': 0,\r
36             'plugin': 0,\r
37             'command': 1,\r
38             }\r
39         self._c = {\r
40             'root': self.AddRoot(\r
41                 text='Commands and Settings', image=self.image['root']),\r
42             }\r
43         self.Bind(wx.EVT_TREE_SEL_CHANGED, self._on_selection_changed)\r
44         self.Bind(wx.EVT_TREE_ITEM_ACTIVATED, self._on_execute)\r
45 \r
46         self._callbacks = callbacks\r
47         self._setup_commands(commands, selected)\r
48 \r
49     def _setup_commands(self, commands, selected):\r
50         self._plugins = {}    # {name: hooke.plugin.Plugin()}\r
51         self._commands = {}   # {name: hooke.command.Command()}\r
52 \r
53         # In both of the following dicts, command names are\r
54         # (plugin.name, command.name) to avoid cross-plugin\r
55         # collisions.  See ._is_command().\r
56         self._id_for_name = {}  # {name: id}\r
57         self._name_for_id = {}  # {id: name}\r
58 \r
59         selected = None\r
60         plugins = sorted(set([c.plugin for c in commands]),\r
61                          key=lambda p:p.name)\r
62         for plugin in plugins:\r
63             self._plugins[plugin.name] = plugin\r
64             _id = self.AppendItem(\r
65                 parent=self._c['root'],\r
66                 text=plugin.name,\r
67                 image=self.image['plugin'])\r
68             self._id_for_name[plugin.name] = _id\r
69             self._name_for_id[_id] = plugin.name\r
70         for command in sorted(commands, key=lambda c:c.name):\r
71             name = (command.plugin.name, command.name)\r
72             self._commands[name] = command\r
73             _id = self.AppendItem(\r
74                 parent=self._id_for_name[command.plugin.name],\r
75                 text=command.name,\r
76                 image=self.image['command'])\r
77             self._id_for_name[name] = _id\r
78             self._name_for_id[_id] = name\r
79             if command.name == selected:\r
80                 selected = _id\r
81 \r
82         for plugin in self._plugins.values():\r
83             self.Expand(self._id_for_name[plugin.name])\r
84         # make sure the selected command/plugin is visible in the tree\r
85         if selected is not None:\r
86             self.SelectItem(selected, True)\r
87             self.EnsureVisible(selected)\r
88 \r
89     def _is_command(self, name):  # name from ._id_for_name / ._name_for_id\r
90         """Return `True` if `name` corresponds to a :class:`hooke.command.Command`.\r
91         """\r
92         # Plugin names are strings, Command names are tuples.\r
93         # See ._setup_commands().\r
94         return not isinstance(name, types.StringTypes)\r
95 \r
96     def _canonical_id(self, _id):\r
97         """Return a canonical form of `_id` suitable for accessing `._name_for_id`.\r
98 \r
99         For some reason, `.GetSelection()`, etc. return items that\r
100         hash differently than the original `.AppendItem()`-returned\r
101         IDs.  This means that `._name_for_id[self.GetSelection()]`\r
102         will raise `KeyError`, even if there is an id `X` in\r
103         `._name_for_id` for which `X == self.GetSelection()` will\r
104         return `True`.  This method "canonicalizes" IDs so that the\r
105         hashing is consistent.\r
106         """\r
107         for c_id in self._name_for_id.keys():\r
108             if c_id == _id:\r
109                 return c_id\r
110         return _id\r
111 \r
112     def _on_selection_changed(self, event):\r
113         active_id = event.GetItem()\r
114         selected_id = self.GetSelection()\r
115         name = self._name_for_id[self._canonical_id(selected_id)]\r
116         if self._is_command(name):\r
117             self.select_command(self._commands[name])\r
118         else:\r
119             self.select_plugin(self._plugins[name])\r
120 \r
121     def _on_execute(self, event):\r
122         self.execute()\r
123 \r
124     def select_plugin(self, plugin):\r
125         in_callback(self, plugin)\r
126 \r
127     def select_command(self, command):\r
128         in_callback(self, command)\r
129 \r
130     def execute(self):\r
131         item = self.GetSelection()\r
132         if item.IsOk():\r
133             if not self.ItemHasChildren(item):\r
134                 item_text = self.GetItemText(item)\r
135                 # TODO: generate args\r
136                 in_callback(self, command, args)\r
137 \r
138 \r
139 class Commands (wx.Panel):\r
140     """\r
141 \r
142     `callbacks` is shared with the underlying :class:`Tree`.\r
143     """\r
144     def __init__(self, commands, selected, callbacks, *args, **kwargs):\r
145         super(Commands, self).__init__(*args, **kwargs)\r
146         self._c = {\r
147             'tree': Tree(\r
148                 commands=commands,\r
149                 selected=selected,\r
150                 callbacks=callbacks,\r
151                 parent=self,\r
152                 pos=wx.Point(0, 0),\r
153                 size=wx.Size(160, 250),\r
154                 style=wx.TR_DEFAULT_STYLE|wx.NO_BORDER|wx.TR_HIDE_ROOT),\r
155             'execute': wx.Button(self, label='Execute'),\r
156             }\r
157         sizer = wx.BoxSizer(wx.VERTICAL)\r
158         sizer.Add(self._c['execute'], 0, wx.EXPAND)\r
159         sizer.Add(self._c['tree'], 1, wx.EXPAND)\r
160         self.SetSizer(sizer)\r
161         sizer.Fit(self)\r
162 \r
163         self.Bind(wx.EVT_BUTTON, self._on_execute_button)\r
164         self._callbacks = callbacks\r
165 \r
166     def _on_execute_button(self, event):\r
167         self._c['tree'].execute()\r