Example #1
0
class GridWorldEnv(gym.Env):
    metadata = {
        'render.modes': ['human']
    }

    def __init__(self):
        self.width = 16
        self.height = 9
        self._cell_size = 10

        self.action_space = spaces.Discrete(4)
        self.observation_space = spaces.Box(self.height * self._cell_size, self.width * self._cell_size, 1)

        self.viewer = Viewer(width=self.width, height=self.height, cell_size=self._cell_size)

        self._seed()
        self.reset()

    def _seed(self, seed=None):
        self.np_random, seed = seeding.np_random(seed)
        return [seed]

    def _step(self, action):
        assert self.action_space.contains(action), "%r (%s) invalid" % (action, type(action))
        self.viewer.move_agent(action)
        self.state = self.viewer.get_state()
        done = self.viewer.is_on_goal()
        reward = 1 if done else 0
        return self.state, reward, done, {}

    def _reset(self):
        self.viewer.reset_agent()
        self.state = self.viewer.get_state()
        return self.state

    def _render(self, mode='human', close=False):
        if close:
            if self.viewer is not None:
                self.viewer.close()
                self.viewer = None
            return
        return self.viewer.render()

    def set_grid_size(self, width, height):
        self.width = width
        self.height = height
        self.viewer = Viewer(height=self.height, width=self.width, cell_size=self._cell_size)
        self.reset()
Example #2
0
class ContainedText(AttrElem):
    """Base class for a text widget contained as a cell in a canvas.
    Both Captions and Cells are derived from this class.

    """
    def __init__(self, table, parentviewer, attrs):
        AttrElem.__init__(self, attrs)
        self._table = table
        self._container = table.container

##      from profile import Profile
##      from pstats import Stats
##      p = Profile()
##      # can't use runcall because that doesn't return the results
##      p.runctx('self._viewer = Viewer(master=table.container, context=parentviewer.context, scrolling=0, stylesheet=parentviewer.stylesheet, parent=parentviewer)',
##               globals(), locals())
##      Stats(p).strip_dirs().sort_stats('time').print_stats(5)

        self._viewer = Viewer(master=table.container,
                              context=parentviewer.context,
                              scrolling=0,
                              stylesheet=parentviewer.stylesheet,
                              parent=parentviewer)
        if not parentviewer.find_parentviewer():
            self._viewer.RULE_WIDTH_MAGIC = self._viewer.RULE_WIDTH_MAGIC - 6
        # for callback notification
        self._fw = self._viewer.frame
        self._tw = self._viewer.text
        self._tw.config(highlightthickness=0)
        self._width = 0
        self._embedheight = 0

    def new_formatter(self):
        formatter = AbstractFormatter(self._viewer)
        # set parskip to prevent blank line at top of cell if the content
        # starts with a <P> or header element.
        formatter.parskip = 1
        return formatter

    def freeze(self): self._viewer.freeze()
    def unfreeze(self): self._viewer.unfreeze()
    def close(self): self._viewer.close()

    def maxwidth(self):
        return self._maxwidth           # not useful until after finish()
    def minwidth(self):
        return self._minwidth           # likewise

    def height(self):
        return max(self._embedheight, _get_height(self._tw))

    def recalc(self):
        # recalculate width and height upon notification of completion
        # of all context's readers (usually image readers)
        min_nonaligned = self._minwidth
        maxwidth = self._maxwidth
        embedheight = self._embedheight
        # take into account all embedded windows
        for sub in self._viewer.subwindows:
            # the standard interface is used if the object has a
            # table_geometry() method
            if hasattr(sub, 'table_geometry'):
                submin, submax, height = sub.table_geometry()
                min_nonaligned = max(min_nonaligned, submin)
                maxwidth = max(maxwidth, submax)
                embedheight = max(embedheight, height)
            else:
                # this is the best we can do
