class SimpleTransactionEditor(object):   
    def __init__(self, trans, transid, plugin, gui_parent,
                 change_register_function, book):
        """Sub classes should not override this __init__ but instead
        implement simple_init_before_show() to hook in at the right time
        """
        self.trans = trans
        self.transid = transid
        self.plugin = plugin
        self.gui_parent = gui_parent
        self.change_register_function = change_register_function
        self.book = book

        # An offscreen place for the transaction's GUI to hide while not in use.
        self.hide_parent = Window()
        self.hide_parent.hide()
        self.mainvbox = VBox()
        self.hide_parent.add(self.mainvbox)

        self.simple_init_before_show()

        self.mainvbox.show_all()
        self.mainvbox.reparent(self.gui_parent)

    def simple_init_before_show(self):
        """Create the GUI for this type of BoKeep transaction.  Widgets can be
        added to the "self.mainvbox" object."""
        
        raise Exception("simple_init_before_show must be overrided by "
                        "sub classes of SimpleTransactionEditor")

    def detach(self):
        """Detach the BoKeep transaction's GUI from the visible window and
        attach it to a hidden window.
        
        Sub classes overriding this are recommended to do their own work first, and
        then delegate back up to this original detach so it may do the widget reparenting
        work"""
        self.mainvbox.reparent(self.hide_parent)
