# -*- coding: utf-8 -*-

# vtoolheader.py
# This file is part of VWidgets module
# see: http://bazaar.launchpad.net/~vincent-vandevyvre/oqapy/serie-1.0/
#
# Author: Vincent Vande Vyvre <vincent.vandevyvre@swing.be>
# Copyright: 2012-2013 Vincent Vande Vyvre
# Licence: GPL3
#
#
# This file 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 3 of the License, or
# (at your option) any later version.
#
# This 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 file.  If not, see <http://www.gnu.org/licenses/>.
#
# Define a header bar for tool box

import sys
import os

from PyQt5.QtCore import pyqtProperty, pyqtSignal, QLocale, QSize, Qt
from PyQt5.QtGui import QPixmap, QIcon
from PyQt5.QtWidgets import (QHBoxLayout, QBoxLayout, QWidget, QLabel,
                             QPushButton, QToolButton, QSpacerItem, QSizePolicy,
                             QFrame)

LANGS = ['ar', 'he', 'fa', 'ur', 'syr']

class VToolHeader(QWidget):
    """VToolHeader is a widget used as header bar for tool box.

    The header can have a button expand-collapse, a subject related icon, 
    a title and a set of buttons. By default, these widgets are placed from 
    left to right but the orientation may be reversed.
    """
    expandRequest = pyqtSignal(bool)
    headerButtonClicked = pyqtSignal(QPushButton)
    def __init__(self, parent, icon=None, title='Tool Header', 
                                            orientation=Qt.LeftToRight):
        """Instanciate the VToolHeader.

        Args:
        parent -- the parent of the header
        icon -- the icon, may be a QIcon a QPixmap or an image file path
        title -- str(title)
        orientation -- QT.LayoutDirection
        """
        super(VToolHeader, self).__init__(parent)
        dct = dict(orientation_ = orientation,
                    title_ = title,
                    icon_ = QIcon(),
                    collapsible_ = True,
                    auto_oriented = False,
                    buttons = [],
                    is_builded = False,
                    is_expanded = True)

        for k, v in dct.items():
            setattr(self, k, v)

        self.setMaximumSize(99999, 30)
        self.hbox = QHBoxLayout(self)
        self.hbox.setContentsMargins(0, 0, 0, 0)
        self.hbox.setSpacing(0)
        self.content = QFrame()
        self.content.setFrameShape(QFrame.Panel)
        self.content.setFrameShadow(QFrame.Raised)
        self.hbox.addWidget(self.content)
        self.layout = QHBoxLayout(self.content)
        self.layout.setContentsMargins(0, 0, 0, 0)
        self.layout.setSpacing(5)
        self.sub_layout = QHBoxLayout()
        self.sub_layout.setContentsMargins(0, 0, 0, 0)
        self.sub_layout.setSpacing(0)
        self.buttons_layout = QHBoxLayout()
        self.buttons_layout.setContentsMargins(0, 0, 0, 0)
        self.buttons_layout.setSpacing(0)
        self.label = Label(self.title_)

        if icon is not None:
            self.icon_ = self.setIcon(icon)

        self.expanded_icon = self.__make_icon(QPixmap(_down_xpm))
        self.collapsed_left_icon = self.__make_icon(QPixmap(_left_xpm))
        self.collapsed_right_icon = self.__make_icon(QPixmap(_right_xpm))
        self.extra_left_btn = ExtraButton('left', self)
        self.extra_right_btn = ExtraButton('right', self)
        self.extra_left_btn.clicked.connect(self.__shift_buttons_to_left)
        self.extra_right_btn.clicked.connect(self.__shift_buttons_to_right)

        self.__build_header()

    def get_width(self):
        return self.width()

    def get_height(self):
        return self.height()

    def get_geometries(self):
        if self.label is not None:
            if self.getOrientation() == Qt.LeftToRight:
                return self.width(), self.label.x() + self.label.width()

            return self.width(), self.width() - self.label.x()

        used = 0
        if self.collapsible:
            used += 17

        if self.icon_ is not None:
            used += self.height() + 2
 
        return self.width(), used

    def getTitle(self):
        return self.title_

    def setTitle(self, title):
        """Sets the title of the header.

        Args:
        title -- str(title)
        """
        self.label.setText(title)
        self.title_ = title

    title = pyqtProperty('QString', getTitle, setTitle)

    def getOrientation(self):
        return self.orientation_

    def setOrientation(self, orientation):
        """Sets the orientation of the widgets.

        Args:
        orientation -- Qt.LayoutDirection
        """
        if not isinstance(orientation, Qt.LayoutDirection):
            raise ValueError('VToolHeader.setOrientation(Qt.LayoutDirection) '
                        'arg 1 has unexpected value {0}'.format(orientation))

        if orientation != self.orientation_:
            self.orientation_ = orientation
            self.__set_direction()

            if self.collapsible_:
                self.collapse_btn.update_icon()

    orientation = pyqtProperty(Qt.LayoutDirection, getOrientation, setOrientation)

    def isOrientedByLanguage(self):
        return self.auto_oriented

    def setOrientedByLanguage(self, b):
        """Sets the orientation in accordance with the language.

        Theses languages are written from right to left:
            Arabic  [ar]
            Hebrew  [he]
            Persian [fa]
            Urdu    [ur]
            Syriac  [syr]

        Args:
        b -- boolean
        """
        if isinstance(b, bool):
            if b != self.auto_oriented:
                self.auto_oriented = b
                self.__set_direction()

                if self.collapsible_:
                    self.collapse_btn.update_icon()

    orientedByLanguage = pyqtProperty('bool', isOrientedByLanguage,
                                        setOrientedByLanguage)

    def getIcon(self):
        return self.icon_

    def setIcon(self, icon):
        """Sets the title's icon.

        Args:
        icon -- QIcon or QPixmap instance or an image file path
        """
        self.remove_icon()
        self.icon_ = self.__make_icon(icon)
        if self.icon_ is not None:
            self.layout.insertWidget(1, self._get_title_icon())

    icon = pyqtProperty('QIcon', getIcon, setIcon)

    def getFrameShape(self):
        return self.content.frameShape()

    def setFrameShape_(self, shape):
        """Sets the shape of the header.

        Args:
        shape -- QFrame.Shape
        """
        if not isinstance(shape, QFrame.Shape):
            raise ValueError('VToolHeader.setFrameShape_(QFrame.Shape) '
                        'arg 1 has unexpected value: {0}'.format(shape))

        self.content.setFrameShape(shape)

    frameShape = pyqtProperty(QFrame.Shape, getFrameShape, setFrameShape_)

    def getFrameShadow(self):
        return self.content.frameShadow()

    def setFrameShadow_(self, shadow):
        """Sets the shadow for the header.

        Args:
        shadow -- QFrame.Shadow
        """
        if not isinstance(shadow, QFrame.Shadow):
            raise ValueError('VToolHeader.setFrameShadow_(QFrame.Shadow) '
                        'arg 1 has unexpected value: {0}'.format(shadow))

        self.content.setFrameShadow(shadow)

    frameShadow = pyqtProperty(QFrame.Shadow, getFrameShadow, setFrameShadow_)

    def remove_icon(self):
        """Remove the header's icon.

        """
        if self.icon_ is not None:
            ic = self.layout.takeAt(1).widget()
            ic.deleteLater()
            del ic
            self.icon_ = None

    def __make_icon(self, icon):
        """Returns a QIcon made from an image.

        Args:
        icon -- a QIcon or a QPixmap instance or an image file path

        Returns:
        QIcon instance
        """
        if icon is None:
            return None

        if isinstance(icon, QIcon):
            return icon

        elif isinstance(icon, QPixmap):
            return QIcon(icon)

        else:
            try:
                if os.path.isfile(icon):
                    img = QPixmap(icon)

                    if img.isNull(): 
                        sys.stderr.write('VToolHeader icon is null: {0}\n'\
                                    .format(icon))
                        return None

                    return QIcon(img)

                else:
                    sys.stderr.write('VToolHeader icon file not found: {0}\n'\
                                    .format(icon))
                    return None
            except:
                sys.stderr.write('VToolHeader.__make_icon() expected icon,'\
                                    ' got {0}\n'.format(icon))
                return None

    def hasCollapseButton(self):
        return self.collapsible_

    def setCollapseButton(self, b):
        """Sets the button collapse-expand.

        This button is used to show-hide the tool box that have this header.
        The icon, by default, is a little black triangle and can be customized.

            see: set_expanded_icon(), set_collapsed_left_icon(), 
                 set_collapsed_right_icon() and set_expanding_buttons()

        Args:
        b -- boolean
        """
        if not isinstance(b, bool):
            raise TypeError("ToolHeader.set_collapsible() expected boolean, "
                                "got {0}".format(type(b)))

        if self.collapsible_ != b:
            if not b:
                self._remove_collapse_button()

            else:
                self._add_collapse_button()

        self.collapsible_ = b

    collapsible = pyqtProperty('bool', hasCollapseButton, setCollapseButton)

    def setCollapsible(self, b):
        """Retrocompatibility function"""
        sys.stderr.write('VToolHeader.setCollapsible() is deprecated, use '
                            'setCollapseButton() instead.\n')
        self.setCollapseButton(b)

    def __set_direction(self):
        """Sets the layout's direction.

        """
        if self.auto_oriented:
            loc = QLocale()
            # TODO QLocale.textDirection() is implemented since Qt 4.7
            if QLocale.name(loc).split('_')[0] in LANGS:
                self.orientation_ = Qt.RightToLeft

            else:
                self.orientation_ = Qt.LeftToRight
        
        if self.orientation_ == Qt.LeftToRight:
            direct = QBoxLayout.LeftToRight
            self.collapsed_icon = self.collapsed_left_icon

        else:
            direct = QBoxLayout.RightToLeft
            self.collapsed_icon = self.collapsed_right_icon

        self.hbox.setDirection(direct)
        self.layout.setDirection(direct)

    def __build_header(self):
        """Build the header.

        """
        self.__set_direction()

        if self.collapsible_:
            self.collapse_btn = CollapseButton(self)
            self.collapse_btn.stateChanged.connect(self.set_expanded)
            self.layout.addWidget(self.collapse_btn)

        if self.icon_ is not None:
            self.layout.addWidget(self._get_title_icon())

        self.layout.addWidget(self.label)
        spacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
        self.layout.addItem(spacer)

        self.layout.addLayout(self.sub_layout)
        self.sub_layout.addWidget(self.extra_left_btn)
        self.sub_layout.addLayout(self.buttons_layout)
        self.sub_layout.addWidget(self.extra_right_btn)

        self.extra_left_btn.hide()
        self.extra_right_btn.hide()
        self.is_builded = True

    def _get_title_icon(self):
        """Returns a QLabel painted with the header's icon.

        """
        label = QLabel()
        s = self.get_height() - 4
        px = QPixmap(self.icon_.pixmap(s, s, QIcon.Active, QIcon.On))
        label.setPixmap(px)

        return label

    def _add_collapse_button(self):
        """Add the collapse-expand button.

        """
        self.collapse_btn = CollapseButton(self)
        self.collapse_btn.stateChanged.connect(self.set_expanded)

        self.layout.insertWidget(0, self.collapse_btn)

    def _remove_collapse_button(self):
        """Remove the collapse-expand button.

        """
        self.collapse_btn.deleteLater()
        self.collapse_btn.setParent(None)
        del self.collapse_btn

    def add_button(self, icon=None, name=None):
        """Add a tool button to the header.

        Args:
        icon -- the icon used when the button is activated
        name -- the name of the button used for the property objectName
        """
        icon = self.__make_icon(icon)

        if name is None:
            name = 'headerButton_{0}'.format(len(self.buttons))

        button = HeaderButton(name, icon, self)
        button.activated.connect(self.__on_button_clicked)
        self.buttons_layout.addWidget(button)
        self.buttons.append(button)

        return button

    def remove_button(self, btn):
        """Remove a button from the header.

        Args:
        btn -- the instance of the button
        """
        self.buttons.remove(btn)
        btn.deleteLater()
        btn.setParent(None)

    def count_buttons(self):
        return len(self.buttons)

    def __on_button_clicked(self, btn):
        """Emit the signal 'headerButtonClicked' when a button is clicked.

        Args:
        btn --- the button instance
        """
        self.headerButtonClicked.emit(btn)

    def set_expanded_icon(self, icon):
        """Sets the icon wich represent the state expanded.

        Args:
        icon -- a QIcon or a QPixmap instance or an image file path
        """
        ic = self.__make_icon(icon)
        if ic is not None:
            self.expanded_icon = ic
            self.update_expanding_button()

    def set_collapsed_left_icon(self, icon):
        """Sets the icon wich represent the state collapsed.

        This icon is used for the header oriented from left to rigth.

        Args:
        icon -- a QIcon or a QPixmap instance or an image file path
        """                 
        ic = self.__make_icon(icon)
        if ic is not None:
            self.collapsed_left_icon = ic
            self.update_expanding_button()

    def set_collapsed_right_icon(self, icon):
        """Sets the icon wich represent the state collapsed.

        This icon is used for the header oriented from right to left.

        Args:
        icon -- a QIcon or a QPixmap instance or an image file path
        """
        ic = self.__make_icon(icon)
        if ic is not None:
            self.collapsed_right_icon = ic
            self.update_expanding_button()

    def set_expanding_buttons(self, down=None, left=None, right=None):
        """Sets the icons used for the button collapse-expand.

        Args:
        down -- icon wich represent the state expanded
        left -- the left side icon wich represent the state collapsed
        right -- the right side icon wich represent the state collapsed
        """
        if down is not None:
            self.set_expanded_icon(down)

        if left is not None:
            self.set_collapsed_left_icon(left)

        if right is not None:
            self.set_collapsed_right_icon(right)

    def update_expanding_button(self):
        """Update the collapse-expand button when the state has changed.

        """
        if self.orientation_ == Qt.LeftToRight:
            self.collapsed_icon = self.collapsed_left_icon

        else:
            self.collapsed_icon = self.collapsed_right_icon

        if self.collapsible:
            if self.is_expanded:
                self.collapse_btn.setIcon(self.expanded_icon)

            else:
                self.collapse_btn.setIcon(self.collapsed_icon)

        self.collapse_btn.set_icons()

    def set_expanded(self, b):
        """Emit the signal 'expandRequest(b)' when the button collapse-expand
        is clicked.

        Args:
        b -- boolean, True if expanding is requested or False for collapsing
        """
        self.is_expanded = b
        self.expandRequest.emit(b)

    def set_style_sheet(self, sheet):
        """Sets a style sheet to the header.

        Args:
        sheet -- str()
        """
        try:
            self.content.setStyleSheet(sheet)
        except:
            sys.stderr.write('ToolHeader.set_style_sheet(sheet): arg 1 has'
                    ' unexpected type: "{0}", ignoring.\n'.format(type(sheet)))

    def resizeEvent(self, e):
        for b in self.buttons:
            b.resize()

        if self.icon_ is not None:
            self._get_title_icon()

        if self.isVisible():
            self.__check_visibility()

    def showEvent(self, e):
        self.__check_visibility()

    def __check_visibility(self):
        """Check if all buttons are visible, if not, an arrow is placed at the
        right of the buttons.

        """
        width, used = self.get_geometries()
        remain = width - used
        # count how much buttons can be shown in the available space
        places = remain / self.height()
        if places < len(self.buttons):
            for b in self.buttons[:places]:
                b.show()

            for b in self.buttons[places:]:
                b.hide()

            self.extra_right_btn.show()
            self.extra_left_btn.hide()

        else:
            for b in self.buttons:
                b.show()

            self.extra_right_btn.hide()
            self.extra_left_btn.hide()

    def __shift_buttons_to_left(self):
        """Move all buttons to the left.

        """
        vsb = self.__get_visibles()
        self.buttons[vsb[-1]].hide()
        self.buttons[vsb[0]-1].show()
        self.extra_right_btn.show()

        if self.buttons[0].isVisible():
            self.extra_left_btn.hide()

    def __shift_buttons_to_right(self):
        """Move all buttons to the right.

        """
        vsb = self.__get_visibles()
        if vsb:
            self.buttons[vsb[0]].hide()
            self.buttons[vsb[-1]+1].show()
            self.extra_left_btn.show()

        if self.buttons[-1].isVisible():
            self.extra_right_btn.hide()

    def __get_visibles(self):
        """Returns the list of all buttons currently visible.

        """
        visibles = []
        for idx, _ in enumerate(self.buttons):
            if _.isVisible():
                visibles.append(idx)

        return visibles