##              print 'non-conformant embedded window:', sub.__class__
##              print 'using generic method, which may be incorrect'
                geom = sub.winfo_geometry()
                match = CELLGEOM_RE.search(geom)
                if match:
                    [w, h, x, y] = map(grailutil.conv_integer,
                                       match.group(1, 2, 3, 4))
                min_nonaligned = max(min_nonaligned, w) # x+w?
                maxwidth = max(maxwidth, w)             # x+w?
                embedheight = max(embedheight, h)       # y+h?
        self._embedheight = embedheight
        self._minwidth = min_nonaligned
        self._maxwidth = maxwidth
        return len(self._viewer.subwindows)

    def finish(self, padding=0):
        # TBD: if self.layout == AUTOLAYOUT???
        self._x = self._y = 0
        fw = self._fw
        tw = self._tw
        # Set the padding before grabbing the width, but it could be
        # denoted as a percentage of the viewer width
        if type(padding) == StringType:
            try:
                # divide by 200 since padding is a percentage and we
                # want to put equal amounts of pad on both sides of
                # the picture.
                padding = int(self._table.get_available_width() *
                              string.atoi(padding[:-1]) / 200)
            except ValueError:
                padding = 0
        tw['padx'] = padding
        # TBD: according to the W3C table spec, minwidth should really
        # be max(min_left + min_right, min_nonaligned).  Also note
        # that minwidth is recalculated by minwidth() call
        self._minwidth, self._maxwidth = _get_widths(self._tw)
        # first approximation of height.  this is the best we can do
        # without forcing an update_idletasks() fireworks display
        tw['height'] = _get_linecount(tw) + 1
        # initially place the cell in the canvas at position (0,0),
        # with the maximum width and closest approximation height.
        # situate() will be called later with the final layout
        # parameters.
        self._tag = self._container.create_window(
            0, 0,
            window=fw, anchor=NW,
            width=self._maxwidth,
            height=fw['height'])

    def situate(self, x=0, y=0, width=None, height=None):
        # canvas.move() deals in relative positioning, but we want
        # absolute coordinates
        xdelta = x - self._x
        ydelta = y - self._y
        self._x = x
        self._y = y
        self._container.move(self._tag, xdelta, ydelta)
        if width <> None and height <> None:
            self._container.itemconfigure(self._tag,
                                          width=width, height=height)
        elif width <> None:
            self._container.itemconfigure(self._tag, width=width)
        else:
            self._container.itemconfigure(self._tag, height=height)
