class TableViewWidget(InsertedObjectWidget): def __init__(self, model): InsertedObjectWidget.__init__(self) self.textarea_width = 0 self.model = model # used in pageview self._has_cursor = False # Skip table object, if someone moves cursor around in textview # used here self._timer = None # NONE or number of current GObject.timer, which is running self._keep_toolbar_open = False # a cell is currently edited, toolbar should not be hidden self._cellinput_canceled = None # cell changes should be skipped self._toolbar_enabled = True # sets if toolbar should be shown beneath a selected table # Toolbar for table actions self.toolbar = self.create_toolbar() self.toolbar.show_all() self.toolbar.set_no_show_all(True) self.toolbar.hide() # Create treeview self._init_treeview(model) # package gui elements self.vbox = Gtk.VBox() self.add(self.vbox) self.vbox.pack_end(self.toolbar, True, True, 0) self.scroll_win = ScrolledWindow(self.treeview, Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER, Gtk.ShadowType.NONE) self.vbox.pack_start(self.scroll_win, True, True, 0) # signals model.connect('model-changed', self.on_model_changed) def _init_treeview(self, model): # Actual gtk table object self.treeview = self.create_treeview(model) # Hook up signals & set options self.treeview.connect('button-press-event', self.on_button_press_event) self.treeview.connect('focus-in-event', self.on_focus_in, self.toolbar) self.treeview.connect('focus-out-event', self.on_focus_out, self.toolbar) self.treeview.connect('move-cursor', self.on_move_cursor) # Set options self.treeview.set_grid_lines(Gtk.TreeViewGridLines.BOTH) self.treeview.set_receives_default(True) self.treeview.set_size_request(-1, -1) self.treeview.set_border_width(2) # disable interactive column search self.treeview.set_enable_search(False) #Gtk.binding_entry_remove(Gtk.TreeView, Gdk.KEY_f, Gdk.ModifierType.CONTROL_MASK) self.treeview.set_search_column(-1) def on_model_changed(self, model): self.scroll_win.remove(self.treeview) self.treeview.destroy() self._init_treeview(model) self.scroll_win.add(self.treeview) self.scroll_win.show_all() def old_do_size_request(self, requisition): # TODO - FIX this behavior model = self.get_model() wraps = model.get_wraps() if not any(wraps): return InsertedObjectWidget.do_size_request(self, requisition) # Negotiate how to wrap .. for col in self.treeview.get_columns(): cr = col.get_cell_renderers()[0] cr.set_property('wrap-width', -1) # reset size #~ col.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) # allow column shrinks #~ col.set_max_width(0) # shrink column #~ col.set_max_width(-1) # reset value #~ col.set_sizing(Gtk.TreeViewColumnSizing.GROW_ONLY) # reset value InsertedObjectWidget.do_size_request(self, requisition) #~ print("Widget requests: %i textview: %i" % (requisition.width, self._textview_width)) if requisition.width > self._textview_width: # Figure out width of fixed cols fixed = 0 for col, wrap in zip(self.treeview.get_columns(), wraps): if not wrap: fixed += col.get_width() nwrap = sum(wraps) wrap_size = (self._textview_width - fixed) // nwrap # Set width for wrappable cols #~ print("Fixed, nwrap, wrap_size", (fixed, nwrap, wrap_size)) for col, wrap in zip(self.treeview.get_columns(), wraps): if wrap: cr = col.get_cell_renderers()[0] cr.set_property('wrap-width', wrap_size) # reset size # Update request InsertedObjectWidget.do_size_request(self, requisition) else: pass def on_focus_in(self, treeview, event, toolbar): '''After a table is selected, this function will be triggered''' self._keep_toolbar_open = False if self._timer: GObject.source_remove(self._timer) if self._toolbar_enabled: toolbar.show() def on_focus_out(self, treeview, event, toolbar): '''After a table is deselected, this function will be triggered''' def receive_alarm(): if self._keep_toolbar_open: self._timer = None if self._timer: self._timer = None treeview.get_selection().unselect_all() if self._toolbar_enabled: toolbar.hide() return False self._timer = GObject.timeout_add(500, receive_alarm) def create_toolbar(self): '''This function creates a toolbar which is displayed next to the table''' toolbar = Gtk.Toolbar() toolbar.set_orientation(Gtk.Orientation.HORIZONTAL) toolbar.set_style(Gtk.ToolbarStyle.ICONS) toolbar.set_border_width(1) for pos, stock, handler, data, tooltip in ( (0, Gtk.STOCK_ADD, self.on_add_row, None, _('Add row')), # T: tooltip on mouse hover (1, Gtk.STOCK_DELETE, self.on_delete_row, None, _('Remove row')), # T: tooltip on mouse hover (2, Gtk.STOCK_COPY, self.on_clone_row, None, _('Clone row')), # T: tooltip on mouse hover (3, None, None, None, None), (4, Gtk.STOCK_GO_UP, self.on_move_row, -1, _('Row up')), # T: tooltip on mouse hover (5, Gtk.STOCK_GO_DOWN, self.on_move_row, 1, _('Row down')), # T: tooltip on mouse hover (6, None, None, None, None), (7, Gtk.STOCK_PREFERENCES, self.on_change_columns, None, _('Change columns')), # T: tooltip on mouse hover (8, None, None, None, None), (9, Gtk.STOCK_HELP, self.on_open_help, None, _('Open help')), # T: tooltip on mouse hover ): if stock is None: toolbar.insert(Gtk.SeparatorToolItem(), pos) else: button = Gtk.ToolButton(stock) if data: button.connect('clicked', handler, data) else: button.connect('clicked', handler) button.set_tooltip_text(tooltip) toolbar.insert(button, pos) toolbar.set_size_request(300, -1) toolbar.set_icon_size(Gtk.IconSize.MENU) return toolbar def _column_alignment(self, aligntext): ''' The column alignment must be converted from numeric to keywords ''' if aligntext == 'left': align = 0.0 elif aligntext == 'center': align = 0.5 elif aligntext == 'right': align = 1.0 else: align = None return align def create_treeview(self, model): '''Initializes a treeview with its model (liststore) and all its columns''' treeview = Gtk.TreeView(model.liststore) # Set default sorting function. model.liststore.set_default_sort_func(lambda *a: 0) aligns = model.get_aligns() wraps = model.get_wraps() for i, headcol in enumerate(model.headers): cell = Gtk.CellRendererText() tview_column = Gtk.TreeViewColumn(headcol, cell) tview_column.set_sizing( Gtk.TreeViewColumnSizing.AUTOSIZE) # allow column shrinks treeview.append_column(tview_column) # set title as label header_label = self.create_headerlabel(headcol) tview_column.set_widget(header_label) # set properties of column tview_column.set_attributes(cell, markup=i) cell.set_property('editable', True) cell.set_property( 'yalign', 0.0) # no vertical alignment, text starts on the top tview_column.set_sort_column_id(i) # set sort function model.liststore.set_sort_func(i, self.sort_by_number_or_string, i) # set alignment - left center right align = self._column_alignment(aligns[i]) if align: tview_column.set_alignment(align) cell.set_alignment(align, 0.0) # set wrap mode, wrap-size is set elsewhere if wraps[i]: cell.set_property('wrap-mode', Pango.WrapMode.WORD) # callbacks after an action cell.connect('edited', self.on_cell_changed, treeview.get_model(), i) cell.connect('editing-started', self.on_cell_editing_started, treeview.get_model(), i) cell.connect('editing-canceled', self.on_cell_editing_canceled) return treeview def create_headerlabel(self, title): return TableViewWidget.create_headerlabel(title) @staticmethod def create_headerlabel(title): ''' Sets options for the treeview header''' col_widget = Gtk.VBox() col_widget.show() col_label = Gtk.Label(label='<u>' + title + '</u>') col_label.set_use_markup(True) col_label.show() col_widget.pack_start(col_label, True, True, 0) #col_align.add(col_label) '''col_entry = InputEntry() col_entry.set_name('treeview-header-entry') col_entry.show() col_widget.pack_start(col_entry, True, True, 0)''' return col_widget def get_treeview(self): # treeview of current table return self.treeview def set_preferences(self, preferences): self._toolbar_enabled = preferences.get('show_helper_toolbar', True) self.treeview.set_grid_lines(GTK_GRIDLINES[preferences.get( 'grid_lines', LINES_BOTH)]) def on_move_cursor(self, view, step_size, count): ''' If you try to move the cursor out of the tableditor release the cursor to the parent textview ''' return None # let parent handle this signal def fetch_cell_by_event(self, event, treeview): ''' Looks for the cell where the mouse clicked on it ''' liststore = treeview.get_model() (xpos, ypos) = event.get_coords() (treepath, treecol, xrel, yrel) = treeview.get_path_at_pos(int(xpos), int(ypos)) treeiter = liststore.get_iter(treepath) cellvalue = liststore.get_value(treeiter, treeview.get_columns().index(treecol)) return cellvalue def get_linkurl(self, celltext): ''' Checks a cellvalue if it contains a link and returns only the link value ''' linkregex = r'<span foreground="blue">.*?<span.*?>(.*?)</span></span>' matches = re.match(linkregex, celltext) linkvalue = matches.group(1) if matches else None return linkvalue def on_button_press_event(self, treeview, event): ''' Displays a context-menu on right button click Opens the link of a tablecell on CTRL pressed and left button click ''' if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 1 and event.get_state( ) & Gdk.ModifierType.CONTROL_MASK: # With CTRL + LEFT-Mouse-Click link of cell is opened cellvalue = self.fetch_cell_by_event(event, treeview) linkvalue = self.get_linkurl(cellvalue) if linkvalue: self.emit('link-clicked', {'href': str(linkvalue)}) return if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3: # Right button opens context menu self._keep_toolbar_open = True cellvalue = self.fetch_cell_by_event(event, treeview) linkvalue = self.get_linkurl(cellvalue) linkitem_is_activated = (linkvalue is not None) menu = Gtk.Menu() for stock, handler, data, tooltip in ( (Gtk.STOCK_ADD, self.on_add_row, None, _('Add row')), # T: menu item (Gtk.STOCK_DELETE, self.on_delete_row, None, _('Delete row')), # T: menu item (Gtk.STOCK_COPY, self.on_clone_row, None, _('Clone row')), # T: menu item (None, None, None, None), # T: menu item (Gtk.STOCK_JUMP_TO, self.on_open_link, linkvalue, _('Open cell content link')), # T: menu item (None, None, None, None), (Gtk.STOCK_GO_UP, self.on_move_row, -1, _('Row up')), # T: menu item (Gtk.STOCK_GO_DOWN, self.on_move_row, 1, _('Row down')), # T: menu item (None, None, None, None), (Gtk.STOCK_PREFERENCES, self.on_change_columns, None, _('Change columns')) # T: menu item ): if stock is None: menu.append(Gtk.SeparatorMenuItem()) else: item = Gtk.ImageMenuItem(stock) item.set_always_show_image(True) item.set_label(_(tooltip)) if data: item.connect_after('activate', handler, data) else: item.connect_after('activate', handler) if handler == self.on_open_link: item.set_sensitive(linkitem_is_activated) menu.append(item) menu.show_all() gtk_popup_at_pointer(menu, event) def on_add_row(self, action): ''' Context menu: Add a row ''' selection = self.treeview.get_selection() model, treeiter = selection.get_selected() if not treeiter: # no selected item self.selection_info() return # Set default sorting. model.set_sort_column_id(-1, Gtk.SortType.ASCENDING) row = len(self.treeview.get_columns()) * [''] path = model.insert_after(treeiter, row) def on_clone_row(self, action): ''' Context menu: Clone a row ''' selection = self.treeview.get_selection() model, treeiter = selection.get_selected() if not treeiter: # no selected item self.selection_info() return path = model.get_path(treeiter) row = list(model[path[0]]) # copy model.insert_after(treeiter, row) def on_delete_row(self, action): ''' Context menu: Delete a row ''' selection = self.treeview.get_selection() model, treeiter = selection.get_selected() if not treeiter: # no selected item self.selection_info() return if len(model) > 1: model.remove(treeiter) else: md = Gtk.MessageDialog( None, Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.WARNING, Gtk.ButtonsType.CLOSE, _("The table must consist of at least on row!\n No deletion done." )) # T: Popup dialog md.run() md.destroy() def on_move_row(self, action, direction): ''' Trigger for moving a row one position up/down ''' selection = self.treeview.get_selection() model, treeiter = selection.get_selected() if not treeiter: # no selected item self.selection_info() return path = model.get_path(treeiter) newpos = path[0] + direction if 0 > newpos or newpos >= len( model ): # first item cannot be pushed forward, last not backwards return newiter = model.get_iter((newpos, )) # Set default sorting. model.set_sort_column_id(-1, Gtk.SortType.ASCENDING) # Change values of two rows. for col in range(model.get_n_columns()): value = model.get_value(treeiter, col) newvalue = model.get_value(newiter, col) model.set_value(newiter, col, value) model.set_value(treeiter, col, newvalue) def on_open_link(self, action, link): ''' Context menu: Open a link, which is written in a cell ''' self.emit('link-clicked', {'href': str(link)}) def on_open_help(self, action): ''' Context menu: Open help ''' ZIM_APPLICATION.run('--manual', 'Plugins:Table Editor') def on_change_columns(self, action): ''' Context menu: Edit table, run the EditTableDialog ''' aligns = self.model.get_aligns() wraps = self.model.get_wraps() headers = [col.get_title() for col in self.treeview.get_columns()] ids = [i for i in range(len(headers))] definition = ids, headers, wraps, aligns newdefinition = EditTableDialog(self.get_toplevel(), definition).run() if newdefinition: self.model.change_model( newdefinition) # Will call back to change our treeview def on_cell_changed(self, cellrenderer, path, text, liststore, colid): ''' Trigger after cell-editing, to transform displayed table cell into right format ''' self._keep_toolbar_open = False markup = CellFormatReplacer.input_to_cell(text) liststore[path][colid] = markup self._cellinput_canceled = False def on_cell_editing_started(self, cellrenderer, editable, path, liststore, colid): ''' Trigger before cell-editing, to transform text-field data into right format ''' self._keep_toolbar_open = True editable.connect('focus-out-event', self.on_cell_focus_out, cellrenderer, path, liststore, colid) markup = liststore[path][colid] markup = CellFormatReplacer.cell_to_input(markup) editable.set_text(markup) self._cellinput_canceled = False def on_cell_focus_out(self, editable, event, cellrenderer, path, liststore, colid): if not self._cellinput_canceled: self.on_cell_changed(cellrenderer, path, editable.get_text(), liststore, colid) def on_cell_editing_canceled(self, renderer): ''' Trigger after a cell is edited but any change is skipped ''' self._cellinput_canceled = True def sort_by_number_or_string(self, liststore, treeiter1, treeiter2, colid): ''' Sort algorithm for sorting numbers correctly and putting 10 after 3. This part can be improved in future to support also currencies, dates, floats, etc. :param liststore: model of treeview :param treeiter1: treeiter 1 :param treeiter2: treeiter 2 :param colid: a column number :return: -1 / first data is smaller than second, 0 / equality, 1 / else ''' data1 = natural_sort_key(liststore.get_value(treeiter1, colid)) data2 = natural_sort_key(liststore.get_value(treeiter2, colid)) return (data1 > data2) - (data1 < data2) # python3 jargon for "cmp()" def selection_info(self): ''' Info-Popup for selecting a cell before this action can be done ''' md = Gtk.MessageDialog( None, Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.WARNING, Gtk.ButtonsType.CLOSE, _("Please select a row, before you push the button.")) # T: md.run() md.destroy()
class TableViewWidget(CustomObjectWidget): __gsignals__ = { 'size-request': 'override', } def __init__(self, obj, liststore, headers, attrs): ''' This is a group of GTK Gui elements which are directly displayed within the wiki textarea On initilizing also some signals are registered and a toolbar is initialized :param obj: a Table-View-Object :param liststore: a gtk.ListStore object :param headers: list of titles :param attrs: table settings, like alignment and wrapping :return: ''' CustomObjectWidget.__init__(self) self.textarea_width = 0 # used in pageview self._has_cursor = False # Skip table object, if someone moves cursor around in textview # used here self.obj = obj self._timer = None # NONE or number of current gobject.timer, which is running self._keep_toolbar_open = False # a cell is currently edited, toolbar should not be hidden self._cellinput_canceled = None # cell changes should be skipped self._toolbar_enabled = True # sets if toolbar should be shown beneath a selected table # Toolbar for table actions self.toolbar = self.create_toolbar() self.toolbar.show_all() self.toolbar.set_no_show_all(True) self.toolbar.hide() # Create treeview self._init_treeview(liststore, headers, attrs) # package gui elements self.vbox.pack_end(self.toolbar) self.scroll_win = ScrolledWindow(self.treeview, gtk.POLICY_NEVER, gtk.POLICY_NEVER, gtk.SHADOW_NONE) self.vbox.pack_start(self.scroll_win) def _init_treeview(self, liststore, headers, attrs): # Actual gtk table object self.treeview = self.create_treeview(liststore, headers, attrs) # Hook up signals & set options self.treeview.connect('button-press-event', self.on_button_press_event) self.treeview.connect('focus-in-event', self.on_focus_in, self.toolbar) self.treeview.connect('focus-out-event', self.on_focus_out, self.toolbar) self.treeview.connect('move-cursor', self.on_move_cursor) # Set options self.treeview.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_BOTH) self.treeview.set_receives_default(True) self.treeview.set_size_request(-1, -1) self.treeview.set_border_width(2) # disable interactive column search self.treeview.set_enable_search(False) gtk.binding_entry_remove(gtk.TreeView, gtk.keysyms.f, gtk.gdk.CONTROL_MASK) self.treeview.set_search_column(-1) def on_model_changed(self, liststore, headers, attrs): '''Called by TableViewObject when columns changed, replaces the treeview idget with a new one for the new model ''' self.scroll_win.remove(self.treeview) self._init_treeview(liststore, headers, attrs) self.scroll_win.add(self.treeview) self.scroll_win.show_all() def do_size_request(self, requisition): wraps = self.obj.get_wraps() if not any(wraps): return CustomObjectWidget.do_size_request(self, requisition) # Negotiate how to wrap .. for col in self.treeview.get_columns(): cr = col.get_cell_renderers()[0] cr.set_property('wrap-width', -1) # reset size #~ col.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE) # allow column shrinks #~ col.set_max_width(0) # shrink column #~ col.set_max_width(-1) # reset value #~ col.set_sizing(gtk.TREE_VIEW_COLUMN_GROW_ONLY) # reset value CustomObjectWidget.do_size_request(self, requisition) #~ print "Widget requests: %i textview: %i" % (requisition.width, self._textview_width) if requisition.width > self._textview_width: # Figure out width of fixed cols fixed = 0 for col, wrap in zip(self.treeview.get_columns(), wraps): if not wrap: fixed += col.get_width() nwrap = sum(wraps) wrap_size = (self._textview_width - fixed) // nwrap # Set width for wrappable cols #~ print "Fixed, nwrap, wrap_size", (fixed, nwrap, wrap_size) for col, wrap in zip(self.treeview.get_columns(), wraps): if wrap: cr = col.get_cell_renderers()[0] cr.set_property('wrap-width', wrap_size) # reset size # Update request CustomObjectWidget.do_size_request(self, requisition) else: pass def on_focus_in(self, treeview, event, toolbar): '''After a table is selected, this function will be triggered''' self._keep_toolbar_open = False if self._timer: gobject.source_remove(self._timer) if self._toolbar_enabled: toolbar.show() def on_focus_out(self, treeview, event, toolbar): '''After a table is deselected, this function will be triggered''' def receive_alarm(): if self._keep_toolbar_open: self._timer = None if self._timer: self._timer = None treeview.get_selection().unselect_all() if self._toolbar_enabled: toolbar.hide() return False self._timer = gobject.timeout_add(500, receive_alarm) def create_toolbar(self): '''This function creates a toolbar which is displayed next to the table''' toolbar = gtk.Toolbar() toolbar.set_orientation(gtk.ORIENTATION_HORIZONTAL) toolbar.set_style(gtk.TOOLBAR_ICONS) toolbar.set_border_width(1) tooltips = gtk.Tooltips() for pos, stock, handler, data, tooltip in ( (0, gtk.STOCK_ADD, self.on_add_row, None, _('Add row')), # T: tooltip on mouse hover (1, gtk.STOCK_DELETE, self.on_delete_row, None, _('Remove row')), # T: tooltip on mouse hover (2, gtk.STOCK_COPY, self.on_clone_row, None, _('Clone row')), # T: tooltip on mouse hover (3, None, None, None, None), (4, gtk.STOCK_GO_UP, self.on_move_row, -1, _('Row up')), # T: tooltip on mouse hover (5, gtk.STOCK_GO_DOWN, self.on_move_row, 1, _('Row down')), # T: tooltip on mouse hover (6, None, None, None, None), (7, gtk.STOCK_PREFERENCES, self.on_change_columns, None, _('Change columns')), # T: tooltip on mouse hover (8, None, None, None, None), (9, gtk.STOCK_HELP, self.on_open_help, None, _('Open help')), # T: tooltip on mouse hover ): if stock is None: toolbar.insert(gtk.SeparatorToolItem(), pos) else: button = gtk.ToolButton(stock) if data: button.connect('clicked', handler, data) else: button.connect('clicked', handler) tooltips.set_tip(button, tooltip) toolbar.insert(button, pos) toolbar.set_size_request(300, -1) toolbar.set_icon_size(gtk.ICON_SIZE_MENU) return toolbar def _column_alignment(self, aligntext): ''' The column alignment must be converted from numeric to keywords ''' if aligntext == 'left': align = 0.0 elif aligntext == 'center': align = 0.5 elif aligntext == 'right': align = 1.0 else: align = None return align def create_treeview(self, liststore, headers, attrs): ''' Initializes a treeview with its model (liststore) and all its columns :param headers: a list of title values for the column-headers :param rows: a list of list of cells, for the table body :param attrs: some more attributes, which define the layout of a column :return: gtk.treeview ''' treeview = gtk.TreeView(liststore) for i, headcol in enumerate(headers): cell = gtk.CellRendererText() tview_column = gtk.TreeViewColumn(headcol, cell) tview_column.set_sizing( gtk.TREE_VIEW_COLUMN_AUTOSIZE) # allow column shrinks treeview.append_column(tview_column) # set title as label header_label = self.create_headerlabel(headcol) tview_column.set_widget(header_label) # set properties of column tview_column.set_attributes(cell, markup=i) cell.set_property('editable', True) cell.set_property( 'yalign', 0.0) # no vertical alignment, text starts on the top tview_column.set_sort_column_id(i) # set sort function liststore.set_sort_func(i, self.sort_by_number_or_string, i) # set alignment - left center right align = self._column_alignment(attrs['aligns'][i]) if align: tview_column.set_alignment(align) cell.set_alignment(align, 0.0) # set wrap mode, wrap-size is set elsewhere if attrs['wraps'][i]: cell.set_property('wrap-mode', pango.WRAP_WORD) # callbacks after an action cell.connect('edited', self.on_cell_changed, treeview.get_model(), i) cell.connect('editing-started', self.on_cell_editing_started, treeview.get_model(), i) cell.connect('editing-canceled', self.on_cell_editing_canceled) return treeview def create_headerlabel(self, title): return TableViewWidget.create_headerlabel(title) @staticmethod def create_headerlabel(title): ''' Sets options for the treeview header''' col_widget = gtk.VBox() col_widget.show() col_label = gtk.Label('<u>' + title + '</u>') col_label.set_use_markup(True) col_label.show() col_widget.pack_start(col_label) #col_align.add(col_label) '''col_entry = InputEntry() col_entry.set_name('treeview-header-entry') col_entry.show() col_widget.pack_start(col_entry)''' return col_widget def get_treeview(self): # treeview of current table return self.treeview def set_preferences(self, preferences): self._toolbar_enabled = preferences.get('show_helper_toolbar', True) self.treeview.set_grid_lines(GTK_GRIDLINES[preferences.get( 'grid_lines', LINES_BOTH)]) def on_move_cursor(self, view, step_size, count): ''' If you try to move the cursor out of the tableditor release the cursor to the parent textview ''' return None # let parent handle this signal def fetch_cell_by_event(self, event, treeview): ''' Looks for the cell where the mouse clicked on it ''' liststore = treeview.get_model() (xpos, ypos) = event.get_coords() (treepath, treecol, xrel, yrel) = treeview.get_path_at_pos(int(xpos), int(ypos)) treeiter = liststore.get_iter(treepath) cellvalue = liststore.get_value(treeiter, treeview.get_columns().index(treecol)) return cellvalue def get_linkurl(self, celltext): ''' Checks a cellvalue if it contains a link and returns only the link value ''' linkregex = r'<span foreground="blue">.*?<span.*?>(.*?)</span></span>' matches = re.match(linkregex, celltext) linkvalue = matches.group(1) if matches else None return linkvalue def on_button_press_event(self, treeview, event): ''' Displays a context-menu on right button click Opens the link of a tablecell on CTRL pressed and left button click ''' if event.type == gtk.gdk.BUTTON_PRESS and event.button == 1 and event.get_state( ) & gtk.gdk.CONTROL_MASK: # With CTRL + LEFT-Mouse-Click link of cell is opened cellvalue = self.fetch_cell_by_event(event, treeview) linkvalue = self.get_linkurl(cellvalue) if linkvalue: self.obj.emit('link-clicked', {'href': linkvalue}) return if event.type == gtk.gdk.BUTTON_PRESS and event.button == 3: # Right button opens context menu self._keep_toolbar_open = True cellvalue = self.fetch_cell_by_event(event, treeview) linkvalue = self.get_linkurl(cellvalue) linkitem_is_activated = (linkvalue is not None) menu = gtk.Menu() for stock, handler, data, tooltip in ( (gtk.STOCK_ADD, self.on_add_row, None, _('Add row')), # T: menu item (gtk.STOCK_DELETE, self.on_delete_row, None, _('Delete row')), # T: menu item (gtk.STOCK_COPY, self.on_clone_row, None, _('Clone row')), # T: menu item (None, None, None, None), # T: menu item (gtk.STOCK_JUMP_TO, self.on_open_link, linkvalue, _('Open cell content link')), # T: menu item (None, None, None, None), (gtk.STOCK_GO_UP, self.on_move_row, -1, _('Row up')), # T: menu item (gtk.STOCK_GO_DOWN, self.on_move_row, 1, _('Row down')), # T: menu item (None, None, None, None), (gtk.STOCK_PREFERENCES, self.on_change_columns, None, _('Change columns')) # T: menu item ): if stock is None: menu.append(gtk.SeparatorMenuItem()) else: item = gtk.ImageMenuItem(stock) item.set_always_show_image(True) item.set_label(_(tooltip)) if data: item.connect_after('activate', handler, data) else: item.connect_after('activate', handler) if handler == self.on_open_link: item.set_sensitive(linkitem_is_activated) menu.append(item) menu.show_all() menu.popup(None, None, None, event.button, event.time) def on_add_row(self, action): ''' Context menu: Add a row ''' selection = self.treeview.get_selection() model, treeiter = selection.get_selected() if not treeiter: # no selected item self.selection_info() return row = len(self.treeview.get_columns()) * [''] path = model.insert_after(treeiter, row) self.obj.set_modified(True) def on_clone_row(self, action): ''' Context menu: Clone a row ''' selection = self.treeview.get_selection() model, treeiter = selection.get_selected() if not treeiter: # no selected item self.selection_info() return path = model.get_path(treeiter) row = model[path[0]] model.insert_after(treeiter, row) self.obj.set_modified(True) def on_delete_row(self, action): ''' Context menu: Delete a row ''' selection = self.treeview.get_selection() model, treeiter = selection.get_selected() if not treeiter: # no selected item self.selection_info() return if len(model) > 1: model.remove(treeiter) self.obj.set_modified(True) else: md = gtk.MessageDialog( None, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_WARNING, gtk.BUTTONS_CLOSE, _("The table must consist of at least on row!\n No deletion done." )) # T: Popup dialog md.run() md.destroy() def on_move_row(self, action, direction): ''' Trigger for moving a row one position up/down ''' selection = self.treeview.get_selection() model, treeiter = selection.get_selected() if not treeiter: # no selected item self.selection_info() return path = model.get_path(treeiter) newpos = path[0] + direction if 0 > newpos or newpos >= len( model ): # first item cannot be pushed forward, last not backwards return newiter = model.get_iter((newpos, )) model.swap(treeiter, newiter) self.obj.set_modified(True) def on_open_link(self, action, link): ''' Context menu: Open a link, which is written in a cell ''' self.obj.emit('link-clicked', {'href': link}) def on_open_help(self, action): ''' Context menu: Open help ''' get_zim_application('--manual', 'Plugins:Table Editor').spawn() pass def on_change_columns(self, action): ''' Context menu: Edit table, run the EditTableDialog ''' aligns = self.obj.get_aligns() wraps = self.obj.get_wraps() titles = [col.get_title() for col in self.treeview.get_columns()] old_model = [] for i in range(len(titles)): old_model.append([i, titles[i], aligns[i], wraps[i]]) new_model = EditTableDialog(self.get_toplevel(), old_model).run() if new_model: self.obj.change_model( new_model) # Will call back to change our treeview def on_cell_changed(self, cellrenderer, path, text, liststore, colid): ''' Trigger after cell-editing, to transform displayed table cell into right format ''' self._keep_toolbar_open = False markup = CellFormatReplacer.input_to_cell(text, True) liststore[path][colid] = markup self._cellinput_canceled = False def on_cell_editing_started(self, cellrenderer, editable, path, liststore, colid): ''' Trigger before cell-editing, to transform text-field data into right format ''' self._keep_toolbar_open = True editable.connect('focus-out-event', self.on_cell_focus_out, cellrenderer, path, liststore, colid) markup = liststore[path][colid] markup = CellFormatReplacer.cell_to_input(markup, True) editable.set_text(markup) self._cellinput_canceled = False def on_cell_focus_out(self, editable, event, cellrenderer, path, liststore, colid): if not self._cellinput_canceled: self.on_cell_changed(cellrenderer, path, editable.get_text(), liststore, colid) def on_cell_editing_canceled(self, renderer): ''' Trigger after a cell is edited but any change is skipped ''' self._cellinput_canceled = True def sort_by_number_or_string(self, liststore, treeiter1, treeiter2, colid): ''' Sort algorithm for sorting numbers correctly and putting 10 after 3. This part can be improved in future to support also currencies, dates, floats, etc. :param liststore: model of treeview :param treeiter1: treeiter 1 :param treeiter2: treeiter 2 :param colid: a column number :return: -1 / first data is smaller than second, 0 / equality, 1 / else ''' data1 = liststore.get_value(treeiter1, colid) data2 = liststore.get_value(treeiter2, colid) if data1.isdigit() and data2.isdigit(): data1 = int(data1) data2 = int(data2) self.obj.set_modified(True) return cmp(data1, data2) def selection_info(self): ''' Info-Popup for selecting a cell before this action can be done ''' md = gtk.MessageDialog( None, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_WARNING, gtk.BUTTONS_CLOSE, _("Please select a row, before you push the button.")) # T: md.run() md.destroy()