#!/usr/bin/env python3 import os from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import exc from sqlalchemy.sql.expression import func from src.Config import ConfigManager from src.Logging import createLogger logger = createLogger("CommonDAO") db_path = os.path.join(ConfigManager.profile_folder, 'data.db') engine = create_engine('sqlite:///' + db_path, echo=ConfigManager.debugSql) if not os.path.exists(db_path): logger.info("Create database: " + db_path) from .entities.Persistent import Base Base.metadata.create_all(engine) sessionMaker = sessionmaker(bind=engine, expire_on_commit=False) class SessionDAO: _session = None def getSession(self): ''' Get the current session.
class EditorCtrl(BaseController): log = createLogger(__name__) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.ui = EditorUI(self) @ensureLoading def start(self): self.ui.show() def stop(self): self.ui.close() def load(self): self.metatags = metatagsDao.getAll() self.tags = tagsDao.getAll() def setupUpdateEvents(self): super().setupUpdateEvents() self.on_update[UPDATE_METATAGS] = [] self.on_update[UPDATE_TAGS] = [] def _updateTags(self): self.tags = tagsDao.getAll() self.trigger(UPDATE_TAGS) def _updateMetatags(self): self.metatags = metatagsDao.getAll() self.trigger(UPDATE_METATAGS) # Public methods def addTag(self, name, metatag): self.log.info("Add tag %s with metatag %s" % (name, metatag.name)) try: tagsDao.insert(name, metatag) except Exception: return False # Update self._updateTags() return True def addMetatag(self, name): self.log.info("Add metatag %s" % name) try: metatagsDao.insert(name) except Exception: return False # Update self._updateMetatags() return True def deleteTag(self, tag): self.log.info("Delete tag %s" % tag.name) try: tagsDao.delete(tag) except Exception: return False # Update self._updateTags() return True def deleteMetatag(self, metatag): self.log.info("Delete metatag %s" % metatag.name) try: metatagsDao.delete(metatag) except Exception: return False # Update self._updateMetatags() return True def editTag(self, tag, new_name, new_metatag): self.log.info("Edit tag %s" % tag.name) self.log.info("New name %s, new metatag: %s" % (new_name, new_metatag.name)) try: tagsDao.update(tag, new_name, new_metatag) except Exception: return False # Update self._updateTags() return True def editMetatag(self, metatag, new_name): self.log.info("Edit metatag %s" % metatag.name) self.log.info("New name %s" % new_name) try: metatagsDao.update(metatag, name=new_name) except Exception: return False # Update self._updateMetatags() self._updateTags() return True # Update listeners def onUpdateMetatags(self, func): self.onUpdate(UPDATE_METATAGS, func) def onUpdateTags(self, func): self.onUpdate(UPDATE_TAGS, func)
class EditorUI(BaseInterface): log = createLogger(__name__) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.window = None self.log.debug("Done") def _build(self): ''' Build the interface. ''' self.builder = Gtk.Builder() ui_file = os.path.join(GLADE_FOLDER, 'Editor.glade') self.builder.add_from_file(ui_file) self._buildTagsAutocompleteBox() self.window = self.builder.get_object('Main') self.window.resize(400, 100) # Register window app = self.ctrl.services.getApplication() app.add_window(self.window) # Register the update events self.ctrl.onUpdateMetatags(self.onUpdateMetatags) self.ctrl.onUpdateTags(self.onUpdateTags) # Connect signals self.builder.connect_signals(self) def _buildTagsAutocompleteBox(self): self.tags_store = Gtk.ListStore(int, str, int) edit_entry = self.builder.get_object("EditTagSelect") self._buildAutocompletionBox(edit_entry, self.onTagChanged) delete_entry = self.builder.get_object("DeleteTagSelect") self._buildAutocompletionBox(delete_entry) def _buildAutocompletionBox(self, search_entry, on_match_selected=None): ''' Set the autocompletion behaviour. ''' completion = Gtk.EntryCompletion.new() # Set model completion.set_model(self.tags_store) completion.set_text_column(1) # Settings completion.set_inline_completion(False) completion.set_popup_completion(True) completion.set_popup_set_width(True) completion.set_popup_single_match(True) completion.set_inline_selection(True) if on_match_selected is not None: completion.connect("match-selected", on_match_selected) # Add to entry search_entry.set_completion(completion) def show(self): if self.window is None: self._build() self.window.show() def close(self): self.window.close() def setParent(self, window): self.window.set_transient_for(window) # LISTENERS def _updateMetatagSelector(self, selector): selector.remove_all() for metatag in self.ctrl.metatags: selector.append_text(metatag.name) def onUpdateMetatags(self): selector_ids = ["AddTagMetatagSelect", "EditTagMetatagSelect", "EditMetatagSelect", "DeleteMetatagSelect"] for sid in selector_ids: selector = self.builder.get_object(sid) self._updateMetatagSelector(selector) def onUpdateTags(self): self.tags_store.clear() tags_store = [[index, tag, tag.name.lower()] for index, tag in enumerate(self.ctrl.tags)] sorted_tags = sorted(tags_store, key=lambda element: element[2]) for position, tag, _ in sorted_tags: self.tags_store.append([tag.id, tag.name, position]) # SELECTORS GETTERS def _getSelectedTag(self, widget): ''' Return the tag selected in the widget. Widget should extend Gtk.Entry. ''' tag_name = widget.get_text() matched_tags = list(filter(lambda t: t.name == tag_name, self.ctrl.tags)) if(len(matched_tags) == 0): return None else: return matched_tags[0] # SIGNALS def _getWidgetText(self, selector_id): label = self.builder.get_object(selector_id) name = label.get_text() return name.strip() def _getSelectorElement(self, selector_id, model): selector = self.builder.get_object(selector_id) active = selector.get_active() if active == -1: return None else: return model[active] def _resetLabel(self, selector_id): label = self.builder.get_object(selector_id) label.set_text("") def _resetSelector(self, selector_id): selector = self.builder.get_object(selector_id) selector.set_active(-1) def onClose(self, widget): self.ctrl.stop() def onAddMetatag(self, widget): name = self._getWidgetText("AddMetatagName") if len(name) == 0: return success = self.ctrl.addMetatag(name) if success: # Reset self._resetLabel("AddMetatagName") else: self.createMessageDialog("Error", "Could not add the metatag %s" % name) def onAddTag(self, widget): name = self._getWidgetText("AddTagName") metatag = self._getSelectorElement("AddTagMetatagSelect", self.ctrl.metatags) if len(name) == 0 or metatag is None: return success = self.ctrl.addTag(name, metatag) if success: # Reset self._resetLabel("AddTagName") self._resetSelector("AddTagMetatagSelect") else: self.createMessageDialog("Error", "Could not add the tag %s" % name) def onEditMetatag(self, widget): metatag = self._getSelectorElement("EditMetatagSelect", self.ctrl.metatags) new_name = self._getWidgetText("EditMetatagName") if metatag is None or len(new_name) == 0: return success = self.ctrl.editMetatag(metatag, new_name) if success: # Reset self._resetSelector("EditMetatagSelect") self._resetLabel("EditMetatagName") else: self.createMessageDialog("Error", "Could not rename the metatag %s" % metatag.name) def onEditTag(self, widget): tag_widget = self.builder.get_object("EditTagSelect") tag = self._getSelectedTag(tag_widget) new_name = self._getWidgetText("EditTagName") new_metatag = self._getSelectorElement("EditTagMetatagSelect", self.ctrl.metatags) if tag is None or len(new_name) == 0 or new_metatag is None: return success = self.ctrl.editTag(tag, new_name, new_metatag) if success: # Reset self._resetLabel("EditTagSelect") self._resetLabel("EditTagName") self._resetSelector("EditTagMetatagSelect") else: self.createMessageDialog("Error", "Could not edit the tag %s" % tag.name) def onDeleteMetatag(self, widget): metatag = self._getSelectorElement("DeleteMetatagSelect", self.ctrl.metatags) if metatag is None: return onConfirm = lambda : self.onDeleteMetatagReal(metatag) dialog = self.createConfirmationDialog("Confirm deletion", "Do you want to delete the metatag %s ?" % metatag.name, onConfirm) def onDeleteTag(self, widget): tag_widget = self.builder.get_object("DeleteTagSelect") tag = self._getSelectedTag(tag_widget) if tag is None: return onConfirm = lambda : self.onDeleteTagReal(tag) dialog = self.createConfirmationDialog("Confirm deletion", "Do you want to delete the tag %s ?" % tag.name, onConfirm) def onDeleteMetatagReal(self, metatag): success = self.ctrl.deleteMetatag(metatag) if success: self._resetSelector("DeleteMetatagSelect") else: self.createMessageDialog("Error", "Could not delete metatag %s" % metatag.name) def onDeleteTagReal(self, tag): success = self.ctrl.deleteTag(tag) if success: self._resetLabel("DeleteTagSelect") else: self.createMessageDialog("Error", "Could not delete tag %s" % tag.name) def onMetatagChanged(self, widget): metatag = self._getSelectorElement("EditMetatagSelect", self.ctrl.metatags) if metatag is None: return label = self.builder.get_object("EditMetatagName") label.set_text(metatag.name) def onTagChanged(self, widget, model, path): _, _, index = model[path] tag = self.ctrl.tags[index] if tag is None: return # Set name label = self.builder.get_object("EditTagName") label.set_text(tag.name) # Set metatag active_index = -1 for index, metatag in enumerate(self.ctrl.metatags): if metatag.name == tag.metatag.name: active_index = index break selector = self.builder.get_object("EditTagMetatagSelect") selector.set_active(active_index)
class MoverUI(BaseInterface): log = createLogger(__name__) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.window = None self.metatag_selectors = {} self.custom_selectors = {} self.suggested_tags = [] self.log.debug("Done") def _build(self): ''' Build the interface. ''' self.builder = Gtk.Builder() ui_file = os.path.join(GLADE_FOLDER, 'AddFile.glade') self.builder.add_from_file(ui_file) self.window = self.builder.get_object('Main') self.window.resize(WINDOW_WIDTH, WINDOW_HEIGHT) self.window.set_title("Move file to profile: %s" % ConfigManager.getProfileName()) # Register window app = self.ctrl.services.getApplication() self.window.set_icon_from_file(app.icon_path) app.add_window(self.window) # Build custom components self._buildCustom() # Setup target name cell if self.ctrl.target_name_pattern is not None: entry = self.builder.get_object('DestinationName') entry.set_editable(False) # Register the update events self.ctrl.onUpdatePath(self.onPathChange) # Connect signals self.builder.connect_signals(self) def _buildCustom(self): ''' Build custom components. ''' custom_box = self.builder.get_object("CustomSelectors") # Build metatags selectors for metatag, selector_name in self.ctrl.metatags.items(): selector = None if selector_name == SELECTOR_AUTOCOMPLETE: selector = self._buildAutocompletion(metatag) elif selector_name == SELECTOR_COMBOBOX: selector = self._buildComboBox(metatag) self.metatag_selectors[metatag] = selector # Add to UI self._addToCustomSelectors(custom_box, metatag, selector) ''' box = self._buildSelectorBox(metatag, selector) custom_box.add(box) ''' for entry_name, entry in self.ctrl.custom_entries.items(): selector = None if entry["type"] == SELECTOR_FREETEXT: selector = self._buildFreeTextEntry(entry_name, entry) if selector is None: continue self.custom_selectors[entry_name] = selector self._addEntryToCustomSelectors(custom_box, entry_name, selector) custom_box.show_all() self.onSelectorValueChaged() # Build suggested tags pass def show(self): if self.window is None: self._build() self.window.show() def _addToCustomSelectors(self, custom_grid, metatag, selector): self._addEntryToCustomSelectors(custom_grid, metatag.name, selector) def _addEntryToCustomSelectors(self, custom_grid, entry_name, selector): label = Gtk.Label(entry_name + ": ") label.set_halign(Gtk.Align.END) label.set_valign(Gtk.Align.FILL) custom_grid.add(label) selector.set_halign(Gtk.Align.START) selector.set_valign(Gtk.Align.FILL) custom_grid.attach_next_to(selector, label, Gtk.PositionType.RIGHT, 1, 1) def _buildAutocompletion(self, metatag): ''' Build an autocompletion box for a metatag. :param dao.entities.IMetatag: metatag :return: Autocompletion box for the metatag :rtype: Gtk.SearchEntry ''' completion = Gtk.EntryCompletion.new() # Set model model = Gtk.ListStore(str) for tag in metatag.tags: model.append([tag.name]) completion.set_model(model) completion.set_text_column(0) # Settings completion.set_inline_completion(False) completion.set_popup_completion(True) completion.set_popup_set_width(True) completion.set_popup_single_match(True) completion.set_inline_selection(True) # Create entry with completion entry = Gtk.SearchEntry() entry.set_completion(completion) entry.set_width_chars(ENTRIES_WIDTH_CHARS) # Set default value value = self._getMetatagDefaultValue(metatag) if value is not None: entry.set_text(value) # Connect events entry.connect("changed", self.onSelectorChanged, metatag) return entry def _buildComboBox(self, metatag): ''' Build a selector box for a metatag. :param dao.entities.IMetatag: metatag :return: Combo box for the metatag :rtype: Gtk.ComboBoxText ''' # Build the selector selector = Gtk.ComboBoxText() for tag in metatag.tags: selector.append_text(tag.name) # Get default value value = self._getMetatagDefaultValue(metatag) if value is None: selector.set_active(0) else: for i, tag in enumerate(metatag.tags): if tag.name == value: selector.set_active(i) break # Connect signals selector.connect("changed", self.onSelectorChanged, metatag) return selector def _buildFreeTextEntry(self, name, entry): default_value = entry["default-value"] selector = Gtk.Entry() selector.set_text(default_value) selector.set_width_chars(ENTRIES_WIDTH_CHARS) selector.connect("changed", self.onCustomSelectorChanged, name) return selector def _getMetatagDefaultValue(self, metatag): ''' Get the default value for a metatag. :param dao.entitis.IMetatag: metatag :returns: Default value :rtype: str ''' value = None if self.ctrl.default_values is not None and \ DEFAULT_METATAGS in self.ctrl.default_values and \ metatag.name in self.ctrl.default_values[DEFAULT_METATAGS]: value = self.ctrl.default_values[DEFAULT_METATAGS][metatag.name] return value def _buildSuggestedTagList(self): ''' Build the suggested tags list. ''' pass def _getSelectorValue(self, metatag, widget): ''' Get the value of a custom selector. :param Gtk.ComboBoxText or Gtk.Entry: Custom selector :param dao.entities.IMetatag: metatag associated to the seletor :return: Value of the selector :rtype: str ''' if type(widget) == Gtk.ComboBoxText: active = widget.get_active() tag = metatag.tags[active] value = tag.name else: value = widget.get_text().strip() return value def _getPatternBasicReplace(self): ''' Common keywords. {_FileExtension}: source filename extension, None if it is a folder ''' replacements = {} _, ext = os.path.splitext(self.ctrl.path) replacements["{_FileExtension}"] = ext return replacements def _getPatternMetatagsReplace(self): ''' Get a dictionary with the replacements. e.g. {'Content' : 'documents'} :return: Dictionary with replacements :rtype: dict str -> srt ''' replacements = {} for metatag, selector in self.metatag_selectors.items(): value = self._getSelectorValue(metatag, selector) replace_key = "{%s}" % metatag.name replacements[replace_key] = value return replacements def _getPatternCustomEntriesReplace(self): replacements = {} for entry_name, selector in self.custom_selectors.items(): value = self._getSelectorValue(None, selector) replace_key = "{%s}" % entry_name replacements[replace_key] = value return replacements def _getPatternAdvancedReplace(self, replacements): ''' Get a dictionary with advanced replacements. These keys can be configured in the config folder/modules/moverKeys.py class. :param dict replacements: Metatag replacements are returned by _getPatternMetatagsReplace :return: Custom replacements :rtype: dict str -> str ''' if self.ctrl.custom_target_keys is None or \ self.ctrl.custom_target_keys_evaluator is None: return {} advanced = {} target_name = self.getDestinationName() self.log.info("Target name: %s" % target_name) for key in self.ctrl.custom_target_keys: replace_key = '{' + key + '}' value = self.ctrl.custom_target_keys_evaluator( key, self.ctrl.path, target_name, replacements) advanced[replace_key] = value return advanced def _evaluateTargetFolderPattern(self): ''' Evaluate the target folder pattern using the values in the custom selectors. :return: Evaluated target folder :rtype: str ''' target_folder = self._evaluatePattern(self.ctrl.target_folder_pattern) if target_folder.startswith('/'): target_folder = target_folder[1:] if not target_folder.endswith('/'): target_folder = target_folder + '/' return target_folder def _evaluateTargetNamePattern(self): return self._evaluatePattern(self.ctrl.target_name_pattern) def _evaluatePattern(self, pattern): if pattern is None: return '/' replacements = self._getPatternBasicReplace() replacements.update(self._getPatternMetatagsReplace()) replacements.update(self._getPatternCustomEntriesReplace()) advanced_replacements = self._getPatternAdvancedReplace(replacements) self.log.debug("Advanced replacements %s" % str(advanced_replacements)) for key, value in replacements.items(): pattern = pattern.replace(key, value) for key, value in advanced_replacements.items(): pattern = pattern.replace(key, value) # Replace replicated / pattern = pattern.replace('//', '/') # Remove special characters pattern = pattern.replace('?', '').replace('!', '').replace('*', '').replace(':', '') return pattern def _setDefaultTargetName(self, name): if self.ctrl.default_values is not None: if DEFAULT_TARGET_NAME in self.ctrl.default_values: name = self.ctrl.default_values[DEFAULT_TARGET_NAME] self.setDestinationName(name) self.onTargetPathChanged() def _getFileTags(self): ''' Get the tags to be applied to the file. :return: list of tuples (tag_name, metatag) :rtype: list of (str, dao.entities.Common.IMetatag) ''' tags = [] for metatag, selector in self.metatag_selectors.items(): name = self._getSelectorValue(metatag, selector) if len(name) > 0: tags.append((name, metatag)) # TODO: Get suggested tags values return tags def _setAddEnabled(self, enable): self.log.debug("Enable button: %r" % enable) btn = self.builder.get_object('AddFileButton') btn.set_sensitive(enable) # Error message def _showErrorMessage(self, msg): label = self.builder.get_object('ErrorMessage') label.set_text(msg) def _hideErrorMessage(self): label = self.builder.get_object('ErrorMessage') label.set_text('') # Custom signals def onSelectorChanged(self, widget, metatag): self.log.debug("Selector for %s changed" % metatag.name) value = self._getSelectorValue(metatag, widget) self.log.debug("New value: %s" % value) self.onSelectorValueChaged() def onCustomSelectorChanged(self, widget, name): self.log.debug("Selector for %s changed" % name) self.onSelectorValueChaged() def onSelectorValueChaged(self): target_folder = self._evaluateTargetFolderPattern() self.setDestinationFolder(target_folder) if self.ctrl.target_name_pattern is not None: target_name = self._evaluateTargetNamePattern() self.setDestinationName(target_name) # Public methods def onPathChange(self): # Set basename basename = os.path.basename(self.ctrl.path) label = self.builder.get_object('SourceName') label.set_text('Source: %s' % basename) # Set default target name self._setDefaultTargetName(basename) # Re evaluate pattern self.onSelectorValueChaged() def setDestinationFolder(self, folder): self.log.debug("Set destination folder: %s" % folder) label = self.builder.get_object('DestinationLabel') label.set_text(folder) def getDestinationFolder(self): label = self.builder.get_object('DestinationLabel') return label.get_text() def getDestinationName(self): entry = self.builder.get_object('DestinationName') return entry.get_text() def setDestinationName(self, name): entry = self.builder.get_object('DestinationName') entry.set_text(name) # SIGNALS def onAdd(self, widget): tags = self._getFileTags() folder = self.getDestinationFolder() name = self.getDestinationName() target = self.ctrl.moveFileTo(folder, name) self.ctrl.addFile(target, tags) self.ctrl.stop() def onCancel(self, widget): self.ctrl.stop() def onUpdateFilename(self, widget): self.onSelectorValueChaged() self.onTargetPathChanged() def onTargetPathChanged(self): folder = self.getDestinationFolder() name = self.getDestinationName() path = self.ctrl.getTargetFullPath(folder, name) self.log.debug("New path: %s" % path) if not os.path.exists(path): self._setAddEnabled(True) self._hideErrorMessage() else: self._setAddEnabled(False) self._showErrorMessage("Target path already exists")
class TaggerUI(BaseInterface): log = createLogger(__name__) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.window = None def _build(self): ''' Build the interface. Should be called only once. ''' self.log.info("Building...") self.builder = Gtk.Builder() ui_file = os.path.join(GLADE_FOLDER, 'Tagger.glade') self.builder.add_from_file(ui_file) # Create main window self.window = self.builder.get_object("TagFile") self.window.resize(400, 600) # Set window title fname = os.path.basename(self.ctrl.file.name) self.window.set_title("Tag: " + fname) # Register main window app = self.ctrl.services.getApplication() self.window.set_icon_from_file(app.icon_path) app.add_window(self.window) # Setup ui variables self.tags_store = Gtk.ListStore(int, str, int) self.metatag_tagstore = {} self.metatag_view = {} self.metatag_selected = None self.tag_selected = None # Build interface self._buildAutocompletionBox() # Register the create events self.ctrl.onUpdateMetatags(self.updateMetatagList) self.ctrl.onUpdateTags(self.updateTagsList) self.ctrl.onUpdateTags(self.updateTagsGrids) self.ctrl.onUpdateFileTag(self.onFileTagChanged) # Add a signal handler self.builder.connect_signals(self) def show(self): ''' Show the main window. ''' if self.window is None: self._build() self.window.show() def _buildAutocompletionBox(self): ''' Set the autocompletion behaviour. ''' completion = Gtk.EntryCompletion.new() # Set model completion.set_model(self.tags_store) completion.set_text_column(1) # Settings completion.set_inline_completion(False) completion.set_popup_completion(True) completion.set_popup_set_width(True) completion.set_popup_single_match(True) completion.set_inline_selection(True) # Connect completion.connect("match-selected", self.onTagCompletionSelected) # Add to entry sentry = self.builder.get_object("TFSearchEntry") sentry.set_completion(completion) def _buildTagsGridFor(self, metatag): ''' Build the tags grid for the given metatag :param dao.Common.IMetatag metatag :return: List of tags associated :rtype: Gtk.ListStore ''' self.metatag_tagstore[metatag.id] = Gtk.ListStore(int, str, bool) # Treeview treeview = Gtk.TreeView(model=self.metatag_tagstore[metatag.id]) treeview.set_hexpand(True) # Cell renderer renderer_toggle = Gtk.CellRendererToggle() column_toggle = Gtk.TreeViewColumn("", renderer_toggle, active=2) renderer_toggle.connect("toggled", self.onTagToggled, self.metatag_tagstore[metatag.id]) treeview.append_column(column_toggle) renderer_text = Gtk.CellRendererText() column_text = Gtk.TreeViewColumn("Tag", renderer_text, text=1) treeview.append_column(column_text) treeview.set_search_column(1) # Add scrolled window scrolled_view = Gtk.ScrolledWindow() scrolled_view.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) scrolled_view.add(treeview) scrolled_view.set_vexpand(True) self.metatag_view[metatag.id] = scrolled_view # Add to the main window all_tags_grid = self.builder.get_object("TFTagsBox") all_tags_grid.add(scrolled_view) return self.metatag_tagstore[metatag.id] @inhibitSignals def updateMetatagList(self): ''' Update the metatag selectors. ''' # Grid selector selector = self.builder.get_object("TFCategorySelector") selector.remove_all() for metatag in self.ctrl.metatags: selector.append_text(metatag.name) # Add tag selector add_selector = self.builder.get_object("TFAddTagCategory") add_selector.remove_all() for metatag in self.ctrl.metatags: add_selector.append_text(metatag.name) # Tags grids for metatag in self.ctrl.metatags: if not metatag.id in self.metatag_tagstore: self._buildTagsGridFor(metatag) if len(self.ctrl.metatags) > 0: selector.set_active(0) add_selector.set_active(0) self._setMetatag(self.ctrl.metatags[0]) def updateTagsList(self): ''' Update the tags list. ''' self.tags_store.clear() sorted_tags = sorted(self.ctrl.tags, key=lambda tag: tag.name.lower()) for tag in sorted_tags: if self.ctrl.isTagInAutocomplete(tag): self.tags_store.append([tag.id, tag.name, tag.metatag.id]) def updateTagsGrids(self): ''' Update the tags grids. ''' # Clear all for metatag in self.ctrl.metatags: if metatag.id in self.metatag_tagstore: self.metatag_tagstore[metatag.id].clear() # Populate for tag in self.ctrl.tags: metatag = tag.metatag if metatag.id in self.metatag_tagstore: self.metatag_tagstore[metatag.id].append( [tag.id, tag.name, self.ctrl.fileHasTag(tag.id)]) def _setMetatag(self, metatag): ''' Set the metatag in the selector. ''' # Hide old if self.metatag_selected is not None: self.metatag_view[self.metatag_selected.id].hide() # Set and show new self.metatag_selected = metatag self.metatag_view[self.metatag_selected.id].show_all() def _setTagToFile(self, tag_id, add): ''' Toggle a tag. :param int tag_id: tag id :param bool add: True if I should add the tag, False otherwise ''' if add: self.ctrl.addTagToFile(tag_id) else: self.ctrl.removeTagFromFile(tag_id) def onFileTagChanged(self, tag=None, added=True): ''' When a tag has been added or removed from a file ''' if tag is None: return # Edit all the list_store if tag.metatag.id in self.metatag_tagstore: store = self.metatag_tagstore[tag.metatag.id] for i in range(len(store)): tag_id, _, _ = store[i] if tag_id == tag.id: store[i][-1] = added break # Edit the tag_selected button if present if self.tag_selected == tag.id: btn = self.builder.get_object('TFSearchTagValue') btn.set_active(added) def onCreateMetatag(self, widget): entry = self.builder.get_object("TFAddCategoryName") name = entry.get_text().strip() if len(name) > 0: self.ctrl.createMetatag(name) def onCreateTag(self, widget): entry = self.builder.get_object("TFAddTagName") name = entry.get_text().strip() if len(name) == 0: return True m_selector = self.builder.get_object("TFAddTagCategory") metatag = self.ctrl.metatags[m_selector.get_active()] tag = self.ctrl.createTag(name, metatag) self.ctrl.addTagToFile(tag.id) # SIGNALS def onTagCompletionSelected(self, widget, model, path): ''' On tag selected from autocompletion. ''' tag_id, tag_name, metatag_id = model[path] self.tag_selected = tag_id # Show name label = self.builder.get_object("TFSearchTagName") label.set_text(tag_name) label.show() # Clear entry entry = self.builder.get_object("TFSearchEntry") entry.set_text("") # Set/Unset btn = self.builder.get_object('TFSearchTagValue') btn.set_active(True) btn.show() self._setTagToFile(self.tag_selected, True) # Return True otherwise the match-selected default behaviour # overwrites the text inside the entry # see: https://developer.gnome.org/gtk3/stable/GtkEntryCompletion.html#GtkEntryCompletion-match-selected return True @withInhibit def onMetatagChange(self, widget): ''' On metagag changed event. ''' index = widget.get_active() self._setMetatag(self.ctrl.metatags[index]) def onTagToggled(self, widget, path=None, model=None): ''' On tag toggled (from the autocompletion box or tag grid). ''' if model is None: tag = self.tag_selected add = widget.get_active() else: tag = model[path][0] add = not model[path][2] if tag is None: return self._setTagToFile(tag, add) return True def onCloseClicked(self, widget): self.log.info("Stop the controller") self.ctrl.stop() def onDestroy(self, widget): self.log.info("Stop the controller") self.ctrl.stop(False)
class MoverCtrl(BaseController): log = createLogger(__name__) def __init__(self, services, path): super().__init__(services) self.path = path self.loadConfiguration() self.ui = MoverUI(self) def loadConfiguration(self): self.metatags = {} metatags = ConfigManager.UI.getMoverMetatags() if metatags is not None: for name, selector in metatags.items(): metatag = metatagsDao.getByName(name) self.metatags[metatag] = selector self.custom_entries = ConfigManager.UI.getMoverCustomEntries() self.target_folder_pattern = ConfigManager.UI.getMoverTargetFolder() self.target_name_pattern = ConfigManager.UI.getMoverTargetName() self.custom_target_keys = ConfigManager.UI.getMoverCustomPatternKeys() if self.custom_target_keys is not None: self.custom_target_keys_evaluator = self._loadCustomKeysEvaluator() self.default_values = self._getDefaultValues() def _loadCustomKeysEvaluator(self): module_path = ConfigManager.UI.getMoverCustomPatternKeysEvaluator() self.log.debug("Try to load %s" % module_path) custom = loadModuleFromPath("custom.keys", module_path) if custom is None: return None else: return custom.evaluate def _getDefaultValues(self): ''' Get the default target name for the given path and some automatic tag. ''' module_path = ConfigManager.UI.getMoverDefaultValues() self.log.debug("Try to load %s" % module_path) if not os.path.exists(module_path): return None custom = loadModuleFromPath("custom.values", module_path) if custom is None: return None else: return custom.values(self.path) def setupUpdateEvents(self): ''' Initialize the lists of the functions to call on update event. ''' self.on_update = {} self.on_update[UPDATE_PATH] = [] @ensureLoading def start(self): self.ui.show() def stop(self, close_ui=True): self.log.info("Stopping controller") if close_ui: self.log.info("Close ui") self.ui.close() return self.log.info("Ui closed, cleanup if necessary") def getTargetFullPath(self, folder, fname): ''' Get the full path for a file saved with the given name in a folder in the root. :param str folder: target folder :param str fname: target name :return: target path :rtype: str ''' target_base = os.path.join(folder, fname) while target_base.startswith('/'): target_base = target_base[1:] return os.path.join(ConfigManager.getRoot(), target_base) def moveFileTo(self, folder, fname): ''' Move the instance file to the given folder with the given name. :param str folder: target folder :param str fname: target name :return: target path :rtype: str ''' target = self.getTargetFullPath(folder, fname) self.log.info("Moving file from %s to %s" % (self.path, target)) folder = os.path.dirname(target) if not os.path.isdir(folder): self.log.debug("Create folder %s" % folder) os.makedirs(folder) shutil.move(self.path, target) return target def addFile(self, path, tags): ''' Open the tagger for a file. :param str path: Path of the file :param list of str: Tags names to add to the file :return: File added :rtype: dao.entities.Common.IFile ''' self.log.info("Add file %s" % path) file = addFile(path) self.log.info("Added file #%d, name: %s, mime: %s" % (file.id, file.name, file.mime)) # Apply tags session = tagsDao.openSession() filesDao.setSession(session) for tag_info in tags: name, metatag = tag_info self.log.info("Add tag %s" % name) tag = tagsDao.getByName(name) if tag is None: self.log.info("Create tag %s [%s]" % (name, metatag.name)) tag = tagsDao.insert(name, metatag) file = filesDao.addTag(file, tag) tagsDao.closeSession(commit=True) filesDao.closeSession(commit=True) # Open file if ConfigManager.UI.getMoverOpenOnTag(): folder = os.path.join(file.relpath, file.name) openFile(folder) self.services.getApplication().openTagger(file) return file # Update listeners def onUpdatePath(self, func): self.onUpdate(UPDATE_PATH, func)
class BrowserCtrl(BaseController): log = createLogger(__name__) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.log.debug("Initialize") self.ui = BrowserUI(self) self.log.debug("Done") def setupUpdateEvents(self): super().setupUpdateEvents() self.on_update[UPDATE_METATAGS] = [] self.on_update[UPDATE_TAGS] = [] self.on_update[UPDATE_FILES] = [] self.on_update[UPDATE_USED_TAGS] = [] self.on_update[UPDATE_AVAILABLE_METATAGS] = [] self.on_update[UPDATE_AVAILABLE_TAGS] = [] @ensureLoading def start(self): self.ui.show() def load(self): self.metatags = metatagsDao.getAll() self.tags = tagsDao.getAll() self.used_tags = [] self.name_filter = None self.files = self._getFiles(self.used_tags) self.available_tags = self._getAvailableTags(self.files) self.available_metatags = self._getAvailableMetatags( self.available_tags) def _getRandomFiles(self): ''' Get a list of random files. :return: A list of random files :rtype: list of dao.entities.Common.IFile ''' return filesDao.getRandom(START_RANDOM_FILES) def _getAvailableTags(self, files): ''' Get the tags useful to filter on the given files. :param list of IFile files: Files :return: list of tags :rtype: list of dao.entities.Common.ITag ''' if len(self.used_tags) == 0 and not self.name_filter: # Get all the tags with at least one file tagged return tagsDao.getAllWithOneFileTagged() # Get the common tags used_ids = list(map(lambda t: t.id, self.used_tags)) if self.name_filter is not None: name_like = "%" + self.name_filter + "%" common_tags = tagsDao.getRelatedTags(used_ids, name_like=name_like) else: common_tags = tagsDao.getRelatedTags(used_ids) # Remove the used tags available_tags = [] for tag in common_tags: if tag.id not in used_ids: available_tags.append(tag) return available_tags def _getAvailableMetatags(self, tags): metatags = {} for tag in tags: if not tag.metatag.id in metatags: metatags[tag.metatag.id] = tag.metatag return sorted(list(metatags.values()), key=lambda m: m.name.lower()) def _getFiles(self, tags, name=None): if len(tags) == 0 and name is None: if ConfigManager.UI.getRandomize(): return self._getRandomFiles() else: return [] return filesDao.getByNameAndTags(name=name, tags=tags) def addTag(self, tag): self.used_tags.append(tag) self._searchFiles() def removeTag(self, tag): self.used_tags.remove(tag) self._searchFiles() def addNameFilter(self, name): if name is None: self.name_filter = None else: self.name_filter = '%'.join(name.strip().split()) if self.name_filter == '': self.name_filter = None else: self.name_filter = '%' + self.name_filter + '%' self._searchFiles() def _searchFiles(self): self.files = self._getFiles(self.used_tags, self.name_filter) self.available_tags = self._getAvailableTags(self.files) self.available_metatags = self._getAvailableMetatags( self.available_tags) # Trigger self.trigger(UPDATE_USED_TAGS) self.trigger(UPDATE_FILES) self.trigger(UPDATE_AVAILABLE_METATAGS) self.trigger(UPDATE_AVAILABLE_TAGS) def openTagger(self, file): tagger_ctrl = self.services.getApplication().openTagger(file) def removeFile(self, file): ''' Remove a file from database and filesystem. :param dao.entities.IFileLazy file: File to remove ''' # Remove the file from the system self.log.info("Remove file: %s" % file.name) System.removeFile(file) self.log.debug("Remove from controller files list") # Remove file from the list for cfile in self.files: if cfile.id == file.id: self.files.remove(cfile) break # Update tags and metatags self.log.debug("Update available tags and metatags") self.available_tags = self._getAvailableTags(self.files) self.available_metatags = self._getAvailableMetatags( self.available_tags) self.trigger(UPDATE_FILES) self.trigger(UPDATE_AVAILABLE_METATAGS) self.trigger(UPDATE_AVAILABLE_TAGS) # Update listeners def onUpdateMetags(self, func): self.onUpdate(UPDATE_METATAGS, func) def onUpdateTags(self, func): self.onUpdate(UPDATE_TAGS, func) def onUpdateFiles(self, func): self.onUpdate(UPDATE_FILES, func) def onUpdateUsedTags(self, func): self.onUpdate(UPDATE_USED_TAGS, func) def onUpdateAvailableMetatags(self, func): self.onUpdate(UPDATE_AVAILABLE_METATAGS, func) def onUpdateAvailableTags(self, func): self.onUpdate(UPDATE_AVAILABLE_TAGS, func)
class TaggerCtrl(BaseController): log = createLogger(__name__) def __init__(self, services, file): super().__init__(services) self.loadConfiguration() self.ui = TaggerUI(self) self.file = file # Ensure tags are defined if not hasattr(self.file, 'tags'): self.file = filesDao.getById(self.file.id) self.log.debug("Initialized") def loadConfiguration(self): self.autocomplete_metatags = ConfigManager.UI.getTaggerAutocompleteMetatags( ) def isTagInAutocomplete(self, tag): if self.autocomplete_metatags is None: return True if tag.metatag.name in self.autocomplete_metatags: return True else: return False def setupUpdateEvents(self): super().setupUpdateEvents() self.on_update[UPDATE_METATAGS] = [] self.on_update[UPDATE_TAGS] = [] self.on_update[UPDATE_FILE_TAG] = [] @ensureLoading def start(self): self.ui.show() def stop(self, close_ui=True): self.log.info("Stopping controller") if close_ui: self.log.info("Close ui") self.ui.close() return self.log.info("Ui closed, cleanup if necessary") # TODO def load(self): self.metatags = metatagsDao.getAll() self.tags = tagsDao.getAll() def fileHasTag(self, tag_id): ''' Check if the file has the given tag. :param int tag_id: Tag id :return: True if the file has this tag, False otherwise :rtype: bool ''' in_tags = False for tag in self.file.tags: if tag.id == tag_id: in_tags = True break return in_tags def addTagToFile(self, tag_id): if self.fileHasTag(tag_id): return tag = tagsDao.getById(tag_id) self.file = filesDao.addTag(self.file, tag) self.trigger(UPDATE_FILE_TAG, (tag, True)) def removeTagFromFile(self, tag_id): if not self.fileHasTag(tag_id): return tag = tagsDao.getById(tag_id) self.file = filesDao.removeTag(self.file, tag) self.trigger(UPDATE_FILE_TAG, (tag, False)) def createMetatag(self, name): metatag = metatagsDao.insert(name) self.metatags = metatagsDao.getAll() self.trigger(UPDATE_METATAGS) return metatag def createTag(self, name, metatag): tag = tagsDao.insert(name, metatag) self.tags = tagsDao.getAll() self.trigger(UPDATE_TAGS) return tag # Update listeners def onUpdateMetatags(self, func): self.onUpdate(UPDATE_METATAGS, func) def onUpdateTags(self, func): self.onUpdate(UPDATE_TAGS, func) def onUpdateFileTag(self, func): self.onUpdate(UPDATE_FILE_TAG, func)
class BrowserUI(BaseInterface): log = createLogger(__name__) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.window = None self.log.debug("Done") def _build(self): ''' Build the interface. Should be called only once. ''' self.log.debug("Building...") # Load the builder self.builder = self._loadBuilder() # Set the logo self._setLogo() # Register main window self._loadMainWindow() # Update the UI self._initializeUIVariables() # Setup files view self._setupFilesView() # Resgister events on the controller self._registerEvents() # Add a signal handler self.builder.connect_signals(self) self.log.debug("Done") def _loadBuilder(self): ''' Load the gtk builder. :return: The UI builder :rtype: Gtk.Builder ''' builder = Gtk.Builder() ui_file = os.path.join(GLADE_FOLDER, 'Browser.glade') builder.add_from_file(ui_file) return builder def _setLogo(self): ''' Set the logo. ''' logo = self.builder.get_object("Logo") logo_file = os.path.join(ConfigManager.getOverridesFolder(), "logo.png") if not os.path.exists(logo_file): logo_file = os.path.join(ICONS_FOLDER, "logo.png") logo.set_from_pixbuf(Pixbuf.new_from_file(logo_file)) def _loadMainWindow(self): ''' Load the main window, set the default size and register on the application. ''' self.window = self.builder.get_object("Main") self.window.resize(1300, 800) # Register main window app = self.ctrl.services.getApplication() self.window.set_icon_from_file(app.icon_path) app.add_window(self.window) def _initializeUIVariables(self): ''' Initialize the variables used in the UI. ''' self.metatag_selected = self._getDefaultMetatag() self.selected_tag_name = '' self.files_limit = FILES_LIMIT def _setupFilesView(self): ''' Create setup the files view and create its right-click menu. ''' # Set menu self.files_view_menu = FilesViewMenu.new(self.openFolder, self.openTagger, self.removeFile) # Set files store self.files_store = Gtk.ListStore(int, str, Pixbuf, str) # Setup files view files_view = self.builder.get_object('FilesView') files_view.set_text_column(1) files_view.set_pixbuf_column(2) files_view.set_model(self.files_store) def _registerEvents(self): ''' Register the event listeners on the controller. ''' # Register the create event self.ctrl.onUpdateTags(self.recreateTagsList) self.ctrl.onUpdateAvailableMetatags(self.updateMetatagSelector) # Register the update events self.ctrl.onUpdateAvailableTags(self.updateTagsList) self.ctrl.onUpdateFiles(self.updateFilesList) self.ctrl.onUpdateUsedTags(self.updateUsedTags) def show(self): ''' Show the main window. ''' if self.window is None: self._build() self.window.show() def recreateTagsList(self): self.tags_buttons = {} tags_list = self.builder.get_object("TagsList") # Clean the container self.cleanContainer(tags_list) # Add the tag buttons for tag in self.ctrl.tags: btn = self._createTagButton(tag, self.onTagSelected) tags_list.add(btn) self.tags_buttons[tag.id] = btn @inhibitSignals def updateMetatagSelector(self): ''' Update the metatag selector. ''' selector = self.builder.get_object("MetatagSelector") if len(self.ctrl.available_metatags) == 0: # TODO: better selector.hide() return if self.metatag_selected is None or not self._isCurrentMetatagAvailable( ): self.metatag_selected = self.ctrl.available_metatags[0] # Update the selector selector.remove_all() active_index = -1 for index, metatag in enumerate(self.ctrl.available_metatags): selector.append_text(metatag.name) if metatag.name == self.metatag_selected.name: active_index = index # Set the active metatag if active_index > -1: selector.set_active(active_index) selector.show_all() def updateTagsList(self): ''' Update the tags list. ''' no_tags_label = self.builder.get_object("NoTagsAvailable") # Show/hide the no tags available label if len(self.ctrl.available_tags) == 0: no_tags_label.show() else: no_tags_label.hide() # Hide all the tags tags_list = self.builder.get_object("TagsList") for child in tags_list.get_children(): child.hide() # Show available tags for tag in self.ctrl.available_tags: self.tags_buttons[tag.id].show() # Filter self.filterTagsList() def updateFilesList(self, append=False): if not append: self.files_store.clear() max_files = len(self.ctrl.files) max_index = min(len(self.files_store) + self.files_limit, max_files) for i in range(len(self.files_store), max_index): file = self.ctrl.files[i] pixbuf = self._getFilePixbuf(file) relpath = os.path.join(file.relpath, file.name) self.files_store.append([file.id, file.name, pixbuf, relpath]) files_view = self.builder.get_object('FilesView') files_view.show_all() # Show/hide the load more files button btn = self.builder.get_object("LoadMoreFiles") if len(self.files_store) == max_files: btn.hide() else: btn.show() def updateUsedTags(self): ''' Update the list of the used tags. ''' tags_list = self.builder.get_object("UsedTagsList") self.cleanContainer(tags_list) for tag in self.ctrl.used_tags: btn = self._createTagButton(tag, self.onTagRemoved) tags_list.add(btn) tags_list.show_all() def filterTagsList(self): ''' Hide the tags with metatag not selected or name filtered. ''' for tag in self.ctrl.available_tags: btn = self.tags_buttons[tag.id] if tag.metatag.id == self.metatag_selected.id and \ self.selected_tag_name in tag.name.lower(): btn.show() else: btn.hide() def _createTagButton(self, tag, activate_function=None): ''' Create the select tag button. ''' btn = Gtk.LinkButton() btn.set_label(tag.name) btn.set_size_request(160, 0) lbl = btn.get_children()[0] lbl.set_line_wrap(True) lbl.set_justify(Gtk.Justification.CENTER) if activate_function is not None: btn.connect("activate-link", activate_function, tag) return btn def _getFilePixbuf(self, file): ''' Get the pixbuf for a file. Try to use the thumbnail if possible, fallback to the mime icon. :param dao.entities.Common.IFile file: File :return: Pixbuf for the file :rtype: GdkPixbuf.Pixbuf ''' icon_name = "thumbnails/%d/%d.png" % (ICON_SIZE, file.id) icon_path = os.path.join(ConfigManager.profile_folder, icon_name) pixbuf = None # Try to load the thumbnail if os.path.exists(icon_path): try: pixbuf = Pixbuf.new_from_file(icon_path) except Exception: pixbuf = None # Use the mime icon if pixbuf is None: pixbuf = self._getMimePixbuf(file.mime) return pixbuf def _getMimePixbuf(self, mime): ''' Get the icon pixbuf representing a mime. :param str mime: Mime :return: Pixbuf for the mime :rtype: GdkPixbuf.Pixbuf ''' # Get the icon name theme = Gtk.IconTheme.get_default() name = None for icon_name in self._generateGtkIconNames(mime): if theme.has_icon(icon_name): name = icon_name break # Load the pixbuf try: pixbuf = theme.load_icon(name, ICON_SIZE, 0) except Exception: pixbuf = None return pixbuf def _generateGtkIconNames(self, mime): ''' Generate a list of possible icon names for a given mimetype. :param str mime: Mime :return: Generator of possible icon names :rtype: Generator of str ''' # Specific mime yield mime alt_mime = mime.replace('/', '-') yield alt_mime yield 'gnome-mime-' + alt_mime # Generic mime gmime = mime.split('/')[0] yield gmime yield 'gnome-mime-' + gmime yield Gtk.STOCK_FILE def _isCurrentMetatagAvailable(self): ''' Check if the current metatag is available. ''' if self.metatag_selected is None: return False available = False for metatag in self.ctrl.available_metatags: if metatag.id == self.metatag_selected.id: available = True break return available def _getDefaultMetatag(self): name = ConfigManager.UI.getMetatagName() if name is None: return None default = None for metatag in self.ctrl.available_metatags: if metatag.name == name: default = metatag break return default def _getFileInStore(self, file_id): file = None for cfile in self.ctrl.files: if cfile.id == file_id: file = cfile break return file # Menu options def _getFilesViewSelected(self): ''' Get the file currently selected in the files view. Return only if there is only one file selected. :return: The selected file if any, None otherwise :rtype: dao.entities.IFileLazy ''' filesview = self.builder.get_object('FilesView') paths = filesview.get_selected_items() if len(paths) != 1: return None path = paths[0] file_id = self.files_store[path][0] file = self._getFileInStore(file_id) return file def openFolder(self, widget, data): file = self._getFilesViewSelected() if file is None: return folder = os.path.dirname(os.path.join(file.relpath, file.name)) self.log.info("Opening folder: %s" % folder) openFile(folder) def openTagger(self, widget, data): file = self._getFilesViewSelected() if file is None: return self.log.info("Opening tagger for: %s" % file.name) self.ctrl.openTagger(file) def removeFile(self, widget, data): file = self._getFilesViewSelected() if file is None: return self.log.info("Confirm file removal: %s" % file.name) onConfirm = lambda: self.onRemoveFile(file) dialog = self.createConfirmationDialog( "Confirm deletion", "Do you want to delete %s ?" % file.name, onConfirm) def onRemoveFile(self, file): self.log.info("Delete file: %s" % file.name) self.ctrl.removeFile(file) @withInhibit def onMetatagChange(self, widget): ''' On metagag changed event. ''' index = widget.get_active() self.metatag_selected = self.ctrl.available_metatags[index] self.filterTagsList() def onTagSelected(self, widget, tag): self.ctrl.addTag(tag) return True def onTagRemoved(self, widget, tag): self.ctrl.removeTag(tag) return True def onFilterByFileName(self, widget): if ConfigManager.UI.getFastFilter(): self._onFilterByFileName(widget) def onFilterByFileNameEnter(self, widget): self._onFilterByFileName(widget) def _onFilterByFileName(self, widget): name = widget.get_text() name = name.strip() if len(name) == 0: name = None self.ctrl.addNameFilter(name) def onFilterByTagName(self, widget): self.selected_tag_name = widget.get_text().lower() self.filterTagsList() def onLoadMoreFiles(self, *args, **kwargs): self.files_limit += FILES_LIMIT self.updateFilesList(append=True) def onFileClick(self, icon, treepath): findex = int(treepath.to_string()) relpath = self.files_store[findex][-1] self.log.info("Opening file: %s" % relpath) openFile(relpath) def onButtonPress(self, widget, event): # event.button == 3 iff right-click if event.type != Gdk.EventType.BUTTON_PRESS or event.button != 3: return False coords = event.get_coords() ipath = widget.get_path_at_pos(coords[0], coords[1]) if ipath is None: return False # Unselect other paths widget.unselect_all() # select the element widget.select_path(ipath) # Show right-click menu self.files_view_menu.popup(None, None, None, None, event.button, event.time) return True