class Label(QLabel):
    def __init__(self, text="", parent=None):
        super(Label, self).__init__(parent)
        self.setText(text)


class CollapseButton(QPushButton):
    stateChanged = pyqtSignal(bool)
    def __init__(self, parent=None):
        """Instanciate the collapse-expand button.

        """
        super(CollapseButton, self).__init__(parent)
        self.header = parent
        self.is_expanded = parent.is_expanded
        self.update_icon()
        self.setMaximumSize(12, 12)
        self.setIconSize(QSize(12, 12))
        self.setFlat(True)
        self.setAutoDefault(False)
        self.clicked.connect(self.set_expanded)

    def set_icons(self):
        self.expanded_icon = self.header.expanded_icon
        self.collapsed_icon = self.header.collapsed_icon 

    def set_expanded(self):
        self.is_expanded = not self.is_expanded
        self.update_icon()
        self.stateChanged.emit(self.is_expanded)

    def update_icon(self):
        self.set_icons()
        if self.is_expanded:
            self.setIcon(self.expanded_icon)

        else:
            self.setIcon(self.collapsed_icon)

class ExtraButton(QToolButton):
    def __init__(self, side, parent=None):
        """Instanciate the extra button.

        Extra button is used when the header is too short for show all buttons.
        It is a simple arrow like in a tool bar.

        Args:
        side -- 'left' or 'right'
        parent -- the header
        """
        super(ExtraButton, self).__init__(parent)
        self.side = side
        self.setMaximumSize(12,  parent.height())
        self.setAutoRaise(True)
        self.setAutoRepeat(True)

        if side == 'left':
            self.setArrowType(Qt.LeftArrow)

        else:
            self.setArrowType(Qt.RightArrow)


