3 # Copyright (C) 2014 Antonio Ospite <ao2@ao2.it>
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.
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.
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/>.
19 from wx.lib.pubsub import Publisher as pub
20 from wx.lib.wordwrap import wordwrap
25 ICON_FILE = "res/ao2.ico"
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)
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
40 vsizer = wx.BoxSizer(orient=wx.VERTICAL)
43 # Instantiate the Model and set up the view
44 self.model = CanvasModel.Canvas(IMAGE_WIDTH, IMAGE_HEIGHT,
47 self.view = CanvasView.CanvasView(self, model=self.model,
48 base_image=base_image)
49 vsizer.Add(self.view, 0, wx.SHAPED)
51 icon = wx.Icon(ICON_FILE, wx.BITMAP_TYPE_ICO)
55 menu_bar = self._BuildMenu()
56 self.SetMenuBar(menu_bar)
59 tool_bar = self._BuildTools()
60 self.SetToolBar(tool_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)
69 pub.subscribe(self.UpdateStatusBar, "NEW PIXEL")
70 pub.subscribe(self.UpdateView, "NEW PIXEL")
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)
78 self.Bind(wx.EVT_CLOSE, self.OnQuit)
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)
85 def _BuildTools(self):
86 tool_bar = wx.ToolBar(self, style=wx.TB_HORZ_LAYOUT|wx.TB_TEXT)
88 color_picker_label = wx.StaticText(tool_bar, label=" Color picker ")
89 tool_bar.AddControl(color_picker_label)
93 ID_COLOR_PICKER = wx.NewId()
94 color_picker = wx.ColourPickerCtrl(tool_bar, ID_COLOR_PICKER, self.color,
97 tool_bar.AddControl(color_picker)
98 wx.EVT_COLOURPICKER_CHANGED(self, ID_COLOR_PICKER, self.OnPickColor)
100 tool_bar.AddSeparator()
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)
112 def _BuildMenu(self):
113 menu_bar = wx.MenuBar()
116 file_menu = wx.Menu()
117 menu_bar.Append(file_menu, '&File')
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)
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)
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)
131 file_menu.AppendSeparator()
134 export_menu = wx.Menu()
135 file_menu.AppendMenu(wx.ID_ANY, 'E&xport', export_menu)
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)
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)
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)
152 help_menu = wx.Menu()
153 menu_bar.Append(help_menu, '&Help')
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)
161 def addPixel(self, event):
162 x, y = event.GetLogicalPosition(self.view.dc)
163 self.SetStatusText("Last Click at %-3d,%-3d" % (x, y), 0)
165 r, theta = CanvasView.cartesian2polar(x, y, self.view.offset_angle)
166 self.model.setPixelColor(r, theta, self.color)
168 def OnNewBitmap(self, event):
169 if self.ShowConfirmationDialog() == wx.ID_YES:
171 self.view.drawAllPixels()
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()
182 if ret == wx.ID_CANCEL:
185 if self.view.loadImage(file_path):
186 self.view.drawAllPixels()
189 self.ShowErrorDialog("Image is not %dx%d" % (self.model.width,
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()
200 if ret == wx.ID_CANCEL:
203 bitmap = wx.BitmapFromBuffer(self.model.width, self.model.height,
204 self.model.pixels_array)
205 bitmap.SaveFile(file_path, wx.BITMAP_TYPE_PNG)
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()
215 if ret == wx.ID_CANCEL:
218 self.model.saveAsAnimation(file_path)
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()
228 if ret == wx.ID_CANCEL:
231 self.view.pixels_buffer.SaveFile(file_path, wx.BITMAP_TYPE_PNG)
233 def OnQuit(self, event):
234 if self.ShowConfirmationDialog("Exit the program?") == wx.ID_YES:
237 def OnPickColor(self, event):
238 self.color = event.Colour.asTuple()
240 def OnShowGrid(self, event):
241 self.view.draw_grid = event.Checked()
244 def OnLeftDown(self, event):
246 self.view.CaptureMouse()
248 def OnLeftUp(self, event):
249 self.view.ReleaseMouse()
251 def OnMouseMotion(self, event):
252 if event.Dragging() and event.LeftIsDown():
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)
262 def UpdateView(self, event):
263 if self.model.last_pixel:
264 self.view.drawPixel(self.model.last_pixel)
267 def ShowConfirmationDialog(self, message=None):
269 message = "With this operation you can loose your data.\n\n"
270 message += "Are you really sure you want to proceed?"
272 dialog = wx.MessageDialog(self, message, "Warning!",
273 style=wx.YES_NO | wx.ICON_QUESTION)
274 ret = dialog.ShowModal()
279 def ShowErrorDialog(self, message):
280 dialog = wx.MessageDialog(self, message, "Error!",
281 style=wx.OK | wx.ICON_ERROR)
282 ret = dialog.ShowModal()
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"