# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# Mobius Forensic Toolkit
# Copyright (C) 2008,2009,2010,2011,2012,2013,2014,2015,2016,2017,2018,2019,2020,2021,2022,2023,2024 Eduardo Aguiar
#
# 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, 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, see <http://www.gnu.org/licenses/>.
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
import os
import os.path
import traceback

import mobius
import pymobius
import pymobius.item_browser
from gi.repository import GdkPixbuf
from gi.repository import Gtk

from about_dialog import AboutDialog
from add_item_dialog import AddItemDialog
from metadata import *
from new_case_dialog import NewCaseDialog

# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief Constants
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
(ITEM_ICON, ITEM_NAME, ITEM_OBJ) = range(3)

# DND mimetypes
REPORT_DATA_MIMETYPE = 'application/x-mobius-report-data'
URI_LIST_MIMETYPE = 'text/uri-list'


# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief ICE window
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
class ICEWindow(object):

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief initialize object
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __init__(self):
        self.__mediator = pymobius.mediator.copy()
        self.__case = None
        self.__itemlist = []
        factory = self.__mediator.call('ui.new-factory')

        # window
        self.__widget = self.__mediator.call('ui.new-window')
        self.__widget.connect('delete-event', self.on_window_delete_event)

        # set window size
        if mobius.core.has_config('ice.window.width'):
            width = mobius.core.get_config('ice.window.width')
            height = mobius.core.get_config('ice.window.height')
            self.__widget.resize(width, height)

        else:
            self.__widget.set_default_size(800, 600)

        # set window icon
        path = self.__mediator.call('extension.get-icon-path', EXTENSION_ID)
        icon = self.__mediator.call('ui.new-icon-from-file', path)
        self.__widget.set_icon(icon)

        # set window accel group
        self.__accel_group = Gtk.AccelGroup()
        self.__widget.add_accel_group(self.__accel_group)

        # vbox
        vbox = Gtk.VBox()
        vbox.set_border_width(5)
        vbox.set_spacing(5)
        vbox.show()
        self.__widget.add(vbox)

        # menubar
        menubar = Gtk.MenuBar()
        menubar.show()
        vbox.pack_start(menubar, False, False, 0)

        item = Gtk.MenuItem.new_with_mnemonic('_File')
        item.show()
        menubar.append(item)

        menu = Gtk.Menu()
        menu.show()
        item.set_submenu(menu)

        item = Gtk.MenuItem.new_with_mnemonic('_New')
        item.connect("activate", self.__on_file_new)
        item.show()
        menu.append(item)

        item = Gtk.MenuItem.new_with_mnemonic('_Open')
        item.connect("activate", self.__on_file_open)
        item.show()
        menu.append(item)

        item = Gtk.SeparatorMenuItem.new()
        item.show()
        menu.append(item)

        self.close_file_menuitem = Gtk.MenuItem.new_with_mnemonic("_Close")
        self.close_file_menuitem.set_sensitive(False)
        self.close_file_menuitem.connect("activate", self.__on_file_close)
        self.close_file_menuitem.show()
        menu.append(self.close_file_menuitem)

        item = Gtk.MenuItem.new_with_mnemonic("_Quit")
        item.connect("activate", self.__on_file_quit)
        item.show()
        menu.append(item)

        item = Gtk.MenuItem.new_with_mnemonic('_Tools')
        item.show()
        menubar.append(item)

        self.__tools_menu = Gtk.Menu()
        self.__tools_menu.show()
        item.set_submenu(self.__tools_menu)

        menu_tools = [(r.id, r.value) for r in mobius.core.get_resources('menu.tools')]
        menu_tools = list((title, item_id, icon_path, callback) for item_id, (icon_path, title, callback) in menu_tools)

        for (title, item_id, icon_path, callback) in sorted(menu_tools, key=lambda x: x[0]):
            self.__add_tools_menu_item(item_id, title, icon_path, callback)

        item = Gtk.MenuItem.new_with_mnemonic('_Help')
        item.show()
        menubar.append(item)

        menu = Gtk.Menu()
        menu.show()
        item.set_submenu(menu)

        item = Gtk.MenuItem.new_with_mnemonic('_About')
        item.connect("activate", self.__on_help_about)
        item.show()
        menu.append(item)

        # toolbar
        toolbar = Gtk.Toolbar()
        toolbar.set_style(Gtk.ToolbarStyle.ICONS)
        toolbar.show()
        vbox.pack_start(toolbar, False, False, 0)

        toolitem = Gtk.ToolButton.new()
        toolitem.set_icon_name('document-new')
        toolitem.connect("clicked", self.__on_file_new)
        toolitem.show()
        toolitem.set_tooltip_text("New case")
        toolbar.insert(toolitem, -1)

        toolitem = Gtk.ToolButton.new()
        toolitem.set_icon_name('document-open')
        toolitem.connect("clicked", self.__on_file_open)
        toolitem.show()
        toolitem.set_tooltip_text("Open case")
        toolbar.insert(toolitem, -1)

        toolitem = Gtk.SeparatorToolItem()
        toolitem.show()
        toolbar.insert(toolitem, -1)

        self.add_toolitem = Gtk.ToolButton.new()
        self.add_toolitem.set_icon_name('list-add')
        self.add_toolitem.set_sensitive(False)
        self.add_toolitem.connect("clicked", self.__on_add_item)
        self.add_toolitem.show()
        self.add_toolitem.set_tooltip_text("Add items to case")
        toolbar.insert(self.add_toolitem, -1)

        self.remove_toolitem = Gtk.ToolButton.new()
        self.remove_toolitem.set_icon_name('list-remove')
        self.remove_toolitem.set_sensitive(False)
        self.remove_toolitem.connect("clicked", self.__on_remove_item)
        self.remove_toolitem.show()
        self.remove_toolitem.set_tooltip_text("Remove items from case")
        toolbar.insert(self.remove_toolitem, -1)

        self.report_toolitem = Gtk.ToolButton.new()

        image_buffer = factory.new_image_buffer_by_id('report.run')
        image = Gtk.Image.new_from_pixbuf(image_buffer.get_ui_widget())
        image.show()
        self.report_toolitem.set_icon_widget(image)

        self.report_toolitem.set_sensitive(False)
        self.report_toolitem.connect("clicked", self.__on_report_item)
        self.report_toolitem.show()
        self.report_toolitem.set_tooltip_text("Run report on selected item")
        toolbar.insert(self.report_toolitem, -1)

        # hpaned
        self.__hpaned = Gtk.HPaned()

        position = mobius.core.get_config('ice.hpaned-position')
        if position:
            self.__hpaned.set_position(position)

        vbox.pack_start(self.__hpaned, True, True, 0)

        # treeview
        self.__treeview = self.__mediator.call('ui.new-widget', 'case-treeview')
        self.__treeview.set_multiple_selection(True)
        self.__treeview.add_dnd_dest_mimetype(URI_LIST_MIMETYPE)
        self.__treeview.add_dnd_dest_mimetype(REPORT_DATA_MIMETYPE)
        self.__treeview.set_control(self, 'treeview')
        self.__treeview.show()
        self.__hpaned.pack1(self.__treeview.get_ui_widget(), True, True)

        # details
        frame = Gtk.Frame()
        frame.show()
        self.__hpaned.pack2(frame, False, True)

        self.__view_selector = self.__mediator.call('ui.new-widget', 'view-selector')
        self.__view_selector.show()
        frame.add(self.__view_selector.get_ui_widget())

        for resource in mobius.core.get_resources('view'):
            self.__view_selector.add(resource.id, resource.value())

        # update window
        self.__update_window()

        # connect to events
        self.__event_uid_1 = mobius.core.subscribe('attribute-modified', self.__on_attribute_modified)
        self.__event_uid_2 = mobius.core.subscribe('attribute-removed', self.__on_attribute_removed)
        self.__event_uid_3 = mobius.core.subscribe('resource-added', self.__on_resource_added)
        self.__event_uid_4 = mobius.core.subscribe('resource-removed', self.__on_resource_removed)

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief show window
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def show(self):
        self.__widget.show()

        if mobius.core.has_config('ice.window.x'):
            x = mobius.core.get_config('ice.window.x')
            y = mobius.core.get_config('ice.window.y')
            self.__widget.move(x, y)

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief handle window->on_delete_event
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def on_window_delete_event(self, widget, *args):

        # if there is at most one case opened, performs like a file->quit
        if mobius.model.get_case_count() < 2:
            return self.__on_file_quit(widget)

        # otherwise, performs like a file->close
        else:
            return self.__on_file_close(widget)

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief handle file->new
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __on_file_new(self, widget, *args):

        # run new case dialog
        dialog = NewCaseDialog()
        response = dialog.run()

        case_id = dialog.case_id
        case_name = dialog.case_name
        case_folder = dialog.case_folder

        dialog.destroy()

        # if user hit OK, create a new case
        if response == Gtk.ResponseType.OK:
            case = mobius.model.new_case(case_folder)
            case.root_item.id = case_id
            case.root_item.name = case_name

            window = self.__get_window_case()
            window.__set_case(case)

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief handle file->open
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __on_file_open(self, widget, *args):

        # choose file
        fs = Gtk.FileChooserDialog(title='Choose case folder to open')
        fs.set_action(Gtk.FileChooserAction.SELECT_FOLDER)
        fs.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
        fs.add_button(Gtk.STOCK_OK, Gtk.ResponseType.OK)

        rc = fs.run()
        folder = fs.get_filename()
        fs.destroy()

        if rc != Gtk.ResponseType.OK:
            return

        # check if case.sqlite exists
        if not os.path.exists(os.path.join(folder, 'case.sqlite')):
            dialog = mobius.ui.message_dialog(mobius.ui.MESSAGE_DIALOG_TYPE_ERROR)
            dialog.text = 'Could not find "case.sqlite" file inside folder'
            dialog.add_button(mobius.ui.BUTTON_OK)
            rc = dialog.run()
            return

        # open case
        case = mobius.model.open_case(folder)
        window = self.__get_window_case()
        window.__set_case(case)

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief handle file->close
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __on_file_close(self, widget, *args):
        root_item = self.__case.root_item
        case_name = root_item.name or root_item.id

        # show 'would you like to close case' dialog
        # show dialog
        dialog = mobius.ui.message_dialog(mobius.ui.MESSAGE_DIALOG_TYPE_QUESTION)
        dialog.text = "Do you want to close '%s'?" % case_name
        dialog.add_button(mobius.ui.BUTTON_YES)
        dialog.add_button(mobius.ui.BUTTON_NO)
        dialog.set_default_response(mobius.ui.BUTTON_NO)
        rc = dialog.run()

        # if response = YES, set current version and show extension manager
        if rc != mobius.ui.BUTTON_YES:
            return True

        # close case
        mobius.model.close_case(self.__case)

        # close working area
        if mobius.model.get_case_count() > 0:
            self.__destroy()

        else:
            self.__set_case(None)

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief handle file->quit. quit application
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __on_file_quit(self, widget, *args):

        # show confirmation dialog
        dialog = mobius.ui.message_dialog(mobius.ui.MESSAGE_DIALOG_TYPE_QUESTION)
        dialog.text = "Do you really want to quit from Mobius Forensic Toolkit?"
        dialog.add_button(mobius.ui.BUTTON_YES)
        dialog.add_button(mobius.ui.BUTTON_NO)
        dialog.set_default_response(mobius.ui.BUTTON_NO)
        rc = dialog.run()

        if rc != mobius.ui.BUTTON_YES:
            return True

        # stop UI
        self.__destroy()

        ui = mobius.ui.ui()
        ui.stop()

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief handle help->about
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __on_help_about(self, widget, *args):
        dialog = AboutDialog()
        dialog.run()
        dialog.destroy()

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief call report item dialog
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __on_report_item(self, widget, *args):
        selected_items = self.__treeview.get_selected_items()

        if len(selected_items) == 1:
            item = selected_items[0]
            item_proxy = pymobius.item_browser.ItemBrowser(item)

            dialog = self.__mediator.call('report.run-dialog')
            dialog.run(item_proxy)
            dialog.destroy()

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief handle treeview->retrieve-icon
    # @param item treeitem
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def treeview_on_retrieve_icon(self, item):
        category_manager = mobius.core.category_manager()
        category = category_manager.get_category(item.category)
        return category.icon_data

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief handle treeview->selection-changed
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def treeview_on_selection_changed(self, itemlist):
        self.__itemlist = itemlist

        # can add item
        add_item_enabled = len(itemlist) == 1

        # can remove item
        has_case_item = any(item for item in itemlist if item.category == 'case')
        remove_item_enabled = len(itemlist) > 0 and not has_case_item

        # can run report
        report_run_enabled = len(itemlist) == 1

        # enable/disable widgets
        self.add_toolitem.set_sensitive(add_item_enabled)
        self.remove_toolitem.set_sensitive(remove_item_enabled)
        self.report_toolitem.set_sensitive(report_run_enabled)

        # update multiview
        self.__view_selector.set_data(itemlist)

        # emit 'case.selection-changed'
        self.__mediator.emit('case.selection-changed', self.__case, itemlist)

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief handle treeview->on_reordering_item
    # @param item item being reordered
    # @param old_parent old parent item
    # @param new_parent new parent item
    # @param pos item order into new parent's children
    # @return True if can/False if cannot
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def treeview_on_reordering_item(self, item, old_parent, new_parent, pos):

        # check if case item is being moved
        if item.category == 'case':
            return False

        # check if destination is valid
        if not new_parent:
            return False

        # move item
        transaction = self.__case.new_transaction()
        item.move(pos + 1, new_parent)
        transaction.commit()

        return True

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief handle treeview->on_file_dropped
    # @param parent parent item
    # @param pos child position
    # @param mimetype data mimetype
    # @param data data
    # @return True if accepted/False otherwise
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def treeview_on_file_dropped(self, parent, pos, mimetype, data):
        rc = False

        if parent:

            # REPORT DATA DND
            if mimetype == REPORT_DATA_MIMETYPE:
                rc = self.__on_dnd_report_data(parent, pos, data)

            # FILE DND
            elif mimetype == URI_LIST_MIMETYPE:
                rc = self.__on_dnd_uri_list(parent, pos, data)

        return rc

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief Handle treeview->on_remove_item
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def treeview_on_remove_item(self, item):
        transaction = self.__case.new_transaction()
        item.remove()
        transaction.commit()

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief Handle REPORT_DATA_MIMETYPE DND
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __on_dnd_report_data(self, parent, pos, data):
        uid = int(data)
        report = self.__mediator.call('ui.dnd-pop', uid)
        transaction = self.__case.new_transaction()

        child = parent.new_child('report-data', pos + 1)
        child.rid = report.rid
        child.name = report.name
        child.application = report.app
        child.widget = report.widget
        child.data = report.data

        transaction.commit()

        return True

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief handle URI_LIST_MIMETYPE DND
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __on_dnd_uri_list(self, parent, pos, data):

        # get DND handlers
        handlers = []
        fallback = None

        for resource in mobius.core.get_resources('ui.dnd-file'):

            if resource.id == 'fallback':
                fallback = resource.value

            else:
                handlers.append(resource.value)

        if fallback:
            handlers.append(fallback)

        # process files
        uri_list = data.decode('utf-8').rstrip().split('\r\n')
        item_count = 0
        transaction = self.__case.new_transaction()

        for uri in uri_list:

            # run through ui.dnd-file handlers until one handles this file
            for handler in handlers:
                count = handler(parent, pos, uri)

                if count > 0:
                    pos += count
                    item_count += count
                    break

        transaction.commit()

        return item_count > 0

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief handle add item
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __on_add_item(self, widget, *args):

        # get selected item
        if self.__treeview.count_selected_items() != 1:
            return

        parent = self.__treeview.get_selected_items()[0]

        # call dialog
        dialog = AddItemDialog()

        last_category_added = mobius.core.get_config('ice.last-category-added')
        if last_category_added:
            dialog.set_category(last_category_added)

        rc = dialog.run()

        # read data
        amount = dialog.get_amount()
        category = dialog.get_category()
        attribute_values = dialog.get_attribute_values()
        dialog.destroy()

        if rc != Gtk.ResponseType.OK or not category:
            return

        # create items
        transaction = self.__case.new_transaction()

        for i in range(amount):
            item = parent.new_child(category.id)

            # set item attributes
            for attr_id, attr_value in attribute_values:
                item.set_attribute(attr_id, attr_value)

            item.expand_masks()

        transaction.commit()

        # reload item
        self.__treeview.reload_selected_items(True)
        self.__treeview.expand_selected_items()

        # save configuration
        transaction = mobius.core.new_config_transaction()
        mobius.core.set_config('ice.last-category-added', category.id)
        transaction.commit()

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief handle remove item
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __on_remove_item(self, widget, *args):
        count = self.__treeview.count_selected_items()

        # show dialog
        dialog = mobius.ui.message_dialog(mobius.ui.MESSAGE_DIALOG_TYPE_QUESTION)
        dialog.text = 'You are about to remove %s. Are you sure?' % ('an item' if count == 1 else '%d items' % count)
        dialog.add_button(mobius.ui.BUTTON_YES)
        dialog.add_button(mobius.ui.BUTTON_NO)
        dialog.set_default_response(mobius.ui.BUTTON_NO)
        rc = dialog.run()

        if rc != mobius.ui.BUTTON_YES:
            return

        # remove items
        self.__treeview.remove_selected_items()

        # set case modified
        self.__update_window()  # @todo check if necessary

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief Handle attribute-modified event
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __on_attribute_modified(self, obj, attr_id, old_value, new_value):
        self.__on_attribute_changed(obj, attr_id)

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief Handle attribute-removed event
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __on_attribute_removed(self, obj, attr_id, old_value):
        self.__on_attribute_changed(obj, attr_id)

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief Handle attribute changed
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __on_attribute_changed(self, obj, attr_id):

        if self.__case and obj.case and obj.case.uid == self.__case.uid:

            if attr_id == 'name':
                self.__treeview.reload_selected_items(False)

                if obj.uid == 1:  # change window title
                    self.__update_window()

            elif attr_id == 'children':
                self.__treeview.reload_selected_items(True)

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief Handle resource-added event
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __on_resource_added(self, report_id, resource):
        item_id = report_id.rsplit('.', 1)[1]

        if report_id.startswith('menu.tools.'):
            icon_path, title, callback = resource.value
            self.__add_tools_menu_item(item_id, title, icon_path, callback)

        elif report_id.startswith('view.'):
            view = resource.value()
            toolitem = self.__view_selector.add(item_id, view)
            toolitem.set_active(True)

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief Handle resource-removed event
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __on_resource_removed(self, report_id):
        item_id = report_id.rsplit('.', 1)[1]

        if report_id.startswith('menu.tools.'):
            self.__remove_tools_menu_item(item_id)

        elif report_id.startswith('view.'):
            self.__view_selector.remove(item_id)

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief Set case object
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __set_case(self, case):
        self.__case = case

        # populate treeview
        if self.__case:
            item = self.__case.root_item
            self.__treeview.set_root_items([item])

        else:
            self.__treeview.clear()

        # set case for all views
        for view in self.__view_selector:
            if hasattr(view, 'set_case'):
                view.set_case(self.__case)

        # update window
        self.__update_window()

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief get window for a case
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __get_window_case(self):

        # create new working area if necessary
        if not self.__case:
            window = self

        else:
            window = ICEWindow()
            window.show()

        # return window
        return window

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief save configuration and destroy window
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __destroy(self):
        # get window position and size
        x, y = self.__widget.get_position()
        width, height = self.__widget.get_size()

        # save configuration
        transaction = mobius.core.new_config_transaction()

        mobius.core.set_config('ice.window.x', x)
        mobius.core.set_config('ice.window.y', y)
        mobius.core.set_config('ice.window.width', width)
        mobius.core.set_config('ice.window.height', height)
        mobius.core.set_config('ice.hpaned-position', self.__hpaned.get_position())

        for view in self.__view_selector:
            if hasattr(view, 'on_destroy'):
                try:
                    view.on_destroy()
                except Exception as e:
                    mobius.core.logf('WRN %s %s' % (str(e), traceback.format_exc()))

        transaction.commit()

        # destroy widget
        mobius.core.unsubscribe(self.__event_uid_1)
        mobius.core.unsubscribe(self.__event_uid_2)
        mobius.core.unsubscribe(self.__event_uid_3)
        mobius.core.unsubscribe(self.__event_uid_4)

        self.__mediator.clear()
        self.__widget.destroy()

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief update window controls and appearance
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __update_window(self):

        # enable/disable widgets
        if self.__case:
            self.close_file_menuitem.set_sensitive(True)
            self.__hpaned.show()
        else:
            self.close_file_menuitem.set_sensitive(False)
            self.__hpaned.hide()

        # update window's title
        app = mobius.core.application()
        title = '%s - %s' % (app.title, EXTENSION_NAME)

        if self.__case:
            root = self.__case.root_item
            title += ' - ' + root.name

        self.__widget.set_title(title)

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief add item to tools menus
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __add_tools_menu_item(self, item_id, title, icon_path, callback):
        pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(icon_path, 20, 20)

        hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 5)
        hbox.show()

        image = Gtk.Image.new_from_pixbuf(pixbuf)
        image.show()
        hbox.pack_start(image, False, False, 0)

        label = Gtk.Label.new(title)
        label.set_xalign(0.0)
        label.show()
        hbox.pack_start(label, True, True, 0)

        item = Gtk.MenuItem.new()
        item.add(hbox)
        item.item_id = item_id
        item.title = title
        item.connect('activate', callback)
        item.show()

        # find item position
        pos = 0
        children = self.__tools_menu.get_children()

        while pos < len(children) and children[pos].title < title:
            pos += 1

        # insert item
        self.__tools_menu.insert(item, pos)

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief remove item from tools menus
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __remove_tools_menu_item(self, item_id):
        for child in self.__tools_menu.get_children():
            if child.item_id == item_id:
                self.__tools_menu.remove(child)


# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# Service <app.start> implementation
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
def svc_app_start():
    # create working area
    window = ICEWindow()
    window.show()

    # start graphical interface
    # mobius.ui.set_ui_implementation ("gtk3")

    ui = mobius.ui.ui()
    ui.start()


# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief API initialization
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
def pvt_start_api():
    pymobius.mediator.advertise('app.start', svc_app_start)