class HeaderButton(QPushButton):
    activated = pyqtSignal(QPushButton)
    def __init__(self, name, icon, parent=None):
        """Instanciate a header button.

        Args:
        name -- a name for the property objectName
        icon -- the icon
        parent -- the header
        """
        super(HeaderButton, self).__init__(parent)
        self.header = parent
        self.setObjectName(name)
        self.icon = icon
        height = parent.get_height() -2
        self.setMaximumSize(height, height)
        self.setFlat(True)
        self.setAutoDefault(False)
        self.set_icon()

    def mousePressEvent(self, e):
        QPushButton.mousePressEvent(self, e)
        self.activated.emit(self)

    def set_icon(self):
        if self.icon is None:
            self.setText("?")
        else:
            height = self.header.get_height() -2
            self.setIconSize(QSize(height, height))
            self.setIcon(self.icon)

    def resize(self):
        height = self.header.get_height() -2
        self.setMaximumSize(height, height)
        self.set_icon()


# Expended state icon
_down_xpm = [ "14 12 22 1",
"e c None", "p c #000000", "c c #000000", "l c #000000",
"h c #000000", "s c #000000", "f c #000000", "n c #000000",
"t c #000000", "a c #000000", "j c #000000", ". c #000000",
"q c #000000", "d c #000000", "m c #000000", "o c #000000",
"b c #000000", "k c #000000", "i c #000000", "g c #000000",
"r c #000000", "# c #000000", 
".############.", "ab##########ba","cd##########dc", 
"efb########bfe", "ee.g######g.ee", "eehi######ihee",
"eeejk####kjeee", "eeelm####mleee", "eeeeno##oneeee", 
"eeeepqrrqpeeee","eeeeeskkseeeee", "eeeeeetteeeeee"]

