The Coppice Blog

News, tips, and sneak peeks

Indie Development & Coppice Delays

Posted:

This blog post is going to be a bit different. You may have noticed that things have been a bit quiet of late. It has been 6 months since Coppice 2021.1.1 was released. And in that time there has only been a single post on this blog. You're probably wondering what is going on with Coppice and when the next version is coming out. So I would like to give an update and explanation.

You'll also have noticed something in that last sentence: "I". Previously on this blog I've used "we" in all posts, though in reality they have all been written by myself: Martin Pilkington, founder and owner of M Cubed Software and creator of Coppice. It has very much being the Royal We. While Coppice does have some outside contributors, its development is almost exclusively carried out by myself.

In a way I've been hiding behind that "we" on this blog, and it has been a bit restricting. So going forward I'm just going to write things on The Coppice Blog in the first person. That way you know you're hearing directly from the person who writes the software you use.

I'd kind of forgotten one of the key assets of being an indie developer is being able to be out there a bit more and be closer to those who are using your apps. Not trying to hide that Coppice is written by a single person also has another benefit: it lets me better explain the process of developing Coppice, including things like delays.

The way I structure my work week is I spend about half of it doing contract work. This helps provide a steady income to allow me to spend the other half of my week working on other projects. Ideally "other projects" would mean "working on Coppice". Unfortunately, 2021 has mostly had other plans.

The lack of updates to Coppice have largely come down to two things taking away that extra time. The first was temporarily needing to spend more time working on contracting. Software development ebbs and flows and occasionally you need to put more time in on some projects, especially when software is getting close to release. So I lost quite a lot of time there.

The second big thing has been moving house. Needless to say, moving house during a pandemic, especially when lots of other people are having a similar idea and causing delays to things like banks and solicitors, is not the most relaxing and peaceful of tasks. I was often finding that the week days I'd set aside for Coppice were mostly being taken up with phone calls, emails, and reading paperwork. And by the weekend I was too exhausted and stressed to code. And this all went on for the best part of 6 months

Sadly the stress didn't end after moving house. Buying new furniture, sorting out problems with the house, and simply getting everything organised is still an ongoing problem, even several months after moving in.

When you're the sole developer, designer, support rep, etc for a company then things like this mean delays to the product. It's often a lot easier for big companies to deal with an individual not having as much focus, and it's easier for that individual to focus if they're only doing one thing. So hopefully being more transparent about the size of the "team" behind Coppice on this blog will make such issues a bit more understandable.

Thankfully, things are starting to settle down a bit more. I'm getting back into the swing of developing Coppice and am having a lot of fun doing so. The next major release, 2021.2, is coming along really well and I'm hoping to have it released before the end of October. It features a bunch of great new features and improvements, including a re-designed Page Selector, a new Link Inspector, and much more.

Coppice's Page Selector window, now with a more compact layout than previously An inspector in Coppice titled Link. It contains a text field with the word 'Link' entered. To the left is a page icon and the right a button with a cross. Below is a menu showing auto-complete options matching the entered text, plus options to create a new text or image page

As always, if you enjoyed this post and want to stay up-to-date on everything to do with Coppice (including an upcoming post with a more in-depth look at Coppice 2021.2), then don't forget to subscribe to The Coppice Blog. Also be sure to follow @coppiceapp on Twitter, where you'll often see retweets from my personal account that show off in-progress features and bugfixes.

Tech Talk: Adding Lists to Coppice

Posted:

Tech Talk is a series of posts revealing some of the inner workings of Coppice and some of the technical challenges we've faced while building it. This time we want to cover how we implemented support for lists in Coppice 2021.1.

Our original plan was to quickly follow up our first release of Coppice with an update to improve text editing. Based on what we saw in Apple's APIs, we had expected this to be a relatively simple job and would have allowed us to get a release out before the end of 2020.

For the most part, we were correct. We managed to implement features such as editing line height and paragraph spacing very quickly and with minimal trouble. However, lists ended up requiring a lot more effort on our end to implement, delaying release for a few months. So what makes lists so much more difficult?

Text Editing 101

Rich text in Cocoa is represented by a class called NSAttributedString. This allows you to apply a series of attributes to parts of text. These attributes cover almost everything you can think of, such as the font, the text colour, the paragraph styling, and much more.

In mosts cases when editing an attribute, you just want to add or remove an attribute for exactly what the user has selected. For example, if you select some text and make it bold, you don't care if the selection is part way through a paragraph or even a word, you only make the selected part bold.

