tortoise/contextmenu.py
author TK Soh <teekaysoh@yahoo.com>
Sat, 01 Dec 2007 16:25:19 +0000
changeset 337 1f161ca182e3
parent 336 8c38569461b5
child 338 237daf1d4ffe
permissions -rw-r--r--
cmenu: use different menu for hgk/hgview

# Published under the GNU GPL, v2 or later.
# Copyright (C) 2007 Jelmer Vernooij <jelmer@samba.org>
# Copyright (C) 2007 Henry Ludemann <misc@hl.id.au>
# Copyright (C) 2007 TK Soh <teekaysoh@gmail.com>

import os
import tempfile
import pythoncom
from win32com.shell import shell, shellcon
import win32con
import win32process
import win32event
import win32ui
import win32gui
import win32gui_struct
import win32api
import _winreg
from mercurial import hg, ui, repo
from thgutil import *

S_OK = 0
S_FALSE = 1

def open_dialog(cmd, cmdopts='', cwd=None, root=None, filelist=[], title=None, notify=False):
    app_path = find_path("hgproc", get_prog_root(), '.EXE;.BAT')
    print "proc app = ", app_path

    if filelist:
        fd, tmpfile = tempfile.mkstemp(prefix="tortoisehg_filelist_")
        os.write(fd, "\n".join(filelist))
        os.close(fd)

    # start gpopen
    gpopts = "--command %s" % cmd
    if root:
        gpopts += " --root %s" % shellquote(root)
    if filelist:
        gpopts += " --listfile %s --deletelistfile" % (shellquote(tmpfile))
    if notify:
        gpopts += " --notify"
    if title:
        gpopts += " --title %s" % shellquote(title)
    if cwd:
        gpopts += " --cwd %s" % shellquote(cwd)

    cmdline = '%s %s -- %s' % (shellquote(app_path), gpopts, cmdopts)

    try:
        run_program(cmdline)
    except win32api.error, details:
        win32ui.MessageBox("Error executing command - %s" % (details), "gpopen")
    print "open_dialog: done"

def get_clone_repo_name(dir, repo_name):
    dest_clone = os.path.join(dir, repo_name)
    if os.path.exists(dest_clone):
        dest_clone = os.path.join(dir, "Clone of " + repo_name)

    i = 2
    while os.path.exists(dest_clone):
        dest_clone = os.path.join(dir, "Clone of (%s) %s" % (i, repo_name))
        i += 1
    return dest_clone

def run_program(cmdline):
    print "run_program: %s" % (cmdline)

    import subprocess
    pop = subprocess.Popen(cmdline, 
                           shell=False,
                           creationflags=win32con.CREATE_NO_WINDOW,
                           stderr=subprocess.STDOUT,
                           stdout=subprocess.PIPE,
                           stdin=subprocess.PIPE)
    print "run_program: done"
    
