"""   QuArK  -  Quake Army Knife

Core of the Map and Model editors.
"""
#
# Copyright (C) 1996-99 Armin Rigo
# THIS FILE IS PROTECTED BY THE GNU GENERAL PUBLIC LICENCE
# FOUND IN FILE "COPYING.TXT"
#


#
# See comments in file mapeditor.py.
#


import qmenu
import qtoolbar
import qhandles
import qmacro

import qbasemgr
from qeditor import *
from qdictionnary import Strings


def drawview(view,mapobj,mode=0):
        #
        # tig: does the drawing, for later redefinition
        #
        view.drawmap(mapobj, mode)

class BaseEditor:

    MouseDragMode = None

    def __init__(self, form):
        "Called when there is a map/model to display."
        # debug("MapEditor opens")
        self.form = form
        form.info = self
        self.layout = None
        self.dragobject = None
        self.Root = None
        self.TexSource = None
        #self.drawmode = <from setupchanged()>
        #self.grid = <from setupchanged()>
        #self.gridstep = <from setupchanged()>
        self.lastscale = 0
        self.setupchanged(None)
        self.ReopenRoot(form)
        self.setupchanged1 = (self.setupchanged,)
        apply(SetupRoutines.append, self.setupchanged1)

   # def __del__(self):
   #     debug("MapEditor closes")

    def ReopenRoot(self, form):
        self.gamecfg = quarkx.setupsubset().shortname
        self.texflags = quarkx.setupsubset()["Q2TexFlags"]
        self.fileobject = form.fileobject
        self.Root = None
        self.TexSource = None
        self.OpenRoot()
        if self.layout is None:
            nlayoutname = quarkx.setupsubset(self.MODE, "Layouts")["_layout"]
            list = self.manager.LayoutsList[:]
            list.reverse()
            for layouts in list:
                if layouts.shortname == nlayoutname:
                    break
        else:
            layouts = self.layout.__class__
        self.setlayout(form, layouts())


    def drawmap(self, view):
        "Draws the map/model on the given view."

        #
        # Stop any pending timer that would cause this view to be redrawn later.
        #
        try:
            view.info["timer"]   # check the presence of the "timer" attribute
            quarkx.settimer(qbasemgr.RefreshView, view, 0)
            qbasemgr.RefreshView(view)   # re-invalidate the whole view
            return
        except:
            pass

        #
        # First read the view's scale.
        #

        scale1 = self.lastscale
        if scale1<=0: scale1=1.0
        if view.info["type"]!="3D":
            try:
                scale1 = view.info["scale"]
            except KeyError:
                pass

        #
        # If the scale has just changed, we must rebuild the handles
        # because some handles depend on the same, like the face normal
        # handle, whose length vary in 3D space so that it always
        # seems to be on the same length on the 2D view.
        #

        if scale1 != self.lastscale:
            self.lastscale = scale1
            self.buildhandles()

        #
        # Define the functions that draw the axis and the grid
        #

        setup = quarkx.setupsubset(self.MODE, "Display")

        if view.viewmode == "wire":
            def DrawAxis(setup=setup, view=view, MODE=self.MODE):
                X, Y, Z = setup["MapLimit"]
                ax = []
                if MapOption("DrawAxis", MODE):
                    ax.append((-X, 0, 0,  X, 0, 0))
                    ax.append(( 0,-Y, 0,  0, Y, 0))
                    ax.append(( 0, 0,-Z,  0, 0, Z))
                if view.info["type"]!="3D" and MapOption("DrawMapLimit", MODE):
                    # this big "map-limits" cube looks bad in perspective views
                    ax.append((-X,-Y,-Z,  X,-Y,-Z))
                    ax.append((-X,-Y, Z,  X,-Y, Z))
                    ax.append((-X, Y,-Z,  X, Y,-Z))
                    ax.append((-X, Y, Z,  X, Y, Z))
                    ax.append((-X,-Y,-Z, -X, Y,-Z))
                    ax.append((-X,-Y, Z, -X, Y, Z))
                    ax.append(( X,-Y,-Z,  X, Y,-Z))
                    ax.append(( X,-Y, Z,  X, Y, Z))
                    ax.append((-X,-Y,-Z, -X,-Y, Z))
                    ax.append((-X, Y,-Z, -X, Y, Z))
                    ax.append(( X,-Y,-Z,  X,-Y, Z))
                    ax.append(( X, Y,-Z,  X, Y, Z))
                if ax:
                    cv = view.canvas()
                    cv.pencolor = MapColor("Axis", MODE)
                    for x1,y1,z1,x2,y2,z2 in ax:
                        p1 = view.proj(x1,y1,z1)
                        p2 = view.proj(x2,y2,z2)
                        cv.line(p1, p2)
        else:
            def DrawAxis():
                pass

        solidgrid = MapOption("SolidGrid", self.MODE)
        def DrawGrid(self=self, setup=setup, solidgrid=solidgrid, view=view):
            if MapOption("GridVisible", self.MODE) and self.gridstep:
                # Note: QuArK does not draw grids on perspective views currently.
                try:
                    highlight = int(setup["GridHighlight"])
                except:
                    highlight = 0

                gs = self.gridstep
                diff = setup["GridMinStep"][0] / (gs*self.lastscale)
                if diff>1:
                    if diff*diff*diff > highlight:
                        gs = 0
                    mode = DG_ONLYHIGHLIGHTED
                else:
                    mode = 0

                if gs:
                    if view.viewmode == "wire":
                        viewcolor = view.color
                        if solidgrid:
                            mode = mode | DG_LINES
                            gridcol = MapColor("GridLines", self.MODE)
                        else:
                            if viewcolor == MapColor("ViewXZ", self.MODE):
                                gridcol = MapColor("GridXZ", self.MODE)
                            else:
                                gridcol = MapColor("GridXY", self.MODE)
                    else:
                        if solidgrid:
                            mode = mode | DG_LINES
                        gridcol = 0x555555
                        viewcolor = 0
                    mode = mode + highlight
                    gridhcol = quarkx.middlecolor(gridcol, viewcolor, setup["GridHFactor"][0])
                    # print gridcol, gridhcol
                    nullvect = view.vector('0')
                    zero = view.proj(nullvect)
                    xyz = [(view.proj(quarkx.vect(gs,0,0))-zero),
                           (view.proj(quarkx.vect(0,gs,0))-zero),
                           (view.proj(quarkx.vect(0,0,gs))-zero)]
                    Grids = []
                    for i in (0,1,2):
                        f = abs(xyz[i].normalized.z)   # between 0 (plane viewed exactly from side) and 1 (plane viewed exactly from front)
                        if f >= 0.1:
                            Grids.append((f, i))
                    Grids.sort()
                    for f, i in Grids:
                        view.drawgrid(xyz[i-2], xyz[i-1], quarkx.middlecolor(gridcol, viewcolor, f), mode, quarkx.middlecolor(gridhcol, viewcolor, f))

        #
        # Draw the axis and the grid in the correct order
        #

        if not solidgrid: DrawAxis()

        if view.viewmode == "wire":
            DrawGrid()

        if solidgrid: DrawAxis()

        #
        # Call the layout to update the map view limits, i.e. the
        # limits below and after which the map is grayed out.
        #

        self.layout.drawing(view)

        #
        # Fill the background of the selected object
        #

        ex = self.layout.explorer
        fs = ex.focussel
        if (fs is not None) and (view.viewmode == "wire"):
            mode = self.drawmode | DM_BACKGROUND
            if MapOption("BBoxSelected", self.MODE): mode=mode|DM_BBOX
            self.ObjectMgr.im_func("drawback", fs, self, view, mode)

        #
        # Draw the map
        #

        mode = self.drawmode
        if MapOption("BBoxAlways", self.MODE): mode=mode|DM_BBOX
        if self.Root.selected:
            #
            # tiglari asks:  is this the right technique?
            #
            #view.drawmap(self.Root, mode)     # draw the whole map in normal back lines
            drawview(view, self.Root, mode)
        else:
            #
            # Draw the unselected items first
            #
            #view.drawmap(self.Root, mode | DM_DONTDRAWSEL)     # draw the map in back lines, don't draw selected items
            drawview(view, self.Root, mode | DM_DONTDRAWSEL)
            #
            # Then the selected ones over them
            #
            mode = self.drawmode
            if MapOption("BBoxSelected", self.MODE): mode=mode|DM_BBOX
            list = ex.sellist
            if len(list)==1:
                self.ObjectMgr.im_func("drawsel", list[0], view, mode)
            else:
                for sel in list:    # draw the selected objects in "highlight" white-and-black lines
                    view.drawmap(sel, mode | DM_SELECTED, view.setup.getint("SelMultColor"))

        #
        # Send the above drawed map items to the 3D renderer
        #

        if view.viewmode != "wire":
            view.solidimage(self.TexSource)  # in case of solid or textured view, this computes and draws the full solid or textured image
            if MapOption("GridVisibleTex", self.MODE):
                DrawGrid()

        #
        # Additionnal drawings will appear in wireframe over the solid or texture image.
        # In our case, we simply draw the selected objects again.
        #

        if (fs is not None) and (view.viewmode != "wire"):
            mode = self.drawmode
            if MapOption("BBoxSelected", self.MODE): mode=mode|DM_BBOX
            self.ObjectMgr.im_func("drawback", fs, self, view, mode)
        self.finishdrawing(view)


    def finishdrawing(self, view):
        "Additionnal map view drawings, e.g. handles."

        #
        # Which handle is the user currently dragging ?
        #

        if self.dragobject is None:
            draghandle = None
        else:
            draghandle = self.dragobject.handle

        #
        # Draw all handles.
        #

        cv = view.canvas()
        for h in view.handles:
            h.draw(view, cv, draghandle)

        #
        # Draw the red wireframe image.
        #

        if self.dragobject is not None:
            self.dragobject.drawredimages(view)


    def drawmapsel(self, view):     # draw the selection only (for the 3D view page in the multi-pages-panel)
        ex = self.layout.explorer
        for sel in ex.sellist:
            view.drawmap(sel)
        view.solidimage(self.TexSource)
        self.finishdrawing(view)


    def CloseRoot(self):
        pass

    def onclose(self, form):
        "Called when the map/model editor is closed."
        if self.setupchanged1[0] in SetupRoutines:
            apply(SetupRoutines.remove, self.setupchanged1)
        self.setupchanged1 = (None, )
        if self.layout is not None:
            quarkx.setupsubset(self.MODE, "Layouts")["_layout"] = self.layout.shortname
        self.setlayout(form, None)
        self.form = None
        self.CloseRoot()
        self.Root = None
        # self.savesetupinfos()
        self.fileobject = None
        self.dragobject = None
        form.info = None


    def setupview(self, v, drawmap=None, flags=MV_AUTOFOCUS, copycol=1):
        "To be called at least once for each map view."

        if drawmap is None:
            drawmap = self.drawmap     # method to draw the map view

        def draw1(view, self=self, drawmap=drawmap):
            if self.dragobject is not None:
                obj, backup = self.dragobject.backup()
            else:
                backup = None
            try:
                drawmap(view)
            finally:
                if backup is not None:
                    obj.copyalldata(backup)

        v.ondraw = draw1
        v.onmouse = self.mousemap
        v.ondrop = self.dropmap
        v.flags = v.flags | flags
        self.lastscale = 0    # force a handle rebuild
        if copycol and (self.layout is not None) and len(self.layout.views):
            copyfrom = self.layout.views[0]
            v.color = copyfrom.color
            v.darkcolor = copyfrom.darkcolor
        if MapOption("CrossCursor", self.MODE):
            v.cursor = CR_CROSS
            v.handlecursor = CR_ARROW
        else:
            v.cursor = CR_ARROW
            v.handlecursor = CR_CROSS


    def setlayout(self, form, nlayout):
        "Assigns a new layout to the map/model editor."

        form.mainpanel.hide()
        self.clearrefs(form)
        if self.layout is not None:
            self.layout.destroyscreen(form)
        self.layout = nlayout
        if nlayout is not None:
            nlayout.editor = self
            nlayout.buildscreen(form)
            if nlayout.explorer is None:
                raise "Invalid layout, missing Explorer"
            nlayout.explorer.onselchange = self.explorerselchange
            nlayout.explorer.onrootchange = self.explorerrootchange
            nlayout.explorer.onmenu = self.explorermenu
            nlayout.explorer.ondrop = self.explorerdrop
            nlayout.explorer.oninsert = self.explorerinsert
            nlayout.setupchanged(None)
            self.lastscale = 0    # force a call to buildhandles()
            if self.Root is not None:
                for v in nlayout.views:
                    self.setupview(v, copycol=0)
                nlayout.explorer.addroot(self.Root)
            nlayout.updateviewproj()
            nlayout.postinitviews()
            if not self.lockviews:
                nlayout.UnlockViews()
            self.initmenu(form)
        else:
            form.menubar = []
            form.shortcuts = {}
            form.numshortcuts = {}
            quarkx.update(form)
        form.mainpanel.show()
        #if nlayout is not None:
        #    for v in nlayout.views:
        #        if v.info["type"] != "3D":
        #             v.screencenter = quarkx.vect(0,0,0)



    def setlayoutclick(self, m):
        "Called by the last items of the 'Layout' menu."
        self.setlayout(quarkx.clickform, m.layout())


    def clearrefs(self, form):
        for name,dlg in qmacro.dialogboxes.items():
            if dlg.owner == form:
                del qmacro.dialogboxes[name]
                dlg.close()

    def setupchanged(self, level):
        "Update the setup-dependant parameters."
        setup = quarkx.setupsubset(self.MODE, "Display")
        qhandles.lengthnormalvect, = setup["NormalVector"]
        self.gridstep, = setup["GridStep"]
        if MapOption("GridActive", self.MODE):
            self.grid = self.gridstep
        else:
            self.grid = 0
        self.drawmode = setup.getint("ViewMode")
        if MapOption("ComputePolys", self.MODE):
            self.drawmode = self.drawmode | DM_COMPUTEPOLYS
        self.linearbox = not (not MapOption("LinearBox", self.MODE))
        self.lockviews = not MapOption("UnlockViews", self.MODE)
        setup = quarkx.setupsubset(self.MODE, "Colors")
        c = setup["InvertedColors"]
        if c != qhandles.mapicons_c:
            if c:
                filename = "images\\MapIcons-w.bmp"
            else:
                filename = "images\\MapIcons-b.bmp"
            qhandles.mapicons_c = c
            qhandles.mapicons = quarkx.loadimages(filename, 16, (0,0))
        if self.layout is not None:
            self.layout.setupchanged(level)
            self.explorerselchange()


    def savesetupinfos(self):
        setup = quarkx.setupsubset(self.MODE, "Display")
        setup["GridStep"] = (self.gridstep,)
        setup.setint("ViewMode", self.drawmode & DM_MASKOOV)
        setup["ComputePolys"] = "1"[not (self.drawmode & DM_COMPUTEPOLYS):]
        setup = quarkx.setupsubset(self.MODE, "Options")
        setup["LinearBox"] = "1"[not self.linearbox:]
        setup["UnlockViews"] = "1"[:not self.lockviews]
        if self.gridstep:
            setup["GridActive"] = "1"[not self.grid:]


    def explorerselchange(self, ex=None):
        self.buildhandles()
        self.invalidateviews(1)
        self.layout.selchange()



    #
    # Function to check for invalid objects while making an action.
    #
    def ok(self, undo, msg):
        undo.ok(self.Root, msg)


    def invalidateviews(self, rebuild=0):
        "Force all views to be redrawn."
        for v in self.layout.views:
            v.invalidate(rebuild)

    def invalidatetexviews(self):
        "Force all non-wireframe views to be redrawn."
        for v in self.layout.views:
            if v.viewmode != "wire":
                v.invalidate(1)


    def explorerrootchange(self, ex, old, new):
        self.Root = new
        self.fileobject['Root'] = new.name


    def mousemap(self, view, x, y, flags, handle):
        "Called by QuArK upon mouse operation."

        #
        # Are we simply moving the mouse over the view ?
        #

        if flags & MB_MOUSEMOVE:
            if handle is None:
                s = view.info["type"] + " view"
                min, max = view.depth
                list = map(quarkx.ftos, self.aligntogrid(view.space(quarkx.vect(x, y, min))).tuple + self.aligntogrid(view.space(quarkx.vect(x, y, max))).tuple)
                tag = 0
                if list[0]==list[3]: tag = 1
                if list[1]==list[4]: tag = tag + 2
                if list[2]==list[5]: tag = tag + 4
                if tag==6:
                    s = s + "     y: " + list[1] + "   z: " + list[2]
                elif tag==5:
                    s = s + "     x: " + list[0] + "   z: " + list[2]
                elif tag==3:
                    s = s + "     x: " + list[0] + "   y: " + list[1]
            else:
                s = quarkx.getlonghint(handle.hint)
            self.showhint(s)

        #
        # Are we currently dragging the mouse ? Send to the dragobject.
        #

        elif flags & MB_DRAGGING:
            if self.dragobject is not None:
                self.dragobject.dragto(x, y, flags)
                if self.dragobject.hint is not None:
                    self.showhint(self.dragobject.hint)
                try:
                    if self.dragobject.InfiniteMouse:
                        return 2 - (not MapOption("HideMouseDrag", self.MODE))
                except:
                    pass

        #
        # Are we finished dragging the mouse ? Notify the dragobject.
        #

        elif flags & MB_DRAGEND:
            if self.dragobject is not None:
                self.dragobject.ok(self, x, y, flags)
                self.dragobject = None

        else:
            #
            # Read the setup to determine what the mouse click should do.
            #
            setup = quarkx.setupsubset(self.MODE, "Mouse")
            s = setup[mouseflags(flags)]

            #
            # Did the user make just a simple click ?
            #

            if flags & MB_CLICKED:
                #
                # Send the click to MouseClicked
                #
                flags = self.HandlesModule.MouseClicked(self,view,x,y,s,handle)
                #
                # Did the mouse click change the selection ?
                #
                if "S" in flags:
                    self.layout.actionmpp()  # update the multi-pages-panel
                #
                # Must we open a pop-up menu ?
                #
                if "M" in flags:
                    menu = self.explorermenu(None, view, view.space(x,y,view.proj(view.screencenter).z))
                    if menu is not None:
                        view.popupmenu(menu, x,y)

            #
            # Or did the user start to drag the mouse ?
            #

            elif flags & MB_DRAGSTART:
                #
                # First report the current grid size to the module qhandles
                #
                qhandles.setupgrid(self)
                #
                # Create a dragobject to hold information about the current dragging
                #
                self.dragobject = self.HandlesModule.MouseDragging(self,view,x,y,s,handle)
                #
                # If successful, immediately begin to drag
                #
                if self.dragobject is not None:
                    self.dragobject.views = self.layout.views
                    self.dragobject.dragto(x, y, flags | MB_DRAGGING)


    def gridmenuclick(self, sender):
        self.setgrid(sender.grid)

    def setgrid(self, ngrid):
        if (self.grid == ngrid) and (self.gridstep == ngrid):
            return
        self.grid = self.gridstep = ngrid
        self.gridchanged()

    def gridchanged(self, repaint=1):
        if self.layout is not None:
            self.layout.setgrid(self)
        self.savesetupinfos()
        if repaint:
            self.invalidateviews()

    def togglegrid(self, sender):
        self.grid = not self.grid and self.gridstep
        self.gridchanged(0)

    def customgrid(self, m):
        CustomGrid(self)

    def aligntogrid(self, v):
        qhandles.setupgrid(self)
        return qhandles.aligntogrid(v, 0)


    def editcmdgray(self, Cut1, Copy1, Delete1):
        # must Copy and Cut commands be grayed out ?
        s = self.layout.explorer.sellist
        if len(s):
            Copy1.state = 0
            Cut1.state = (self.Root in s) and qmenu.disabled
        else:
            Cut1.state = qmenu.disabled
            Copy1.state = qmenu.disabled
        Delete1.state = Cut1.state


    def trash1drop(self, btn, list, x, y, source):
        "Drop on the trash button."
        #
        # Did we drag a button of the User Data Panel ?
        #
        if isinstance(source, UserDataPanelButton):
            source.udp.deletebutton(source)  # if so, delete the button
        else:
            self.deleteitems(list)           # else simply delete the given map items

            
    def visualselection(self):
        "Visual selection (this is overridden by MapEditor)."
        return self.layout.explorer.sellist


    def setscaleandcenter(self, scale1, ncenter):
        "Set the scale and center point of the main views."
        ok = 0
        ignore = [], []
        for ifrom, linkfrom, ito, linkto in self.layout.sblinks:
            ignore[ito].append(linkto)
        for v in self.layout.views:
            if v.info["type"]!="3D":
                diff = v.info["scale"]/scale1
                ok = ok or (diff<=0.99) or (diff>=1.01)
                setviews([v], "scale", scale1)
                pp1 = v.proj(v.screencenter)
                pp2 = v.proj(ncenter)
                if not pp1.visible or not pp2.visible:
                    move = abs(v.screencenter-ncenter)*scale1>=4.9
                else:
                    dx = (not (v in ignore[0])) and abs(pp2.x - pp1.x)
                    dy = (not (v in ignore[1])) and abs(pp2.y - pp1.y)
                    move = dx>=4 or dy>=4
                if move:
                    v.screencenter = ncenter
                    ok = 1
        return ok


    def linear1click(self, btn):
        "Click on the 'linear box' button."
        self.linearbox = not self.linearbox
        self.savesetupinfos()
        try:
            self.layout.buttons["linear"].state = self.linearbox and qtoolbar.selected
            quarkx.update(self.layout.editor.form)
        except KeyError:
            pass
        if len(self.layout.explorer.sellist):
            self.lastscale = 0      # rebuild the handles with or without the linear box handles
            self.invalidateviews()

    def lockviewsclick(self, btn):
        "Click on the 'lock views' button."
        self.lockviews = not self.lockviews
        self.savesetupinfos()
        try:
            self.layout.buttons["lockv"].state = self.lockviews and qtoolbar.selected
            quarkx.update(self.layout.editor.form)
        except KeyError:
            pass
        if self.lockviews:
            self.layout.LockViews()
        else:
            self.layout.UnlockViews()


    def showhint(self, text=None):
        "Called when the mouse is over a control with a hint."
        if self.layout is None:
            return ""
        if (text is not None) and (self.layout.hinttext != text) and (text[:1]!="?"):
            self.layout.hinttext = text
            if self.layout.hintcontrol is not None:
                self.layout.hintcontrol.repaint()
        return self.layout.hinttext


    def initquickkeys(self, quickkeys):
        "Setup the 'numshortcuts' attribute of the editor window."
        #
        # See mapmenus.QuickKeys
        #
        nsc = {}
        try:
            n = len(self.layout.mpp.pagebtns)
        except:
            n = 0
        if n>9: n=9
        for i in range(n):
            def fn1(editor=self, page=i):
                editor.layout.mpp.viewpage(page)
            nsc[49+i] = fn1
        setup = quarkx.setupsubset(self.MODE, "Keys")
        for fn in quickkeys:
            s = setup.getint(fn.__name__)
            if s:
                def fn1(editor=self, fn=fn):
                    if editor.layout is None:
                        return
                    view = editor.form.focus
                    try:
                        if view is None or view.type!="mapview":
                            fn(editor)
                        else:
                            fn(editor, view)
                    except NeedViewError:    # "view" was required
                        return
                    return 1    # "eat" the key, don't process it any further
                nsc[s] = fn1
        self.form.numshortcuts = nsc


    def movekey(self, view, dx,dy):
        if (view is None) or (view.info["type"] in ("2D","3D")):
            raise NeedViewError
        list = self.layout.explorer.sellist
        if len(list):   # make the selected object(s) move
            vx = view.vector("x")
            if vx: vx = vx.normalized
            vy = view.vector("y")
            if vy: vy = vy.normalized
            gs = self.gridstep or 32
            self.moveby(Strings[558], gs * (vx*dx + vy*dy))
        else:   # make the view scroll
            hbar, vbar = view.scrollbars
            qhandles.MakeScroller(self.layout, view)(hbar[0] + 32*dx, vbar[0] + 32*dy)


    def interestingpoint(self):
        #
        # Computes some point that looks "centered".
        # Plug-ins can override this for special cases,
        # when they have got another point to be considered the center.
        #
        return None


NeedViewError = "this key only applies to a 2D map view"

