# imager.py # Dexter Kozen (dck10) and Walker White (wmw2) # October 26, 2012 """This module provides the primary Controller for our application. The application corresponds to the class ImagerApp. All of the other classes are individual controllers for each of the View components defined in imager.kv. The View (imager.kv) and this Controller module (imager.py) have the same name because they are so tightly interconnected.""" from kivy.app import App from kivy.uix.popup import Popup from kivy.uix.boxlayout import BoxLayout from kivy.properties import ObjectProperty, StringProperty from kivy.config import Config from kivy.clock import Clock import os.path import image_array import image_processor import image_panel class LoadDialog(BoxLayout): """Instance is a controller for a LoadDialog, a pop-up dialog to load a file. The View for this controller is defined in imager.kv.""" # These fields are 'hooks' to connect to the imager.kv file # They work sort of like @properties, but are different filechooser = ObjectProperty(None) load = ObjectProperty(None) cancel = ObjectProperty(None) class SaveDialog(BoxLayout): """Instance is a controller for a SaveDialog, a pop-up dialog to save a file. The View for this controller is defined in imager.kv.""" # These fields are 'hooks' to connect to the imager.kv file # They work sort of like @properties, but are different save = ObjectProperty(None) text_input = ObjectProperty(None) cancel = ObjectProperty(None) class ErrorDialog(BoxLayout): """Instance is a controller for an ErrorDialog, a pop-up dialog to notify the user of an error. The View for this controller is defined in imager.kv.""" # These fields are 'hooks' to connect to the imager.kv file # They work sort of like @properties, but are different label = StringProperty('') ok = ObjectProperty(None) class WarningDialog(BoxLayout): """Instance is a controller for a WarningDialog, a pop-up dialog to warn the user. It differs from ErrorDialog in that it may be nested inside of another pop-up dialog. The View for this controller is defined in imager.kv.""" # These fields are 'hooks' to connect to the imager.kv file # They work sort of like @properties, but are different label = StringProperty('') data = StringProperty('') ok = ObjectProperty(None) cancel = ObjectProperty(None) class Main(BoxLayout): """Instance is a controller for the primary application. This controller manages all of the buttons and text fields of the application. It instantiates ImageProcessor (the student defined class), and uses that sub-controller to process images. The View for this controller is defined in imager.kv. Instance variables: _operand : current executing option, if any, or None _op_args : arguments for the executing option or None _popup : active popup window, if any, or None """ # These fields are 'hooks' to connect to the imager.kv file # They work sort of like @properties, but are different source = StringProperty('samples/goldhill.jpg') original_image = ObjectProperty(None) current_image = ObjectProperty(None) grayscale = ObjectProperty(None) hidden_text = ObjectProperty(None) text_input = ObjectProperty(None) image_processor = ObjectProperty(None) notifier = ObjectProperty(None) def config(self): """Configures the application at start-up. Loads the currently selected image file, creates an ImageArray for that file, creates an ImageProcessor to handle the array, and connects the ImageProcessor to the two ImagePanel objects.""" # Load the image into an array array = image_array.load_file(self.source) # Create the processor for the given ImageArray self.image_processor = image_processor.ImageProcessor(array) # Set up the display panels self.original_image_panel = image_panel.ImagePanel(self.original_image, self.image_processor.original) self.current_image_panel = image_panel.ImagePanel(self.current_image, self.image_processor.current) # For later use self._operand = None self._op_args = None self._popup = None def error(self, msg): """Report an error to the user :param msg: the error message **Precondition**: a string The error message will take up most of the Window, and last until the user dismisses it.""" assert type(msg) == str, `msg`+' is not a string' content = ErrorDialog(label=msg, ok=self._dismiss_popup) self._popup = Popup(title='Error', content=content, size_hint=(0.9, 0.9)) self._popup.open() def do(self, trans, *args): """Perform a transformation on the image. :param trans: transformation method in ImageProcessor **Precondition** : a reference to a method or function, not a string for its name :param args: list of arguments for `transform` **Precondition**: a list or tuple with valid argument values This method does not enforce its preconditions. Use with care.""" if not self._operand is None: return # Say PROCESSING... self.notifier.color = [1,1,1,1] self._operand = trans self._op_args = args # Process the transform on the next clock cycle. Clock.schedule_once(self._do_async) def _do_async(self,dt): """Perform the active image transform. Hidden method that allows us to spread a transformation over two clock cycles. This allows us to print a progress message on the screen.""" # Perform the transformation if len(self._op_args) == 0: self._operand() else: self._operand(self._op_args[0]) # Remove the status message and redisplay self.notifier.color = [0,0,0,0] self.current_image_panel.display(self.image_processor.current) self._operand = None self._op_args = None def _dismiss_popup(self): """Used to dismiss the currently active pop-up""" self._popup.dismiss() self._popup = None def load(self): """Open a dialog to load an image file.""" content = LoadDialog(load=self._load_helper, cancel=self._dismiss_popup) self._popup = Popup(title="Load image", content=content, size_hint=(0.9, 0.9)) self._popup.open() def _load_helper(self, path, filename): """Callback function for load. Called when user selects a file. This method loads the image file and redisplays the ImagePanels. Hidden method used only internally. No preconditions enforced.""" self._dismiss_popup() if (len(filename) == 0): return self.source = str(os.path.join(path, filename[0])) self.config() self.original_image_panel.display() self.current_image_panel.display() def save(self): """Save the image in the current ImageArray to a file.""" content = SaveDialog(save=self._save_png, cancel=self._dismiss_popup) self._popup = Popup(title="Save image", content=content, size_hint=(0.9, 0.9)) self._popup.open() def _save_png(self, path, filename): """Check whether file exists before saving. Saves the file if does not exist or user confirms. Hidden method used only internally. No preconditions except png suffix enforced.""" print 'save path', `path` print 'save fname', `filename` assert filename.lower().endswith('.png') self._dismiss_popup() if os.path.isfile(filename): msg = 'File\n{}\nexists. Overwrite?' self._file_warning(msg.format(filename), filename, self._force_save) else: self._just_save(filename) def _force_save(self, filename): """Forceably saves the specified file, without user confirmation. Hidden method used only internally. No preconditions enforced.""" self._dismiss_popup() self._just_save(filename) def _just_save(self, filename): # prepare image for saving im = self.image_processor.current.get_image() # Direct file descriptor save broken on Windows # with open(filename, 'w') as f: try: im.save(filename, 'PNG') except: self.error('Cannot save image file: ' + filename) #f.close def _file_warning(self, msg, filename, ok): """Alerts the user of an issue when trying to load or save a file Hidden method used only internally. No preconditions enforced.""" content = WarningDialog(label=msg, data=filename, ok=ok, cancel=self._dismiss_popup) self._popup = Popup(title='Warning', content=content, size_hint=(0.9, 0.9)) self._popup.open() class ImagerApp(App): """Instance is an Imager application. This class is the primary controller. It creates all of the views and other objects. However, after start-up, it delegates all activity to the `Main` class.""" def build(self): """Read kivy file and perform layout""" Config.set('graphics', 'width', '1000') Config.set('graphics', 'height', '500') #Config.set('graphics', 'resizable', '0') # make not resizable return Main() def on_start(self): """Start up the app and initialize values""" super(ImagerApp, self).on_start() self.root.config() # Application Code if __name__ == '__main__': ImagerApp().run()