# Left side collapsed icon
_left_xpm = [ "12 14 22 1",
"b c None", "o c #000000", "a c #000000", "k c #000000",
"g c #000000", "r c #000000", "f c #000000", "n c #000000",
"t c #000000", "# c #000000", "j c #000000", ". c #000000",
"q c #000000", "e c #000000", "m c #000000", "p c #000000",
"d c #000000", "l c #000000", "i c #000000", "h c #000000",
"s c #000000", "c c #000000", 
".#abbbbbbbbb", "cdefbbbbbbbb",
"cccd.gbbbbbb", "cccchijkbbbb", "cccccclmnobb", "ccccccccpqrb",
"cccccccccslt", "cccccccccslt", "ccccccccpqrb", "cccccclmnobb",
"cccchijkbbbb", "cccd.gbbbbbb", "cdefbbbbbbbb", ".#abbbbbbbbb"]

# Right side collapsed icon
_right_xpm = [ "12 14 22 1",
". c None", "l c #000000", "# c #000000", "h c #000000",
"g c #000000", "p c #000000", "c c #000000", "m c #000000",
"s c #000000", "a c #000000", "i c #000000", "b c #000000",
"q c #000000", "d c #000000", "n c #000000", "r c #000000",
"e c #000000", "o c #000000", "j c #000000", "k c #000000",
"t c #000000", "f c #000000", 
".........#ab", "........cdef",
"......gbefff", "....hijkffff", "..lmnoffffff", ".pqrffffffff",
"sotfffffffff", "sotfffffffff", ".pqrffffffff", "..lmnoffffff",
"....hijkffff", "......gbefff", "........cdef", ".........#ab"]
