9822c4d508cc5e71a18e89c29013e19cffe822be
[PoPiPaint.git] / CanvasFrame.py
1 #!/usr/bin/env python
2 #
3 # Copyright (C) 2014  Antonio Ospite <ao2@ao2.it>
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
18 import wx
19 from wx.lib.pubsub import Publisher as pub
20 from wx.lib.wordwrap import wordwrap
21
22 import CanvasModel
23 import CanvasView
24
25 ICON_FILE = "res/ao2.ico"
26
27 IMAGE_WIDTH = 101
28 IMAGE_HEIGHT = 30
29 INTERNAL_RADIUS = 2
30
31
32 class CanvasFrame(wx.Frame):
33     def __init__(self, *args, **kwargs):
34         base_image = kwargs.pop('base_image', None)
35         wx.Frame.__init__(self, *args, **kwargs)
36
37         # Set up a sizer BEFORE every other action, in order to prevent any
38         # weird side effects; for instance self.SetToolBar() seems to resize
39         # child windows...
40         vsizer = wx.BoxSizer(orient=wx.VERTICAL)
41         self.SetSizer(vsizer)
42
43         # Instantiate the Model and set up the view
44         self.model = CanvasModel.Canvas(IMAGE_WIDTH, IMAGE_HEIGHT,
45                                         INTERNAL_RADIUS)
46
47         self.view = CanvasView.CanvasView(self, model=self.model,
48                                           base_image=base_image)
49         vsizer.Add(self.view, 0, wx.SHAPED)
50
51         icon = wx.Icon(ICON_FILE, wx.BITMAP_TYPE_ICO)
52         self.SetIcon(icon)
53
54         # Set up the menu bar
55         menu_bar = self._BuildMenu()
56         self.SetMenuBar(menu_bar)
57
58         # Tool bar
59         tool_bar = self._BuildTools()
60         self.SetToolBar(tool_bar)
61
62         # Status bar
63         status_bar = wx.StatusBar(self)
64         status_bar.SetWindowStyle(status_bar.GetWindowStyle() ^ wx.ST_SIZEGRIP)
65         status_bar.SetFieldsCount(3)
66         self.SetStatusBar(status_bar)
67
68         # View callbacks
69         pub.subscribe(self.UpdateStatusBar, "NEW PIXEL")
70         pub.subscribe(self.UpdateView, "NEW PIXEL")
71
72         # Controller Methods
73         self.view.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
74         self.view.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
75         self.view.Bind(wx.EVT_MOTION, self.OnMouseMotion)
76
77         # other Events
78         self.Bind(wx.EVT_CLOSE, self.OnQuit)
79
80         # The frame gets resized to fit all its elements
81         self.GetSizer().Fit(self)
82         # and centered on screen
83         self.Center(wx.BOTH | wx.CENTER_ON_SCREEN)
84
85     def _BuildTools(self):
86         tool_bar = wx.ToolBar(self, style=wx.TB_HORZ_LAYOUT|wx.TB_TEXT)
87
88         color_picker_label = wx.StaticText(tool_bar, label=" Color picker ")
89         tool_bar.AddControl(color_picker_label)
90
91         self.color = wx.WHITE
92
93         ID_COLOR_PICKER = wx.NewId()
94         color_picker = wx.ColourPickerCtrl(tool_bar, ID_COLOR_PICKER, self.color,
95                                            size=wx.Size(32,32),
96                                            name="Color Picker")
97         tool_bar.AddControl(color_picker)
98         wx.EVT_COLOURPICKER_CHANGED(self, ID_COLOR_PICKER, self.OnPickColor)
99
100         tool_bar.AddSeparator()
101
102         ID_SHOW_GRID = wx.NewId()
103         show_grid_checkbox = wx.CheckBox(tool_bar, ID_SHOW_GRID, label="Show grid", style=wx.ALIGN_RIGHT)
104         show_grid_checkbox.SetValue(self.view.draw_grid)
105         tool_bar.AddControl(show_grid_checkbox)
106         wx.EVT_CHECKBOX(tool_bar, ID_SHOW_GRID, self.OnShowGrid)
107
108         tool_bar.Realize()
109
110         return tool_bar
111
112     def _BuildMenu(self):
113         menu_bar = wx.MenuBar()
114
115         # File menu
116         file_menu = wx.Menu()
117         menu_bar.Append(file_menu, '&File')
118
119         ID_NEW_BITMAP = wx.ID_NEW
120         file_menu.Append(ID_NEW_BITMAP, 'New Bitmap', 'Start a new bitmap')
121         wx.EVT_MENU(self, ID_NEW_BITMAP, self.OnNewBitmap)
122
123         ID_LOAD_BITMAP = wx.ID_OPEN
124         file_menu.Append(ID_LOAD_BITMAP, 'Load Bitmap', 'Load a bitmap')
125         wx.EVT_MENU(self, ID_LOAD_BITMAP, self.OnLoadBitmap)
126
127         ID_SAVE_BITMAP = wx.ID_SAVE
128         file_menu.Append(ID_SAVE_BITMAP, 'Save Bitmap', 'Save a bitmap')
129         wx.EVT_MENU(self, ID_SAVE_BITMAP, self.OnSaveBitmap)
130
131         file_menu.AppendSeparator()
132
133         # Export sub-menu
134         export_menu = wx.Menu()
135         file_menu.AppendMenu(wx.ID_ANY, 'E&xport', export_menu)
136
137         ID_EXPORT_ANIMATION = wx.NewId()
138         export_menu.Append(ID_EXPORT_ANIMATION, 'Export animation', 'Export as animation')
139         wx.EVT_MENU(self, ID_EXPORT_ANIMATION, self.OnExportAnimation)
140
141         ID_EXPORT_SNAPSHOT = wx.NewId()
142         export_menu.Append(ID_EXPORT_SNAPSHOT, 'Export snapshot',
143                           'Export a snapshot of the current canvas')
144         wx.EVT_MENU(self, ID_EXPORT_SNAPSHOT, self.OnExportSnapshot)
145
146         # Last item of file_menu
147         ID_EXIT_MENUITEM = wx.ID_EXIT
148         file_menu.Append(ID_EXIT_MENUITEM, 'E&xit\tAlt-X', 'Exit the program')
149         wx.EVT_MENU(self, ID_EXIT_MENUITEM, self.OnQuit)
150
151         # Help menu
152         help_menu = wx.Menu()
153         menu_bar.Append(help_menu, '&Help')
154
155         ID_HELP_MENUITEM = wx.ID_HELP
156         help_menu.Append(ID_HELP_MENUITEM, 'About\tAlt-A', 'Show Informations')
157         wx.EVT_MENU(self, ID_HELP_MENUITEM, self.ShowAboutDialog)
158
159         return menu_bar
160
161     def addPixel(self, event):
162         x, y = event.GetLogicalPosition(self.view.dc)
163         self.SetStatusText("Last Click at %-3d,%-3d" % (x, y), 0)
164
165         r, theta = CanvasView.cartesian2polar(x, y, self.view.offset_angle)
166         self.model.setPixelColor(r, theta, self.color)
167
168     def OnNewBitmap(self, event):
169         if self.ShowConfirmationDialog() == wx.ID_YES:
170             self.model.Reset()
171             self.view.drawAllPixels()
172             self.view.Refresh()
173
174     def OnLoadBitmap(self, event):
175         dialog = wx.FileDialog(self, "Load bitmap", "", "",
176                                "PNG files (*.png)|*.png",
177                                wx.FD_OPEN | wx.FD_FILE_MUST_EXIST)
178         ret = dialog.ShowModal()
179         file_path = dialog.GetPath()
180         dialog.Destroy()
181
182         if ret == wx.ID_CANCEL:
183             return
184
185         if self.view.loadImage(file_path):
186             self.view.drawAllPixels()
187             self.view.Refresh()
188         else:
189             self.ShowErrorDialog("Image is not %dx%d" % (self.model.width,
190                                                          self.model.height))
191
192     def OnSaveBitmap(self, event):
193         dialog = wx.FileDialog(self, "Save bitmap", "", "",
194                                "PNG files (*.png)|*.png",
195                                wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT)
196         ret = dialog.ShowModal()
197         file_path = dialog.GetPath()
198         dialog.Destroy()
199
200         if ret == wx.ID_CANCEL:
201             return
202
203         bitmap = wx.BitmapFromBuffer(self.model.width, self.model.height,
204                                      self.model.pixels_array)
205         bitmap.SaveFile(file_path, wx.BITMAP_TYPE_PNG)
206
207     def OnExportAnimation(self, event):
208         dialog = wx.FileDialog(self, "Save animation", "", "animation.h",
209                                "C header files (*.h)|*.h",
210                                wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT)
211         ret = dialog.ShowModal()
212         file_path = dialog.GetPath()
213         dialog.Destroy()
214
215         if ret == wx.ID_CANCEL:
216             return
217
218         self.model.saveAsAnimation(file_path)
219
220     def OnExportSnapshot(self, event):
221         dialog = wx.FileDialog(self, "Take snapwhot", "", "snapshot.png",
222                                "PNG files (*.png)|*.png",
223                                wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT)
224         ret = dialog.ShowModal()
225         file_path = dialog.GetPath()
226         dialog.Destroy()
227
228         if ret == wx.ID_CANCEL:
229             return
230
231         self.view.pixels_buffer.SaveFile(file_path, wx.BITMAP_TYPE_PNG)
232
233     def OnQuit(self, event):
234         if self.ShowConfirmationDialog("Exit the program?") == wx.ID_YES:
235             self.Destroy()
236
237     def OnPickColor(self, event):
238         self.color = event.Colour.asTuple()
239
240     def OnShowGrid(self, event):
241         self.view.draw_grid = event.Checked()
242         self.view.Refresh()
243
244     def OnLeftDown(self, event):
245         self.addPixel(event)
246         self.view.CaptureMouse()
247
248     def OnLeftUp(self, event):
249         self.view.ReleaseMouse()
250
251     def OnMouseMotion(self, event):
252         if event.Dragging() and event.LeftIsDown():
253             self.addPixel(event)
254
255     def UpdateStatusBar(self, event):
256         if self.model.last_pixel:
257             x, y = self.model.last_pixel
258             r, theta = self.model.toPolar(x, y)
259             self.SetStatusText("r: %-4.1f theta: %-4.1f" % (r, theta), 1)
260             self.SetStatusText("x: %-2d y: %-2d" % (x, y), 2)
261
262     def UpdateView(self, event):
263         if self.model.last_pixel:
264             self.view.drawPixel(self.model.last_pixel)
265             self.view.Refresh()
266
267     def ShowConfirmationDialog(self, message=None):
268         if not message:
269             message = "With this operation you can loose your data.\n\n"
270             message += "Are you really sure you want to proceed?"
271
272         dialog = wx.MessageDialog(self, message, "Warning!",
273                                   style=wx.YES_NO | wx.ICON_QUESTION)
274         ret = dialog.ShowModal()
275         dialog.Destroy()
276
277         return ret
278
279     def ShowErrorDialog(self, message):
280         dialog = wx.MessageDialog(self, message, "Error!",
281                                   style=wx.OK | wx.ICON_ERROR)
282         ret = dialog.ShowModal()
283         dialog.Destroy()
284
285     def ShowAboutDialog(self, event):
286         info = wx.AboutDialogInfo()
287         info.Name = "PoPiPaint - Polar Pixel Painter"
288         info.Copyright = "(C) 2014 Antonio Ospite"
289         text = "A prototype program for the JMPrope project,"
290         text += "the programmable jump rope with LEDs."
291         info.Description = wordwrap(text, 350, wx.ClientDC(self))
292         info.WebSite = ("http://ao2.it", "http://ao2.it")
293         info.Developers = ["Antonio Ospite"]
294         info.License = "GNU/GPLv3"
295         wx.AboutBox(info)