Example #3
0
class Browser:
    """The Browser class provides the top-level GUI.

    It is a blatant rip-off of Mosaic's look and feel, with menus, a
    stop button, a URL display/entry area, and (last but not least) a
    viewer area.  But then, so are all other web browsers. :-)

    """
    def __init__(self,
                 master,
                 app=None,
                 width=None,
                 height=None,
                 geometry=None):
        self.master = master
        if not app:
            app = grailutil.get_grailapp()
        prefs = app.prefs
        self.app = app

        if not width: width = prefs.GetInt('browser', 'default-width')
        if not height: height = prefs.GetInt('browser', 'default-height')

        self.create_widgets(width=width, height=height, geometry=geometry)
        self.root.iconname('Grail')
        app.add_browser(self)

    def create_widgets(self, width, height, geometry):
        # I'd like to be able to set the widget name here, but I'm not
        # sure what the correct thing to do is.  Setting it to `grail'
        # is definitely *not* the right thing to do since this causes
        # all sorts of problems.
        self.root = tktools.make_toplevel(self.master, class_='Grail')
        self._window_title("Grail: New Browser")
        if geometry:
            self.root.geometry(geometry)
        self.root.protocol("WM_DELETE_WINDOW", self.on_delete)
        self.topframe = Frame(self.root)
        self.topframe.pack(fill=X)
        self.create_logo()
        self.create_menubar()
        self.create_urlbar()
        self.create_statusbar()
        self.viewer = Viewer(self.root,
                             browser=self,
                             width=width,
                             height=height)
        self.context = self.viewer.context
        if self.app.prefs.GetBoolean('browser', 'show-logo'):
            self.logo_init()

    def create_logo(self):
        self.logo = Button(self.root,
                           name="logo",
                           command=self.stop_command,
                           state=DISABLED)
        self.logo.pack(side=LEFT,
                       fill=BOTH,
                       padx=10,
                       pady=10,
                       in_=self.topframe)
        self.root.bind("<Alt-period>", self.stop_command)
        self.logo_animate = 0

    def create_menubar(self):
        # Create menu bar, menus, and menu entries

        # Create menu bar
        self.mbar = Menu(self.root, name="menubar", tearoff=0)
        self.root.config(menu=self.mbar)

        # Create the menus
        self.create_menu("file")
        self.create_menu("go")
        self.histmenu = self.gomenu  # backward compatibility for Ping
        self.create_menu("search")
        self.create_menu("bookmarks")
        self.create_menu("preferences")

        # List of user menus (reset on page load)
        self.user_menus = []

        if self.get_helpspec():
            self.create_menu("help")

    def create_menu(self, name):
        menu = Menu(self.mbar, name=name)
        self.mbar.add_cascade(label=str.capitalize(name), menu=menu)
        setattr(self, name + "menu", menu)
        getattr(self, "create_menu_" + name)(menu)

    def _menucmd(self, menu, label, accelerator, command):
        if not accelerator:
            menu.add_command(label=label, command=command)
            return
        underline = None
        if len(accelerator) == 1:
            # do a lot to determine the underline position
            underline = str.find(label, accelerator)
            if underline == -1:
                accelerator = str.lower(accelerator)
                underline = str.find(label, accelerator)
                if underline == -1:
                    underline = None
                accelerator = str.upper(accelerator)
        menu.add_command(label=label,
                         command=command,
                         underline=underline,
                         accelerator="Alt-" + accelerator)
        self.root.bind("<Alt-%s>" % accelerator, command)
        if len(accelerator) == 1:
            self.root.bind("<Alt-%s>" % str.lower(accelerator), command)

    def create_menu_file(self, menu):
        self._menucmd(menu, "New Window", "N", self.new_command)
        self._menucmd(menu, "Clone Current Window", "K", self.clone_command)
        self._menucmd(menu, "View Source", "V", self.view_source_command)
        self._menucmd(menu, 'Open Location...', "L", self.open_uri_command)
        self._menucmd(menu, 'Open File...', "O", self.open_file_command)
        self._menucmd(menu, 'Open Selection', "E", self.open_selection_command)
        menu.add_separator()
        self._menucmd(menu, "Save As...", "S", self.save_as_command)
        self._menucmd(menu, "Print...", "P", self.print_command)
        from ancillary import DocumentInfo
        self._menucmd(menu, "Document Info...", "D",
                      DocumentInfo.DocumentInfoCommand(self))
        menu.add_separator()
        self._menucmd(menu, "I/O Status Panel...", "I", self.iostatus_command)
        menu.add_separator()
        self._menucmd(menu, "Close", "W", self.close_command),
        if not self.app.embedded:
            self._menucmd(menu, "Quit", "Q", self.quit_command)

    def create_menu_go(self, menu):
        self._menucmd(menu, "Back", "Left", self.back_command)
        self._menucmd(menu, "Forward", "Right", self.forward_command)
        self._menucmd(menu, "Reload", "R", self.reload_command)
        menu.add_separator()
        self._menucmd(menu, 'History...', "H", self.show_history_command)
        self._menucmd(menu, "Home", None, self.home_command)

    def create_menu_search(self, menu):
        menu.grail_browser = self  # Applet compatibility
        from ancillary import SearchMenu
        SearchMenu.SearchMenu(menu, self.root, self)

    def create_menu_bookmarks(self, menu):
        menu.grail_browser = self  # Applet compatibility
        from ancillary import BookmarksGUI
        self.bookmarksmenu_menu = BookmarksGUI.BookmarksMenu(menu)

    def create_menu_preferences(self, menu):
        from ancillary.PrefsPanels import PrefsPanelsMenu
        PrefsPanelsMenu(menu, self)

    def create_menu_help(self, menu):
        lines = self.get_helpspec()
        i = 0
        n = len(lines) - 1
        while i < n:
            label = lines[i]
            i = i + 1
            if label == '-':
                menu.add_separator()
            else:
                url = lines[i]
                i = i + 1
                self._menucmd(menu, label, None, HelpMenuCommand(self, url))

    __helpspec = None

    def get_helpspec(self):
        if self.__helpspec is not None:
            return self.__helpspec
        raw = self.app.prefs.Get('browser', 'help-menu')
        lines = filter(None, map(str.strip, str.split(raw, '\n')))
        lines = map(str.split, lines)
        self.__helpspec = tuple(map(str.join, lines))
        return self.__helpspec

    def create_urlbar(self):
        f = Frame(self.topframe)
        f.pack(fill=X)
        l = Label(self.root, name="uriLabel")
        l.pack(side=LEFT, in_=f)
        self.entry = Entry(self.root, name="uriEntry")
        self.entry.pack(side=LEFT, fill=X, expand=1, in_=f)
        self.entry.bind('<Return>', self.load_from_entry)

    def create_statusbar(self):
        msg_frame = Frame(self.root, name="statusbar")
        msg_frame.pack(fill=X, side=BOTTOM, in_=self.topframe)
        msg_frame.propagate(OFF)
        fontspec = self.app.prefs.Get('presentation', 'message-font')
        fontspec = str.strip(fontspec) or None
        self.msg = Label(self.root, font=fontspec, name="status")
        self.msg.pack(fill=X, in_=msg_frame)

    # --- External interfaces ---

    def get_async_image(self, src):
        # XXX This is here for the 0.2 ImageLoopItem applet only
        return self.context.get_async_image(src)

    def allowstop(self):
        self.logo_start()

    def clearstop(self):
        self.logo_stop()

    def clear_reset(self):
        num = len(self.user_menus)
        if num:
            last = self.mbar.index(END)
            if num > 1:
                self.mbar.delete(last - num + 1, last)
            else:
                self.mbar.delete(last)
        for b in self.user_menus:
            b.destroy()
        self.user_menus[:] = []

    def set_url(self, url):
        self.set_entry(url)
        title, when = self.app.global_history.lookup_url(url)
        self.set_title(title or url)

    def set_title(self, title):
        self._window_title(TITLE_PREFIX + title)

    def message(self, string=""):
        self.msg['text'] = string

    def messagevariable(self, variable=None):
        if variable:
            self.msg['textvariable'] = variable
        else:
            self.msg['textvariable'] = ""
            self.msg['text'] = ""

    message_clear = messagevariable

    def error_dialog(self, exception, msg):
        if self.app:
            self.app.error_dialog(exception, msg, root=self.root)
        else:
            print("ERROR:", msg)

    def load(self, *args, **kw):
        """Interface for applets."""
        return self.context.load(args, kw)

    def valid(self):
        return self.app and self in self.app.browsers

    # --- Internals ---

    def _window_title(self, title):
        self.root.title(title)
        self.root.iconname(title)

    def set_entry(self, url):
        self.entry.delete('0', END)
        self.entry.insert(END, url)

    def close(self):
        self.context.stop()
        self.viewer.close()
        self.root.destroy()
        self.bookmarksmenu_menu.close()
        self.bookmarksmenu_menu = None
        if self.app:
            self.app.del_browser(self)
            self.app.maybe_quit()

    # --- Callbacks ---

    # WM_DELETE_WINDOW on toplevel

    def on_delete(self):
        self.close()

    # <Return> in URL entry field

    def load_from_entry(self, event):
        url = str.strip(self.entry.get())
        if url:
            self.context.load(grailutil.complete_url(url))
        else:
            self.root.bell()

    # Stop command

    def stop_command(self, event=None):
        if self.context.busy():
            self.context.stop()
            self.message("Stopped.")

    # File menu commands

    def new_command(self, event=None):
        b = Browser(self.master, self.app)
        return b

    def clone_command(self, event=None):
        b = Browser(self.master, self.app)
        b.context.clone_history_from(self.context)
        return b

    def open_uri_command(self, event=None):
        from ancillary import OpenURIDialog
        dialog = OpenURIDialog.OpenURIDialog(self.root)
        uri, new = dialog.go()
        if uri:
            if new:
                browser = Browser(self.master, self.app)
            else:
                browser = self
            browser.context.load(grailutil.complete_url(uri))

    def open_file_command(self, event=None):
        from tkinter import filedialog
        dialog = filedialog.LoadFileDialog(self.master)
        filename = dialog.go(key="load")
        if filename:
            import urllib
            self.context.load('file:' + urllib.pathname2url(filename))

    def open_selection_command(self, event=None):
        try:
            selection = self.root.selection_get()
        except TclError:
            self.root.bell()
            return
        uri = str.join(str.split(selection), '')
        self.context.load(grailutil.complete_url(uri))

    def view_source_command(self, event=None):
        self.context.view_source()

    def save_as_command(self, event=None):
        self.context.save_document()

    def print_command(self, event=None):
        self.context.print_document()

    def iostatus_command(self, event=None):
        self.app.open_io_status_panel()

    def close_command(self, event=None):
        # File/Close
        self.close()

    def quit_command(self, event=None):
        # File/Quit
        if self.app: self.app.quit()
        else: self.close()

    # History menu commands

    def home_command(self, event=None):
        home = self.app.prefs.Get('landmarks', 'home-page')
        if not home:
            home = self.app.prefs.Get('landmarks', 'default-home-page')
        self.context.load(home)

    def reload_command(self, event=None):
        self.context.reload_page()

    def forward_command(self, event=None):
        self.context.go_forward()

    def back_command(self, event=None):
        self.context.go_back()

    def show_history_command(self, event=None):
        self.context.show_history_dialog()

    # --- Animated logo ---

    def logo_init(self):
        """Initialize animated logo and display the first image.

        This doesn't start the animation sequence -- use logo_start()
        for that.

        """
        self.logo_index = 0  # Currently displayed image
        self.logo_last = -1  # Last image; -1 if unknown
        self.logo_id = None  # Tk id of timer callback
        self.logo_animate = 1  # True if animating
        self.logo_next()

    def logo_next(self):
        """Display the next image in the logo animation sequence.

        If the first image can't be found, disable animation.

        """
        self.logo_index = self.logo_index + 1
        if self.logo_last > 0 and self.logo_index > self.logo_last:
            self.logo_index = 1
        entytyname = "grail.logo.%d" % self.logo_index
        image = self.app.load_dingbat(entytyname)
        if not image:
            if self.logo_index == 1:
                self.logo_animate = 0
                return
            self.logo_index = 1
            entytyname = "grail.logo.%d" % self.logo_index
            image = self.app.load_dingbat(entytyname)
            if not image:
                self.logo_animate = 0
                return
        self.logo.config(image=image, state=NORMAL)

    def logo_start(self):
        """Start logo animation.

        If we can't/don't animate the logo, enable the stop button instead.

        """
        self.logo.config(state=NORMAL)
        if not self.logo_animate:
            return
        if not self.logo_id:
            self.logo_index = 0
            self.logo_next()
            self.logo_id = self.root.after(200, self.logo_update)

    def logo_stop(self):
        """Stop logo animation.

        If we can't/don't animate the logo, disable the stop button instead.

        """
        if not self.logo_animate:
            self.logo.config(state=DISABLED)
            return
        if self.logo_id:
            self.root.after_cancel(self.logo_id)
            self.logo_id = None
        self.logo_index = 0
        self.logo_next()

    def logo_update(self):
        """Keep logo animation going."""
        self.logo_id = None
        if self.logo_animate:
            self.logo_next()
            if self.logo_animate:
                self.logo_id = self.root.after(200, self.logo_update)

    # --- API for searching ---

    def search_for_pattern(self, pattern, re_flag, case_flag, backwards_flag):
        textwidget = self.viewer.text
        try:
            index = textwidget.index(SEL_FIRST)
            index = '%s + %s chars' % (str(index), backwards_flag and '0'
                                       or '1')
        except TclError:
            index = '1.0'
        length = IntVar(textwidget)
        hitlength = None
        hit = textwidget.search(pattern,
                                index,
                                count=length,
                                nocase=not case_flag,
                                rep=re_flag,
                                backwards=backwards_flag)
        if hit:
            try:
                textwidget.tag_remove(SEL, SEL_FIRST, SEL_LAST)
            except TclError:
                pass
            hitlength = length.get()
            textwidget.tag_add(SEL, hit, "%s + %s chars" % (hit, hitlength))
            textwidget.yview_pickplace(SEL_FIRST)
        return hit