class multipage_glade_editor(object):
    def __init__(self,
                 trans, transid, plugin, gui_parent, change_register_function,
                 book, display_mode=TRANSACTION_ALL_EDIT_FIRST_TIME,
                 transaction_edit_finished_function=null_function):
        self.trans = trans
        self.transid = transid
        self.plugin = plugin
        self.gui_parent = gui_parent
        self.change_register_function = change_register_function
        self.book = book
        self.display_mode = display_mode
        self.transaction_edit_finished_function = (
            null_function if display_mode not in HEADLESS_MODES
            else transaction_edit_finished_function )

        self.hide_parent = Window()
        self.hide_parent.hide()
        self.mainvbox = VBox()
        self.hide_parent.add(self.mainvbox)

        config = self.trans.get_configuration_and_provide_on_load_hook()
        config_module_name = self.plugin.config_module_name
        if not config_valid(config):
            # even in the case of a broken config, we should still
            # display all of the data we have available...
            self.mainvbox.pack_start(Label("no configuration"))
        elif not self.trans.can_safely_proceed_with_config_module(config):
            # should display all data that's available instead of just
            # this label
            #
            # and should give
            # user an overide option where they either pick an old config
            # for one time use or just blow out the memory of having used
            # a different config...
            #
            # should also print the checksum itself so they know
            # what they need...
            #
            # perhaps eventually we even put in place some archival support
            # for saving old glade and config files and then code into the
            # the transaction -- hey, code you need to be editable is
            # over here..
            #
            # now hopefully there is no marking of this transaction dirty
            # in this mode and the extra safegaurds we put into
            # MultipageGladeTransaction don't get activated
            self.mainvbox.pack_start(
                Label("out of date configuration. data is read only here for "
                      "the safety of your old information, last adler "
                      "CRC was %s" % self.trans.config_crc_cache ))
        else:
            # if the safety cache was relied on before we need to tell the
            # backend that the transaction is actually dirty,
            # and now that we know that we have a workable config,
            # there's a chance that we'll actually be able to avoid
            # relying on the cache this time
            if self.trans.get_safety_cache_was_used():
                self.change_register_function()

            self.page_label = Label("")
            (x_align, y_align) = self.page_label.get_alignment()
            self.page_label.set_alignment(0.0, y_align)
            self.mainvbox.pack_start(self.page_label, expand=False)

            # establish maincontainer, which is where the actual glade
            # pages are put by attach_current_page
            #
            # The order of placement here is important, we place this
            # after page_label has already been added to main
            # and we also need to do this prior to attach_current_page
            # being called, as it depends on self.maincontainer being
            # there
            #
            # when we're in headless mode the maincontainer can just be
            # the mainvbox itself
            #
            # but, outside headless mode we save screen real-estate and
            # place a scrolled window (which becomes the maincontainer)
            # inside the mainvbox and the glade by glade pages end up
            # in there instead (again, in attach_current_page)
            if display_mode in HEADLESS_MODES:
                self.maincontainer = self.mainvbox
            else:
                self.maincontainer = Viewport()
                sw = ScrolledWindow()
                sw.set_policy(POLICY_AUTOMATIC, POLICY_AUTOMATIC)
                sw.add( self.maincontainer)
                self.mainvbox.pack_start(sw)

            self.glade_pages = [
                self.__setup_page(glade_file, top_glade_element)
                for glade_file, top_glade_element in config.pages ]
            self.glade_pages_by_ident_index = dict(
                ( (key, self.glade_pages[i])
                  for i, key in enumerate(config.pages)
                  ) # end generator expression
                ) # end dict
            
            self.__setup_auto_widgets()

            self.current_page = 0
            self.attach_current_page()

            button_hbox = HBox()
            self.mainvbox.pack_end(button_hbox, expand=False)
            self.nav_buts = dict( (Button(), i)
                                  for i in range(2) )
            for but, i in self.nav_buts.iteritems():
                but.set_property('use-stock', True)
                but.set_label( STOCK_GO_BACK
                               if i == GLADE_BACK_NAV else STOCK_GO_FORWARD )
                button_hbox.pack_start(but, expand=False)
                but.connect("clicked", self.nav_but_clicked)

            config.gui_initialization_hook(
                self, self.trans, self.plugin, self.book)

        self.mainvbox.show_all()
        self.mainvbox.reparent(self.gui_parent)

    def page_is_current(self, page):
        # assumption is that config is fine and we're on a page
        assert( hasattr(self, 'glade_pages_by_ident_index') and
                True )
        return \
            self.glade_pages_by_ident_index[page] == self.current_widget_dict

    def __setup_page(self, glade_file, top_glade_element):
        widget_dict = {}
        
        # this should come from the config, seeing how we're setting up
        # our stuff manually
        event_handlers_dict = {}
        load_glade_file_get_widgets_and_connect_signals(
            glade_file, top_glade_element,
            widget_dict, event_handlers_dict )
        widget_dict[top_glade_element].hide()
        return widget_dict

    def __setup_auto_widgets(self):
        # go through each page
        for key, widget_dict in self.glade_pages_by_ident_index.iteritems():
            # go through each widget
            for widget_name, widget in widget_dict.iteritems():
                widget_key = (key, widget_name)

                # determine if the current widget is one that we can take
                # care of automatically saving, we do this with a linear
                # search through a table of eligible types,
                # where the first element of each entry is Widget class
                #
                # Once we have a match do one of the following
                #  * Change the widget from the existing stored stage
                #  * Establish new state from some default
                # The next two entries in the table
                # (change_widget_from_state, new_state_from_widget)
                # provide functions to do this
                #
                # Lastly, we establish event handlers for this widget using
                # the last element in the table
                for (cls,
                     change_widget_from_state, new_state_from_widget,
                     establish_event_handlers) in ( # start table

                    (Entry,
                     lambda w, wk: w.set_text(self.trans.get_widget_state(wk)),
                     lambda w: '',
                     lambda w: w.connect("changed", self.entry_changed),
                     ), # Entry

                    (Calendar,
                     self.__change_calendar_from_saved_version,
                     get_current_date_of_gtkcal,
                     lambda w: w.connect( "day_selected",
                                          self.calendar_changed ), 
                     ), # Calendar

                    (CheckButton,
                     lambda w, wk:
                         w.set_active(self.trans.get_widget_state(wk)),
                     get_state_of_checkbutton,
                     lambda w: w.connect("toggled", self.checkbutton_changed),
                     ), # CheckButton

                    ): # end of table and for declartion

                    # does the widget match the type in the current table entry?
                    if isinstance(widget, cls):

                        # use the three remaining functions in the table
                        # as appropriate
                        if self.trans.has_widget_state( widget_key ):
                            change_widget_from_state(widget, widget_key)
                        else:
                            self.trans.update_widget_state(
                                widget_key,
                                new_state_from_widget(widget) )
                        establish_event_handlers(widget)

    def __change_calendar_from_saved_version(self, widget, widget_key):
        set_current_date_of_gtkcal(widget, 
                                   self.trans.get_widget_state( widget_key ) )

    def attach_current_page(self):
        config = self.plugin.get_configuration(allow_reload=False)
        self.current_widget_dict = self.glade_pages[self.current_page] 
        self.current_window = self.current_widget_dict[
            config.pages[self.current_page][TOP_WIDGET] ]
        self.current_top_vbox = self.current_window.child
        self.current_top_vbox.reparent(
            self.maincontainer)
        self.page_label.set_text( "page %s of %s" %( self.current_page + 1,
                                                     len(config.pages) ) )
        self.update_auto_labels()

    def detach_current_page(self):
        # put the spawn back to wence it came
        self.current_top_vbox.reparent(
            self.current_window)

    def detach(self):
        if hasattr(self, 'current_page'):
            self.detach_current_page()
        self.mainvbox.reparent(self.hide_parent)

    def page_change_acceptable_by_input_valid(self):
        bad_fields = ', '.join( widget_name
            for widget_name, widget in self.current_widget_dict.iteritems()
            if not self.widget_valid(widget_name, widget)
            )
        if bad_fields == '':
            return True
        else:
            # this is kind of primiative, it would be better to
            # just highlight them by inserting red lights or something
            gtk_error_message("The following fields are invalid %s" %
                              bad_fields )
            return False
    
    def __current_page_ident(self):
        config = self.plugin.get_configuration(allow_reload=False)
        return config.pages[self.current_page]

    def __entry_widget_is_check_excempt(self, widget_name):
        config = self.plugin.get_configuration(allow_reload=False)
        return (self.__current_page_ident(), widget_name) in \
            config.non_decimal_check_labels

    def widget_valid(self, widget_name, widget):
        config = self.plugin.get_configuration(allow_reload=False)
        if isinstance(widget, Entry) and \
                not self.__entry_widget_is_check_excempt(widget_name):
            try:
                entry_to_decimal_convert(
                    widget.get_text(), widget_name,
                    self.__current_page_ident(),
                    config)
            except EntryTextToDecimalConversionFail:
                return False
        # this covers not only the else case on the first if, but the
        # the case with the above try, except passes without exception
        return True

    def nav_but_clicked(self, but, *args):
        config = self.plugin.get_configuration(allow_reload=False)
        
        old_page = self.current_page
        delta = -1 if self.nav_buts[but] == GLADE_BACK_NAV else 1
        new_page = old_page + delta
        # reject a change outside the acceptable range.. and hmm,
        # perhaps this event handler should never even run under those
        # conditions because we should really just grey the buttons
        if not (new_page < 0 or new_page == len(self.glade_pages)) and \
                self.page_change_acceptable_by_input_valid() and \
                config.page_change_acceptable(self.trans, old_page, new_page):

            # intentionally done before the page is actually attached,
            # that's what we mean by pre
            config.page_pre_change_config_hooks(self.trans, old_page, new_page)

            self.detach_current_page()
            self.current_page = new_page
            self.attach_current_page()
            
            # intentionally done after the page is actually attached,
            # that's what we mean by post
            config.page_post_change_config_hooks(self.trans, old_page, new_page)

    entry_changed = make_widget_changed_func(lambda w: w.get_text() )
    calendar_changed = make_widget_changed_func(get_current_date_of_gtkcal)
    checkbutton_changed = make_widget_changed_func(get_state_of_checkbutton)

    def update_auto_labels(self):
        config = self.plugin.get_configuration(allow_reload=False)
        # this function should never be called if the config hasn't been
        # checked out as okay
        assert( hasattr(config, 'auto_update_labels') )
        for page, label_name, label_source_func in config.auto_update_labels:
            if self.page_is_current(page):
                try:
                    label_text = str(label_source_func(
                            self.trans.widget_states, config))
                except EntryTextToDecimalConversionFail, e:
                    label_text = ''
                except WidgetFindError, no_find_e:
                    label_text = str(no_find_e)
                self.current_widget_dict[label_name].set_text(label_text)