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