Example #4
0
class ContainedText(AttrElem):
    """Base class for a text widget contained as a cell in a canvas.
    Both Captions and Cells are derived from this class.

    """
    def __init__(self, table, parentviewer, attrs):
        AttrElem.__init__(self, attrs)
        self._table = table
        self._container = table.container

##      from profile import Profile
##      from pstats import Stats
##      p = Profile()
##      # can't use runcall because that doesn't return the results
##      p.runctx('self._viewer = Viewer(master=table.container, context=parentviewer.context, scrolling=0, stylesheet=parentviewer.stylesheet, parent=parentviewer)',
##               globals(), locals())
##      Stats(p).strip_dirs().sort_stats('time').print_stats(5)

        self._viewer = Viewer(master=table.container,
                              context=parentviewer.context,
                              scrolling=0,
                              stylesheet=parentviewer.stylesheet,
                              parent=parentviewer)
        if not parentviewer.find_parentviewer():
            self._viewer.RULE_WIDTH_MAGIC = self._viewer.RULE_WIDTH_MAGIC - 6
        # for callback notification
        self._fw = self._viewer.frame
        self._tw = self._viewer.text
        self._tw.config(highlightthickness=0)
        self._width = 0
        self._embedheight = 0

    def new_formatter(self):
        formatter = AbstractFormatter(self._viewer)
        # set parskip to prevent blank line at top of cell if the content
        # starts with a <P> or header element.
        formatter.parskip = 1
        return formatter

    def freeze(self): self._viewer.freeze()
    def unfreeze(self): self._viewer.unfreeze()
    def close(self): self._viewer.close()

    def maxwidth(self):
        return self._maxwidth           # not useful until after finish()
    def minwidth(self):
        return self._minwidth           # likewise

    def height(self):
        return max(self._embedheight, _get_height(self._tw))

    def recalc(self):
        # recalculate width and height upon notification of completion
        # of all context's readers (usually image readers)
        min_nonaligned = self._minwidth
        maxwidth = self._maxwidth
        embedheight = self._embedheight
        # take into account all embedded windows
        for sub in self._viewer.subwindows:
            # the standard interface is used if the object has a
            # table_geometry() method
            if hasattr(sub, 'table_geometry'):
                submin, submax, height = sub.table_geometry()
                min_nonaligned = max(min_nonaligned, submin)
                maxwidth = max(maxwidth, submax)
                embedheight = max(embedheight, height)
            else:
                # this is the best we can do
