# Copyright (C) 2009 Canonical Ltd
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA

import os, sys
import subprocess
from PyQt4 import QtCore, QtGui

import bzrlib
from bzrlib import (
    config,
    errors,
    lazy_import,
    osutils,
    trace,
    urlutils,
    )
from bzrlib.cmdline import split
from bzrlib.plugins import explorer
from bzrlib.plugins.explorer.lib import (
    accessories_dialog,
    app_runner,
    app_suite as mod_app_suite,
    desktop_env,
    helpers,
    history_manager,
    hosted_url_resolver,
    kinds,
    location as mod_location,
    location_set_browser,
    location_viewer,
    script_engine,
    switch_dialog,
    tool_dialogs,
    ui_explorer,
    welcome,
    workspace_models,
    wt_browser,
    sidebar_toolbox,
    )
from bzrlib.plugins.explorer.lib.custom_dialogs import custom_dialog_registry
from bzrlib.plugins.explorer.lib.builders import (
    toolset_builders,
    )
from bzrlib.plugins.explorer.lib.extensions import (
    accessories,
    bookmarks,
    tools,
    )
from bzrlib.plugins.explorer.lib.explorer_preferences import (
    DEFAULT_PREFERENCES,
    ExplorerPreferencesDialog,
    load_preferences,
    )
from bzrlib.plugins.explorer.lib.i18n import gettext, N_

try:
    from bzrlib.plugins import qbzr
except ImportError, e:
    raise errors.BzrError('Bazaar Explorer requires QBzr available from '
        'https://launchpad.net/qbzr,\n see installation instructions at '
        'http://doc.bazaar.canonical.com/plugins/en/plugin-installation.html')
if qbzr.version_info < (0, 17):
    trace.warning('Bazaar Explorer works better with QBzr 0.17 or later')
import bzrlib.plugins.qbzr.lib.util
from bzrlib.plugins.qbzr.lib.util import (
    open_browser,
    QBzrWindow,
    )

lazy_import.lazy_import(globals(), """
from bzrlib.plugins.explorer.lib.compatibility import send2trash
""")


_LP_PROJECT = "bzr-explorer"
_HELP_URL = "http://doc.bazaar.canonical.com/explorer/en/documentation.html"
_CODE_NAME = "James Cook"
_CODE_NAME_URL = "http://en.wikipedia.org/wiki/James_Cook"


_MSG_TYPE_TO_ICON = {
    "note":              QtGui.QMessageBox.Information,
    "warning":           QtGui.QMessageBox.Warning,
    "error":             QtGui.QMessageBox.Critical,
    }


# Actions that should automatically refresh the page on successful completion
_AUTO_REFRESH_CMDS = [
    'add', 'commit',
    'pull', 'merge', 'update', 'push', 'send',
    'conflicts', 'switch', 'bind', 'unbind', 'revert', 'uncommit',
    'shelve', 'unshelve',
    'ignore',
    # This one really ought to be handled by a custom dialog so that the new
    # location can be opened. Refreshing the current page (often the
    # repository view) is better than nothing though.
    'branch',
    ]


def split_command_options(command):
    # command could have spaces in its path (e.g. on Windows)
    # so split it on spaces is wrong thing
    if sys.platform == 'win32':
        command = command.replace('\\', '/')
    elif sys.platform == 'darwin':
        # Map xx.app -> "open -a /Applications/xx.app"
        if command.endswith(".app") and not command.startswith("open"):
            command = 'open -a "%s"' % command
    return split(command)