However, certain things require changing attributes beyond the selection. For example, if you select a word but then choose to right align the text, you want it to right align the entire paragraph. This means you need to calculate the range of the paragraph (or possibly even multiple paragraphs) that contain the selected text, and apply the new paragraph styles to that bigger range.

Unfortunately, lists add many additional points of complexity:

  1. Each entry of a list is its own paragraph. The means that editing a list requires finding and editing all paragraphs in that list

  2. Lists can be nested. If you edit the root level of a list (e.g. changing from a bulleted list to a numbered list), you don't want to apply those edits to any nested entries.

  3. Lists are not just attributes. Almost every other change you can apply to text or a paragraph simply requires editing the attributes. However, lists also require modifying the text. You need to add, modify, or remove the markers at the start of every list entry.

There are other forms of complexity as well. If you hit return, it should create a new list item. Hitting return while on an empty line should end the list. And inserting a new list item part way through a numbered list should shift all the numbers below that point. Thankfully, these sorts of changes are handled by a Cocoa class called NSTextView which handles the actual text editing.

However, in order for NSTextView to know how to handle these things, we need to mark up our NSAttributedString with information on the location and type of lists. This is done with a class called NSTextList.

NSTextList

NSTextList is a fairly simple class that represents a single list. It only has 3 properties:

  • markerFormat - a string representing the marker. For example, if you wanted a numbered list with markers like #1., #2., #3. you could specify this as #{decimal}.
  • startingNumber - this is only relevant to ordered lists, but lets you change the starting point of the list
  • listOptions - Some additional options that aren't really relevant to us.

Each NSTextList represents one "level" of a list hierarchy. These levels are stored in the textLists property on an NSParagraphStyle. If you have a flat list, this will just hold a single NSTextList. In nested lists, it will hold all the parent lists too. For example, if you are 3 levels deep in a list, you would have 3 NSTextLists, one for the root level, one for the second level, and one for the deepest level.

Example showing a list with 3 levels of nesting, with a second column showing the number of list items in the array for that part of the list. At the first level the array only has the item 'list1', at the second level it has list1 and list2, and at the third level it has list1, list2, and list3

By setting these NSTextLists, you can give NSTextView enough information to let users make changes to the list through regular text editing. However, NSTextView will not actually respond to any changes you make in code (for example, adding a list). That requires a LOT more work…

How to Modify a List

So let's say you have selected some text and want to modify the list attribute in some way (be that making the selection a list, removing a list contained in the selection, or modifying an existing list in the selection). You have the selection and you have a new NSTextList with the changes, but how do we go about correctly modifying the text?

1. Calculate the range and level of the list

In order to make any changes we need to know the full range of the list in text. We also need to know what level of list to change if we have a nested list. The behaviour in most apps seems to be that you should edit the highest level in the selection. So if you have 3 levels in a list and your selection covers entries in levels 3 and 2, then any changes should apply only to level 2.

Thankfully, these calculations aren't too difficult. To find the level, you simply enumerate all the paragraph styles in the selection and find the one with the shortest textLists array.

if let currentLevel = self.level {
    self.level = min(currentLevel, max(paragraphStyle.textLists.count - 1, 0))
} else {
    self.level = max(paragraphStyle.textLists.count - 1, 0)
}

(Note: to perform the calculation we created a "List Calculator" class so we can store the values in a property while we perform a calculation. We also zero index the levels, because arrays.)