##              print 'non-conformant embedded window:', sub.__class__
##              print 'using generic method, which may be incorrect'
                geom = sub.winfo_geometry()
                if CELLGEOM_RE.search(geom) >= 0:
                    [w, h, x, y] = map(grailutil.conv_integer,
                                       CELLGEOM_RE.group(1, 2, 3, 4))
                min_nonaligned = max(min_nonaligned, w) # x+w?
                maxwidth = max(maxwidth, w)             # x+w?
                embedheight = max(embedheight, h)       # y+h?
        self._embedheight = embedheight
        self._minwidth = min_nonaligned
        self._maxwidth = maxwidth
        return len(self._viewer.subwindows)

    def finish(self, padding=0):
        # TBD: if self.layout == AUTOLAYOUT???
        self._x = self._y = 0
        fw = self._fw
        tw = self._tw
        # Set the padding before grabbing the width, but it could be
        # denoted as a percentage of the viewer width
        if isinstance(padding,str):
            try:
                # divide by 200 since padding is a percentage and we
                # want to put equal amounts of pad on both sides of
                # the picture.
                padding = int(self._table.get_available_width() *
                              int(padding[:-1]) / 200)
            except ValueError:
                padding = 0
        tw['padx'] = padding
        # TBD: according to the W3C table spec, minwidth should really
        # be max(min_left + min_right, min_nonaligned).  Also note
        # that minwidth is recalculated by minwidth() call
        self._minwidth, self._maxwidth = _get_widths(self._tw)
        # first approximation of height.  this is the best we can do
        # without forcing an update_idletasks() fireworks display
        tw['height'] = _get_linecount(tw) + 1
        # initially place the cell in the canvas at position (0,0),
        # with the maximum width and closest approximation height.
        # situate() will be called later with the final layout
        # parameters.
        self._tag = self._container.create_window(
            0, 0,
            window=fw, anchor=NW,
            width=self._maxwidth,
            height=fw['height'])

    def situate(self, x=0, y=0, width=None, height=None):
        # canvas.move() deals in relative positioning, but we want
        # absolute coordinates
        xdelta = x - self._x
        ydelta = y - self._y
        self._x = x
        self._y = y
        self._container.move(self._tag, xdelta, ydelta)
        if width != None and height != None:
            self._container.itemconfigure(self._tag,
                                          width=width, height=height)
        elif width != None:
            self._container.itemconfigure(self._tag, width=width)
        else:
            self._container.itemconfigure(self._tag, height=height)