"""Windows shell extension that adds context menu items to Mercurial repository"""
class ContextMenuExtension:
    _reg_progid_ = "Mercurial.ShellExtension.ContextMenu"
    _reg_desc_ = "Mercurial Shell Extension"
    _reg_clsid_ = "{EEE9936B-73ED-4D45-80C9-AF918354F885}"
    _com_interfaces_ = [shell.IID_IShellExtInit, shell.IID_IContextMenu]
    _public_methods_ = [
        "Initialize", # From IShellExtInit
        "QueryContextMenu", "InvokeCommand", "GetCommandString" # IContextMenu
        ]

    registry_keys = [
        (_winreg.HKEY_CLASSES_ROOT, r"*\shellex\ContextMenuHandlers\TortoiseHg"),
        (_winreg.HKEY_CLASSES_ROOT, r"Directory\Background\shellex\ContextMenuHandlers\TortoiseHg"),
        (_winreg.HKEY_CLASSES_ROOT, r"Directory\shellex\ContextMenuHandlers\TortoiseHg"),
        (_winreg.HKEY_CLASSES_ROOT, r"Folder\shellex\ContextMenuHandlers\TortoiseHg"),
        (_winreg.HKEY_CLASSES_ROOT, r"Directory\shellex\DragDropHandlers\TortoiseHg"),
        (_winreg.HKEY_CLASSES_ROOT, r"Folder\shellex\DragDropHandlers\TortoiseHg"),
        ]

    def __init__(self):
        print "ContextMenuExtension: __init__ called"
        self._folder = None
        self._filenames = []
        self._handlers = {}

    def Initialize(self, folder, dataobj, hkey):
        print "Initialize: cwd = ", os.getcwd()
        if folder:
            self._folder = shell.SHGetPathFromIDList(folder)
            print "folder = ", self._folder

        if dataobj:
            format_etc = win32con.CF_HDROP, None, 1, -1, pythoncom.TYMED_HGLOBAL
            sm = dataobj.GetData(format_etc)
            num_files = shell.DragQueryFile(sm.data_handle, -1)
            for i in range(num_files):
                self._filenames.append(shell.DragQueryFile(sm.data_handle, i))

    def create_submenu(self, commands, idCmd, idCmdFirst):
        menu = win32gui.CreatePopupMenu()
        for menu_info in commands:
            fstate = win32con.MF_BYCOMMAND
            if len(menu_info) == 0:
                win32gui.InsertMenu(menu, idCmd, 
                        win32con.MF_BYPOSITION|win32con.MF_SEPARATOR, 
                        idCmdFirst + idCmd, None)
                idCmd += 1
                continue
            elif len(menu_info) == 2:
                text, subcommands = menu_info
                submenu, idCmd = self.create_submenu(subcommands, idCmd, idCmdFirst)
                item, _ = win32gui_struct.PackMENUITEMINFO(
                        text=text, hSubMenu=submenu, wID=idCmdFirst + idCmd)
                win32gui.InsertMenuItem(menu, idCmdFirst + idCmd, True, item)
                self._handlers[idCmd] = ("", lambda x,y: 0)
                idCmd += 1
                continue
            elif len(menu_info) == 4:
                text, help_text, command, enabled = menu_info
                if not enabled: fstate |= win32con.MF_GRAYED
            elif len(menu_info) == 3:
                text, help_text, command = menu_info
                
            item, _ = win32gui_struct.PackMENUITEMINFO(
                        text=text, fState=fstate, wID=idCmdFirst + idCmd)
            win32gui.InsertMenuItem(menu, idCmdFirst+idCmd, True, item)
            self._handlers[idCmd] = (help_text, command)
            idCmd += 1
        return (menu, idCmd)

    def QueryContextMenu(self, hMenu, indexMenu, idCmdFirst, idCmdLast, uFlags):
        if uFlags & shellcon.CMF_DEFAULTONLY:
            return 0

        # only support Overlays In Explorer
        print "QueryContextMenu: checking if in explorer"
        modname = win32api.GetModuleFileName(win32api.GetModuleHandle(None))
        print "modname = %s" % modname
        if not modname.lower().endswith("\\explorer.exe"):
            print "QueryContextMenu: not in explorer"
            return 0 

        # As we are a context menu handler, we can ignore verbs.
        self._handlers = {}
        if self._folder and self._filenames:
            commands = self._get_commands_dragdrop()
        else:
            commands = self._get_commands()
        idCmd = 0
        if len(commands) > 0:
            # a brutal hack to detect if we are the first menu to go on to the 
            # context menu. If we are not the first, then add a menu separator
            # The number '30000' is just a guess based on my observation
            print "idCmdFirst = ", idCmdFirst
            if idCmdFirst >= 30000:
                win32gui.InsertMenu(hMenu, indexMenu,
                                    win32con.MF_SEPARATOR|win32con.MF_BYPOSITION,
                                    idCmdFirst+idCmd, None)
                indexMenu += 1
                idCmd += 1
            
            # create submenus with Hg commands
            submenu, idCmd = self.create_submenu(commands, idCmd, idCmdFirst)

            # add Hg submenus to context menu
            opt = {'text': "TortoiseHg", 'hSubMenu': submenu, 'wID': idCmdFirst+idCmd}
            icon_path = get_icon_path("tortoise", "hg.ico")
            print "icon path =", icon_path
            if icon_path:
                opt['hbmpChecked'] = opt['hbmpUnchecked'] = \
                                     icon_to_bitmap(icon_path, type="MENUCHECK")
            item, extras = win32gui_struct.PackMENUITEMINFO(**opt)
            win32gui.InsertMenuItem(hMenu, indexMenu, True, item)
            
            # a submenu item need a (dummay) handler too 
            self._handlers[idCmd] = ("", lambda x,y: 0)
            
            indexMenu += 1
            idCmd += 1

            # menu separator
            win32gui.InsertMenu(hMenu, indexMenu,
                                win32con.MF_SEPARATOR|win32con.MF_BYPOSITION,
                                idCmdFirst+idCmd, None)
            indexMenu += 1
            idCmd += 1

        # Return total number of menus & submenus we've added
        return idCmd

    def _get_commands_dragdrop(self):
        """
        Get a list of commands valid for the current selection.

        Each command is a tuple containing (display text, handler).
        """
        
        print "_get_commands_dragdrop() on %s" % ", ".join(self._filenames)        

        # we can only accept dropping one item
        if len(self._filenames) > 1:
            return []

        def _open_repo(path):
            u = ui.ui()
            root = find_root(path)
            if root:
                try:
                    repo = hg.repository(u, path=root)
                    return repo
                except repo.RepoError:
                    pass

            return None

        # open repo
        drag_repo = None
        drop_repo = None
        
        print "drag = %s" % self._filenames[0]
        print "drop = %s" % self._folder
        
        drag_path = self._filenames[0]
        drag_repo = _open_repo(drag_path)
        if not drag_repo:
            return []
        if drag_repo and drag_repo.root != drag_path:
            return []   # dragged item must be a hg repo root directory
        print "drag root = %s" % drag_repo.root

        drop_repo = _open_repo(self._folder)

        print "_get_commands_dragdrop(): adding hg commands"
        
        result = []
        result.append((_("Create Clone"), 
                       _("Create clone here from source"),
                       self._clone_here))

        if drop_repo:
            print "_get_commands_dragdrop(): drop zone is a hg repo too"
            print "drop root = %s" % drag_repo.root
            result.append((_("Push to"), 
                           _("Push source into the repo here"),
                           self._push_here))
            result.append((_("Pull from"), 
                           _("Pull new change from dragged repo"),
                           self._pull_here))
            result.append((_("Incoming"), 
                           _("show new changesets found in source"),
                           self._incoming_here))
            result.append((_("Outgoing"), 
                           _("show changesets not found in destination"),
                           self._outgoing_here))
        return result
        
    def _get_commands(self):
        """
        Get a list of commands valid for the current selection.

        Each command is a tuple containing (display text, handler).
        """
        
        print "_get_commands() on %s" % ", ".join(self._filenames)        

        # open repo
        result = []
        tree = None
        u = ui.ui()
        rpath = self._folder or self._filenames[0]
        root = find_root(rpath)
        if root is None:
            print "%s: not in repo" % rpath
            result.append((_("Create repo here"),
                           _("create a new repository in this directory"),
                           self._init))
            result.append((_("Clone a repository"),
                           _("clone a repository"),
                           self._clone))
            return result

        print "file = %s\nroot = %s" % (rpath, root)
        
        try:
            tree = hg.repository(u, path=root)
        except repo.RepoError:
            print "%s: can't open repo" % dir
            return []

        print "_get_commands(): adding hg commands"
        
        if tree is not None:
            # Commit (qct, gcommit, or internal)
            result.append((_("Commit"), 
                           _("Commit changes with GUI tool"),
                           self._commit))

            # Working directory status (gstatus, internal)
            result.append((_("Status"),
                           _("Repository status"),
                           self._status))

            # Mercurial standard commands
            result.append((_("Diff"),
                           _("View changes"),
                           self._diff))

            # Visual Diff (any extdiff command)
            result.append((_("Visual diff"),
                           _("View changes using GUI diff tool"),
                           self._vdiff))
                           
            result.append((_("Add"),
                           _("Add files to Hg repository"),
                           self._add))
            result.append((_("Remove"),
                           _("Remove selected files on the next commit"),
                           self._remove))
            result.append((_("Revert"),
                           _("Revert selected files"),
                           self._revert))

            result.append([])   # separator

            result.append((_("Rollback"),
                           _("Rollback the last transaction"),
                           self._rollback))
            result.append((_("Recover"),
                           _("Recover from an interrupted commit or pull"),
                           self._recover))
            result.append((_("Verify"),
                           _("Verify repository consistency"),
                           self._verify))
            result.append((_("Update"),
                           _("update working directory"),
                           self._update))
            result.append((_("Merge"),
                           _("merge working directory with another revision"),
                           self._merge))

            result.append([])   # separator

            # Visual history (hgk, hgview, glog, or internal)
            result.append((_("View history"),
                           _("View revision history"),
                           self._history))
            result.append((_("Revision graph"),
                           _("View history with DAG graph"),
                           self._view))
            result.append((_("Current revision status..."),
                           _("Show various revision info"),
                           self._tip))

            result.append([])   # separator

            result.append((_("View Tags"),
                           _("list repository tags"),
                           self._show_tags))
            result.append((_("Add Tag"),
                           _("add a tag for the current or given revision"),
                           self._add_tag))

            result.append([])   # separator

            result.append((_("Clone"),
                           _("Clone a repository"),
                           self._clone))
            result.append((_("Pull"),
                           _("Pull from default repository"),
                           self._pull))
            result.append((_("Push"),
                           _("Push to default repository"),
                           self._push))
            result.append((_("Incoming"),
                           _("show new changesets found in source"),
                           self._incoming))
            result.append((_("Outgoing"),
                           _("show changesets not found in destination"),
                           self._outgoing))
            result.append((_("Web server"),
                           _("start web server for this repository"),
                           self._serve))

            result.append([])   # separator

            # Optionally add an Options submenu
            c = ui.ui().config('tortoisehg', 'hgconfig', None)
            if c in ['1', 'yes', 'True']:
                config = []
                config.append((_("Username"),
                    _("Configure username"),
                    self._uname))
                config.append((_("Paths"),
                    _("Configure remote paths"),
                    self._paths))
                config.append((_("Web"),
                    _("Configure repository web data"),
                    self._web))
                result.append((_("Options"), config))
                result.append([])   # separator

            result.append((_("Help"),
                           _("Basic Mercurial help text"),
                           self._help))

        return result

    def InvokeCommand(self, ci):
        mask, hwnd, verb, params, dir, nShow, hotkey, hicon = ci
        if verb >> 16:
            # This is a textual verb invocation... not supported.
            return S_FALSE
        if verb not in self._handlers:
            raise Exception("Unsupported command id %i!" % verb)
        self._handlers[verb][1](hwnd)

    def GetCommandString(self, cmd, uFlags):
        if uFlags & shellcon.GCS_VALIDATEA or uFlags & shellcon.GCS_VALIDATEW:
            if cmd in self._handlers:
                return S_OK
            return S_FALSE
        if uFlags & shellcon.GCS_VERBA or uFlags & shellcon.GCS_VERBW:
            return S_FALSE
        if uFlags & shellcon.GCS_HELPTEXTA or uFlags & shellcon.GCS_HELPTEXTW:
            # The win32com.shell implementation encodes the resultant
            # string into the correct encoding depending on the flags.
            return self._handlers[cmd][0]
        return S_FALSE

    def _commit(self, parent_window):
        '''[tortoisehg] commit = [qct | gcommit | internal]'''
        ct = ui.ui().config('tortoisehg', 'commit', 'internal')
        if ct == 'internal':
            self._commit_simple(parent_window)
            return
        hgpath = find_path('hg')
        if hgpath:
            targets = self._filenames or [self._folder]
            root = find_root(targets[0])
            cmd = "%s --repository %s %s" % \
                    (shellquote(hgpath), shellquote(root), ct)
            run_program(cmd)

    def _uname(self, parent_window):
        hgpath = find_path('hg')
        if not hgpath: return
        targets = self._filenames or [self._folder]
        root = find_root(targets[0])
        cmd = "%s --repository %s config username" % (shellquote(hgpath), 
                shellquote(root))
        run_program(cmd)

    def _paths(self, parent_window):
        hgpath = find_path('hg')
        if not hgpath: return
        targets = self._filenames or [self._folder]
        root = find_root(targets[0])
        cmd = "%s --repository %s config paths" % (shellquote(hgpath), 
                shellquote(root))
        run_program(cmd)

    def _web(self, parent_window):
        hgpath = find_path('hg')
        if not hgpath: return
        targets = self._filenames or [self._folder]
        root = find_root(targets[0])
        cmd = "%s --repository %s config web" % (shellquote(hgpath), 
                shellquote(root))
        run_program(cmd)

    def _vdiff(self, parent_window):
        '''[tortoisehg] vdiff = <any extdiff command>'''
        diff = ui.ui().config('tortoisehg', 'vdiff', None)
        if not diff:
            msg = "You must configure tortoisehg.vdiff in your Mercurial.ini"
            title = "Visual Diff Not Configured"
            win32ui.MessageBox(msg, title, win32con.MB_OK|win32con.MB_ICONERROR)
            return
        hgpath = find_path('hg')
        if hgpath:
            targets = self._filenames or [self._folder]
            root = find_root(targets[0])
            quoted_files = [shellquote(s) for s in targets]
            cmd = "%s --repository %s %s %s" %  \
                (shellquote(hgpath), shellquote(root),
                   diff, " ".join(quoted_files))
            run_program(cmd)

    def _view(self, parent_window):
        '''[tortoisehg] view = [hgk | hgview]'''
        view = ui.ui().config('tortoisehg', 'view', '')
        if not view:
            msg = "You must configure tortoisehg.view in your Mercurial.ini"
            title = "Revision Graph Tool Not Configured"
            win32ui.MessageBox(msg, title, win32con.MB_OK|win32con.MB_ICONERROR)
            return

        targets = self._filenames or [self._folder]
        root = find_root(targets[0])
        if view == 'hgview':
            hgviewpath = find_path('hgview')
            cmd = "%s --repository=%s" % \
                    (shellquote(hgviewpath), shellquote(root))
            if len(self._filenames) == 1:
                cmd += " --file=%s" % shellquote(self._filenames[0])
            run_program(cmd)
        else:
            hgpath = find_path('hg')
            if not hgpath: return
            if view == 'hgk':
                cmd = "%s --repository %s view" % \
                        (shellquote(hgpath), shellquote(root))
                run_program(cmd)
            else:
                msg = "Revision graph viewer %s not recognized" % view
                title = "Unknown history tool"
                win32ui.MessageBox(msg, title, win32con.MB_OK|win32con.MB_ICONERROR)

    def _history(self, parent_window):
        '''[tortoisehg] log = [glog | internal]'''
        log = ui.ui().config('tortoisehg', 'log', 'internal')
        targets = self._filenames or [self._folder]
        root = find_root(targets[0])
        if log == 'internal':
            self._log(parent_window)
        else:
            hgpath = find_path('hg')
            if not hgpath: return
            if log == 'glog':
                quoted_files = [shellquote(s) for s in targets]
                cmd = "%s --repository %s glog %s" % \
                        (shellquote(hgpath), shellquote(root),
                         " ".join(quoted_files))
                run_program(cmd)
            else:
                msg = "History viewer %s not recognized" % log
                title = "Unknown history tool"
                win32ui.MessageBox(msg, title, win32con.MB_OK|win32con.MB_ICONERROR)

    def _clone_here(self, parent_window):
        src = self._filenames[0]
        dest = self._folder
        repo_name = os.path.basename(src)
        dest_clone = get_clone_repo_name(dest, repo_name)
        cmdopts = "--verbose"
        repos = [src, dest_clone]
        open_dialog('clone', cmdopts, cwd=dest, filelist=repos)

    def _push_here(self, parent_window):
        src = self._filenames[0]
        dest = self._folder
        msg = "Push changes from %s into %s?" % (src, dest)
        title = "Mercurial: push"
        rv = win32ui.MessageBox(msg, title, win32con.MB_OKCANCEL)
        if rv == 2:
            return

        cmdopts = "--verbose"
        open_dialog('push', cmdopts, root=src, filelist=[dest])

    def _pull_here(self, parent_window):
        src = self._filenames[0]
        dest = self._folder
        msg = "Pull changes from %s?" % (src)
        title = "Mercurial: pull"
        rv = win32ui.MessageBox(msg, title, win32con.MB_OKCANCEL)
        if rv == 2:
            return

        cmdopts = "--verbose"
        open_dialog('pull', cmdopts, root=src, filelist=[dest])

    def _incoming_here(self, parent_window):
        src = self._filenames[0]
        dest = self._folder
        cmdopts = "--verbose"
        open_dialog('incoming', cmdopts, root=src, filelist=[dest])

    def _outgoing_here(self, parent_window):
        src = self._filenames[0]
        dest = self._folder
        cmdopts = "--verbose"
        open_dialog('outgoing', cmdopts, root=src, filelist=[dest])

    def _init(self, parent_window):
        dest = self._folder or self._filenames[0]
        msg = "Create Hg repository in %s?" % (dest)
        title = "Mercurial: init"
        rv = win32ui.MessageBox(msg, title, win32con.MB_OKCANCEL)
        if rv == 2:
            return
        try:
            hg.repository(ui.ui(), dest, create=1)
        except:
            msg = "Error creating repo"
            win32ui.MessageBox(msg, title, 
                               win32con.MB_OK|win32con.MB_ICONERROR)
            
    def _status(self, parent_window):
        '''[tortoisehg] status = [gstatus | internal]'''
        stat = ui.ui().config('tortoisehg', 'status', 'internal')
        if stat == 'internal':
            self._run_dialog('status')
        elif stat == 'gstatus':
            targets = self._filenames or [self._folder]
            root = find_root(targets[0])
            quoted_files = [shellquote(s) for s in targets]
            hgpath = find_path('hg')
            cmd = "%s --repository %s gstatus %s" % \
                    (shellquote(hgpath), shellquote(root),
                     " ".join(quoted_files))
            run_program(cmd)
        else:
            msg = "Status viewer %s not recognized" % stat
            title = "Unknown status tool"
            win32ui.MessageBox(msg, title, win32con.MB_OK|win32con.MB_ICONERROR)

    def _clone(self, parent_window):
        self._run_dialog('clone', True)

    def _pull(self, parent_window):
        self._run_dialog('pull', True)

    def _push(self, parent_window):
        self._run_dialog('push', True)

    def _incoming(self, parent_window):
        self._run_dialog('incoming', True)

    def _outgoing(self, parent_window):
        self._run_dialog('outgoing', True)

    def _serve(self, parent_window):
        self._run_dialog('serve', noargs=True)

    def _add(self, parent_window):
        self._run_dialog('add', modal=True)

    def _remove(self, parent_window):
        self._run_dialog('remove')

    def _revert(self, parent_window):
        self._run_dialog('status')

    def _tip(self, parent_window):
        self._run_dialog('tip', True)

    def _parents(self, parent_window):
        self._run_dialog('parents', True)

    def _heads(self, parent_window):
        self._run_dialog('heads', True)

    def _log(self, parent_window):
        self._run_dialog('log', verbose=False)

    def _show_tags(self, parent_window):
        self._run_dialog('tags', True, verbose=False)

    def _add_tag(self, parent_window):
        self._run_dialog('tag', True, verbose=False)

    def _diff(self, parent_window):
        self._run_dialog('diff')

    def _merge(self, parent_window):
        self._run_dialog('merge', noargs=True)

    def _rollback(self, parent_window):
        targets = self._filenames or [self._folder]
        root = find_root(targets[0])
        msg = "Confirm rollback: %s" % root
        title = "Mercurial: rollback"
        rv = win32ui.MessageBox(msg, title, win32con.MB_OKCANCEL)
        if rv == 1:
            self._run_dialog('rollback', noargs=True)

    def _recover(self, parent_window):
        targets = self._filenames or [self._folder]
        root = find_root(targets[0])
        msg = "Confirm recovering %s?" % root
        title = "Mercurial: recover"
        rv = win32ui.MessageBox(msg, title, win32con.MB_OKCANCEL)
        if rv == 1:
            self._run_dialog('recover', noargs=True)

    def _verify(self, parent_window):
        self._run_dialog('verify', noargs=True)

    def _commit_simple(self, parent_window):
        self._run_dialog('commit')

    def _update(self, parent_window):
        self._run_dialog('update', noargs=True)

    def _run_dialog(self, hgcmd, noargs=False, verbose=True, modal=False):
        if self._folder:
            cwd = self._folder
        elif self._filenames:
            f = self._filenames[0]
            cwd = os.path.isdir(f) and f or os.path.dirname(f)
        else:
            win32ui.MessageBox("Can't get cwd!", 'Hg ERROR', 
                   win32con.MB_OK|win32con.MB_ICONERROR)
            return

        targets = self._filenames or [self._folder]
        root = find_root(targets[0])
        filelist = []
        if noargs == False:
            filelist = targets
        cmdopts = "%s" % (verbose and "--verbose" or "")
        print "_run_program_dialog: cmdopts = ", cmdopts
        title = "Hg %s" % hgcmd
        open_dialog(hgcmd, cmdopts, cwd=cwd, root=root, filelist=filelist)

    def _help(self, parent_window):
        open_dialog('help', '--verbose')