For the range, you again enumerate all the paragraph styles in the selection. This time though, we can use some helper methods to find the range of the list (if there is one) or paragraph (if there isn't). You then combine these ranges together to get the full extent of the list (or the paragraphs to convert to a list).

var newRange = paragraphStyleEffectiveRange //From enumerating the styles

//If we're in a list then we need to get the full range of the list, which may be outside the selection range
if let list = paragraphStyle.textLists.last {
    let listRange = textStorage.range(of: list, at: effectiveRange.location)
    if (listRange.location != NSNotFound) {
        newRange = listRange
    }
} else {
    //If we're outside of a list then we just want the current paragraph
    newRange = (textStorage.string as NSString).paragraphRange(for: effectiveRange)
}

//Merge the ranges
guard let editRange = self.editingRange else {
    self.editingRange = newRange
    return
}
self.editingRange = editRange.union(newRange)

Once we have our range and level, we can move to the next step

2. Update the Attributes

Next, we need to update the attributes. This is actually the simplest step, though first we need to store a copy of the attributed string before we modify it (it will become apparent why in the next step).

We start to enumerate the paragraph styles in the list's range. All we need to do here is take each paragraph style's textLists array and modify it by either adding, removing, or swapping the NSTextList

var textLists = newParagraphStyle.textLists
//If we're setting a list then we want to replace the list at the desired level
if let listType = listType {
    if (textLists.count > level) {
        textLists[level] = listType
    } else {
        textLists = [listType]
    }
} else {
    //If we have no list then we're removing all lists
    textLists = []
}
newParagraphStyle.textLists = textLists

We then update the attributes on the attributed strings.

3. Update the List Markers

Finally, we need to update the list markers. This happens in two stages.

The first happens while we're enumerating the paragraph styles in step 2. We need to enumerate the lines in the range each paragraph style covers in the old copy of the attributed string we made (as a single paragraph style may be shared across multiple paragraphs). The reason we enumerate the old copy of the attributed string, is we want to calculate the ranges for the old markers so we know what range of text to replace.

(oldString.string as NSString).enumerateSubstrings(in: effectiveRange, options: .byLines) { (substring, substringRange, effectiveRange, _) in
    var existingRange = NSRange(location: substringRange.location, length: 0)
    //If we had an old list then we want to calculate the marker so we can get its range for replacement
    if let oldList = oldParagraphStyle.textLists.last {
        var itemNumber = oldString.itemNumber(in: oldList, at: substringRange.location)
        //We need to manually handle the startingItemNumber as itemNumber(in:at:) doesn't (despite being giving the list)
        if (oldList.startingItemNumber > 1) {
            itemNumber = oldList.startingItemNumber + (itemNumber - 1)
        }
        //We just need the length of the marker as the location is always the start of the line
        //We also add 2 as we always have a tab before and after
        existingRange.length = oldList.marker(forItemNumber: itemNumber).count + 2
    }

    //Add the range and text to replace. We don't actually replace here as we don't want to mess up enumerateAttributes()
    if let list = textLists.last {
        replacements.append((existingRange, "\t\(list.marker(forItemNumber: textStorage.itemNumber(in: list, at: substringRange.location)))\t", newParagraphStyle))
    } else {
        replacements.append((existingRange, "", newParagraphStyle))
    }
}

We store the existing marker range, the new marker, and the paragraph style for that marker in a replacements array for use in part 2.

Part 2 is actually replacing the list markers and happens after we've finished enumerating the paragraph styles. The reason for this is that modifying the list markers can change the length of the text, which can mess up range calculations. By doing all the calculations first, we can then do the replacements in reverse order, ensuring we never have to adjust any ranges.

//Going from back to front (so the ranges remain valid) apply all the list replacements
for (range, string, paragraphStyle) in replacements.reversed() {
    textStorage.replaceCharacters(in: range, with: string)
    //If we're adding a list then we need to make absolutely sure what is added has the paragraph style
    //This is especially true for the earliest range we're adding as it may use the attributes of the text before
    if (range.length == 0) {
        let addedRange = NSRange(location: range.location, length: string.count)
        textStorage.removeAttribute(.paragraphStyle, range: addedRange)
        textStorage.addAttribute(.paragraphStyle, value: paragraphStyle, range: addedRange)
    }
    //We also want to update the selectionLocation so the cursor goes back to the start of the location, which may have shifted due to other list items changing above
    if (range.location < selectedLocation) {
        selectedLocation += (string.count - range.length)
    }
}

And with all that done you should now have correctly updated lists in your code.

That is quite a lot of code, but you can see a more complete version in a demo project here (available under an MIT licence). You may be wondering why we are sharing this if it took us so much work to implement. Isn't this just helping potential competitors? Maybe. But we believe that a rising tide lifts all boats.

Proper support for lists is a great feature to have in Coppice, but it doesn't define Coppice. If someone takes this code and uses it to implement list support in their app, then that's great. The Mac platform gets a little bit better. And who knows, maybe somebody has a better way of doing this that we've missed and they'll let us know about it.

If you enjoyed this post, or just want to stay up-to-date on everything to do with Coppice then don't forget to subscribe to The Coppice Blog or follow @coppiceapp on Twitter.

Coppice 2021.1.1

Posted:

Coppice 2021.1.1 is now available. It fixes some UI issues with the Link/Jump to Page dialogues and the search sidebar.

You can update Coppice by going to Coppice > Check for Updates… in the menu bar in Coppice, or by downloading directly from our website. A full list of changes are available on our release notes page

Coppice's menu bar with the Coppice/Application menu open. The 'Check for Updates…' item is highlighted

Coppice 2021.1

Posted:

We're happy to announce the release of Coppice 2021.1, the first major update to Coppice. This release brings a raft of improvements to text editing, a canvas export feature, and numerous bug fixes. For a full list of changes check out the release notes, but we'll give you a summary below.

You can download Coppice 2021.1 by going to Coppice > Check for Updates… in Coppice's menu bar or by clicking the button below

What's New

Text Editing

The biggest improvements in Coppice 2021.1 are in the editor for text pages. With the initial release of Coppice we focused on implementing the core functionality you expect from a rich text editor, such as the ability to customise the font, the size, and the colour of text.

Coppice 2021.1 brings several new options for customising the layout of your text, all found in the new Paragraph Inspector. Firstly, we've given you more control over the spacing of your text with controls for changing the line height and paragraph spacing.

The big new feature though is native support for lists in text. You can now create bulleted and numbered lists, and even nest them several levels deep.

We've also fixed a bunch of bugs, including making the alignment control far more reliable.


Canvas Export

We strongly believe that your data is your own, and part of this is allowing you to share what you create in Coppice with others and to use it in other apps. We already have full support for exporting pages from a Coppice document, but until now there hasn't been a way to export Canvases.

With Coppice 2021.1 we've start on the path to rectifying that by adding the ability to export a Canvas as an JPEG image. This is great if you have a Canvas you want to share online or with someone who doesn't have a Mac.


Improved Document Format

The last improvement is hopefully one you won't notice: future-proofing the document format. We occasionally need to change how Coppice stores your data in a document, usually to support new features. To ensure that an older version of Coppice doesn't accidentally corrupt a document, it will first check a version number in the document. If a version of Coppice encounters a newer version of document than it can handle, it refuses to open it.

A macOS alert. The title says 'The document  Sample Document.coppicedoc could not be opened. This document was saved by a newer version of Coppice'. The sub title says 'Please download the latest version of Coppice to open this Document

This works incredibly well, but it's a fairly blunt tool. Thankfully, we've been able to make Coppice smarter, reducing the amount of times we'll need to update the document format in future, making the above dialogue a much rarer occurrence.

Unfortunately this change has necessitated updating the document version this time, so documents saved with Coppice 2021.1 won't open in version 2020.1.

What Took So Long?

Those who have followed this blog for a while may remember that we had hoped to get this release out before Christmas last year (indeed, we'd originally called this version 2020.2). So why is it only just being released in mid-February?

The answer is Lists. We had been working on the assumption that Lists would be relatively easy to add support for, as Apple's technologies actually do most of the hard work for us. We thought we'd just need to provide a UI for the existing list support and it would "just work™".

Unfortunately this ended up being way more involved than we had expected, requiring a lot more work on our end. In the coming weeks we'll be writing up a new Tech Talk article going into the details of what we had to do to get lists working, so be sure to subscribe to The Coppice Blog to get notified when that is released.

We're now looking ahead to the next release: 2021.2. We're only just getting started, but we'll be posting about our progress here and on Twitter, so be sure to follow @coppiceapp for updates.

Sneak Peek: Paragraph Styling

Posted:

Welcome to our first Sneak Peek post. Sneak Peek will show features that we're working on for the next version of Coppice. These features will often still be in development so are subject to change before release. In this post we'll look at the new Paragraph Styling options we're adding to Coppice.

Coppice 2020.1 already has support for text styling, allowing you to change the font, size, and colour of some text. However, the options for styling paragraphs is limited to just the alignment. Given the importance of text pages in Coppice, we wanted to focus our first update on giving you more flexibility.

When you edit a text page in the new version of Coppice, you will see a new Paragraph inspector on the right. It allows you to change 4 attributes of a paragraph.

The paragraph inspector with controls in 3 rows. The first is a segmented control for alignment. The second row has text fields and steppers for changing the line height and paragraph spacing. The third row has a pop up button for editing a list
  • The first is the alignment, which has been moved from the Font inspector (previously called the Text inspector). This has gained support for aligning text as justified, and has fixed a few issues with the previous implementation
  • Next are controls for line height and paragraph spacing. These let you adjust how much space is between lines of text or below each paragraph
  • Finally is full support for lists, from a simple bullet or numbered list, to nested lists with support for custom list markers.
A text page in coppice with a sheet appearing over it. The sheet has options for customising a list, including the bullet/number, any prefix or suffix, and the starting number.

We had actually hoped to ship these changes today as version 2020.2, but unfortunately we hit a few snags during development. So rather than rush the feature out we've decided to delay the release until next year (where it will appear as 2021.1). Giving you a quality update is more important than an arbitrary deadline

However, we were able to fix some bugs in Coppice so we've packaged those up as 2020.1.2, which you can download now using Coppice's built in software update, or from the Coppice website.