Example #5
0
class Browser:
    """The Browser class provides the top-level GUI.

    It is a blatant rip-off of Mosaic's look and feel, with menus, a
    stop button, a URL display/entry area, and (last but not least) a
    viewer area.  But then, so are all other web browsers. :-)

    """
    def __init__(self, master, app=None,
                 width=None, height=None,
                 geometry=None):
        self.master = master
        if not app:
            app = grailutil.get_grailapp()
        prefs = app.prefs
        self.app = app

        if not width: width = prefs.GetInt('browser', 'default-width')
        if not height: height = prefs.GetInt('browser', 'default-height')

        self.create_widgets(width=width, height=height, geometry=geometry)
        self.root.iconname('Grail')
        app.add_browser(self)

    def create_widgets(self, width, height, geometry):
        # I'd like to be able to set the widget name here, but I'm not
        # sure what the correct thing to do is.  Setting it to `grail'
        # is definitely *not* the right thing to do since this causes
        # all sorts of problems.
        self.root = tktools.make_toplevel(self.master, class_='Grail')
        self._window_title("Grail: New Browser")
        if geometry:
            self.root.geometry(geometry)
        self.root.protocol("WM_DELETE_WINDOW", self.on_delete)
        self.topframe = Frame(self.root)
        self.topframe.pack(fill=X)
        self.create_logo()
        self.create_menubar()
        self.create_urlbar()
        self.create_statusbar()
        self.viewer = Viewer(self.root, browser=self,
                             width=width, height=height)
        self.context = self.viewer.context
        if self.app.prefs.GetBoolean('browser', 'show-logo'):
            self.logo_init()

    def create_logo(self):
        self.logo = Button(self.root, name="logo",
                           command=self.stop_command,
                           state=DISABLED)
        self.logo.pack(side=LEFT, fill=BOTH, padx=10, pady=10,
                       in_=self.topframe)
        self.root.bind("<Alt-period>", self.stop_command)
        self.logo_animate = 0

    def create_menubar(self):
        # Create menu bar, menus, and menu entries

        # Create menu bar
        self.mbar = Menu(self.root, name="menubar", tearoff=0)
        self.root.config(menu=self.mbar)

        # Create the menus
        self.create_menu("file")
        self.create_menu("go")
        self.histmenu = self.gomenu     # backward compatibility for Ping
        self.create_menu("search")
        self.create_menu("bookmarks")
        self.create_menu("preferences")

        # List of user menus (reset on page load)
        self.user_menus = []

        if self.get_helpspec():
            self.create_menu("help")

    def create_menu(self, name):
        menu = Menu(self.mbar, name=name)
        self.mbar.add_cascade(label=string.capitalize(name), menu=menu)
        setattr(self, name + "menu", menu)
        getattr(self, "create_menu_" + name)(menu)

    def _menucmd(self, menu, label, accelerator, command):
        if not accelerator:
            menu.add_command(label=label, command=command)
            return
        underline = None
        if len(accelerator) == 1:
            # do a lot to determine the underline position
            underline = string.find(label, accelerator)
            if underline == -1:
                accelerator = string.lower(accelerator)
                underline = string.find(label, accelerator)
                if underline == -1:
                    underline = None
                accelerator = string.upper(accelerator)
        menu.add_command(label=label, command=command, underline=underline,
                         accelerator="Alt-" + accelerator)
        self.root.bind("<Alt-%s>" % accelerator, command)
        if len(accelerator) == 1:
            self.root.bind("<Alt-%s>" % string.lower(accelerator), command)

    def create_menu_file(self, menu):
        self._menucmd(menu, "New Window", "N", self.new_command)
        self._menucmd(menu, "Clone Current Window", "K", self.clone_command)
        self._menucmd(menu, "View Source", "V", self.view_source_command)
        self._menucmd(menu, 'Open Location...', "L", self.open_uri_command)
        self._menucmd(menu, 'Open File...', "O", self.open_file_command)
        self._menucmd(menu, 'Open Selection', "E",
                      self.open_selection_command)
        menu.add_separator()
        self._menucmd(menu, "Save As...", "S", self.save_as_command)
        self._menucmd(menu, "Print...", "P", self.print_command)
        import DocumentInfo
        self._menucmd(menu, "Document Info...", "D",
                      DocumentInfo.DocumentInfoCommand(self))
        menu.add_separator()
        self._menucmd(menu, "I/O Status Panel...", "I", self.iostatus_command)
        menu.add_separator()
        self._menucmd(menu, "Close", "W", self.close_command),
        if not self.app.embedded:
            self._menucmd(menu, "Quit", "Q", self.quit_command)

    def create_menu_go(self, menu):
        self._menucmd(menu, "Back", "Left", self.back_command)
        self._menucmd(menu, "Forward", "Right", self.forward_command)
        self._menucmd(menu, "Reload", "R", self.reload_command)
        menu.add_separator()
        self._menucmd(menu, 'History...', "H", self.show_history_command)
        self._menucmd(menu, "Home", None, self.home_command)

    def create_menu_search(self, menu):
        menu.grail_browser = self       # Applet compatibility
        import SearchMenu
        SearchMenu.SearchMenu(menu, self.root, self)

    def create_menu_bookmarks(self, menu):
        menu.grail_browser = self # Applet compatibility
        import BookmarksGUI
        self.bookmarksmenu_menu = BookmarksGUI.BookmarksMenu(menu)

    def create_menu_preferences(self, menu):
        from PrefsPanels import PrefsPanelsMenu
        PrefsPanelsMenu(menu, self)

    def create_menu_help(self, menu):
        lines = self.get_helpspec()
        i = 0
        n = len(lines) - 1
        while i < n:
            label = lines[i]
            i = i+1
            if label == '-':
                menu.add_separator()
            else:
                url = lines[i]
                i = i+1
                self._menucmd(menu, label, None, HelpMenuCommand(self, url))

    __helpspec = None
    def get_helpspec(self):
        if self.__helpspec is not None:
            return self.__helpspec
        raw = self.app.prefs.Get('browser', 'help-menu')
        lines = filter(None, map(string.strip, string.split(raw, '\n')))
        lines = map(string.split, lines)
        self.__helpspec = tuple(map(string.join, lines))
        return self.__helpspec

    def create_urlbar(self):
        f = Frame(self.topframe)
        f.pack(fill=X)
        l = Label(self.root, name="uriLabel")
        l.pack(side=LEFT, in_=f)
        self.entry = Entry(self.root, name="uriEntry")
        self.entry.pack(side=LEFT, fill=X, expand=1, in_=f)
        self.entry.bind('<Return>', self.load_from_entry)

    def create_statusbar(self):
        msg_frame = Frame(self.root, name="statusbar")
        msg_frame.pack(fill=X, side=BOTTOM, in_=self.topframe)
        msg_frame.propagate(OFF)
        fontspec = self.app.prefs.Get('presentation', 'message-font')
        fontspec = string.strip(fontspec) or None
        self.msg = Label(self.root, font=fontspec, name="status")
        self.msg.pack(fill=X, in_=msg_frame)

    # --- External interfaces ---

    def get_async_image(self, src):
        # XXX This is here for the 0.2 ImageLoopItem applet only
        return self.context.get_async_image(src)

    def allowstop(self):
        self.logo_start()

    def clearstop(self):
        self.logo_stop()

    def clear_reset(self):
        num = len(self.user_menus)
        if num:
            last = self.mbar.index(END)
            if num > 1:
                self.mbar.delete(last-num+1, last)
            else:
                self.mbar.delete(last)
        for b in self.user_menus:
            b.destroy()
        self.user_menus[:] = []

    def set_url(self, url):
        self.set_entry(url)
        title, when = self.app.global_history.lookup_url(url)
        self.set_title(title or url)

    def set_title(self, title):
        self._window_title(TITLE_PREFIX + title)

    def message(self, string = ""):
        self.msg['text'] = string

    def messagevariable(self, variable=None):
        if variable:
            self.msg['textvariable'] = variable
        else:
            self.msg['textvariable'] = ""
            self.msg['text'] = ""
    message_clear = messagevariable

    def error_dialog(self, exception, msg):
        if self.app:
            self.app.error_dialog(exception, msg, root=self.root)
        else:
            print "ERROR:", msg

    def load(self, *args, **kw):
        """Interface for applets."""
        return apply(self.context.load, args, kw)

    def valid(self):
        return self.app and self in self.app.browsers

    # --- Internals ---

    def _window_title(self, title):
        self.root.title(title)
        self.root.iconname(title)

    def set_entry(self, url):
        self.entry.delete('0', END)
        self.entry.insert(END, url)

    def close(self):
        self.context.stop()
        self.viewer.close()
        self.root.destroy()
        self.bookmarksmenu_menu.close()
        self.bookmarksmenu_menu = None
        if self.app:
            self.app.del_browser(self)
            self.app.maybe_quit()

    # --- Callbacks ---

    # WM_DELETE_WINDOW on toplevel

    def on_delete(self):
        self.close()

    # <Return> in URL entry field

    def load_from_entry(self, event):
        url = string.strip(self.entry.get())
        if url:
            self.context.load(grailutil.complete_url(url))
        else:
            self.root.bell()

    # Stop command

    def stop_command(self, event=None):
        if self.context.busy():
            self.context.stop()
            self.message("Stopped.")

    # File menu commands

    def new_command(self, event=None):
        b = Browser(self.master, self.app)
        return b

    def clone_command(self, event=None):
        b = Browser(self.master, self.app)
        b.context.clone_history_from(self.context)
        return b

    def open_uri_command(self, event=None):
        import OpenURIDialog
        dialog = OpenURIDialog.OpenURIDialog(self.root)
        uri, new = dialog.go()
        if uri:
            if new:
                browser = Browser(self.master, self.app)
            else:
                browser = self
            browser.context.load(grailutil.complete_url(uri))

    def open_file_command(self, event=None):
        import FileDialog
        dialog = FileDialog.LoadFileDialog(self.master)
        filename = dialog.go(key="load")
        if filename:
            import urllib
            self.context.load('file:' + urllib.pathname2url(filename))

    def open_selection_command(self, event=None):
        try:
            selection = self.root.selection_get()
        except TclError:
            self.root.bell()
            return
        uri = string.joinfields(string.split(selection), '')
        self.context.load(grailutil.complete_url(uri))

    def view_source_command(self, event=None):
        self.context.view_source()

    def save_as_command(self, event=None):
        self.context.save_document()

    def print_command(self, event=None):
        self.context.print_document()

    def iostatus_command(self, event=None):
        self.app.open_io_status_panel()

    def close_command(self, event=None):
        # File/Close
        self.close()

    def quit_command(self, event=None):
        # File/Quit
        if self.app: self.app.quit()
        else: self.close()

    # History menu commands

    def home_command(self, event=None):
        home = self.app.prefs.Get('landmarks', 'home-page')
        if not home:
            home = self.app.prefs.Get('landmarks', 'default-home-page')
        self.context.load(home)

    def reload_command(self, event=None):
        self.context.reload_page()

    def forward_command(self, event=None):
        self.context.go_forward()

    def back_command(self, event=None):
        self.context.go_back()

    def show_history_command(self, event=None):
        self.context.show_history_dialog()

    # --- Animated logo ---

    def logo_init(self):
        """Initialize animated logo and display the first image.

        This doesn't start the animation sequence -- use logo_start()
        for that.

        """
        self.logo_index = 0             # Currently displayed image
        self.logo_last = -1             # Last image; -1 if unknown
        self.logo_id = None             # Tk id of timer callback
        self.logo_animate = 1           # True if animating
        self.logo_next()

    def logo_next(self):
        """Display the next image in the logo animation sequence.

        If the first image can't be found, disable animation.

        """
        self.logo_index = self.logo_index + 1
        if self.logo_last > 0 and self.logo_index > self.logo_last:
            self.logo_index = 1
        entytyname = "grail.logo.%d" % self.logo_index
        image = self.app.load_dingbat(entytyname)
        if not image:
            if self.logo_index == 1:
                self.logo_animate = 0
                return
            self.logo_index = 1
            entytyname = "grail.logo.%d" % self.logo_index
            image = self.app.load_dingbat(entytyname)
            if not image:
                self.logo_animate = 0
                return
        self.logo.config(image=image, state=NORMAL)

    def logo_start(self):
        """Start logo animation.

        If we can't/don't animate the logo, enable the stop button instead.

        """
        self.logo.config(state=NORMAL)
        if not self.logo_animate:
            return
        if not self.logo_id:
            self.logo_index = 0
            self.logo_next()
            self.logo_id = self.root.after(200, self.logo_update)

    def logo_stop(self):
        """Stop logo animation.

        If we can't/don't animate the logo, disable the stop button instead.

        """
        if not self.logo_animate:
            self.logo.config(state=DISABLED)
            return
        if self.logo_id:
            self.root.after_cancel(self.logo_id)
            self.logo_id = None
        self.logo_index = 0
        self.logo_next()

    def logo_update(self):
        """Keep logo animation going."""
        self.logo_id = None
        if self.logo_animate:
            self.logo_next()
            if self.logo_animate:
                self.logo_id = self.root.after(200, self.logo_update)

    # --- API for searching ---

    def search_for_pattern(self, pattern,
                           regex_flag, case_flag, backwards_flag):
        textwidget = self.viewer.text
        try:
            index = textwidget.index(SEL_FIRST)
            index = '%s + %s chars' % (str(index),
                                       backwards_flag and '0' or '1')
        except TclError:
            index = '1.0'
        length = IntVar(textwidget)
        hitlength = None
        hit = textwidget.search(pattern, index, count=length,
                                nocase=not case_flag,
                                regexp=regex_flag,
                                backwards=backwards_flag)
        if hit:
            try:
                textwidget.tag_remove(SEL, SEL_FIRST, SEL_LAST)
            except TclError:
                pass
            hitlength = length.get()
            textwidget.tag_add(SEL, hit, "%s + %s chars" % (hit, hitlength))
            textwidget.yview_pickplace(SEL_FIRST)
        return hit