class QExplorerMainWindow(QBzrWindow):

    def __init__(self, parent=None, location_list=None, hat=None,
            desktop=None, dry_run=False, _script=None):
        """Create a main window.

        :param location: path/URL to location to open or None.
          preferences are used to select it.
        :param hat: name of hat to use. If None, the
          preferences are used to select one. If the hat doesn't
          yet exist, it will be created.
        :param desktop: desktop environment to use: gnome, kde, etc.
          If None, an intelligent guess is made.
        :param dry_run: if True, just show commands that will be
          run without running them.
        :param _script: ID of an internal script to run. Do not use.
        """
        # Init the UI
        QBzrWindow.__init__(self, [], parent)
        self.ui = ui_explorer.Ui_MainWindow()
        self.ui.setupUi(self)

        # setup refresher
        self._refresher = _Refresher(self._do_refresh)

        # Setup some useful attributes
        self.app_name = gettext("Bazaar Explorer")
        self.desktop_env = desktop_env.DesktopEnvironment(desktop)
        self.original_dir = os.path.abspath(os.getcwdu())
        self.current_dir = self.original_dir
        self.dry_run = dry_run
        self._app_runner = app_runner.ApplicationRunner(self.show_error)
        self._custom_modeless_dialogs = {}
        # permitted values are None, local, remote
        self.location_category = None
        self.location_viewer = None
        self._kind = None
        self._terminal = self.desktop_env.default_terminal()
        self._user_config = None
        self._refresh_user_configuration()
        # keys are root, selected, filename, dirname, basename
        # (plus those from the base context)
        self.location_context = {}

        # Get the preferences to use
        self._preferences = load_preferences()
        self._preferences_dlg = None

        # Load the accessories and dependent objects
        clothes_to_wear = self._preferences["clothes-to-wear"]
        bags_to_blacklist = self._preferences["bags-to-blacklist"]
        self._accessories = accessories.Accessories(clothes_to_wear,
            bags_to_blacklist, self.refresh_menus)
        kinds.register_resolver("logos", self._accessories.logo_path)
        self._accessories_dlg = None

        # Init the toolbar
        toolbar_style = self._preferences.get('toolbar-style')
        menu_actions = self._init_toolbar_menus()
        self._toolbar_builder = toolset_builders.ToolbarBuilder(
            self._accessories, self._do_open_tool, self.action_provider,
            menu_actions, toolbar_style)
        self._toolbar_dock = QtGui.QToolBar(gettext("Toolbar"))
        self.addToolBar(self._toolbar_dock)
        self._toolbar_name = None
        self._toolbar_stack = None
        toolbar_contents = self._preferences.get("toolbar-contents")
        self._kind_specific_toolbars_enabled = \
            toolbar_contents == "kind-specific"
        #self._use_toolbar(self._preferences.get("toolbar-contents"))
        self.setUnifiedTitleAndToolBarOnMac(True)

        # Init the toolbox and working tree browser.
        # Maybe the ordering of these ought to be a preference?
        toolbox_style = self._preferences.get('toolbox-style')
        self._toolbox = sidebar_toolbox.Toolbox(
            self._accessories, self._do_open_tool, self.action_provider,
            menu_actions, gettext("Toolbox"), toolbox_style)
        self.addDockWidget(QtCore.Qt.RightDockWidgetArea, self._toolbox)
        self._wt_browser = wt_browser.WorkingTreeBrowser(
            self._do_view_action, self.ui.action_Browse,
            gettext("Working Tree"))
        self.addDockWidget(QtCore.Qt.RightDockWidgetArea, self._wt_browser)
        # Set up windows array used by QBzr TreeWidget when working tree
        # browser is undocked
        self._wt_browser.windows = self.windows

        # Load the bookmarks and tools menus
        self.init_hats()

        # Apply the application suite preference, remembering what it implies
        self._apply_app_suite(self._preferences.get('app-suite'))

        # Initialise the objects needed for hosted URL resolution
        self._hosted_url_resolver = hosted_url_resolver.HostedUrlResolver()

        # File edit tracking.
        self.init_edit_tracking()

        # Finish initialising the UI
        self.restoreSize("explorer", (800, 600))
        # [bialix 2011/05/09] this is just dirty hack
        # but I don't have any better workaround right now:
        # qbzr config should not be cached inside explorer
        # strictly to say it should not be cached at all
        if getattr(bzrlib.plugins.qbzr.lib.util, '_qbzr_config', None) is not None:
            bzrlib.plugins.qbzr.lib.util._qbzr_config = None
        self.init_status_bar()
        self.init_actions()
        self.init_history()
        self.init_location_viewer()
        self.init_platform_specifics()
        self.apply_preferences()
        if location_list is not None:
            for location in location_list:
                self.open_saved_location(location)
        else:
            self.do_welcome()
        if _script:
            engine = script_engine.ScriptEngine()
            engine.run(_script, self)

    def _refresh_user_configuration(self):
        """Call this after editing bazaar.conf."""
        if self._user_config is None:
            # first time load
            try:
                self._user_config = config.GlobalStack()
            except AttributeError: # bzr < 2.6
                self._user_config = config.GlobalConfig()
        else:
            # reload the data
            helpers.reload_config_obj(self._user_config,
                config.config_filename())
        self._editor = self.get_default_editor()
        self._base_context = {
            "EDITOR": self._editor,
            "BZR_LOG": trace._get_bzr_log_filename(),
            }

    def get_default_editor(self):
        try:
            result = self._user_config.get('editor')
        except AttributeError: # bzr < 2.6
            result = self._user_config.get_editor()
        if result is not None:
            result = result.strip()
        return result or self.desktop_env.default_editor()

    def init_platform_specifics(self):
        action_shortcuts = dict([
            (self.ui.action_Initialize, [QtGui.QKeySequence.New]),
            (self.ui.action_Open, [QtGui.QKeySequence.Open]),
            (self.ui.action_Close, [QtGui.QKeySequence.Close]),
            (self.ui.action_Copy, [QtGui.QKeySequence.Copy]),
            (self.ui.action_Help_Contents, [QtGui.QKeySequence.HelpContents]),
            # This aren't exposed on the menu yet
            #(self.ui.action_Cut, [QtGui.QKeySequence.Cut]),
            #(self.ui.action_Paste, [QtGui.QKeySequence.Paste]),
            #(self.ui.action_Find, [QtGui.QKeySequence.Find]),
            ])
        if sys.platform == 'win32':
            # on windows it's nice to have Ctrl+W to close tab
            # as in Firefox and some other applications ;-)
            action_shortcuts[self.ui.action_Close].append(
                QtGui.QKeySequence('Ctrl+W'))
            # on windows quit application is Alt+F4 not Ctrl+Q
            action_shortcuts[self.ui.action_Quit] = [
                QtGui.QKeySequence('Alt+F4')]
        for action, shortcuts in action_shortcuts.iteritems():
            action.setShortcuts(shortcuts)

        # Put the Preferences action in the right place
        try:
            menu, title = self.desktop_env.preferences_info()
            menu_obj = self.ui.__dict__["menu_%s" % (menu,)]
        except KeyError:
            # TODO: mutter something
            pass
        else:
            menu_obj.addSeparator()
            prefs_action = self.ui.action_Preferences
            prefs_action.setText(self._text_with_app_name(title))
            menu_obj.addAction(prefs_action)

        # Tweak action labels as required
        custom_labels = self.desktop_env.action_labels()
        if custom_labels:
            for action, label in custom_labels.items():
                try:
                    action_obj = self.ui.__dict__["action_%s" % (action,)]
                except KeyError:
                    # TODO: mutter something
                    continue
                action_obj.setText(self._text_with_app_name(label))

    def _text_with_app_name(self, text):
        """If text contains %s, sustitute the application name into it.
        @return: translated result.
        """
        if text.find("%s") != -1:
            text = text % (self.app_name,)
        return gettext(text)

    def _init_toolbar_menus(self):
        # Add menus for toolbar actions with them
        self.ui.action_Start.setMenu(self.ui.menu_Start)
        self.ui.action_Collaborate.setMenu(self.ui.menu_Collaborate)
        self.ui.action_Explore.setMenu(self.ui.menu_Explore)
        self.ui.action_Work.setMenu(self.ui.menu_Work)
        return ['Start', 'Collaborate', 'Explore', 'Work']

    def init_status_bar(self):
        # Hosted indicator
        self._hosted_indicator = self._build_hosted_indicator()
        self.ui.statusbar.addPermanentWidget(self._hosted_indicator)
        # Progress monitoring
        self._cursors = []
        self._busy_indicator = self._build_busy_indicator()
        self.ui.statusbar.addPermanentWidget(self._busy_indicator)

    def _build_hosted_indicator(self):
        icon = kinds.icon_for_kind(kinds.OPEN_HOSTED_ACTION)
        action = QtGui.QAction(icon, gettext("Open on hosting service"), self)
        self.connect(action, QtCore.SIGNAL("triggered(bool)"),
            self.open_hosted_web_url)
        indicator = QtGui.QToolButton()
        indicator.setDefaultAction(action)
        indicator.setAutoRaise(True)
        indicator.setVisible(False)
        if sys.platform == 'darwin':
            size = QtCore.QSize(8,8)
            indicator.setIconSize(size)
        return indicator

    def _build_busy_indicator(self):
        indicator = QtGui.QProgressBar()
        # Make it really small
        indicator.setMaximumHeight(10)
        # Note: Setting both the min & max to 0 means 'busy for unknown time'.
        indicator.setRange(0, 0)
        indicator.setVisible(False)
        return indicator

    def init_actions(self):
        self._init_action_mappings()
        self.ui.action_Diagnostic_Mode.setChecked(self.dry_run)

        # Define the action -> slot mapping
        trigger_links = [
            # File ...
            (self.ui.action_Open, self.do_open),
            (self.ui.action_Open_Location, self.do_open_location),
            (self.ui.action_Accessories, self.do_accessories),
            (self.ui.action_Close, self.do_close),
            (self.ui.action_Quit, self.do_quit),
            # Edit ...
            (self.ui.action_Preferences, self.do_preferences),
            # View ...
            (self.ui.action_Refresh, self.do_full_refresh),
            # Bazaar ...
            (self.ui.action_Initialize, self.do_bzr_init),
            (self.ui.action_Branch, self.do_bzr_branch),
            (self.ui.action_Checkout, self.do_bzr_checkout),
            (self.ui.action_Import, self.do_bzr_import),
            (self.ui.action_Add, self.do_bzr_add),
            (self.ui.action_Diff, self.do_bzr_diff),
            (self.ui.action_Commit, self.do_bzr_commit),
            (self.ui.action_Pull, self.do_bzr_pull),
            (self.ui.action_Merge, self.do_bzr_merge),
            (self.ui.action_Update, self.do_bzr_update),
            (self.ui.action_Push, self.do_bzr_push),
            (self.ui.action_Send, self.do_bzr_send),
            (self.ui.action_Export, self.do_bzr_export),
            (self.ui.action_Browse, self.do_bzr_browse),
            (self.ui.action_Log, self.do_bzr_log),
            (self.ui.action_Annotate, self.do_bzr_annotate),
            (self.ui.action_Info, self.do_bzr_info),
            (self.ui.action_System_Info, self.do_bzr_system_info),
            (self.ui.action_System_Log, self.do_bzr_system_log),
            (self.ui.action_Conflicts, self.do_bzr_conflicts),
            (self.ui.action_Switch, self.do_bzr_switch),
            (self.ui.action_Bind, self.do_bzr_bind),
            (self.ui.action_Unbind, self.do_bzr_unbind),
            (self.ui.action_Revert, self.do_bzr_revert),
            (self.ui.action_Uncommit, self.do_bzr_uncommit),
            (self.ui.action_Shelve, self.do_bzr_shelve),
            (self.ui.action_Unshelve, self.do_bzr_unshelve),
            (self.ui.action_Filter_View, self.do_bzr_view),
            (self.ui.action_Rename, self.do_bzr_rename),
            (self.ui.action_Delete, self.do_bzr_delete),
            (self.ui.action_Tag, self.do_bzr_tag),
            (self.ui.action_Plugins, self.do_bzr_plugins),
            (self.ui.action_All_Commands, self.do_bzr_advanced),
            (self.ui.action_Ignore_Unknowns, self.do_bzr_ignore),
            # Bookmarks ...
            (self.ui.action_Add_Bookmark, self.do_bookmarks_add),
            (self.ui.action_Edit_Bookmarks, self.do_bookmarks_edit_my),
            (self.ui.action_Refresh_Bookmarks, self.do_bookmarks_refresh),
            # Tools ...
            (self.ui.action_Add_Tool, self.do_add_tool),
            (self.ui.action_Edit_Tools, self.do_tools_edit_my),
            (self.ui.action_Refresh_Tools, self.do_tools_refresh),
            # Settings ...
            (self.ui.action_Configuration, self.do_set_configuration),
            (self.ui.action_Ignores, self.do_set_ignores),
            (self.ui.action_Rules, self.do_set_rules),
            (self.ui.action_Locations, self.do_set_locations),
            (self.ui.action_Credentials, self.do_set_credentials),
            (self.ui.action_Branch_Configuration,
                self.do_set_branch_configuration),
            (self.ui.action_Working_Tree_Ignores,
                self.do_set_working_tree_ignores),
            # Help ...
            (self.ui.action_Help_Contents, self.do_help_contents),
            (self.ui.action_Welcome, self.do_welcome),
            (self.ui.action_Report_a_Problem, self.do_report_a_problem),
            (self.ui.action_Translate_This_Application,
                self.do_translate_this_application),
            (self.ui.action_Get_Help_Online, self.do_get_help_online),
            (self.ui.action_About, self.do_about),
            ]
        toggle_links = [
            (self.ui.action_Toolbar, self.refresh_toolbar),
            (self.ui.action_WorkingTree_Browser,
                self.refresh_workingtree_browser),
            (self.ui.action_Toolbox, self.refresh_toolbox),
            (self.ui.action_Status_Bar, self.refresh_status_bar),
            (self.ui.action_Diagnostic_Mode, self.refresh_diagnostic_mode),
            ]
        for action, slot in trigger_links:
            self.connect(action, QtCore.SIGNAL("triggered(bool)"), slot)
        for action, slot in toggle_links:
            self.connect(action, QtCore.SIGNAL("toggled(bool)"), slot)

    def _init_action_mappings(self):
        # Declare the action (and menu) to conditions mapping. None if no
        # dependencies.
        self._conditions_by_action = {
            # Bazaar ...
            self.ui.action_Start:       None,
            self.ui.action_Add:         ['working-tree'],
            self.ui.action_Diff:        ['working-tree'],  # or a branch?
            self.ui.action_Commit:      ['working-tree'],
            self.ui.action_Plugins:     None,
            self.ui.action_All_Commands:None,
            # Bazaar/Start ...
            self.ui.action_Initialize:  None,
            self.ui.action_Branch:      None,
            self.ui.action_Checkout:    None,
            self.ui.action_Import:      None,
            # Bazaar/Collaborate ...
            self.ui.action_Pull:        ['branch', 'bound-branch', 'checkout'],
            self.ui.action_Merge:       ['working-tree'],
            self.ui.action_Update:      ['working-tree'],
            self.ui.action_Push:        ['branch', 'bound-branch', 'checkout'],
            self.ui.action_Send:        ['branch', 'bound-branch', 'checkout'],
            self.ui.action_Export:      ['branch', 'bound-branch', 'checkout'],
            self.ui.action_Tag:         ['branch', 'bound-branch', 'checkout'],
            # Bazaar/Explore ...
            self.ui.action_Browse:      ['branch', 'bound-branch', 'checkout'],
            self.ui.action_Log:         ['branch', 'bound-branch', 'checkout'],
            self.ui.action_Annotate:    ['branch', 'bound-branch', 'checkout'],
            self.ui.action_Info:        ['branch', 'bound-branch', 'checkout', 'repository'],
            self.ui.action_System_Info: None,
            self.ui.action_System_Log:  None,
            # Bazaar/Work ...
            self.ui.action_Conflicts:   ['working-tree'],
            self.ui.action_Switch:      ['checkout', 'bound-branch'],
            self.ui.action_Bind:        ['branch', 'checkout'],
            self.ui.action_Unbind:      ['bound-branch'],
            self.ui.action_Revert:      ['working-tree'],
            self.ui.action_Uncommit:    ['branch', 'bound-branch', 'checkout'],
            self.ui.action_Shelve:      ['working-tree'],
            self.ui.action_Unshelve:    ['working-tree'],
            self.ui.action_Filter_View: ['working-tree'],
            self.ui.action_Rename:      ['working-tree'],
            self.ui.action_Delete:      ['working-tree'],
            self.ui.action_Ignore_Unknowns: ['working-tree'],
            # Bookmarks
            self.ui.action_Add_Bookmark: ['branch', 'bound-branch', 'checkout',
                'repository', 'virtual-repository'],
            # Settings ...
            self.ui.action_Branch_Configuration: ['branch', 'bound-branch', 'checkout'],
            self.ui.action_Working_Tree_Ignores: ['working-tree'],
            # Special toolbar actions
            self.ui.action_Collaborate: ['branch', 'bound-branch', 'checkout'],
            self.ui.action_Explore:     None,
            self.ui.action_Work:        ['branch', 'bound-branch', 'checkout'],
            }
        if self.app_suite.name() == 'qbzr':
            self._conditions_by_action[self.ui.action_Log].append('repository')

        # Declare the action to command mapping
        self._command_by_action = {
            # Bazaar ...
            self.ui.action_Add:         "add",
            self.ui.action_Diff:        "diff",
            self.ui.action_Commit:      "commit",
            self.ui.action_Plugins:     "plugins",
            # Bazaar/Start ...
            self.ui.action_Initialize:  "init",
            self.ui.action_Branch:      "branch",
            self.ui.action_Checkout:    "checkout",
            self.ui.action_Import:      "import",
            # Bazaar/Collaborate ...
            self.ui.action_Pull:        "pull",
            self.ui.action_Merge:       "merge",
            self.ui.action_Update:      "update",
            self.ui.action_Push:        "push",
            self.ui.action_Send:        "send",
            self.ui.action_Tag:         "tag",
            self.ui.action_Export:      "export",
            # Bazaar/Explore ...
            self.ui.action_Browse:      "list",
            self.ui.action_Log:         "log",
            self.ui.action_Annotate:    "annotate",
            self.ui.action_Info:        "info",
            self.ui.action_System_Info: "version",
            self.ui.action_System_Log:  ".bzr.log",
            # Bazaar/Work ...
            self.ui.action_Conflicts:   "conflicts",
            self.ui.action_Switch:      "switch",
            self.ui.action_Bind:        "bind",
            self.ui.action_Unbind:      "unbind",
            self.ui.action_Revert:      "revert",
            self.ui.action_Uncommit:    "uncommit",
            self.ui.action_Shelve:      "shelve",
            self.ui.action_Unshelve:    "unshelve",
            self.ui.action_Filter_View: "view",
            self.ui.action_Rename:      "rename",
            self.ui.action_Delete:      "delete",
            self.ui.action_Ignore_Unknowns: "ignore",
            }

    def init_history(self):
        self._history_manager = history_manager.HistoryManager()
        self.connect(self.ui.menu_Open_Recent,
            QtCore.SIGNAL("aboutToShow()"), self._refresh_open_recent)

    def _refresh_open_recent(self):
        def opener(target):
            def open_location():
                self.open_saved_location(target)
            return open_location
        self.ui.menu_Open_Recent.clear()
        # TODO: maybe make the maximum number a preference
        maximum = 20
        history = self._history_manager.recent_locations(maximum=maximum)
        for i, (location, kind, title) in enumerate(history):
            if i < 9:
                title = "&%d %s" % (i + 1, title)
            elif i == 9:
                self.ui.menu_Open_Recent.addSeparator()
            action = self.ui.menu_Open_Recent.addAction(title,
                opener(location))
            action.setIcon(kinds.icon_for_kind(kind))
            action.setStatusTip(location)

    def init_location_viewer(self):
        action_map = {
            "close":            self.ui.action_Close,
            "open-local":       self.ui.action_Open,
            "open-location":    self.ui.action_Open_Location,
            }
        self.location_viewer = location_viewer.LocationViewer(action_map,
            self._preferences.get("location-selector-style"),
            self._history_manager)
        self.setCentralWidget(self.location_viewer)
        self.connect(self.location_viewer,
            QtCore.SIGNAL("currentChanged"), self.changed_view)
        self.changed_view()

    def current_view(self):
        return self.location_viewer.current_model()

    def changed_view(self):
        """The current view has changed - handle it."""
        view = self.current_view()
        # Refresh the context
        self.location_category = None
        self.location_context = self._base_context
        if view:
            self.location_category = view.category()
            if self.location_category is not None:
                self.location_context = self._base_context.copy()
                self.location_context.update(view.context())

        # Refresh the title bar
        if view:
            full_title = "%s - %s" % (view.display_name(), self.app_name)
            self.current_dir = view.location
        else:
            full_title = self.app_name
            self.current_dir = self.original_dir
        self.set_title(full_title)

        # Refresh the menus & toolbar
        some_views = bool(view)
        self.ui.action_Close.setEnabled(some_views)
        self.ui.action_Refresh.setEnabled(some_views)
        available = self._supported_commands
        kind = None
        working_tree = None
        wt_applicable = False
        if view:
            kind = view.kind()
            wt_applicable = view.has_wt()
            if view.supports_wt():
                working_tree = view.working_tree()
        for action, conditions in self._conditions_by_action.items():
            try:
                is_available = self._command_by_action[action] in available
            except KeyError:
                # Menus don't need a command
                is_available = True
            if conditions is None:
                action.setEnabled(is_available)
            elif view:
                legal = (working_tree and "working-tree" in conditions
                    or kind in conditions)
                action.setEnabled(is_available and legal)
            else:
                action.setEnabled(False)
        if kind != self._kind:
            self._kind = kind
            if self._kind_specific_toolbars_enabled:
                self._use_toolbar_for_kind(kind)

        # Update the working tree browser, hiding it for pages where
        # it's never applicable
        action = self.ui.action_WorkingTree_Browser
        action.setEnabled(wt_applicable)
        wt_visible = wt_applicable and action.isChecked()
        self._wt_browser.setVisible(wt_visible)

        # Set the current directory. Note that we can't always
        # change to the root of a selected branch or repository
        # because it might be remote.
        if self.location_category == "local":
            root_url = self.location_context["root"]
            if not root_url.startswith("file://"):
                raise AssertionError("root location is not a URL: %s" % (root_url,))
            root_path = urlutils.local_path_from_url(root_url)
            try:
                os.chdir(root_path)
                current_dir = root_path
            except OSError:
                # Directory has been removed - tell the user & close the page
                title = gettext("Error")
                msg = gettext("Unable to change to %s - closing page."
                    % (root_path,))
                self.show_error(title, msg)
                self.do_close()
                return
        else:
            try:
                os.chdir(self.original_dir)
                current_dir = self.original_dir
            except OSError:
                # It probably doesn't matter. We could change to the user's
                # home directory here say but there seems limited benefit
                # either way
                current_dir = os.getcwdu()
                pass
        if wt_visible:
            self._wt_browser.set_tree(view.tree, view.branch)

        # Update the toolbox panel, hiding it for pages where
        # it's less applicable
        self._update_toolbox_visibility()

        # Refresh the status bar
        hosted_branch_available = bool(view and self._get_hosted_web_url())
        self._hosted_indicator.setVisible(hosted_branch_available)

        # Switch the hat
        suggestion = self._accessories.select_hat_for_path(current_dir)
        if suggestion:
            self._suggest_hat(suggestion, current_dir)

    def _update_toolbox_visibility(self):
        if self._kind in [
            kinds.BRANCH_KIND,
            kinds.BOUND_BRANCH_KIND,
            kinds.CHECKOUT_KIND,
            ]:
            tbox_applicable = self._preferences['toolbox-on-status-view']
        elif self._kind in [
            kinds.REPOSITORY_KIND,
            kinds.VIRTUAL_REPO_KIND,
            ]:
            tbox_applicable = self._preferences['toolbox-on-repository-view']
        elif self._kind == kinds.WELCOME_PAGE:
            tbox_applicable = self._preferences['toolbox-on-welcome-view']
        else:
            tbox_applicable = False
        action = self.ui.action_Toolbox
        action.setEnabled(tbox_applicable)
        tbox_visible = tbox_applicable and action.isChecked()
        self._toolbox.setVisible(tbox_visible)

    def _suggest_hat(self, hat, path):
        title = gettext("Hat Available")
        msg = gettext(
            "A %(hat)s hat is available but not associated with this "
            "location. Would you like to use it when visiting %(path)s?")
        text = msg % {'hat': hat, 'path': path}
        if self.ask_question(title, text):
            self._accessories.hat_selector.set_rule(path, hat)
            self._accessories.select_hat_for_path(path)
        else:
            # Don't bother the user again with this question
            self._accessories.hat_selector.set_rule(path, "")

    def _refresh_dynamic_menu(self, menu, header_items, body_callable,
            footer_items=None):
        """Refresh a dynamic menu."""
        menu.clear()

        # Add the header items
        if header_items:
            for kind, item in header_items:
                if kind == 'action':
                    menu.addAction(item)
                elif kind == 'menu':
                    menu.addMenu(item)
            menu.addSeparator()

        # Add the body
        body_callable(menu)

        # Add the footer items
        if footer_items:
            menu.addSeparator()
            for kind, item in footer_items:
                if kind == 'action':
                    menu.addAction(item)
                elif kind == 'menu':
                    menu.addMenu(item)

    ## Preference management ##

    def apply_preferences(self):
        # Find the non-default preferences and apply them
        for name, value in self._preferences.items():
            default_value = DEFAULT_PREFERENCES.get(name)
            if value != default_value:
                self._preference_changed(name, value, old_value=value)

    def _preference_changed(self, name, new_value, old_value=None, parent=None):
        """Handle the change of a preference."""
        trace.mutter("setting preference %s to %s (was %s)" %
            (name, new_value, old_value))
        if name == 'app-suite':
            self._apply_app_suite(new_value)
        elif name == 'toolbar-contents':
            self._apply_toolbar_contents(new_value)
        elif name == 'toolbar-style':
            self._apply_toolbar_style(new_value)
        elif name == 'workingtree-style':
            self._apply_workingtree_style(new_value)
        elif name == 'location-selector-style':
            if new_value != old_value:
                self.show_restart_required(parent)
        elif name == 'toolbox-style':
            if new_value != old_value:
                self.show_restart_required()
        elif name.startswith('toolbox-on-'):
            self._update_toolbox_visibility()
        elif name == 'workingtree-default-to-edit':
            self._apply_workingtree_default_to_edit(new_value)
        elif name == 'language':
            if new_value != old_value:
                self.show_restart_required(parent)

    def _apply_app_suite(self, app_suite):
        try:
            self.app_suite = mod_app_suite.app_suite_registry.get(app_suite)
        except KeyError:
            trace.mutter("unable to use application suite %s - using default"
                % (app_suite,))
            # GTK (say) was installed but no longer is. Fall back to QBzr.
            self.app_suite = mod_app_suite.app_suite_registry.get()
        self._supported_commands = self.app_suite.keys()

    def _apply_toolbar_contents(self, contents):
        self._kind_specific_toolbars_enabled = contents == 'kind-specific'
        if self._kind_specific_toolbars_enabled:
            self._use_toolbar_for_kind(self._kind)
        else:
            self._use_toolbar(contents)

    def _apply_toolbar_style(self, style):
        self._toolbar_builder.set_style(style)
        # We need to resize the dock here when the toolbar height shrinks.
        # I can't work out how to do that. :-(
        # (The workaround is simple though: restart the app.)
        #self._toolbar_dock.adjustSize()

    def _apply_workingtree_style(self, style):
        actual_style = self._wt_browser.get_style()
        if actual_style != style:
            self._wt_browser.set_style(style)

    def _apply_workingtree_default_to_edit(self, enabled):
        if enabled:
            action = "edit"
        else:
            action = "open"
        self._wt_browser.set_default_action(action)

    ## Hat management ##

    def init_hats(self):
        self.init_bookmarks()
        self.init_tools()
        self.refresh_menus()

    def refresh_menus(self):
        self._refresh_bookmarks_menu()
        self._refresh_tools_menu()
        self._refresh_custom_editors()

    def reload_menus(self):
        self.do_bookmarks_refresh()
        self.do_tools_refresh()
        self._refresh_custom_editors()

    def _refresh_custom_editors(self):
        desktop = self.desktop_env.name()
        editors = {}
        for acc in self._accessories.items():
            editors.update(acc.custom_editors(desktop))
        self.desktop_env.set_custom_editors(editors)

    ## Bookmark management ##

    def init_bookmarks(self):
        self._std_bookmark_menu_items = [
            ("action", self.ui.action_Add_Bookmark),
            ("action", self.ui.action_Edit_Bookmarks),
            ]

    def _refresh_bookmarks_menu(self):
        def bookmark_appender(menu):
            for acc in self._accessories.items():
                for entry in acc.bookmarks().root():
                    self._add_bookmark_entry_to_menu(entry, menu)
        self._refresh_dynamic_menu(self.ui.menu_Bookmarks,
            self._std_bookmark_menu_items, bookmark_appender)
        if self.location_viewer is not None:
            self.location_viewer.refresh_view_for_key("welcome")

    def _add_bookmark_entry_to_menu(self, entry, menu):
        if isinstance(entry, bookmarks.Bookmark):
            def bm_open_callback():
                self._do_open_bookmark(entry)
            label = entry.title
            action = menu.addAction(entry.title, bm_open_callback)
            action.setStatusTip(entry.location)
            if entry.kind is not None:
                action.setIcon(kinds.icon_for_kind(entry.kind))
        elif isinstance(entry, bookmarks.BookmarkFolder):
            submenu = QtGui.QMenu(entry.title, menu)
            for child in entry:
                self._add_bookmark_entry_to_menu(child, submenu)
            menu.addMenu(submenu)
        elif isinstance(entry, bookmarks.BookmarkSeparator):
            menu.addSeparator()

    def _do_open_bookmark(self, bm):
        self.open_saved_location(bm.location)

    ## Tool management ##

    def init_tools(self):
        self._std_tool_menu_items = [
            ("action", self.ui.action_Add_Tool,),
            ("action", self.ui.action_Edit_Tools),
            ]

    def _refresh_tools_menu(self):
        def tool_appender(menu):
            submenus = {}
            for acc in self._accessories.items():
                for entry in acc.tools().root():
                    self._add_tool_entry_to_menu(entry, menu, submenus)

        if self.desktop_env.name() == 'windows':
            footer_actions = [("action", self.ui.action_Preferences)]
        else:
            footer_actions = None
        self._refresh_dynamic_menu(self.ui.menu_Tools,
            self._std_tool_menu_items, tool_appender, footer_actions)
        self._toolbox.set_location(self.current_dir)

    def _add_tool_entry_to_menu(self, entry, menu, submenus):
        if isinstance(entry, tools.Tool):
            def open_callback():
                self._do_open_tool(entry)
            label = entry.title
            action = menu.addAction(entry.title, open_callback)
            tip = "(%s) %s" % (entry.type, entry.action)
            action.setStatusTip(tip)
            if entry.icon:
                icon = kinds.icon_by_resource_path(entry.icon)
            else:
                icon = kinds.icon_for_kind(entry.type)
            action.setIcon(icon)
        elif isinstance(entry, tools.ToolFolder):
            submenu = submenus.get(entry.title)
            if submenu is None:
                submenu = QtGui.QMenu(entry.title, menu)
                if entry.icon:
                    icon = kinds.icon_by_resource_path(entry.icon)
                    submenu.setIcon(icon)
                submenus[entry.title] = submenu
                new_menu = True
            else:
                new_menu = False
                if entry.existing == 'replace':
                    submenu.clear()
            for child in entry:
                self._add_tool_entry_to_menu(child, submenu, submenus)
            if new_menu:
                menu.addMenu(submenu)
        elif isinstance(entry, tools.ToolSeparator):
            menu.addSeparator()
        elif isinstance(entry, tools.ToolSet):
            name = entry.name
            project = entry.project
            toolset = accessories.find_toolset(name)
            for child in toolset.as_tool_folder({'project': project}):
                self._add_tool_entry_to_menu(child, menu, submenus)
        elif isinstance(entry, tools.ToolAction):
            name = entry.name
            try:
                action = self.action_provider(name)
                menu.addAction(action)
            except KeyError:
                mutter("skipping unknown action %s building tool-menu" % name)

    def _do_open_tool(self, tool):
        old_loc_context = []
        if tool.type == 'link':
            link = mod_app_suite.command_to_expanded(tool.action,
                self._escape_context(self.location_context))
            self.open_web_browser(link)
        else:
            if  (tool.uses_selected):
                # populate 'wt_selected' info for context from working tree
                if isinstance(self._wt_browser._browser,
                              wt_browser._QBrowseStyleBrowser):
                    selected_items = \
                        self._wt_browser._browser._tree_viewer.\
                        get_selection_items()
                    if len(selected_items) > 0:
                        old_loc_context = self.location_context.copy()
                        paths = [item.path for item in selected_items]
                        # NOTE:
                        # Unless items are quoted then filenames with spaces
                        # in are not handled properly on Windows
                        escaped = [self._escape_context_item(p, "'")\
                            for p in paths]
                        self.location_context['wt_selected'] = " ".join(escaped)
                elif isinstance(self._wt_browser._browser,
                                wt_browser._ClassicBrowser):
                    if self._wt_browser._browser.\
                        _action_panel._selected_fileinfo:
                        old_loc_context = self.location_context.copy()
                        selected_item = self._wt_browser._browser.\
                            _action_panel._selected_fileinfo.filePath()
                        self.location_context['wt_selected'] = \
                            self._escape_context_item(selected_item, "'")
            #
            args = mod_app_suite.command_to_args(tool.action,
                self._escape_context(self.location_context))
            if (tool.type == 'bzr') or (tool.type == 'bzr-exec'):
                # If the command looks like it's a GUI one,
                # run it directly. Otherwise, run it via qrun
                # making sure that we're passing --ui-mode.
                if args[0][0] in ['q', 'g']:
                    args = ['bzr'] + args
                elif tool.type == 'bzr-exec':
                    args = ['bzr', 'qrun', '--ui-mode', '--execute', '--'] + args
                else:
                    args = ['bzr', 'qrun', '--ui-mode', '--'] + args
            self.run_app(args, use_shell=(tool.type=='shell'))
            if old_loc_context:
                self.location_context = old_loc_context.copy()

    ## Show message helpers ##

    def _message(self, type, title, message, extra_info=None, details=None, parent=None):
        if parent is None:
            parent = self
        mbox = QtGui.QMessageBox(parent)
        mbox.setIcon(_MSG_TYPE_TO_ICON[type])
        mbox.setWindowTitle(gettext(title))
        mbox.setText(gettext(message))
        if extra_info:
            mbox.setInformativeText(extra_info)
        if details:
            mbox.setDetailedText(details)
        mbox.exec_()

    def show_note(self, title, message, extra_info=None, details=None, parent=None):
        self._message("note", title, message, extra_info, details, parent)

    def show_warning(self, title, message, extra_info=None, details=None, parent=None):
        self._message("warning", title, message, extra_info, details, parent)

    def show_error(self, title, message, extra_info=None, details=None, parent=None):
        self._message("error", title, message, extra_info, details, parent)

    def show_not_available_yet(self):
        self.show_note("Sorry", u"Action is not available yet.")

    def show_restart_required(self, parent=None):
        self.show_warning(gettext("Restart Required"), gettext(
            "Please restart Bazaar Explorer for this preference to take effect."),
            parent=parent)

    ## Request input helpers ##

    def ask_question(self, title, message):
        reply = QtGui.QMessageBox.question(self, title, message,
            QtGui.QMessageBox.Yes | QtGui.QMessageBox.No,
            QtGui.QMessageBox.Yes)
        return reply == QtGui.QMessageBox.Yes

    def request_string(self, title, message):
        window_flags = QtCore.Qt.WindowFlags(QtCore.Qt.Sheet)
        (value, ok) = QtGui.QInputDialog.getText(self, title,
            message, QtGui.QLineEdit.Normal, '', window_flags)
        if ok and not value.isEmpty():
            return unicode(value)
        else:
            return None

    ## Progress monitoring helpers ##

    def progress_started(self, text):
        """Show that something is happening."""
        # XXX: This still isn't quite right. We probably need a timer
        # every 50-100 milliseconds to repaint the busy indicator.
        # Moving to a ProgressDialog is another option, though it may not
        # solve the problem.
        self.ui.statusbar.showMessage(text)
        self._busy_indicator.setVisible(True)
        self._busy_indicator.reset()
        self._cursors.append(self.cursor())
        busy_cursor = QtGui.QCursor(QtCore.Qt.BusyCursor)
        QtGui.QApplication.setOverrideCursor(busy_cursor)

    def progress_finished(self):
        """Show that something has completed."""
        if self._cursors:
            self._cursors.pop()
            if len(self._cursors) == 0:
                self._busy_indicator.setVisible(False)
            QtGui.QApplication.restoreOverrideCursor()
            self.ui.statusbar.clearMessage()

    ## Process execution helpers ##

    def open_web_browser(self, url):
        url = urlutils.unescape(url)
        if self.dry_run:
            msg = u'Browser URL: %s' % (url,)
            self.show_note("Diagnostic Mode", msg)
            return
        QtGui.QDesktopServices.openUrl(QtCore.QUrl(url))

    def open_hosted_web_url(self):
        url = self._get_hosted_web_url()
        if url is not None:
            self.open_web_browser(url)
        else:
            self.show_error(gettext("Sorry"), gettext(
                "The hosted web URL for this location is unknown."))

    def _get_hosted_web_url(self):
        """Get the web URL for the current location.

        @return: the URL as a string or None if none.
        """
        view = self.current_view()
        if view is None:
            return None
        kind = view.kind()
        if kind == kinds.REPOSITORY_KIND:
            if view.category() == 'local' and os.path.exists("trunk"):
                return self._hosted_url_resolver.get_project_url("trunk")
            else:
                return None
        return self._hosted_url_resolver.get_branch_url('.')

    def run_app(self, args, refresh_on_success=False, success_callback=None,
                use_shell=False):
        """Run an external application.

        :param args: list of arguments. The first one is the command name
          which is expected to be on a path (or have an absolute path).
        :param refresh_on_success: if True and the process exits successfully
          and the current page on process start is still current, refresh it
        :param success_callback: callable to invoke if the command completes
          successfully; if not None, overrides refresh_on_success
        :param use_shell: if True, cause the subprocess to use a shell
          to invoke args.
        """
        # Replace 'bzr' with the executable that started this process
        if args[0] == 'bzr':
            args[:1] = self._get_bzr_executable()
        
        # In diagnostic mode, just display what we would have done
        if self.dry_run:
            msg = u'Command: %s' % (args[0],)
            if len(args) > 1:
                info = "Parameters:\n%s" % "\n".join(args[1:])
            else:
                info = None
            details = "Working Directory: %s" % (os.getcwdu(),)
            self.show_note("Diagnostic Mode", msg, info, details=details)
            return

        # Run the application
        if success_callback is None and refresh_on_success:
            view_on_launch = self.current_view()
            def conditional_refresh():
                if self.current_view() == view_on_launch:
                    self.do_refresh()
            success_callback = conditional_refresh
        self._app_runner.run_app(args, success_callback, use_shell=use_shell)

    def _get_bzr_executable(self):
        # XXX this is copied form qbzr/lib/subprocess. Try refactor so that
        # we have code reuse.
        if getattr(sys, "frozen", None) is not None:
            bzr_exe = sys.executable
            possible_exes = ("bzrw.exe", "bzr.exe")
            if os.path.basename(bzr_exe) in possible_exes:
                return [bzr_exe]
            else:
                # Was run from something like tbzrcommand.
                for possible_exe in possible_exes:
                    bzr_exe = os.path.join(os.path.dirname(sys.executable),
                                           possible_exe)
                    if os.path.isfile(bzr_exe):
                        return [bzr_exe]
                raise errors.BzrError(gettext('Could not locate "bzr.exe".'))
        else:
            # otherwise running as python script.
            # ensure run from bzr, and not others, e.g. tbzrcommand.py
            script = sys.argv[0]
            # make absolute, because we may be running in a different
            # dir.
            script = os.path.abspath(script)
            if os.path.basename(script) == "bzr":
                return [sys.executable, script]
            else:
                import bzrlib
                # are we running directly from a bzr directory?
                script = os.path.join(bzrlib.__path__[0], "..", "bzr")
                if os.path.isfile(script):
                    return [sys.executable, script]
                # maybe from an installed bzr?
                script = os.path.join(sys.prefix, "scripts", "bzr")
                if os.path.isfile(script):
                    return [sys.executable, script]
                
                raise errors.BzrError(
                    gettext('Could not locate "bzr" script.'))

    def run_bzr_gui(self, cmd_id, context=None):
        """Run a bzr GUI application.

        Most commands are runs as external applications but a select
        few are run as internal dialogs to get better integration, e.g.
        smarter refreshing.

        :param cmd_id: command name in app_suite, e.g. commit
        :param context: the context to use or None for the default one
        """
        if context is None:
            context = self._escape_context(self.location_context)
        # Run this command internally if we know how to
        if (self._preferences.get('custom-dialog-%s' % cmd_id) and
            self._run_internal_gui(cmd_id, context)):
            return

        # No luck - try as an external application
        args, post_action = self.get_command_from_app_suite(cmd_id, context)
        if args:
            refresh_on_success = post_action == 'refresh'
            self.run_app(args, refresh_on_success)
        else:
            msg = u"Action not yet supported by current app_suite."
            details = "Profile name is: %s\nAction identifier is: %s" % \
                (self.app_suite.name(), cmd_id)
            self.show_error("Sorry", msg, details=details)

    def get_command_from_app_suite(self, cmd_id, context=None):
        """Convert a cmd-id into a command-line given the active app_suite.

        :param cmd_id: command name in app_suite, e.g. commit
        :param context: the context to use or None for the default one
        :return: args, post_action where
          args is a list of command line arguments;
          post_action is one of the following values:
          * "refresh" - refresh the current page
          * None - do nothing
        """
        if context is None:
            context = self._escape_context(self.location_context)
        args = self.app_suite.lookup(cmd_id, context, self.location_category)
        post_action = None
        auto_refresh = self._preferences.get('status-auto-refresh')
        if auto_refresh and args and cmd_id in _AUTO_REFRESH_CMDS:
            post_action = "refresh"
        return args, post_action

    def open_terminal(self):
        self.run_app(split_command_options(self._terminal))

    ## File editing helpers ##

    def edit_config_file(self, name):
        """Edit a configuration file.

        The current page is refreshed if the file is actually changed.

        :param name: basename of Bazaar configuration file.
        """
        cmd = self.get_command_from_app_suite("config:%s" % (name,))[0]
        if cmd:
            cfg_path = self._lookup_config_path(name)
            if cfg_path is not None:
                self._record_edit_callback(cfg_path, self.do_refresh)
            self.run_app(cmd, success_callback=self._refresh_user_configuration)
        else:
            cfg_path = self._lookup_config_path(name)
            if cfg_path is not None:
                self.edit_file(cfg_path, self.do_refresh)

    def _lookup_config_path(self, name):
        if name == "branch.conf":
            branch_root = self.location_context['branch_root']
            cfg_path = urlutils.join(branch_root, ".bzr/branch/branch.conf")
            if cfg_path.startswith("file://"):
                cfg_path = urlutils.local_path_from_url(cfg_path)
            else:
                msg = gettext("Cannot edit remote file.")
                details = gettext("URL is: %s") % (cfg_path,)
                self.show_error(gettext("Sorry"), msg, details=details)
                return None
        elif name == ".bzrignore":
            cfg_path = name
        else:
            cfg_path = self._get_config_path(name)
        return cfg_path

    def edit_file(self, path, edit_callback=None):
        if os.path.isdir(path):
            # "edit" on a directory best translates to opening it
            # in the relevant file manager, given there's no guarantee
            # that a user's editor will do something sensible with a
            # directory.
            self.open_path(osutils.abspath(path))
            return

        if sys.platform in ['darwin', 'win32'] and not os.path.exists(path):
            # open -t on OS X and wordpad on Windows require the file to be there
            open(path, 'w').close()
        editor = self.desktop_env.get_custom_editor_for(path) or self._editor
        self._record_edit_callback(path, edit_callback)
        try:
            self.run_app(split_command_options(editor) + [osutils.abspath(path)])
        except OSError, ex:
            msg = gettext("Failed to edit '%(path)s' using '%(editor)s'.") % \
                    {'path': path, 'editor': editor}
            info = gettext(
                "You may want to install the matching application or change the configured editor.")
            details = unicode(ex)
            self.show_error(gettext("Sorry"), msg, info, details)

    def init_edit_tracking(self):
        # A map of pathnames to methods to call when edits are detected
        self._file_edited_callbacks = {}
        self._file_tracker = QtCore.QFileSystemWatcher()
        self.connect(self._file_tracker,
            QtCore.SIGNAL("fileChanged(QString)"), self._file_edited_handler)

    def _record_edit_callback(self, path, edit_callback):
        if not edit_callback:
            return
        self._file_edited_callbacks[path] = edit_callback
        self._file_tracker.addPath(path)

    def _file_edited_handler(self, path):
        path = unicode(path)
        callback = self._file_edited_callbacks.get(path)
        if callback:
            callback()

    ## Directory/file management helpers ##

    def _get_config_path(self, name):
        return osutils.pathjoin(config.config_dir(), name)

    def open_path(self, path):
        if self.dry_run:
            msg = u'Open: %s' % (path,)
            self.show_note("Diagnostic Mode", msg)
            return
        url = QtCore.QUrl.fromLocalFile(path)
        QtGui.QDesktopServices.openUrl(url)

    def delete_path(self, path):
        if self.dry_run:
            msg = u'Delete: %s' % (path,)
            self.show_note("Diagnostic Mode", msg)
            return
        deleter = osutils.delete_any
        if osutils.isdir(path):
            title = gettext("Delete Directory")
            if os.listdir(path):
                deleter = osutils.rmtree
                msg_template = gettext("Delete directory %s and all its children?")
            else:
                msg_template = gettext("Delete empty directory %s?")
        elif osutils.islink(path):
            title = gettext("Delete Symlink")
            msg_template = gettext("Delete symlink %s?")
        else:
            title = gettext("Delete File")
            msg_template = gettext("Delete file %s?")
        msg = msg_template % (path,)
        if self.ask_question(title, msg):
            if self._preferences.get('delete-to-trash'):
                self._delete_to_trash(path, deleter)
            else:
                deleter(path)
            if self._preferences.get('status-auto-refresh'):
                self.do_refresh()

    def _delete_to_trash(self, path, killer):
        try:
            send2trash(path)
        except OSError, e:
            try:
                err = unicode(e)
            except UnicodeError:
                err = str(e)
            msg = gettext(
                """Can't delete "%(path)s" to trash.\n\n"""
                "Details:\n"
                "  %(error)s"
                "\n\n"
                "Delete it without sending to trash?"
                ) % dict(path=path, error=err)
            if self.ask_question(gettext("Error"), msg):
                killer(path)

    def new_file(self, in_directory):
        if self.dry_run:
            msg = u'New File: %s' % (path,)
            self.show_note("Diagnostic Mode", msg)
            return
        title = gettext("New File")
        info = gettext("Destination directory: %s" % in_directory)
        label = gettext("Name of new file:")
        message = "%s\n\n%s" % (info, label)
        name = self.request_string(title, message)
        if name:
            path = osutils.pathjoin(in_directory, name)
            try:
                open(path, 'w').close()
            except OSError, ex:
                title = gettext("Sorry")
                why = gettext(unicode(ex))
                msg = "%s %s." % (gettext(u"Unable to create file"), path)
                self.show_error(title, msg, extra_info=why)

    def new_directory(self, in_directory):
        title = gettext("New Directory")
        info = gettext("Destination directory: %s" % in_directory)
        label = gettext("Name of new directory:")
        message = "%s\n\n%s" % (info, label)
        name = self.request_string(title, message)
        if name:
            path = osutils.pathjoin(in_directory, name)
            if self.dry_run:
                msg = u'New Directory: %s' % (path,)
                self.show_note("Diagnostic Mode", msg)
                return
            try:
                os.mkdir(path)
            except OSError, ex:
                title = gettext("Sorry")
                why = gettext(unicode(ex))
                msg = "%s %s." % (gettext(u"Unable to create directory"), path)
                self.show_error(title, msg, extra_info=why)

    def _do_view_action(self, action, target, context=None):
        """Do an action on a target.

        :param action: one of the following:
          * open-location - open a location in Explorer
          * link - open a link in a web browser
          * edit - edit a file
          * open - open a path using the default application
          * delete - delete a path and any children it may have
          * new-file - create a new file
          * new-directory - create a new directory
          * refresh-page - refresh the current page
          * task - run a task and monitor its progress
          * xxx - run logical bzr command xxx. The commands
            currently supported are diff, annotate.
        :param target: the target object ...
          for the open-location action, the url;
          for the link action, the url;
          for the task action, a (message, callable) tuple
          for other actions, the path relative to the root of the location.
        :param context: the context to use if already known; if this
          is set for a logical command, then target is not used to
          build a context.
        """
        if action == 'open-location':
            self.open_saved_location(target)
        elif action == 'link':
            self.open_web_browser(target)
        elif action == 'edit':
            self.edit_file(target, edit_callback=self.do_refresh)
        elif action == 'open':
            self.open_path(target)
        elif action == 'delete':
            self.delete_path(target)
        elif action == 'new-file':
            self.new_file(target)
        elif action == 'new-directory':
            self.new_directory(target)
        elif action == 'refresh-page':
            # We don't want a full refresh here because the WT
            # should already have been refreshed
            self.location_viewer.refresh_current()
        elif action == 'task':
            msg, callable = target
            self.run_task(msg, callable)
        else:
            if context is None and target is not None:
                context = self._context_with_selection([target])
            self.run_bzr_gui(action, context)

    def _context_with_selection(self, selection):
        """Build a context with the selection given.

        :param selection: a list of selected items
        :return: None if selection is None or empty, otherwise the
          current location context with additions placeholders defined.
          (See app_suite.py for details).
        """
        if not selection:
            return None
        result = self._escape_context(self.location_context)
        filename = selection[0]
        dirname, basename = os.path.split(filename)
        result['filename'] = self._escape_context_item(filename)
        result['dirname'] = self._escape_context_item(dirname)
        result['basename'] = self._escape_context_item(basename)
        escaped = [self._escape_context_item(s) for s in selection]
        result['selected'] = " ".join(escaped)
        return result

    def _escape_context(self, context):
        """Ensure values in context with embedded spaces are surrounded by quotes."""
        result = {}
        for key, value in context.items():
            result[key] = self._escape_context_item(value)
        return result

    def _escape_context_item(self, s, q='"'):
        """If s contains spaces, surround it with quotes, otherwise return it."""
        if s and ' ' in s:
            return '%s%s%s' % (q,s,q)
        else:
            return s

    def run_task(self, msg, task_callable):
        """Run a task that might take some time without blocking the UI.

        :param msg: message to display in status bar
        :param task_callable: the potentially long callable
        """
        def callable_with_cleanup():
            try:
                task_callable()
            finally:
                self.progress_finished()

        self.progress_started(msg + " ...")
        # We need a short delay in here to allow the GUI to repaint.
        # 0 is *meant* to work but doesn't appear to (on Linux at least).
        short_delay = 10  # milliseconds
        QtCore.QTimer.singleShot(short_delay, callable_with_cleanup)

    def _run_internal_gui(self, cmd_id, context=None):
        """Run a GUI command as an internal dialog.

        :return: True if the dialog could be executed internally.
        """
        internal_handlers = {
            # map of command-id to tuple of starter, modal, finisher
            'init': (self._start_init, True, self._finish_init),
            'branch': (self._start_branch, False, self._finish_branch),
            'checkout': (self._start_checkout, False, self._finish_checkout),
            'switch': (self._start_switch, False, self._finish_switch),
            }
        if cmd_id in internal_handlers and cmd_id in custom_dialog_registry:
            if self.dry_run:
                msg = u'Internal dialog for: %s' % (cmd_id,)
                details = "Working Directory: %s" % (os.getcwdu(),)
                self.show_note("Diagnostic Mode", msg, None, details=details)
            else:
                starter, modal, finisher = internal_handlers[cmd_id]
                dialog = starter(context)
                if modal:
                    result = dialog.exec_()
                    if finisher:
                        finisher(result, dialog)
                else:
                    dialog.show()
                    if finisher:
                        # We keep track of the most recently opened dialog for
                        # each command-id so we can find it during the finisher.
                        self._custom_modeless_dialogs[cmd_id] = dialog
                        # NOTE: This isn't actually the close event but the
                        # subprocess competion event. We may want to tweak
                        # that in the future.
                        self.connect(dialog.process_widget,
                            QtCore.SIGNAL("finished()"), finisher)
            return True
        return False

    def _start_init(self, context=None):
        # TODO: use a 'Projects' directory if configured
        default_init_location = u'.'
        model_name = self._preferences.get('workspace-model')
        try:
            model = workspace_models.workspace_model_registry.get(model_name)
        except KeyError:
            # Use the default if the preference is no longer in the registry
            model = workspace_models.workspace_model_registry.get()
        return custom_dialog_registry.get('init')(
            default_init_location, model=model, parent=self)

    def _finish_init(self, result, dialog):
        if result:
            new_location = osutils.abspath(dialog.get_location())
            self.open_location(new_location)

    def _start_branch(self, context=None):
        from_location = None
        if context:
            from_location = context.get('branch_root')
            if from_location and from_location.startswith("file://"):
                from_location = urlutils.local_path_from_url(from_location)
        bind = self._preferences.get('bind-branches-by-default')
        dialog = custom_dialog_registry.get('branch')
        try:
            return dialog(from_location, bind=bind, parent=self)
        except TypeError:
            trace.mutter(
                "Ignoring bind preference for branch dialog."
                "QBzr version may be out of date.")
            return dialog(from_location, parent=self)

    def _finish_branch(self):
        dialog = self._custom_modeless_dialogs['branch']
        to_location = dialog.get_to_location()
        if to_location:
            new_location = osutils.abspath(to_location)
            self.open_location(new_location)

    def _start_checkout(self, context=None):
        from_location = None
        if context:
            from_location = context.get('branch_root')
            if from_location and from_location.startswith("file://"):
                from_location = urlutils.local_path_from_url(from_location)
        dialog = custom_dialog_registry.get('checkout')
        lightweight = self._preferences.get('lightweight-checkout-by-default')
        return dialog(from_location, parent=self, lightweight=lightweight)

    def _finish_checkout(self):
        dialog = self._custom_modeless_dialogs['checkout']
        to_location = dialog.get_to_location()
        if to_location:
            new_location = osutils.abspath(to_location)
            self.open_location(new_location)

    def _start_switch(self, context=None):
        branch, bzrdir, location = None, None, None
        if context:
            branch_location = context.get('branch-root')
            checkout_location = context.get('root')
        return custom_dialog_registry.get('switch')(branch_location,
                                                        checkout_location,
                                                        parent=self)
    
    def _finish_switch(self):
        dialog = self._custom_modeless_dialogs['switch']
        self.do_refresh()

    ## Toolbar helper methods ##

    def _use_toolbar(self, name):
        if name == self._toolbar_name:
            return
        #print "switching to toolbar %s" % (name,)
        if self._toolbar_stack is None:
            # construct a stack of toolbars
            stack = QtGui.QStackedWidget()
            for key in self._toolbar_builder.keys():
                stack.addWidget(self._toolbar_builder.get(key))
            self._toolbar_dock.addWidget(stack)
            self._toolbar_stack = stack
        toolbar = self._get_toolbar(name)
        self._toolbar_stack.setCurrentWidget(toolbar)
        self._toolbar_name = name

    def _get_toolbar(self, name):
        """Build a toolbar with the given name.

        If the name is unknown, a default toolbar is returned.
        """
        tb = self._toolbar_builder.get(name)
        return tb

    def _use_toolbar_for_kind(self, kind):
        """Switch to the toolbar for the given kind."""
        if kind is not None:
            name = "kind:%s" % (kind,)
            if name in self._toolbar_builder.keys():
                self._use_toolbar(name)
                return
        # Fall back to the welcome toolbar
        self._use_toolbar("kind:welcome")

    ## General helper methods ##

    def open_saved_location(self, location):
        self.open_location(location, auto_virtual_repo_open=True)

    def open_location(self, location, auto_virtual_repo_open=False):
        """Display the location in a new tab.

        :param location: path or URL to open.
        """
        def _open_location_task():
            if not self.location_viewer.select_view_for_key(location):
                try:
                    model = mod_location.LocationModel(location,
                        self._do_view_action, self.dry_run)
                except errors.BzrError, ex:
                    # Switch off progress monitoring before displaying the error
                    self.progress_finished()
                    self._show_unable_to_open_location(location, ex,
                        auto_virtual_repo_open)
                    return
                self.location_viewer.add_location(location, model)
            self.changed_view()

        # Do the processing in the background
        msg = gettext("Opening %s") % (location,)
        self.run_task(msg, _open_location_task)

    def _show_unable_to_open_location(self, location, ex,
        auto_virtual_repo_open):
        title = gettext("Unable to open location")
        if isinstance(ex, errors.NotBranchError):
            if auto_virtual_repo_open:
                self.open_virtual_repo(location)
                return
            # Offer to open the location as a workspace
            problem = gettext(
                u"%s is not a branch, checkout or repository.") % \
                (location,)
            question = gettext(
                "Do you want to open it as a virtual repository, "
                "searching for nested locations?")
            msg = u"%s\n\n%s" % (problem, question)
            if self.ask_question(title, msg):
                self.open_virtual_repo(location)
        else:
            # We're not really sure what the issue is so report it.
            # We really want the exception to localise the text itself
            # here so that parameters work correctly. For now, we do
            # the translation after parameter substition and hope for
            # the best ...
            why = gettext(unicode(ex))
            msg = "%s %s." % (gettext(u"Unable to open"), location)
            self.show_error(title, msg, extra_info=why)

    def open_virtual_repo(self, location):
        model = mod_location.VirtualRepoModel(location, self._do_view_action)
        self.location_viewer.add_location(location, model)
        self.changed_view()

    def action_provider(self, name):
        attr = "action_%s" % (name,)
        return self.ui.__dict__[attr]

    ## File actions ##

    def do_open(self):
        location = QtGui.QFileDialog.getExistingDirectory(self,
            gettext("Open Directory"))
        if location:
            self.open_location(unicode(location))

    def do_open_location(self):
        location = self.request_string(gettext("Open Location"),
            gettext("Enter the location URL, "
            "e.g. lp:bzr, http://example.com/my.bzr/"))
        if location:
            self.open_location(location)

    def do_accessories(self):
        if self._accessories_dlg is None:
            self._accessories_dlg = \
                accessories_dialog.ExplorerAccessoriesDialog(self._accessories,
                    self.edit_file, self.reload_menus, self._preferences)
        self._accessories_dlg.show()
        self._accessories_dlg.raise_()
        self._accessories_dlg.activateWindow()

    def do_close(self):
        self.location_viewer.close_current()
        self.changed_view()

    def do_quit(self):
        self.close()

    ## Edit actions ##

    def do_preferences(self):
        if self._preferences_dlg is None:
            title = None
            if self.desktop_env.name() == 'windows':
                title = gettext("Options")
            self._preferences_dlg = ExplorerPreferencesDialog(
                self._preferences, self._preference_changed,
                self._toolbar_builder, title, self)
        self._preferences_dlg.show()
        self._preferences_dlg.raise_()
        self._preferences_dlg.activateWindow()

    ## View actions ##

    def refresh_toolbar(self):
        visible = self.ui.action_Toolbar.isChecked()
        self._toolbar_dock.setVisible(visible)

    def refresh_workingtree_browser(self):
        visible = self.ui.action_WorkingTree_Browser.isChecked()
        self._wt_browser.setVisible(visible)

    def refresh_toolbox(self):
        visible = self.ui.action_Toolbox.isChecked()
        self._toolbox.setVisible(visible)

    def refresh_status_bar(self):
        visible = self.ui.action_Status_Bar.isChecked()
        self.ui.statusbar.setVisible(visible)

    def do_full_refresh(self):
        self.do_refresh(reopen=True)

    def do_refresh(self, reopen=False):
        self._refresher.launch(reopen)  # shedule refresh in the background

    def _do_refresh(self, reopen=False):
        self.location_viewer.refresh_current(reopen=reopen)
        self.processEvents()
        # only refresh working tree if current view has one
        view = self.current_view()
        if view and view.has_wt():
            self._wt_browser.refresh()
            self.processEvents()
        self.changed_view()

    def refresh_diagnostic_mode(self):
        self.dry_run = self.ui.action_Diagnostic_Mode.isChecked()

    ## Bazaar actions ##

    def do_bzr_init(self):
        self.run_bzr_gui("init")

    def do_bzr_branch(self):
        self.run_bzr_gui("branch")

    def do_bzr_checkout(self):
        self.run_bzr_gui("checkout")

    def do_bzr_import(self):
        self.run_bzr_gui("import")

    def do_bzr_add(self):
        self.run_bzr_gui("add")

    def do_bzr_diff(self):
        self.run_bzr_gui("diff")

    def do_bzr_commit(self):
        self.run_bzr_gui("commit")

    def do_bzr_pull(self):
        self.run_bzr_gui("pull")

    def do_bzr_merge(self):
        self.run_bzr_gui("merge")

    def do_bzr_update(self):
        self.run_bzr_gui("update")

    def do_bzr_push(self):
        self.run_bzr_gui("push")

    def do_bzr_send(self):
        self.run_bzr_gui("send")

    def do_bzr_export(self):
        self.run_bzr_gui("export")

    def do_bzr_conflicts(self):
        self.run_bzr_gui("conflicts")

    def do_bzr_browse(self):
        self.run_bzr_gui("browse")

    def do_bzr_log(self):
        self.run_bzr_gui("log")

    def do_bzr_annotate(self):
        self.run_bzr_gui("annotate")

    def do_bzr_info(self):
        self.run_bzr_gui("info")

    def do_bzr_system_info(self):
        self.run_bzr_gui("version")

    def do_bzr_system_log(self):
        self.run_bzr_gui(".bzr.log")

    def do_bzr_rename(self):
        self.run_bzr_gui("rename")

    def do_bzr_delete(self):
        self.run_bzr_gui("delete")

    def do_bzr_switch(self):
        self.location_viewer.set_reopen_on_next_refresh()
        self.run_bzr_gui("switch")

    def do_bzr_bind(self):
        self.location_viewer.set_reopen_on_next_refresh()
        self.run_bzr_gui("bind")

    def do_bzr_unbind(self):
        self.location_viewer.set_reopen_on_next_refresh()
        self.run_bzr_gui("unbind")

    def do_bzr_revert(self):
        self.run_bzr_gui("revert")

    def do_bzr_uncommit(self):
        self.run_bzr_gui("uncommit")

    def do_bzr_shelve(self):
        self.run_bzr_gui("shelve")

    def do_bzr_unshelve(self):
        self.run_bzr_gui("unshelve")

    def do_bzr_view(self):
        self.run_bzr_gui("view")

    def do_bzr_tag(self):
        self.run_bzr_gui("tag")

    def do_bzr_plugins(self):
        self.run_bzr_gui("plugins")

    def do_bzr_advanced(self):
        # Use a preference to decide whether to run qrun or a terminal
        if self._preferences['advanced-commands-terminal']:
            self.open_terminal()
        else:
            self.run_bzr_gui("advanced")

    def do_bzr_ignore(self):
        self.run_bzr_gui("ignore")

    ## Bookmarks actions ##

    def do_bookmarks_add(self):
        view = self.current_view()
        title = view.display_name()
        bm = bookmarks.Bookmark(title, view.location, view.kind())
        # We always add bookmarks into the wallet (personal extensions)
        store = self._accessories.wallet.bookmarks()
        store.root().append(bm)
        store.save()
        self._refresh_bookmarks_menu()

    def do_bookmarks_edit_my(self):
        # Edit bookmarks in my wallet
        self.edit_file(self._accessories.wallet.bookmarks_path(),
             edit_callback=self.do_bookmarks_refresh)

    def do_bookmarks_refresh(self):
        for acc in self._accessories.items():
            acc.bookmarks().load()
        self._refresh_bookmarks_menu()

    ## Tools actions ##

    def do_add_tool(self):
        dlg = tool_dialogs.AddToolDialog(self)
        if dlg.exec_():
            tool = dlg.get_tool()
            store = self._accessories.wallet.tools()
            store.root().append(tool)
            store.save()
            self.do_tools_refresh()

    def do_tools_edit_my(self):
        # Edit tools in my wallet
        self.edit_file(self._accessories.wallet.tools_path(),
            edit_callback=self.do_tools_refresh)

    def do_tools_refresh(self):
        for acc in self._accessories.items():
            acc.tools().load()
        self._refresh_tools_menu()
        self._toolbox.rebuild()

    ## Settings actions ##

    def do_set_configuration(self):
        self.edit_config_file("bazaar.conf")

    def do_set_ignores(self):
        self.edit_config_file("ignore")

    def do_set_rules(self):
        self.edit_config_file("rules")

    def do_set_locations(self):
        self.edit_config_file("locations.conf")

    def do_set_credentials(self):
        self.edit_config_file("authentication.conf")

    def do_set_branch_configuration(self):
        self.edit_config_file("branch.conf")

    def do_set_working_tree_ignores(self):
        self.edit_config_file(".bzrignore")

    ## Help actions ##

    def do_help_contents(self):
        self.open_web_browser(_HELP_URL)

    def do_welcome(self):
        if self.location_viewer.select_view_for_key("welcome"):
            return

        # Build the location sets
        bookmark_stores = [p.bookmarks() for p in self._accessories.items()]
        bookmarks_browser = location_set_browser.BookmarksBrowser(
            bookmark_stores, self.open_saved_location)
        history_browser = location_set_browser.HistoryBrowser(
            self._history_manager, self.open_saved_location)
        self.location_viewer.locationAdded.connect(history_browser.refresh_view)
        location_sets = [
            (gettext("Bookmarks"), bookmarks_browser),
            (gettext("Recently Opened"), history_browser),
            ]

        model = welcome.WelcomeModel(gettext("Welcome"), self._do_view_action,
            location_sets, self.action_provider)
        self.location_viewer.add_location("welcome", model)
        self.changed_view()

    def do_report_a_problem(self):
        self.open_web_browser("https://bugs.launchpad.net/%s" % _LP_PROJECT)

    def do_translate_this_application(self):
        self.open_web_browser("https://translations.launchpad.net/%s" % \
            _LP_PROJECT)

    def do_get_help_online(self):
        self.open_web_browser("https://answers.launchpad.net/%s" % _LP_PROJECT)

    def do_about(self):
        from bzrlib import _format_version_tuple
        using_info = "QBzr %s, bzrlib %s, PyQt %s, Qt %s, Python %s" % (
            _format_version_tuple(qbzr.version_info),
            bzrlib.__version__,
            QtCore.PYQT_VERSION_STR,
            QtCore.QT_VERSION_STR,
            _format_version_tuple(sys.version_info))
        version = '%s&nbsp;&nbsp;<a href="%s">"%s"</a>' % \
            (_format_version_tuple(explorer.version_info), _CODE_NAME_URL,
                _CODE_NAME)
        tpl = {
            'app_name': self.app_name,
            'explorer_version': version,
            'project_url': "http://doc.bazaar.canonical.com/explorer/en/",
            'using_information': using_info,
            'purpose_text': ("Version Control for Human Beings"),
            'version_text': gettext("Version"),
            'copyright_text': gettext("Copyright"),
            'ian_url': "http://bazaarvcs.wordpress.com/2010/09/01/farewell-ian/",
        }
        about_title = gettext("About %s" % (self.app_name,))
        about_info = (
            u"<b>%(app_name)s</b> \u2014 %(purpose_text)s<br>"
            u"%(version_text)s %(explorer_version)s<br>"
            u"<br>"
            u"<small>%(using_information)s</small><br>"
            u"<hr />"
            u"<br>"
            u'Many thanks to <a href="%(ian_url)s">Ian Clatworthy</a> '
              u'for his ideas and design.<br>'
            u"<br>"
            u"%(copyright_text)s \u00A9 2009-2011 Canonical Ltd<br>"
            u"<br>"
            u'<a href="%(project_url)s">%(project_url)s</a>' % tpl)
        QtGui.QMessageBox.about(self, about_title, about_info)


class _Refresher(object):
    """Internal lock-like object to avoid refreshing too much."""

    def __init__(self, refresh_func):
        self.sheduled = False
        self.reopen = False
        self.refresh_func = refresh_func

    def launch(self, reopen=False):
        if not self.sheduled:
            self.sheduled = True
            self.reopen = reopen
            #print 'refresh sheduled (reopen=%s)' % reopen
            QtCore.QTimer.singleShot(1, self._refresh)
        else:
            #print 'refresh is already sheduled'
            pass

    def _refresh(self):
        #print 'refresh started'
        try:
            self.refresh_func(self.reopen)
        finally:
            self.sheduled = False
        #print 'refresh finished'
