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